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