Skip to content

Commit 177874b

Browse files
committed
Write bin test for saguard
1 parent 7e7908c commit 177874b

5 files changed

Lines changed: 221 additions & 0 deletions

File tree

bin_tests/src/modes/behavior.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ pub fn get_behavior(mode_str: &str) -> Box<dyn Behavior> {
138138
"panic_hook_string" => Box::new(test_014_panic_hook_string::Test),
139139
"panic_hook_unknown_type" => Box::new(test_015_panic_hook_unknown_type::Test),
140140
"errno_preservation" => Box::new(test_016_errno_preservation::Test),
141+
"sigchld_sigpipe_saguard" => Box::new(test_017_sigchld_sigpipe_saguard::Test),
141142
"runtime_preload_logger" => Box::new(test_000_donothing::Test),
142143
_ => panic!("Unknown mode: {mode_str}"),
143144
}

bin_tests/src/modes/unix/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ pub mod test_013_panic_hook_after_fork;
1717
pub mod test_014_panic_hook_string;
1818
pub mod test_015_panic_hook_unknown_type;
1919
pub mod test_016_errno_preservation;
20+
pub mod test_017_sigchld_sigpipe_saguard;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Integration test for SaGuard during crash handling.
5+
//
6+
// Verifies that SIGCHLD and SIGPIPE handlers installed by the application are
7+
// not invoked during crash handling (because SaGuard suppresses them), even
8+
// though the crash handler spawns child processes (SIGCHLD) and writes to
9+
// pipes (SIGPIPE).
10+
//
11+
// Expected operation:
12+
// 1. setup() installs custom SIGCHLD and SIGPIPE handlers that write marker files when invoked
13+
// 2. pre() verifies the handlers actually work as a baseline check
14+
// 3. post() cleans up marker files and sets the output targets to "crash_sigchld" and
15+
// "crash_sigpipe" files. If the SaGuard is working, the crash handler will suppress these
16+
// signals and the marker files will not be created
17+
//
18+
// The integration test asserts that "crash_sigchld" and "crash_sigpipe" do
19+
// not exist after the crash
20+
use crate::modes::behavior::Behavior;
21+
use crate::modes::behavior::{
22+
atom_to_clone, file_write_msg, fileat_content_equals, remove_permissive, removeat_permissive,
23+
set_atomic, trigger_sigpipe,
24+
};
25+
26+
use libc;
27+
use libdd_crashtracker::CrashtrackerConfiguration;
28+
use std::path::{Path, PathBuf};
29+
use std::sync::atomic::AtomicPtr;
30+
31+
pub const CRASH_SIGCHLD_FILENAME: &str = "crash_sigchld";
32+
pub const CRASH_SIGPIPE_FILENAME: &str = "crash_sigpipe";
33+
34+
pub struct Test;
35+
36+
impl Behavior for Test {
37+
fn setup(
38+
&self,
39+
output_dir: &Path,
40+
_config: &mut CrashtrackerConfiguration,
41+
) -> anyhow::Result<()> {
42+
setup(output_dir)
43+
}
44+
45+
fn pre(&self, output_dir: &Path) -> anyhow::Result<()> {
46+
// verify SIGCHLD handler fires
47+
verify_sigchld(output_dir, "pre_sigchld.check")?;
48+
49+
// verify SIGPIPE handler fires
50+
verify_sigpipe(output_dir, "pre_sigpipe.check")?;
51+
52+
Ok(())
53+
}
54+
55+
fn post(&self, output_dir: &Path) -> anyhow::Result<()> {
56+
removeat_permissive(output_dir, CRASH_SIGCHLD_FILENAME);
57+
removeat_permissive(output_dir, CRASH_SIGPIPE_FILENAME);
58+
59+
// Point the handlers at the crash-time marker files
60+
// If SaGuard works, these files wont be created during crash handling
61+
set_atomic(
62+
&SIGCHLD_OUTPUT_FILE,
63+
output_dir.join(CRASH_SIGCHLD_FILENAME),
64+
);
65+
set_atomic(
66+
&SIGPIPE_OUTPUT_FILE,
67+
output_dir.join(CRASH_SIGPIPE_FILENAME),
68+
);
69+
Ok(())
70+
}
71+
}
72+
73+
static SIGCHLD_OUTPUT_FILE: AtomicPtr<PathBuf> = AtomicPtr::new(std::ptr::null_mut());
74+
static SIGPIPE_OUTPUT_FILE: AtomicPtr<PathBuf> = AtomicPtr::new(std::ptr::null_mut());
75+
76+
extern "C" fn sigchld_handler(_: libc::c_int) {
77+
let ofile = match atom_to_clone(&SIGCHLD_OUTPUT_FILE) {
78+
Ok(f) => f,
79+
_ => return,
80+
};
81+
file_write_msg(&ofile, "SIGCHLD_FIRED").ok();
82+
}
83+
84+
extern "C" fn sigpipe_handler(_: libc::c_int) {
85+
let ofile = match atom_to_clone(&SIGPIPE_OUTPUT_FILE) {
86+
Ok(f) => f,
87+
_ => return,
88+
};
89+
file_write_msg(&ofile, "SIGPIPE_FIRED").ok();
90+
}
91+
92+
fn verify_sigchld(output_dir: &Path, filename: &str) -> anyhow::Result<()> {
93+
set_atomic(&SIGCHLD_OUTPUT_FILE, output_dir.join(filename));
94+
95+
match unsafe { libc::fork() } {
96+
-1 => anyhow::bail!("Failed to fork"),
97+
0 => unsafe {
98+
libc::_exit(0);
99+
},
100+
_ => {
101+
// Wait for child to exit
102+
loop {
103+
let mut status: libc::c_int = 0;
104+
if -1 == unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) } {
105+
break;
106+
}
107+
}
108+
}
109+
}
110+
111+
match fileat_content_equals(output_dir, filename, "SIGCHLD_FIRED") {
112+
Ok(true) => (),
113+
_ => anyhow::bail!("SIGCHLD handler did not fire during baseline check"),
114+
}
115+
116+
remove_permissive(&output_dir.join(filename));
117+
set_atomic(&SIGCHLD_OUTPUT_FILE, output_dir.join("INVALID"));
118+
Ok(())
119+
}
120+
121+
fn verify_sigpipe(output_dir: &Path, filename: &str) -> anyhow::Result<()> {
122+
set_atomic(&SIGPIPE_OUTPUT_FILE, output_dir.join(filename));
123+
124+
trigger_sigpipe()?;
125+
126+
match fileat_content_equals(output_dir, filename, "SIGPIPE_FIRED") {
127+
Ok(true) => (),
128+
_ => anyhow::bail!("SIGPIPE handler did not fire during baseline check"),
129+
}
130+
131+
remove_permissive(&output_dir.join(filename));
132+
set_atomic(&SIGPIPE_OUTPUT_FILE, output_dir.join("INVALID"));
133+
Ok(())
134+
}
135+
136+
pub fn setup(output_dir: &Path) -> anyhow::Result<()> {
137+
let mut sigset: libc::sigset_t = unsafe { std::mem::zeroed() };
138+
unsafe {
139+
libc::sigemptyset(&mut sigset);
140+
}
141+
142+
// Install SIGCHLD handler
143+
let sigchld_action = libc::sigaction {
144+
sa_sigaction: sigchld_handler as *const () as usize,
145+
sa_mask: sigset,
146+
sa_flags: libc::SA_RESTART | libc::SA_SIGINFO,
147+
#[cfg(target_os = "linux")]
148+
sa_restorer: None,
149+
};
150+
unsafe {
151+
if libc::sigaction(libc::SIGCHLD, &sigchld_action, std::ptr::null_mut()) != 0 {
152+
anyhow::bail!("Failed to set up SIGCHLD handler");
153+
}
154+
}
155+
156+
// Install SIGPIPE handler
157+
let sigpipe_action = libc::sigaction {
158+
sa_sigaction: sigpipe_handler as *const () as usize,
159+
sa_mask: sigset,
160+
sa_flags: libc::SA_RESTART | libc::SA_SIGINFO,
161+
#[cfg(target_os = "linux")]
162+
sa_restorer: None,
163+
};
164+
unsafe {
165+
if libc::sigaction(libc::SIGPIPE, &sigpipe_action, std::ptr::null_mut()) != 0 {
166+
anyhow::bail!("Failed to set up SIGPIPE handler");
167+
}
168+
}
169+
170+
// Initialize output file pointers to INVALID
171+
set_atomic(&SIGCHLD_OUTPUT_FILE, output_dir.join("INVALID"));
172+
set_atomic(&SIGPIPE_OUTPUT_FILE, output_dir.join("INVALID"));
173+
174+
Ok(())
175+
}

bin_tests/src/test_types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub enum TestMode {
2020
RuntimeCallbackFrameInvalidUtf8,
2121
RuntimePreloadLogger,
2222
ErrnoPreservation,
23+
SigChldSigPipeSaGuard,
2324
}
2425

2526
impl TestMode {
@@ -41,6 +42,7 @@ impl TestMode {
4142
Self::RuntimeCallbackFrameInvalidUtf8 => "runtime_callback_frame_invalid_utf8",
4243
Self::RuntimePreloadLogger => "runtime_preload_logger",
4344
Self::ErrnoPreservation => "errno_preservation",
45+
Self::SigChldSigPipeSaGuard => "sigchld_sigpipe_saguard",
4446
}
4547
}
4648

@@ -62,6 +64,7 @@ impl TestMode {
6264
Self::RuntimeCallbackFrameInvalidUtf8,
6365
Self::RuntimePreloadLogger,
6466
Self::ErrnoPreservation,
67+
Self::SigChldSigPipeSaGuard,
6568
]
6669
}
6770
}
@@ -92,6 +95,7 @@ impl std::str::FromStr for TestMode {
9295
"runtime_callback_frame_invalid_utf8" => Ok(Self::RuntimeCallbackFrameInvalidUtf8),
9396
"runtime_preload_logger" => Ok(Self::RuntimePreloadLogger),
9497
"errno_preservation" => Ok(Self::ErrnoPreservation),
98+
"sigchld_sigpipe_saguard" => Ok(Self::SigChldSigPipeSaGuard),
9599
_ => Err(format!("Unknown test mode: {}", s)),
96100
}
97101
}

bin_tests/tests/crashtracker_bin_test.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,46 @@ fn test_crash_tracking_bin_errno_preservation() {
123123
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
124124
}
125125

126+
#[test]
127+
#[cfg_attr(miri, ignore)]
128+
fn test_crash_tracking_bin_sigchld_sigpipe_saguard() {
129+
use bin_tests::modes::unix::test_017_sigchld_sigpipe_saguard::{
130+
CRASH_SIGCHLD_FILENAME, CRASH_SIGPIPE_FILENAME,
131+
};
132+
133+
let config = CrashTestConfig::new(
134+
BuildProfile::Release,
135+
TestMode::SigChldSigPipeSaGuard,
136+
CrashType::NullDeref,
137+
);
138+
let artifacts = StandardArtifacts::new(config.profile);
139+
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
140+
141+
let validator: ValidatorFn = Box::new(|_payload, fixtures| {
142+
// SaGuard shouldve suppressed SIGCHLD and SIGPIPE during crash handling,
143+
// so the marker files shouldnt exist
144+
let sigchld_path = fixtures.output_dir.join(CRASH_SIGCHLD_FILENAME);
145+
let sigpipe_path = fixtures.output_dir.join(CRASH_SIGPIPE_FILENAME);
146+
147+
assert!(
148+
!sigchld_path.exists(),
149+
"SIGCHLD handler fired during crash handling; SaGuard did not suppress it. \
150+
File {:?} should not exist.",
151+
sigchld_path
152+
);
153+
assert!(
154+
!sigpipe_path.exists(),
155+
"SIGPIPE handler fired during crash handling; SaGuard did not suppress it. \
156+
File {:?} should not exist.",
157+
sigpipe_path
158+
);
159+
160+
Ok(())
161+
});
162+
163+
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
164+
}
165+
126166
#[test]
127167
#[cfg_attr(miri, ignore)]
128168
fn test_crash_tracking_bin_unhandled_exception() {

0 commit comments

Comments
 (0)