Apply worker file ownership and mode via an open descriptor - #9006
Conversation
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
left a comment
There was a problem hiding this comment.
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>
|
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.
|
|
Following up on the test-coverage point: exercising the |
|
The follow-up test coverage landed in #9014 (OCI launch-command enforcement). The |
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.