From 6b89e2f1d56dbf1ef5d5b7234cbe9dd80b7b036d Mon Sep 17 00:00:00 2001 From: max-amb Date: Fri, 22 May 2026 22:35:25 +0100 Subject: [PATCH 1/2] head: Fixed TOCTOU bug in checking of metadata --- src/uu/head/src/head.rs | 77 ++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 896e0c923e1..e27b4d68a6e 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -13,7 +13,7 @@ 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}; +use std::path::PathBuf; use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; use uucore::error::{FromIo, UError, UResult, USimpleError}; @@ -447,34 +447,66 @@ 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() { - Ok(m) => m, + 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() == std::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; } }; - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - writeln!(stdout)?; + + let metadata = match file_handle.metadata() { + Ok(m) => m, + Err(err) => { + show!(err.map_err_context( + || translate!("head-error-cannot-open", "name" => file.quote()) + )); + continue; } - 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( @@ -484,15 +516,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(()) }; From d9cbb8c5115a9e140c8d357bdaecaf607e9c00c5 Mon Sep 17 00:00:00 2001 From: max-amb Date: Fri, 22 May 2026 22:48:05 +0100 Subject: [PATCH 2/2] Fixed missing import and qualification for windows --- src/uu/head/src/head.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index e27b4d68a6e..10d72341c9e 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -13,6 +13,8 @@ use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write}; use std::num::TryFromIntError; #[cfg(unix)] use std::os::fd::AsFd; +#[cfg(windows)] +use std::path::Path; use std::path::PathBuf; use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; @@ -473,7 +475,7 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { Err(err) => { #[cfg(windows)] // On Windows, `File::open` on a directory fails with "Permission denied"). - if err.kind() == std::io::ErrorKind::PermissionDenied { + 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