mem_isolate/lib.rs
1//! # `mem-isolate`: *Contain memory leaks and fragmentation*
2//!
3//! It runs your function via a `fork()`, waits for the result, and returns it.
4//!
5//! This grants your code access to an exact copy of memory and state at the
6//! time just before the call, but guarantees that the function will not affect
7//! the parent process's memory footprint in any way.
8//!
9//! It forces functions to be *memory pure* (pure with respect to memory), even
10//! if they aren't.
11//!
12//! ```
13//! use mem_isolate::execute_in_isolated_process;
14//!
15//! // No heap, stack, or program memory out here...
16//! let result = mem_isolate::execute_in_isolated_process(|| {
17//! // ...Can be affected by anything in here
18//! Box::leak(Box::new(vec![42; 1024]));
19//! });
20//! ```
21//!
22//! To keep things simple, this crate exposes only two public interfaces:
23//!
24//! * [`execute_in_isolated_process`] - The function that executes your code in
25//! an isolated process.
26//! * [`MemIsolateError`] - The error type that function returns ☝️
27//!
28//! For more code examples, see
29//! [`examples/`](https://github.com/brannondorsey/mem-isolate/tree/main/examples).
30//! [This
31//! one](https://github.com/brannondorsey/mem-isolate/blob/main/examples/error-handling-basic.rs)
32//! in particular shows how you should think about error handling.
33//!
34//! For more information, see the
35//! [README](https://github.com/brannondorsey/mem-isolate).
36//!
37//! ## Limitations
38//!
39//! #### Performance & Usability
40//!
41//! * Works only on POSIX systems (Linux, macOS, BSD)
42//! * Data returned from the `callable` function must be serialized to and from
43//! the child process (using `serde`), which can be expensive for large data.
44//! * Excluding serialization/deserialization cost,
45//! `execute_in_isolated_process()` introduces runtime overhead on the order
46//! of ~1ms compared to a direct invocation of the `callable`.
47//!
48//! In performance-critical systems, these overheads can be no joke. However,
49//! for many use cases, this is an affordable trade-off for the memory safety
50//! and snapshotting behavior that `mem-isolate` provides.
51//!
52//! #### Safety & Correctness
53//!
54//! The use of `fork()`, which this crate uses under the hood, has a slew of
55//! potentially dangerous side effects and surprises if you're not careful.
56//!
57//! * For **single-threaded use only:** It is generally unsound to `fork()` in
58//! multi-threaded environments, especially when mutexes are involved. Only
59//! the thread that calls `fork()` will be cloned and live on in the new
60//! process. This can easily lead to deadlocks and hung child processes if
61//! other threads are holding resource locks that the child process expects to
62//! acquire.
63//! * **Signals** delivered to the parent process won't be automatically
64//! forwarded to the child process running your `callable` during its
65//! execution. See one of the `examples/blocking-signals-*` files for [an
66//! example](https://github.com/brannondorsey/mem-isolate/blob/main/examples/blocking-signals-minimal.rs)
67//! of how to handle this.
68//! * **[Channels](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)**
69//! can't be used to communicate between the parent and child processes.
70//! Consider using shared mmaps, pipes, or the filesystem instead.
71//! * **Shared mmaps** break the isolation guarantees of this crate. The child
72//! process will be able to mutate `mmap(..., MAP_SHARED, ...)` regions
73//! created by the parent process.
74//! * **Panics** in your `callable` won't panic the rest of your program, as
75//! they would without `mem-isolate`. That's as useful as it is harmful,
76//! depending on your use case, but it's worth noting.
77//! * **Mutable references, static variables, and raw pointers** accessible to
78//! your `callable` won't be modified as you would expect them to. That's kind
79//! of the whole point of this crate... ;)
80//!
81//! Failing to understand or respect these limitations will make your code more
82//! susceptible to both undefined behavior (UB) and heap corruption, not less.
83//!
84//! ## Feature Flags
85//!
86//! The following crate [feature
87//! flags](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section)
88//! are available:
89//!
90//! * `tracing`: Enable [tracing](https://docs.rs/tracing) instrumentation.
91//! Instruments all high-level functions in
92//! [`lib.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/src/lib.rs)
93//! and creates spans for child and parent processes in
94//! [`execute_in_isolated_process`]. Events are mostly `debug!` and `error!`
95//! level. See
96//! [`examples/tracing.rs`](https://github.com/brannondorsey/mem-isolate/blob/main/examples/tracing.rs)
97//! for an example.
98//!
99//! By default, no additional features are enabled.
100#![warn(missing_docs)]
101#![warn(clippy::pedantic, clippy::unwrap_used)]
102#![warn(missing_debug_implementations)]
103
104#[cfg(not(any(target_family = "unix")))]
105compile_error!(
106 "Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g. Linux, macOS, BSD)"
107);
108
109mod macros;
110use macros::{debug, error};
111#[cfg(feature = "tracing")]
112// Don't import event macros like debug, error, etc. directly to avoid conflicts
113// with our macros (see just above^)
114use tracing::{Level, instrument, span};
115
116use libc::{EINTR, c_int};
117use std::fmt::Debug;
118use std::fs::File;
119use std::io::{Read, Write};
120use std::os::unix::io::FromRawFd;
121
122#[cfg(test)]
123mod tests;
124
125mod c;
126use c::{
127 ForkReturn, PipeFds, SystemFunctions, WaitpidStatus, child_process_exited_on_its_own,
128 child_process_killed_by_signal,
129};
130
131pub mod errors;
132pub use errors::MemIsolateError;
133use errors::{
134 CallableDidNotExecuteError::{ChildPipeCloseFailed, ForkFailed, PipeCreationFailed},
135 CallableExecutedError::{ChildPipeWriteFailed, DeserializationFailed, SerializationFailed},
136 CallableStatusUnknownError::{
137 CallableProcessDiedDuringExecution, ChildProcessKilledBySignal, ParentPipeCloseFailed,
138 ParentPipeReadFailed, UnexpectedChildExitStatus, UnexpectedWaitpidReturnValue, WaitFailed,
139 },
140};
141
142use MemIsolateError::{CallableDidNotExecute, CallableExecuted, CallableStatusUnknown};
143
144// Re-export the serde traits our public API depends on
145pub use serde::{Serialize, de::DeserializeOwned};
146
147// Child process exit status codes
148const CHILD_EXIT_HAPPY: i32 = 0;
149const CHILD_EXIT_IF_READ_CLOSE_FAILED: i32 = 3;
150const CHILD_EXIT_IF_WRITE_FAILED: i32 = 4;
151
152#[cfg(feature = "tracing")]
153const HIGHEST_LEVEL: Level = Level::ERROR;
154
155/// Executes a user-supplied `callable` in a forked child process so that any
156/// memory changes during execution do not affect the parent. The child
157/// serializes its result (using bincode) and writes it through a pipe, which
158/// the parent reads and deserializes.
159///
160/// # Example
161///
162/// ```rust
163/// use mem_isolate::execute_in_isolated_process;
164///
165/// let leaky_fn = || {
166/// // Leak 1KiB of memory
167/// let data: Vec<u8> = Vec::with_capacity(1024);
168/// let data = Box::new(data);
169/// Box::leak(data);
170/// };
171///
172/// let _ = execute_in_isolated_process(leaky_fn);
173/// // However, the memory is not leaked in the parent process here
174/// ```
175///
176/// # Errors
177///
178/// Error handling is organized into three levels:
179///
180/// 1. The first level describes the effect of the error on the `callable` (e.g.
181/// did your callable function execute or not)
182/// 2. The second level describes what `mem-isolate` operation caused the error
183/// (e.g. did serialization fail)
184/// 3. The third level is the underlying OS error if it is available (e.g. an
185/// `io::Error`)
186///
187/// For most applications, you'll care only about the first level:
188///
189/// ```rust
190/// use mem_isolate::{execute_in_isolated_process, MemIsolateError};
191///
192/// // Function that might cause memory issues
193/// let result = execute_in_isolated_process(|| {
194/// // Some operation
195/// "Success!".to_string()
196/// });
197///
198/// match result {
199/// Ok(value) => println!("Callable succeeded: {}", value),
200/// Err(MemIsolateError::CallableDidNotExecute(_)) => {
201/// // Safe to retry, callable never executed
202/// println!("Callable did not execute, can safely retry");
203/// },
204/// Err(MemIsolateError::CallableExecuted(_)) => {
205/// // Do not retry unless idempotent
206/// println!("Callable executed but result couldn't be returned");
207/// },
208/// Err(MemIsolateError::CallableStatusUnknown(_)) => {
209/// // Retry only if idempotent
210/// println!("Unknown if callable executed, retry only if idempotent");
211/// }
212/// }
213/// ```
214///
215/// For a more detailed look at error handling, see the documentation in the
216/// [`errors`] module.
217///
218/// # Important Note on Closures
219///
220/// When using closures that capture and mutate variables from their
221/// environment, these mutations **only occur in the isolated child process**
222/// and do not affect the parent process's memory. For example, it may seem
223/// surprising that the following code will leave the parent's `counter`
224/// variable unchanged:
225///
226/// ```rust
227/// use mem_isolate::execute_in_isolated_process;
228///
229/// let mut counter = 0;
230/// let result = execute_in_isolated_process(|| {
231/// counter += 1; // This increment only happens in the child process
232/// counter // Returns 1
233/// });
234/// assert_eq!(counter, 0); // Parent's counter remains unchanged
235/// ```
236///
237/// This is the intended behavior as the function's purpose is to isolate all
238/// memory effects of the callable. However, this can be surprising, especially
239/// for [`FnMut`] or [`FnOnce`] closures.
240///
241/// # Limitations
242///
243/// #### Performance & Usability
244///
245/// * Works only on POSIX systems (Linux, macOS, BSD)
246/// * Data returned from the `callable` function must be serialized to and from
247/// the child process (using `serde`), which can be expensive for large data.
248/// * Excluding serialization/deserialization cost,
249/// `execute_in_isolated_process()` introduces runtime overhead on the order
250/// of ~1ms compared to a direct invocation of the `callable`.
251///
252/// In performance-critical systems, these overheads can be no joke. However,
253/// for many use cases, this is an affordable trade-off for the memory safety
254/// and snapshotting behavior that `mem-isolate` provides.
255///
256/// #### Safety & Correctness
257///
258/// The use of `fork()`, which this crate uses under the hood, has a slew of
259/// potentially dangerous side effects and surprises if you're not careful.
260///
261/// * For **single-threaded use only:** It is generally unsound to `fork()` in
262/// multi-threaded environments, especially when mutexes are involved. Only
263/// the thread that calls `fork()` will be cloned and live on in the new
264/// process. This can easily lead to deadlocks and hung child processes if
265/// other threads are holding resource locks that the child process expects to
266/// acquire.
267/// * **Signals** delivered to the parent process won't be automatically
268/// forwarded to the child process running your `callable` during its
269/// execution. See one of the `examples/blocking-signals-*` files for [an
270/// example](https://github.com/brannondorsey/mem-isolate/blob/main/) of how
271/// to handle this.
272/// * **[Channels](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)**
273/// can't be used to communicate between the parent and child processes.
274/// Consider using shared mmaps, pipes, or the filesystem instead.
275/// * **Shared mmaps** break the isolation guarantees of this crate. The child
276/// process will be able to mutate `mmap(..., MAP_SHARED, ...)` regions
277/// created by the parent process.
278/// * **Panics** in your `callable` won't panic the rest of your program, as
279/// they would without `mem-isolate`. That's as useful as it is harmful,
280/// depending on your use case, but it's worth noting.
281/// * **Mutable references, static variables, and raw pointers** accessible to
282/// your `callable` won't be modified as you would expect them to. That's kind
283/// of the whole point of this crate... ;)
284///
285/// Failing to understand or respect these limitations will make your code more
286/// susceptible to both undefined behavior (UB) and heap corruption, not less.
287#[cfg_attr(feature = "tracing", instrument(skip(callable)))]
288pub fn execute_in_isolated_process<F, T>(callable: F) -> Result<T, MemIsolateError>
289where
290 F: FnOnce() -> T,
291 T: Serialize + DeserializeOwned,
292{
293 #[cfg(feature = "tracing")]
294 let parent_span = span!(HIGHEST_LEVEL, "parent").entered();
295
296 let sys = get_system_functions();
297 let PipeFds { read_fd, write_fd } = create_pipe(&sys)?;
298
299 match fork(&sys)? {
300 ForkReturn::Child => {
301 #[cfg(feature = "tracing")]
302 let _child_span = {
303 std::mem::drop(parent_span);
304 span!(HIGHEST_LEVEL, "child").entered()
305 };
306
307 #[cfg(test)]
308 // Disable mocking in the child process, fixes test failures on macOS
309 c::mock::disable_mocking();
310
311 // NOTE: Fallible actions in the child must either serialize
312 // and send their error over the pipe, or exit with a code
313 // that can be inerpreted by the parent.
314 // TODO: Consider removing the serializations and just
315 // using exit codes as the only way to communicate errors.
316 // TODO: Get rid of all of the .expect()s
317
318 let mut writer = unsafe { File::from_raw_fd(write_fd) };
319 close_read_end_of_pipe_in_child_or_exit(&sys, &mut writer, read_fd);
320
321 let result = execute_callable(callable);
322 let encoded = serialize_result_or_error_value(result);
323 write_and_flush_or_exit(&sys, &mut writer, &encoded);
324 exit_happy(&sys)
325 }
326 ForkReturn::Parent(child_pid) => {
327 close_write_end_of_pipe_in_parent(&sys, write_fd)?;
328
329 // Read the data from the pipe before waiting for the child to exit
330 // to prevent deadlocks. Don't bubble up errors before the waitpid
331 // to avoid creating zombie processes.
332 let pipe_result = read_all_of_child_result_pipe(read_fd);
333
334 let waitpid_bespoke_status = wait_for_child(&sys, child_pid)?;
335 error_if_child_unhappy(waitpid_bespoke_status)?;
336
337 // Defer the buffer emptiness check until after the waitpid to preserve
338 // the behavior that child processes that panic will result in a
339 // CallableProcessDiedDuringExecution error.
340 let buffer = pipe_result?;
341 error_if_buffer_is_empty(&buffer)?;
342 deserialize_result(&buffer)
343 }
344 }
345}
346
347#[must_use]
348#[cfg_attr(feature = "tracing", instrument)]
349fn get_system_functions() -> impl SystemFunctions {
350 // Use the appropriate implementation based on build config
351 #[cfg(not(test))]
352 let sys = c::RealSystemFunctions;
353
354 #[cfg(test)]
355 let sys = if c::mock::is_mocking_enabled() {
356 // Use the mock from thread-local storage
357 c::mock::get_current_mock()
358 } else {
359 // Create a new fallback mock if no mock is active
360 c::mock::MockableSystemFunctions::with_fallback()
361 };
362
363 debug!("using {:?}", sys);
364 sys
365}
366
367#[cfg_attr(feature = "tracing", instrument)]
368fn create_pipe<S: SystemFunctions>(sys: &S) -> Result<PipeFds, MemIsolateError> {
369 let pipe_fds = match sys.pipe() {
370 Ok(pipe_fds) => pipe_fds,
371 Err(err) => {
372 let err = CallableDidNotExecute(PipeCreationFailed(err));
373 error!("error creating pipe, propagating {:?}", err);
374 return Err(err);
375 }
376 };
377 debug!("pipe created: {:?}", pipe_fds);
378 Ok(pipe_fds)
379}
380
381#[cfg_attr(feature = "tracing", instrument)]
382fn fork<S: SystemFunctions>(sys: &S) -> Result<ForkReturn, MemIsolateError> {
383 match sys.fork() {
384 Ok(result) => Ok(result),
385 Err(err) => {
386 let err = CallableDidNotExecute(ForkFailed(err));
387 error!("error forking, propagating {:?}", err);
388 Err(err)
389 }
390 }
391}
392
393#[cfg_attr(feature = "tracing", instrument(skip(callable)))]
394fn execute_callable<F, T>(callable: F) -> T
395where
396 F: FnOnce() -> T,
397{
398 debug!("starting execution of user-supplied callable");
399 #[allow(clippy::let_and_return)]
400 let result = {
401 #[cfg(feature = "tracing")]
402 let _span = span!(HIGHEST_LEVEL, "inside_callable").entered();
403 callable()
404 };
405 debug!("finished execution of user-supplied callable");
406 result
407}
408
409#[cfg_attr(feature = "tracing", instrument)]
410fn wait_for_child<S: SystemFunctions>(
411 sys: &S,
412 child_pid: c_int,
413) -> Result<WaitpidStatus, MemIsolateError> {
414 debug!("waiting for child process");
415 let waitpid_bespoke_status = loop {
416 match sys.waitpid(child_pid) {
417 Ok(status) => break status,
418 Err(wait_err) => {
419 if wait_err.raw_os_error() == Some(EINTR) {
420 debug!("waitpid interrupted with EINTR, retrying");
421 continue;
422 }
423 let err = CallableStatusUnknown(WaitFailed(wait_err));
424 error!("error waiting for child process, propagating {:?}", err);
425 return Err(err);
426 }
427 }
428 };
429
430 debug!(
431 "wait completed, received status: {:?}",
432 waitpid_bespoke_status
433 );
434 Ok(waitpid_bespoke_status)
435}
436
437#[cfg_attr(feature = "tracing", instrument)]
438fn error_if_child_unhappy(waitpid_bespoke_status: WaitpidStatus) -> Result<(), MemIsolateError> {
439 let result = if let Some(exit_status) = child_process_exited_on_its_own(waitpid_bespoke_status)
440 {
441 debug!("child process exited with status: {}", exit_status);
442 match exit_status {
443 CHILD_EXIT_HAPPY => Ok(()),
444 CHILD_EXIT_IF_READ_CLOSE_FAILED => {
445 Err(CallableDidNotExecute(ChildPipeCloseFailed(None)))
446 }
447 CHILD_EXIT_IF_WRITE_FAILED => Err(CallableExecuted(ChildPipeWriteFailed(None))),
448 unhandled_status => Err(CallableStatusUnknown(UnexpectedChildExitStatus(
449 unhandled_status,
450 ))),
451 }
452 } else if let Some(signal) = child_process_killed_by_signal(waitpid_bespoke_status) {
453 Err(CallableStatusUnknown(ChildProcessKilledBySignal(signal)))
454 } else {
455 Err(CallableStatusUnknown(UnexpectedWaitpidReturnValue(
456 waitpid_bespoke_status,
457 )))
458 };
459
460 if let Ok(()) = result {
461 debug!("child process exited happily on its own");
462 } else {
463 error!("child process signaled an error, propagating {:?}", result);
464 }
465
466 result
467}
468
469#[cfg_attr(feature = "tracing", instrument)]
470fn deserialize_result<T: DeserializeOwned>(buffer: &[u8]) -> Result<T, MemIsolateError> {
471 match bincode::deserialize::<Result<T, MemIsolateError>>(buffer) {
472 Ok(Ok(result)) => {
473 debug!("successfully deserialized happy result");
474 Ok(result)
475 }
476 Ok(Err(err)) => {
477 debug!("successfully deserialized error result: {:?}", err);
478 Err(err)
479 }
480 Err(err) => {
481 let err = CallableExecuted(DeserializationFailed(err.to_string()));
482 error!("failed to deserialize result, propagating {:?}", err);
483 Err(err)
484 }
485 }
486}
487
488/// Doesn't matter if the value is an error or not, we just want to serialize it either way
489///
490/// # Panics
491///
492/// Panics if the serialization of a [`MemIsolateError`] fails
493#[cfg_attr(feature = "tracing", instrument(skip(result)))]
494fn serialize_result_or_error_value<T: Serialize>(result: T) -> Vec<u8> {
495 match bincode::serialize(&Ok::<T, MemIsolateError>(result)) {
496 Ok(encoded) => {
497 debug!(
498 "serialization successful, resulted in {} bytes",
499 encoded.len()
500 );
501 encoded
502 }
503 Err(err) => {
504 let err = CallableExecuted(SerializationFailed(err.to_string()));
505 error!(
506 "serialization failed, now attempting to serialize error: {:?}",
507 err
508 );
509 #[allow(clippy::let_and_return)]
510 let encoded = bincode::serialize(&Err::<T, MemIsolateError>(err))
511 .expect("failed to serialize error");
512 debug!(
513 "serialization of error successful, resulting in {} bytes",
514 encoded.len()
515 );
516 encoded
517 }
518 }
519}
520
521#[cfg_attr(feature = "tracing", instrument)]
522fn write_and_flush_or_exit<S, W>(sys: &S, writer: &mut W, buffer: &[u8])
523where
524 S: SystemFunctions,
525 W: Write + Debug,
526{
527 let result = writer.write_all(buffer).and_then(|()| writer.flush());
528 #[allow(unused_variables)]
529 if let Err(err) = result {
530 error!("error writing to pipe: {:?}", err);
531 // If we can't write to the pipe, we can't communicate the error either
532 // so we rely on the parent correctly interpreting the exit code
533 let exit_code = CHILD_EXIT_IF_WRITE_FAILED;
534 debug!("exiting child process with exit code: {}", exit_code);
535 #[allow(clippy::used_underscore_items)]
536 sys._exit(exit_code);
537 } else {
538 debug!("wrote and flushed to pipe successfully");
539 }
540}
541
542fn exit_happy<S: SystemFunctions>(sys: &S) -> ! {
543 // NOTE: We don't wrap this in #[cfg_attr(feature = "tracing", instrument)]
544 // because doing so results in a compiler error because of the `!` return type
545 // No idea why its usage is fine without the cfg_addr...
546 #[cfg(feature = "tracing")]
547 let _span = {
548 const FN_NAME: &str = stringify!(exit_happy);
549 span!(HIGHEST_LEVEL, FN_NAME).entered()
550 };
551
552 let exit_code = CHILD_EXIT_HAPPY;
553 debug!("exiting child process with exit code: {}", exit_code);
554
555 #[allow(clippy::used_underscore_items)]
556 sys._exit(exit_code);
557}
558
559#[cfg_attr(feature = "tracing", instrument)]
560fn read_all_of_child_result_pipe(read_fd: c_int) -> Result<Vec<u8>, MemIsolateError> {
561 // Read from the pipe by wrapping the read fd as a File
562 let mut buffer = Vec::new();
563 {
564 let mut reader = unsafe { File::from_raw_fd(read_fd) };
565 if let Err(err) = reader.read_to_end(&mut buffer) {
566 let err = CallableStatusUnknown(ParentPipeReadFailed(err));
567 error!("error reading from pipe, propagating {:?}", err);
568 return Err(err);
569 }
570 } // The read_fd will automatically be closed when the File is dropped
571 debug!("successfully read {} bytes from pipe", buffer.len());
572 Ok(buffer)
573}
574
575#[cfg_attr(feature = "tracing", instrument)]
576fn error_if_buffer_is_empty(buffer: &[u8]) -> Result<(), MemIsolateError> {
577 if buffer.is_empty() {
578 let err = CallableStatusUnknown(CallableProcessDiedDuringExecution);
579 error!("buffer unexpectedly empty, propagating {:?}", err);
580 return Err(err);
581 }
582 Ok(())
583}
584
585#[cfg_attr(feature = "tracing", instrument)]
586fn close_write_end_of_pipe_in_parent<S: SystemFunctions>(
587 sys: &S,
588 write_fd: c_int,
589) -> Result<(), MemIsolateError> {
590 if let Err(err) = sys.close(write_fd) {
591 let err = CallableStatusUnknown(ParentPipeCloseFailed(err));
592 error!("error closing write end of pipe, propagating {:?}", err);
593 return Err(err);
594 }
595 debug!("write end of pipe closed successfully");
596 Ok(())
597}
598
599#[cfg_attr(feature = "tracing", instrument)]
600fn close_read_end_of_pipe_in_child_or_exit<S: SystemFunctions>(
601 sys: &S,
602 writer: &mut (impl Write + Debug),
603 read_fd: c_int,
604) {
605 if let Err(close_err) = sys.close(read_fd) {
606 let err = CallableDidNotExecute(ChildPipeCloseFailed(Some(close_err)));
607 error!(
608 "error closing read end of pipe, now attempting to serialize error: {:?}",
609 err
610 );
611
612 let encoded = bincode::serialize(&err).expect("failed to serialize error");
613 writer
614 .write_all(&encoded)
615 .expect("failed to write error to pipe");
616 writer.flush().expect("failed to flush error to pipe");
617
618 let exit_code = CHILD_EXIT_IF_READ_CLOSE_FAILED;
619 error!("exiting child process with exit code: {}", exit_code);
620 #[allow(clippy::used_underscore_items)]
621 sys._exit(exit_code);
622 } else {
623 debug!("read end of pipe closed successfully");
624 }
625}