chmod: fix TOCTOU race in recursive traversal#11918
chmod: fix TOCTOU race in recursive traversal#11918mattsu2020 wants to merge 21 commits intouutils:mainfrom
Conversation
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.
libc 0.2.182 does not define SYS_fchmodat2 on all architectures (e.g. aarch64 musl). Define the syscall number (452) locally since it is part of the Linux stable ABI.
The commit fixes a formatting issue in the `safe_chmod_file` function call for file symlinks in the chmod utility. The function call was improperly formatted with incorrect line breaks and spacing, which has been corrected to improve code readability and maintain consistency with the project's formatting standards.
On Linux 6.6+, chmod_at() uses fchmodat2 instead of fchmodat when AT_SYMLINK_NOFOLLOW is needed. Update check-safe-traversal.sh to trace and accept fchmodat2 as a valid safe traversal syscall.
|
GNU testsuite comparison: |
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.
libc 0.2.182 does not define SYS_fchmodat2 on all architectures (e.g. aarch64 musl). Define the syscall number (452) locally since it is part of the Linux stable ABI.
The commit fixes a formatting issue in the `safe_chmod_file` function call for file symlinks in the chmod utility. The function call was improperly formatted with incorrect line breaks and spacing, which has been corrected to improve code readability and maintain consistency with the project's formatting standards.
On Linux 6.6+, chmod_at() uses fchmodat2 instead of fchmodat when AT_SYMLINK_NOFOLLOW is needed. Update check-safe-traversal.sh to trace and accept fchmodat2 as a valid safe traversal syscall.
|
Thanks for the fix — TOCTOU in 1. Add a
|
- Add // SAFETY: comment on unsafe syscall block per project convention - Restrict fchmodat2 (syscall 452) to asm-generic architectures only (x86_64, x86, arm, aarch64, riscv); MIPS/SPARC/PowerPC/Alpha use different numbering - Add test verifying NoFollow chmod doesn't modify symlink target
|
maybe do the improvements too |
- Add O_PATH + /proc/self/fd fallback for musl on kernel < 6.6 where fchmodat returns EOPNOTSUPP for AT_SYMLINK_NOFOLLOW - Cache ENOSYS result with AtomicBool to skip fchmodat2 on old kernels - Remove unnecessary nix::Mode round-trip on the fchmodat2 path - Document residual TOCTOU in handle_symlink_during_safe_recursion as intentional for -L mode
| 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.
…d_at_via_opath Use rustix::fs::openat and rustix::fs::chmod instead of raw unsafe libc::openat/libc::chmod calls in the O_PATH fallback path, eliminating all unsafe blocks from chmod_at_via_opath.
|
|
||
| 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.
Co-authored-by: oech3 <79379754+oech3@users.noreply.github.com>
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