Skip to content

Apply worker file ownership and mode via an open descriptor - #9006

Merged
rzo1 merged 2 commits into
masterfrom
worker-launcher-fd-permissions
Aug 24, 2026
Merged

Apply worker file ownership and mode via an open descriptor#9006
rzo1 merged 2 commits into
masterfrom
worker-launcher-fd-permissions

Conversation

@reiabreu

Copy link
Copy Markdown
Contributor

setup_permissions() opens each entry with O_NOFOLLOW, confirms via fstat that it is still the same object fts_read() classified (matching device and inode), and uses fchown/fchmod on that descriptor instead of lchown/chmod by pathname.

How this was tested

Built the worker-launcher native tree with autoreconf -i && ./configure && make check (autotools, compiled with -Werror); the test suite passes.

setup_permissions() opens each entry with O_NOFOLLOW, confirms via fstat
that it is still the same object fts_read() classified (matching device
and inode), and uses fchown/fchmod on that descriptor instead of lchown/
chmod by pathname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rzo1 rzo1 added this to the 3.1.0 milestone Aug 23, 2026

@rzo1 rzo1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently low on time, thus a LLM based review.

The fd-based approach is the right way to close the by-path TOCTOU, but one issue undercuts it:

1. open() lacks O_NONBLOCK, so a race swap to a FIFO hangs the setuid-root launcher indefinitely (major).
setup_permissions runs as root (seteuid(0) before the fts walk) over a tree whose entries are owned/writable by the topology user — which is exactly why this PR adds the st_dev/st_ino guard. But in the window between fts_read() classifying an entry as FTS_F and the open() at line 517, the user can unlink the regular file and mkfifo a same-named FIFO. O_NOFOLLOW doesn't reject FIFOs (only symlinks) and O_DIRECTORY isn't set for files, so open(O_RDONLY) on a writerless FIFO blocks indefinitely — and because it blocks, control never reaches the dev/ino check, so that guard can't mitigate this vector. The result is a root-privileged launcher hung forever, wedging supervisor worker setup/cleanup. The pre-change code used lchown+chmod by path and never opened anything, so this hang is newly introduced by the fix. Adding O_NONBLOCK to open_flags fixes it: it's a no-op for regular files/dirs, fchown/fchmod on the fd still work, and the existing fstat check then cleanly rejects the swapped object.

2. No tests for the new setup_permissions branches (minor).
test-worker-launcher.c has no reference to setup_permissions/setup_dir_permissions, so none of the new branches — open() failure, fstat() failure, the dev/ino-mismatch rejection, or the fchown/fchmod-via-fd success path — are exercised; reverting to the old by-path code fails no test. The fail-closed dev/ino branch in particular is worth a test (would need the function de-static'd or a test-visible wrapper, since setup_dir_permissions calls seteuid(0)).

…ed FIFO

O_NOFOLLOW rejects a symlink but not a FIFO, so if an entry is replaced by a
FIFO between fts_read() classifying it and the open, open(O_RDONLY) would block
the launcher. O_NONBLOCK avoids that; it is a no-op for regular files and
directories, and the fstat device/inode check still rejects the swapped object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@reiabreu

Copy link
Copy Markdown
Contributor Author

Thank you for the feedback, @rzo1. I'm afraid I haven't touched C in many years, so I relied entirely on Claude for these fixes.

  • 1 (O_NONBLOCK): Added O_NONBLOCK to the open flags in setup_permissions — no-op for regular files/dirs, fchown/fchmod on the fd still work, and the fstat dev/ino check now rejects a file swapped for a writerless FIFO instead of blocking the root launcher.
  • 2 (tests): Not covered here — setup_dir_permissions calls seteuid(0) and setup_permissions is static, so the open/fstat-failure and dev/ino-mismatch branches need the function de-static'd or a test wrapper; left for a separate test-harness change.

@reiabreu

Copy link
Copy Markdown
Contributor Author

Following up on the test-coverage point: exercising the setup_permissions branches needs the function de-static'd, and the full privileged walk needs root (which CI's make check doesn't run as). I'll fold what's unit-testable — the dev/ino decision — into the same worker-launcher test-coverage follow-up PR once these PRs are on master, alongside the main.c enforcement tests noted on #9008 and #9010.

@rzo1
rzo1 merged commit 6aa0066 into master Aug 24, 2026
7 checks passed
@rzo1
rzo1 deleted the worker-launcher-fd-permissions branch August 24, 2026 10:59
@reiabreu

Copy link
Copy Markdown
Contributor Author

The follow-up test coverage landed in #9014 (OCI launch-command enforcement). The setup_permissions fd/dev-ino branch noted here is not included: its privileged walk runs under seteuid(0) and needs root, which CI's make check does not run as, so only an artificial decision-helper could be unit-tested without root.

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.

2 participants