Skip to content

Windows: no socket clone a child process can inherit (sbx proxy, tool bridge) - #79

Merged
blockmandev merged 1 commit into
mainfrom
fix/socket-clones-leak-to-children
Oct 27, 2026
Merged

blockmandev merged 1 commit into
mainfrom
fix/socket-clones-leak-to-children

Conversation

@blockmandev

Copy link
Copy Markdown
Member

Fixes the flaky Windows proxy test, which turned out to be a real bug: on Windows, a cloned socket can leak into child processes.

Root cause

The sandbox proxy read each request through client.try_clone(). On Windows that clone is created inheritable. GetHandleInformation confirms it: the accepted socket has inherit=0, and its clone has inherit=1.

Any child process started from the same process while the clone was open inherited the connection. That includes the sandboxed command and icacls, because Command with piped stdio inherits handles. Closing our side then sent nothing, and the child's exit reset the connection, throwing away a 403/502 the client had already received (os error 10054).

  • In the tests: the children were the ACL tests' icacls. Bisecting showed the proxy tests fail 6 of 6 runs beside windows::acl::tests, and never beside any other module.
  • In real use: sbx starts processes from the same process its proxy runs in. A refused or finished connection could be held open inside the command, or reset.

The same pattern is fixed in incode's tool bridge. It read each request through try_clone, while the toolbox may be starting shell commands, background jobs included.

Change

  • proxy.rs: serve reads and writes through &TcpStream, and pipe copies both ways on scoped threads that borrow the two sockets. No clone exists.
  • tool_bridge.rs: the request is read through a borrow.

Tested

  • New test: a_refusal_reaches_the_client_while_processes_are_started_beside_it starts child processes in a loop while 200 requests are refused, and every refusal must be read whole. With the clone put back, it loses 1–2 of 60 on every run.
  • incord-sandbox: all targets pass (lib 76, escape_windows 23).
    • The whole lib passed 10 of 10 runs; before the fix, it failed on the first run.
    • The proxy and ACL tests together had 0 failures in 20 runs.
  • incode: cargo test --lib 2415 passed, tool_bridge 17 passed.
  • Lint: clippy --workspace --all-targets -D warnings and fmt are clean.

Root cause of the flaky proxy tests (connection reset, os error 10054, at
proxy.rs while reading a 403/502): serve() read the request through
client.try_clone(), and on Windows that clone is created INHERITABLE
(measured with GetHandleInformation: the accepted socket inherit=0, its
clone inherit=1). Any process the same process started while the clone
was open (Command with piped stdio inherits handles) took a copy of the
connection; closing ours then sent nothing, and the child's exit reset
the connection, discarding a reply the client had already received. In
the test suite the children were the ACL tests' icacls; bisected: proxy
tests fail 6 of 6 beside windows::acl::tests and never beside any other
module. In real use sbx starts the sandboxed command and icacls from the
process the proxy runs in, so a refused or finished connection could
hang open inside the command or be reset.

serve() now reads and writes through &TcpStream, and pipe() copies both
ways on scoped threads borrowing the two sockets: no clone exists to
leak. The same shape in incode's tool bridge (answer_connection read the
request through try_clone while the toolbox may be starting shell
commands, background jobs included) is fixed the same way.

New test: a_refusal_reaches_the_client_while_processes_are_started_beside_it
starts children in a loop while 200 requests are refused; every refusal
must be read whole. With the clone put back it loses 1 or 2 of 60 on
every run.

Tested: incord-sandbox all targets (lib 76, escape_windows 23), the whole
sandbox lib 10 of 10 runs with no failure (it failed on the first run
before), proxy+acl paired 0 of 20; cargo test --lib 2415, tool_bridge 17;
clippy --workspace --all-targets -D warnings; fmt.
@blockmandev
blockmandev merged commit 8c969f4 into main Oct 27, 2026
12 checks passed
@blockmandev
blockmandev deleted the fix/socket-clones-leak-to-children branch October 27, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant