Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

panic! in Command child forked from non-main thread results in exit status 0 #79740

Closed
ijackson opened this issue Dec 5, 2020 · 8 comments · Fixed by #81858
Closed

panic! in Command child forked from non-main thread results in exit status 0 #79740

ijackson opened this issue Dec 5, 2020 · 8 comments · Fixed by #81858
Labels
A-runtime Area: std's runtime and "pre-main" init for handling backtraces, unwinds, stack overflows C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@ijackson
Copy link
Contributor

ijackson commented Dec 5, 2020

If a panic occurs in a child process forked off by Command on a thread other than the main thread, the whole child process exits 0, so it looks like things have worked when in fact they've gone catastrophically wrong. This seems like a serious error handling bug.

@rustbot modify labels +T-libs +libs-impl

Steps to reproduce

use std::io;
use std::process::Command;
use std::os::unix::process::CommandExt;
use std::thread;

fn main() -> Result<(),io::Error> {
    let thr = thread::spawn(||{
        let mut c = Command::new("echo");
        c.arg("hi");
        unsafe {
            c.pre_exec(|| panic!("crash now!"));
        }
        let st = c.status().expect("get status");
        println!("{:?}", st);
    });
    thr.join().expect("joined");
    Ok(())
}

Actual behaviour

thread '<unnamed>' panicked at 'crash now!', t.rs:11:27
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
ExitStatus(ExitStatus(0))

Expected behaviour

  • The spawned thread builds the Command.
  • Command.status tries to run the command, so
  • Command.status forks, and then runs the pre_exec closure
  • The pre_exec closure panics
  • Consequently, the child process exits with SIGABRT or in any case nonzero
  • st is not a success

Specifically, the last line should be something like ExitStatus(ExitStatus(25856)) (which is what you get without the thread::spawn)

Meta

rustc --version --verbose:

rustc 1.50.0-nightly (3ff10e74a 2020-12-04)
binary: rustc
commit-hash: 3ff10e74a74ed093fcabac1de27fe1cd65bbbb4a
commit-date: 2020-12-04
host: x86_64-unknown-linux-gnu
release: 1.50.0-nightly
Backtrace (not particularly enlightening)

``` thread '' panicked at 'crash now!', t.rs:11:27 stack backtrace: 0: std::panicking::begin_panic 1: t::main::{{closure}}::{{closure}} 2: as core::ops::function::FnMut>::call_mut at /rustc/1773f60ea5d42e86b8fdf78d2fc5221ead222bc1/library/alloc/src/boxed.rs:1314:9 3: std::sys::unix::process::process_inner::::do_exec at /rustc/1773f60ea5d42e86b8fdf78d2fc5221ead222bc1/library/std/src/sys/unix/process/process_unix.rs:228:13 4: std::sys::unix::process::process_inner::::spawn at /rustc/1773f60ea5d42e86b8fdf78d2fc5221ead222bc1/library/std/src/sys/unix/process/process_unix.rs:58:36 5: std::process::Command::status at /rustc/1773f60ea5d42e86b8fdf78d2fc5221ead222bc1/library/std/src/process.rs:904:9 6: t::main::{{closure}} note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. ExitStatus(ExitStatus(0))


</p>
</details>
@ijackson ijackson added the C-bug Category: This is a bug. label Dec 5, 2020
@rustbot
Copy link
Collaborator

rustbot commented Dec 5, 2020

Error: Label libs-impl can only be set by Rust team members

Please let @rust-lang/release know if you're having trouble with this bot.

@ijackson

This comment has been minimized.

@ijackson

This comment has been minimized.

@ijackson
Copy link
Contributor Author

ijackson commented Dec 5, 2020

@rustbot modify labels +T-libs

@rustbot rustbot added the T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. label Dec 5, 2020
@jonas-schievink
Copy link
Contributor

Unwinding from a pre_exec callback is probably UB

@jonas-schievink jonas-schievink added the A-runtime Area: std's runtime and "pre-main" init for handling backtraces, unwinds, stack overflows label Dec 5, 2020
@ijackson
Copy link
Contributor Author

ijackson commented Dec 5, 2020 via email

@tmiasko
Copy link
Contributor

tmiasko commented Dec 6, 2020

Trying to catch the unwind seems plausible even if attempting to unwind causes undefined behaviour already. Alternatively the callbacks could be invoked through a function with #[unwind(aborts)] attribute.

@ijackson
Copy link
Contributor Author

Here is another program with weird behaviour:

use std::process::Command;
use std::os::unix::process::CommandExt;
use std::panic::catch_unwind;

fn main() {
    let got = catch_unwind(||{
        let mut c = Command::new("echo");
        c.arg("hi");
        unsafe {
            c.pre_exec(|| panic!("crash now!"));
        }
        let st = c.status().expect("get status");
        println!("{:?}", st);
    });
    eprintln!("got={:?}", &got);
}

Output

thread 'main' panicked at 'crash now!', src/main.rs:10:27
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
got=Err(Any)
got=Ok(())

Demonstrating that catch_unwind returned twice!

I think the solution has to be that in the child process, unwinding must stop at fork. I will send an MR.

Mark-Simulacrum pushed a commit to ijackson/rust that referenced this issue Jan 27, 2021
Unwinding past fork() in the child causes whatever traps the unwind
to return twice.  This is very strange and clearly not desirable.

With the default behaviour of the thread library, this can even result
in a panic in the child being transformed into zero exit status (ie,
success) as seen in the parent!

If unwinding reaches the fork point, the child should abort.

Annotating the closure with #[unwind(aborts)] is not sufficiently
stable right now, so we use catch_unwind.  This requires marking the
closure UnwindSafe - which is fine regardless of the contents of the
closure, since we abort on unwind and won't therefore touch anything
the closure might have captured.

Fixes rust-lang#79740.

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
Dylan-DPC-zz pushed a commit to Dylan-DPC-zz/rust that referenced this issue May 7, 2021
Do not allocate or unwind after fork

### Objective scenarios

 * Make (simple) panics safe in `Command::pre_exec_hook`, including most `panic!` calls, `Option::unwrap`, and array bounds check failures.
 * Make it possible to `libc::fork` and then safely panic in the child (needed for the above, but this requirement means exposing the new raw hook API which the `Command` implementation needs).
 * In singlethreaded programs, where panic in `pre_exec_hook` is already memory-safe, prevent the double-unwinding malfunction rust-lang#79740.

I think we want to make panic after fork safe even though the post-fork child environment is only experienced by users of `unsafe`, beause the subset of Rust in which any panic is UB is really far too hazardous and unnatural.

#### Approach

 * Provide a way for a program to, at runtime, switch to having panics abort.  This makes it possible to panic without making *any* heap allocations, which is needed because on some platforms malloc is UB in a child forked from a multithreaded program (see rust-lang#80263 (comment), and maybe also the SuS [spec](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html)).
 * Make that change in the child spawned by `Command`.
 * Document the rules comprehensively enough that a programmer has a fighting chance of writing correct code.
 * Test that this all works as expected (and in particular, that there aren't any heap allocations we missed)

Fixes rust-lang#79740

#### Rejected (or previously attempted) approaches

 * Change the panic machinery to be able to unwind without allocating, at least when the payload and message are both `'static`.  This seems like it would be even more subtle.  Also that is a potentially-hot path which I don't want to mess with.
 * Change the existing panic hook mechanism to not convert the message to a `String` before calling the hook.  This would be a surprising change for existing code and would not be detected by the type system.
 * Provide a `raw_panic_hook` function to intercept panics in a way that doesn't allocate.  (That was an earlier version of this MR.)

### History

This MR could be considered a v2 of rust-lang#80263.  Thanks to everyone who commented there.  In particular, thanks to `@m-ou-se,` `@Mark-Simulacrum` and `@hyd-dev.`  (Tagging you since I think you might be interested in this new MR.)  Compared to rust-lang#80263, this MR has very substantial changes and additions.

Additionally, I have recently (2021-04-20) completely revised this series following very helpful comments from `@m-ou-se.`

r? `@m-ou-se`
bors added a commit to rust-lang-ci/rust that referenced this issue May 15, 2021
Do not allocate or unwind after fork

### Objective scenarios

 * Make (simple) panics safe in `Command::pre_exec_hook`, including most `panic!` calls, `Option::unwrap`, and array bounds check failures.
 * Make it possible to `libc::fork` and then safely panic in the child (needed for the above, but this requirement means exposing the new raw hook API which the `Command` implementation needs).
 * In singlethreaded programs, where panic in `pre_exec_hook` is already memory-safe, prevent the double-unwinding malfunction rust-lang#79740.

I think we want to make panic after fork safe even though the post-fork child environment is only experienced by users of `unsafe`, beause the subset of Rust in which any panic is UB is really far too hazardous and unnatural.

#### Approach

 * Provide a way for a program to, at runtime, switch to having panics abort.  This makes it possible to panic without making *any* heap allocations, which is needed because on some platforms malloc is UB in a child forked from a multithreaded program (see rust-lang#80263 (comment), and maybe also the SuS [spec](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html)).
 * Make that change in the child spawned by `Command`.
 * Document the rules comprehensively enough that a programmer has a fighting chance of writing correct code.
 * Test that this all works as expected (and in particular, that there aren't any heap allocations we missed)

Fixes rust-lang#79740

#### Rejected (or previously attempted) approaches

 * Change the panic machinery to be able to unwind without allocating, at least when the payload and message are both `'static`.  This seems like it would be even more subtle.  Also that is a potentially-hot path which I don't want to mess with.
 * Change the existing panic hook mechanism to not convert the message to a `String` before calling the hook.  This would be a surprising change for existing code and would not be detected by the type system.
 * Provide a `raw_panic_hook` function to intercept panics in a way that doesn't allocate.  (That was an earlier version of this MR.)

### History

This MR could be considered a v2 of rust-lang#80263.  Thanks to everyone who commented there.  In particular, thanks to `@m-ou-se,` `@Mark-Simulacrum` and `@hyd-dev.`  (Tagging you since I think you might be interested in this new MR.)  Compared to rust-lang#80263, this MR has very substantial changes and additions.

Additionally, I have recently (2021-04-20) completely revised this series following very helpful comments from `@m-ou-se.`

r? `@m-ou-se`
@bors bors closed this as completed in 820123a May 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-runtime Area: std's runtime and "pre-main" init for handling backtraces, unwinds, stack overflows C-bug Category: This is a bug. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants