add std::os::unix::process::CommandExt::fd rust-lang/rust#145687

Open

49 comments and reviews loaded in 3.47s

Qelxiros Avatar
Qelxiros on 2025-08-20 22:52:51 UTC · edited
Qelxiros Avatar
Qelxiros on 2025-08-20 22:52:51 UTC · edited
View on GitHub

View all comments

ACP: rust-lang/libs-team#623
Tracking issue: #144989

try-job: aarch64-apple
try-job: arm-android
try-job: test-various

rustbot Avatar
rustbot on 2025-08-20 22:52:56 UTC
rustbot Avatar
rustbot on 2025-08-20 22:52:56 UTC
View on GitHub

r? @tgross35

rustbot has assigned @tgross35.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

tgross35 Avatar
tgross35 requested changes on 2025-08-27 09:38:18 UTC
tgross35 left a comment · edited
View on GitHub

Has this code been run? The implementation just duplicates the fd without interacting with self.

Please make sure that all new API, even unstable, always gets document0ation and examples, as well as tests if the functionality can't be covered in a simple example. This is tricky API, make sure to cover the FD_CLOEXEC behavior in tests and include something to the effect of https://rust-lang.zulipchat.com/#narrow/channel/327149-t-libs-api.2Fapi-changes/topic/Extra.20FDs.20in.20CommandExt/near/439547420 in docs.

The tests at https://github.com/google/command-fds/tree/main can probably serve as reference.

View changes since this review

library/std/src/os/fd/process.rs · outdated
5 #[unstable(feature = "command_pass_fds", issue = "144989")]
6 pub trait CommandExt {
7 fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>);
8 }
tgross35 Avatar tgross35 on 2025-08-27 08:50:50 UTC
View on GitHub

Can this just reuse the existing CommandExt rather than creating a new one?

Qelxiros Avatar Qelxiros on 2025-08-27 21:55:52 UTC
View on GitHub

The existing CommandExt implementations are in other modules of std::os. According to the ACP, this trait should live in std::os::fd.

tgross35 Avatar tgross35 on 2025-09-03 04:23:17 UTC
View on GitHub

I did notice it said that, but do you know why? Having two CommandExts (one of which isn't in a process module like Command itself is) doesn't make any sense to me so I am thinking this is just an oversight.

Qelxiros Avatar Qelxiros on 2025-09-03 12:50:36 UTC
View on GitHub

I'm not sure what you mean about process modules. There are (prior to this PR) three documented instances of CommandExt, each of which is contained in a process module. This PR adds a fourth such instance and a process module for std::os::fd.

tgross35 Avatar tgross35 on 2025-09-03 13:30:59 UTC
View on GitHub

I mean specifically the OS process module, of which there is only one per platform (where the existing CommandExts live), not just any module named process. Can you explain why it makes more sense to add a new trait in fd rather than just adding a new method to the existing Unix CommandExt?

Qelxiros Avatar Qelxiros on 2025-09-03 13:35:09 UTC
View on GitHub

As I understand it, the std::os::fd module exists for fd-related operations. It includes WASI (https://github.com/rust-lang/rust/blob/master/library/std/src/os/mod.rs#L185-L186) because fds are handled similarly to unix. If this API works on WASI, why not expose it there?

tgross35 Avatar tgross35 on 2025-09-03 14:58:08 UTC
View on GitHub

I think that if there is interest on wasi, it would be easy enough to add a std::os::wasi::CommandExt. This would also mean pre_exec can be provided.

Does the current impl work on WASI? Afaik our libc doesn't have dup/dup2 or fnctl for that platform, so it would need a fallback anyway.

library/std/src/os/fd/process.rs · outdated
16 libc::dup2(old_fd.into().as_raw_fd(), new_fd);
17 libc::fcntl(new_fd, F_SETFD, 0);
tgross35 Avatar tgross35 on 2025-08-27 09:14:53 UTC
View on GitHub

We shouldn't have any errors here since old_fd should be valid. It would still be a good idea for consistency to check the return values and turn them into a panic (probably just .expecting the result of cvt_nz)

library/std/src/os/fd/process.rs · outdated
14 use libc::F_SETFD;
15
16 libc::dup2(old_fd.into().as_raw_fd(), new_fd);
17 libc::fcntl(new_fd, F_SETFD, 0);
tgross35 Avatar tgross35 on 2025-08-27 09:33:49 UTC
View on GitHub

Is it correct to rely on 0 here? dup2 doesn't set FD_CLOEXEC but doesn't really specify what would happen with other flags if they get added. It might be better to F_GETFD the flags and explicitly unset only FD_CLOEXEC. In any case, it could use a comment.

@the8472 may know better

tgross35 Avatar tgross35 on 2025-08-27 09:33:57 UTC
View on GitHub
rust-log-analyzer Avatar
rust-log-analyzer on 2025-08-27 22:31:55 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-08-27 22:31:55 UTC · hidden as outdated
View on GitHub

The job aarch64-gnu-llvm-19-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
 Documenting std v0.0.0 (/checkout/library/std)
error: unresolved link to `process::Command`
  --> library/std/src/os/fd/process.rs:10:25
   |
10 | /// Extensions to the [`process::Command`] builder for Unix and WASI, platforms that support file
   |                         ^^^^^^^^^^^^^^^^ no item named `process` in scope
   |
   = note: `-D rustdoc::broken-intra-doc-links` implied by `-D warnings`
   = help: to override `-D warnings` add `#[allow(rustdoc::broken_intra_doc_links)]`

error: could not document `std`
warning: build failed, waiting for other jobs to finish...
[RUSTC-TIMING] std test:false 7.265
Command `/checkout/obj/build/aarch64-unknown-linux-gnu/stage0/bin/cargo doc --target aarch64-unknown-linux-gnu -Zbinary-dep-depinfo -j 4 -Zroot-dir=/checkout --locked --color always --release --features 'backtrace panic-unwind compiler-builtins-c' --manifest-path /checkout/library/sysroot/Cargo.toml --no-deps --target-dir /checkout/obj/build/aarch64-unknown-linux-gnu/stage2-std/aarch64-unknown-linux-gnu/doc -Zskip-rustdoc-fingerprint -Zrustdoc-map -p alloc -p compiler_builtins -p core -p panic_abort -p panic_unwind -p proc_macro -p rustc-std-workspace-core -p std -p std_detect -p sysroot -p test -p unwind [workdir=/checkout]` failed with exit code 101
Created at: src/bootstrap/src/core/build_steps/doc.rs:752:21
Executed at: src/bootstrap/src/core/build_steps/doc.rs:789:22

Command has failed. Rerun with -v to see more details.
Bootstrap failed while executing `--stage 2 test --skip tests --skip coverage-map --skip coverage-run --skip library --skip tidyselftest`
Build completed unsuccessfully in 0:31:43
  local time: Wed Aug 27 22:26:40 UTC 2025
  network time: Wed, 27 Aug 2025 22:31:42 GMT
##[error]Process completed with exit code 1.
dominik-korsa Avatar
dominik-korsa commented on 2025-09-02 23:12:57 UTC
library/std/src/os/fd/process.rs · outdated
52 impl CommandExt for Command {
53 fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
54 let old = old_fd.into().as_raw_fd();
55 unsafe {
56 self.as_inner_mut().pre_exec(Box::new(move || {
57 cvt_r(|| libc::dup2(old, new_fd))?;
58 let flags = cvt(libc::fcntl(new_fd, F_GETFD))?;
59 cvt(libc::fcntl(new_fd, F_SETFD, flags & !FD_CLOEXEC))?;
60 cvt_r(|| libc::close(old))?;
61 Ok(())
62 }))
63 }
64
65 self
66 }
67 }
dominik-korsa Avatar dominik-korsa on 2025-09-02 23:12:57 UTC
View on GitHub

Using pre_exec for this can reduce the performance of starting the child process.
See my Zulip thread on this:
#t-libs-api/api-changes > Extra FDs in CommandExt @ 💬
also mentioned in this comment in the tracking issue:
#144989 (comment)

Using pre_exec switches the spawn implementation from posix_spawn to fork + execve syscalls. Forking is slower than posix_spawn, because it needs to copy the programs memory (in practice copy on write optimizations are used, but they are still somewhat costly - see my benchmarks also linked in the Zulip message). posix_spawn supports setting the passed FDs, so this feature should be used if possible.

tgross35 Avatar tgross35 on 2025-09-02 23:35:07 UTC
View on GitHub

Do you happen to have a sketch of what a better interface would look like?

dominik-korsa Avatar dominik-korsa on 2025-09-04 15:56:57 UTC
View on GitHub

No, but the issue I've described here is not a problem with the signature of fd. The extension should not use the public pre_exec method, the implementation needs to be modified instead.

dominik-korsa Avatar dominik-korsa on 2025-09-04 16:16:03 UTC
View on GitHub

I believe this file is of interest:

if let Some(ret) = self.posix_spawn(&theirs, envp.as_ref())? {
return Ok((ret, ours));
}

Note that there are multiple structs called Command because of how the target-specific functionality is implemented.

Command.spawn first tries to run the posix_spawn method, but its implementations return None if an attribute which the limited posix_spawn syscall cannot handle is set - see the implementations of the posix_spawn method.

Please keep this behavior in mind when adding new Command attributes and creating tests - cases when spawn uses the posix_spawn syscall and when it does not should both be tested. I'm not sure if there is a way to check which spawn variant was chosen in the tests, it is an implementation detail after all.

dominik-korsa Avatar dominik-korsa on 2025-09-04 16:18:28 UTC
View on GitHub

The extension should not use the public pre_exec method, the implementation needs to be modified instead.

See how other methods in the CommandExt trait are implemented in the library/std/src/os/fd/process.rs file that you modified - they all call the actual implementation using .as.inner_mut()

tgross35 Avatar tgross35 on 2025-09-04 21:20:11 UTC
View on GitHub

It would be reasonably straightforward to add a Vec<(OwnedFd, RawFd)> to Command that could be used either with something based on posix_spawn_file_actions_adddup2 or with the exec fallback (also avoiding 1 closure per fd). But aside from being slow, is there any correctness problem with the current implementation?

As long as there isn't anything explicitly wrong, I think it would be reasonable to get the current implementation over the line first so we have some tests. Any chance you would be interested in submitting a followup changing the implementation, since you have been looking into this a lot?

I'm not sure if there is a way to check which spawn variant was chosen in the tests, it is an implementation detail after all.

Adding a last_spawn_was_posix_spawn field to Command for debugging would be fine. That will be accessible from tests once they're moved to within std/src.

dominik-korsa Avatar dominik-korsa on 2025-09-05 08:35:22 UTC · edited
View on GitHub

But aside from being slow, is there any correctness problem with the current implementation?

No, I'm not claiming this implementation is incorrect - I also haven't reviewed it thoroughly though.

Any chance you would be interested in submitting a followup changing the implementation, since you have been looking into this a lot?

If I have the time, sure.

👍1
tgross35 Avatar
tgross35 requested changes on 2025-09-03 07:07:59 UTC
library/std/src/os/fd/process.rs · outdated
34 /// use std::process::Command;
35 /// use std::os::fd::process::CommandExt;
36 ///
37 /// eprintln!("chom");
tgross35 Avatar tgross35 on 2025-09-03 04:26:30 UTC
View on GitHub

Are the eprintln!s here intended to be kept?

library/std/src/os/fd/process.rs · outdated
53 fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
54 let old = old_fd.into().as_raw_fd();
55 unsafe {
56 self.as_inner_mut().pre_exec(Box::new(move || {
tgross35 Avatar tgross35 on 2025-09-03 06:30:19 UTC
View on GitHub

I guess the current impl may or may not change based on #145687 (comment), but can't this just use self.pre_exec(...)? Which just handles the as_inner_mut + Box

Qelxiros Avatar Qelxiros on 2025-09-03 12:53:47 UTC
View on GitHub

That method exists in unix::process::CommandExt. I could add it here, but that might fall outside the scope of the ACP.

library/std/src/os/fd/process.rs · outdated
43 /// eprintln!("chom");
44 ///
45 /// let output = command.output().inspect(|o| eprintln!("{o:?}")).unwrap();
46 /// assert_eq!(String::from_utf8_lossy(&output.stdout).split_whitespace().map(|s| s.parse::<usize>().unwrap()).collect::<Vec<_>>(), vec![0,1,2,3,5]);
tgross35 Avatar tgross35 on 2025-09-03 07:00:15 UTC · edited
View on GitHub

procfs isn't cross-platform, /dev is though. Exhaustively enumerating all fds also seems potentially unreliable without closing excess descriptors first, imagining an OS could have more default descriptors.

Suggestion to avoid this and also change the named file for a pipe to help avoid conflicts:

// Create a pipe between this process and the child
let (pipe_reader, mut pipe_writer) = io::pipe()?;

let fd_num = 123;

let mut cmd = Command::new("cat");
// `cat` will print the contents of a fd to its stdout
cmd.arg(format!("/dev/fd/{fd_num}"))
    .stdout(Stdio::piped())
    // And we connect the pipe to that fd
    .fd(fd_num, pipe_reader);

// Launch the process
let mut child = cmd.spawn()?;
let mut stdout = child.stdout.take().unwrap();

// And write to the pipe
pipe_writer.write_all(b"Hello, world!")?;
drop(pipe_writer);

// `cat` should read what we sent and send it back via stdout
child.wait()?;
assert_eq!(io::read_to_string(&mut stdout)?, "Hello, world!");

Ok(())
library/std/src/os/fd/process.rs · outdated
51 #[unstable(feature = "command_pass_fds", issue = "144989")]
52 impl CommandExt for Command {
53 fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
54 let old = old_fd.into().as_raw_fd();
tgross35 Avatar tgross35 on 2025-09-03 07:07:53 UTC
View on GitHub

I think I am missing something here; old_fd.into() creates an OwnedFd that isn't bound to anything, then only the integer raw fd is moved into the closure. Why doesn't the file immediately get closed when the OwnedFd is dropped, before spawn happens?

tgross35 Avatar tgross35 on 2025-09-03 07:09:59 UTC
View on GitHub

I think old_fd also needs to be moved into the child and closed there after dup. Please see #145687 (comment) (also linked above)

tgross35 Avatar tgross35 on 2025-09-03 08:16:53 UTC
View on GitHub

Yeah, the current test is a false positive; it happens to work because close seems to remap the fd to /dev/null, which shows up if you write something to the file and try to read it. Poking things slightly breaks things, e.g. using status rather than output or opening more files to change the number of existing descriptors https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=be191ed9866ec5390fbc64225564ea07

tgross35 Avatar
tgross35 on 2025-09-03 08:30:01 UTC · edited
tgross35 Avatar
tgross35 on 2025-09-03 08:30:01 UTC · edited
View on GitHub

Please make sure that all new API, even unstable, always gets document0ation and examples, as well as tests if the functionality can't be covered in a simple example. This is tricky API, make sure to cover the FD_CLOEXEC behavior in tests and include something to the effect of https://rust-lang.zulipchat.com/#narrow/channel/327149-t-libs-api.2Fapi-changes/topic/Extra.20FDs.20in.20CommandExt/near/439547420 in docs.

The tests at https://github.com/google/command-fds/tree/main can probably serve as reference.

Please make sure to note this; a single test unfortunately doesn't cover the nuances that can show up here. https://github.com/google/command-fds/blob/38670577b393c5b6f4e17b4854128cf6120a3ca1/src/lib.rs#L206 has a few test cases, I think it would be completely reasonable to port each of these to a version that matches our API.

Edit: realizing that repo is Apache-2.0 so we can't take anything directly. But tests from it that we should include are:

  • Multiple parent fds mapped to the same child fd
  • Two fds, swapped between parent and child
  • Stdin is mapped

It would also be good to test that:

  • A nonexistant fd errors on startup
  • Save the original raw fd. Ensure it remains open until the command is run, then gets closed after spawn (just fcntl(old_fd, F_GETFD) to check if it's still open)
Qelxiros Avatar
Qelxiros on 2025-09-03 14:06:42 UTC
Qelxiros Avatar
Qelxiros on 2025-09-03 14:06:42 UTC
View on GitHub

fd_test_swap is broken; I'm not confident that swapping fds like that is possible without a dedicated function since they seem to clobber each other. fd_test_close_time is also broken, at least on my machine. I'm not sure why that is, but I'd appreciate any guidance you may have.

rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-03 14:43:19 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-03 14:43:19 UTC · hidden as outdated
View on GitHub

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
failures:

---- fd_test_close_time stdout ----

thread 'fd_test_close_time' (55845) panicked at library/std/tests/fd_passing.rs:77:5:
assertion `left == right` failed
  left: 1
 right: -1
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
   4: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---- fd_test_close_time stdout end ----

failures:
    fd_test_close_time

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.45ms

error: test failed, to rerun pass `-p std --test fd_passing`
Bootstrap failed while executing `test --stage 1 core alloc std test proc_macro`
Build completed unsuccessfully in 0:05:15
  local time: Wed Sep  3 14:43:06 UTC 2025
  network time: Wed, 03 Sep 2025 14:43:06 GMT
##[error]Process completed with exit code 1.
rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-03 18:23:10 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-03 18:23:10 UTC · hidden as outdated
View on GitHub

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
failures:

---- fd_test_close_time stdout ----

thread 'fd_test_close_time' (55852) panicked at library/std/tests/fd_passing.rs:78:5:
assertion `left == right` failed
  left: 1
 right: -1
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
   4: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---- fd_test_close_time stdout end ----

failures:
    fd_test_close_time

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.26ms

error: test failed, to rerun pass `-p std --test fd_passing`
Bootstrap failed while executing `test --stage 1 core alloc std test proc_macro`
Build completed unsuccessfully in 0:05:00
  local time: Wed Sep  3 18:22:57 UTC 2025
  network time: Wed, 03 Sep 2025 18:22:57 GMT
##[error]Process completed with exit code 1.
tgross35 Avatar
tgross35 on 2025-09-04 21:14:18 UTC
tgross35 Avatar
tgross35 on 2025-09-04 21:14:18 UTC
View on GitHub

fd_test_swap is broken; I'm not confident that swapping fds like that is possible without a dedicated function since they seem to clobber each other.

That's expected with the current implementation when both fds need to be alive at the same time; this is what we should check for. The goal is to make sure we don't quietly change this behavior by accident.

fd_test_close_time is also broken, at least on my machine. I'm not sure why that is, but I'd appreciate any guidance you may have.

This one is unfortunately trickier. Linux aggressively reuses fds so it's getting mapped to something else after close, so I guess checking flags isn't enough. Instead, checking that the dev+ino are unequal should work:

use std::{fs, io};
use std::os::fd::AsRawFd;
use std::os::unix::fs::MetadataExt;

#[test]
fn whatever() {
    let (_pipe_reader, pipe_writer) = io::pipe().unwrap();

    let fd = pipe_writer.as_raw_fd();
    let fd_path = format!("/dev/fd/{fd}");

    // let mut cmd = Command::new("cat") ...

    // Get the identifier of the fd (metadata follows symlinks)
    let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open"));

    // stand in for cmd.spawn().unwrap();
    drop(pipe_writer);

    // After the child is spawned, our fd should be closed
    match fs::metadata(&fd_path) {
        // Ok; fd exists but points to a different file
        Ok(md) => assert_ne!(md_file_id(&md), fd_id),
        // Ok; fd does not exist
        Err(_) => ()
    }

    // ...
}

/// Use dev + ino to uniquely identify a file
fn md_file_id(md: &fs::Metadata) -> (u64, u64) {
    (md.dev(), md.ino())
}
tgross35 Avatar
tgross35 commented on 2025-09-04 21:16:33 UTC
library/std/tests/fd_passing.rs · outdated
tgross35 Avatar tgross35 on 2025-09-04 19:19:24 UTC
View on GitHub

These can go in library/std/src/sys/process/unix/unix/tests.rs which already has the right cfg settings

library/std/src/os/unix/process.rs · outdated
230 /// If this method is called multiple times with the same `new_fd`, all but one file descriptor
231 /// will be lost.
tgross35 Avatar tgross35 on 2025-09-04 19:19:36 UTC
View on GitHub

Move this to above the second example, since that is where it is relevant

library/std/src/os/unix/process.rs · outdated
237 /// use std::os::fd::process::CommandExt;
238 /// use std::io::{self, Write};
239 ///
240 /// fn main() -> io::Result<()> {
tgross35 Avatar tgross35 on 2025-09-04 19:22:52 UTC
View on GitHub

Nit, the error handling is hidden to make the example more compact:

    /// # fn main() -> io::Result<()> {
    /// ...
    /// # Ok(()) <- also remove the empty line before this one
    /// # }

The block can then be dedented (same for the second test)

library/std/src/os/unix/process.rs · outdated
358 let flags = cvt(libc::fcntl(new_fd, F_GETFD))?;
359 cvt(libc::fcntl(new_fd, F_SETFD, flags & !FD_CLOEXEC))?;
tgross35 Avatar tgross35 on 2025-09-04 19:24:17 UTC
View on GitHub
Suggested change
let flags = cvt(libc::fcntl(new_fd, F_GETFD))?;
cvt(libc::fcntl(new_fd, F_SETFD, flags & !FD_CLOEXEC))?;
let flags = cvt(libc::fcntl(new_fd, libc::F_GETFD))?;
cvt(libc::fcntl(new_fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC))?;

Nit, usually we keep it obvious what comes from libc (also lets us gate in a single place if needed)

library/std/tests/fd_passing.rs · outdated
15 let fd_num = 0;
16
17 let mut cmd = Command::new("cat");
18 cmd.stdout(Stdio::piped()).fd(fd_num, pipe_reader);
tgross35 Avatar tgross35 on 2025-09-04 19:27:33 UTC
View on GitHub
Suggested change
cmd.stdout(Stdio::piped()).fd(fd_num, pipe_reader);
cmd.stdout(Stdio::piped()).fd(libc::STDIN_FILENO, pipe_reader);

Avoiding magic numbers

library/std/tests/fd_passing.rs · outdated
36 let num2 = pipe_reader2.as_raw_fd();
37
38 let mut cmd = Command::new("cat");
39 cmd.arg(format!("/dev/fd/{}", num1))
tgross35 Avatar tgross35 on 2025-09-04 19:29:48 UTC
View on GitHub
Suggested change
cmd.arg(format!("/dev/fd/{}", num1))
cmd.arg(format!("/dev/fd/{}", num1))
.arg(format!("/dev/fd/{}", num2))

If cat is given multiple files it will print their output back-to-back, so the output from both can be checked (one will be clobbered, add a comment saying that is expected)

library/std/tests/fd_passing.rs · outdated
55 }
56
57 #[test]
58 fn fd_test_close_time() {
tgross35 Avatar tgross35 on 2025-09-04 19:30:19 UTC
View on GitHub

The test here isn't the most obvious, could you add a comment about what we are checking?

library/std/tests/fd_passing.rs · outdated
65 let mut cmd = Command::new("cat");
66 cmd.arg(format!("/dev/fd/{fd_num}")).stdout(Stdio::piped()).fd(fd_num, pipe_reader);
67
68 assert_ne!(unsafe { fcntl(original, F_GETFD) }, -1);
tgross35 Avatar tgross35 on 2025-09-04 19:32:50 UTC
View on GitHub

When checking system errors always prefer comparing with >/< rather than ==. More robust if some specific system returns an error code rather than specifically "-1"

Suggested change
assert_ne!(unsafe { fcntl(original, F_GETFD) }, -1);
// Ensure that the original fd remains open before we spawn
assert!(unsafe { fcntl(original, F_GETFD) } >= 0);
tgross35 Avatar
tgross35 commented on 2025-09-04 21:32:33 UTC
library/std/tests/fd_passing.rs · outdated
tgross35 Avatar tgross35 on 2025-09-04 21:32:32 UTC
View on GitHub

Also add a test that checks for errors if a garbage fd is passed. Something like

// Check that an invalid fd causes an error
fn whatever() {
    let mut cmd = Command::new("echo");
    
    // Pick a fd that is unlikely to be used, and sanity check that it is indeed closed
    // NOTE: This is not TOCTOU-safe, technically the OS could assign `fd` before we try to spawn
    // the command. However, this is unlikely and there isn't really a better way to ensure a fd
    // remains unassigned.
    let fd = c_int::MAX / 2;
    assert!(unsafe { fcntl(fd, F_GETFD) } < 0);

    cmd.fd(fd, fd);
    assert!(cmd.spawn().is_err());
}
Qelxiros Avatar Qelxiros on 2025-09-07 19:09:50 UTC
View on GitHub

This test doesn't compile because the second parameter of fd is of type OwnedFd. I could construct a garbage one with unsafe, but at that point it's up to the user to determine that the fd is valid, right?

tgross35 Avatar tgross35 on 2025-09-07 19:13:15 UTC
View on GitHub

You're right! Ignore me here

Qelxiros Avatar
Qelxiros on 2025-09-07 19:13:06 UTC
Qelxiros Avatar
Qelxiros on 2025-09-07 19:13:06 UTC
View on GitHub

Even when checking device/inode values, fd_test_close_time fails on my machine. I think that guarantees that the file descriptor is staying open longer than it should, but I have no idea why. I'm a little out of my depth here, but I'll try to look into it. Let me know if you think of anything else.

rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-07 19:53:27 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-07 19:53:27 UTC · hidden as outdated
View on GitHub

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
test sys::process::unix::common::tests::test_setsid_no_posix_spawn ... ok
test sys::process::unix::common::tests::test_setsid_posix_spawn ... ok
test sys::process::unix::common::tests::unix_exit_statuses ... ok
test sys::process::unix::unix::tests::exitstatus_display_tests ... ok
test sys::process::unix::unix::tests::fd_test_close_time ... FAILED
test sys::process::unix::unix::tests::fd_test_stdin ... ok
cat: /dev/fd/16: No such file or directory
test sys::process::unix::unix::tests::fd_test_swap ... ok
aborting due to panic at library/std/src/sys/process/unix/unix/tests.rs:68:27:
crash now!
test net::udp::tests::test_read_with_timeout ... ok
test sys::process::unix::unix::unsupported_wait_status::tests::compare_with_linux ... ok
test sys::thread_local::key::tests::destructors ... ok
tgross35 Avatar
tgross35 requested changes on 2025-09-25 11:51:00 UTC
library/std/src/os/unix/process.rs · resolved
351 fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
352 let old = old_fd.into();
353 unsafe {
354 self.pre_exec(move || {
355 cvt_r(|| libc::dup2(old.as_raw_fd(), new_fd))?;
356 let flags = cvt(libc::fcntl(new_fd, libc::F_GETFD))?;
357 cvt(libc::fcntl(new_fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC))?;
358 cvt_r(|| libc::close(old.as_raw_fd()))?;
359 Ok(())
360 })
361 }
362 }
tgross35 Avatar tgross35 on 2025-09-25 11:48:07 UTC
View on GitHub

After thinking about this a bit more, I think I overlooked a pretty obvious answer; this gets run after fork (and before exec) so it's working with its own copy of old_fd FD rather than the original. IOW the close here is closing the child's copy of the fd, but the parent's still exists and is not accessible from the child, so it winds up leaked. I think there might actually be a double close here because there's the explicit libc::close and the drop of old_fd (usually we warn on this but I'm guessing panics don't work quite right after fork).

I don't think there is a good way to do this in pre_exec because the parent's copy needs to get cleaned up only after a successful call to fork since that's the point where the child has its own fds keeping the resources open.

Would you be up for changing to the alternative discussed around #145687 (comment)? It would roughly look like:

  • Add a Vec<(OwnedFd, RawFd)> to the Unix command
  • .fd(...) pushes an entry
  • Update the default strategy posix_spawn at :
    • Call posix_spawn_file_actions_adddup2 for each vec entry
    • Call posix_spawn_file_actions_addclose for each original
  • Update the fork+exec fallback in the same file, basically move what's here
  • After spawning, clear the vec so the OwnedFDs get dropped and closed in the parent process
  • Run existing tests once for each implementation. Probably easiest to turn the existing #[test]s into regular functions with a use_exec: bool arg, and if true adds a dummy pre_exec closure to force the fork+exec fallback.
library/std/src/sys/process/unix/unix/tests.rs · outdated
135 #[test]
136 fn fd_test_close_time() {
137 let (_pipe_reader, pipe_writer) = io::pipe().unwrap();
138
139 let fd = pipe_writer.as_raw_fd();
140 let fd_path = format!("/dev/fd/{fd}");
141
142 let mut cmd = Command::new("true");
143 cmd.fd(123, pipe_writer);
144
145 // Get the identifier of the fd (metadata follows symlinks)
146 let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open"));
147
148 cmd.spawn().unwrap().wait().unwrap();
149
150 // After the child is spawned, our fd should be closed
151 match fs::metadata(&fd_path) {
152 // Ok; fd exists but points to a different file
153 Ok(md) => assert_ne!(md_file_id(&md), fd_id),
154 // Ok; fd does not exist
155 Err(_) => (),
156 }
157 }
tgross35 Avatar tgross35 on 2025-09-25 11:50:55 UTC
View on GitHub

I think this should be enhanced by checking what FDs are available from the child too, the same IDs can be obtained from the stat CLI tool. I'll need to think about this a bit.

rustbot Avatar
rustbot on 2025-09-25 13:12:58 UTC · hidden as resolved
rustbot Avatar
rustbot on 2025-09-25 13:12:58 UTC · hidden as resolved
View on GitHub

⚠️ Warning ⚠️

rustbot Avatar
rustbot on 2025-09-25 13:14:34 UTC · hidden as outdated
rustbot Avatar
rustbot on 2025-09-25 13:14:34 UTC · hidden as outdated
View on GitHub

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-25 14:02:00 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-09-25 14:02:00 UTC · hidden as outdated
View on GitHub

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
test sys::process::unix::common::tests::test_setsid_no_posix_spawn ... ok
test sys::process::unix::common::tests::test_setsid_posix_spawn ... ok
test sys::process::unix::common::tests::unix_exit_statuses ... ok
test sys::process::unix::unix::tests::exitstatus_display_tests ... ok
cat: /dev/fd/16: No such file or directory
test sys::process::unix::unix::tests::fd_tests_exec ... ok
cat: /dev/fd/16: No such file or directory
test sys::process::unix::unix::tests::fd_tests_posix_spawn ... FAILED
aborting due to panic at library/std/src/sys/process/unix/unix/tests.rs:69:27:
crash now!
test net::udp::tests::test_read_with_timeout ... ok
test sys::process::unix::unix::unsupported_wait_status::tests::compare_with_linux ... ok
test sys::thread_local::key::tests::destructors ... ok
---
failures:

---- sys::process::unix::unix::tests::fd_tests_posix_spawn stdout ----

thread 'sys::process::unix::unix::tests::fd_tests_posix_spawn' (56320) panicked at library/std/src/sys/process/unix/unix/tests.rs:169:19:
assertion `left != right` failed
  left: (15, 153798)
 right: (15, 153798)
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
tgross35 Avatar
tgross35 commented on 2025-09-26 00:58:55 UTC
tgross35 left a comment · edited
View on GitHub

I think the test failure comes from not dropping the owned fds after a successful posix spawn. But the GH mobile review UI is so bad I honestly have no idea, I’ll look again later.

View changes since this review

library/std/src/sys/process/unix/unix.rs · outdated
124 125 drop(env_lock);
125 126 drop(output);
126 127
128 self.clear_fds();
tgross35 Avatar tgross35 on 2025-09-26 00:52:05 UTC
View on GitHub

Maybe name this function close_owned_fds to make it obvious why it’s being called.

library/std/src/sys/process/unix/unix/tests.rs · outdated
152 let mut cmd = Command::new("true");
153 cmd.fd(123, pipe_writer);
154
155 if use_exec {
tgross35 Avatar tgross35 on 2025-09-26 00:57:45 UTC
View on GitHub

I think it would be good to add a last_spawn_was_posix_spawn: Option<bool> field that gets set to None at the start of the spawn call, then Some(true) if posix_spawn succeeds and Some(false) if exec succeeds. Then we can assert that the right strategy was used on platforms that support it (which is more useful than just here).

Qelxiros Avatar
Qelxiros on 2025-10-06 19:56:47 UTC
Qelxiros Avatar
Qelxiros on 2025-10-06 19:56:47 UTC
View on GitHub

@rustbot ready

tgross35 Avatar
tgross35 commented on 2025-10-16 21:49:32 UTC
tgross35 left a comment · edited
View on GitHub

I think this looks pretty good! Left some suggested comments since the logic here can be tricky, but with those and assuming the try job passes LGTM.

@dominik-korsa if you get a chance to review as well, that would be appreciated.

@bors2 try

View changes since this review

library/std/src/sys/process/unix/common.rs · resolved
104 104 pgroup: Option<pid_t>,
105 105 setsid: bool,
106 fds: Vec<(OwnedFd, RawFd)>,
107 last_spawn_was_posix_spawn: Option<bool>,
tgross35 Avatar tgross35 on 2025-10-16 20:42:21 UTC
View on GitHub

Could use a comment like

For testing purposes, store Some(true) if the last spawn used posix_spawn, Some(false) if exec was used, and None if this hasn't been spawned yet.

library/std/src/sys/process/unix/common.rs · resolved
103 103 create_pidfd: bool,
104 104 pgroup: Option<pid_t>,
105 105 setsid: bool,
106 fds: Vec<(OwnedFd, RawFd)>,
tgross35 Avatar tgross35 on 2025-10-16 20:43:11 UTC
View on GitHub

Could use a comment like

A map of parent FDs to child FDs to be inherited during spawn.

library/std/src/sys/process/unix/common.rs · resolved
376 pub fn close_owned_fds(&mut self) {
377 self.fds.clear();
378 }
tgross35 Avatar tgross35 on 2025-10-16 20:44:07 UTC
View on GitHub
Suggested change
pub fn close_owned_fds(&mut self) {
self.fds.clear();
}
/// Clear the fd vector, closing all descriptors owned by this `Command`.
pub fn close_owned_fds(&mut self) {
self.fds.clear();
}
library/std/src/sys/process/unix/unix.rs · outdated
718 729 ))?;
719 730 }
731 for &(ref old_fd, new_fd) in self.get_fds() {
732 use crate::os::fd::AsRawFd;
tgross35 Avatar tgross35 on 2025-10-16 20:46:04 UTC
View on GitHub

This import can go at the top of the posix_spawn function

library/std/src/sys/process/unix/unix/tests.rs · outdated
101 let mut child = cmd.spawn().unwrap();
102 let mut stdout = child.stdout.take().unwrap();
103
104 assert_ne!(cmd.last_spawn_was_posix_spawn().unwrap_or(false), use_exec);
tgross35 Avatar tgross35 on 2025-10-16 20:52:57 UTC
View on GitHub

These can all use .unwrap() rather than unwrap_or right? Since it's unconditionally set when spawning.

tgross35 Avatar tgross35 on 2025-10-16 21:13:21 UTC
View on GitHub

Ah we actually also have to account for platforms that don't support posix_spawn. I think it might be easiest to just put this in a separate function.

Taking that config from fn posix_spawn in unix.rs:

#[track_caller]
fn assert_spawn_method(cmd: &Command, use_exec: bool) {
    let used_posix_spawn = cmd.last_spawn_was_posix_spawn().unwrap();
    if use_exec {
        assert!(!used_posix_spawn, "posix_spawn used but exec was expected");
    } else if cfg!(any(
        target_os = "freebsd",
        target_os = "illumos",
        all(target_os = "linux", target_env = "gnu"),
        all(target_os = "linux", target_env = "musl"),
        target_os = "nto",
        target_vendor = "apple",
        target_os = "cygwin",
    )) {
        assert!(used_posix_spawn, "platform supports posix_spawn but it wasn't used");
    } else {
        assert!(!used_posix_spawn);
    }
}
library/std/src/sys/process/unix/unix/tests.rs · resolved
74 83 || signal == libc::SIGSEGV
75 84 );
76 85 }
86
tgross35 Avatar tgross35 on 2025-10-16 20:57:36 UTC
View on GitHub

Group these into a module so they can get a combined doc comment:

/// For `Command`'s fd-related tests, we want to be sure they work both with exec
/// and with `posix_spawn`. We test both the default which should use `posix_spawn`
/// on supported platforms, and using `pre_exec` to force spawn using `exec`.
mod fd_impls {
    fn test_stdin(use_exec: bool) { ... }
    ...
}

#[test]
fn fd_tests_posix_spawn() {
    fd_impls:test_stdin(false);
    ...
}
library/std/src/sys/process/unix/unix/tests.rs · outdated
75 84 );
76 85 }
86
87 fn fd_test_stdin(use_exec: bool) {
tgross35 Avatar tgross35 on 2025-10-16 20:59:50 UTC
View on GitHub
Suggested change
fn fd_test_stdin(use_exec: bool) {
/// Check setting the child's stdin via `.fd`.
fn fd_test_stdin(use_exec: bool) {
library/std/src/sys/process/unix/unix.rs · resolved
72 73
73 74 if let Some(ret) = self.posix_spawn(&theirs, envp.as_ref())? {
75 self.last_spawn_was_posix_spawn(true);
76 self.close_owned_fds();
tgross35 Avatar tgross35 on 2025-10-16 21:18:27 UTC
View on GitHub
Suggested change
self.close_owned_fds();
// Close fds in the parent that have been duplicated to the child
self.close_owned_fds();
library/std/src/sys/process/unix/unix/tests.rs · outdated
110 assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello, world!");
111 }
112
113 fn fd_test_swap(use_exec: bool) {
tgross35 Avatar tgross35 on 2025-10-16 21:38:45 UTC
View on GitHub
/// Check that the last `.fd` mapping is preserved when there are conflicts.
fn fd_test_swap(use_exec: bool) { ... }
library/std/src/sys/process/unix/unix/tests.rs · resolved
190 fd_test_stdin(true);
191 fd_test_swap(true);
192 fd_test_close_time(true);
193 }
tgross35 Avatar tgross35 on 2025-10-16 21:45:15 UTC
View on GitHub

Nit so failures point at the behavior rather than at spawn vs. exec

Suggested change
}
#[test]
fn fd_test_stdin() {
fd_imp::test_stdin(false);
fd_imp::test_stdin(true);
}
#[test]
fn fd_test_swap() {
fd_imp::test_swap(false);
fd_imp::test_swap(true);
}
#[test]
fn fd_test_close_time() {
fd_imp::test_close_time(false);
fd_imp::test_close_time(true);
}
rust-bors Avatar
rust-bors on 2025-10-16 21:49:37 UTC · hidden as outdated
rust-bors Avatar
rust-bors on 2025-10-16 21:49:37 UTC · hidden as outdated
View on GitHub

⌛ Trying commit fe30064 with merge 35fbbee

To cancel the try build, run the command @bors try cancel.

Workflow: https://github.com/rust-lang/rust/actions/runs/18575740493

rust-log-analyzer Avatar
rust-log-analyzer on 2025-10-16 23:13:07 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-10-16 23:13:07 UTC · hidden as outdated
View on GitHub

The job arm-android failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
explicit panic
stack backtrace:
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
test thread::tests::test_join_panic ... ok
cat: test sys::process::unix::unix::tests::fd_tests_exec ... FAILED
/dev/fd/6: No such file or directory
cat: /dev/fd/10: No such file or directory
test thread::tests::test_park_timeout_unpark_before ... ok
test thread::tests::test_named_thread ... ok
test thread::tests::test_park_timeout_unpark_not_called ... ok
test thread::tests::test_park_unpark_before ... ok
test thread::tests::test_park_timeout_unpark_called_other_thread ... ok
---
failures:

---- sys::process::unix::unix::tests::fd_tests_posix_spawn stdout ----

thread 'sys::process::unix::unix::tests::fd_tests_posix_spawn' (10973) panicked at library/std/src/sys/process/unix/unix/tests.rs:104:5:
assertion `left != right` failed
  left: false
 right: false
stack backtrace:
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---- sys::process::unix::unix::tests::fd_tests_posix_spawn stdout end ----
---- sys::process::unix::unix::tests::fd_tests_exec stdout ----

thread 'sys::process::unix::unix::tests::fd_tests_exec' (10971) panicked at library/std/src/sys/process/unix/unix/tests.rs:146:5:
assertion `left == right` failed
  left: ""
 right: "Hello from pipe 1!"
stack backtrace:
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---- sys::process::unix::unix::tests::fd_tests_exec stdout end ----

failures:
    sys::process::unix::unix::tests::fd_tests_posix_spawn
    sys::process::unix::unix::tests::fd_tests_exec

rust-bors Avatar
rust-bors on 2025-10-16 23:13:41 UTC
rust-bors Avatar
rust-bors on 2025-10-16 23:13:41 UTC
View on GitHub

💔 Test for 35fbbee failed: CI. Failed jobs:

tgross35 Avatar
tgross35 on 2025-10-20 19:17:08 UTC
tgross35 Avatar
tgross35 on 2025-10-20 19:17:08 UTC
View on GitHub

@rustbot author

rustbot Avatar
rustbot on 2025-10-20 19:17:12 UTC
rustbot Avatar
rustbot on 2025-10-20 19:17:12 UTC
View on GitHub

Reminder, once the PR becomes ready for a review, use @rustbot ready.

tgross35 Avatar
tgross35 on 2025-10-20 19:18:18 UTC · edited
tgross35 Avatar
tgross35 on 2025-10-20 19:18:18 UTC · edited
View on GitHub

I'm actually going to reroll for the final review since a lot of the implementation and tests here came from me.

r? libs

tgross35 Avatar
tgross35 on 2025-10-20 19:18:43 UTC
tgross35 Avatar
tgross35 on 2025-10-20 19:18:43 UTC
View on GitHub

Ibraheem has a lot of reviews

r? libs

rustbot Avatar
rustbot on 2025-10-30 14:36:36 UTC · hidden as resolved
rustbot Avatar
rustbot on 2025-10-30 14:36:36 UTC · hidden as resolved
View on GitHub

⚠️ Warning ⚠️

rustbot Avatar
rustbot on 2025-10-30 14:37:26 UTC · hidden as outdated
rustbot Avatar
rustbot on 2025-10-30 14:37:26 UTC · hidden as outdated
View on GitHub

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

tgross35 Avatar
tgross35 on 2025-10-30 18:06:14 UTC
tgross35 Avatar
tgross35 on 2025-10-30 18:06:14 UTC
View on GitHub

@bors2 try

rust-bors Avatar
rust-bors on 2025-10-30 18:06:18 UTC · hidden as outdated
rust-bors Avatar
rust-bors on 2025-10-30 18:06:18 UTC · hidden as outdated
View on GitHub

⌛ Trying commit dd4c41c with merge bb5247b

To cancel the try build, run the command @bors try cancel.

Workflow: https://github.com/rust-lang/rust/actions/runs/18950582240

rust-log-analyzer Avatar
rust-log-analyzer on 2025-10-30 19:36:35 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2025-10-30 19:36:35 UTC · hidden as outdated
View on GitHub

The job arm-android failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
test sys::process::unix::common::tests::unix_exit_statuses ... ok
test sys::process::unix::unix::tests::exitstatus_display_tests ... ok
test sys::process::unix::unix::tests::fd_test_close_time ... FAILED
test sys::personality::dwarf::tests::dwarf_reader ... ok
cat: /dev/fd/5: No such file or directory
cat: /dev/fd/18: No such file or directory
test sys::process::unix::unix::tests::fd_test_swap ... FAILED
aborting due to panic at library/std/src/sys/process/unix/unix/tests.rs:67:27:
crash now!
test sys::process::unix::unix::tests::fd_test_stdin ... ok
test sys::thread_local::key::tests::destructors ... ok
test sys::thread_local::key::tests::smoke ... ok
---
---- sys::process::unix::unix::tests::fd_test_swap stdout ----

thread 'sys::process::unix::unix::tests::fd_test_swap' (10921) panicked at library/std/src/sys/process/unix/unix/tests.rs:157:9:
assertion `left == right` failed
  left: ""
 right: "Hello from pipe 1!"
stack backtrace:
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
---- sys::process::unix::unix::tests::fd_test_swap stdout end ----

failures:
rust-bors Avatar
rust-bors on 2025-10-30 19:37:06 UTC
rust-bors Avatar
rust-bors on 2025-10-30 19:37:06 UTC
View on GitHub

💔 Test for bb5247b failed: CI. Failed jobs:

Qelxiros Avatar
Qelxiros on 2025-11-04 15:49:27 UTC
Qelxiros Avatar
Qelxiros on 2025-11-04 15:49:27 UTC
View on GitHub

It looks like the behavior of test_swap is platform-dependent. I don't have easy access to non-Linux machines for testing purposes, so I've removed the test.

@rustbot ready

Mark-Simulacrum Avatar
Mark-Simulacrum commented on 2025-11-08 15:13:17 UTC
library/std/src/os/unix/process.rs · resolved
222 ///
223 /// If `new_fd` is an open file descriptor and closing it would produce one or more errors,
224 /// those errors will be lost when this function is called. See
225 /// [`man 2 dup`](https://www.man7.org/linux/man-pages/man2/dup.2.html#NOTES) for more information.
Mark-Simulacrum Avatar Mark-Simulacrum on 2025-11-08 15:13:17 UTC
View on GitHub

We should clarify somewhere what the difference between new_fd and old_fd is, right?

(And new_fd is not invalidated in the parent process, only the child, right?)

Mark-Simulacrum Avatar
Mark-Simulacrum commented on 2025-11-08 15:36:29 UTC
library/std/src/os/unix/process.rs · outdated
300 ///
301 /// [`posix_spawn`]: https://www.man7.org/linux/man-pages/man3/posix_spawn.3.html
302 #[unstable(feature = "command_pass_fds", issue = "144989")]
303 fn last_spawn_was_posix_spawn(&self) -> Option<bool>;
Mark-Simulacrum Avatar Mark-Simulacrum on 2025-11-08 15:36:25 UTC
View on GitHub

This doesn't seem mentioned in the ACP or tracking issue, and seems like an odd place to put this. It's also not clear to me why this is on Command vs on Child?

I'm not convinced we want to make this a stable API -- posix_spawn feels like a low level detail rather than something callers should care about. If we do want this exposed we should explain what callers are supposed to do with the information.

Qelxiros Avatar Qelxiros on 2025-11-17 17:22:41 UTC
View on GitHub

This is for testing purposes (see #145687 (comment)). Would it be better to mark it perma-unstable? (I'm not sure exactly how to do that)

I don't think it can be private because tests are treated as separate crates.

tgross35 Avatar tgross35 on 2025-11-19 12:25:28 UTC
View on GitHub

It should be possible to make it private. Integration tests in library/std/tests are treated as separate crates, but these are in src/ so can use internal API.

rustbot Avatar
rustbot on 2025-11-17 17:21:13 UTC · hidden as outdated
rustbot Avatar
rustbot on 2025-11-17 17:21:13 UTC · hidden as outdated
View on GitHub

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

Qelxiros Avatar
Qelxiros on 2025-11-18 01:42:17 UTC
Qelxiros Avatar
Qelxiros on 2025-11-18 01:42:17 UTC
View on GitHub

@rustbot ready

tgross35 Avatar
tgross35 on 2025-11-19 12:35:07 UTC
tgross35 Avatar
tgross35 on 2025-11-19 12:35:07 UTC
View on GitHub

It looks like the behavior of test_swap is platform-dependent. I don't have easy access to non-Linux machines for testing purposes, so I've removed the test.

One failing platform certainly isn't a reason to remove a completely valid test entirely. Mark it #[cfg_attr(not(target_os = "android"), should_panic)] (and any other platforms that fail) with a FIXME, it will be worth seeing if we can fix this somehow given users are probably going to expect consistent behavior here.

tgross35 Avatar
tgross35 commented on 2025-11-19 12:42:22 UTC
library/std/src/sys/process/unix/unix/tests.rs · outdated
116 pipe_writer.write_all(b"Hello, world!").unwrap();
117 drop(pipe_writer);
118
119 child.wait().unwrap();
tgross35 Avatar tgross35 on 2025-11-19 12:42:23 UTC
View on GitHub
Suggested change
child.wait().unwrap();
child.wait().unwrap().exit_ok().unwrap();

Verify the child didn't run into errors, applies a few places

rustbot Avatar
rustbot on 2026-01-04 17:04:28 UTC · hidden as resolved
rustbot Avatar
rustbot on 2026-01-04 17:04:28 UTC · hidden as resolved
View on GitHub

⚠️ Warning ⚠️

rustbot Avatar
rustbot on 2026-01-04 17:06:54 UTC · hidden as outdated
rustbot Avatar
rustbot on 2026-01-04 17:06:54 UTC · hidden as outdated
View on GitHub

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

rust-log-analyzer Avatar
rust-log-analyzer on 2026-01-04 17:40:06 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2026-01-04 17:40:06 UTC · hidden as outdated
View on GitHub

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)

error[E0599]: no method named `as_inner` found for reference `&process::Command` in the current scope
   --> library/std/src/sys/process/unix/unix/tests.rs:216:32
    |
216 |     let used_posix_spawn = cmd.as_inner().get_last_spawn_was_posix_spawn().unwrap();
    |                                ^^^^^^^^
    |
    = help: items from traits can only be used if the trait is in scope
help: trait `AsInner` which provides `as_inner` is implemented but not in scope; perhaps you want to import it
    |
  1 + use crate::sys::AsInner;
    |
help: there is a method `as_inner_mut` with a similar name
    |
216 |     let used_posix_spawn = cmd.as_inner_mut().get_last_spawn_was_posix_spawn().unwrap();
    |                                        ++++

Some errors have detailed explanations: E0432, E0599.
For more information about an error, try `rustc --explain E0432`.
[RUSTC-TIMING] std test:true 18.068
rust-log-analyzer Avatar
rust-log-analyzer on 2026-01-14 17:12:47 UTC · hidden as outdated
rust-log-analyzer Avatar
rust-log-analyzer on 2026-01-14 17:12:47 UTC · hidden as outdated
View on GitHub

The job pr-check-2 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
test sys::process::unix::common::tests::test_setsid_no_posix_spawn ... ok
test sys::process::unix::common::tests::test_setsid_posix_spawn ... ok
test sys::process::unix::common::tests::unix_exit_statuses ... ok
test sys::process::unix::unix::tests::exitstatus_display_tests ... ok
test sys::process::unix::unix::tests::fd_test_close_time ... ok
test sys::process::unix::unix::tests::fd_test_stdin ... ok
cat: /dev/fd/16: No such file or directory
test sys::process::unix::unix::tests::fd_test_swap ... FAILED
aborting due to panic at library/std/src/sys/process/unix/unix/tests.rs:68:27:
crash now!
test net::udp::tests::test_read_with_timeout ... ok
test sys::process::unix::unix::unsupported_wait_status::tests::compare_with_linux ... ok
test sys::thread_local::key::tests::destructors ... ok
rust-bors Avatar
rust-bors on 2026-02-05 15:45:35 UTC · hidden as outdated
rust-bors Avatar
rust-bors on 2026-02-05 15:45:35 UTC · hidden as outdated
View on GitHub

☔ The latest upstream changes (presumably #152163) made this pull request unmergeable. Please resolve the merge conflicts.

rustbot Avatar
rustbot on 2026-03-06 15:56:52 UTC
rustbot Avatar
rustbot on 2026-03-06 15:56:52 UTC
View on GitHub

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

joboet Avatar
joboet commented on 2026-03-06 16:34:04 UTC
library/std/src/sys/process/unix/unix.rs
293 301 }
294 302
303 for &(ref old_fd, new_fd) in self.get_fds() {
304 cvt_r(|| libc::dup2(old_fd.as_raw_fd(), new_fd))?;
joboet Avatar joboet on 2026-03-06 16:34:04 UTC
View on GitHub

This is problematic and probably unsound (as it violates I/O safety) if new_fd is accidentally equal to the file descriptor number that is used by the pipe or socket used to return errors. That would lead to spawn inaccurately reporting a success, even if an error occurs (since this half of the pipe will be closed).

Qelxiros Avatar Qelxiros on 2026-03-07 16:40:19 UTC
View on GitHub

Is there a way to check for that? If not, it seems impossible to avoid.

Qelxiros Avatar Qelxiros on 2026-03-16 19:05:31 UTC
View on GitHub

@joboet friendly nudge :)

joboet Avatar joboet on 2026-03-16 19:41:05 UTC
View on GitHub

You could use dup2 (ideally std's try_clone wrapper) to clone the pipe/socket to a different descriptor and then close the original, effectively moving the descriptor.

Qelxiros Avatar Qelxiros on 2026-03-30 17:43:29 UTC
View on GitHub

Maybe I don't understand the issue. If new_fd might collide with the pipe/socket, then it might still collide after I move the pipe/socket, right?

mmastrac Avatar
mmastrac commented on 2026-04-02 20:13:32 UTC
library/std/src/sys/process/unix/unix.rs
736 new_fd,
737 ))?;
738 cvt_nz(libc::posix_spawn_file_actions_addclose(
739 file_actions.0.as_mut_ptr(),
mmastrac Avatar mmastrac on 2026-04-02 20:13:32 UTC
View on GitHub

If old_fd == new_fd (which is useful for clearing FD_CLOEXEC), I think the adddup2 should run, but addclose should be skipped.