Skip to main content

libdd_crashtracker/collector/
api.rs

1// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3#![cfg(unix)]
4
5use super::{crash_handler::enable, receiver_manager::Receiver};
6use crate::{
7    clear_spans, clear_traces, collector::signal_handler_manager::register_crash_handlers,
8    crash_info::Metadata, reset_counters, shared::configuration::CrashtrackerReceiverConfig,
9    update_config, update_metadata, CrashtrackerConfiguration,
10};
11
12pub static DEFAULT_SYMBOLS: [libc::c_int; 4] =
13    [libc::SIGBUS, libc::SIGABRT, libc::SIGSEGV, libc::SIGILL];
14
15pub fn default_signals() -> Vec<libc::c_int> {
16    Vec::from(DEFAULT_SYMBOLS)
17}
18
19/// Reinitialize the crash-tracking infrastructure after a fork.
20/// This should be one of the first things done after a fork, to minimize the
21/// chance that a crash occurs between the fork, and this call.
22/// In particular, reset the counters that track the profiler state machine.
23///
24/// PRECONDITIONS:
25///     This function assumes that the crash-tracker has previously been
26///     initialized.
27/// SAFETY:
28///     Crash-tracking functions are not reentrant.
29///     No other crash-handler functions should be called concurrently.
30/// ATOMICITY:
31///     This function is not atomic. A crash during its execution may lead to
32///     unexpected crash-handling behaviour.
33pub fn on_fork(
34    config: CrashtrackerConfiguration,
35    receiver_config: CrashtrackerReceiverConfig,
36    metadata: Metadata,
37) -> anyhow::Result<()> {
38    clear_spans()?;
39    clear_traces()?;
40    reset_counters()?;
41    // Leave the old signal handler in place: they are unaffected by fork.
42    // https://man7.org/linux/man-pages/man2/sigaction.2.html
43    // The altstack (if any) is similarly unaffected by fork:
44    // https://man7.org/linux/man-pages/man2/sigaltstack.2.html
45
46    update_metadata(metadata)?;
47    update_config(config)?;
48    Receiver::update_stored_config(receiver_config)?;
49    Ok(())
50}
51
52/// Initialize the crash-tracking infrastructure.
53///
54/// PRECONDITIONS:
55///     None.
56/// SAFETY:
57///     Crash-tracking functions are not reentrant.
58///     No other crash-handler functions should be called concurrently.
59/// ATOMICITY:
60///     This function is not atomic. A crash during its execution may lead to
61///     unexpected crash-handling behaviour.
62pub fn init(
63    config: CrashtrackerConfiguration,
64    receiver_config: CrashtrackerReceiverConfig,
65    metadata: Metadata,
66) -> anyhow::Result<()> {
67    update_metadata(metadata)?;
68    update_config(config.clone())?;
69    Receiver::update_stored_config(receiver_config)?;
70    register_crash_handlers(&config)?;
71    enable();
72    Ok(())
73}
74
75/// Reconfigure the crash-tracking infrastructure.
76///
77/// PRECONDITIONS:
78///     None.
79/// SAFETY:
80///     Crash-tracking functions are not reentrant.
81///     No other crash-handler functions should be called concurrently.
82/// ATOMICITY:
83///     This function is not atomic. A crash during its execution may lead to
84///     unexpected crash-handling behaviour.
85pub fn reconfigure(
86    config: CrashtrackerConfiguration,
87    receiver_config: CrashtrackerReceiverConfig,
88    metadata: Metadata,
89) -> anyhow::Result<()> {
90    update_metadata(metadata)?;
91    update_config(config.clone())?;
92    Receiver::update_stored_config(receiver_config)?;
93    enable();
94    Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::{begin_op, insert_span, insert_trace, StacktraceCollection};
101    use chrono::Utc;
102    use libdd_common::tag;
103    use libdd_common::Endpoint;
104    use std::time::Duration;
105    // We can't run this in the main test runner because it (deliberately) crashes,
106    // and would make all following tests unrunable.
107    // To run this test,
108    // ./build-profiling-ffi /tmp/libdatadog
109    // mkdir /tmp/crashreports
110    // look in /tmp/crashreports for the crash reports and output files
111    #[ignore]
112    #[test]
113    fn test_crash() {
114        let time = Utc::now().to_rfc3339();
115        let dir = "/tmp/crashreports/";
116        let output_url = format!("file://{dir}{time}.txt");
117
118        let endpoint = Some(Endpoint::from_slice(&output_url));
119
120        let path_to_receiver_binary =
121            "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver".to_string();
122        let create_alt_stack = true;
123        let use_alt_stack = true;
124        let resolve_frames = StacktraceCollection::EnabledWithInprocessSymbols;
125        let stderr_filename = Some(format!("{dir}/stderr_{time}.txt"));
126        let stdout_filename = Some(format!("{dir}/stdout_{time}.txt"));
127        let timeout = Duration::from_secs(10);
128        let receiver_config = CrashtrackerReceiverConfig::new(
129            vec![],
130            vec![],
131            path_to_receiver_binary,
132            stderr_filename,
133            stdout_filename,
134        )
135        .unwrap();
136        let config = CrashtrackerConfiguration::new(
137            vec![],
138            create_alt_stack,
139            use_alt_stack,
140            endpoint,
141            resolve_frames,
142            default_signals(),
143            Some(timeout),
144            None,
145            true,
146        )
147        .unwrap();
148        let metadata = Metadata::new(
149            "libname".to_string(),
150            "version".to_string(),
151            "family".to_string(),
152            vec![],
153        );
154        init(config, receiver_config, metadata).unwrap();
155        begin_op(crate::OpTypes::ProfilerCollectingSample).unwrap();
156        insert_span(42).unwrap();
157        insert_trace(u128::MAX).unwrap();
158        insert_span(12).unwrap();
159        insert_trace(99399939399939393993).unwrap();
160
161        let tag = tag!("apple", "banana");
162        let metadata2 = Metadata::new(
163            "libname".to_string(),
164            "version".to_string(),
165            "family".to_string(),
166            vec![tag.to_string()],
167        );
168        update_metadata(metadata2).expect("metadata");
169
170        std::thread::sleep(Duration::from_secs(2));
171
172        let p: *const u32 = std::ptr::null();
173        let q = unsafe { *p };
174        assert_eq!(q, 3);
175    }
176
177    #[test]
178    fn test_altstack_paradox() {
179        let time = Utc::now().to_rfc3339();
180        let dir = "/tmp/crashreports/";
181        let output_url = format!("file://{dir}{time}.txt");
182
183        let endpoint = Some(Endpoint::from_slice(&output_url));
184
185        let create_alt_stack = true;
186        let use_alt_stack = false;
187        let resolve_frames = StacktraceCollection::EnabledWithInprocessSymbols;
188        let timeout = Duration::from_secs(10);
189
190        // This should return an error, because we're creating an altstack without using it
191        let config = CrashtrackerConfiguration::new(
192            vec![],
193            create_alt_stack,
194            use_alt_stack,
195            endpoint,
196            resolve_frames,
197            default_signals(),
198            Some(timeout),
199            None,
200            true,
201        );
202
203        // This is slightly over-tuned to the language of the error message, but it'd require some
204        // novel engineering just for this test in order to tighten this up.
205        let err = config.unwrap_err();
206        assert_eq!(
207            err.to_string(),
208            "Cannot create an altstack without using it"
209        );
210    }
211
212    #[cfg(target_os = "linux")]
213    fn get_sigaltstack() -> Option<libc::stack_t> {
214        let mut sigaltstack = libc::stack_t {
215            ss_sp: std::ptr::null_mut(),
216            ss_flags: 0,
217            ss_size: 0,
218        };
219        let res = unsafe { libc::sigaltstack(std::ptr::null(), &mut sigaltstack) };
220        if res == 0 {
221            Some(sigaltstack)
222        } else {
223            None
224        }
225    }
226
227    #[cfg_attr(miri, ignore)]
228    #[cfg(target_os = "linux")]
229    #[test]
230    fn test_altstack_use_create() {
231        // This test initializes crashtracking in a fork, then waits on the exit status of the
232        // child. We check for an atypical exit status in order to ensure that only our
233        // desired exit path is taken.
234
235        let time = Utc::now().to_rfc3339();
236        let dir = "/tmp/crashreports/";
237        let output_url = format!("file://{dir}{time}.txt");
238
239        let endpoint = Some(Endpoint::from_slice(&output_url));
240
241        let path_to_receiver_binary =
242            "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver".to_string();
243        let create_alt_stack = true;
244        let use_alt_stack = true;
245        let resolve_frames = StacktraceCollection::EnabledWithInprocessSymbols;
246        let stderr_filename = Some(format!("{dir}/stderr_{time}.txt"));
247        let stdout_filename = Some(format!("{dir}/stdout_{time}.txt"));
248        let signals = default_signals();
249        let timeout = Duration::from_secs(10);
250        let receiver_config = CrashtrackerReceiverConfig::new(
251            vec![],
252            vec![],
253            path_to_receiver_binary,
254            stderr_filename,
255            stdout_filename,
256        )
257        .unwrap();
258        let config = CrashtrackerConfiguration::new(
259            vec![],
260            create_alt_stack,
261            use_alt_stack,
262            endpoint,
263            resolve_frames,
264            signals,
265            Some(timeout),
266            None,
267            true,
268        )
269        .unwrap();
270        let metadata = Metadata::new(
271            "libname".to_string(),
272            "version".to_string(),
273            "family".to_string(),
274            vec![],
275        );
276
277        // At this point we fork, because we're going to be looking at process-level state
278        match unsafe { libc::fork() } {
279            -1 => {
280                panic!("Failed to fork");
281            }
282            0 => {
283                // Child process
284                // Get the current state of the altstack
285                let initial_sigaltstack = get_sigaltstack();
286                assert!(
287                    initial_sigaltstack.is_some(),
288                    "Failed to get initial sigaltstack"
289                );
290
291                // Initialize crashtracking.  This will
292                // - create a new altstack
293                // - set the SIGUBS/SIGSEGV handlers with SA_ONSTACK
294                init(config, receiver_config, metadata).unwrap();
295
296                // Get the state of the altstack after initialization
297                let after_init_sigaltstack = get_sigaltstack();
298
299                // Compare the initial and after-init sigaltstacks
300                if initial_sigaltstack == after_init_sigaltstack {
301                    eprintln!("Initial sigaltstack: {initial_sigaltstack:?}");
302                    std::process::exit(-5);
303                }
304
305                // Check the SIGBUS and SIGSEGV handlers are set with SA_ONSTACK
306                let mut sigaction = libc::sigaction {
307                    sa_sigaction: 0,
308                    sa_mask: unsafe { std::mem::zeroed::<libc::sigset_t>() },
309                    sa_flags: 0,
310                    sa_restorer: None,
311                };
312
313                let mut exit_code = -5;
314
315                for signal in default_signals() {
316                    let signame = crate::signal_from_signum(signal).unwrap();
317                    exit_code -= 1;
318                    let res = unsafe { libc::sigaction(signal, std::ptr::null(), &mut sigaction) };
319                    if res != 0 {
320                        eprintln!("Failed to get {signame:?} handler");
321                        std::process::exit(exit_code);
322                    }
323
324                    exit_code -= 1;
325                    if sigaction.sa_flags & libc::SA_ONSTACK != libc::SA_ONSTACK {
326                        eprintln!("Expected {signame:?} handler to have SA_ONSTACK");
327                        std::process::exit(exit_code);
328                    }
329                }
330
331                // OK, we're done
332                std::process::exit(42);
333            }
334            pid => {
335                // Parent process
336                let mut status = 0;
337                let _ = unsafe { libc::waitpid(pid, &mut status, 0) };
338
339                // `status` is not the exit code, gotta unwrap some layers
340                if libc::WIFEXITED(status) {
341                    let exit_code = libc::WEXITSTATUS(status);
342                    assert_eq!(exit_code, 42, "Child process exited with unexpected status");
343                } else {
344                    panic!("Child process did not exit normally");
345                }
346            }
347        }
348    }
349
350    #[cfg_attr(miri, ignore)]
351    #[cfg(target_os = "linux")]
352    #[test]
353    fn test_altstack_use_nocreate() {
354        // Similar to the other test, this one operates inside of a fork in order to prevent
355        // poisoning the main process state.
356
357        let time = Utc::now().to_rfc3339();
358        let dir = "/tmp/crashreports/";
359        let output_url = format!("file://{dir}{time}.txt");
360
361        let endpoint = Some(Endpoint::from_slice(&output_url));
362
363        let path_to_receiver_binary =
364            "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver".to_string();
365        let create_alt_stack = false; // Use, but do _not_ create
366        let use_alt_stack = true;
367        let resolve_frames = StacktraceCollection::EnabledWithInprocessSymbols;
368        let stderr_filename = Some(format!("{dir}/stderr_{time}.txt"));
369        let stdout_filename = Some(format!("{dir}/stdout_{time}.txt"));
370        let signals = default_signals();
371        let timeout = Duration::from_secs(10);
372        let receiver_config = CrashtrackerReceiverConfig::new(
373            vec![],
374            vec![],
375            path_to_receiver_binary,
376            stderr_filename,
377            stdout_filename,
378        )
379        .unwrap();
380        let config = CrashtrackerConfiguration::new(
381            vec![],
382            create_alt_stack,
383            use_alt_stack,
384            endpoint,
385            resolve_frames,
386            signals,
387            Some(timeout),
388            None,
389            true,
390        )
391        .unwrap();
392        let metadata = Metadata::new(
393            "libname".to_string(),
394            "version".to_string(),
395            "family".to_string(),
396            vec![],
397        );
398
399        // At this point we fork, because we're going to be looking at process-level state
400        match unsafe { libc::fork() } {
401            -1 => {
402                panic!("Failed to fork");
403            }
404            0 => {
405                // Child process
406                // Get the current state of the altstack
407                let initial_sigaltstack = get_sigaltstack();
408                assert!(
409                    initial_sigaltstack.is_some(),
410                    "Failed to get initial sigaltstack"
411                );
412
413                // Initialize crashtracking.  This will
414                // - create a new altstack
415                // - set the SIGUBS/SIGSEGV handlers with SA_ONSTACK
416                init(config, receiver_config, metadata).unwrap();
417
418                // Get the state of the altstack after initialization
419                let after_init_sigaltstack = get_sigaltstack();
420
421                // Compare the initial and after-init sigaltstacks:  they should be the same!
422                if initial_sigaltstack != after_init_sigaltstack {
423                    eprintln!("Initial sigaltstack: {initial_sigaltstack:?}");
424                    std::process::exit(-5);
425                }
426
427                // Even though the other test checks for the SA_ONSTACK flag on the signal handlers,
428                // we double-check here because the options need to be decoupled
429                let mut sigaction = libc::sigaction {
430                    sa_sigaction: 0,
431                    sa_mask: unsafe { std::mem::zeroed::<libc::sigset_t>() },
432                    sa_flags: 0,
433                    sa_restorer: None,
434                };
435
436                // First, SIGBUS
437                let res =
438                    unsafe { libc::sigaction(libc::SIGBUS, std::ptr::null(), &mut sigaction) };
439                if res != 0 {
440                    eprintln!("Failed to get SIGBUS handler");
441                    std::process::exit(-6);
442                }
443                if sigaction.sa_flags & libc::SA_ONSTACK != libc::SA_ONSTACK {
444                    eprintln!("Expected SIGBUS handler to have SA_ONSTACK");
445                    std::process::exit(-7);
446                }
447
448                // Second, SIGSEGV
449                let res =
450                    unsafe { libc::sigaction(libc::SIGSEGV, std::ptr::null(), &mut sigaction) };
451                if res != 0 {
452                    eprintln!("Failed to get SIGSEGV handler");
453                    std::process::exit(-8);
454                }
455                if sigaction.sa_flags & libc::SA_ONSTACK != libc::SA_ONSTACK {
456                    eprintln!("Expected SIGSEGV handler to have SA_ONSTACK");
457                    std::process::exit(-9);
458                }
459
460                // OK, we're done
461                std::process::exit(42);
462            }
463            pid => {
464                // Parent process
465                let mut status = 0;
466                let _ = unsafe { libc::waitpid(pid, &mut status, 0) };
467
468                // `status` is not the exit code, gotta unwrap some layers
469                if libc::WIFEXITED(status) {
470                    let exit_code = libc::WEXITSTATUS(status);
471                    assert_eq!(exit_code, 42, "Child process exited with unexpected status");
472                } else {
473                    panic!("Child process did not exit normally");
474                }
475            }
476        }
477    }
478
479    #[cfg_attr(miri, ignore)]
480    #[cfg(target_os = "linux")]
481    #[test]
482    fn test_altstack_nouse() {
483        // This checks that when we do not request the altstack, we do not get the altstack
484
485        let time = Utc::now().to_rfc3339();
486        let dir = "/tmp/crashreports/";
487        let output_url = format!("file://{dir}{time}.txt");
488
489        let endpoint = Some(Endpoint::from_slice(&output_url));
490
491        let path_to_receiver_binary =
492            "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver".to_string();
493        let create_alt_stack = false;
494        let use_alt_stack = false;
495        let resolve_frames = StacktraceCollection::EnabledWithInprocessSymbols;
496        let stderr_filename = Some(format!("{dir}/stderr_{time}.txt"));
497        let stdout_filename = Some(format!("{dir}/stdout_{time}.txt"));
498        let signals = default_signals();
499        let timeout = Duration::from_secs(10);
500        let receiver_config = CrashtrackerReceiverConfig::new(
501            vec![],
502            vec![],
503            path_to_receiver_binary,
504            stderr_filename,
505            stdout_filename,
506        )
507        .unwrap();
508        let config = CrashtrackerConfiguration::new(
509            vec![],
510            create_alt_stack,
511            use_alt_stack,
512            endpoint,
513            resolve_frames,
514            signals,
515            Some(timeout),
516            None,
517            true,
518        )
519        .unwrap();
520        let metadata = Metadata::new(
521            "libname".to_string(),
522            "version".to_string(),
523            "family".to_string(),
524            vec![],
525        );
526
527        // At this point we fork, because we're going to be looking at process-level state
528        match unsafe { libc::fork() } {
529            -1 => {
530                panic!("Failed to fork");
531            }
532            0 => {
533                // Child process
534                // Get the current state of the altstack
535                let initial_sigaltstack = get_sigaltstack();
536                assert!(
537                    initial_sigaltstack.is_some(),
538                    "Failed to get initial sigaltstack"
539                );
540
541                // Initialize crashtracking.  This will
542                // - create a new altstack
543                // - set the SIGUBS/SIGSEGV handlers with SA_ONSTACK
544                init(config, receiver_config, metadata).unwrap();
545
546                // Get the state of the altstack after initialization
547                let after_init_sigaltstack = get_sigaltstack();
548
549                // Compare the initial and after-init sigaltstacks:  they should be the same because
550                // we did not enable anything!  This checks that we don't
551                // erroneously build the altstack.
552                if initial_sigaltstack != after_init_sigaltstack {
553                    eprintln!("Initial sigaltstack: {initial_sigaltstack:?}");
554                    std::process::exit(-5);
555                }
556
557                // Similarly, we need to be extra sure that SA_ONSTACK is not present.
558                let mut sigaction = libc::sigaction {
559                    sa_sigaction: 0,
560                    sa_mask: unsafe { std::mem::zeroed::<libc::sigset_t>() },
561                    sa_flags: 0,
562                    sa_restorer: None,
563                };
564
565                // First, SIGBUS
566                let res =
567                    unsafe { libc::sigaction(libc::SIGBUS, std::ptr::null(), &mut sigaction) };
568                if res != 0 {
569                    eprintln!("Failed to get SIGBUS handler");
570                    std::process::exit(-6);
571                }
572                if sigaction.sa_flags & libc::SA_ONSTACK == libc::SA_ONSTACK {
573                    eprintln!("Expected SIGBUS handler not to have SA_ONSTACK");
574                    std::process::exit(-7);
575                }
576
577                // Second, SIGSEGV
578                let res =
579                    unsafe { libc::sigaction(libc::SIGSEGV, std::ptr::null(), &mut sigaction) };
580                if res != 0 {
581                    eprintln!("Failed to get SIGSEGV handler");
582                    std::process::exit(-8);
583                }
584                if sigaction.sa_flags & libc::SA_ONSTACK == libc::SA_ONSTACK {
585                    eprintln!("Expected SIGSEGV handler not to have SA_ONSTACK");
586                    std::process::exit(-9);
587                }
588
589                // OK, we're done
590                std::process::exit(42);
591            }
592            pid => {
593                // Parent process
594                let mut status = 0;
595                let _ = unsafe { libc::waitpid(pid, &mut status, 0) };
596
597                // `status` is not the exit code, gotta unwrap some layers
598                if libc::WIFEXITED(status) {
599                    let exit_code = libc::WEXITSTATUS(status);
600                    assert_eq!(exit_code, 42, "Child process exited with unexpected status");
601                } else {
602                    panic!("Child process did not exit normally");
603                }
604            }
605        }
606    }
607
608    #[cfg_attr(miri, ignore)]
609    #[cfg(target_os = "linux")]
610    #[test]
611    fn test_waitall_nohang() {
612        // This test checks whether the crashtracking implementation can cause malformed `waitall()`
613        // idioms to hang.
614        // Consider the following code from the Ruby runtime:
615        //
616        //   static VALUE
617        //   proc_waitall(VALUE _)
618        //   {
619        //       VALUE result;
620        //       rb_pid_t pid;
621        //       int status;
622        //
623        //       result = rb_ary_new();
624        //       rb_last_status_clear();
625        //
626        //       for (pid = -1;;) {
627        //           pid = rb_waitpid(-1, &status, 0);
628        //           if (pid == -1) {
629        //               int e = errno;
630        //               if (e == ECHILD)
631        //                   break;
632        //               rb_syserr_fail(e, 0);
633        //           }
634        //           rb_ary_push(result, rb_assoc_new(PIDT2NUM(pid), rb_last_status_get()));
635        //       }
636        //       return result;
637        //   }
638        //
639        // The intent here is to wait for all of one's child processes to exit.  This is a pretty
640        // standard operation in multi-process situations, with one important caveat:  usually you
641        // know your children ahead of time and can wait on them in a controlled,
642        // intentional matter. Previous versions of crashtracking, which spawned long-lived
643        // receiver processes, would interfere with this
644        //
645        // This implements the inner behavior of a test which allows the caller to control which
646        // options are used.
647
648        let time = Utc::now().to_rfc3339();
649        let dir = "/tmp/crashreports/";
650        let output_url = format!("file://{dir}{time}.txt");
651
652        let endpoint = Some(Endpoint::from_slice(&output_url));
653
654        let path_to_receiver_binary =
655            "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver".to_string();
656        let create_alt_stack = true; // doesn't matter, but most runtimes use it, so take it
657        let use_alt_stack = true;
658        let resolve_frames = StacktraceCollection::EnabledWithInprocessSymbols;
659        let stderr_filename = Some(format!("{dir}/stderr_{time}.txt"));
660        let stdout_filename = Some(format!("{dir}/stdout_{time}.txt"));
661        let signals = default_signals();
662        let timeout = Duration::from_secs(10);
663        let receiver_config = CrashtrackerReceiverConfig::new(
664            vec![],
665            vec![],
666            path_to_receiver_binary,
667            stderr_filename,
668            stdout_filename,
669        )
670        .unwrap();
671        let config = CrashtrackerConfiguration::new(
672            vec![],
673            create_alt_stack,
674            use_alt_stack,
675            endpoint,
676            resolve_frames,
677            signals,
678            Some(timeout),
679            None,
680            true,
681        )
682        .unwrap();
683
684        let metadata = Metadata::new(
685            "libname".to_string(),
686            "version".to_string(),
687            "family".to_string(),
688            vec![],
689        );
690
691        // Since this test ultimately mutates process state, it's done inside of a fork just like
692        // the other tests of the same ilk.
693        match unsafe { libc::fork() } {
694            -1 => {
695                panic!("Failed to fork");
696            }
697            0 => {
698                // Child process
699                // This is where the test actually happens!
700                init(config, receiver_config, metadata).unwrap();
701
702                // Now spawn some short-lived child processes.
703                // Note:  it's easy to confirm this test actually works by cranking the sleep
704                // duration up past the timeout duration. At such a point, the test
705                // should fail.
706                let mut children = vec![];
707                let sleep_duration = Duration::from_millis(100);
708                let timeout_duration = Duration::from_millis(150);
709                for _ in 0..10 {
710                    match unsafe { libc::fork() } {
711                        -1 => {
712                            panic!("Failed to fork");
713                        }
714                        0 => {
715                            // Grandchild process
716                            std::thread::sleep(sleep_duration);
717                            std::process::exit(0); // normal exit, since we're testing waitall
718                        }
719                        pid => {
720                            // Parent process
721                            children.push(pid); // unused in this test
722                        }
723                    }
724                }
725
726                // Now, do the equivalent of the waitall loop.
727                // One caveat is that we do not want to hang the test, so rather than an unbounded
728                // `waitpid()`, use WNOHANG within a timer loop.
729                let start_time = std::time::Instant::now();
730                loop {
731                    if start_time.elapsed() > timeout_duration {
732                        eprintln!("Timed out waiting for children to exit");
733                        std::process::exit(-6);
734                    }
735
736                    // Call waitpid with WNOHANG
737                    let mut status = 0;
738                    let pid = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) };
739                    let errno = std::io::Error::last_os_error().raw_os_error().unwrap();
740
741                    if pid == -1 && errno == libc::ECHILD {
742                        // No more children!  Done!
743                        std::process::exit(42);
744                    }
745                }
746            }
747            pid => {
748                // Parent process
749                let mut status = 0;
750                let _ = unsafe { libc::waitpid(pid, &mut status, 0) };
751
752                // `status` is not the exit code, gotta unwrap some layers
753                if libc::WIFEXITED(status) {
754                    let exit_code = libc::WEXITSTATUS(status);
755                    assert_eq!(exit_code, 42, "Child process exited with unexpected status");
756                } else {
757                    panic!("Child process did not exit normally");
758                }
759            }
760        }
761    }
762}