diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index bcb688ee7b3..6121c98e275 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -4,7 +4,7 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) datelike datetime filetime lpszfilepath mktime strtime timelike utime DATETIME UTIME futimens -// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS +// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS CREAT pub mod error; @@ -23,9 +23,8 @@ use rustix::fs::Timestamps; use rustix::fs::futimens; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; -#[cfg(unix)] +use std::fs; use std::fs::OpenOptions; -use std::fs::{self, File}; use std::io::{Error, ErrorKind}; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; @@ -486,7 +485,16 @@ fn touch_file( return Ok(()); } - if let Err(e) = File::create(path) { + // Open with O_CREAT but no O_TRUNC: if an attacker plants a + // symlink at `path` between the metadata check above and this + // open, we must not truncate the symlink's target. Matches GNU + // touch (issue #10019). + if let Err(e) = OpenOptions::new() + .write(true) + .create(true) + .truncate(false) + .open(path) + { // we need to check if the path is the path to a directory (ends with a separator) // we can't use File::create to create a directory // we cannot use path.is_dir() because it calls fs::metadata which we already called diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 61e071da317..86c38fb20f0 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1080,3 +1080,23 @@ fn test_touch_device_files() { .succeeds() .no_output(); } + +/// Regression for #10019: when an attacker plants a symlink at the +/// target path between touch's existence check and the create open, +/// touch must not truncate the symlink's target. The fix drops O_TRUNC. +#[test] +#[cfg(unix)] +fn test_touch_does_not_truncate_through_planted_symlink() { + use std::os::unix::fs::symlink; + + let (at, mut ucmd) = at_and_ucmd!(); + at.write("victim", "do not truncate me"); + // Plant `target` as a symlink to victim *before* touch runs. This + // is a deterministic stand-in for the real race: the attacker wins + // it instead of racing against it. + symlink(at.plus("victim"), at.plus("target")).unwrap(); + + ucmd.arg("target").succeeds(); + + assert_eq!(at.read("victim"), "do not truncate me"); +}