Follow-up fixes from the PR #113 review#155
Conversation
…scall list Signed-off-by: Cong Wang <cwang@multikernel.io>
…d variant POLICY_EVENT_SYSCALLS only covered openat/openat2/connect/sendto/bind/execve, so the SyscallEvent path/path2/flags/protocol fields documented for sendmsg, sendmmsg and the fs-mutation syscalls never fired in a policy_fn-only sandbox; sandlock learn only saw them because COW and network supervision happened to pull those syscalls into the notif list. Replace the const with policy_event_syscalls(), matching everything emit_policy_event can decode, including the legacy single-path variants. Extending interception also exposed two verdict bypasses: openat2 and legacy open emit the same "openat" event as openat, and sendmsg/sendmmsg the same class of send as sendto, but none of them were held, so a callback returning Verdict::Deny was silently ignored for those variants. Add all of them to the held set and document the exact held/observation-only split on PolicyCallback: fs-mutation events stay observation-only, fs_deny is the mechanism for blocking paths. New tests: fs-mutation events carry resolved paths in a policy_fn-only sandbox, and deny verdicts block openat2 and sendmsg with EPERM. Signed-off-by: Cong Wang <cwang@multikernel.io>
renameat, the middle-generation variant between rename and renameat2, was absent from fs_denied_path_syscalls, syscall_name, both resolve functions, and the policy event set: a raw syscall(SYS_renameat, ...) could rename a denied file away, bypassing the deny gate, and emitted no policy event. Add arch::sys_renameat() (Option-returning: aarch64 kept renameat in the generic ABI, riscv64 provides only renameat2) and wire it everywhere renameat2 is handled; events are normalized to the "renameat2" name like legacy rename. Also add the legacy fs syscalls to syscall_category's File arm, which previously only covered getdents. Signed-off-by: Cong Wang <cwang@multikernel.io>
…capture The /proc/<pid>/maps scan armed by an execve event fired on the pid's next event, but execvp issues one execve attempt per $PATH candidate, so the next event after a failed attempt is just another execve, still running the pre-exec image. For the workload's first exec that image is the sandbox runtime itself, leaking the sandlock binary and its libraries (libgcc_s, /usr/lib libc aliases) into every learned profile. A successful execve never returns, so another exec event from a pending pid can only mean the previous attempt failed. Scan only on the first non-exec event, which is guaranteed to run under the new image. Signed-off-by: Cong Wang <cwang@multikernel.io>
- validate --collapse-prefix sensitivity up front in run(), before the workload executes, instead of after a full observation pass; drop the now-unused force_sensitive parameter from collapse_by_threshold - deduplicate the exit-status handling into require_clean_exit() and return anyhow errors instead of calling std::process::exit - pin [program].exec to the absolute binary path observed via /proc/<pid>/exe; argv[0] may be a bare name resolved through $PATH - stop is_junk_path from swallowing /dev/ttyS*/ttyUSB* (stable hardware paths); only the controlling terminal /dev/tty is session-specific - move tempfile back to dev-dependencies (only tests use it) - un-splice the to_toml impl from the middle of parse_input's doc comment - finish the truncated SyscallEvent.protocol doc sentence - drop the unexplained exit-code-2 tolerance in test_learn_then_run_write - close the unterminated code fence in docs/learn.md - remove a stray blank line in builder.rs Signed-off-by: Cong Wang <cwang@multikernel.io>
|
@ghazariann Please take a look. Thanks! |
|
Thanks for the learn cleanups, agree with all of them. The execve point in particular: a successful execve never returns, so the only correct window to scan maps is the next non-exec event from that pid. Didn't think of that, good catch. Am I right that the two review comments from PR #113 have already been resolved?
If so, a few remaining gaps worth tracking based on your and @dzeriks comments:
Anything else I am missing? I will open a follow-up PR once this one merges. |
|
Correcting my earlier version of this comment: I had run the deny matrix against 0.8.5 as released, then read this branch — the destination direction is already handled here. What does look source-only is the test side. Concretely: dropping the For calibration on the released version — on 0.8.5 that exact sequence succeeds ( |
…a denied path Signed-off-by: Cong Wang <cwang@multikernel.io>
|
Good catch, added in f9ef80b: To confirm they pin the arm you called out, I ran them against a mutated |
|
@ghazariann Yes, both resolved here:
Your list looks right to me, with one addition: sendmmsg multi-destination under-decoding. 3aec66f only put the TODO on record (notif.rs:1880); only the first mmsghdr entry is decoded, so a multi-destination sendmmsg still under-learns. Call it N4. Two things you can drop from the tracking: the 256-byte path cap is already gone (read_path_for_event reads up to PATH_MAX), so E1 is purely lexical-vs-canonical now; and the #72 write-collapse guards plus the nested-create ancestors fix landed in learn.rs before merge. On ordering, I'd start with N1: bind never round-tripping means server workloads produce profiles that fail outright, while the rest degrade quality rather than correctness. Happy to review the follow-up PR when it's up. |
Follow-up to #113 (sandlock learn), addressing the remaining review findings plus deeper issues uncovered while fixing them.
sandlock-core
Deny-path gating: truncate/unlink (
c1fa373). Tracing the review's resolution/enforcement coupling concern showed the deny precheck was already scope-filtered (so #113 never widened enforcement), but revealed a pre-existing hole dating to the Rust rewrite: afs_deny'd file inside a granted write tree could be wiped viatruncate(2)or deleted viaunlinkat(2), since neither was intercepted (the fd-based open deny never sees path-based destruction).fs_denied_path_syscalls()is now the single source of truth for both the BPF notif list and the runtime precheck, and gates truncate, unlinkat (coversAT_REMOVEDIR), and legacy unlink/rmdir. This hole exists in every release through 0.8.5.policy_fn contract (
7759974).POLICY_EVENT_SYSCALLSnow matches everythingemit_policy_eventcan decode, so documentedSyscallEventfields fire for a policy_fn-only sandbox instead of depending on COW or network supervision side effects. Extending interception exposed two verdict bypasses: openat2/legacy open emit the same "openat" event as openat, and sendmsg/sendmmsg the same send class as sendto, but none were held, soVerdict::Denywas silently ignored for those variants. All are now held, andPolicyCallbackdocuments the exact held/observation-only split.renameat(2) coverage (
ef5e6fa). The middle-generation variant between rename and renameat2 was absent everywhere: a rawsyscall(SYS_renameat, ...)could rename a denied file away and emitted no policy event. Addedarch::sys_renameat()(Option-returning; riscv64 provides only renameat2) and wired it alongside renameat2. Also moved the legacy fs syscalls intosyscall_category's File arm.sandlock learn
Self-capture (
7765c82). Learned profiles included the sandlock binary and its libraries: execvp issues one execve attempt per $PATH candidate, and the/proc/<pid>/mapsscan armed by an execve event fired on the pid's next event, which after a failed attempt is another execve still running the pre-exec (sandbox runtime) image. Since a successful execve never returns, the scan now skips exec events and fires on the first non-exec event, guaranteed to run under the new image. This also removes the/usr/libsymlink-alias duplicates the old-image scan produced.Cleanup batch (
9e16376). Upfront--collapse-prefixvalidation (before the workload runs), exit-status handling deduplicated intorequire_clean_exit()returning anyhow errors,[program].execpinned to the observed absolute binary path,is_junk_pathno longer swallows stable serial devices (/dev/ttyS*,/dev/ttyUSB*), tempfile back to dev-dependencies, doc-comment and docs/learn.md fixes.Tests
New regression tests: truncate/unlink/renameat deny bypasses (content-preservation asserts), fs-mutation events with resolved paths in a policy_fn-only sandbox, and
Verdict::Denyblocking openat2 and sendmsg with EPERM. Full sandlock-core suite green locally (517 unit + 296 integration).Not in this PR
Known remaining learn issues, queued separately: unbracketed IPv6 endpoints in
network.allow, phantomudp://<ip>:80entries from glibc getaddrinfo address-sorting probes, andbindnever being written toallow_bind(server workloads do not round-trip).