Skip to content

Conversation

@Qelxiros
Copy link
Contributor

@Qelxiros Qelxiros commented Aug 20, 2025

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

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

@rustbot
Copy link
Collaborator

rustbot commented Aug 20, 2025

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

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Aug 20, 2025
Copy link
Contributor

@tgross35 tgross35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 27, 2025
@Qelxiros Qelxiros requested a review from tgross35 August 27, 2025 21:56
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Aug 27, 2025
@rust-log-analyzer

This comment has been minimized.

Comment on lines 52 to 111
impl CommandExt for Command {
fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
let old = old_fd.into().as_raw_fd();
unsafe {
self.as_inner_mut().pre_exec(Box::new(move || {
cvt_r(|| libc::dup2(old, new_fd))?;
let flags = cvt(libc::fcntl(new_fd, F_GETFD))?;
cvt(libc::fcntl(new_fd, F_SETFD, flags & !FD_CLOEXEC))?;
cvt_r(|| libc::close(old))?;
Ok(())
}))
}

self
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

@dominik-korsa dominik-korsa Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 3, 2025
@tgross35
Copy link
Contributor

tgross35 commented Sep 3, 2025

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
Copy link
Contributor Author

Qelxiros commented Sep 3, 2025

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

This comment has been minimized.

@rustbot rustbot added the O-unix Operating system: Unix-like label Sep 3, 2025
@rust-log-analyzer

This comment has been minimized.

@tgross35
Copy link
Contributor

tgross35 commented Sep 4, 2025

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())
}

@Qelxiros
Copy link
Contributor Author

Qelxiros commented Sep 7, 2025

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.

@tgross35
Copy link
Contributor

Ibraheem has a lot of reviews

r? libs

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@tgross35
Copy link
Contributor

@bors2 try

@rust-bors

This comment has been minimized.

rust-bors bot added a commit that referenced this pull request Oct 30, 2025
add std::os::unix::process::CommandExt::fd

try-job: aarch64-apple
try-job: arm-android
try-job: test-various
@rust-log-analyzer

This comment has been minimized.

@rust-bors
Copy link

rust-bors bot commented Oct 30, 2025

💔 Test for bb5247b failed: CI. Failed jobs:

@Qelxiros
Copy link
Contributor Author

Qelxiros commented Nov 4, 2025

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

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Nov 4, 2025
///
/// [`posix_spawn`]: https://www.man7.org/linux/man-pages/man3/posix_spawn.3.html
#[unstable(feature = "command_pass_fds", issue = "144989")]
fn last_spawn_was_posix_spawn(&self) -> Option<bool>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Mark-Simulacrum Mark-Simulacrum added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 8, 2025
@rustbot
Copy link
Collaborator

rustbot commented Nov 17, 2025

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
Copy link
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Nov 18, 2025
@tgross35
Copy link
Contributor

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.

pipe_writer.write_all(b"Hello, world!").unwrap();
drop(pipe_writer);

child.wait().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
child.wait().unwrap();
child.wait().unwrap().exit_ok().unwrap();

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

@Mark-Simulacrum Mark-Simulacrum added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

O-unix Operating system: Unix-like S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants