Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions inc/usersim/fwp_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ USERSIM_API void
usersim_fwp_set_sublayer_guids(
_In_ const GUID& default_sublayer, _In_ const GUID& connect_v4_sublayer, _In_ const GUID& connect_v6_sublayer);

// Test-only: fail the next 'count' WFP FwpmFilterDeleteById calls without removing the filter or issuing a delete
// notification, reproducing the DELETE_FAILED reference-leak scenario. Pass 0 to clear the injection.
USERSIM_API void
usersim_fwp_set_filter_delete_failure_count(uint32_t count);

// Test-only: number of WFP filters currently present in the simulated engine.
USERSIM_API uint32_t
usersim_fwp_get_fwpm_filter_count();

// Test-only: remove any WFP filters left in the simulated engine (cleanup after fault-injection tests).
USERSIM_API void
usersim_fwp_clear_fwpm_filters();

USERSIM_API void
usersim_fwp_sock_ops_v4_remove_flow_context(_In_ uint64_t flow_id);

Expand Down
24 changes: 24 additions & 0 deletions src/fwp_um.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS FwpmFilterDeleteById0(_In_ HANDLE en
// Skip fault injection for this API because return failure status requires to remove filter from the list.
auto& engine = *reinterpret_cast<fwp_engine_t*>(engine_handle);

// Test-only fault injection: fail the delete without removing the filter or issuing a delete notification,
// reproducing the field DELETE_FAILED reference-leak scenario.
if (engine.consume_filter_delete_failure()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this not check if fault injection is enabled?

@mikeagun mikeagun Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is separate from the cxplat_fault_injection_inject_fault() harness intentionally.

It's a deterministic, opt‑in knob so a test can fail an exact number of specific deletes, which the random harness can't express, and it models the real semantics (fail without removing the filter or firing a notification).

@saxena-anurag saxena-anurag Aug 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, but can you add some more comments around this new option that is added. Mainly explaining how this is different than the other fault injection and explaining how to use it.

Asking as context gets lost over time.

return (NTSTATUS)STATUS_UNSUCCESSFUL;
}

if (engine.remove_fwpm_filter(id)) {
return STATUS_SUCCESS;
} else {
Expand Down Expand Up @@ -1124,6 +1130,24 @@ usersim_fwp_set_sublayer_guids(
fwp_engine_t::get()->set_sublayer_guids(default_sublayer, connect_v4_sublayer, connect_v6_sublayer);
}

void
usersim_fwp_set_filter_delete_failure_count(uint32_t count)
{
fwp_engine_t::get()->set_filter_delete_failure_count(count);
}

uint32_t
usersim_fwp_get_fwpm_filter_count()
{
return (uint32_t)fwp_engine_t::get()->get_fwpm_filter_count();
}

void
usersim_fwp_clear_fwpm_filters()
{
fwp_engine_t::get()->clear_fwpm_filters();
}

void
usersim_fwp_sock_ops_v4_remove_flow_context(
_In_ uint64_t flow_id)
Expand Down
53 changes: 48 additions & 5 deletions src/fwp_um.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ typedef class fwp_engine_t

callout = get_fwps_callout(&filter->action.calloutKey);
CXPLAT_DEBUG_ASSERT(callout != nullptr);
fwps_filter.filterId = id;
fwps_filter.context = filter->rawContext;
}

Expand All @@ -158,8 +159,10 @@ typedef class fwp_engine_t
exclusive_lock_t l(lock);
for (auto& it : fwpm_filters) {
if (it.first == id) {
// May be null if the callout function has already been unregistered (e.g., during driver
// unload); in that case WFP delivers no delete notification (handled below).
callout = get_fwps_callout(&it.second.action.calloutKey);
CXPLAT_DEBUG_ASSERT(callout != nullptr);
fwps_filter.filterId = id;
fwps_filter.context = it.second.rawContext;
break;
}
Expand All @@ -168,14 +171,53 @@ typedef class fwp_engine_t
return_value = fwpm_filters.erase(id) == 1;
}

CXPLAT_DEBUG_ASSERT(callout != nullptr);
__analysis_assume(callout != nullptr);
// Invoke filter delete notification callback.
callout->notifyFn(FWPS_CALLOUT_NOTIFY_DELETE_FILTER, &callout->calloutKey, &fwps_filter);
// If the callout function is still registered, deliver the delete notification as real WFP does. Once the
// callout has been unregistered (e.g., during driver unload), WFP delivers no delete notification.
if (callout != nullptr) {
callout->notifyFn(FWPS_CALLOUT_NOTIFY_DELETE_FILTER, &callout->calloutKey, &fwps_filter);
}

return return_value;
}

// Test-only: remove any WFP filters left in the engine (used to clean up after fault-injection tests that
// intentionally leave filters undeletable). Does not issue notifications.
void
clear_fwpm_filters()
{
exclusive_lock_t l(lock);
fwpm_filters.clear();
}

// Test-only fault injection: fail the next 'count' FwpmFilterDeleteById calls without removing the filter or
// issuing a delete notification, reproducing the WFP DELETE_FAILED reference-leak scenario.
void
set_filter_delete_failure_count(uint32_t count)
{
exclusive_lock_t l(lock);
_filter_delete_failure_count = count;
}

// Returns true (and consumes one) if the next FwpmFilterDeleteById call should be failed for fault injection.
bool
consume_filter_delete_failure()
{
exclusive_lock_t l(lock);
if (_filter_delete_failure_count > 0) {
_filter_delete_failure_count--;
return true;
}
return false;
}

// Test-only: number of WFP filters currently present in the engine.
size_t
get_fwpm_filter_count()
{
shared_lock_t l(lock);
return fwpm_filters.size();
}

_Requires_lock_not_held_(this->lock) void add_fwpm_provider(_In_ const FWPM_PROVIDER* provider)
{
UNREFERENCED_PARAMETER(provider);
Expand Down Expand Up @@ -336,6 +378,7 @@ typedef class fwp_engine_t
std::shared_mutex lock;
uint32_t next_id = 1;
uint32_t next_flow_id = 1;
uint32_t _filter_delete_failure_count = 0; // Test-only WFP filter delete fault-injection counter.
std::unordered_map<size_t, FWPS_CALLOUT3> fwps_callouts;
std::unordered_map<size_t, FWPM_CALLOUT0> fwpm_callouts;
std::unordered_map<size_t, FWPM_FILTER0> fwpm_filters;
Expand Down