From 61756085fa4fdb06fd7a5f6c3aec9f3b1704c8fe Mon Sep 17 00:00:00 2001 From: Gustavo Noronha Silva Date: Wed, 2 Oct 2024 07:41:13 -0300 Subject: [PATCH] Add setsid implementation --- Cargo.lock | 10 ++ Cargo.toml | 2 + src/uu/setsid/Cargo.toml | 16 +++ src/uu/setsid/setsid.md | 7 ++ src/uu/setsid/src/main.rs | 1 + src/uu/setsid/src/setsid.rs | 199 +++++++++++++++++++++++++++++++++++ tests/by-util/test_setsid.rs | 174 ++++++++++++++++++++++++++++++ tests/tests.rs | 4 + 8 files changed, 413 insertions(+) create mode 100644 src/uu/setsid/Cargo.toml create mode 100644 src/uu/setsid/setsid.md create mode 100644 src/uu/setsid/src/main.rs create mode 100644 src/uu/setsid/src/setsid.rs create mode 100644 tests/by-util/test_setsid.rs diff --git a/Cargo.lock b/Cargo.lock index dccef6f..7974485 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,7 @@ dependencies = [ "uu_lsmem", "uu_mountpoint", "uu_rev", + "uu_setsid", "uucore", "xattr", ] @@ -950,6 +951,15 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_setsid" +version = "0.0.1" +dependencies = [ + "clap", + "libc", + "uucore", +] + [[package]] name = "uucore" version = "0.0.27" diff --git a/Cargo.toml b/Cargo.toml index 4ccd475..96da0ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ feat_common_core = [ "lsmem", "ctrlaltdel", "rev", + "setsid", "last" ] @@ -69,6 +70,7 @@ lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/ mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" } ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } +setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" } [dev-dependencies] diff --git a/src/uu/setsid/Cargo.toml b/src/uu/setsid/Cargo.toml new file mode 100644 index 0000000..9889d5c --- /dev/null +++ b/src/uu/setsid/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "uu_setsid" +version = "0.0.1" +edition = "2021" + +[dependencies] +uucore = { workspace = true } +libc = { workspace = true } +clap = { workspace = true } + +[lib] +path = "src/setsid.rs" + +[[bin]] +name = "setsid" +path = "src/main.rs" diff --git a/src/uu/setsid/setsid.md b/src/uu/setsid/setsid.md new file mode 100644 index 0000000..dbcb937 --- /dev/null +++ b/src/uu/setsid/setsid.md @@ -0,0 +1,7 @@ +# setsid + +``` +setsid [options] [argument ...] +``` + +Run a program in a new session. diff --git a/src/uu/setsid/src/main.rs b/src/uu/setsid/src/main.rs new file mode 100644 index 0000000..916937a --- /dev/null +++ b/src/uu/setsid/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_setsid); diff --git a/src/uu/setsid/src/setsid.rs b/src/uu/setsid/src/setsid.rs new file mode 100644 index 0000000..a4d99e5 --- /dev/null +++ b/src/uu/setsid/src/setsid.rs @@ -0,0 +1,199 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::builder::ValueParser; +use clap::{crate_version, Command}; +use clap::{Arg, ArgAction}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("setsid.md"); +const USAGE: &str = help_usage!("setsid.md"); + +#[cfg(target_family = "unix")] +mod unix { + pub use std::ffi::{OsStr, OsString}; + pub use std::os::unix::process::CommandExt; + pub use std::{io, process}; + pub use uucore::error::{FromIo, UIoError, UResult}; + + // The promise made by setsid(1) is that it forks if the process + // is already a group leader, not session leader. + pub fn already_group_leader() -> bool { + let leader = unsafe { libc::getpgrp() }; + leader == process::id() as i32 + } + + pub fn report_failure_to_exec(error: io::Error, executable: &OsStr, set_error: bool) { + let kind = error.kind(); + + // FIXME: POSIX wants certain exit statuses for specific errors, should + // these be handled by uucore::error? We should be able to just return + // the UError here. + uucore::show_error!( + "failed to execute {}: {}", + executable.to_string_lossy(), + UIoError::from(error) + ); + + if set_error { + if kind == io::ErrorKind::NotFound { + uucore::error::set_exit_code(127); + } else if kind == io::ErrorKind::PermissionDenied { + uucore::error::set_exit_code(126); + } + } + } + + // This function will be potentially called after a fork(), so what it can do + // is quite restricted. This is the meat of the program. + pub fn prepare_child(take_controlling_tty: bool) -> io::Result<()> { + // SAFETY: this is effectively a wrapper to the setsid syscall. + let pid = unsafe { libc::setsid() }; + + // We fork if we are already a group leader, so an error + // here should be impossible. + assert_eq!(pid, process::id() as i32); + + // On some platforms (i.e. aarch64 Linux) TIOCSCTTY is the same type as the second argument, + // but on some it is u64, while the expected type is u32. + // SAFETY: the ioctl should not make any changes to memory, basically a wrapper + // to the syscall. + #[allow(clippy::useless_conversion)] + if take_controlling_tty && unsafe { libc::ioctl(0, libc::TIOCSCTTY.into(), 1) } < 0 { + // This is unfortunate, but we are bound by the Result type pre_exec requires, + // as well as the limitations imposed by this being executed post-fork(). + // Ideally we would return an io::Error of the Other kind so that we could handle + // everything at the same place, but that would require an allocation. + uucore::show_error!( + "failed to set the controlling terminal: {}", + UIoError::from(io::Error::last_os_error()) + ); + + // SAFETY: this is actually safer than calling process::exit(), as that may + // allocate, which is not safe post-fork. + unsafe { libc::_exit(1) }; + } + Ok(()) + } + + pub fn spawn_command( + mut to_run: process::Command, + executable: &OsStr, + wait_child: bool, + ) -> UResult<()> { + let mut child = match to_run.spawn() { + Ok(child) => child, + Err(error) => { + report_failure_to_exec(error, executable, wait_child); + return Ok(()); + } + }; + + if !wait_child { + return Ok(()); + } + + match child.wait() { + Ok(status) => { + uucore::error::set_exit_code(status.code().unwrap()); + Ok(()) + } + Err(error) => { + Err(error.map_err_context(|| format!("failed to wait on PID {}", child.id()))) + } + } + } +} + +#[cfg(target_family = "unix")] +use unix::*; + +#[cfg(target_family = "unix")] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + let force_fork = matches.get_flag("fork"); + let wait_child = matches.get_flag("wait"); + let take_controlling_tty = matches.get_flag("ctty"); + + let command: Vec<_> = match matches.get_many::("command") { + Some(v) => v.collect(), + None => return Err(uucore::error::USimpleError::new(1, "no command specified")), + }; + + // We know we have at least one item, as none was + // handled as an error on the match above. + let executable = command[0]; + let arguments = command.get(1..).unwrap_or(&[]); + + let mut to_run = process::Command::new(executable); + to_run.args(arguments.iter()); + + // SAFETY: pre_exec() happens post-fork, so the process can potentially + // be in a broken state; allocations are not safe, and we should exit + // as soon as possible if we cannot go ahead. + unsafe { + to_run.pre_exec(move || prepare_child(take_controlling_tty)); + }; + + if force_fork || already_group_leader() { + spawn_command(to_run, executable, wait_child)?; + } else { + let error = to_run.exec(); + report_failure_to_exec(error, executable, true); + } + + Ok(()) +} + +#[cfg(not(target_family = "unix"))] +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + Err(uucore::error::USimpleError::new( + 1, + "`setsid` is unavailable on non-UNIX-like platforms.", + )) +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new("ctty") + .short('c') + .action(ArgAction::SetTrue) + .value_parser(ValueParser::bool()) + .help("Take the current controlling terminal"), + ) + .arg( + Arg::new("fork") + .short('f') + .action(ArgAction::SetTrue) + .value_parser(ValueParser::bool()) + .long_help("Always create a new process. By default this is only done if we are already a process group lead."), + ) + .arg( + Arg::new("wait") + .short('w') + .action(ArgAction::SetTrue) + .value_parser(ValueParser::bool()) + .help("Wait for the command to finish and exit with its exit code."), + ) + .arg( + Arg::new("command") + .help("Program to be executed, followed by its arguments") + .index(1) + .action(ArgAction::Set) + .trailing_var_arg(true) + .value_parser(ValueParser::os_string()) + .num_args(1..), + ) +} diff --git a/tests/by-util/test_setsid.rs b/tests/by-util/test_setsid.rs new file mode 100644 index 0000000..cf3d159 --- /dev/null +++ b/tests/by-util/test_setsid.rs @@ -0,0 +1,174 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(target_family = "unix")] +mod unix { + use crate::common::util::{TestScenario, UCommand, TESTS_BINARY}; + + #[test] + fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); + + new_ucmd!() + .arg("-f") + .fails() + .code_is(1) + .stderr_is("setsid: no command specified\n"); + } + + #[test] + fn fork_isolates_child_exit_code() { + new_ucmd!() + .arg("-f") + .arg("/usr/bin/false") + .succeeds() + .no_output(); + } + + #[test] + fn non_fork_returns_child_exit_code() { + new_ucmd!() + .arg("/usr/bin/false") + .fails() + .code_is(1) + .no_output(); + } + + #[test] + fn fork_wait_returns_child_exit_code() { + new_ucmd!() + .arg("-f") + .arg("-w") + .arg("/usr/bin/false") + .fails() + .code_is(1) + .no_output(); + } + + #[test] + fn non_fork_returns_not_found_error() { + new_ucmd!() + .arg("/usr/bin/this-tool-does-not-exist-hopefully") + .fails() + .code_is(127) + .stderr_is("setsid: failed to execute /usr/bin/this-tool-does-not-exist-hopefully: No such file or directory\n"); + } + + #[test] + fn non_fork_on_non_executable_returns_permission_denied_error() { + new_ucmd!() + .arg("/etc/passwd") + .fails() + .code_is(126) + .stderr_is("setsid: failed to execute /etc/passwd: Permission denied\n"); + } + + #[test] + fn fork_isolates_not_found_error() { + new_ucmd!() + .arg("-f") + .arg("/usr/bin/this-tool-does-not-exist-hopefully") + .succeeds(); + // no test for output, as it's a race whether the not found error gets printed + // quickly enough, potential flakyness + } + + #[test] + fn unprivileged_user_cannot_steal_controlling_tty() { + let shell_cmd = + format!("{TESTS_BINARY} setsid -w -c {TESTS_BINARY} setsid -w -c /b/usrin/true"); + UCommand::new() + .terminal_simulation(true) + .arg(&shell_cmd) + .fails() + .code_is(1) + .no_stdout() + .stderr_is("setsid: failed to set the controlling terminal: Permission denied\r\n"); + } + + #[cfg(target_os = "linux")] + #[test] + fn unprivileged_user_can_take_new_controlling_tty() { + let shell_cmd = format!( + "/usr/bin/cat /proc/self/stat; {TESTS_BINARY} setsid -w -c /usr/bin/cat /proc/self/stat" + ); + + let cmd_result = UCommand::new() + .terminal_simulation(true) + .arg(&shell_cmd) + .succeeds(); + + let output = cmd_result.code_is(0).no_stderr().stdout_str(); + + // /proc/self/stat format has controlling terminal as the 7th + // space-separated item; if we managed to change the controlling + // terminal, we should see a difference there + let (before, after) = output + .split_once('\n') + .expect("expected 2 lines of output at least"); + let before = before + .split_whitespace() + .nth(6) + .expect("unexpected stat format"); + let after = after + .split_whitespace() + .nth(6) + .expect("unexpected stat format"); + + assert_ne!(before, after); + } + + #[cfg(target_os = "linux")] + #[test] + fn setsid_takes_session_leadership() { + let shell_cmd = format!( + "/usr/bin/cat /proc/self/stat; {TESTS_BINARY} setsid /usr/bin/cat /proc/self/stat" + ); + + let cmd_result = UCommand::new() + .terminal_simulation(true) + .arg(&shell_cmd) + .run(); + + let output = cmd_result.code_is(0).no_stderr().stdout_str(); + + // /proc/self/stat format has sessiion ID as the 6th space-separated + // item; if we managed to get session leadership, we should see a + // difference there... + let (before, after) = output + .split_once('\n') + .expect("expected 2 lines of output at least"); + let before = before + .split_whitespace() + .nth(5) + .expect("unexpected stat format"); + let after = after + .split_whitespace() + .nth(5) + .expect("unexpected stat format"); + + assert_ne!(before, after); + + // ...and it should actually be the PID of our child! We take the child + // PID here to avoid differences in handling by different shells or + // distributions. + let pid = after.split_whitespace().next().unwrap(); + assert_eq!(after, pid); + } +} + +#[cfg(not(target_family = "unix"))] +mod non_unix { + use crate::common::util::{TestScenario, UCommand}; + + #[test] + fn unsupported_platforms() { + new_ucmd!() + .arg("/usr/bin/true") + .fails() + .code_is(1) + .stderr_is("setsid: `setsid` is unavailable on non-UNIX-like platforms.\n"); + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 57e9513..64cfc84 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -25,6 +25,10 @@ mod test_ctrlaltdel; #[path = "by-util/test_rev.rs"] mod test_rev; +#[cfg(feature = "setsid")] +#[path = "by-util/test_setsid.rs"] +mod test_setsid; + #[cfg(feature = "last")] #[path = "by-util/test_last.rs"] mod test_last;