Skip to content

Commit a8f0aaa

Browse files
authored
feat(crashtracking)!: collect all threads (#1878)
# What does this PR do? When a process crashes the crashtracker now captures a full stack trace for every live non-crashing thread and includes them in the crash report under error.threads. Previously only the crashing thread's stack was reported. Customers had no visibility into what other threads were doing at the time of the crash, making it much harder to diagnose race conditions, deadlocks, and cross-thread interactions. Thread collection runs entirely in the receiver process after it has finished reading the crash pipe. Because the crashed process stays alive in its signal handler until the receiver calls `finish()`, all threads remain valid ptrace targets for the entire collection window. The receiver: 1. Enumerates threads from` /proc/<pid>/task/` 2. Attaches to each non-crashing thread with `PTRACE_SEIZE` + `PTRACE_INTERRUPT` 3. Uses libunwind remote unwinding (`_UPT_create` / `unw_init_remote` / `unw_step_remote`) to walk each thread's stack 4. Thread names and states are read from /proc/<pid>/task/<tid>/stat` 5. Thread collection is opt-in using `CrashtrackerConfiguration::set_collect_all_threads(true)` and bounded by a max of 256. The user can also set a lower boundary using `CrashtrackerConfiguration::set_max_threads()` . The collection timeout is the remaining slice of the receiver's deadline after the crash pipe is fully read, so the total receiver lifetime is always bounded by `config.timeout()` # Breaking changes Updated data schema to 1.7; there is now a `Threads` object. Errors intake upload still sends an array of `ThreadData`, as that is the schema that the enrichment pipeline expects `CrashtrackerConfig` now has ``` pub struct CrashtrackerConfiguration { additional_files: Vec<String>, collect_all_threads: bool, <-- NEW create_alt_stack: bool, demangle_names: bool, endpoint: Option<Endpoint>, max_threads: usize, <-- NEW resolve_frames: StacktraceCollection, signals: Vec<i32>, timeout: Duration, unix_socket_path: Option<String>, use_alt_stack: bool, } ``` ``` config.set_collect_all_threads(true); config.set_max_threads(128); // how many threads to collect ``` ``` CrashtrackerConfiguration::builder() .collect_all_threads(true) .max_threads(128) .build()?; ``` # Additional notes One enhancement we can do is to collect even the crashing thread in the receiver along with all threads, since enumerating all threads touches the crashing thread also. We can do this by checking for the crashing thread first in the receiver, then checking all threads. However, this will be explored in a future PR/investigation, as this is changing the core crashing thread unwinding logic # How to test the change? Run a crash with a program with multiple threads, with all thread collection turned on. Bin tests also added to test e2e flow ``` { "is_crash": true, "kind": "UnixSignal", "message": "Process terminated with SEGV_MAPERR (SIGSEGV)", "thread_name": "crashtracker_bi", "source_type": "Crashtracking", "stack": { "format": "Datadog Crashtracker 1.0", "frames": [ { "ip": "0x5d1e89d2adcb", "module_base_address": "0x5d1e89cfd000", "sp": "0x7ffc9f327330", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x000000000002ddcb", "column": 13, "file": "/home/bits/go/src/github.com/DataDog/libdatadog/bin_tests/src/bin/crashtracker_bin_test.rs", "function": "cause_segfault", "line": 37 }, ... { "ip": "0x5d1e89d29405", "module_base_address": "0x5d1e89cfd000", "sp": "0x7ffc9f327d70", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x000000000002c405", "function": "_start" } ], "incomplete": false }, "threads": [ { "crashed": false, "name": "ct_worker_0", "stack": { "format": "Datadog Crashtracker 1.0", "frames": [ { "ip": "0x5d1e89d34b57", "sp": "0x778c6131ed48", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x0000000000037b57", "column": 9, "file": "/home/bits/go/src/github.com/DataDog/libdatadog/bin_tests/src/modes/unix/test_017_multi_thread_collection.rs", "function": "worker_fn_0", "line": 21 }, { "ip": "0x5d1e89d34b2c", "sp": "0x778c6131ed50", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x0000000000037b2c", "column": 18, "file": "/home/bits/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs", "function": "__rust_begin_short_backtrace<bin_tests::modes::unix::test_017_multi_thread_collection::{impl#0}::post::{closure_env#0}, ()>", "line": 158 }, { "ip": "0x5d1e89d349e4", "sp": "0x778c6131ed70", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x00000000000379e4", "column": 5, "file": "/home/bits/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs", "function": "call_once<std::thread::{impl#0}::spawn_unchecked_::{closure_env#1}<bin_tests::modes::unix::test_017_multi_thread_collection::{impl#0}::post::{closure_env#0}, ()>, ()>", "line": 250 }, { "ip": "0x5d1e89d8223f", "sp": "0x778c6131edf0", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x000000000008523f", "column": 17, "file": "/rustc/ded5c06cf21d2b93bffd5d884aa6e96934ee4234/library/std/src/sys/thread/unix.rs", "function": "std::sys::thread::unix::Thread::new::thread_start", "line": 126 }, { "ip": "0x778c613c7ac3", "sp": "0x778c6131ee20", "build_id": "4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7", "build_id_type": "GNU", "file_type": "ELF", "path": "/usr/lib/x86_64-linux-gnu/libc.so.6", "relative_address": "0x0000000000094ac3", "column": 8, "file": "./nptl/pthread_create.c", "function": "start_thread", "line": 442 }, { "ip": "0x778c614598c0", "sp": "0x778c6131eec0", "build_id": "4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7", "build_id_type": "GNU", "file_type": "ELF", "path": "/usr/lib/x86_64-linux-gnu/libc.so.6", "relative_address": "0x00000000001268c0", "column": 0, "file": "./misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S", "function": "__clone3", "line": 83 } ], "incomplete": false }, "state": "R" }, { "crashed": false, "name": "ct_worker_1", "stack": { "format": "Datadog Crashtracker 1.0", "frames": [ { "ip": "0x5d1e89d34894", "sp": "0x778c6111dd48", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x0000000000037894", "column": 9, "file": "/home/bits/go/src/github.com/DataDog/libdatadog/bin_tests/src/modes/unix/test_017_multi_thread_collection.rs", "function": "worker_fn_1", "line": 29 }, { "ip": "0x5d1e89d34869", "sp": "0x778c6111dd50", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x0000000000037869", "column": 18, "file": "/home/bits/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs", "function": "__rust_begin_short_backtrace<bin_tests::modes::unix::test_017_multi_thread_collection::{impl#0}::post::{closure_env#1}, ()>", "line": 158 }, { "ip": "0x5d1e89d34719", "sp": "0x778c6111dd70", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x0000000000037719", "column": 5, "file": "/home/bits/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs", "function": "call_once<std::thread::{impl#0}::spawn_unchecked_::{closure_env#1}<bin_tests::modes::unix::test_017_multi_thread_collection::{impl#0}::post::{closure_env#1}, ()>, ()>", "line": 250 }, { "ip": "0x5d1e89d8223f", "sp": "0x778c6111ddf0", "build_id": "8d4cd090dde4a270bed5fb7f8168dcc1291051e0", "build_id_type": "GNU", "file_type": "ELF", "path": "/home/bits/go/src/github.com/DataDog/libdatadog/target/release/crashtracker_bin_test", "relative_address": "0x000000000008523f", "column": 17, "file": "/rustc/ded5c06cf21d2b93bffd5d884aa6e96934ee4234/library/std/src/sys/thread/unix.rs", "function": "std::sys::thread::unix::Thread::new::thread_start", "line": 126 }, { "ip": "0x778c613c7ac3", "sp": "0x778c6111de20", "build_id": "4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7", "build_id_type": "GNU", "file_type": "ELF", "path": "/usr/lib/x86_64-linux-gnu/libc.so.6", "relative_address": "0x0000000000094ac3", "column": 8, "file": "./nptl/pthread_create.c", "function": "start_thread", "line": 442 }, { "ip": "0x778c614598c0", "sp": "0x778c6111dec0", "build_id": "4f7b0c955c3d81d7cac1501a2498b69d1d82bfe7", "build_id_type": "GNU", "file_type": "ELF", "path": "/usr/lib/x86_64-linux-gnu/libc.so.6", "relative_address": "0x00000000001268c0", "column": 0, "file": "./misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S", "function": "__clone3", "line": 83 } ], "incomplete": false }, "state": "R" } ] } ```
1 parent 982b6bd commit a8f0aaa

21 files changed

Lines changed: 1758 additions & 66 deletions

bin_tests/src/modes/behavior.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ 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+
"multi_thread_collection" => Box::new(test_017_multi_thread_collection::Test),
142+
"thread_limit" => Box::new(test_018_thread_limit::Test),
141143
"runtime_preload_logger" => Box::new(test_000_donothing::Test),
142144
_ => panic!("Unknown mode: {mode_str}"),
143145
}

bin_tests/src/modes/unix/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ 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_multi_thread_collection;
21+
pub mod test_018_thread_limit;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Tests that the crashtracker collects stack information for all threads, not
5+
//! just the crashing thread.
6+
7+
use crate::modes::behavior::Behavior;
8+
use libdd_crashtracker::CrashtrackerConfiguration;
9+
use std::path::Path;
10+
use std::sync::{Arc, Barrier};
11+
use std::thread;
12+
use std::time::Duration;
13+
14+
pub struct Test;
15+
16+
// Black box to prevent compiler optimization
17+
#[inline(never)]
18+
fn worker_fn_0() {
19+
loop {
20+
std::hint::black_box(0x17_00u64);
21+
std::hint::spin_loop();
22+
}
23+
}
24+
25+
#[inline(never)]
26+
fn worker_fn_1() {
27+
loop {
28+
std::hint::black_box(0x17_01u64);
29+
std::hint::spin_loop();
30+
}
31+
}
32+
33+
impl Behavior for Test {
34+
fn setup(
35+
&self,
36+
_output_dir: &Path,
37+
config: &mut CrashtrackerConfiguration,
38+
) -> anyhow::Result<()> {
39+
config.set_collect_all_threads(true);
40+
config.set_max_threads(32);
41+
Ok(())
42+
}
43+
44+
fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> {
45+
Ok(())
46+
}
47+
48+
fn post(&self, _output_dir: &Path) -> anyhow::Result<()> {
49+
let barrier = Arc::new(Barrier::new(3));
50+
51+
let b0 = Arc::clone(&barrier);
52+
let h0 = thread::Builder::new()
53+
.name("ct_worker_0".to_string())
54+
.spawn(move || {
55+
b0.wait();
56+
worker_fn_0();
57+
})?;
58+
59+
let b1 = Arc::clone(&barrier);
60+
let h1 = thread::Builder::new()
61+
.name("ct_worker_1".to_string())
62+
.spawn(move || {
63+
b1.wait();
64+
worker_fn_1();
65+
})?;
66+
67+
barrier.wait();
68+
thread::sleep(Duration::from_millis(20));
69+
70+
std::mem::forget(h0);
71+
std::mem::forget(h1);
72+
Ok(())
73+
}
74+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Stress-tests thread collection by spawning 512 named worker threads and
5+
//! verifying the crash report contains all of them.
6+
7+
use crate::modes::behavior::Behavior;
8+
use libdd_crashtracker::{default_max_threads, CrashtrackerConfiguration};
9+
use std::path::Path;
10+
use std::sync::{Arc, Barrier};
11+
12+
pub struct Test;
13+
14+
fn worker_fn(i: usize) {
15+
std::hint::black_box(i);
16+
std::thread::sleep(std::time::Duration::from_millis(1000))
17+
}
18+
19+
const THREAD_COUNT: usize = default_max_threads();
20+
21+
impl Behavior for Test {
22+
fn setup(
23+
&self,
24+
_output_dir: &Path,
25+
config: &mut CrashtrackerConfiguration,
26+
) -> anyhow::Result<()> {
27+
config.set_collect_all_threads(true);
28+
config.set_max_threads(THREAD_COUNT);
29+
Ok(())
30+
}
31+
32+
fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> {
33+
Ok(())
34+
}
35+
36+
fn post(&self, _output_dir: &Path) -> anyhow::Result<()> {
37+
let barrier = Arc::new(Barrier::new(THREAD_COUNT + 1));
38+
39+
for i in 0..THREAD_COUNT {
40+
let barrier = Arc::clone(&barrier);
41+
std::thread::Builder::new()
42+
.name(format!("worker-{i}"))
43+
.spawn(move || {
44+
barrier.wait();
45+
worker_fn(i);
46+
})
47+
.expect("failed to spawn thread");
48+
}
49+
50+
barrier.wait();
51+
Ok(())
52+
}
53+
}

bin_tests/src/test_types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub enum TestMode {
2020
RuntimeCallbackFrameInvalidUtf8,
2121
RuntimePreloadLogger,
2222
ErrnoPreservation,
23+
MultiThreadCollection,
24+
ThreadLimit,
2325
}
2426

2527
impl TestMode {
@@ -41,6 +43,8 @@ impl TestMode {
4143
Self::RuntimeCallbackFrameInvalidUtf8 => "runtime_callback_frame_invalid_utf8",
4244
Self::RuntimePreloadLogger => "runtime_preload_logger",
4345
Self::ErrnoPreservation => "errno_preservation",
46+
Self::MultiThreadCollection => "multi_thread_collection",
47+
Self::ThreadLimit => "thread_limit",
4448
}
4549
}
4650

@@ -62,6 +66,8 @@ impl TestMode {
6266
Self::RuntimeCallbackFrameInvalidUtf8,
6367
Self::RuntimePreloadLogger,
6468
Self::ErrnoPreservation,
69+
Self::MultiThreadCollection,
70+
Self::ThreadLimit,
6571
]
6672
}
6773
}
@@ -92,6 +98,8 @@ impl std::str::FromStr for TestMode {
9298
"runtime_callback_frame_invalid_utf8" => Ok(Self::RuntimeCallbackFrameInvalidUtf8),
9399
"runtime_preload_logger" => Ok(Self::RuntimePreloadLogger),
94100
"errno_preservation" => Ok(Self::ErrnoPreservation),
101+
"multi_thread_collection" => Ok(Self::MultiThreadCollection),
102+
"thread_limit" => Ok(Self::ThreadLimit),
95103
_ => Err(format!("Unknown test mode: {}", s)),
96104
}
97105
}

bin_tests/tests/crashtracker_bin_test.rs

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use bin_tests::{
2121
ArtifactsBuild, BuildProfile,
2222
};
2323
use libdd_crashtracker::{
24-
CrashtrackerConfiguration, Metadata, SiCodes, SigInfo, SignalNames, StacktraceCollection,
24+
default_max_threads, CrashtrackerConfiguration, Metadata, SiCodes, SigInfo, SignalNames,
25+
StacktraceCollection,
2526
};
2627
use serde_json::Value;
2728

@@ -188,6 +189,178 @@ fn test_crash_tracking_bin_runtime_callback_frame() {
188189
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
189190
}
190191

192+
/// Tests that when `collect_all_threads` is enabled, the crash report contains
193+
/// entries in `error.threads` for background threads beyond the crashing thread.
194+
///
195+
/// The behavior (test_017_multi_thread_collection.rs) enables `collect_all_threads`,
196+
/// spawns two named sleeping worker threads in `post()`, and then crashes the main thread.
197+
///
198+
/// Thread collection now happens in the receiver process using libunwind remote unwinding
199+
/// via ptrace (_UPT_create / unw_init_remote / unw_step_remote). The parent process stays
200+
/// alive until the receiver completes, guaranteeing threads are valid ptrace targets.
201+
///
202+
/// We verify:
203+
/// - `error.threads` is non-empty.
204+
/// - Each thread entry is well-formed: `crashed=false`, `name`, and `stack` present.
205+
/// - None of the additional threads are marked as crashed (the crashing thread is in
206+
/// `error.stack`, not `error.threads`).
207+
/// - Both worker threads are present by name (ct_worker_0, ct_worker_1).
208+
/// - Each worker has their work frame in the stack trace.
209+
#[test]
210+
#[cfg(target_os = "linux")]
211+
#[cfg_attr(miri, ignore)]
212+
fn test_crash_tracking_multi_thread_collection() {
213+
let config = CrashTestConfig::new(
214+
BuildProfile::Release,
215+
TestMode::MultiThreadCollection,
216+
CrashType::NullDeref,
217+
);
218+
let artifacts = StandardArtifacts::new(config.profile);
219+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
220+
221+
let validator: ValidatorFn = Box::new(|payload, _fixtures| {
222+
let error = &payload["error"];
223+
let threads = &error["threads"];
224+
225+
let thread_count = threads["count"]
226+
.as_u64()
227+
.expect("threads.count should be a number");
228+
let all_threads = threads["threads"]
229+
.as_array()
230+
.expect("threads.threads should be a JSON array");
231+
232+
let thread_names: Vec<&str> = all_threads
233+
.iter()
234+
.map(|t| t["name"].as_str().unwrap_or("<none>"))
235+
.collect();
236+
237+
assert!(
238+
!all_threads.is_empty(),
239+
"error.threads should be non-empty when collect_all_threads is enabled; got payload: {}",
240+
serde_json::to_string_pretty(payload).unwrap_or_default()
241+
);
242+
243+
// Every thread entry must be structurally valid and non-crashing
244+
for thread in all_threads {
245+
assert!(
246+
thread["name"].is_string(),
247+
"thread entry missing 'name': {thread:?}"
248+
);
249+
assert!(
250+
thread["crashed"].is_boolean(),
251+
"thread entry missing 'crashed': {thread:?}"
252+
);
253+
assert!(
254+
thread["stack"].is_object(),
255+
"thread entry missing 'stack': {thread:?}"
256+
);
257+
assert!(
258+
!thread["crashed"].as_bool().unwrap_or(true),
259+
"threads in error.threads must have crashed=false: {thread:?}"
260+
);
261+
}
262+
263+
// Both named workers must be present; the behavior spawns exactly two
264+
for expected in ["ct_worker_0", "ct_worker_1"] {
265+
assert!(
266+
thread_names.contains(&expected),
267+
"Expected worker thread '{expected}' in error.threads; \
268+
got: {thread_names:?}"
269+
);
270+
}
271+
272+
for expected in ["ct_worker_0", "ct_worker_1"] {
273+
let worker = all_threads
274+
.iter()
275+
.find(|t| t["name"].as_str() == Some(expected))
276+
.unwrap_or_else(|| panic!("{expected} should be in threads"));
277+
278+
let frames = worker["stack"]["frames"]
279+
.as_array()
280+
.unwrap_or_else(|| panic!("{expected} stack.frames should be an array"));
281+
282+
let worker_fn = if expected == "ct_worker_0" {
283+
"worker_fn_0"
284+
} else {
285+
"worker_fn_1"
286+
};
287+
let has_worker_frame = frames.iter().any(|f| {
288+
f["function"]
289+
.as_str()
290+
.map(|name| name.contains(worker_fn))
291+
.unwrap_or(false)
292+
});
293+
assert!(
294+
has_worker_frame,
295+
"{expected} stack should contain a frame for '{worker_fn}' but got: {frames:?}"
296+
);
297+
}
298+
299+
assert!(
300+
thread_count == 2,
301+
"expected 2 threads, got {}",
302+
thread_count
303+
);
304+
305+
Ok(())
306+
});
307+
308+
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
309+
}
310+
311+
/// Spawns default max threads and verifies the crash report contains all of them.
312+
#[test]
313+
#[cfg(target_os = "linux")]
314+
#[cfg_attr(miri, ignore)]
315+
fn test_crash_tracking_thread_limit() {
316+
const THREAD_COUNT: usize = default_max_threads();
317+
318+
let config = CrashTestConfig::new(
319+
BuildProfile::Release,
320+
TestMode::ThreadLimit,
321+
CrashType::NullDeref,
322+
);
323+
let artifacts = StandardArtifacts::new(config.profile);
324+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
325+
326+
let validator: ValidatorFn = Box::new(move |payload, _fixtures| {
327+
let threads = &payload["error"]["threads"];
328+
329+
let thread_array = threads["threads"]
330+
.as_array()
331+
.expect("error.threads.threads should be a JSON array");
332+
let count = threads["count"]
333+
.as_u64()
334+
.expect("error.threads.count should be a number") as usize;
335+
336+
assert!(
337+
thread_array.len() >= THREAD_COUNT,
338+
"expected at least {THREAD_COUNT} thread entries, got {} \
339+
(count field: {})",
340+
thread_array.len(),
341+
count,
342+
);
343+
assert_eq!(
344+
count,
345+
thread_array.len(),
346+
"threads.count ({count}) should equal the number of thread entries ({})",
347+
thread_array.len(),
348+
);
349+
350+
// All entries must be non-crashing
351+
for thread in thread_array {
352+
assert!(
353+
!thread["crashed"].as_bool().unwrap_or(true),
354+
"threads in error.threads must have crashed=false: {thread:?}"
355+
);
356+
}
357+
358+
Ok(())
359+
});
360+
361+
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
362+
}
363+
191364
#[test]
192365
#[cfg(target_os = "linux")]
193366
#[cfg_attr(miri, ignore)]

0 commit comments

Comments
 (0)