Skip to content
133 changes: 81 additions & 52 deletions std/process.d
Original file line number Diff line number Diff line change
Expand Up @@ -1046,21 +1046,16 @@ private Pid spawnProcessPosix(scope const(char[])[] args,
if (getrlimit(RLIMIT_NOFILE, &r) != 0)
abortOnError(forkPipeOut, InternalError.getrlimit, .errno);

immutable maxDescriptors = cast(int) r.rlim_cur;
immutable long maxDescriptors = r.rlim_cur;
Comment thread
Poita marked this conversation as resolved.
Outdated

// Missing druntime declaration
pragma(mangle, "dirfd")
extern(C) nothrow @nogc int dirfd(DIR* dir);

DIR* dir = null;

// We read from /dev/fd or /proc/self/fd only if the limit is high enough
if (maxDescriptors > 128*1024)
{
// Try to open the directory /dev/fd or /proc/self/fd
dir = opendir("/dev/fd");
if (dir is null) dir = opendir("/proc/self/fd");
}
// Always try /dev/fd enumeration first — it's the most
// efficient approach and handles unlimited RLIMIT_NOFILE.
DIR* dir = opendir("/dev/fd");
if (dir is null) dir = opendir("/proc/self/fd");

// If we have a directory, close all file descriptors except stdin, stdout, and stderr
if (dir)
Expand All @@ -1085,52 +1080,52 @@ private Pid spawnProcessPosix(scope const(char[])[] args,
close(fd);
}
}
else
else if (maxDescriptors > 0 && maxDescriptors <= 128*1024)
{
// This is going to allocate 8 bytes for each possible file descriptor from lowfd to r.rlim_cur
if (maxDescriptors <= 128*1024)
Comment thread
Poita marked this conversation as resolved.
// This is going to allocate 8 bytes for each possible file descriptor from lowfd to rlim_cur.
// NOTE: malloc() and getrlimit() are not on the POSIX async
// signal safe functions list, but practically this should
// not be a problem. Java VM and CPython also use malloc()
// in its own implementation via opendir().
import core.stdc.stdlib : malloc;
import core.sys.posix.poll : pollfd, poll, POLLNVAL;

immutable int maxToClose = cast(int)(maxDescriptors - lowfd);

// Call poll() to see which ones are actually open:
auto pfds = cast(pollfd*) malloc(pollfd.sizeof * maxToClose);
if (pfds is null)
{
abortOnError(forkPipeOut, InternalError.malloc, .errno);
}

foreach (i; 0 .. maxToClose)
{
// NOTE: malloc() and getrlimit() are not on the POSIX async
// signal safe functions list, but practically this should
// not be a problem. Java VM and CPython also use malloc()
// in its own implementation via opendir().
import core.stdc.stdlib : malloc;
import core.sys.posix.poll : pollfd, poll, POLLNVAL;

immutable maxToClose = maxDescriptors - lowfd;

// Call poll() to see which ones are actually open:
auto pfds = cast(pollfd*) malloc(pollfd.sizeof * maxToClose);
if (pfds is null)
{
abortOnError(forkPipeOut, InternalError.malloc, .errno);
}

foreach (i; 0 .. maxToClose)
{
pfds[i].fd = i + lowfd;
pfds[i].events = 0;
pfds[i].revents = 0;
}

if (poll(pfds, maxToClose, 0) < 0)
// couldn't use poll, use the slow path.
goto LslowClose;

foreach (i; 0 .. maxToClose)
{
// POLLNVAL will be set if the file descriptor is invalid.
if (!(pfds[i].revents & POLLNVAL)) close(pfds[i].fd);
}
pfds[i].fd = i + lowfd;
pfds[i].events = 0;
pfds[i].revents = 0;
}
else

if (poll(pfds, maxToClose, 0) < 0)
// couldn't use poll, use the slow path.
goto LslowClose;

foreach (i; 0 .. maxToClose)
{
// POLLNVAL will be set if the file descriptor is invalid.
if (!(pfds[i].revents & POLLNVAL)) close(pfds[i].fd);
}
}
else
{
LslowClose:
// Fall back to closing everything up to a sane limit.
// When rlim_cur is huge (e.g. unlimited), cap to avoid
// iterating over billions of file descriptors.
immutable long closeMax = maxDescriptors > 1_048_576 ? 1_048_576 : maxDescriptors;
foreach (i; lowfd .. cast(int) closeMax)
{
LslowClose:
// Fall back to closing everything.
foreach (i; lowfd .. maxDescriptors)
{
close(i);
}
close(i);
}
}
}
Expand Down Expand Up @@ -1800,6 +1795,40 @@ version (Posix) @system unittest
testFDs();
}

// Test that spawning a process works when RLIMIT_NOFILE is very large.
// Regression test: a cast(int) of rlim_cur caused overflow when the limit
// was unlimited (RLIM_INFINITY), making the fd-closing code attempt a
// massive malloc that would fail with "Cannot allocate memory".
version (Posix) @system unittest
{
import core.sys.posix.sys.resource : rlimit, getrlimit, setrlimit, RLIMIT_NOFILE;

// Save current limit
rlimit originalLimit;
if (getrlimit(RLIMIT_NOFILE, &originalLimit) != 0)
return; // Can't test if we can't get the limit

// Set RLIMIT_NOFILE to a value that overflows int (> int.max)
rlimit highLimit;
highLimit.rlim_cur = cast(ulong) int.max + 1;
Comment thread
Poita marked this conversation as resolved.
Outdated
highLimit.rlim_max = originalLimit.rlim_max;

// If we can't raise the limit (e.g. no permission), try with rlim_max
if (setrlimit(RLIMIT_NOFILE, &highLimit) != 0)
{
highLimit.rlim_cur = originalLimit.rlim_max;
if (highLimit.rlim_cur <= int.max)
return; // Can't set a high enough limit to test the overflow
if (setrlimit(RLIMIT_NOFILE, &highLimit) != 0)
return;
}
scope(exit) setrlimit(RLIMIT_NOFILE, &originalLimit);

// This should not throw "Failed to allocate memory"
TestScript prog = "exit 0";
assert(execute(prog.path).status == 0);
}

@system unittest // Environment variables in spawnProcess().
{
// We really should use set /a on Windows, but Wine doesn't support it.
Expand Down
Loading