Skip to content

Commit

Permalink
feat: support services with multiple ExecStartPre/ExecStart/ExecStart…
Browse files Browse the repository at this point in the history
…Post directives (WIP)
  • Loading branch information
desbma committed Dec 6, 2023
1 parent e549755 commit 8bfffee
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 25 deletions.
54 changes: 54 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 @@ -18,6 +18,7 @@ itertools = { version = "0.11.0", default-features = false, features = ["use_std
lazy_static = { version = "1.4.0", default-features = false }
log = { version = "0.4.19", default-features = false, features = ["max_level_trace", "release_max_level_info"] }
nix = { version = "0.26.2", default-features = false, features = ["fs"] }
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
regex = { version = "1.9.1", default-features = false, features = ["std", "perf"] }
signal-hook = { version = "0.3.17", default-features = false, features = ["iterator"] }
simple_logger = { version = "4.2.0", default-features = false, features = ["colors", "stderr"] }
Expand Down
13 changes: 13 additions & 0 deletions src/cl.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Command line interface
use std::path::PathBuf;

use clap::Parser;

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -29,6 +31,17 @@ pub enum Action {
/// How hard we should harden
#[arg(short, long, default_value_t, value_enum)]
mode: HardeningMode,
/// Generate profile data file to be merged with others instead of generating systemd options directly
#[arg(short, long, default_value = None)]
profile_data_path: Option<PathBuf>,
},
/// Merge profile data from previous runs to generate systemd options
MergeProfileData {
/// How hard we should harden
#[arg(short, long, default_value_t, value_enum)]
mode: HardeningMode,
/// Profile data paths
paths: Vec<PathBuf>,
},
/// Act on a systemd service unit
#[clap(subcommand)]
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ fn main() -> anyhow::Result<()> {

// Handle CL args
match args.action {
cl::Action::Run { command, mode } => {
cl::Action::Run {
command,
mode,
profile_data_path,
} => {
// Build supported systemd options
let sd_opts = sd_options(&sd_version, &kernel_version, &mode)?;

Expand Down Expand Up @@ -74,6 +78,7 @@ fn main() -> anyhow::Result<()> {
// Report
systemd::report_options(resolved_opts);
}
cl::Action::MergeProfileData { mode, paths } => todo!(),
cl::Action::Service(cl::ServiceAction::StartProfile {
service,
mode,
Expand Down
64 changes: 40 additions & 24 deletions src/systemd/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

use itertools::Itertools;
use rand::Rng;

use crate::cl::HardeningMode;
use crate::systemd::{
Expand Down Expand Up @@ -61,29 +62,12 @@ impl Service {
"Hardening config already exists at {harden_fragment_path:?} and may conflict with profiling"
);

// Check we have no ExecStartPre/ExecStartPost
// Currently we don't profile those, so we'd rather throw an explicit error
let config_paths_bufs = self.config_paths()?;
let config_paths = config_paths_bufs
.iter()
.map(|p| p.as_path())
.collect::<Vec<_>>();
if !Self::config_vals("ExecStartPre", &config_paths)?.is_empty()
|| !Self::config_vals("ExecStartPost", &config_paths)?.is_empty()
{
anyhow::bail!("Services with ExecStartPre/ExecStartPost are not supported")
}

// Read current service startup command
log::info!("Located unit config file(s): {config_paths:?}");
let cmd = Self::config_vals("ExecStart", &config_paths)?
.into_iter()
.at_most_one()
.map_err(|_| {
anyhow::anyhow!("Services with multiple ExecStart directives are not supported")
})?
.ok_or_else(|| anyhow::anyhow!("Unable to get service ExecStart command"))?;
log::info!("Startup command: {cmd}");

// Write new fragment
fs::create_dir_all(fragment_path.parent().unwrap())?;
Expand All @@ -104,17 +88,49 @@ impl Service {
}
// strace may slow down enough to risk reaching some service timeouts
writeln!(fragment_file, "TimeoutStartSec=infinity")?;
writeln!(fragment_file, "ExecStart=")?;

// Profile data dir
let mut rng = rand::thread_rng();
let profile_data_dir = PathBuf::from(format!(
"/{}-profile-data_{:08x}",
env!("CARGO_PKG_NAME"),
rng.gen::<u32>()
));
writeln!(
fragment_file,
"ExecStart={} run -m {} -- {}",
env::current_exe()?
.to_str()
.ok_or_else(|| anyhow::anyhow!("Unable to decode current executable path"))?,
mode,
cmd
"TemporaryFileSystem={}",
profile_data_dir.to_str().unwrap()
)?;

let shh_bin = env::current_exe()?
.to_str()
.ok_or_else(|| anyhow::anyhow!("Unable to decode current executable path"))?
.to_string();

// Wrap all ExecStartXxx directives
let mut exec_start_idx = 1;
for exec_start_opt in ["ExecStartPre", "ExecStart", "ExecStartPost"] {
let exec_start_cmds = Self::config_vals(exec_start_opt, &config_paths)?;
if !exec_start_cmds.is_empty() {
writeln!(fragment_file, "{}=", exec_start_opt)?;
}
for cmd in exec_start_cmds {
let profile_data_path = profile_data_dir.join(format!("{:03}", exec_start_idx));
exec_start_idx += 1;
writeln!(
fragment_file,
"{}={} run -m {} -p {} -- {}",
exec_start_opt,
shh_bin,
mode,
profile_data_path.to_str().unwrap(),
cmd
)?;
}
}

// TODO ExecStopPost shh invocation that merges previous profiles

log::info!("Config fragment written in {fragment_path:?}");
Ok(())
}
Expand Down

0 comments on commit 8bfffee

Please sign in to comment.