mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
touch: drop O_TRUNC on create to close TOCTOU race (#10019)
This commit is contained in:
committed by
Dorian Péron
parent
44087eb515
commit
ca0c842e71
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"), "");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user