From 4ce2cdba640077ef6bcf778dc377c059bd187be0 Mon Sep 17 00:00:00 2001 From: Michael Agun Date: Wed, 15 Jul 2026 14:18:00 -0700 Subject: [PATCH] Add bind classify variants that select the WFP filter by callout key usersim's test_callout() selects a single WFP filter by (layer, sublayer) first-match. When multiple callouts are registered at the same layer and sublayer -- for example the legacy bind callout and the CGROUP_SOCK_ADDR bind callout, both at ALE_RESOURCE_ASSIGNMENT -- that first-match can dispatch to the wrong callout, so a test cannot reliably target a specific callout. Add usersim_fwp_bind_ipv4_by_callout() / usersim_fwp_bind_ipv6_by_callout(), which select the filter bound to a caller-supplied callout key via a new get_fwpm_filter_by_callout_under_lock() helper. test_callout() and test_bind_ipv4()/test_bind_ipv6() take an optional callout_key; when null, behavior is unchanged (layer+sublayer first-match), so existing callers are unaffected. This lets a consumer (e.g. the ebpf-for-windows netebpfext bind tests) exercise a specific bind callout explicitly even when multiple bind callouts share a WFP layer. The public _by_callout wrappers assert a non-null callout_key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c2ba0624-5e1d-4385-9972-eec1b8c595f1 --- inc/usersim/fwp_test.h | 10 ++++++++++ src/fwp_um.cpp | 44 ++++++++++++++++++++++++++++++++++++------ src/fwp_um.h | 28 ++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/inc/usersim/fwp_test.h b/inc/usersim/fwp_test.h index c9dfbad..cc60c90 100644 --- a/inc/usersim/fwp_test.h +++ b/inc/usersim/fwp_test.h @@ -37,6 +37,16 @@ usersim_fwp_bind_ipv4(_In_ fwp_classify_parameters_t* parameters); USERSIM_API FWP_ACTION_TYPE usersim_fwp_bind_ipv6(_In_ fwp_classify_parameters_t* parameters); +// Bind-hook classify variants that select the WFP filter bound to a specific callout key. Use these +// to target a specific bind callout (e.g., the CGROUP_SOCK_ADDR bind callout) when multiple callouts +// are registered at the ALE_RESOURCE_ASSIGNMENT layer, instead of relying on layer+sublayer +// first-match ordering. +USERSIM_API FWP_ACTION_TYPE +usersim_fwp_bind_ipv4_by_callout(_In_ fwp_classify_parameters_t* parameters, _In_ const GUID* callout_key); + +USERSIM_API FWP_ACTION_TYPE +usersim_fwp_bind_ipv6_by_callout(_In_ fwp_classify_parameters_t* parameters, _In_ const GUID* callout_key); + USERSIM_API FWP_ACTION_TYPE usersim_fwp_cgroup_inet4_recv_accept(_In_ fwp_classify_parameters_t* parameters); diff --git a/src/fwp_um.cpp b/src/fwp_um.cpp index cdd3142..22a7521 100644 --- a/src/fwp_um.cpp +++ b/src/fwp_um.cpp @@ -130,7 +130,7 @@ fwp_engine_t::classify_test_packet(_In_ const GUID* layer_guid, NET_IFINDEX if_i // This is used to test the bind hook. FWP_ACTION_TYPE -fwp_engine_t::test_bind_ipv4(_In_ fwp_classify_parameters_t* parameters) +fwp_engine_t::test_bind_ipv4(_In_ fwp_classify_parameters_t* parameters, _In_opt_ const GUID* callout_key) { FWPS_INCOMING_VALUE0 incoming_value[FWPS_FIELD_ALE_RESOURCE_ASSIGNMENT_V4_MAX] = {}; incoming_value[FWPS_FIELD_ALE_RESOURCE_ASSIGNMENT_V4_IP_LOCAL_PORT].value.uint16 = parameters->destination_port; @@ -148,12 +148,13 @@ fwp_engine_t::test_bind_ipv4(_In_ fwp_classify_parameters_t* parameters) FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V4, _default_sublayer, incoming_value, - nullptr); + nullptr, + callout_key); } // This is used to test the IPv6 bind hook. FWP_ACTION_TYPE -fwp_engine_t::test_bind_ipv6(_In_ fwp_classify_parameters_t* parameters) +fwp_engine_t::test_bind_ipv6(_In_ fwp_classify_parameters_t* parameters, _In_opt_ const GUID* callout_key) { FWPS_INCOMING_VALUE0 incoming_value[FWPS_FIELD_ALE_RESOURCE_ASSIGNMENT_V6_MAX] = {}; incoming_value[FWPS_FIELD_ALE_RESOURCE_ASSIGNMENT_V6_IP_LOCAL_PORT].value.uint16 = parameters->destination_port; @@ -171,7 +172,8 @@ fwp_engine_t::test_bind_ipv6(_In_ fwp_classify_parameters_t* parameters) FWPM_LAYER_ALE_RESOURCE_ASSIGNMENT_V6, _default_sublayer, incoming_value, - nullptr); + nullptr, + callout_key); } _Requires_lock_not_held_(this->lock) FWP_ACTION_TYPE fwp_engine_t::test_callout( @@ -179,7 +181,8 @@ _Requires_lock_not_held_(this->lock) FWP_ACTION_TYPE fwp_engine_t::test_callout( _In_ const GUID& layer_guid, _In_ const GUID& sublayer_guid, _In_ FWPS_INCOMING_VALUE0* incoming_value, - _Out_opt_ uint64_t* flow_id) + _Out_opt_ uint64_t* flow_id, + _In_opt_ const GUID* callout_key) { FWPS_INCOMING_VALUES incoming_fixed_values = {.layerId = layer_id, .incomingValue = incoming_value}; FWPS_INCOMING_METADATA_VALUES incoming_metadata_values = {}; @@ -188,7 +191,12 @@ _Requires_lock_not_held_(this->lock) FWP_ACTION_TYPE fwp_engine_t::test_callout( { shared_lock_t l(lock); - const FWPM_FILTER* fwpm_filter = get_fwpm_filter_with_context_under_lock(layer_guid, sublayer_guid); + // When a specific callout key is requested, select the filter bound to that callout so the + // intended callout is exercised even if multiple callouts are registered at this layer and + // sublayer. Otherwise fall back to first-match by layer+sublayer. + const FWPM_FILTER* fwpm_filter = + callout_key ? get_fwpm_filter_by_callout_under_lock(layer_guid, sublayer_guid, *callout_key) + : get_fwpm_filter_with_context_under_lock(layer_guid, sublayer_guid); if (!fwpm_filter) { return FWP_ACTION_CALLOUT_UNKNOWN; } @@ -1069,6 +1077,30 @@ usersim_fwp_bind_ipv6(_In_ fwp_classify_parameters_t* parameters) return fwp_engine_t::get()->test_bind_ipv6(parameters); } +FWP_ACTION_TYPE +usersim_fwp_bind_ipv4_by_callout(_In_ fwp_classify_parameters_t* parameters, _In_ const GUID* callout_key) +{ + CXPLAT_DEBUG_ASSERT(callout_key != nullptr); + if (callout_key == nullptr) { + // Guard release builds where the assert is compiled out: without a callout key the engine would + // silently fall back to layer+sublayer first-match and could exercise the wrong callout. + return FWP_ACTION_CALLOUT_UNKNOWN; + } + return fwp_engine_t::get()->test_bind_ipv4(parameters, callout_key); +} + +FWP_ACTION_TYPE +usersim_fwp_bind_ipv6_by_callout(_In_ fwp_classify_parameters_t* parameters, _In_ const GUID* callout_key) +{ + CXPLAT_DEBUG_ASSERT(callout_key != nullptr); + if (callout_key == nullptr) { + // Guard release builds where the assert is compiled out: without a callout key the engine would + // silently fall back to layer+sublayer first-match and could exercise the wrong callout. + return FWP_ACTION_CALLOUT_UNKNOWN; + } + return fwp_engine_t::get()->test_bind_ipv6(parameters, callout_key); +} + FWP_ACTION_TYPE usersim_fwp_cgroup_inet4_recv_accept(_In_ fwp_classify_parameters_t* parameters) { diff --git a/src/fwp_um.h b/src/fwp_um.h index 6a1d30f..be968c1 100644 --- a/src/fwp_um.h +++ b/src/fwp_um.h @@ -218,10 +218,10 @@ typedef class fwp_engine_t classify_test_packet(_In_ const GUID* layer_guid, NET_IFINDEX if_index); FWP_ACTION_TYPE - test_bind_ipv4(_In_ fwp_classify_parameters_t* parameters); + test_bind_ipv4(_In_ fwp_classify_parameters_t* parameters, _In_opt_ const GUID* callout_key = nullptr); FWP_ACTION_TYPE - test_bind_ipv6(_In_ fwp_classify_parameters_t* parameters); + test_bind_ipv6(_In_ fwp_classify_parameters_t* parameters, _In_opt_ const GUID* callout_key = nullptr); FWP_ACTION_TYPE test_cgroup_inet4_recv_accept(_In_ fwp_classify_parameters_t* parameters); @@ -268,7 +268,8 @@ typedef class fwp_engine_t _In_ const GUID& layer_guid, _In_ const GUID& sublayer_guid, _In_ FWPS_INCOMING_VALUE0* incoming_value, - _Out_opt_ uint64_t* flow_handle); + _Out_opt_ uint64_t* flow_handle, + _In_opt_ const GUID* callout_key = nullptr); _Requires_lock_not_held_(this->lock) void test_remove_flow_context( uint64_t flow_id, @@ -298,6 +299,27 @@ typedef class fwp_engine_t return nullptr; } + // Select a filter by its bound callout key at the given layer and sublayer. This disambiguates the + // case where multiple callouts (each with its own filter) are registered at the same WFP layer and + // sublayer (e.g., the legacy bind callout and the CGROUP_SOCK_ADDR bind callout both at + // ALE_RESOURCE_ASSIGNMENT). Selecting the filter by its callout key lets a test target a specific + // callout explicitly instead of relying on layer+sublayer first-match ordering. The sublayer is + // still matched (mirroring get_fwpm_filter_with_context_under_lock) so selection stays unambiguous + // even if a callout ever owns filters on more than one sublayer. + _Ret_maybenull_ const FWPM_FILTER* + get_fwpm_filter_by_callout_under_lock( + _In_ const GUID& layer_guid, _In_ const GUID& sublayer_guid, _In_ const GUID& callout_key) + { + for (auto& [first, filter] : fwpm_filters) { + if (memcmp(&filter.layerKey, &layer_guid, sizeof(GUID)) == 0 && + memcmp(&filter.subLayerKey, &sublayer_guid, sizeof(GUID)) == 0 && + memcmp(&filter.action.calloutKey, &callout_key, sizeof(GUID)) == 0 && filter.rawContext != 0) { + return &filter; + } + } + return nullptr; + } + _Ret_maybenull_ const GUID* get_callout_key_from_layer_guid_under_lock(_In_ const GUID* layer_guid) {