Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d2c6079
tests/util: Make native_module_helper_t safely movable
Jun 15, 2026
cd1b65c
netebpfext: Restore sock_addr context between multi-attach programs
Jun 15, 2026
3793f4a
netebpfext: Check FWPS_RIGHT_ACTION_WRITE in sock_addr classify funct…
Jun 15, 2026
627a706
tests/util: Add optional compartment_id to socket_helper constructors
Jun 15, 2026
dd25e29
tests/socket: Extend execute_connection_test for multi-program scenarios
Jun 19, 2026
023c28e
tests/socket: Add bind multi-program test suite and scenario helpers
Jun 19, 2026
fc8c798
docs: Document bind hook multi-attach verdict combination semantics
Jun 19, 2026
feb487c
tests/socket: Address adversarial review findings in multi-attach tests
Jun 19, 2026
8f1802a
netebpfext: Fix auth-connect rights check skipping connection context…
Jun 19, 2026
85a1999
tests/socket: Fix bpf_prog_detach2 failure for BPF_ATTACH_TYPE_BIND p…
Jun 23, 2026
3e0f198
tests/socket: Remove compartment_id infrastructure from socket helpers
Jun 23, 2026
d09ba99
Merge branch 'main' into pr1-bind-multi-attach
Alan-Jowett Jul 7, 2026
c7930a5
tests/socket: detach legacy bind natively; drop libbpf-compat support…
Aug 12, 2026
777221f
tests/socket: use ASCII characters in multi-attach test comments
Aug 12, 2026
04c0c45
docs: clarify bind hook multi-attach verdict and WFP semantics
Aug 12, 2026
0f2aa0d
docs: state bind verdict as permit/deny and link WFP arbitration docs
Aug 12, 2026
cc9eaac
netebpfext: invoke sock_addr programs for observation when write-righ…
Aug 12, 2026
baa4375
netebpfext: factor out sock_addr verdict-to-WFP mapping
Aug 13, 2026
9f6540f
docs: list all 16 bind multi-attach scenarios in the coverage table
Aug 13, 2026
eb9897e
netebpfext: skip sock_addr programs when the write right is revoked
Aug 14, 2026
4a09f26
tests/socket: verify bind wildcard attachment precedence
Aug 27, 2026
377a3d1
Update wildcard and specific bind expectations
Copilot Aug 28, 2026
f19d5ea
Validate wildcard attach test updates
Copilot Aug 28, 2026
22f3919
Revert "Validate wildcard attach test updates"
Copilot Aug 28, 2026
25c79fc
Split required wildcard attach scenarios
Copilot Aug 28, 2026
55103dc
Clarify wildcard reject test intent
Copilot Aug 28, 2026
78b77e6
Clarify expected wildcard failure
Copilot Aug 28, 2026
d8d81c1
Clarify required reject scenario
Copilot Aug 28, 2026
112a52d
Describe required scope interaction
Copilot Aug 28, 2026
c8c9236
Keep bind scenarios test-local
Copilot Aug 28, 2026
019feaf
Update docs and tests to match intended behavior
mikeagun Aug 28, 2026
b402380
Merge branch 'main' into pr1-bind-multi-attach
saxena-anurag Aug 29, 2026
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
37 changes: 35 additions & 2 deletions docs/BindHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,41 @@ typedef enum _ebpf_sock_addr_verdict
} ebpf_sock_addr_verdict_t;
```

When multiple bind programs are attached, the verdicts are combined: if any program
rejects, the bind is blocked.
When multiple bind programs are attached, the verdicts are combined using a
most-restrictive accumulation rule:

- **Priority**: `REJECT` (2) > `PROCEED_HARD` (1) > `PROCEED_SOFT` (0)
- The accumulated verdict is the highest-priority value seen across all
attached programs.
- **Short-circuit on REJECT**: If any program returns `REJECT`, the provider
Comment thread
keith-horton marked this conversation as resolved.
loop stops immediately — subsequent programs are not invoked.
- If no programs are attached (or all are detached), the default verdict is
`PROCEED_SOFT` (permit).
- An unknown/invalid return value from a program is treated as `REJECT`.

The accumulated verdict then interacts with WFP:

- `PROCEED_SOFT`: bind is allowed unless a WFP filter with higher weight blocks it.
- `PROCEED_HARD`: bind is allowed unconditionally — clears `FWPS_RIGHT_ACTION_WRITE`
so no subsequent WFP filter can override.
Comment thread
mikeagun marked this conversation as resolved.
Outdated
Comment thread
mikeagun marked this conversation as resolved.
Outdated
- `REJECT`: bind is denied (returns `WSAEACCES` / `EACCES`).

### Multi-Attach Test Coverage

The following scenarios are exercised in `tests/socket/socket_tests.cpp`
(tagged `[bind_tests][multi_attach]`), across TCP/UDP and IPv4/IPv6:

| Scenario | Programs | Expected Result |
|---|---|---|
| All soft permits | 2× `PROCEED_SOFT` | Bind allowed |
| Second program rejects | `PROCEED_SOFT` + `REJECT` | Bind denied |
| First program rejects (short-circuit) | `REJECT` + `PROCEED_SOFT` | Bind denied |
| Soft + hard mix | `PROCEED_SOFT` + `PROCEED_HARD` | Bind allowed (hard priority) |
| Soft permits blocked by WFP | 2× `PROCEED_SOFT` + WFP block | Bind denied |
| Hard overrides WFP | `PROCEED_SOFT` + `PROCEED_HARD` + WFP block | Bind allowed |
Comment thread
mikeagun marked this conversation as resolved.
Outdated
| Detach middle program | 3 programs → detach REJECT middle | Bind recovers |
| Detach and reattach | Detach + reattach with new verdict | Verdict updates |
| Three soft permits | 3× `PROCEED_SOFT` | Bind allowed |

## Architecture

Expand Down
1 change: 1 addition & 0 deletions libs/api/libbpf_program.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ _does_attach_type_support_attachable_fd(enum bpf_attach_type type)
case BPF_CGROUP_INET4_LISTEN:
case BPF_CGROUP_INET6_LISTEN:
case BPF_CGROUP_SOCK_OPS:
case BPF_ATTACH_TYPE_BIND:
Comment thread
mikeagun marked this conversation as resolved.
Outdated
supported = TRUE;
break;
default:
Expand Down
113 changes: 89 additions & 24 deletions netebpfext/net_ebpf_ext_sock_addr.c
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ static bool
_net_ebpf_extension_sock_addr_process_verdict(_Inout_ void* program_context, int program_verdict);

static bool
_net_ebpf_extension_sock_addr_bind_process_verdict(_Inout_ void* program_context, int program_verdict);
_net_ebpf_extension_sock_addr_authorize_process_verdict(_Inout_ void* program_context, int program_verdict);

//
// sock_addr helper functions.
Expand Down Expand Up @@ -1402,7 +1402,7 @@ net_ebpf_ext_sock_addr_register_providers()
.create_filter_context = _net_ebpf_extension_sock_addr_create_filter_context,
.delete_filter_context = _net_ebpf_extension_sock_addr_delete_filter_context,
.validate_client_data = _net_ebpf_extension_sock_addr_validate_client_data,
.process_verdict = _net_ebpf_extension_sock_addr_bind_process_verdict,
.process_verdict = _net_ebpf_extension_sock_addr_authorize_process_verdict,
};

const net_ebpf_extension_hook_provider_dispatch_table_t listen_dispatch_table = {
Expand Down Expand Up @@ -2058,23 +2058,42 @@ _net_ebpf_extension_sock_addr_process_verdict(_Inout_ void* program_context, int
return TRUE;
}

// Multi-attach verdict accumulator for the sock_addr bind hook. Tracks the
// most-restrictive verdict across attached programs in net_ebpf_sock_addr_t::verdict
// using _get_verdict_priority(), and returns FALSE on REJECT so the hook provider
// loop stops invoking subsequent programs. Address/port writes to the context are
// ignored at bind (the WFP ALE_RESOURCE_ASSIGNMENT layer does not support address
// rewrite), so no redirect handling is performed here.
// Multi-attach verdict accumulator for sock_addr authorization gates that do
// not support context rewrite, such as the sock_addr bind hook
// (ALE_RESOURCE_ASSIGNMENT) and the sock_addr listen hook (ALE_AUTH_LISTEN).
Comment thread
mikeagun marked this conversation as resolved.
Outdated
Comment thread
lakshk98 marked this conversation as resolved.
Outdated
// Tracks the most-restrictive normalized verdict across attached programs in
// net_ebpf_sock_addr_t::verdict using _get_verdict_priority(), and returns
// FALSE on REJECT so the hook provider loop stops invoking subsequent
// programs.
//
// Any address/port writes a program makes to the context are silently ignored
// for WFP purposes (the underlying ALE layer does not support address
// rewrite). To prevent one program's writes from being observed by subsequent
// programs in the same multi-attach invocation, the context is restored from
// net_ebpf_sock_addr_t::original_context after each program. The caller must
// set original_context to point at a pristine snapshot of bpf_sock_addr_t
// before invoking any program.
static bool
_net_ebpf_extension_sock_addr_bind_process_verdict(_Inout_ void* program_context, int program_verdict)
_net_ebpf_extension_sock_addr_authorize_process_verdict(_Inout_ void* program_context, int program_verdict)
Comment thread
mikeagun marked this conversation as resolved.
Outdated
{
bpf_sock_addr_t* sock_addr_ctx = (bpf_sock_addr_t*)program_context;
net_ebpf_sock_addr_t* context = CONTAINING_RECORD(sock_addr_ctx, net_ebpf_sock_addr_t, base);
bpf_sock_addr_t* original_context = context->original_context;
int normalized_verdict = _normalize_sock_addr_verdict(program_verdict);

// original_context must be set by the caller before invoking programs.
// It points to a caller's stack variable and is only valid during synchronous program invocation.
ASSERT(original_context != NULL);

if (_get_verdict_priority(normalized_verdict) > _get_verdict_priority(context->verdict)) {
context->verdict = normalized_verdict;
}

// Restore the context so the next attached program sees the original
// WFP-provided values, not whatever the previous program may have written
// to user_ip / user_port / msg_src_*.
*sock_addr_ctx = *original_context;
Comment thread
mikeagun marked this conversation as resolved.

return normalized_verdict != BPF_SOCK_ADDR_VERDICT_REJECT;
}

Expand Down Expand Up @@ -2120,6 +2139,14 @@ net_ebpf_extension_sock_addr_authorize_listen_classify(
UNREFERENCED_PARAMETER(classify_context);
UNREFERENCED_PARAMETER(flow_context);

if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) {
Comment thread
mikeagun marked this conversation as resolved.
// A callout with higher weight has revoked the write permission. Bail out
// without touching classify_output->actionType.
EBPF_EXT_LOG_MESSAGE(
EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting.");
goto Exit;
}

classify_output->actionType = FWP_ACTION_PERMIT;

filter_context = (net_ebpf_extension_sock_addr_wfp_filter_context_t*)filter->context;
Expand Down Expand Up @@ -2235,6 +2262,14 @@ net_ebpf_extension_sock_addr_authorize_recv_accept_classify(
UNREFERENCED_PARAMETER(classify_context);
UNREFERENCED_PARAMETER(flow_context);

if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) {
// A callout with higher weight has revoked the write permission. Bail out
// without touching classify_output->actionType.
EBPF_EXT_LOG_MESSAGE(
EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting.");
goto Exit;
}
Comment on lines +2294 to +2301

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

make a macro out of this.


classify_output->actionType = FWP_ACTION_PERMIT;

filter_context = (net_ebpf_extension_sock_addr_wfp_filter_context_t*)filter->context;
Expand Down Expand Up @@ -2355,6 +2390,14 @@ net_ebpf_extension_sock_addr_bind_classify(
UNREFERENCED_PARAMETER(classify_context);
UNREFERENCED_PARAMETER(flow_context);

if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) {
// A callout with higher weight has revoked the write permission. Bail out
// without touching classify_output->actionType.
EBPF_EXT_LOG_MESSAGE(
EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting.");
goto Exit;
}

classify_output->actionType = FWP_ACTION_PERMIT;

filter_context = (net_ebpf_extension_sock_addr_wfp_filter_context_t*)filter->context;
Expand Down Expand Up @@ -2383,10 +2426,17 @@ net_ebpf_extension_sock_addr_bind_classify(

// Initialize the accumulated verdict to PROCEED_SOFT so that if no program updates it
// (e.g. all clients are filtered out), the bind defaults to permit.
// The bind process_verdict callback updates net_ebpf_sock_addr_ctx.verdict with the
// The authorize_process_verdict callback updates net_ebpf_sock_addr_ctx.verdict with the
// most-restrictive verdict across multi-attach programs and short-circuits on REJECT.
net_ebpf_sock_addr_ctx.verdict = BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT;

// Snapshot the context so the shared authorize_process_verdict callback can restore it
// between programs. The snapshot is stack-local and only valid for the synchronous
// program invocation below.
bpf_sock_addr_t sock_addr_ctx_original;
memcpy(&sock_addr_ctx_original, sock_addr_ctx, sizeof(sock_addr_ctx_original));
net_ebpf_sock_addr_ctx.original_context = &sock_addr_ctx_original;

program_result =
net_ebpf_extension_hook_expand_stack_and_invoke_programs(sock_addr_ctx, &filter_context->base, &ignored_result);
if (program_result == EBPF_OBJECT_NOT_FOUND) {
Expand All @@ -2399,9 +2449,9 @@ net_ebpf_extension_sock_addr_bind_classify(
goto Exit;
}

// Use the accumulated verdict from the bind process_verdict callback. Bind hooks do not
// Use the accumulated verdict from the authorize_process_verdict callback. Bind hooks do not
// support address modification: any changes the program made to user_ip/user_port are
// silently ignored.
// silently ignored (and restored between programs by the shared accumulator).
verdict = net_ebpf_sock_addr_ctx.verdict;
switch (verdict) {
case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT:
Expand Down Expand Up @@ -2465,6 +2515,7 @@ net_ebpf_extension_sock_addr_authorize_connection_classify(
bpf_sock_addr_t* sock_addr_ctx = &net_ebpf_sock_addr_ctx.base;
uint32_t compartment_id = UNSPECIFIED_COMPARTMENT_ID;
ebpf_result_t program_result;
bool rights_revoked = FALSE;

UNREFERENCED_PARAMETER(incoming_metadata_values);
UNREFERENCED_PARAMETER(layer_data);
Expand Down Expand Up @@ -2506,9 +2557,21 @@ net_ebpf_extension_sock_addr_authorize_connection_classify(
}

// First, try to find and use existing connection context from redirect layer.
// This must happen before the rights check so the cached entry is always cleaned up,
// even when a higher-weight callout has revoked our write permission.
verdict = _net_ebpf_ext_find_and_remove_connection_context(
incoming_metadata_values->transportEndpointHandle, sock_addr_ctx);

if ((classify_output->rights & FWPS_RIGHT_ACTION_WRITE) == 0) {
// A callout with higher weight has revoked the write permission. Bail out
// without touching classify_output->actionType (the Exit-block switch is
// also skipped via rights_revoked). The cache cleanup above has already run.
EBPF_EXT_LOG_MESSAGE(
EBPF_EXT_TRACELOG_LEVEL_VERBOSE, EBPF_EXT_TRACELOG_KEYWORD_SOCK_ADDR, "No \"write\" right; exiting.");
rights_revoked = TRUE;
goto Exit;
}

// CONNECT_AUTHORIZATION programs run for all non-REJECT verdicts from the redirect layer.
// REJECT is already final. PROCEED_HARD and PROCEED_SOFT both allow authorization programs
// to run so they can make decisions based on route-dependent metadata.
Expand Down Expand Up @@ -2552,18 +2615,20 @@ net_ebpf_extension_sock_addr_authorize_connection_classify(
Exit:
// Set action type based on verdict.
// Clear FWPS_RIGHT_ACTION_WRITE for block and hard permit.
switch (verdict) {
case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT:
classify_output->actionType = FWP_ACTION_PERMIT;
break;
case BPF_SOCK_ADDR_VERDICT_PROCEED_HARD:
classify_output->actionType = FWP_ACTION_PERMIT;
classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE;
break;
default:
classify_output->actionType = FWP_ACTION_BLOCK;
classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE;
break;
if (!rights_revoked) {
switch (verdict) {
case BPF_SOCK_ADDR_VERDICT_PROCEED_SOFT:
classify_output->actionType = FWP_ACTION_PERMIT;
break;
case BPF_SOCK_ADDR_VERDICT_PROCEED_HARD:
classify_output->actionType = FWP_ACTION_PERMIT;
classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE;
break;
default:
classify_output->actionType = FWP_ACTION_BLOCK;
classify_output->rights &= ~FWPS_RIGHT_ACTION_WRITE;
break;
}
}

_net_ebpf_ext_log_sock_addr_classify(
Expand Down
22 changes: 22 additions & 0 deletions tests/libs/util/native_helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,25 @@ _native_module_helper::~_native_module_helper()
DeleteFileA(_file_name.c_str());
}
}

_native_module_helper::_native_module_helper(_native_module_helper&& other) noexcept
: _file_name(std::move(other._file_name)), _delete_file_on_destruction(other._delete_file_on_destruction),
_is_main_thread(other._is_main_thread)
{
other._delete_file_on_destruction = false;
}

_native_module_helper&
_native_module_helper::operator=(_native_module_helper&& other) noexcept
{
if (this != &other) {
if (_delete_file_on_destruction && !_file_name.empty()) {
DeleteFileA(_file_name.c_str());
}
_file_name = std::move(other._file_name);
_delete_file_on_destruction = other._delete_file_on_destruction;
_is_main_thread = other._is_main_thread;
other._delete_file_on_destruction = false;
}
return *this;
}
15 changes: 15 additions & 0 deletions tests/libs/util/native_helper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@
typedef class _native_module_helper
{
public:
_native_module_helper() = default;

// Non-copyable: the destructor deletes the underlying .sys file, so two
// instances with the same _file_name and _delete_file_on_destruction = true
// would race / double-delete.
_native_module_helper(const _native_module_helper&) = delete;
_native_module_helper&
operator=(const _native_module_helper&) = delete;

// Movable: transfer the file ownership flag and clear it on the source so
// only the destination's destructor deletes the file.
_native_module_helper(_native_module_helper&& other) noexcept;
_native_module_helper&
operator=(_native_module_helper&& other) noexcept;

void
initialize(_In_z_ const char* file_name_prefix)
{
Expand Down
Loading
Loading