Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <unistd.h>

#include "utils.h"
Expand Down Expand Up @@ -157,6 +158,48 @@ _Static_assert((INFRA_PT_POOL_OFF & 0xFFF) == 0 &&
/* Build-time version string (generated by make into build/version.h) */
#include "version.h"

/* Host descriptors used outside the guest FD table. Blocking I/O may hold two
* duplicated descriptors per guest thread (for example, copy_file_range()),
* requiring up to 2 * MAX_THREADS descriptors. Keep another 128 descriptors
* for runtime pipes, fork IPC, debugger sockets, sysroot/FUSE plumbing, and
* similar bounded overhead. Operations whose descriptor use grows with their
* input set, such as ppoll(), require separate accounting.
*/
#define HOST_FD_RESERVE 256

static int host_nofile_ensure_capacity(void)
{
const rlim_t required = (rlim_t) FD_TABLE_SIZE + HOST_FD_RESERVE;
struct rlimit limit;

if (getrlimit(RLIMIT_NOFILE, &limit) < 0) {
log_error("failed to read host RLIMIT_NOFILE: %s", strerror(errno));
return -1;
}
if (limit.rlim_cur >= required)
return 0;

if (limit.rlim_max != RLIM_INFINITY && limit.rlim_max < required) {
log_error(
"host RLIMIT_NOFILE hard limit %llu is below the required %llu "
"(%d guest FDs + %d elfuse reserve)",
(unsigned long long) limit.rlim_max, (unsigned long long) required,
FD_TABLE_SIZE, HOST_FD_RESERVE);
return -1;
}

rlim_t old_soft = limit.rlim_cur;
limit.rlim_cur = required;
if (setrlimit(RLIMIT_NOFILE, &limit) < 0) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
log_error("failed to raise host RLIMIT_NOFILE from %llu to %llu: %s",
(unsigned long long) old_soft, (unsigned long long) required,
strerror(errno));
return -1;
}

return 0;
}

/* Verify the host CPU's DC ZVA granule matches the shim's hardcoded value.
*
* DCZID_EL0 is readable from EL0 without trapping, so guest libc reads the
Expand Down Expand Up @@ -365,6 +408,14 @@ int main(int argc, char **argv)
}
proc_set_fakeroot_enabled(fakeroot);

/* Top-level processes establish the capacity; fork helpers normally
* inherit it, but recheck before receiving the parent's FD table. Guest
* RLIMIT_NOFILE state is virtualized separately and cannot lower this
* internal host reserve.
*/
if (host_nofile_ensure_capacity() < 0)
return 1;

/* Block the vCPU-preemption signals and start the sigwait thread before any
* vCPU thread exists, so both the normal path and the fork-child path below
* inherit the block on every thread they spawn.
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/fork-state.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/* Fork IPC protocol identity. Bump this whenever the header layout or ordered
* fork payload changes incompatibly.
*/
#define FORK_IPC_PROTOCOL_MAGIC 0x454C464CU /* "ELFL" */
#define FORK_IPC_PROTOCOL_MAGIC 0x454C464DU /* "ELFM" */

#define IPC_MAGIC_HEADER FORK_IPC_PROTOCOL_MAGIC
#define IPC_MAGIC_SENTINEL 0x454C4F4BU /* "ELOK" */
Expand Down Expand Up @@ -72,6 +72,12 @@ typedef struct {
*/
uint32_t shm_is_clone;
uint32_t _pad2;
/* Guest-visible NOFILE state is separate from the host capacity reserved
* by elfuse. The child restores this after installing inherited FDs so
* descriptors above a subsequently lowered soft limit survive fork.
*/
uint64_t nofile_cur;
uint64_t nofile_max;
} ipc_header_t;

typedef struct {
Expand Down
22 changes: 22 additions & 0 deletions src/runtime/forkipc.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "syscall/proc.h"
#include "syscall/proc-pidfd.h"
#include "syscall/signal.h"
#include "syscall/sys.h"

#include "debug/log.h"
#include "debug/syscall-hist.h"
Expand Down Expand Up @@ -115,6 +116,12 @@ int fork_child_main(int ipc_fd,
log_error("fork-child: bad magic 0x%x", hdr.magic);
return 1;
}
if (hdr.nofile_cur > hdr.nofile_max || hdr.nofile_max > FD_TABLE_SIZE) {
log_error("fork-child: invalid RLIMIT_NOFILE %llu/%llu",
(unsigned long long) hdr.nofile_cur,
(unsigned long long) hdr.nofile_max);
return 1;
}
log_debug("fork-child: pid=%lld ppid=%lld", (long long) hdr.child_pid,
(long long) hdr.parent_pid);

Expand Down Expand Up @@ -266,6 +273,16 @@ int fork_child_main(int ipc_fd,
return 1;
}

/* Linux preserves already-open descriptors above a newly lowered soft
* limit across fork. Install the inherited table under the default table
* limit first, then restore the parent's guest-visible limit.
*/
if (sys_nofile_restore(hdr.nofile_cur, hdr.nofile_max) < 0) {
log_error("fork-child: failed to restore RLIMIT_NOFILE");
guest_destroy(&g);
return 1;
}

/* Must follow fork_ipc_recv_fd_table: the keepalive recv resolves each
* payload guest_fd to its (now installed) child-side host master fd.
*/
Expand Down Expand Up @@ -1646,6 +1663,9 @@ int64_t sys_clone(hv_vcpu_t vcpu,
* but before sibling vCPUs resume. Declared up front so all goto paths to
* fail_snapshot can free it unconditionally. Header
*/
uint64_t nofile_cur, nofile_max;
sys_nofile_snapshot(&nofile_cur, &nofile_max);

ipc_header_t hdr = {
.magic = IPC_MAGIC_HEADER,
.ipa_bits = g->ipa_bits,
Expand Down Expand Up @@ -1685,6 +1705,8 @@ int64_t sys_clone(hv_vcpu_t vcpu,
flags & (LINUX_CLONE_CHILD_SETTID | LINUX_CLONE_CHILD_CLEARTID),
.ctid_gva = ctid_gva,
.shm_is_clone = (snapshot_shm_fd >= 0) ? 1 : 0,
.nofile_cur = nofile_cur,
.nofile_max = nofile_max,
};
proc_registry_publish_self();
if (fork_ipc_write_all(ipc_sock, &hdr, sizeof(hdr)) < 0) {
Expand Down
5 changes: 0 additions & 5 deletions src/syscall/fdtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ void fd_set_rlimit_nofile(int cur)
rlimit_nofile_cur = cur;
}

int fd_get_rlimit_nofile(void)
{
return rlimit_nofile_cur;
}

/* Bitmap for O(1) lowest-free-FD allocation. A set bit means the FD is free
* (FD_CLOSED). bit_ctz64 on each word finds the lowest free FD in O(1) per
* word, vs O(FD_TABLE_SIZE) linear scan.
Expand Down
3 changes: 0 additions & 3 deletions src/syscall/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,6 @@ int64_t sys_mmap_anon(guest_t *g, uint64_t addr, uint64_t length, int prot);
*/
void fd_set_rlimit_nofile(int cur);

/* Get the current guest RLIMIT_NOFILE soft limit. */
int fd_get_rlimit_nofile(void);

/* Borrowed-or-owned host fd reference.
*
* Single-threaded guests borrow the raw host fd directly (no dup, no close).
Expand Down
107 changes: 80 additions & 27 deletions src/syscall/sys.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ static linux_sysinfo_t cached_sysinfo;
static pthread_mutex_t rlimit_lock = PTHREAD_MUTEX_INITIALIZER;
static linux_rlimit64_t cached_linux_rlimits[10];
static uint8_t cached_linux_rlimit_valid[10];
static linux_rlimit64_t guest_nofile_limit = {
.rlim_cur = FD_TABLE_SIZE,
.rlim_max = FD_TABLE_SIZE,
};
#define RLIMIT_CACHE_SIZE ((int) (ARRAY_SIZE(cached_linux_rlimits)))

_Static_assert(sizeof(struct rusage) == sizeof(linux_rusage_t),
Expand Down Expand Up @@ -659,6 +663,63 @@ static void rlimit_cache_set(int resource, linux_rlimit64_t lim)
pthread_mutex_unlock(&rlimit_lock);
}

void sys_nofile_snapshot(uint64_t *cur, uint64_t *max)
{
pthread_mutex_lock(&rlimit_lock);
if (cur)
*cur = guest_nofile_limit.rlim_cur;
if (max)
*max = guest_nofile_limit.rlim_max;
pthread_mutex_unlock(&rlimit_lock);
}

int sys_nofile_restore(uint64_t cur, uint64_t max)
{
if (cur > max || max > FD_TABLE_SIZE)
return -1;

pthread_mutex_lock(&rlimit_lock);
guest_nofile_limit.rlim_cur = cur;
guest_nofile_limit.rlim_max = max;
fd_set_rlimit_nofile((int) cur);
pthread_mutex_unlock(&rlimit_lock);
return 0;
}

static int64_t sys_prlimit64_nofile(guest_t *g,
uint64_t new_gva,
uint64_t old_gva)
{
linux_rlimit64_t new_lim = {0};
if (new_gva != 0 &&
guest_read_small(g, new_gva, &new_lim, sizeof(new_lim)) < 0)
return -LINUX_EFAULT;

pthread_mutex_lock(&rlimit_lock);
linux_rlimit64_t old_lim = guest_nofile_limit;

int error = 0;
if (new_gva != 0) {
if (new_lim.rlim_cur > new_lim.rlim_max) {
error = LINUX_EINVAL;
} else if (new_lim.rlim_max > guest_nofile_limit.rlim_max) {
/* The guest has no capability path for raising its hard limit. */
error = LINUX_EPERM;
} else {
guest_nofile_limit = new_lim;
fd_set_rlimit_nofile((int) new_lim.rlim_cur);
}
}
pthread_mutex_unlock(&rlimit_lock);

if (error)
return -error;
if (old_gva != 0 &&
guest_write_small(g, old_gva, &old_lim, sizeof(old_lim)) < 0)
return -LINUX_EFAULT;
return 0;
}

int64_t sys_prlimit64(guest_t *g,
int pid,
int resource,
Expand All @@ -670,6 +731,8 @@ int64_t sys_prlimit64(guest_t *g,
int mac_res = translate_rlimit_resource(resource);
if (mac_res < 0)
return -LINUX_EINVAL;
if (resource == 7 /* RLIMIT_NOFILE */)
return sys_prlimit64_nofile(g, new_gva, old_gva);

/* Get old limits BEFORE setting new ones (Linux prlimit64 atomically
* returns old values and sets new ones; approximate by get-then-set).
Expand Down Expand Up @@ -704,15 +767,6 @@ int64_t sys_prlimit64(guest_t *g,
return linux_errno();

rlimit_cache_set(resource, new_lim);

/* Track RLIMIT_NOFILE in the guest FD table so fd_alloc enforces the
* limit and returns -EMFILE.
*/
if (resource == 7 /* RLIMIT_NOFILE */) {
int cur = (new_lim.rlim_cur == UINT64_MAX) ? FD_TABLE_SIZE
: (int) new_lim.rlim_cur;
fd_set_rlimit_nofile(cur);
}
}

/* Write old limits to guest after set (so guest sees pre-set values) */
Expand Down Expand Up @@ -763,30 +817,29 @@ int sys_format_limits(char *buf, size_t bufsz)
char soft[24], hard[24];
int res = rows[i].linux_res;

if (RANGE_CHECK(res, 0, RLIMIT_CACHE_SIZE)) {
if (res == 7 /* RLIMIT_NOFILE */) {
uint64_t cur, max;
sys_nofile_snapshot(&cur, &max);
linux_rlimit64_t lim = {.rlim_cur = cur, .rlim_max = max};
snprintf(soft, sizeof(soft), "%llu",
(unsigned long long) lim.rlim_cur);
snprintf(hard, sizeof(hard), "%llu",
(unsigned long long) lim.rlim_max);
} else if (RANGE_CHECK(res, 0, RLIMIT_CACHE_SIZE)) {
linux_rlimit64_t lim;
if (!rlimit_cache_get(res, &lim)) {
/* RLIMIT_NOFILE (Linux 7): use the guest FD table limit rather
* than host getrlimit, which may return RLIM_INFINITY on macOS.
*/
if (res == 7) {
int cur = fd_get_rlimit_nofile();
lim.rlim_cur = (uint64_t) cur;
lim.rlim_max = (uint64_t) FD_TABLE_SIZE;
} else {
int mac_res = translate_rlimit_resource(res);
if (mac_res >= 0) {
struct rlimit rl;
if (getrlimit(mac_res, &rl) == 0) {
lim = translate_host_rlimit(res, rl);
} else {
lim.rlim_cur = UINT64_MAX;
lim.rlim_max = UINT64_MAX;
}
int mac_res = translate_rlimit_resource(res);
if (mac_res >= 0) {
struct rlimit rl;
if (getrlimit(mac_res, &rl) == 0) {
lim = translate_host_rlimit(res, rl);
} else {
lim.rlim_cur = UINT64_MAX;
lim.rlim_max = UINT64_MAX;
}
} else {
lim.rlim_cur = UINT64_MAX;
lim.rlim_max = UINT64_MAX;
}
}
if (lim.rlim_cur == UINT64_MAX)
Expand Down
7 changes: 7 additions & 0 deletions src/syscall/sys.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ int64_t sys_prlimit64(guest_t *g,
uint64_t new_gva,
uint64_t old_gva);

/* Snapshot/restore the guest-visible RLIMIT_NOFILE across the posix_spawn
* fork boundary. The host limit is internal capacity and is deliberately not
* exposed through these helpers.
*/
void sys_nofile_snapshot(uint64_t *cur, uint64_t *max);
int sys_nofile_restore(uint64_t cur, uint64_t max);

/* Format /proc/self/limits content into buf.
*
* Returns bytes written (excluding NUL) or -1 on error.
Expand Down
Loading
Loading