Repository navigation
fix: fail the launch when the egress socket cannot be bound - #16
Conversation
The launcher started the egress socket in a task. Then it waited two seconds for the socket file and continued. When the bind failed, the error stayed in the task. The user saw an unrelated bind-mount ENOENT and "spawn /opt/memory/memory: Invalid argument". Now the launcher binds the socket before it starts the task. A failed bind stops the launch with its own error. Egress::bind_unix refuses a path that is too long for a unix socket. The error gives the path, its length and the limit: 108 bytes on Linux and 104 on macOS, with the closing NUL. The unconfined runner uses TCP and does not use the socket. It does not bind the socket now, so a long root does not stop an unconfined launch. The unconfined runner also keeps its TCP listener and serves on it. Before, it closed the listener and bound the address again in the task. The relay binds its loopback port before it starts the harness. When the bind fails, the relay stops with exit 1 and does not start the harness. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The egress removed any file at the socket path before it bound the socket. This removed a regular file or a symlink. It also removed a socket that another launch used, and the new socket then took that launch's new connections. Now Egress::bind_unix examines the path first: - Nothing is there: it binds. - A socket is there and no listener answers on it: it removes the socket and binds. - A socket is there and a listener answers on it: it stops with "in use by another launch". - Something that is not a socket is there: it stops and keeps it. SUN_PATH now comes from the size of libc::sockaddr_un minus the offset of sun_path. This gives the correct limit on each unix system, without unsafe code. New tests: - An unconfined launch binds no socket while the harness runs. - "amem inside" with a requests directory that is too long fails, and the error gives the length. This test does not need namespaces, so CI runs it. - After "amem inside" and after a confined launch, the socket is gone. - A stale socket is replaced; a live socket, a regular file and a symlink are refused and kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The change alters the security-critical egress/confinement socket-binding and launch sequencing, which warrants final human review even though no concrete defects were found.
No open issues 🎉
What changed in this PR
This PR fixes a launch failure mode found by the harness-container survey (#2): when the egress unix socket path (<root>/state/run/egress-<pid>.sock) exceeds the platform limit (108 bytes on Linux, 104 on macOS), the old code started the serve task, waited up to two seconds for the socket file, and continued silently, leaving the user with an unrelated, confusing error. The change makes socket binding happen synchronously before the serve task starts, so a bind failure stops the launch with a clear, length-naming error. It also centralizes serving behind a generic serve(listener) and applies the same bind-before-run discipline to the unconfined TCP runner and the relay.
Changes:
- Add
Egress::bind_unix(path-length check via aSUN_PATHconst computed fromlibc::sockaddr_un, plus stale/live/non-socket handling inremove_stale_socket) and a genericEgress::serve(listener); refactorserve_unix/serve_tcpto use them. - Rework
Session::startto bind before spawning the serve task, makeserve_unixoptional (unconfined binds no unix socket), keep the unconfined TCP listener instead of rebinding, and makerelaybind its loopback port before running the harness. - Add comprehensive tests for the length boundary, stale/live/non-socket cases, confined/unconfined launches from a long root, the relay failure path, and socket cleanup.
| File | Description |
|---|---|
crates/egress/src/lib.rs |
New SocketPath error, SUN_PATH const, bind_unix, generic serve, remove_stale_socket, and supporting unit tests. |
crates/memory/src/main.rs |
Bind socket before spawning serve task; optional serve_unix; unconfined keeps its TCP listener; relay binds before running the harness. |
crates/memory/tests/launch.rs |
New integration tests for long-root launches, relay failure, socket cleanup; inside helper extracted. |
crates/egress/Cargo.toml |
Adds unix-only libc dependency for sockaddr_un sizing. |
Cargo.lock |
Records libc as a dependency of dm-egress. |
Review effort: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
The change modifies confinement-sensitive subsystems (the egress "one way out", launcher socket-binding order, and the in-sandbox relay), which warrant final human review despite clean, well-tested code.
No open issues 🎉
Review effort: Balanced
This PR fixes one of the gaps that the harness-container survey (#2) found in our code: a socket path past 108 bytes (
docs/harness-containers.md, "A socket path past 108 bytes").The problem
The egress socket is at
<root>/state/run/egress-<pid>.sock. A unix socket path takes at most 108 bytes on Linux and 104 on macOS, the closing NUL included. The launcher startedserve_unixas a task, waited up to two seconds for the socket file, and continued when the file did not come. The bind error stayed in the task. With a long root, the user saw only an unrelated bind-mount ENOENT andspawn /opt/memory/memory: Invalid argument.The change
dm_egress:Egress::bind_unix(path)checks the length first.SUN_PATHcomes fromlibc::sockaddr_un, so it is right on every unix. The error names the path and the number of bytes.Egress::serve(listener)serves a listener that is already bound.Session::start:relay: cannot listen on <addr>. Before, the harness ran anyway.Failing first, against
maincargo nextest run -p dm-egress -p amemwith the new tests:egress: path must be shorter than SUN_LEN, with no length.left: Some(0) right: Some(1). The harness ran although the relay could not listen.amem insidewith a 120-character requests directory: exit 1 withNo such file or directory, not the length. This test needs no namespaces, so it runs on CI.state/run while the harness ran: . .. egress-1195928.sock. Onmain, the unconfined runner still bound the socket.Against
ea76792, before the second commit:bind_unixon a live socket succeeded, and took the connections. On a regular file, it deleted the file.Mutations:
finish()without its socket removal:the session takes its socket away: ["egress-1242654.sock"].bind_unixwithout the stale-socket removal:Address already in use.make check: 630 tests, 630 passed, 0 skipped.Adversarial pass
A fresh agent attacked
ea76792and found nothing that blocks the change. It verified these points:It found these gaps, all fixed in
58fa841:main.main.finish()had no tests.bind_unixreplaced a live socket.SUN_PATHwas 104 on every unix other than Linux.Posted by Claude on behalf of @sstriker