diff --git a/src/uu/nohup/src/platform/unix.rs b/src/uu/nohup/src/platform/unix.rs index daefc83ae86..2f6f890a638 100644 --- a/src/uu/nohup/src/platform/unix.rs +++ b/src/uu/nohup/src/platform/unix.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (ToDO) SIGHUP cproc vprocmgr homeout -use std::fs::{File, OpenOptions}; +use std::fs::OpenOptions; use std::io::{Error, IsTerminal as _}; use std::os::unix::{fs::OpenOptionsExt as _, process::CommandExt as _}; use std::process::Command; @@ -69,7 +69,12 @@ pub(crate) fn set_output_file_mode(opt: &mut OpenOptions) { fn replace_fds() -> UResult<()> { use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout, stdout}; if std::io::stdin().is_terminal() { - let new_stdin = File::open(std::path::Path::new("/dev/null")) + // Open /dev/null write-only so the substitute stdin is unreadable, as + // GNU does: a command that mistakenly reads from it gets an error + // instead of a silent EOF. + let new_stdin = OpenOptions::new() + .write(true) + .open("/dev/null") .map_err(|e| PlatformError::CannotReplace("STDIN", e))?; dup2_stdin(&new_stdin).map_err(|e| PlatformError::CannotReplace("STDIN", e.into()))?; } diff --git a/tests/by-util/test_nohup.rs b/tests/by-util/test_nohup.rs index 1500ad91c71..33b39e59c53 100644 --- a/tests/by-util/test_nohup.rs +++ b/tests/by-util/test_nohup.rs @@ -83,6 +83,32 @@ fn test_nohup_with_pseudo_terminal_emulation_on_stdin_stdout_stderr_get_replaced // When stdin is not a TTY (e.g., a pipe), nohup preserves it. // This behavior is already tested indirectly through other tests. +// When stdin is a terminal, the replacement must be unreadable so that a +// command which mistakenly reads from it gets an error instead of a silent +// EOF (GNU opens /dev/null write-only). Since nohup execs the command, the +// exit status is the command's own. +#[test] +#[cfg(unix)] +fn test_nohup_replaced_stdin_is_not_readable() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + ts.ucmd() + .terminal_simulation(true) + .arg("cat") + .fails_with_code(1) + .stderr_contains("nohup: ignoring input and appending output to 'nohup.out'"); + + sleep(std::time::Duration::from_millis(10)); + + // cat's error message goes to stderr, which nohup redirected into nohup.out + let content = std::fs::read_to_string(at.plus_as_string("nohup.out")).unwrap(); + assert!( + content.contains("Bad file descriptor"), + "expected a read error from cat in nohup.out, got: {content:?}" + ); +} + // Test that nohup creates nohup.out in current directory #[test] #[cfg(any(