Skip to content

subproc: mark supervisor fds CLOEXEC before helper exec - #351

Open
ManoharPaturi wants to merge 1 commit into
google:masterfrom
ManoharPaturi:pr-subproc-helperfds
Open

ManoharPaturi wants to merge 1 commit into
google:masterfrom
ManoharPaturi:pr-subproc-helperfds

Conversation

@ManoharPaturi

@ManoharPaturi ManoharPaturi commented Sep 27, 2026 •

Copy link
Copy Markdown

Problem

systemExe() executes external helpers such as newuidmap and newgidmap without first marking unrelated supervisor descriptors close-on-exec. As a result, listening sockets, accepted connections, log descriptors, or other inherited resources can cross the helper execve() boundary unnecessarily.

Change

Before executing an external helper, mark all descriptors above stderr FD_CLOEXEC using nsjail's existing util::makeRangeCOE() helper — the same mechanism the pasta helper path already uses.

The systemExe() error-reporting pipe is already created with O_CLOEXEC. This means it remains available when execve() fails, allowing the child to report the failure, while it is automatically closed when execve() succeeds.

If descriptor sealing itself fails, helper execution is aborted rather than executing with an unknown inherited descriptor set.

Regression test

tests/systemexe_fd_test.cc (wired into make test) demonstrates the behavior before/after: it occupies fd 123 in the supervisor, then runs a helper through systemExe() that checks for /dev/fd/123 in its own process.

  • on master: the helper observes fd 123 and exits 42 → the test fails
  • with this patch: the descriptor is closed by the exec and the helper exits 0 → the test passes

Verified on Ubuntu 24.04: plain build + full test suite, and a full ASan/UBSan build with the suite run under the sanitizers — all green, no sanitizer findings.

Notes

  • The sealing uses close_range(CLOSE_RANGE_CLOEXEC), which requires kernel ≥ 5.9; if support for older kernels without a fallback is preferred (as in containMakeFdsCOE()), that variant is easy to switch to.
  • Thanks to the review that suggested the makeRangeCOE() shape — an earlier revision of this PR closed descriptors manually and cleared O_CLOEXEC on the error pipe, which was strictly worse than the existing primitive.

@ManoharPaturi

Copy link
Copy Markdown
Author

Small branch hygiene fix: my local CI workflow file accidentally ended up in this branch — force-pushed the branch rebuilt from current master with only the intended change. The diff is now just the patch itself; the actions scan failure from earlier is gone.

…elper exec

systemExe() executes external helpers such as newuidmap and newgidmap
without first marking unrelated supervisor descriptors close-on-exec.
Listening sockets, accepted connections, log descriptors, and other
inherited resources could therefore cross the helper execve() boundary
unnecessarily.

Mark all descriptors above stderr FD_CLOEXEC with nsjail's existing
util::makeRangeCOE() before the exec - the same mechanism the pasta
helper path already uses. The systemExe() error-reporting pipe is
created with O_CLOEXEC, so it remains available to report an execve()
failure and is closed automatically on success. If the descriptor
sealing itself fails, helper execution is aborted rather than running
with an unknown inherited descriptor set.

Adds a regression test proving a supervisor-owned fd (123) is not
visible to a helper executed through systemExe().
@ManoharPaturi ManoharPaturi changed the title [subproc] don't leak supervisor fds into setuid helpers subproc: mark supervisor fds CLOEXEC before helper exec Sep 27, 2026
@ManoharPaturi

Copy link
Copy Markdown
Author

Reworked per review: replaced the manual close_range + /proc/self/fd block with util::makeRangeCOE(STDERR_FILENO + 1, ~0U) (the same primitive the pasta helper path uses), keeping the error pipe's O_CLOEXEC semantics intact, and failing closed if the sealing fails. Also added the suggested regression test (tests/systemexe_fd_test.cc, runs in make test): it occupies fd 123 in the supervisor and proves the helper no longer sees it — on master the helper observes the fd and the test fails, with the patch it passes. Verified with a plain build and under ASan/UBSan.

@ManoharPaturi

ManoharPaturi commented Sep 27, 2026 •

Copy link
Copy Markdown
Author

@robertswiecki Could you take a look at this revision? Following review feedback, the change now uses util::makeRangeCOE(STDERR_FILENO + 1, ~0U) — the same mechanism the pasta helper path already uses — instead of manual descriptor closing, and fails closed if the sealing fails. The included regression test parks a descriptor at fd 123 and verifies a helper executed via systemExe() can no longer observe it; it fails on master and passes with the patch. The full suite was also run under ASan/UBSan.

This branch has not been deployed

No deployments
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