Add setsid implementation
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -892,6 +892,7 @@ dependencies = [
|
|||||||
"uu_lsmem",
|
"uu_lsmem",
|
||||||
"uu_mountpoint",
|
"uu_mountpoint",
|
||||||
"uu_rev",
|
"uu_rev",
|
||||||
|
"uu_setsid",
|
||||||
"uucore",
|
"uucore",
|
||||||
"xattr",
|
"xattr",
|
||||||
]
|
]
|
||||||
@@ -950,6 +951,15 @@ dependencies = [
|
|||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uu_setsid"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"libc",
|
||||||
|
"uucore",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uucore"
|
name = "uucore"
|
||||||
version = "0.0.27"
|
version = "0.0.27"
|
||||||
|
@@ -31,6 +31,7 @@ feat_common_core = [
|
|||||||
"lsmem",
|
"lsmem",
|
||||||
"ctrlaltdel",
|
"ctrlaltdel",
|
||||||
"rev",
|
"rev",
|
||||||
|
"setsid",
|
||||||
"last"
|
"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" }
|
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" }
|
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" }
|
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" }
|
last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
16
src/uu/setsid/Cargo.toml
Normal file
16
src/uu/setsid/Cargo.toml
Normal file
@@ -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"
|
7
src/uu/setsid/setsid.md
Normal file
7
src/uu/setsid/setsid.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# setsid
|
||||||
|
|
||||||
|
```
|
||||||
|
setsid [options] <program> [argument ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a program in a new session.
|
1
src/uu/setsid/src/main.rs
Normal file
1
src/uu/setsid/src/main.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uucore::bin!(uu_setsid);
|
199
src/uu/setsid/src/setsid.rs
Normal file
199
src/uu/setsid/src/setsid.rs
Normal file
@@ -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::<OsString>("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..),
|
||||||
|
)
|
||||||
|
}
|
174
tests/by-util/test_setsid.rs
Normal file
174
tests/by-util/test_setsid.rs
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
@@ -25,6 +25,10 @@ mod test_ctrlaltdel;
|
|||||||
#[path = "by-util/test_rev.rs"]
|
#[path = "by-util/test_rev.rs"]
|
||||||
mod test_rev;
|
mod test_rev;
|
||||||
|
|
||||||
|
#[cfg(feature = "setsid")]
|
||||||
|
#[path = "by-util/test_setsid.rs"]
|
||||||
|
mod test_setsid;
|
||||||
|
|
||||||
#[cfg(feature = "last")]
|
#[cfg(feature = "last")]
|
||||||
#[path = "by-util/test_last.rs"]
|
#[path = "by-util/test_last.rs"]
|
||||||
mod test_last;
|
mod test_last;
|
||||||
|
Reference in New Issue
Block a user