Kirill A. Korinsky wrote:
On Tue, 20 Oct 2026 09:03:00 +0200,
David Uhden Collado <[email protected]> wrote:
The existing /dev/fd enumeration uses operations that may allocate
memory after fork, which is unsafe in a multithreaded process.
Probably I need a coffe, but I not really understand that you mean here, but
anyway, if it true, it should be fixed upstream.
++ for fd in 0..=RawFd::MAX {
++ if remaining <= 0 {
++ break;
++ }
++ let flags = libc::fcntl(fd, libc::F_GETFD);
++ if flags == -1 {
++ continue;
++ }
So, instead iterate over a few fd inside /dev/fd you're flooding by fcntl.
I've read https://doc.rust-lang.org/std/os/fd/type.RawFd.html and I see that
pub type RawFd = c_int;
Which reads like loop from 0 till 2^31 on usual system and each step calls
fcntl().
What did I miss?
Hello Kirill,
The loop does not normally reach INT_MAX.
getdtablecount() returns the number of currently open descriptors.
The counter is decremented for every valid descriptor found, including
preserved and CLOEXEC descriptors. The loop stops once all open
descriptors have been accounted for.
For example, with descriptors 0 through 9 open, it stops after 10
iterations, not 2^31.
However, you're right about the worst case. If a process has a
descriptor at a very high number with large gaps below it, the loop
could issue an excessive number of fcntl() calls. This needs
improvement.
The original issue is that std::fs::read_dir("/dev/fd") runs after
fork(), before exec(). Rust's pre_exec documentation requires
async-signal-safe operations because memory allocation or locks
inherited from other threads can cause deadlocks.
https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html
The intention was to avoid that risk while preserving explicitly
inherited descriptors and Rust's CLOEXEC error-reporting pipe.
I agree that this should ultimately be fixed upstream. The concern
about post-fork allocation remains valid, but the replacement needs
a better worst-case bound.
Thanks,
David.