head: Fixed TOCTOU bug in checking of metadata (#12439)

* head: Fixed TOCTOU bug in checking of metadata

* Fixed missing import and qualification for windows
This commit is contained in:
Max Ambaum
2026-06-05 12:18:15 +01:00
committed by GitHub
parent 533edad992
commit 43536897e4
+52 -27
View File
@@ -13,7 +13,9 @@ use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
use std::num::TryFromIntError;
#[cfg(unix)]
use std::os::fd::AsFd;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use uucore::display::{Quotable, print_verbatim};
use uucore::error::{FromIo, UError, UResult, USimpleError};
@@ -444,11 +446,56 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
Ok(())
} else {
// Stat the path first so we know whether to print the header.
// When 0 bytes or 0 lines are requested, there is nothing to
// read, so we should succeed on directories just like GNU head
// does. Skip opening the file entirely in that case.
let zero_output = matches!(options.mode, Mode::FirstBytes(0) | Mode::FirstLines(0));
// GNU head prints "==> name <==" for existing files and
// directories, but NOT for nonexistent ones — those produce
// only an error message.
let metadata = match Path::new(file).metadata() {
let mut print_header = || -> UResult<()> {
if (options.files.len() > 1 && !options.quiet) || options.verbose {
if !first {
writeln!(stdout)?;
}
write!(stdout, "==> ")?;
print_verbatim(file).unwrap();
writeln!(stdout, " <==")?;
first = false;
}
Ok(())
};
let mut file_handle = match File::open(file) {
Ok(f) => f,
Err(err) => {
#[cfg(windows)]
// 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() {
// We need to print the header, as we have an existing directory
print_header()?;
if !zero_output {
show!(USimpleError::new(
1,
translate!("head-error-reading-file", "name" => file.quote(), "err" => "Is a directory")
));
}
continue;
}
}
}
show!(err.map_err_context(
|| translate!("head-error-cannot-open", "name" => file.quote())
));
continue;
}
};
let metadata = match file_handle.metadata() {
Ok(m) => m,
Err(err) => {
show!(err.map_err_context(
@@ -457,21 +504,8 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
continue;
}
};
if (options.files.len() > 1 && !options.quiet) || options.verbose {
if !first {
writeln!(stdout)?;
}
write!(stdout, "==> ")?;
print_verbatim(file).unwrap();
writeln!(stdout, " <==")?;
first = false;
}
// When 0 bytes or 0 lines are requested, there is nothing to
// read, so we should succeed on directories just like GNU head
// does. Skip opening the file entirely in that case (also
// avoids platform differences: on Windows, `File::open` on a
// directory fails with "Permission denied").
let zero_output = matches!(options.mode, Mode::FirstBytes(0) | Mode::FirstLines(0));
print_header()?;
if metadata.is_dir() {
if !zero_output {
show!(USimpleError::new(
@@ -481,15 +515,6 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
}
continue;
}
let mut file_handle = match File::open(file) {
Ok(f) => f,
Err(err) => {
show!(err.map_err_context(
|| translate!("head-error-cannot-open", "name" => file.quote())
));
continue;
}
};
head_file(&mut file_handle, options)?;
Ok(())
};