netebpfext: purge stale WFP state at init and fail loudly when it cannot - #5501
Draft
D. Michael Agun (mikeagun) wants to merge 3 commits into
Draft
netebpfext: purge stale WFP state at init and fail loudly when it cannot#5501D. Michael Agun (mikeagun) wants to merge 3 commits into
D. Michael Agun (mikeagun) wants to merge 3 commits into
Conversation
D. Michael Agun (mikeagun)
force-pushed
the
fix/netebpfext-init-purge
branch
from
August 14, 2026 18:27
d787bbb to
4f48b9b
Compare
PR microsoft#5472 guarantees that driver unload always completes, even when a WFP filter delete permanently fails. That exposes the next problem: the filter that could not be deleted is still installed in WFP. WFP management objects are not owned by the driver that created them, and this extension's engine sessions are non-dynamic, so closing the engine handle does not remove them. A surviving filter therefore pins the callout, sub-layer and provider objects it references, their deletion at unload fails too, and the next FwpmProviderAdd fails with FWP_E_ALREADY_EXISTS (0xC0220009). Observed on a test machine, with the service still reporting STATE: 4 RUNNING afterwards. A stale filter is also actively dangerous. Its rawContext still holds the address of a filter context that the previous driver instance allocated and freed. Today that is inert only because initialization fails at FwpmProviderAdd and so never reaches FwpsCalloutRegister. The moment a callout is registered under a GUID a stale filter references, WFP begins invoking classifyFn with a dangling pointer. Three changes, which have to travel together: * Add _net_ebpf_ext_purge_stale_wfp_objects(), called after FwpmEngineOpen and before FwpmTransactionBegin, in its own transaction so that aborting the initialization transaction cannot roll the purge back. It deletes objects tagged with EBPF_WFP_PROVIDER in reverse dependency order: filters, callout objects, sub-layers, then the provider. Running it before the first FwpsCalloutRegister is what makes it safe: with no callout function registered, WFP delivers no delete notification for the filters removed here, so the purge never touches a stale rawContext. Every *_NOT_FOUND result is success, so a clean boot is a handful of no-op calls. If any eBPF filter survives the purge, initialization fails. A partial purge followed by a successful start is strictly worse than not starting, because it is exactly the case that registers callouts alongside dangling contexts. * Check the result of net_ebpf_extension_initialize_wfp_components() in _net_ebpf_ext_driver_initialize_objects() instead of discarding it with (void). DriverEntry already calls _net_ebpf_ext_driver_uninitialize_objects() and returns the failing status, so the service start now fails with a real error code rather than reporting RUNNING with no WFP components registered. This is a deliberate behaviour change: netebpfext now refuses to load when WFP initialization fails, instead of loading in a silently inert state. Closes the TODO for issue microsoft#521. * Clear rundown_acquired once create_filter_context() succeeds, in _net_ebpf_extension_hook_provider_attach_client(). The provider rundown reference is acquired by the attach frame, but ownership transfers to the filter context the instant it exists; from then on the context releases it, either when its refcount reaches zero or via the unload sweep for a zombie. Without this, a failure between context creation and the end of the function would release the same reference twice. That path is currently unreachable, but it becomes a live rundown underflow as soon as one is added, and the purge work above adds failure paths to this file's neighbourhood.
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.
Adds the positive case, where an unload that leaves filters installed is followed by a start that purges them and succeeds, and the negative case that guards the safety invariant: a stale filter that cannot be deleted must make initialization fail rather than register callouts it still references. The helper gains a wfp_is_initialized() accessor because its constructor cannot use REQUIRE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5196c1b8-883f-440e-8846-e5fd79db2f5d
D. Michael Agun (mikeagun)
force-pushed
the
fix/netebpfext-init-purge
branch
from
August 14, 2026 19:58
4f48b9b to
9205b09
Compare
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.
Follow-up to #5472, stacked on
fix/netebpfext-wfp-delete-failed— that should merge first. Relates to #5486.#5472 makes driver unload always complete when a WFP filter delete permanently fails. This handles the consequence: the filter that could not be deleted is still installed in WFP.
WFP management objects are not owned by the driver that created them, and this extension's engine sessions are non-dynamic, so closing the engine handle does not remove them. A surviving filter pins the callout, sub-layer and provider it references, their deletion at unload fails too, and the next
FwpmProviderAddfails withSTATUS_FWP_ALREADY_EXISTS(0xC0220009). Confirmed on a test machine, with the service still reportingSTATE: 4 RUNNINGand zero WFP components registered.The safety invariant
A stale filter's
rawContextholds the address of a filter context that the previous driver instance freed. It is inert only because initialization fails atFwpmProviderAddand never reachesFwpsCalloutRegister. Register a callout under a GUID a stale filter references, and WFP starts invokingclassifyFnwith a dangling pointer.So: purge every stale filter, or refuse to start. A partial purge followed by a successful start is strictly worse than the current silent failure, which is why both changes below ship together.
Purge
_net_ebpf_ext_purge_stale_wfp_objects()runs afterFwpmEngineOpenand beforeFwpmTransactionBegin, in its own transaction so that aborting the initialization transaction cannot roll it back. It deletesEBPF_WFP_PROVIDER-tagged objects in reverse dependency order: filters, callout objects, sub-layers, then the provider.Running before any
FwpsCalloutRegisteris what makes it safe: with no callout function registered, WFP delivers no delete notification for the filters removed here, so the purge never touches a stalerawContext. Every*_NOT_FOUNDresult is success, so a clean boot is a handful of no-op calls.If the sweep cannot be proven complete — any filter left undeleted, or an enumeration that did not drain — initialization fails.
Fail loudly
The result of
net_ebpf_extension_initialize_wfp_components()is checked instead of discarded with(void).DriverEntryalready calls_net_ebpf_ext_driver_uninitialize_objects()and returns the failing status, so the service start fails with a real error code.Behaviour change: netebpfext now refuses to load when WFP initialization fails, instead of loading in a silently inert state. Its program-info providers are registered after this point, so on that path the network program types no longer appear in
netsh ebpf show programs. That is the intended signal, but it is worth a careful look.On #521: the TODO is kept and expanded rather than removed. That issue asks the driver to register for BFE service status before adding WFP objects, which is still unimplemented, and failing the start makes the gap more visible: a start attempted while BFE is unavailable now fails outright instead of quietly doing nothing. Reviewers may want to weigh whether #521 should land first.
Tracing teardown also moves out of
_net_ebpf_ext_driver_uninitialize_objects()to its two call sites, so it happens after the final log statement rather than before it. The new failure path is the first to routinely reach that code, and it was discarding the exit trace for a failed start.Testing
No new tests in this PR. The existing
netebpfext_unitWFP lifecycle tests ([wfp_cleanup]) provide coverage: they strand undeletable filters across unload, so every subsequent WFP initialization exercises the purge, and the suite fails without it.Dedicated purge tests — positive, negative (the safety invariant), and enumeration-not-drained — and real-hardware A/B validation are still to come. Draft until both land.
The highest-risk unverified assumption is that the WFP mock does not model
enumType, so the driver'sFWP_FILTER_ENUM_FULLY_CONTAINEDplusGUID_NULLlayerKeytemplate is untested against real WFP. If that combination returns nothing on real hardware, the purge would be inert while CI stays green. That is the first thing hardware validation must check.Dependency
Requires the usersim submodule bump. Depends on microsoft/usersim#325.
The submodule URL is temporarily pointed at a fork so that CI can build this pull request before that change merges. Both the URL and the commit must be moved back to
microsoft/usersim, at the merged commit, before this merges.