chmod: fix TOCTOU race in recursive traversal#11918
chmod: fix TOCTOU race in recursive traversal#11918mattsu2020 wants to merge 2 commits intouutils:mainfrom
Conversation
|
GNU testsuite comparison: |
|
Thanks for the fix — TOCTOU in 1. Add a
|
|
maybe do the improvements too |
| use std::os::unix::io::FromRawFd; | ||
|
|
||
| let fd = unsafe { | ||
| libc::openat( |
There was a problem hiding this comment.
| .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid proc path"))?; | ||
|
|
||
| Ok(()) | ||
| let ret = unsafe { libc::chmod(proc_cstr.as_ptr(), mode as libc::mode_t) }; |
There was a problem hiding this comment.
|
|
||
| openat_count=$(grep -c "openat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0") | ||
| unlinkat_count=$(grep -c "unlinkat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0") | ||
| fchmodat_count=$(grep -c "fchmodat(" "$strace_log" 2>/dev/null | tr -d '\n' || echo "0") |
There was a problem hiding this comment.
This pattern counts both together. We have expanded the fchmodat count to include fchmodat2 rather than excluding it.
Use fchmodat2 (Linux 6.6+) with AT_SYMLINK_NOFOLLOW to prevent an attacker from replacing a directory entry with a symlink between the stat and chmod calls. Falls back to fchmodat on older kernels. - Restrict fchmodat2 (syscall 452) to asm-generic architectures only (x86_64, x86, arm, aarch64, riscv) - Add SAFETY comment on unsafe syscall block per project convention - Add O_PATH + /proc/self/fd fallback for musl on kernel < 6.6 - Cache ENOSYS result with AtomicBool to skip fchmodat2 on old kernels - Remove unnecessary nix::Mode round-trip on the fchmodat2 path - safe_chmod_file() takes explicit SymlinkBehavior parameter - Always pass NoFollow for regular entries during recursion - Document residual TOCTOU in symlink branch as intentional for -L - Add test verifying NoFollow chmod doesn't modify symlink target - Update check-safe-traversal.sh to recognize fchmodat2
Replace raw unsafe libc::openat/libc::chmod calls in chmod_at_via_opath with rustix::fs::openat and rustix::fs::chmod, eliminating all unsafe blocks from the O_PATH fallback path. Also includes formatting cleanup per review feedback.
Summary
chmod -Rwhere an attacker could replace a directory entry with a symlink betweenstatandchmodoperationsfchmodat2syscall (Linux 6.6+) withAT_SYMLINK_NOFOLLOWto prevent the race, with fallback tofchmodatfor older kernelsSYS_fchmodat2locally since libc doesn't expose it on all architectures (e.g. aarch64 musl)check-safe-traversal.shto recognizefchmodat2as a valid safe traversal syscallChanges
src/uucore/src/lib/features/safe_traversal.rschmod_at()now usesfchmodat2(raw syscall 452) withAT_SYMLINK_NOFOLLOWon Linux when symlink-following is disabled, falling back tofchmodatonENOSYSSYS_FCHMODAT2locally as 452 instead of relying onlibc::SYS_fchmodat2which is missing on some architecturessrc/uu/chmod/src/chmod.rssafe_chmod_file()now takes an explicitSymlinkBehaviorparameter instead of deriving it fromself.dereferenceSymlinkBehavior::NoFollowfor regular files/dirs during recursion (TOCTOU-safe: we already confirmed via stat the entry is not a symlink)SymlinkBehavior::Followfor file symlinks only when-Lis specifiedutil/check-safe-traversal.shfchmodat2in strace filter for chmod testsfchmodatorfchmodat2as valid safe traversal syscallsfchmodat2in safe syscall statisticsTest
cargo check -p uucorepassescargo test -p uu_chmod— all tests passcargo test -p coreutils -- chmod— all integration tests pass