diff --git a/.vscode/cspell.dictionaries/jargon.wordlist.txt b/.vscode/cspell.dictionaries/jargon.wordlist.txt index d2febb7724f..55284cd0744 100644 --- a/.vscode/cspell.dictionaries/jargon.wordlist.txt +++ b/.vscode/cspell.dictionaries/jargon.wordlist.txt @@ -1,3 +1,4 @@ +ACCMODE AFAICT alloc arity @@ -119,6 +120,8 @@ primality pseudoprime pseudoprimes quantiles +RDONLY +RDWR readonly reparse rposition @@ -143,6 +146,7 @@ symlinks syscall syscalls sysconf +sysfs tokenize toolchain totalram diff --git a/Cargo.lock b/Cargo.lock index fe0ee52a136..42901d9e315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3925,6 +3925,7 @@ version = "0.5.0" dependencies = [ "clap", "fluent", + "libc", "memchr", "memmap2", "regex", diff --git a/src/uu/tac/Cargo.toml b/src/uu/tac/Cargo.toml index 79e6b661055..b576f704dab 100644 --- a/src/uu/tac/Cargo.toml +++ b/src/uu/tac/Cargo.toml @@ -24,7 +24,8 @@ memchr = { workspace = true } memmap2 = { workspace = true } regex = { workspace = true } clap = { workspace = true } -uucore = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true, features = ["signals"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 507dd153199..ccf55eb48f5 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -4,6 +4,9 @@ // file that was distributed with this source code. // spell-checker:ignore (ToDO) sbytes slen dlen memmem memmap Mmap mmap SIGBUS +#[cfg(unix)] +uucore::init_stdio_state_capture!(); + mod error; use clap::{Arg, ArgAction, Command}; @@ -15,8 +18,9 @@ use std::{ fs::{File, read}, path::Path, }; -use uucore::error::UError; -use uucore::error::UResult; +#[cfg(unix)] +use uucore::error::set_exit_code; +use uucore::error::{UError, UResult}; use uucore::{format_usage, show}; use crate::error::TacError; @@ -237,13 +241,24 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR let buf; let data: &[u8] = if filename == "-" { + #[cfg(unix)] + if uucore::signals::stdin_was_closed() { + let e: Box = TacError::ReadError( + OsString::from("-"), + std::io::Error::from_raw_os_error(libc::EBADF), + ) + .into(); + show!(e); + set_exit_code(1); + continue; + } if let Some(mmap1) = try_mmap_stdin() { mmap = mmap1; &mmap } else { let mut buf1 = Vec::new(); if let Err(e) = stdin().read_to_end(&mut buf1) { - let e: Box = TacError::ReadError(OsString::from("stdin"), e).into(); + let e: Box = TacError::ReadError(OsString::from("-"), e).into(); show!(e); continue; } @@ -252,21 +267,32 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR } } else { let path = Path::new(filename); - if path.is_dir() { - let e: Box = TacError::InvalidArgument(filename.clone()).into(); + let Ok(metadata) = path.metadata() else { + let e: Box = TacError::FileNotFound(filename.clone()).into(); show!(e); continue; - } + }; - if path.metadata().is_err() { - let e: Box = TacError::FileNotFound(filename.clone()).into(); + if metadata.is_dir() { + let e: Box = TacError::InvalidArgument(filename.clone()).into(); show!(e); continue; } - if let Some(mmap1) = try_mmap_path(path) { - mmap = mmap1; - &mmap + let mut maybe_data: Option<&[u8]> = None; + // Avoid mmap when the reported size is zero; procfs/sysfs files often + // claim length 0 while still producing data, and mapping them would + // yield an empty buffer (GNU tac-2-nonseekable). + let should_mmap = metadata.is_file() && metadata.len() > 0; + if should_mmap { + if let Some(mmap1) = try_mmap_path(path) { + mmap = mmap1; + maybe_data = Some(&mmap[..]); + } + } + + if let Some(data_slice) = maybe_data { + data_slice } else { match read(path) { Ok(buf1) => { diff --git a/src/uucore/src/lib/features/signals.rs b/src/uucore/src/lib/features/signals.rs index 0bccb2173f6..5175497eacd 100644 --- a/src/uucore/src/lib/features/signals.rs +++ b/src/uucore/src/lib/features/signals.rs @@ -426,6 +426,90 @@ pub fn ignore_interrupts() -> Result<(), Errno> { unsafe { signal(SIGINT, SigIgn) }.map(|_| ()) } +// Detect closed stdin/stdout/stderr before Rust reopens them as /dev/null (see issue #2873) +#[cfg(unix)] +use std::sync::atomic::{AtomicBool, Ordering}; + +#[cfg(unix)] +static STDIN_WAS_CLOSED: AtomicBool = AtomicBool::new(false); +#[cfg(unix)] +static STDOUT_WAS_CLOSED: AtomicBool = AtomicBool::new(false); +#[cfg(unix)] +static STDERR_WAS_CLOSED: AtomicBool = AtomicBool::new(false); + +#[cfg(unix)] +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn capture_stdio_state() { + use nix::libc; + + unsafe { + STDIN_WAS_CLOSED.store( + libc::fcntl(libc::STDIN_FILENO, libc::F_GETFD) == -1, + Ordering::Relaxed, + ); + STDOUT_WAS_CLOSED.store( + libc::fcntl(libc::STDOUT_FILENO, libc::F_GETFD) == -1, + Ordering::Relaxed, + ); + STDERR_WAS_CLOSED.store( + libc::fcntl(libc::STDERR_FILENO, libc::F_GETFD) == -1, + Ordering::Relaxed, + ); + } +} + +#[macro_export] +#[cfg(unix)] +macro_rules! init_stdio_state_capture { + () => { + #[cfg(not(target_os = "macos"))] + #[used] + #[unsafe(link_section = ".init_array")] + static CAPTURE_STDIO_STATE: unsafe extern "C" fn() = $crate::signals::capture_stdio_state; + + #[cfg(target_os = "macos")] + #[used] + #[unsafe(link_section = "__DATA,__mod_init_func")] + static CAPTURE_STDIO_STATE: unsafe extern "C" fn() = $crate::signals::capture_stdio_state; + }; +} + +#[macro_export] +#[cfg(not(unix))] +macro_rules! init_stdio_state_capture { + () => {}; +} + +#[cfg(unix)] +pub fn stdin_was_closed() -> bool { + STDIN_WAS_CLOSED.load(Ordering::Relaxed) +} + +#[cfg(not(unix))] +pub const fn stdin_was_closed() -> bool { + false +} + +#[cfg(unix)] +pub fn stdout_was_closed() -> bool { + STDOUT_WAS_CLOSED.load(Ordering::Relaxed) +} + +#[cfg(not(unix))] +pub const fn stdout_was_closed() -> bool { + false +} + +#[cfg(unix)] +pub fn stderr_was_closed() -> bool { + STDERR_WAS_CLOSED.load(Ordering::Relaxed) +} + +#[cfg(not(unix))] +pub const fn stderr_was_closed() -> bool { + false +} + #[test] fn signal_by_value() { assert_eq!(signal_by_name_or_value("0"), Some(0)); diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 0f5aad48808..fd3c9bb39b2 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -335,3 +335,65 @@ fn test_failed_write_is_reported() { .fails() .stderr_is("tac: failed to write to stdout: No space left on device (os error 28)\n"); } + +#[test] +#[cfg(unix)] +fn test_fifo_argument() { + use std::fs::OpenOptions; + use std::io::Write; + use std::thread; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkfifo("fifo_input"); + let fifo_path = at.plus("fifo_input"); + + let child = scene.ucmd().arg("fifo_input").run_no_wait(); + + let writer = thread::spawn(move || { + let mut pipe = OpenOptions::new().write(true).open(fifo_path).unwrap(); + pipe.write_all(b"line1\nline2\n").unwrap(); + }); + + child.wait().unwrap().success().stdout_is("line2\nline1\n"); + + writer.join().unwrap(); +} + +#[test] +#[cfg(unix)] +fn test_stdin_dev_null_rdwr_is_supported() { + use std::fs::OpenOptions; + + let dev_null = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/null") + .unwrap(); + + new_ucmd!() + .set_stdin(dev_null) + .succeeds() + .no_stderr() + .stdout_is(""); +} + +#[test] +#[cfg(unix)] +fn test_stdin_closed_with_dash_args_fails() { + use std::os::unix::process::CommandExt; + + let ts = TestScenario::new(util_name!()); + let mut cmd = std::process::Command::new(&ts.bin_path); + cmd.arg("tac").args(["-", "-"]); + + unsafe { + cmd.pre_exec(|| { + libc::close(libc::STDIN_FILENO); + Ok(()) + }); + } + + let output = cmd.output().unwrap(); + assert_eq!(output.status.code(), Some(1)); +}