Skip to content

Commit

Permalink
tests: add integration tests running systemd-run
Browse files Browse the repository at this point in the history
  • Loading branch information
desbma committed Jan 31, 2025
1 parent cc6fe8a commit b59c63d
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ fastrand = { version = "2.3.0", default-features = false, features = ["std"] }
nix = { version = "0.29.0", default-features = false, features = ["user"] }
predicates = { version = "3.1.3", default-features = false, features = ["color"] }
pretty_assertions = { version = "1.4.1", default-features = false, features = ["std"] }
shlex = { version = "1.3.0", default-features = false, features = ["std"] }

[features]
default = []
Expand Down
290 changes: 290 additions & 0 deletions tests/systemd-run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
//! These tests run generated options with systemd-run to ensure they are valid
//! and allow the program to execute normally
#![expect(
clippy::tests_outside_test_module,
clippy::unwrap_used,
clippy::shadow_unrelated
)]

use std::{fs, io::BufRead as _, os::unix::fs::FileTypeExt as _, sync::LazyLock};

use assert_cmd::{
assert::{Assert, OutputAssertExt as _},
Command,
};
use predicates::prelude::predicate;

static ALL_SHH_RUN_OPTS: LazyLock<Vec<Vec<&'static str>>> = LazyLock::new(all_shh_run_opts);

/// Run `shh run` for a given command, and return generated systemd options
fn generate_options(cmd: &[&str], run_opts: &[&str]) -> Vec<String> {
const START_OPTION_OUTPUT_SNIPPET: &str =
"-------- Start of suggested service options --------";
const END_OPTION_OUTPUT_SNIPPET: &str = "-------- End of suggested service options --------";
let output = Command::cargo_bin("shh")
.unwrap()
.arg("run")
.args(run_opts)
.arg("--")
.args(cmd)
.unwrap();
let opts = output
.stdout
.clone()
.lines()
// Filter out delimiting lines while letting errors bubble up
.skip_while(|r| {
r.as_ref()
.is_ok_and(|l| !l.starts_with(START_OPTION_OUTPUT_SNIPPET))
})
.skip(1)
.take_while(|r| {
r.as_ref().is_err()
|| r.as_ref()
.is_ok_and(|l| !l.starts_with(END_OPTION_OUTPUT_SNIPPET))
})
.collect::<Result<_, _>>()
.unwrap();
output.assert().success();
opts
}

/// Run systemd-run for given command, with options
fn systemd_run(cmd: &[&str], sd_opts: &[String]) -> Assert {
// TODO why do we need sudo to get output, even when already running as root?
let mut sd_cmd = vec!["sudo", "systemd-run", "-P", "-G", "--wait"];
for sd_opt in sd_opts {
// Some options are supported in systemd unit files but not by systemd-run, work around that
let sd_opt = match sd_opt.as_str() {
// https://github.com/systemd/systemd/issues/36222#issuecomment-2623967515
"PrivateTmp=disconnected" => "PrivateTmpEx=disconnected",
"RestrictAddressFamilies=none" => "RestrictAddressFamilies=",
s => s,
};
sd_cmd.extend(["-p", sd_opt]);
}
sd_cmd.push("--");
sd_cmd.extend(cmd);
eprintln!("{}", shlex::try_join(sd_cmd.clone()).unwrap());
Command::new(sd_cmd[0])
.args(sd_cmd)
.unwrap()
.assert()
.success()
}

/// Generate all combinations of `shh run args` to test
fn all_shh_run_opts() -> Vec<Vec<&'static str>> {
let args_mode = vec![vec![], vec!["-m", "aggressive"]];
let args_fs = vec![
vec![],
vec!["-w"],
vec!["-w", "--merge-paths-threshold", "1"],
vec!["-w", "--merge-paths-threshold", "2"],
vec!["-w", "--merge-paths-threshold", "10"],
vec!["-w", "--merge-paths-threshold", "100"],
];
let args_fw = vec![vec![], vec!["-f"]];
let mut combinations = Vec::with_capacity(args_mode.len() * args_fs.len() * args_fw.len());
for arg_mode in &args_mode {
for arg_fs in &args_fs {
for arg_fw in &args_fw {
let mut args = Vec::with_capacity(arg_mode.len() + arg_fs.len() + arg_fw.len());
args.extend(arg_mode);
args.extend(arg_fs);
args.extend(arg_fw);
combinations.push(args);
}
}
}
combinations
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_true() {
let cmd = ["true"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_write_dev_null() {
let cmd = ["sh", "-c", ": > /dev/null"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_ls_dev() {
let cmd = ["ls", "/dev/"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let asrt = systemd_run(&cmd, &sd_opts);
asrt.stdout(predicate::str::contains("block"))
.stdout(predicate::str::contains("char"))
.stdout(predicate::str::contains("log"));
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_ls_proc() {
let cmd = ["ls", "/proc/1"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_read_kallsyms() {
let cmd = ["head", "/proc/kallsyms"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_ls_modules() {
let cmd = ["ls", "/usr/lib/modules/"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_dmesg() {
let cmd = ["dmesg"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let asrt = systemd_run(&cmd, &sd_opts);
asrt.stdout(predicate::str::contains("0.000000] Linux version"));
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_systemctl() {
let cmd = ["systemctl"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_ss() {
let cmd = ["ss", "-l"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_mmap_wx() {
let cmd = ["python3", "-c", "import mmap, os, tempfile; f = tempfile.NamedTemporaryFile(\"wb\"); f.write(os.urandom(16)); f.flush(); mmap.mmap(f.file.fileno(), 0, prot=mmap.PROT_WRITE|mmap.PROT_EXEC)"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_sched_realtime() {
let cmd = ["python3", "-c", "import os; os.sched_setscheduler(0, os.SCHED_RR, os.sched_param(os.sched_get_priority_min(os.SCHED_RR)))"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_bind() {
let cmd = ["python3", "-c", "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind((\"127.0.0.1\", 1234))"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_sock_packet() {
let cmd = [
"python3",
"-c",
"import socket; socket.socket(socket.AF_PACKET, socket.SOCK_RAW)",
];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let _ = systemd_run(&cmd, &sd_opts);
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_syslog() {
let cmd = ["dmesg", "-S"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let sd_opts = generate_options(&cmd, shh_opts);
let asrt = systemd_run(&cmd, &sd_opts);
asrt.stdout(predicate::str::contains("0.000000] Linux version"));
}
}

#[test]
#[cfg_attr(not(feature = "as-root"), ignore)]
fn systemd_run_mknod() {
let tmp_dir = tempfile::tempdir().unwrap();

let pipe_path = tmp_dir.path().join("pipe");
let cmd = ["mknod", pipe_path.as_os_str().to_str().unwrap(), "p"];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let mut sd_opts = generate_options(&cmd, shh_opts);
let _ = fs::remove_file(&pipe_path);

sd_opts.push(format!("BindPaths={}", tmp_dir.path().to_str().unwrap()));
let _ = systemd_run(&cmd, &sd_opts);
assert!(fs::metadata(&pipe_path).unwrap().file_type().is_fifo());
fs::remove_file(&pipe_path).unwrap();
}

let dev_path = tmp_dir.path().join("dev");
let cmd = [
"mknod",
dev_path.as_os_str().to_str().unwrap(),
"b",
"255",
"255",
];
for shh_opts in &*ALL_SHH_RUN_OPTS {
let mut sd_opts = generate_options(&cmd, shh_opts);
let _ = fs::remove_file(&dev_path);

sd_opts.push(format!("BindPaths={}", tmp_dir.path().to_str().unwrap()));
let _ = systemd_run(&cmd, &sd_opts);
assert!(fs::metadata(&dev_path)
.unwrap()
.file_type()
.is_block_device());
fs::remove_file(&dev_path).unwrap();
}
}

0 comments on commit b59c63d

Please sign in to comment.