diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 6280d44e1e3..8c35e6d14d9 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -63,6 +63,14 @@ fn failure_code() -> i32 { } } +fn open_nohup_file(path: impl AsRef) -> std::io::Result { + OpenOptions::new() + .create(true) + .append(true) + .mode(0o600) + .open(path) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result_with_exit_code( @@ -137,11 +145,7 @@ fn replace_fds() -> UResult<()> { fn find_stdout() -> UResult { let internal_failure_code = failure_code(); - match OpenOptions::new() - .create(true) - .append(true) - .open(Path::new(NOHUP_OUT)) - { + match open_nohup_file(NOHUP_OUT) { Ok(t) => { show_error!( "{}", @@ -156,7 +160,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..36c9f170dbe 100644 --- a/tests/by-util/test_nohup.rs +++ b/tests/by-util/test_nohup.rs @@ -247,3 +247,97 @@ fn test_nohup_stderr_to_stdout() { assert!(content.contains("stdout message")); assert!(content.contains("stderr message")); } + +// Test nohup.out has 0600 permissions +#[test] +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "openbsd", + target_vendor = "apple" +))] +fn test_nohup_output_permissions() { + use std::os::unix::fs::PermissionsExt; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + ts.ucmd() + .terminal_simulation(true) + .args(&["echo", "perms"]) + .succeeds(); + + sleep(std::time::Duration::from_millis(10)); + + let metadata = std::fs::metadata(at.plus("nohup.out")).unwrap(); + let mode = metadata.permissions().mode(); + + assert_eq!( + mode & 0o777, + 0o600, + "nohup.out should have 0600 permissions" + ); +} + +// Test that the fallback nohup.out (in $HOME) also has 0600 permissions +#[test] +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "openbsd" +))] +fn test_nohup_fallback_output_permissions() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + // Skip if root + if unsafe { libc::geteuid() } == 0 { + println!("Skipping test when running as root"); + return; + } + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // Create a fake HOME directory + at.mkdir("home"); + let home_dir_str = at.plus_as_string("home"); + + // Create a read-only directory + at.mkdir("readonly_dir"); + let readonly_path = at.plus("readonly_dir"); + + // Make directory read-only + let mut perms = fs::metadata(&readonly_path).unwrap().permissions(); + perms.set_mode(0o555); + fs::set_permissions(&readonly_path, perms).unwrap(); + + // Run nohup inside the read-only dir + // This forces it to fail writing to CWD and fall back to custom HOME + ts.ucmd() + .env("HOME", &home_dir_str) + .current_dir(&readonly_path) + .terminal_simulation(true) + .arg("true") + .run(); + + // Restore permissions so the test runner can delete the folder later! + let mut perms = fs::metadata(&readonly_path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&readonly_path, perms).unwrap(); + + sleep(std::time::Duration::from_millis(50)); + + // Verify the file exists in HOME and has 0600 permissions + let home_nohup = at.plus("home/nohup.out"); + let metadata = fs::metadata(home_nohup).expect("nohup.out should have been created in HOME"); + let mode = metadata.permissions().mode(); + + assert_eq!( + mode & 0o777, + 0o600, + "Fallback nohup.out should have 0600 permissions" + ); +}