Skip to content

Commit b3b3248

Browse files
committed
fix: stop sequence for sshd
1 parent 2f027f1 commit b3b3248

4 files changed

Lines changed: 39 additions & 15 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function_name = { version = "0.3.0", default-features = false }
3434
goblin = { version = "0.10.1", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] }
3535
itertools = { version = "0.14.0", default-features = false, features = ["use_std"] }
3636
log = { version = "0.4.28", default-features = false, features = ["max_level_trace", "release_max_level_info"] }
37-
nix = { version = "0.30.1", default-features = false, features = ["fs", "user", "process"] }
37+
nix = { version = "0.30.1", default-features = false, features = ["fs", "user", "process", "signal"] }
3838
nom = { version = "8.0.0", default-features = false, features = ["std"] }
3939
path-clean = { version = "1.0.1", default-features = false }
4040
phf = { version = "0.12.1", default-features = false, features = ["std", "macros"] }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Automatic [systemd](https://systemd.io/) service hardening guided by [strace](ht
2121

2222
### Dependencies
2323

24-
Strace needs to be installed and its executable available in the path. A recent Strace version is strongly recommended.
24+
Strace needs to be installed and its executable reachable in `PATH`. **Strace version >= 6.6 is currently required.**
2525

2626
### From source
2727

src/main.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use std::{
88
io,
99
path::Path,
1010
process::Command,
11-
thread,
11+
sync::Arc,
12+
thread::{self, sleep},
13+
time::Duration,
1214
};
1315

1416
use anyhow::Context as _;
@@ -90,11 +92,10 @@ fn main() -> anyhow::Result<()> {
9092
log::info!(
9193
"Detected versions: Systemd {sd_version}, Linux kernel {kernel_version}, strace {strace_version}"
9294
);
93-
if strace_version < strace::StraceVersion::new(6, 4) {
94-
log::warn!(
95-
"Strace version >=6.4 is strongly recommended, if you experience strace output parsing errors, please consider upgrading"
96-
);
97-
}
95+
anyhow::ensure!(
96+
strace_version >= strace::StraceVersion::new(6, 6),
97+
"Strace version >=6.6 is required"
98+
);
9899

99100
// Parse cl args
100101
let args = cl::Args::parse();
@@ -122,17 +123,30 @@ fn main() -> anyhow::Result<()> {
122123
&hardening_opts,
123124
);
124125

126+
// Run strace
127+
let cmd = command.iter().map(|a| &**a).collect::<Vec<&str>>();
128+
let st = Arc::new(
129+
strace::Strace::run(&cmd, strace_log_path)
130+
.context("Failed to setup strace profiling")?,
131+
);
132+
125133
// Start signal handling thread
126134
let mut signals = signal_hook::iterator::Signals::new([
127135
signal_hook::consts::signal::SIGINT,
128136
signal_hook::consts::signal::SIGQUIT,
129137
signal_hook::consts::signal::SIGTERM,
130138
])
131139
.context("Failed to setup signal handlers")?;
140+
let st_sig = Arc::clone(&st);
132141
thread::spawn(move || {
133142
for sig in signals.forever() {
134-
// The strace, and its watched child processes already get the signal, so the iterator will stop naturally
135-
log::info!("Got signal {sig:?}, ignoring");
143+
// Propagate signal to strace after this delay, in most cases everything should have already stopped
144+
const SIGNAL_STRACE_STOP_DELAY: Duration = Duration::from_secs(5);
145+
146+
log::info!("Got signal {sig:?}");
147+
sleep(SIGNAL_STRACE_STOP_DELAY);
148+
log::info!("Stopping strace");
149+
st_sig.stop();
136150
}
137151
});
138152

@@ -146,11 +160,6 @@ fn main() -> anyhow::Result<()> {
146160
env::current_dir().context("Failed to read current directory")?,
147161
);
148162

149-
// Run strace
150-
let cmd = command.iter().map(|a| &**a).collect::<Vec<&str>>();
151-
let st = strace::Strace::run(&cmd, strace_log_path)
152-
.context("Failed to setup strace profiling")?;
153-
154163
// Summarize actions
155164
let logs = st
156165
.log_lines()

src/strace/run.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::{
44
env,
55
fs::File,
66
io::BufReader,
7+
os::unix::process::CommandExt as _,
78
path::{Path, PathBuf},
89
process::{Child, Command, Stdio},
910
};
@@ -46,8 +47,10 @@ impl Strace {
4647
// Start process
4748
// TODO setuid/setgid execution will be broken unless strace runs as root
4849
let child = Command::new(STRACE_BIN)
50+
.process_group(0) // set dedicated process group for easy signal handling, without affecting the shh process
4951
.args([
5052
"--daemonize=grandchild",
53+
"--kill-on-exit",
5154
"--relative-timestamps",
5255
"--follow-forks",
5356
"--status=successful,failed",
@@ -81,6 +84,18 @@ impl Strace {
8184
})
8285
}
8386

87+
pub(crate) fn stop(&self) {
88+
// Strace runs with `--deamonize=grandchild`, so will become child of the init process.
89+
// Consequently, the pid we have is already gone, but because we have started it with a unique process group,
90+
// we can still reliably kill strace.
91+
// Strace also runs with `--kill-on-exit` so the subtree will be reliably killed even if the tracee created
92+
// another process group (sshd does this).
93+
#[expect(clippy::cast_possible_wrap)]
94+
let pgid = nix::unistd::Pid::from_raw(-(self.process.id() as i32));
95+
// Ignore errors because it may have already naturally stopped
96+
let _ = nix::sys::signal::kill(pgid, nix::sys::signal::Signal::SIGKILL);
97+
}
98+
8499
fn pipe_path(dir: &Path) -> PathBuf {
85100
dir.join("strace.pipe")
86101
}

0 commit comments

Comments
 (0)