Repository navigation
Report covert egress and cross-sandbox channels in audit mode (1.3.0) - #13
Merged
Merged
Conversation
Audit-only detections, nothing is denied: - proxy-sni-mismatch: TLS SNI differs from the CONNECT host (domain fronting) - proxy-embedded-url: plain-HTTP request carries a URL for another host - proxy-upload-heavy: lopsided uploads to an allowed target - proxy-bypass-attempt (Linux): UDP and direct TCP outside the egress proxy, observed with a seccomp user-notification supervisor that always continues - timing-channel-suspect: on/off keying in the sandbox's cgroup (Linux) or job object (Windows) CPU and IO counters - shared-write-path / shared-lock-file: concurrent runs that share writable paths or lock files Also gives each Linux sandbox a unique cgroup name: inside the PID namespace every --init is pid 1, so concurrent runs all used safer-exec-1.
Linux egress watch: - Hand the seccomp listener to the engine with SCM_RIGHTS instead of pidfd_getfd. When the copy failed (Docker's default profile), the inner process closed the only listener and every connect of the workload failed with ENOSYS. sendmsg is no longer watched, which also removes the deadlock the first handoff hit and a round trip on IPC and connected-UDP traffic. - Never close the listener before the run ends; retry unexpected errors. - Close duplicated sockets off the serve loop (SO_LINGER could stall it), resolve the thread group for pidfd_open, fall back to /proc socket tables, and label unknown socket types as ip. - Report every UDP socket creation, use the ID_VALID encoding 5.5-5.8 accept, cache executables per pid, bound finish. - Lock the init thread so Landlock and seccomp land on the thread that execs. - Use the unique cgroup name for cgroup v1 too and remove v1 directories. Proxy: - Detections run only for audited runs. - proxy-embedded-url records the request authority, never the path; parses ';' separators and uses the embedded URL's own port. - Count tunnels a server keeps open: track upstream conns, close and drain before flushing; normalize target ports; report run-wide spread uploads. - Cap findings per type and report the overflow. Run registry: - Move it to the user cache dir and deny it in the Seatbelt profile; the macOS sandbox could plant a FIFO there and hang every later run. - Read records without following links or blocking, regular files only, bounded size and count, names must match content.
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.
OpenAI's write-up on securing their model training environment (Securing our frontier model training environment) describes agents getting past containment without breaking any single rule:
Most of these can't be blocked without breaking ordinary builds. So this PR makes safer-exec report them instead. All of these checks only add audit entries: they run when auditing is on, deny nothing, and need no new configuration.
What gets reported
proxy-sni-mismatch: a tunnel's TLS server name doesn't match the host in its CONNECT. That is domain fronting through a CDN or a shared front end.proxy-embedded-url: a plain-HTTP request carries a URL for another host in its query or path. The entry names only the request's host and port, the parameter, and the embedded URL's host. Paths and values are never logged.proxy-upload-heavy: the workload uploaded at least 1 MiB to an allowed host, and at least four times what came back. An upload split across many targets is caught for the run as a whole. Tunnels the server keeps open are closed and counted at the end of the run.proxy-bypass-attempt(Linux): UDP sockets, and connections orsendtos that skip the proxy. The proxy only sees what is sent through it, Landlock doesn't confine UDP, and it confines TCP only by port. So DNS, QUIC, or a direct connect on an open port all go around the hostname allowlist today. To see these, the inner process installs a seccomp user-notification filter and hands the listener to the engine with SCM_RIGHTS. The engine records each call and always lets it continue.timing-channel-suspect(Linux, Windows): the sandbox's total CPU time (and IO, where the counter exists) switches between idle and busy at least 16 times in 10 seconds. The data comes from the run's cgroup on Linux and the job object on Windows.shared-write-path/shared-lock-file: another safer-exec run of the same user, running at the same time, can write the same path or holds the same lock. Runs register in a per-user directory under the user cache dir, and the macOS profile denies the workload access to it.README has a section describing each entry, and THREAT-MODEL §3.21 lists what these checks can't catch. In short:
sendmsgdestinationsTHREAT-MODEL also corrects the row that called UDP evasion "Mitigated"; that only holds with
disableNetwork.Fixed along the way
Inside the PID namespace every
--initis pid 1, so every Linux sandbox named its cgroupsafer-exec-1. Concurrent runs with limits collided: one of them silently lost its limits, and the directory was left behind. Each run now gets a unique name on both cgroup v2 and v1, and the engine removes the directory when the run ends. The init thread is also locked to one OS thread now. That keeps Landlock and seccomp on the thread that execs the target.Review
An independent adversarial review found real problems, and all of them are fixed here. The main ones:
pidfd_getfd), the workload's own connects failed with ENOSYS. The handoff now uses SCM_RIGHTS, which needs no permission the engine could be refused.$TMPDIR, and hang every later run. The registry moved to the user cache dir, the Seatbelt profile denies it, and records are read with no-follow, non-blocking, size and count limits.0443.Version
Bumped to 1.3.0.
Testing
Each environment ran this branch and
mainside by side:-race. Two concurrent audited CLI runs: the second reportedshared-write-path. A workload granted~/Library/Cachescan't list or write the registry.main. The other failures there also fail onmain(read-only/tmp, missingcurl, test data outside the mount). Under the default profile, the workload still reaches the proxy, and UDP and direct connections are reported.systemd-run --user --scope -p Delegate=yes.go vetclean, all packages pass. An audited workload alternating CPU bursts with sleep is reported from the job object's counters.