Skip to content

Commit 32ab405

Browse files
committed
fix: detect closed stdin before Rust sanitizes it to /dev/null
1 parent c085cd1 commit 32ab405

File tree

8 files changed

+129
-34
lines changed

8 files changed

+129
-34
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/tac/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ memchr = { workspace = true }
2424
memmap2 = { workspace = true }
2525
regex = { workspace = true }
2626
clap = { workspace = true }
27-
uucore = { workspace = true }
27+
libc = { workspace = true }
28+
uucore = { workspace = true, features = ["signals"] }
2829
thiserror = { workspace = true }
2930
fluent = { workspace = true }
3031

src/uu/tac/src/tac.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// file that was distributed with this source code.
55

66
// spell-checker:ignore (ToDO) sbytes slen dlen memmem memmap Mmap mmap SIGBUS
7+
uucore::init_stdio_state_capture!();
8+
79
mod error;
810

911
use clap::{Arg, ArgAction, Command};
@@ -15,8 +17,7 @@ use std::{
1517
fs::{File, read},
1618
path::Path,
1719
};
18-
use uucore::error::UError;
19-
use uucore::error::UResult;
20+
use uucore::error::{UError, UResult, set_exit_code};
2021
use uucore::{format_usage, show};
2122

2223
use crate::error::TacError;
@@ -237,6 +238,17 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR
237238
let buf;
238239

239240
let data: &[u8] = if filename == "-" {
241+
#[cfg(unix)]
242+
if uucore::signals::stdin_was_closed() {
243+
let e: Box<dyn UError> = TacError::ReadError(
244+
OsString::from("-"),
245+
std::io::Error::from_raw_os_error(libc::EBADF),
246+
)
247+
.into();
248+
show!(e);
249+
set_exit_code(1);
250+
continue;
251+
}
240252
if let Some(mmap1) = try_mmap_stdin() {
241253
mmap = mmap1;
242254
&mmap

src/uu/tail/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ clap = { workspace = true }
2323
libc = { workspace = true }
2424
memchr = { workspace = true }
2525
notify = { workspace = true }
26-
uucore = { workspace = true, features = ["fs", "parser-size"] }
26+
uucore = { workspace = true, features = ["fs", "parser-size", "signals"] }
2727
same-file = { workspace = true }
2828
fluent = { workspace = true }
2929

src/uu/tail/src/paths.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,5 @@ pub fn path_is_tailable(path: &Path) -> bool {
230230

231231
#[inline]
232232
pub fn stdin_is_bad_fd() -> bool {
233-
// FIXME : Rust's stdlib is reopening fds as /dev/null
234-
// see also: https://github.com/uutils/coreutils/issues/2873
235-
// (gnu/tests/tail-2/follow-stdin.sh fails because of this)
236-
//#[cfg(unix)]
237-
{
238-
//platform::stdin_is_bad_fd()
239-
}
240-
//#[cfg(not(unix))]
241-
false
233+
uucore::signals::stdin_was_closed()
242234
}

src/uu/tail/src/tail.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ use std::fs::File;
3333
use std::io::{self, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout};
3434
use std::path::{Path, PathBuf};
3535
use uucore::display::Quotable;
36-
use uucore::error::{FromIo, UResult, USimpleError, get_exit_code, set_exit_code};
36+
use uucore::error::{FromIo, UResult, USimpleError, set_exit_code};
3737
use uucore::translate;
3838

3939
use uucore::{show, show_error};
4040

41+
uucore::init_stdio_state_capture!();
42+
4143
#[uucore::main]
4244
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
4345
// When we receive a SIGPIPE signal, we want to terminate the process so
@@ -105,10 +107,6 @@ fn uu_tail(settings: &Settings) -> UResult<()> {
105107
}
106108
}
107109

108-
if get_exit_code() > 0 && paths::stdin_is_bad_fd() {
109-
show_error!("{}: {}", text::DASH, translate!("tail-bad-fd"));
110-
}
111-
112110
Ok(())
113111
}
114112

@@ -227,6 +225,17 @@ fn tail_stdin(
227225
}
228226
}
229227

228+
// Check if stdin was closed before Rust reopened it as /dev/null
229+
if paths::stdin_is_bad_fd() {
230+
set_exit_code(1);
231+
show_error!(
232+
"{}",
233+
translate!("tail-error-cannot-fstat", "file" => translate!("tail-stdin-header").quote(), "error" => translate!("tail-bad-fd"))
234+
);
235+
show_error!("{}", translate!("tail-no-files-remaining"));
236+
return Ok(());
237+
}
238+
230239
match input.resolve() {
231240
// fifo
232241
Some(path) => {

src/uu/timeout/src/timeout.rs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
// file that was distributed with this source code.
55

66
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid
7+
uucore::init_stdio_state_capture!();
8+
79
mod status;
810

911
use crate::status::ExitStatus;
1012
use clap::{Arg, ArgAction, Command};
1113
use std::io::ErrorKind;
12-
use std::os::unix::process::ExitStatusExt;
14+
use std::os::unix::process::{CommandExt, ExitStatusExt};
1315
use std::process::{self, Child, Stdio};
1416
use std::sync::atomic::{self, AtomicBool};
1517
use std::time::Duration;
@@ -320,23 +322,34 @@ fn timeout(
320322
#[cfg(unix)]
321323
enable_pipe_errors()?;
322324

323-
let process = &mut process::Command::new(&cmd[0])
325+
let mut command = process::Command::new(&cmd[0]);
326+
command
324327
.args(&cmd[1..])
325328
.stdin(Stdio::inherit())
326329
.stdout(Stdio::inherit())
327-
.stderr(Stdio::inherit())
328-
.spawn()
329-
.map_err(|err| {
330-
let status_code = match err.kind() {
331-
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
332-
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
333-
_ => ExitStatus::CannotInvoke.into(),
334-
};
335-
USimpleError::new(
336-
status_code,
337-
translate!("timeout-error-failed-to-execute-process", "error" => err),
338-
)
339-
})?;
330+
.stderr(Stdio::inherit());
331+
332+
// If stdin was closed before Rust reopened it as /dev/null, close it in child
333+
if uucore::signals::stdin_was_closed() {
334+
unsafe {
335+
command.pre_exec(|| {
336+
libc::close(libc::STDIN_FILENO);
337+
Ok(())
338+
});
339+
}
340+
}
341+
342+
let process = &mut command.spawn().map_err(|err| {
343+
let status_code = match err.kind() {
344+
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
345+
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
346+
_ => ExitStatus::CannotInvoke.into(),
347+
};
348+
USimpleError::new(
349+
status_code,
350+
translate!("timeout-error-failed-to-execute-process", "error" => err),
351+
)
352+
})?;
340353
unblock_sigchld();
341354
catch_sigterm();
342355
// Wait for the child process for the specified time period.

src/uucore/src/lib/features/signals.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp
6+
// spell-checker:ignore (vars/api) fcntl setrlimit setitimer rubout pollable sysconf pgrp GETFD
77
// spell-checker:ignore (vars/signals) ABRT ALRM CHLD SEGV SIGABRT SIGALRM SIGBUS SIGCHLD SIGCONT SIGDANGER SIGEMT SIGFPE SIGHUP SIGILL SIGINFO SIGINT SIGIO SIGIOT SIGKILL SIGMIGRATE SIGMSG SIGPIPE SIGPRE SIGPROF SIGPWR SIGQUIT SIGSEGV SIGSTOP SIGSYS SIGTALRM SIGTERM SIGTRAP SIGTSTP SIGTHR SIGTTIN SIGTTOU SIGURG SIGUSR SIGVIRT SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ STKFLT PWR THR TSTP TTIN TTOU VIRT VTALRM XCPU XFSZ SIGCLD SIGPOLL SIGWAITING SIGAIOCANCEL SIGLWP SIGFREEZE SIGTHAW SIGCANCEL SIGLOST SIGXRES SIGJVM SIGRTMIN SIGRT SIGRTMAX TALRM AIOCANCEL XRES RTMIN RTMAX LTOSTOP
88

99
//! This module provides a way to handle signals in a platform-independent way.
@@ -426,6 +426,73 @@ pub fn ignore_interrupts() -> Result<(), Errno> {
426426
unsafe { signal(SIGINT, SigIgn) }.map(|_| ())
427427
}
428428

429+
// Detect closed stdin/stdout before Rust reopens them as /dev/null (see issue #2873)
430+
#[cfg(unix)]
431+
use std::sync::atomic::{AtomicBool, Ordering};
432+
433+
#[cfg(unix)]
434+
static STDIN_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
435+
#[cfg(unix)]
436+
static STDOUT_WAS_CLOSED: AtomicBool = AtomicBool::new(false);
437+
438+
#[cfg(unix)]
439+
#[allow(clippy::missing_safety_doc)]
440+
pub unsafe extern "C" fn capture_stdio_state() {
441+
use nix::libc;
442+
unsafe {
443+
STDIN_WAS_CLOSED.store(
444+
libc::fcntl(libc::STDIN_FILENO, libc::F_GETFD) == -1,
445+
Ordering::Relaxed,
446+
);
447+
STDOUT_WAS_CLOSED.store(
448+
libc::fcntl(libc::STDOUT_FILENO, libc::F_GETFD) == -1,
449+
Ordering::Relaxed,
450+
);
451+
}
452+
}
453+
454+
#[macro_export]
455+
#[cfg(unix)]
456+
macro_rules! init_stdio_state_capture {
457+
() => {
458+
#[cfg(not(target_os = "macos"))]
459+
#[used]
460+
#[unsafe(link_section = ".init_array")]
461+
static CAPTURE_STDIO_STATE: unsafe extern "C" fn() = $crate::signals::capture_stdio_state;
462+
463+
#[cfg(target_os = "macos")]
464+
#[used]
465+
#[unsafe(link_section = "__DATA,__mod_init_func")]
466+
static CAPTURE_STDIO_STATE: unsafe extern "C" fn() = $crate::signals::capture_stdio_state;
467+
};
468+
}
469+
470+
#[macro_export]
471+
#[cfg(not(unix))]
472+
macro_rules! init_stdio_state_capture {
473+
() => {};
474+
}
475+
476+
#[cfg(unix)]
477+
pub fn stdin_was_closed() -> bool {
478+
STDIN_WAS_CLOSED.load(Ordering::Relaxed)
479+
}
480+
481+
#[cfg(not(unix))]
482+
pub const fn stdin_was_closed() -> bool {
483+
false
484+
}
485+
486+
#[cfg(unix)]
487+
pub fn stdout_was_closed() -> bool {
488+
STDOUT_WAS_CLOSED.load(Ordering::Relaxed)
489+
}
490+
491+
#[cfg(not(unix))]
492+
pub const fn stdout_was_closed() -> bool {
493+
false
494+
}
495+
429496
#[test]
430497
fn signal_by_value() {
431498
assert_eq!(signal_by_name_or_value("0"), Some(0));

0 commit comments

Comments
 (0)