Repository navigation
Wire the compiled policy into the eBPF map the LSM program enforces - #270
Merged
Merged
Conversation
My audit found a silent contract disconnect: userspace wrote maps named policy_fs_allow/policy_net_allow/... (to the wrong pinned path, with hashed-string keys), while the kernel LSM program reads sandbox_policy keyed by cgroup id. The compiled policy never reached the map the kernel consults, so 'hardened' eBPF FS/net enforcement did nothing. This wires the enforced path end to end: - bpf/contract.py: single source of truth for the deny-mask bits (kept in lockstep with the PYI_DENY_* defines in syscall_filter.bpf.c), the little-endian key/value encoding matching struct pyisolate_policy, and compile_deny_mask(policy). The mask is coarse by construction -- it can deny a whole capability class but not express per-path allow-lists, so process creation and ptrace/mount/bpf are always denied, and FS/NET are denied only when the policy grants nothing in that class. Per-path FS is enforced by Landlock and per-destination net by the broker. - BPFManager.set_sandbox_policy writes sandbox_policy at the correct pinned location (/sys/fs/bpf/pyisolate/sandbox_policy), keyed by the cgroup id, which equals the cgroup directory inode that bpf_get_current_cgroup_id() returns. - Supervisor programs the map on spawn (best-effort in dev; fail-closed in hardened) using the sandbox's real cgroup inode. Tests assert the enforcement map name and deny-mask bits match the .bpf.c (guarding against exactly the disconnect that existed), the key/value byte layout matches the kernel struct, cgroup_id resolves to the inode, and a spawn programs sandbox_policy with the derived mask. Kernel enforcement itself is exercised on a real host as before; the wiring/encoding is tested in userspace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDSofWX5HwawsrwbN2nSXR
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.
The bug
The security audit that kicked off this series found a silent contract disconnect in the eBPF path:
to_bpf_map_entries→_update_bpf_map) wrote maps namedpolicy_fs_allow/policy_net_allow/ … — to/sys/fs/bpf/{name}(the wrong path; maps are pinned under/sys/fs/bpf/pyisolate/), with hashed-string keys.syscall_filter.bpf.c) reads a different map,sandbox_policy, keyed bybpf_get_current_cgroup_id().The two never met. The compiled policy was written to maps no kernel program reads, so "hardened" eBPF FS/net enforcement did nothing — while the threat model claimed kernel-enforced FS/network policy.
The fix
This wires the enforced path end-to-end, keyed the way the kernel actually looks policy up.
bpf/contract.py— single source of truth for:PYI_DENY_*defines in the.bpf.c;struct pyisolate_policy { u32 deny_mask; u32 audit_only; };compile_deny_mask(policy).cgroup_id_for_path()— the cgroup id is the cgroup directory inode, which is exactly whatbpf_get_current_cgroup_id()returns, soos.stat(cg_path).st_inois the correct key.BPFManager.set_sandbox_policy()— writessandbox_policyat the correct pinned location, keyed by the cgroup id.Granularity — deliberately coarse, and honest about it
deny_maskis a per-cgroup, per-capability-class switch: it can deny a whole class but not express "allow only/srv/data". So:Per-path filesystem enforcement is delegated to Landlock (#269) and per-destination network to the broker. This PR makes the coarse kernel layer actually connected, rather than claiming per-path eBPF matching it doesn't do. (A real per-path eBPF matcher is a much larger effort and out of scope.)
Tests
tests/test_ebpf_contract.py— all run in userspace (no kernel needed):sandbox_policy) is defined in the.bpf.c, and the Python deny-mask bits equal thePYI_DENY_*defines parsed from the C source. These would have failed on the pre-fix code and prevent the disconnect from silently returning.cgroup_id_for_pathreturns the inode.sandbox_policywith the derived mask.Kernel enforcement itself runs on a real host as before; the wiring and encoding are what's tested here.
Full suite: 441 passed, 2 environment-gated skips; all pre-commit hooks pass.
Next
B-5 reconciles the threat model / docs now that the boundary (process isolation + seccomp + Landlock + this eBPF wiring) is real, and downgrades the sub-interpreter backend's "DEFENDED against hostile Python" language.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DDSofWX5HwawsrwbN2nSXR
Generated by Claude Code