Skip to content

Commit

Permalink
Fix bugzilla 24524: Very slow process fork if RLIMIT_NOFILE is too high
Browse files Browse the repository at this point in the history
  • Loading branch information
schveiguy committed Oct 30, 2024
1 parent 354781f commit 4c32428
Showing 1 changed file with 85 additions and 31 deletions.
116 changes: 85 additions & 31 deletions std/process.d
Original file line number Diff line number Diff line change
Expand Up @@ -1050,62 +1050,115 @@ private Pid spawnProcessPosix(scope const(char[])[] args,

static if (!__traits(compiles, closefrom))
{
// FIXME: This implementation crashes the system when RLIMIT_NOFILE
// has a big value. For a possible solution see:
// https://github.com/dlang/phobos/pull/8990
void fallback (int lowfd) {
// 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;
void fallback (int lowfd)
{
import core.sys.posix.dirent : dirent, opendir, readdir, closedir, DIR;
import core.sys.posix.unistd : close;
import core.sys.posix.stdlib : atoi, malloc, free;
import core.sys.posix.sys.resource : rlimit, getrlimit, RLIMIT_NOFILE;

// Get the maximum number of file descriptors that could be open.
rlimit r;
if (getrlimit(RLIMIT_NOFILE, &r) != 0)
{
abortOnError(forkPipeOut, InternalError.getrlimit, .errno);
}

immutable maxDescriptors = cast(int) r.rlim_cur;

immutable maxToClose = maxDescriptors - lowfd;
// Missing druntime declaration
pragma(mangle, "dirfd")
extern(C) nothrow @nogc int dirfd(DIR* dir);

// 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)
DIR* dir = null;

// We read from /dev/fd or /proc/self/fd only if the limit is high enough
if (maxDescriptors > 128*1024)
{
pfds[i].fd = i + lowfd;
pfds[i].events = 0;
pfds[i].revents = 0;
// Try to open the directory /dev/fd or /proc/self/fd
dir = opendir("/dev/fd");
if (dir is null) dir = opendir("/proc/self/fd");
}
if (poll(pfds, maxToClose, 0) >= 0)

// If we have a directory, close all file descriptors except stdin, stdout, stderr and forkPipeOut
if (dir)
{
foreach (i; 0 .. maxToClose)
scope(exit) closedir(dir);

int opendirfd = dirfd(dir);

// Iterate over all file descriptors
while (true)
{
// POLLNVAL will be set if the file descriptor is invalid.
if (!(pfds[i].revents & POLLNVAL)) close(pfds[i].fd);
dirent* entry = readdir(dir);

if (entry is null) break;
if (entry.d_name[0] == '.') continue;

int fd = atoi(cast(char*) entry.d_name);

// Don't close stdin, stdout, stderr, forkPipeOut or the directory file descriptor
if (fd < 3 || fd == forkPipeOut || fd == opendirfd) continue;

close(fd);
}
}
else
{
// Fall back to closing everything.
foreach (i; lowfd .. maxDescriptors)
// This is going to allocate 8 bytes for each possible file descriptor from lowfd to r.rlim_cur
if (maxDescriptors <= 128*1024)
{
// 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;

// The above, less stdin, stdout, and stderr
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 slowFDClose;

foreach (i; 0 .. maxToClose)
{
// don't close pipe write end
if (pfds[i].fd == forkPipeOut) continue;
// POLLNVAL will be set if the file descriptor is invalid.
if (!(pfds[i].revents & POLLNVAL)) close(pfds[i].fd);
}
}
else
{
close(i);
slowFDClose:
// Fall back to closing everything.
foreach (i; lowfd .. maxDescriptors)
{
if (i == forkPipeOut) continue;
close(i);
}
}
}
}

// closefrom may not be available on the version of glibc we build against.
// Until we find a way to perform this check we will try to use dlsym to
// check for the function. See: https://github.com/dlang/phobos/pull/9048
version (CRuntime_Glibc)
version (CRuntime_Glibc) {
void closefrom (int lowfd) {
static bool tryGlibcClosefrom (int lowfd) {
import core.sys.posix.dlfcn : dlopen, dlclose, dlsym, dlerror, RTLD_LAZY;
Expand All @@ -1129,6 +1182,7 @@ private Pid spawnProcessPosix(scope const(char[])[] args,
if (!tryGlibcClosefrom(lowfd))
fallback(lowfd);
}
}
else
alias closefrom = fallback;
}
Expand Down

0 comments on commit 4c32428

Please sign in to comment.