Skip to content

Add WFP enumeration APIs and model object identity in the WFP mock - #325

Draft
D. Michael Agun (mikeagun) wants to merge 3 commits into
microsoft:mainfrom
mikeagun:fix/fwp-mock-enumeration
Draft

Add WFP enumeration APIs and model object identity in the WFP mock#325
D. Michael Agun (mikeagun) wants to merge 3 commits into
microsoft:mainfrom
mikeagun:fix/fwp-mock-enumeration

Conversation

@mikeagun

Copy link
Copy Markdown
Contributor

The user-mode WFP mock can add and delete filters, callouts, sub-layers and providers, but it cannot enumerate them, and it models neither object identity nor references. Two consequences: code that discovers WFP objects rather than remembering them cannot be tested at all, and a real class of driver bug is invisible to tests.

Enumeration

Adds FwpmFilterCreateEnumHandle0 / FwpmFilterEnum0 / FwpmFilterDestroyEnumHandle0, the three FwpmCallout* equivalents, and FwpmFreeMemory0.

Enumerations are snapshots taken when the enum handle is created, matching the documented behaviour that a WFP enumerator "is not live". That is what makes the common enumerate-everything-then-delete-each-entry pattern terminate. Templates honour providerKey and layerKey filtering. Results are returned as one allocation laid out [pointers][objects][GUIDs], so a single FwpmFreeMemory0 frees the batch as real WFP requires, and the enumeration cursor only advances over entries the caller actually received.

Object identity

add_fwpm_provider / remove_fwpm_provider were no-ops, so FwpmProviderAdd always succeeded and could never report FWP_E_ALREADY_EXISTS. Providers are now stored, and a duplicate add is rejected.

Referential integrity

Nothing tracked references, so a callout, sub-layer or provider could be deleted while a filter still pointed at it. Deleting a referenced object now returns FWP_E_IN_USE. The reference check and the erase happen under a single lock acquisition. The callout check only compares action.calloutKey for callout action types, since that field shares a union with filterType.

Together these reproduce a real driver failure mode: a filter whose delete fails keeps its callout, sub-layer and provider alive, those objects outlive the driver, and the next start fails adding a provider that already exists.

Object fidelity

providerKey in FWPM_FILTER0, FWPM_CALLOUT0 and FWPM_SUBLAYER0 is a pointer into caller memory, but the structures were stored by value, so every stored object aliased whatever the caller happened to pass. Reading it later, which enumeration and the reference checks must do, could dereference memory the caller had already released. It is now deep-copied into the stored entry, with the stored pointer re-bound to that copy.

Deletes of a missing object returned STATUS_INVALID_PARAMETER or STATUS_NOT_FOUND (0xC0000225), where real WFP returns FWP_E_FILTER_NOT_FOUND (0x80320003), FWP_E_CALLOUT_NOT_FOUND (0x80320001) and FWP_E_SUBLAYER_NOT_FOUND (0x80320007). Callers that distinguish "already gone", which is benign, from a genuine failure could not do so, so their not-found handling was silently dead under the mock.

Testing

No new usersim tests. Coverage comes from the netebpfext_unit suite in microsoft/ebpf-for-windows, which drives these APIs through netebpfext's stale-WFP-object purge and exercises the enumeration, provider-identity and FWP_E_IN_USE paths on every WFP init and unload.

Michael Agun added 2 commits August 12, 2026 12:18
The user-mode WFP mock could add and delete filters, callouts, sub-layers and
providers, but could not enumerate any of them, so code that discovers WFP
objects rather than remembering them could not be exercised at all.

Add the enumeration APIs for filters and callouts:

  FwpmFilterCreateEnumHandle0  / FwpmFilterEnum0  / FwpmFilterDestroyEnumHandle0
  FwpmCalloutCreateEnumHandle0 / FwpmCalloutEnum0 / FwpmCalloutDestroyEnumHandle0

plus FwpmFreeMemory0, which callers need to release an enumeration result.

Enumerations are snapshots taken when the enum handle is created, matching the
documented behaviour that a WFP enumerator "is not live" and does not reflect
changes made after the handle exists. That is what makes the common "enumerate
everything, then delete each entry" pattern terminate. The enumeration
templates honour providerKey and layerKey filtering. Results are returned as a
single allocation laid out as [pointers][objects][GUIDs] so that one
FwpmFreeMemory0 call frees the whole batch, as real WFP requires, and the
enumeration cursor only advances over entries the caller actually received.

Two fidelity problems had to be fixed for enumeration to return usable objects:

* FWPM_FILTER0::providerKey and FWPM_CALLOUT0::providerKey are pointers into
  caller memory, and the mock stored the structures by value. Every stored
  object therefore aliased whatever the caller happened to pass, and reading
  providerKey later -- which enumeration by provider must do -- could
  dereference memory the caller had already released. Both are now deep-copied
  into the stored entry, with the stored pointer re-bound to that copy.

* Deletes of a missing object returned STATUS_INVALID_PARAMETER or
  STATUS_NOT_FOUND (0xC0000225), where real WFP returns FWP_E_FILTER_NOT_FOUND
  (0x80320003), FWP_E_CALLOUT_NOT_FOUND (0x80320001) and
  FWP_E_SUBLAYER_NOT_FOUND (0x80320007). Callers that distinguish "already
  gone", which is benign, from a genuine failure could not do so, so their
  not-found handling was silently dead under the mock.
The mock accepted any FwpmProviderAdd and deleted any object on request, so
two behaviours that real WFP relies on could not be observed by a test:

* A provider was not modelled at all -- add_fwpm_provider and
  remove_fwpm_provider were no-ops, so FwpmProviderAdd always succeeded and
  could never report FWP_E_ALREADY_EXISTS.
* Nothing tracked references, so a callout, sub-layer or provider could be
  deleted while a filter still pointed at it, where real WFP returns
  FWP_E_IN_USE.

Together these are the mechanism behind a real class of driver bug: a filter
whose delete fails keeps its callout, sub-layer and provider alive, those
objects outlive the driver, and the next start fails when it tries to add a
provider that is already there. A mock that cannot represent that cannot
regression-test it.

Store providers, and reject a duplicate add. Add reference checks so deleting
a referenced callout, sub-layer or provider returns FWP_E_IN_USE, and fold the
not-found and in-use cases into single delete_fwpm_* methods so the check and
the erase happen under one lock acquisition.

FWPM_SUBLAYER0::providerKey is a caller-owned pointer like the filter and
callout cases, and the new provider reference check dereferences it, so it is
now deep-copied into the stored entry as those already are.

Also drops a stray second fault injection in FwpmProviderDeleteByKey0 that
returned STATUS_NOT_FOUND after the removal had already happened.
FWPM_FILTER0::filterId was never populated when a filter was added, so every
filter returned by FwpmFilterEnum carried an id of zero. A caller that did not
add the filter itself -- the only kind of caller enumeration exists for -- had
no usable handle: FwpmFilterDeleteById reported FWP_E_FILTER_NOT_FOUND, and a
caller that treats "already gone" as success silently deleted nothing. Real WFP
assigns the id on add and reports it through enumeration.

FwpsCalloutUnregisterById0 returned STATUS_INVALID_PARAMETER for an identifier
that matches no registered callout, where real WFP returns
STATUS_FWP_CALLOUT_NOT_FOUND. Cleanup paths unregister every callout they may
have registered and treat "not found" as success, so the generic status turned
a benign no-op into an apparent failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5196c1b8-883f-440e-8846-e5fd79db2f5d
D. Michael Agun (mikeagun) pushed a commit to mikeagun/ebpf-for-windows that referenced this pull request Aug 14, 2026
The netebpfext stale-WFP-object purge discovers objects by enumerating them,
which the user-mode WFP mock could not do, and it relies on the mock modelling
provider identity and object references to be testable at all.

TEMPORARY: this also points the external/usersim submodule URL at a fork so
that CI can build this pull request before the usersim change merges. Both the
URL and the commit must be moved back to microsoft/usersim, at the merged
commit, before this merges.

Depends on microsoft/usersim#325.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant