fix(sec): a hook that cannot evaluate an operation refuses it - #10
Merged
Conversation
Four branches in the LSM programs returned 0 — allow — when the hook could not reach a decision: a `bpf_d_path` failure in `file_open` and `bprm_check_security`, a null `bprm`/`file`/sockaddr argument, an unavailable `PATHBUF` slot, and any address family `socket_connect` does not decode. Three are provokable from inside a scope, which makes them escapes rather than edge cases. `bpf_d_path` fails with -ENAMETOOLONG once the resolved path passes its 4KB buffer, and `execve` never has to pass a path that long: chdir down a deep chain and exec a short *relative* name, and the kernel resolves it to something the hook cannot render. That was a general way out of an exec-enforced scope — the allowlist simply stopped applying (f007). The family default was the same shape in the network hook: an AF_UNIX connect to a local agent socket left an egress-enforced scope unmediated (f034). `deny_unevaluated` now returns the denial errno and emits an audit record in all four. It runs only after the scope has been shown to enforce that dimension, so an unenforced scope is untouched and observe mode still records without blocking. For non-IP families this is a policy statement as much as a code change: the authoring language spells destinations `host:port`, so no rule can ever name an AF_UNIX or AF_NETLINK peer — and "no rule matches" in an enforcing scope means deny. The cost is real and accepted: a network-enforced scope refuses local socket IPC outright, and no enforced scope can open or exec a path longer than the kernel's path buffer. Recorded as research R16, with the inode-identity fallback named as the better long-term answer once the backend can pin inodes. VM matrix: three new cases (`exec-unresolvable-denied`, `file-unresolvable-denied`, `net-unix-denied`), 35/35 on the BPF-LSM VM. All three fail against the pre-fix binary — the exec one only after its descent loop was rewritten to use shell builtins, since a denied `seq` left it in the shallow directory and turned it into a false PASS. Also marks triage f010 as stale rather than open: `hardening.rs::drop_privileges` already sets PR_SET_NO_NEW_PRIVS and empties the capability bounding set except the DAC pair, and the `priv-drop` case asserts it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes triage f007 and f034, and marks f010 stale. Stacked on #9 (which is stacked on #8) — bases retarget as each merges.
The hole
Four branches in the LSM programs returned
0— allow — when the hook couldn't reach a decision. Three of the four are provokable from inside a scope, which makes them escapes rather than edge cases.bpf_d_pathfails inbprm_check_security-ENAMETOOLONGpast its 4KB buffer.execvenever has to pass a >PATH_MAX argument:chdirdown a deep chain, exec a short relative name, and the kernel resolves it to something the hook can't render. The exec allowlist simply stops applying. (f007)bpf_d_pathfails infile_opensocket_connect_ => return 0— an AF_UNIX connect to a local agent socket left an egress-enforced scope unmediated (f034)bprm/file/sockaddr, unavailablePATHBUFThe fix
deny_unevaluatedreturns the denial errno and emits an audit record in all four. It runs only after the scope has been shown to enforce that dimension (network flag set, deny list present, exec allowlist present), so an unenforced scope is untouched and observe mode still records without blocking.For non-IP families this is a policy statement as much as a code change: the authoring language spells destinations
host:port, so no rule can ever name an AF_UNIX or AF_NETLINK peer — and "no rule matches" in an enforcing scope means deny.The cost is real and accepted, and it's documented in
policy.schema.md: a network-enforced scope refuses local socket IPC outright, and no enforced scope can open or execute a path longer than the kernel's path buffer. The better long-term answer for exec is an inode identity fallback, which needs the inode-pinning backendEnforcementPlancurrently refuses (UnsupportedInodePin); deny-on-unresolvable is correct in the meantime and stays correct after. Recorded as research R16 with both alternatives and why they lost.Verification
Three new VM matrix cases —
exec-unresolvable-denied,file-unresolvable-denied,net-unix-denied— and 35/35 on the BPF-LSM VM, which also confirms the programs still verify on a real kernel.All three fail against the pre-fix binary. Worth flagging:
exec-unresolvable-deniedinitially passed pre-fix, which was a false PASS —seqisn't on the test's exec allowlist, so the descent loop never ran and the exec failed for the wrong reason. Rewritten to use shell builtins only, plus aDESCENT_FAILEDguard so the case can never silently prove nothing again. Pre-fix run after that correction:Host side:
cargo clippy --workspace --all-targets -- -D warningsclean,cargo fmtapplied, workspace tests green except the pre-existingrepl_command::no_provider_at_all_reports_what_is_missing(build without--features enforce).f010 is stale, not open
The report claims tool children keep launcher privileges with no
no_new_privsand no capability drop.hardening.rs::drop_privilegesdoes both, and thepriv-dropVM case assertsNoNewPrivs=1 CapBnd=0x6. It was closed by8e2cdbb/3225d44, which the triage run predates. Markednot_applicablein TRIAGE rather than left looking like open work.🤖 Generated with Claude Code