Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -436,6 +435,20 @@ pub fn touch(files: &[InputFile], opts: &Options) -> Result<(), TouchError> {
Ok(())
}

/// Create `path` if it does not exist, without ever truncating it.
///
/// Uses `O_CREAT` but deliberately not `O_TRUNC`: if an attacker plants a
/// symlink at `path` in the window between the metadata check in
/// [`touch_file`] and this open, the open follows it but must not zero the
/// symlink's target. Matches GNU touch (issue #10019).
fn create_without_truncate(path: &Path) -> std::io::Result<fs::File> {
OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(path)
}

/// Create or update the timestamp for a single file.
///
/// # Arguments
Expand Down Expand Up @@ -486,7 +499,7 @@ fn touch_file(
return Ok(());
}

if let Err(e) = File::create(path) {
if let Err(e) = create_without_truncate(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
Expand Down Expand Up @@ -981,4 +994,34 @@ mod tests {
assert_eq!(actual_atime, atime);
assert_eq!(actual_mtime, mtime);
}

// The #10019 fix: the create-open must use O_CREAT without O_TRUNC. During
// the TOCTOU race the open lands on an *existing* file (the symlink's
// target), so opening an existing file must leave its contents intact. This
// deterministically distinguishes the fix from the old File::create, which
// used O_TRUNC and would zero the file here.
#[cfg(unix)]
#[test]
fn create_without_truncate_does_not_truncate_existing_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("victim");
std::fs::write(&path, b"do not truncate me").unwrap();

super::create_without_truncate(&path).unwrap();

assert_eq!(std::fs::read(&path).unwrap(), b"do not truncate me");
}

// The other half of the contract: when the path is missing it must be
// created (as an empty file), matching the old File::create behavior.
#[cfg(unix)]
#[test]
fn create_without_truncate_creates_missing_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("new");

super::create_without_truncate(&path).unwrap();

assert_eq!(std::fs::read(&path).unwrap(), b"");
}
}
35 changes: 35 additions & 0 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,3 +1074,38 @@ fn test_touch_device_files() {
.succeeds()
.no_output();
}

// Touching a symlink to an existing file must not truncate the target, like
// GNU touch. The target exists, so this exercises the update_times path, not
// the create path changed for #10019 — it guards the general "touch never
// truncates" contract. The create-path fix itself is covered by the
// create_without_truncate unit tests in src/touch.rs and the syscall-flag
// check in util/check-safe-traversal.sh.
#[test]
#[cfg(unix)]
fn test_touch_does_not_truncate_symlink_target() {
use std::os::unix::fs::symlink;

let (at, mut ucmd) = at_and_ucmd!();
at.write("victim", "do not truncate me");
symlink(at.plus("victim"), at.plus("link")).unwrap();

ucmd.arg("link").succeeds();

assert_eq!(at.read("victim"), "do not truncate me");
}

// Touching a dangling symlink creates its target as an empty file, like GNU.
#[test]
#[cfg(unix)]
fn test_touch_through_dangling_symlink_creates_target() {
use std::os::unix::fs::symlink;

let (at, mut ucmd) = at_and_ucmd!();
symlink(at.plus("missing"), at.plus("link")).unwrap();

ucmd.arg("link").succeeds();

assert!(at.file_exists("missing"));
assert_eq!(at.read("missing"), "");
}
24 changes: 23 additions & 1 deletion util/check-safe-traversal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ if [ "$USE_MULTICALL" -eq 1 ]; then
AVAILABLE_UTILS=$($COREUTILS_BIN --list)
else
AVAILABLE_UTILS=""
for util in rm chmod chown chgrp du mv cp; do
for util in rm chmod chown chgrp du mv cp touch; do
if [ -f "$PROJECT_ROOT/target/${PROFILE}/$util" ]; then
AVAILABLE_UTILS="$AVAILABLE_UTILS $util"
fi
Expand Down Expand Up @@ -387,6 +387,28 @@ if echo "$AVAILABLE_UTILS" | grep -q "mv" && [ -d /dev/shm ]; then
fi
fi

# Test touch - creating a file must use O_CREAT but never O_TRUNC, so that a
# symlink planted in the metadata-check/open race window (#10019) is not
# truncated. This observes the flags directly, which integration tests cannot.
if echo "$AVAILABLE_UTILS" | grep -q "touch"; then
echo ""
echo "Testing touch (create_no_truncate)..."
if [ "$USE_MULTICALL" -eq 1 ]; then
touch_cmd="$COREUTILS_BIN touch"
else
touch_cmd="$PROJECT_ROOT/target/${PROFILE}/touch"
fi
strace -f -e trace=openat -o strace_touch_create.log $touch_cmd test_touch_new 2>/dev/null || true
cat strace_touch_create.log
if ! grep -q 'openat(.*test_touch_new.*O_CREAT' strace_touch_create.log; then
fail_immediately "touch did not create test_touch_new via openat(O_CREAT)"
fi
if grep 'test_touch_new' strace_touch_create.log | grep -q 'O_TRUNC'; then
fail_immediately "touch opened the target with O_TRUNC - vulnerable to truncating a symlink target (#10019)"
fi
echo "✓ touch creates with O_CREAT and without O_TRUNC"
fi

echo ""
echo "✓ Basic safe traversal verification completed"
echo ""
Expand Down
Loading