rust-lang / rust-lang/rust

`os::unix::process::Command::exec` sometimes allocates, violating async signal safety

Open
#130,756 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-discussion T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

I'm trying to build a small linux container runtime as part of another project. I'd like to do the moral equivalent of the following (extracted out and untested):

fn spawn_in_container(cmd: std::process::Command) -> anyhow::Result<u32> {
    let mut args = clone3::Clone3::default();
    args.exit_signal(libc::SIGCHLD as _)
        .flag_newuser()
        .flag_newns()
        .flag_newpid();

    match unsafe { args.call().context("clone3")? } {
        0 => unsafe { self.child_after_fork(cmd) },
        pid => return Ok(pid),
    }
}

// SAFETY: blah blah blah we can't allocate or anything else
unsafe fn child_after_fork(cmd: std::process::Command) -> ! {
    // ... various container setup

    // If successful, this never returns.
    let e = cmd.exec();
    std::process::abort();
}

do_exec in process_unix.rs makes a big deal about the (un)safety of this operation, so I thought that it would be safe to use Command::exec. Unfortunately, I just caught a deadlock:

#0  0x000072ca4efb0c0b in __lll_lock_wait_private () from target:/usr/lib/libc.so.6
#1  0x000072ca4efc5138 in malloc () from target:/usr/lib/libc.so.6
#2  0x00006458e3b79d7f in alloc::alloc::alloc () at library/alloc/src/alloc.rs:100
#3  alloc::alloc::Global::alloc_impl () at library/alloc/src/alloc.rs:183
#4  alloc::alloc::{impl#1}::allocate () at library/alloc/src/alloc.rs:243
#5  alloc::raw_vec::RawVec::try_allocate_in<u8, alloc::alloc::Global> () at library/alloc/src/raw_vec.rs:230
#6  alloc::raw_vec::RawVec::with_capacity_in<u8, alloc::alloc::Global> () at library/alloc/src/raw_vec.rs:158
#7  alloc::vec::Vec::with_capacity_in<u8, alloc::alloc::Global> () at library/alloc/src/vec/mod.rs:699
#8  alloc::slice::hack::{impl#1}::to_vec<u8, alloc::alloc::Global> () at library/alloc/src/slice.rs:162
#9  alloc::slice::hack::to_vec<u8, alloc::alloc::Global> () at library/alloc/src/slice.rs:111
#10 alloc::slice::{impl#0}::to_vec_in<u8, alloc::alloc::Global> () at library/alloc/src/slice.rs:478
#11 alloc::vec::{impl#11}::clone<u8, alloc::alloc::Global> () at library/alloc/src/vec/mod.rs:2843
#12 std::sys::os_str::bytes::{impl#4}::clone () at library/std/src/sys/os_str/bytes.rs:73
#13 std::ffi::os_str::{impl#10}::clone () at library/std/src/ffi/os_str.rs:641
#14 std::sys_common::process::CommandEnv::capture () at library/std/src/sys_common/process.rs:45
#15 std::sys_common::process::CommandEnv::capture_if_changed () at library/std/src/sys_common/process.rs:58
#16 std::sys::pal::unix::process::process_common::Command::capture_env () at library/std/src/sys/pal/unix/process/process_common.rs:363
#17 0x00006458e3b71913 in std::sys::pal::unix::process::process_common::Command::exec () at library/std/src/sys/pal/unix/process/process_unix.rs:237
#18 std::os::unix::process::{impl#0}::exec () at library/std/src/os/unix/process.rs:227

Something in capture_env is allocating, which violates the rules around what you're allowed to do between fork or clone and exec.

As far as I can tell, this isn't documented one way or the other. So maybe this is a documentation bug, or I missed the documentation. Still, the amount of surface area that has the potential to allocate seems very small here - maybe the allocation would be possible to avoid? That would let me and others use the stdlib Command for this use-case, which would be pretty nice.

Meta

rustc --version --verbose:

rustc 1.81.0 (eeb90cda1 2024-09-04)
binary: rustc
commit-hash: eeb90cda1969383f56a2637cbd3037bdf598841c
commit-date: 2024-09-04
host: x86_64-unknown-linux-gnu
release: 1.81.0
LLVM version: 18.1.7

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in library/std/src/sys/pal/unix/process/process_unix.rs at Command::exec and do_exec, then trace the capture_env path described in the issue. Reproduce the allocation or inspect the related process code to establish the expected async-signal-safety guarantee. Done means either removing the unsafe allocation from this path or documenting the limitation clearly, with appropriate coverage if the repository has an existing process test for it.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.