mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
head: add TOCTOU regression tests and fix comment typo (#12637)
Follow-up to the metadata TOCTOU fix (#11972, PR #12439): - Add a syscall-level regression guard in util/check-safe-traversal.sh: under strace, head must read metadata from the open descriptor (fstat/statx on the fd) and must not stat the path before opening it. Verified this fails on the pre-fix code and passes after. - Add an integration test asserting that an unreadable file produces an error but no "==> name <==" header, matching GNU (the header is only printed after a successful open). - Fix a stray paren in the Windows-branch comment.
This commit is contained in:
@@ -471,7 +471,7 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
#[cfg(windows)]
|
||||
// On Windows, `File::open` on a directory fails with "Permission denied").
|
||||
// On Windows, `File::open` on a directory fails with "Permission denied".
|
||||
if err.kind() == io::ErrorKind::PermissionDenied {
|
||||
if let Ok(m) = Path::new(file).metadata() {
|
||||
if m.is_dir() {
|
||||
|
||||
@@ -980,3 +980,28 @@ fn test_directory_header_with_multiple_files_zero_output() {
|
||||
.stdout_is("==> d <==\n\n==> f <==\n")
|
||||
.no_stderr();
|
||||
}
|
||||
|
||||
/// GNU `head` prints the `==> name <==` header only after the file is
|
||||
/// successfully opened. A file that exists but cannot be opened (e.g. no read
|
||||
/// permission) must therefore produce only an error and no header.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_unreadable_file_prints_no_header() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
at.write("unreadable", "secret\n");
|
||||
at.write("readable", "hello\n");
|
||||
at.set_mode("unreadable", 0o000);
|
||||
|
||||
// Running as root bypasses permission checks, so the open would succeed.
|
||||
if std::fs::File::open(at.plus("unreadable")).is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
ts.ucmd()
|
||||
.args(&["-c", "5", "unreadable", "readable"])
|
||||
.fails_with_code(1)
|
||||
.stdout_is("==> readable <==\nhello")
|
||||
.stdout_does_not_contain("==> unreadable <==")
|
||||
.stderr_contains("cannot open 'unreadable' for reading: Permission denied");
|
||||
}
|
||||
|
||||
@@ -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 touch; do
|
||||
for util in rm chmod chown chgrp du mv cp touch head; do
|
||||
if [ -f "$PROJECT_ROOT/target/${PROFILE}/$util" ]; then
|
||||
AVAILABLE_UTILS="$AVAILABLE_UTILS $util"
|
||||
fi
|
||||
@@ -409,6 +409,33 @@ if echo "$AVAILABLE_UTILS" | grep -q "touch"; then
|
||||
echo "✓ touch creates with O_CREAT and without O_TRUNC"
|
||||
fi
|
||||
|
||||
# Test head - the is-a-directory check must derive from the already-open
|
||||
# descriptor (fstat/statx on the fd), not from a separate path-based stat
|
||||
# performed before the open. A path stat followed by an open is a TOCTOU
|
||||
# window (#11972): the object named by the path can be swapped in between.
|
||||
if echo "$AVAILABLE_UTILS" | grep -q "head"; then
|
||||
echo ""
|
||||
echo "Testing head (fstat_after_open)..."
|
||||
if [ "$USE_MULTICALL" -eq 1 ]; then
|
||||
head_cmd="$COREUTILS_BIN head"
|
||||
else
|
||||
head_cmd="$PROJECT_ROOT/target/${PROFILE}/head"
|
||||
fi
|
||||
echo "headtest" > test_head_file.txt
|
||||
strace -f -e trace=openat,fstat,newfstatat,statx,stat,lstat \
|
||||
-o strace_head_metadata.log $head_cmd -c 4 test_head_file.txt 2>/dev/null || true
|
||||
cat strace_head_metadata.log
|
||||
if ! grep -q 'openat(AT_FDCWD, "test_head_file.txt"' strace_head_metadata.log; then
|
||||
fail_immediately "head did not open test_head_file.txt via openat"
|
||||
fi
|
||||
# The filename should appear only in the openat; any stat-family call naming
|
||||
# the path means head stat'd it before opening - the TOCTOU window.
|
||||
if grep '"test_head_file.txt"' strace_head_metadata.log | grep -qv 'openat('; then
|
||||
fail_immediately "head stat'd the path before opening it - TOCTOU window (#11972); metadata must come from the open descriptor"
|
||||
fi
|
||||
echo "✓ head reads metadata from the open descriptor, not a path stat"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Basic safe traversal verification completed"
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user