Skip to content

Commit e0bafa3

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

File tree

8 files changed

+132
-34
lines changed

8 files changed

+132
-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: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
// file that was distributed with this source code.
55

66
// spell-checker:ignore (ToDO) sbytes slen dlen memmem memmap Mmap mmap SIGBUS
7+
#[cfg(unix)]
8+
uucore::init_stdio_state_capture!();
9+
710
mod error;
811

912
use clap::{Arg, ArgAction, Command};
@@ -15,8 +18,7 @@ use std::{
1518
fs::{File, read},
1619
path::Path,
1720
};
18-
use uucore::error::UError;
19-
use uucore::error::UResult;
21+
use uucore::error::{UError, UResult, set_exit_code};
2022
use uucore::{format_usage, show};
2123

2224
use crate::error::TacError;
@@ -237,6 +239,17 @@ fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UR
237239
let buf;
238240

239241
let data: &[u8] = if filename == "-" {
242+
#[cfg(unix)]
243+
if uucore::signals::stdin_was_closed() {
244+
let e: Box<dyn UError> = TacError::ReadError(
245+
OsString::from("-"),
246+
std::io::Error::from_raw_os_error(libc::EBADF),
247+
)
248+
.into();
249+
show!(e);
250+
set_exit_code(1);
251+
continue;
252+
}
240253
if let Some(mmap1) = try_mmap_stdin() {
241254
mmap = mmap1;
242255
&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: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ 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+
#[cfg(unix)]
42+
uucore::init_stdio_state_capture!();
43+
4144
#[uucore::main]
4245
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
4346
// When we receive a SIGPIPE signal, we want to terminate the process so
@@ -105,10 +108,6 @@ fn uu_tail(settings: &Settings) -> UResult<()> {
105108
}
106109
}
107110

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

@@ -227,6 +226,17 @@ fn tail_stdin(
227226
}
228227
}
229228

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

src/uu/timeout/src/timeout.rs

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

66
// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid
7+
#[cfg(unix)]
8+
uucore::init_stdio_state_capture!();
9+
710
mod status;
811

912
use crate::status::ExitStatus;
1013
use clap::{Arg, ArgAction, Command};
1114
use std::io::ErrorKind;
12-
use std::os::unix::process::ExitStatusExt;
15+
use std::os::unix::process::{CommandExt, ExitStatusExt};
1316
use std::process::{self, Child, Stdio};
1417
use std::sync::atomic::{self, AtomicBool};
1518
use std::time::Duration;
@@ -320,23 +323,34 @@ fn timeout(
320323
#[cfg(unix)]
321324
enable_pipe_errors()?;
322325

323-
let process = &mut process::Command::new(&cmd[0])
326+
let mut command = process::Command::new(&cmd[0]);
327+
command
324328
.args(&cmd[1..])
325329
.stdin(Stdio::inherit())
326330
.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-
})?;
331+
.stderr(Stdio::inherit());
332+
333+
// If stdin was closed before Rust reopened it as /dev/null, close it in child
334+
if uucore::signals::stdin_was_closed() {
335+
unsafe {
336+
command.pre_exec(|| {
337+
libc::close(libc::STDIN_FILENO);
338+
Ok(())
339+
});
340+
}
341+
}
342+
343+
let process = &mut command.spawn().map_err(|err| {
344+
let status_code = match err.kind() {
345+
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
346+
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
347+
_ => ExitStatus::CannotInvoke.into(),
348+
};
349+
USimpleError::new(
350+
status_code,
351+
translate!("timeout-error-failed-to-execute-process", "error" => err),
352+
)
353+
})?;
340354
unblock_sigchld();
341355
catch_sigterm();
342356
// 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)