Add WFP enumeration APIs and model object identity in the WFP mock - #325
Draft
D. Michael Agun (mikeagun) wants to merge 3 commits into
Draft
Add WFP enumeration APIs and model object identity in the WFP mock#325D. Michael Agun (mikeagun) wants to merge 3 commits into
D. Michael Agun (mikeagun) wants to merge 3 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 threeFwpmCallout*equivalents, andFwpmFreeMemory0.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
providerKeyandlayerKeyfiltering. Results are returned as one allocation laid out[pointers][objects][GUIDs], so a singleFwpmFreeMemory0frees 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_providerwere no-ops, soFwpmProviderAddalways succeeded and could never reportFWP_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 comparesaction.calloutKeyfor callout action types, since that field shares a union withfilterType.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
providerKeyinFWPM_FILTER0,FWPM_CALLOUT0andFWPM_SUBLAYER0is 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_PARAMETERorSTATUS_NOT_FOUND(0xC0000225), where real WFP returnsFWP_E_FILTER_NOT_FOUND(0x80320003),FWP_E_CALLOUT_NOT_FOUND(0x80320001) andFWP_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_unitsuite inmicrosoft/ebpf-for-windows, which drives these APIs through netebpfext's stale-WFP-object purge and exercises the enumeration, provider-identity andFWP_E_IN_USEpaths on every WFP init and unload.