Skip to content

netebpfext: Enable multi-attach for the sock_addr listen hook (#5339)#5384

Draft
mikeagun wants to merge 19 commits into
microsoft:mainfrom
mikeagun:pr2-listen-multi-attach
Draft

netebpfext: Enable multi-attach for the sock_addr listen hook (#5339)#5384
mikeagun wants to merge 19 commits into
microsoft:mainfrom
mikeagun:pr2-listen-multi-attach

Conversation

@mikeagun

Copy link
Copy Markdown
Contributor

Summary

Enable multi-attach support for the sock_addr-aligned listen hook (\cgroup/listen4\ / \cgroup/listen6) and add a multi-attach test suite. Resolves #5339.

Depends on #5381 (bind multi-attach framework + tests).

Changes

Production code (\

etebpfext/net_ebpf_ext_sock_addr.c)

  • Change listen \�ttach_capability\ from \SINGLE_ATTACH_PER_HOOK\ to \MULTI_ATTACH_WITH_WILDCARD\
  • Wire _net_ebpf_extension_sock_addr_authorize_process_verdict\ into \listen_dispatch_table\
  • Refactor \�uthorize_listen_classify\ to use accumulated verdict (mirrors bind_classify):
    • Initialize verdict to PROCEED_SOFT
    • Snapshot context for inter-program restoration
    • Use accumulated verdict for action decision
    • Clear FWPS_RIGHT_ACTION_WRITE on invoke failure (fix divergence from bind)

Listen multi-attach test suite (7 scenarios x TCP x IPv4/IPv6)

  • All soft permits → listen allowed
  • Second program rejects → listen denied
  • First program rejects (short-circuit) → listen denied
  • HARD permit overrides WFP block → listen allowed
  • REJECT beats HARD → listen denied
  • Detach middle program → listen recovers

Documentation

  • \docs/ListenHook.md: Verdict combination semantics, priority ordering, WFP interaction, test coverage table

Preserved behaviors

  • REAUTHORIZE early-return (listen-specific, not present in bind)
  • context_deleting early-return
  • Compartment filter
  • No-program default permit
  • Hard-permit/block rights clearing
  • Classify logging

Validation

  • \socket_tests\ +
    etebpfext_unit\ build clean (Debug/x64)
  • All existing single-attach listen tests unchanged and passing locally
  • VM validation pending (will run after PR1 merges or on combined branch)

Resolves #5339

@mikeagun mikeagun added the enhancement New feature or request label Jun 19, 2026
Michael Agun and others added 9 commits June 19, 2026 13:57
native_module_helper_t owns a generated .sys file that the destructor
deletes when _delete_file_on_destruction is true. The class previously
relied on the implicit copy/move members:

  - The implicit copy ctor copied _delete_file_on_destruction and
    _file_name verbatim, so two instances could end up racing to delete
    the same file.
  - The implicit move ctor moved _file_name (which leaves the moved-from
    string in an implementation-defined valid state) but only copied the
    bool flag, leaving the moved-from instance still claiming ownership.
    Whether the source destructor then attempted to delete the wrong file
    depended on std::string's moved-from representation.

Make the type non-copyable and add explicit noexcept move operations that
transfer ownership of _delete_file_on_destruction and clear it on the
source, so only the destination's destructor deletes the file.

This is required for multi-attach tests that push several
native_module_helper_t instances into std::vector containers, which
may reallocate and move-construct elements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The sock_addr bind hook's process_verdict callback silently ignored
address/port writes by attached programs (the WFP ALE_RESOURCE_ASSIGNMENT
layer does not support context rewrite) but did not restore the bpf_sock_addr_t
context between program invocations. Under multi-attach, an earlier
program could mutate context fields such as user_ip / user_port /
msg_src_* and those mutated values would then be visible to any subsequent
attached program, allowing one program to influence the policy decisions
of another.

Rename _net_ebpf_extension_sock_addr_bind_process_verdict to
_net_ebpf_extension_sock_addr_authorize_process_verdict and have it
restore *sock_addr_ctx = *original_context after each program, reusing
the existing original_context snapshot pattern used by the
CONNECT_AUTHORIZATION accumulator. Update bind_classify to set
original_context from a stack-local snapshot of the WFP-populated
context before invoking programs.

The accumulator is also intended to be used by the sock_addr listen
hook once it becomes multi-attach. Naming it for the abstract role
(authorization gates that do not support context rewrite) rather than
for the bind hook avoids a second rename when listen is wired in.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ions

Microsoft's FWPS_CLASSIFY_OUT0 contract gates writes to actionType on
the FWPS_RIGHT_ACTION_WRITE bit; a callout with higher weight can
terminate WFP arbitration and revoke that right before reaching our
callout. The sock_addr bind, listen, recv_accept, and
authorize_connection classify routines wrote actionType unconditionally
on entry and from the verdict-driven switch in their Exit blocks, so
they could attempt to overwrite a prior terminating decision.

Mirror the early-exit rights check already used by
net_ebpf_extension_sock_addr_redirect_connection_classify:

  - For bind, listen, and recv_accept: bail out before the default
    actionType = FWP_ACTION_PERMIT write.
  - For authorize_connection: set a rights_revoked flag and gate the
    Exit-block switch on it, since that classify defers actionType
    writes to Exit.

This is independent of multi-attach but visible alongside it, since
multi-attach configurations are more likely to share filter layers
with other callouts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Multi-attach tests for the sock_addr bind and listen hooks need to bind
and listen inside specific network compartments to exercise wildcard +
compartment-specific filter selectivity. The existing socket_helper
classes create and bind sockets on the current thread compartment with
no way to override.

Add an optional compartment_id parameter (default
UNSPECIFIED_COMPARTMENT_ID = leave compartment untouched, current
behavior) to every public socket_helper constructor and forward it
through the inheritance chain.

The new compartment_scope RAII helper in socket_helper.cpp brackets
the relevant socket calls with SetCurrentThreadCompartmentId save /
restore, so:

  - _base_socket creates and binds the socket inside the requested
    compartment.
  - _stream_server_socket additionally re-enters the compartment for
    listen() and the accept-socket setup, since the base ctor's scope
    has already destructed by the time the derived ctor body runs.

UNSPECIFIED_COMPARTMENT_ID is a no-op fast path, so existing call sites
that omit the parameter are bit-for-bit identical to before.

iphlpapi.lib is pulled in via #pragma comment(lib, ...) in
socket_helper.cpp to avoid touching the test project files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add per-module map discovery, program_policies for targeted map updates,
attachment_step for dynamic detach/reattach, and compartment_id on
program_spec for compartment-aware attachment. Existing single-program
call sites remain source-identical and behavior-identical.

New framework types:
- program_ref: bounds-checked reference to a specific module/program
- program_policy_spec: per-module verdict/policy configuration
- attachment_step/attachment_action: dynamic attach/detach between steps
- counter_expectation: reserved for future invocation counting

The legacy global-first-match map discovery is preserved as a fallback
for callers that do not use program_policies. When program_policies is
non-empty, per-module maps are used instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 9 multi-program bind test scenarios exercising the verdict
accumulation, short-circuit, hard-permit-over-WFP, and dynamic
detach/reattach behaviors of the sock_addr-aligned bind hook
(cgroup/bind4 / cgroup/bind6). Each scenario runs across TCP/UDP
and IPv4/IPv6 via TEMPLATE_TEST_CASE.

Test scenarios:
- All programs PROCEED_SOFT → bind allowed
- Second program REJECT after soft → bind denied
- First program REJECT (short-circuit) → bind denied
- PROCEED_SOFT + PROCEED_HARD mix → hard permit (higher priority)
- Multi-program soft blocked by WFP filter → bind denied
- Multi-program hard overrides WFP filter → bind allowed
- Detach middle program (REJECT) → bind recovers to allow
- Detach and reattach with changed verdict → bind denied
- Three programs all soft → bind allowed

Helper functions added:
- sock_addr_bind_module(): creates a module_spec for the bind hook
- sock_addr_bind_verdict(): creates a per-module policy spec
- sock_addr_bind_wfp_layer(): returns the WFP layer GUID for bind

All tests tagged [bind_tests][multi_attach] for selective execution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expand the brief mention of multi-attach behavior in BindHook.md with
detailed documentation of:
- Verdict priority ordering (REJECT > PROCEED_HARD > PROCEED_SOFT)
- Short-circuit on REJECT behavior
- WFP interaction with accumulated verdicts (soft vs hard permit)
- Default verdict when no programs are attached
- Unknown/invalid verdict treatment

Add a Multi-Attach Test Coverage table summarizing the 9 test scenarios
now exercised in socket_tests.cpp [bind_tests][multi_attach].

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix FR-1/BT-4: Check bpf_prog_detach2 return code in detach_program
helper — SAFE_REQUIRE(rc == 0) before clearing the attached flag.

Add coverage for adversarial review gaps:
- HARD-first ordering with WFP (BT-2): [HARD, SOFT] + WFP → allow
- REJECT + HARD combinations (BT-9): both [REJECT, HARD] and
  [HARD, REJECT] → bind denied (REJECT priority > HARD)
- Third-program-decisive (BT-7): [SOFT, SOFT, REJECT] → denied and
  [SOFT, SOFT, HARD] + WFP → allowed
- Detach first and detach last (BT-8/F2): asymmetric detach positions

Remove unused TCP_ONLY_PARAMS macro (BT-13).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… cleanup

Move the FWPS_RIGHT_ACTION_WRITE check in authorize_connection_classify
to after the connection context cache lookup. Previously, the rights
check at the top of the function caused an early exit that skipped
_net_ebpf_ext_find_and_remove_connection_context, leaving stale cache
entries from the redirect layer when a higher-weight WFP callout had
revoked write permission.

The cache cleanup must run unconditionally so redirect-layer entries
are always consumed, regardless of whether this callout can write
the classify output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mikeagun
mikeagun force-pushed the pr2-listen-multi-attach branch from 57dd9e3 to 98cf56a Compare June 19, 2026 20:57
…rograms

The socket test framework's detach_program helper uses bpf_prog_detach2
uniformly for all attach types. However, BPF_ATTACH_TYPE_BIND was not
included in _does_attach_type_support_attachable_fd, causing detach to
return -ENOTSUP (-129) for legacy bind programs (bind_policy tests).

Add BPF_ATTACH_TYPE_BIND to the whitelist. The bind provider accepts
attach data but ignores it (always global scope), so this is safe for
both the attach and detach paths.

Also remove the unused counter_expectations scaffolding from the test
framework -- it was never populated by any test and added reader overhead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mikeagun
mikeagun force-pushed the pr2-listen-multi-attach branch from 21bfa5e to f6009f1 Compare June 23, 2026 20:39
Michael Agun and others added 5 commits June 23, 2026 16:55
Remove the compartment_id parameter from all socket helper constructors
and the compartment_scope RAII class. This infrastructure is unused by
any current bind multi-attach test (no test sets program_spec.compartment_id)
and will be reintroduced in a focused follow-up branch.

Changes:
- Remove compartment_id param from _base_socket, _client_socket,
  _datagram_client_socket, _stream_client_socket, _server_socket,
  _datagram_server_socket, _stream_server_socket constructors
- Remove compartment_scope class and iphlpapi dependency
- Remove compartment_id field from program_spec struct
- Simplify attach_program/detach_program lambdas to use hardcoded 0
  (wildcard compartment), which is the value all tests were using

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address PR feedback on the libbpf-compat attach path. The legacy bind hook
(BPF_ATTACH_TYPE_BIND) has no per-target attach parameter and its provider
ignores any supplied client data, so bpf_prog_attach/bpf_prog_detach2 would
accept a non-zero attachable_fd and silently treat it as the wildcard
attachment -- giving callers the false impression that a program was scoped
to a specific target.

Reject a non-zero attachable_fd for such attach types with EINVAL. The
wildcard value is still encoded as a 4-byte zero payload, which
bpf_prog_detach2 relies on to locate the matching link to detach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b71da926-7c8c-4526-87ea-084c89545896
Address PR feedback on the bind multi-attach test suite: replace non-ASCII
arrows and dashes in comments and test descriptions with ASCII equivalents,
and spell out the PROCEED_SOFT / PROCEED_HARD verdict names in the scenario
comments so they match the enum used in the code. Comment/description text
only; no test behavior changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b71da926-7c8c-4526-87ea-084c89545896
Address PR feedback on docs/BindHook.md: simplify the "Hard overrides WFP"
scenario row to the minimal programs that demonstrate it, and add a note that
the "no subsequent WFP filter can override" guarantee for PROCEED_HARD applies
to WFP filters only -- among eBPF programs the most-restrictive verdict wins,
so a later program's REJECT still denies a bind another program permitted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b71da926-7c8c-4526-87ea-084c89545896
@mikeagun
mikeagun force-pushed the pr2-listen-multi-attach branch from f6009f1 to 1f3bf35 Compare July 17, 2026 02:43
Michael Agun and others added 4 commits July 17, 2026 09:18
Change the listen hook from ATTACH_CAPABILITY_SINGLE_ATTACH_PER_HOOK to
ATTACH_CAPABILITY_MULTI_ATTACH_WITH_WILDCARD and wire in the shared
authorize_process_verdict callback for verdict accumulation.

Refactor authorize_listen_classify to use the accumulated verdict from
net_ebpf_sock_addr_ctx.verdict (mirroring bind_classify) instead of the
raw per-call program result. This enables the most-restrictive-wins
accumulation and REJECT short-circuit behavior across multiple attached
listen programs.

Changes:
- listen_dispatch_table: add .process_verdict callback
- Provider registration: SINGLE_ATTACH → MULTI_ATTACH_WITH_WILDCARD
- authorize_listen_classify: initialize verdict to PROCEED_SOFT, snapshot
  context for restoration between programs, use accumulated verdict for
  action decision, clear FWPS_RIGHT_ACTION_WRITE on invoke failure
  (fix accidental divergence from bind)

All existing single-attach listen behaviors preserved:
- REAUTHORIZE early-return (listen-specific, not present in bind)
- context_deleting early-return
- Compartment filter
- No-program default permit
- Hard-permit/block rights clearing
- Classify logging

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 7 multi-program listen test scenarios exercising the same verdict
accumulation behaviors as the bind tests, adapted for TCP-only IPv4/IPv6.

Test scenarios:
- All programs PROCEED_SOFT → listen allowed
- Second program REJECT → listen denied
- First program REJECT (short-circuit) → listen denied
- HARD permit overrides WFP block → listen allowed
- REJECT beats HARD → listen denied
- Detach middle program → listen recovers

Helper functions added:
- sock_addr_listen_module(): creates a module_spec for the listen hook
- sock_addr_listen_verdict(): creates a per-module listen policy spec
- sock_addr_listen_wfp_layer(): returns the ALE_AUTH_LISTEN WFP layer GUID

All tests tagged [sock_addr_tests][multi_attach] for selective execution.
Listen is TCP-only so tests use tcp_v4_params/tcp_v6_params.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add detailed multi-attach documentation to ListenHook.md mirroring
the bind hook documentation:
- Verdict priority ordering (REJECT > PROCEED_HARD > PROCEED_SOFT)
- Short-circuit on REJECT behavior
- WFP interaction with accumulated verdicts
- Default verdict when no programs are attached
- Unknown/invalid verdict treatment
- Multi-attach test coverage table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the is_listen_test detection in execute_connection_test to also
check program_policies[].listen_verdict, not just the legacy top-level
listen_verdict field. Without this, multi-attach listen tests using
program_policies would create a dual-stack (IPv6) server socket,
missing the IPv4 listen WFP layer entirely.

This caused IPv4 listen-deny tests to spuriously pass because the
listen hook was never triggered for the dual-stack socket on the
IPv4 ALE_AUTH_LISTEN layer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mikeagun
mikeagun force-pushed the pr2-listen-multi-attach branch from 1f3bf35 to fb8710a Compare July 17, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Complete multi-attach support for listen

2 participants