Skip to content

Conversation

@the8472
Copy link
Member

@the8472 the8472 commented Jun 22, 2024

glibc 2.39 added pidfd_spawnp and pidfd_getpid which makes it possible to get pidfds while staying on the CLONE_VFORK path.

verified that vfork gets used with strace:

$ strace -ff -e pidfd_open,clone3,openat,execve,waitid,close ./x test std --no-doc -- pidfd [...] [pid 2820532] clone3({flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|CLONE_CLEAR_SIGHAND, pidfd=0x7b7f885fec6c, exit_signal=SIGCHLD, stack=0x7b7f88aff000, stack_size=0x9000}strace: Process 2820533 attached <unfinished ...> [pid 2820533] execve("/home/the8472/bin/sleep", ["sleep", "1000"], 0x7ffdd0e268d8 /* 107 vars */) = -1 ENOENT (No such file or directory) [pid 2820533] execve("/home/the8472/.cargo/bin/sleep", ["sleep", "1000"], 0x7ffdd0e268d8 /* 107 vars */) = -1 ENOENT (No such file or directory) [pid 2820533] execve("/usr/local/bin/sleep", ["sleep", "1000"], 0x7ffdd0e268d8 /* 107 vars */) = -1 ENOENT (No such file or directory) [pid 2820533] execve("/usr/bin/sleep", ["sleep", "1000"], 0x7ffdd0e268d8 /* 107 vars */ <unfinished ...> [pid 2820532] <... clone3 resumed> => {pidfd=[3]}, 88) = 2820533 [pid 2820533] <... execve resumed>) = 0 [pid 2820532] openat(AT_FDCWD, "/proc/self/fdinfo/3", O_RDONLY|O_CLOEXEC) = 4 [pid 2820532] close(4) = 0 

Tracking issue: #82971

@rustbot
Copy link
Collaborator

rustbot commented Jun 22, 2024

r? @workingjubilee

rustbot has assigned @workingjubilee.
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 O-unix Operating system: Unix-like 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 Jun 22, 2024
@rust-log-analyzer

This comment has been minimized.

Comment on lines +707 to +736
// The child has been spawned and we are holding its pidfd.
// But we cannot obtain its pid even though pidfd_getpid support was verified earlier.
// This might happen if libc can't open procfs because the file descriptor limit has been reached.
libc::close(pidfd);
return Err(Error::new(
e.kind(),
"pidfd_spawnp succeeded but the child's PID could not be obtained",
));
Copy link
Member Author

Choose a reason for hiding this comment

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

In their unfathomable wisdom the glibc developers have not designed pidfd_spawnp to return the pid and pidfd at the same time even though clone3 does support it, which means we can get into this awkward halfway state.

I'm not sure if this is the best solution for the edge-case. An alternative is to change it to Process { pid: Option<NonZero<pid_t>> } and delay the error reporting by only panicking if someone tries to call Child.id() if the when a pid couldn't be obtained.

Copy link
Member

Choose a reason for hiding this comment

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

lmao.

Copy link
Member

Choose a reason for hiding this comment

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

I think for right now this is best because I think assuming calling .id() won't panic is pretty reasonable and this will let us find out if we do in fact have to make that change. At a glance, it seems like a bit of a "it almost-never really happens and there's no reasonable response" scenario, in which case failing fast is better?

Comment on lines +510 to +524
if self.get_create_pidfd() {
unreachable!("only implemented on linux")
}
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. This currently returns false for all other platforms?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, pidfd is a linux feature

Comment on lines 465 to 526
if #[cfg(target_os = "linux")] {
weak! {
fn pidfd_spawnp(
*mut libc::c_int,
*const libc::c_char,
*const libc::posix_spawn_file_actions_t,
*const libc::posix_spawnattr_t,
*const *mut libc::c_char,
*const *mut libc::c_char
) -> libc::c_int
}

weak! { fn pidfd_getpid(libc::c_int) -> libc::c_int }

static PIDFD_SPAWN_SUPPORTED: AtomicU8 = AtomicU8::new(0);
const UNKNOWN: u8 = 0;
const YES: u8 = 1;
// NO currently forces a fallback to fork/exec. We could be more nuanced here and keep using spawn
// if we know pidfd's aren't supported at all and the fallback would be futile.
const NO: u8 = 2;

if self.get_create_pidfd() {
let flag = PIDFD_SPAWN_SUPPORTED.load(Ordering::Relaxed);
if flag == NO || pidfd_spawnp.get().is_none() || pidfd_getpid.get().is_none() {
return Ok(None);
}
if flag == UNKNOWN {
let mut support = NO;
let our_pid = crate::process::id();
let pidfd =
unsafe { libc::syscall(libc::SYS_pidfd_open, our_pid, 0) } as libc::c_int;
if pidfd >= 0 {
let pid = unsafe { pidfd_getpid.get().unwrap()(pidfd) } as u32;
unsafe { libc::close(pidfd) };
if pid == our_pid {
support = YES
};
}
PIDFD_SPAWN_SUPPORTED.store(support, Ordering::Relaxed);
if support != YES {
return Ok(None);
}
}
}
} else {
if self.get_create_pidfd() {
unreachable!("only implemented on linux")
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to outline this code into a support function and just check for and propagate the Ok(None) return?

Copy link
Member Author

Choose a reason for hiding this comment

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

The weak symbols are needed later in the same function

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll try moving things into a module

Copy link
Member

Choose a reason for hiding this comment

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

Oh hm.

Copy link
Member Author

Choose a reason for hiding this comment

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

Tried moving it to a module but weak! makes it really unergonomic since it generates lets. Turning things into items requires repeating the function signature several times.

Copy link
Member

Choose a reason for hiding this comment

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

ugh, annoying.

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

Copy link
Member

@workingjubilee workingjubilee left a comment

Choose a reason for hiding this comment

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

This looks fine to me.

Comment on lines +707 to +736
// The child has been spawned and we are holding its pidfd.
// But we cannot obtain its pid even though pidfd_getpid support was verified earlier.
// This might happen if libc can't open procfs because the file descriptor limit has been reached.
libc::close(pidfd);
return Err(Error::new(
e.kind(),
"pidfd_spawnp succeeded but the child's PID could not be obtained",
));
Copy link
Member

Choose a reason for hiding this comment

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

I think for right now this is best because I think assuming calling .id() won't panic is pretty reasonable and this will let us find out if we do in fact have to make that change. At a glance, it seems like a bit of a "it almost-never really happens and there's no reasonable response" scenario, in which case failing fast is better?

Comment on lines +708 to +715
let spawn_res = pidfd_spawnp.get().unwrap()(
&mut pidfd,
self.get_program_cstr().as_ptr(),
file_actions.0.as_ptr(),
attrs.0.as_ptr(),
self.get_argv().as_ptr() as *const _,
envp as *const _,
);
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see why they didn't add another argument to pidfd_spawnp: this way it is "exactly the same" arg/ret as posix_spawnp instead of adding another one. They are 1:1 except for "you get a pidfd, not a pid", right?

}
_ => {}
}
PIDFD_SUPPORTED.store(support, Ordering::Relaxed);
Copy link
Member

Choose a reason for hiding this comment

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

Relaxed store because we don't care if we accidentally probe twice in a hypothetical "multiple threads spawn processes with similar Commands" cases, right?

@workingjubilee
Copy link
Member

@bors r+

@bors
Copy link
Collaborator

bors commented Jul 12, 2024

📌 Commit ec0c755 has been approved by workingjubilee

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 12, 2024
@the8472 the8472 mentioned this pull request Jul 12, 2024
9 tasks
@bors bors merged commit f9b3e8b into rust-lang:master Jul 12, 2024
@rustbot rustbot added this to the 1.81.0 milestone Jul 12, 2024
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-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue.

5 participants