experiment#403
Draft
branchseer wants to merge 14 commits into
Draft
Conversation
Add as_os_str (Unix only) and to_cow_os_str (cross-platform) so callers can recover an OsStr from a deserialized NativePath without going through strip_path_prefix.
Adds the shared CallbackKind / CallbackRequest / CALLBACK_ACK wire types used by the synchronous round-trip between a traced process and the supervisor, plus the per-backend Payload endpoint fields (CallbackConf on Unix, callback_pipe_name + callback_mask on Windows). Both are absent / empty when no callback is registered.
Add FileEvent / FileEventKind / FileEventPath / BorrowedFile in a new
crate::callback module and Command::on_file_event(mask, callback) so a
consumer can register a callback that runs in the supervisor process.
Per-backend supervisor servers live in callback::{unix,windows}; the
Unix one wraps a Unix-domain socket and SCM_RIGHTS, the Windows one
wraps a named pipe with DuplicateHandle. SpawnError grows a
CallbackChannelCreation variant for binding failures.
No wiring into the OS-impl spawn() yet — that comes in follow-ups.
…tions The preload connects per-event to the supervisor's Unix-domain socket, passes the freshly opened (or about-to-be-closed) fd via SCM_RIGHTS, sends a length-prefixed CallbackRequest, and blocks reading a single ACK byte before letting the syscall return. The open hooks now call the real libc fn first, capture the result fd, keep the existing shared-memory event, and only then run the blocking post-open callback. New close/fclose hooks fire the pre-close callback while the fd is still valid; the fd's access mode is read with F_GETFL, and obviously-non-file paths (sockets, anon inodes) are filtered out before any round-trip. A thread-local reentrancy guard prevents the round-trip's own socket I/O from recursing. The callback channel is decoded from the EncodedPayload only when one is registered, so the no-callback path remains a single Option check.
Add NotifyResponse { Continue, ReturnFd { fd, cloexec } } and a small
HandlerResponse trait so a handler can ask the supervisor to install a
file descriptor into the target (via SECCOMP_IOCTL_NOTIF_ADDFD with the
SEND flag) and complete the syscall atomically with the new fd as the
result. Keeping HandlerResponse separate from SeccompNotifyHandler
means the impl_handler! macro stays untouched — handlers that always
continue rely on the default impl.
supervise() now delegates to a new supervise_with(init, syscalls)
which builds each per-connection handler via the init closure and
filters exactly the syscalls passed in, letting callers inject
per-spawn state and decide at runtime which syscalls to intercept.
Also exposes Caller::pid() and Fd::raw() — both needed by handlers
that want to look up /proc/<pid>/fdinfo/<fd> for the open mode of a
descriptor about to be closed.
The supervisor's SyscallHandler now optionally carries a FileCallback (injected via with_callback). When set, for each open*-syscall notification the supervisor opens the file itself with the target's flags, runs the callback on its own descriptor, and replies with a ReturnFd response so the kernel installs that descriptor into the target via ADDFD-SEND. For close notifications it reads the access mode out of /proc/<pid>/fdinfo/<fd>, opens a fresh read-only fd for the callback, then continues so the target performs the close. handle_open now takes an is_open_syscall flag so it only fires the post-open callback for real open* notifications — execve / stat / access route through handle_open purely for access recording and must not get an ADDFD response (execve does not return a descriptor). /dev/, /proc/ and /sys/ paths are skipped before any supervisor open to avoid spurious round-trips.
On non-musl hosts, when Command::on_file_event is set, spawn now binds a UnixCallbackServer and threads its socket path + access-mode mask into the Payload so the preload knows where to round-trip. On Linux, supervise_with is invoked with the full syscall list (including close) and a per-spawn closure that hands the FileCallback to each SyscallHandler instance. With no callback registered, close is filtered out of the syscall list so per-close overhead stays at zero. The wait task drains the callback server (so every in-flight callback finishes) before locking the IPC channel.
The preload connects per-event to the supervisor's named pipe and sends [u32 len][CallbackRequest][u64 raw HANDLE], then blocks reading a single ACK byte. The raw handle is duplicated out of this process by the supervisor (DuplicateHandle); we just send its numeric value. NtCreateFile / NtOpenFile are restructured to call the real fn first (so file_handle is populated), keep the existing shared-memory event, and only then run the post-open callback. A new NtClose detour fires the pre-close callback while the handle is still valid. NtClose runs for every handle type, so we keep a lock-free DashMap of file handles whose open mode matched the mask. Only handles in that map get a close callback, and the stored mode is reported back as the event's access mode. A thread-local IN_CALLBACK guard suppresses the recursion that would otherwise happen when the round-trip's own CreateFileW / NtClose go back through the detours.
When Command::on_file_event is set, spawn now binds a WindowsCallbackServer on a uniquely named pipe, threads its name plus the access-mode mask into the Payload, and (after the child has been spawned suspended but before ResumeThread) hands the child's process handle to the server so DuplicateHandle on incoming callbacks can pull the target's raw HANDLE into this process. The wait task drains the callback server before locking the IPC channel so every in-flight callback completes.
read_verify opens the file, reads it, and asserts the content is non-empty — used by the seccomp blocking-callback test to prove the ADDFD-installed descriptor in the target is usable. read_verify_threads runs the same from four concurrent threads to exercise the callback path under concurrency.
file_callback.rs covers the preload backend on Linux glibc / macOS / Windows: blocking proof (target cannot progress while the callback runs), the supervisor can read the passed descriptor, the close callback fires before the close with a still-valid descriptor, the mask filters events, and registering no callback leaves access tracking unchanged. static_executable.rs adds two seccomp-backend cases (ADDFD round-trip + multi-threaded concurrent opens).
e29e264 to
a81d817
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
- Gate fspy::callback::unix to non-musl: it's only used by the preload backend, which itself is excluded on musl, so leaving it in caused dead_code errors under cargo's -D warnings on musl. - Drop a redundant [`FileEvent`](crate::FileEvent) explicit link target — FileEvent is re-exported at the crate root. - Disambiguate the [`impl_handler`] intra-doc link in HandlerResponse's doc to the macro at the crate root.
…ndle_close/open_callback Skipping the thread-local reentrancy guard until the ctor has set the global client keeps these paths infallible during very early (libdyld / pre-ctor) opens and closes, where the thread-local accessor is the only non-trivial operation either path would do anyway. Also surface the actual stdout content in the cancellation test's assertion message — without it, a child that crashes before writing 'ready' shows up as a bare assertion failure with no clue about why.
On macOS, tempfile::tempdir() returns paths under /var/folders/..., which is a symlink to /private/var/folders/.... The open syscall sees the literal /var/ path, but the close handler resolves the fd via F_GETPATH which returns the canonical /private/var/ form. The tests filter callback events by 'starts_with(dir_path)' — without canonical- ization the close event's canonical path fails to match the non- canonical prefix, so the Closing event is dropped and the close callback test fails on macOS only.
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.
Ongoing experiment — opened to run CI, not for merge.
Generated by Claude Code