diff --git a/Cargo.lock b/Cargo.lock index 277321cd990..4c0a680e8a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3602,6 +3602,7 @@ dependencies = [ "clap", "fluent", "libc", + "nix", "thiserror 2.0.17", "uucore", ] diff --git a/src/uu/nohup/Cargo.toml b/src/uu/nohup/Cargo.toml index 50eb258a9f8..6416f3405ec 100644 --- a/src/uu/nohup/Cargo.toml +++ b/src/uu/nohup/Cargo.toml @@ -20,6 +20,7 @@ path = "src/nohup.rs" [dependencies] clap = { workspace = true } libc = { workspace = true } +nix = { workspace = true, features = ["fs"] } uucore = { workspace = true, features = ["fs"] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 6280d44e1e3..9f51e1230ef 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -7,9 +7,11 @@ use clap::{Arg, ArgAction, Command}; use libc::{SIG_IGN, SIGHUP, dup2, signal}; +use nix::sys::stat::{Mode, umask}; use std::env; use std::fs::{File, OpenOptions}; use std::io::{Error, ErrorKind, IsTerminal}; +use std::os::unix::fs::OpenOptionsExt; use std::os::unix::prelude::*; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; @@ -134,14 +136,28 @@ fn replace_fds() -> UResult<()> { Ok(()) } -fn find_stdout() -> UResult { - let internal_failure_code = failure_code(); +/// Open nohup.out file with mode 0o600, temporarily clearing umask. +/// The umask is cleared to ensure the file is created with exactly 0o600 permissions. +fn open_nohup_file(path: &Path) -> std::io::Result { + // Clear umask (set it to 0) and save the old value + let old_umask = umask(Mode::from_bits_truncate(0)); - match OpenOptions::new() + let result = OpenOptions::new() .create(true) .append(true) - .open(Path::new(NOHUP_OUT)) - { + .mode(0o600) + .open(path); + + // Restore previous umask + umask(old_umask); + + result +} + +fn find_stdout() -> UResult { + let internal_failure_code = failure_code(); + + match open_nohup_file(Path::new(NOHUP_OUT)) { Ok(t) => { show_error!( "{}", @@ -156,7 +172,7 @@ fn find_stdout() -> UResult { let mut homeout = PathBuf::from(home); homeout.push(NOHUP_OUT); let homeout_str = homeout.to_str().unwrap(); - match OpenOptions::new().create(true).append(true).open(&homeout) { + match open_nohup_file(&homeout) { Ok(t) => { show_error!( "{}", diff --git a/tests/by-util/test_nohup.rs b/tests/by-util/test_nohup.rs index f3fa0bc948c..4c3e711da95 100644 --- a/tests/by-util/test_nohup.rs +++ b/tests/by-util/test_nohup.rs @@ -3,9 +3,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore winsize Openpty openpty xpixel ypixel ptyprocess +use std::os::unix::fs::PermissionsExt; use std::thread::sleep; use uutests::at_and_ucmd; use uutests::new_ucmd; +use uutests::util::TerminalSimulation; use uutests::util::TestScenario; use uutests::util_name; @@ -247,3 +249,28 @@ fn test_nohup_stderr_to_stdout() { assert!(content.contains("stdout message")); assert!(content.contains("stderr message")); } + +#[test] +fn test_nohup_file_permissions_ignore_umask_always_o600() { + for umask_val in [0o077, 0o000] { + let ts = TestScenario::new(util_name!()); + ts.ucmd() + .terminal_sim_stdio(TerminalSimulation { + stdin: true, + stdout: true, + stderr: true, + size: None, + }) + .umask(umask_val) + .args(&["echo", "test"]) + .succeeds(); + + sleep(std::time::Duration::from_millis(10)); + let mode = std::fs::metadata(ts.fixtures.plus_as_string("nohup.out")) + .unwrap() + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600, "with umask {umask_val:o}, got mode {mode:o}"); + } +}