Repository navigation
Conversation
jserv
left a comment
There was a problem hiding this comment.
Fold similar changes and rework commits.
0d06303 to
509647a
Compare
509647a to
fab7c11
Compare
fab7c11 to
be085bf
Compare
be085bf to
032dd83
Compare
| * Returns false when no limit applies, true with @room set otherwise, 0 at or | ||
| * past the limit. | ||
| */ | ||
| static bool fsize_room(int host_fd, |
There was a problem hiding this comment.
The access-mode ordering cubic raised for ftruncate was fixed in 68e134d, but the write family has the same problem. fsize_room() does not check the access mode, so write, pwrite64, writev, pwritev and pwritev2 on an O_RDONLY descriptor at or past the limit return EFBIG and raise SIGXFSZ instead of EBADF. Linux checks FMODE_WRITE before generic_write_checks.
There was a problem hiding this comment.
Fixed in fe2d66d. fsize_room reads F_GETFL once and applies no limit to a descriptor that is not open for writing, so the host call answers EBADF and no signal is sent. That covers write, pwrite64, writev, pwritev and pwritev2, and the sendfile and splice output side, which go through the same helper. test-rlimit-enforce step 9 now tries write, writev, pwrite and pwritev on an O_RDONLY descriptor at and past the limit; it fails without the change and passes on Linux 6.18.
| return grown; | ||
| } | ||
|
|
||
| void mem_stack_init_extent(guest_t *g, uint64_t sp) |
There was a problem hiding this comment.
With [stack-guard] removed, the window below [stack] is no longer in the region table, and find_free_gap_inner has no stack-gap rule. A non-fixed hinted mmap can therefore land just under the stack, after which the stack cannot grow and the guest gets SIGSEGV. Linux rejects such hints through vm_start_gap(), and main never placed anything there because the stack records covered the whole window.
There was a problem hiding this comment.
Fixed in 09932ef. find_free_gap_inner now ends the usable space 1 MiB in front of a [stack] region, after vm_start_gap, so a hinted or placed mapping cannot land on the next stack pages. A hint further down in the window is still honored, as on Linux, and bounds the stack there. test-rlimit-stack has a case that hints 64 KiB under the stack and then touches that address; it fails without the change and passes on Linux 6.18.
| return false; | ||
|
|
||
| struct stat st; | ||
| if (fstat(host_fd, &st) < 0 || !S_ISREG(st.st_mode)) |
There was a problem hiding this comment.
fsize_room never checks the access mode. With RLIMIT_FSIZE at or below the file position, write(fd, buf, 1) on an O_RDONLY regular file queues SIGXFSZ and returns EFBIG. Linux returns EBADF from vfs_write before it looks at any size, and here the default SIGXFSZ action terminates the guest. truncate_fsize_check already returns early for O_RDONLY. Do the same here (one F_GETFL serves both this and the O_APPEND test).
There was a problem hiding this comment.
Fixed in fe2d66d as suggested: one F_GETFL serves the access-mode test and the O_APPEND test, and an O_RDONLY descriptor returns early the way truncate_fsize_check does. Step 9 of test-rlimit-enforce covers it.
| for (int i = 0; i < *iovcnt; i++) | ||
| total += iov[i].iov_len; | ||
| uint64_t allowed = total; | ||
| int64_t err = fsize_limit_write(host_fd, pos, append_aware, &allowed); |
There was a problem hiding this comment.
When every iovec has iov_len == 0, total is 0, but a position at or past the limit still yields room == 0 and SIGXFSZ. sys_pwritev with iovcnt >= 2, and sys_pwritev_append, reach this with no zero-payload short-circuit. Linux returns 0 from vfs_writev before any limit check.
| int64_t err = fsize_limit_write(host_fd, pos, append_aware, &allowed); | |
| if (total == 0) | |
| return 0; | |
| int64_t err = fsize_limit_write(host_fd, pos, append_aware, &allowed); |
There was a problem hiding this comment.
Fixed in fe2d66d. The zero-count return is in fsize_limit_write rather than in the vector wrapper, so pwrite64 and the RWF_APPEND path get it too. Step 12 of test-rlimit-enforce runs writev, pwritev and pwritev2 with two empty entries at and past the limit and expects 0 with no signal; it fails without the change.
| replace_start = addr >= g->guest_size ? addr : addr - g->ipa_base; | ||
| replace_end = replace_start + length; | ||
| } | ||
| if (!mem_rlimit_allows(g, length, prot, flags, replace_start, replace_end)) |
There was a problem hiding this comment.
This runs before the fd and remaining argument checks in sys_mmap. Under an RLIMIT_AS the request exceeds, a bad fd answers ENOMEM where Linux answers EBADF (fget happens before do_mmap). Moving the check to just before the region table is extended keeps the errno order.
There was a problem hiding this comment.
Fixed in e6b4e0f. The check now runs after the descriptor is opened, and for a fixed request after the address checks and the MAP_FIXED_NOREPLACE overlap test. test-rlimit-enforce asks for a mapping over the limit with fd -1, placed and fixed, and expects EBADF. The Rosetta high-VA fixed path still checks the limit before sys_mmap_high_va looks at the descriptor.
| * stack grows to it when RLIMIT_STACK and its neighbors allow, and the | ||
| * fault stays a SIGSEGV when they do not. | ||
| */ | ||
| if (mat != 0 && mem_stack_grow(g, fault_off)) |
There was a problem hiding this comment.
This hook covers EL0 faults only. Three shim accesses that store to guest memory at EL1 never come here, and the 8 MiB premapped window used to make that safe:
- DC ZVA emulation (
stp xzrinhandle_sysreg_trap) on a not-yet-grown stack page takes an EL1 data abort outside the recover ranges and halts the VM throughrestore_and_bad. - The urandom
strband futexldtrfast paths answerEFAULTfor a valid stack buffer instead of growing.
Either route those aborts to mem_stack_grow and retry, or have the fast paths fall back to HVC #5 when the target is below the stack region.
There was a problem hiding this comment.
DC ZVA: fixed in 09932ef. The shim probes the block with AT S1E0W before the stores and forwards a failed probe on HVC #11; the host takes it as a write data abort at the block address with the fault status from PAR_EL1, so the stack grows and the instruction runs again, or the guest gets SIGSEGV. DC ZVA does not trap on my host, so I checked this with a build that clears SCTLR.DZE: before, an ungrown stack page ends in the crash report; after, it is zeroed, and an unmapped address is SIGSEGV/MAPERR.
urandom: no change. The AT S1E0W probe ahead of the copy already sends an unmapped buffer to the host, which grows the stack; the strb recovery is only for a page revoked between probe and store. test-rlimit-stack now reads from /dev/urandom and calls getrandom into an ungrown page.
futex: the timeout load now forwards to the host instead of answering EFAULT, since copy_from_user grows the stack on Linux. The word load keeps EFAULT: Linux 6.18 answers EFAULT for FUTEX_WAIT on a word under the stack and leaves [stack] where it was.
| * at the limit again. | ||
| */ | ||
| if (!err && new_lim && resource == LINUX_RLIMIT_CPU) | ||
| kill(host_pid, SIGUSR2); |
There was a problem hiding this comment.
The liveness checks only run when the map fails. A table file survives a host SIGKILL of the target (the atexit unlink never runs), and the registry can still resolve that guest pid to a host pid after the host child was collected. Once the pid is reused, this maps the stale file, writes into it, and sends raw SIGUSR2 to whatever process now holds the pid; a non-elfuse process dies from the default action. Recheck proc_child_exited and kill(host_pid, 0) after a successful map. Longer term, stamp the file with something that identifies the process incarnation, so a stale table is rejected outright.
There was a problem hiding this comment.
Fixed in c8b6f6c. proc_child_exited and kill(host_pid, 0) now run on every pass, after a successful map as well, and a mapped table is dropped with ESRCH. With a child whose host process was killed and then collected by waitid(WNOWAIT), prlimit64 wrote the stale table and returned 0 before; it returns ESRCH now. The namespace path was already safe against reuse, because registry_parse_cb drops a record whose start time differs from the live process. No incarnation stamp in this PR: what is left is the window between the liveness check and the kill.
There was a problem hiding this comment.
Update: the stamp is in after all, in 609c76e. The table carries its owner's start time and the exchange is refused with ESRCH when the process under the host pid started at another time.
| if (gva_translate_perm(g, gva, required_perms, &first) < 0 && | ||
| (!mem_stack_grow_for_host((guest_t *) g, gva) || | ||
| gva_translate_perm(g, gva, required_perms, &first) < 0)) | ||
| return NULL; |
There was a problem hiding this comment.
This returns NULL in two cases where the access should succeed.
- A sibling thread grows the stack over
gvabetween the firstgva_translate_permand takingmmap_lock. Thenmem_stack_growseesoff >= r->start, returns false, and the second translation is skipped. mmap_lock_try_acquiregives up after 1 s while another thread holdsmmap_lock(a longpreadfallback insys_mmap). Thenread()into a valid stack buffer getsEFAULT. Worse,deliver_signal_lockedwriting a frame below the extent kills the guest instead of running the handler.
Always retry the translation after the grow attempt. Treat lock contention as "wait", not "refuse", at least on paths that hold no lock ranked below mmap_lock. This hook also fires for gdbstub and crash-report reads. Those grow the guest stack (Linux 6.5+ GUP does not expand stacks for remote access) and queue any TLBI from stack_unmap_approach into a non-vCPU thread's cpu_tlbi_req, where nothing drains it.
| if (gva_translate_perm(g, gva, required_perms, &first) < 0 && | |
| (!mem_stack_grow_for_host((guest_t *) g, gva) || | |
| gva_translate_perm(g, gva, required_perms, &first) < 0)) | |
| return NULL; | |
| if (gva_translate_perm(g, gva, required_perms, &first) < 0) { | |
| (void) mem_stack_grow_for_host((guest_t *) g, gva); | |
| if (gva_translate_perm(g, gva, required_perms, &first) < 0) | |
| return NULL; | |
| } |
There was a problem hiding this comment.
The sibling-growth case is fixed in 09932ef: mem_stack_grow_for_host tells the caller to translate again whenever the address is in stack range, whoever did the growing.
Contention is changed in 2ba2fb0. The wait is chosen by the caller instead of being a 1 s trylock everywhere. The lock order in internal.h ranks a host access to guest memory with mmap_lock, ahead of sig_lock and the rest. A path that touches guest memory with a lower lock held brackets the access with mmap_lock_nowait_begin / mmap_lock_nowait_end, in the way of pagefault_disable, and only there does stack growth fall back to trylock; every other access waits, so a held lock no longer turns into EFAULT. make check-locks builds with -DELFUSE_LOCK_ASSERT, which counts the locks each thread holds and aborts when a thread waits on mmap_lock, or reaches guest memory outside a scope, with one held. The count is used for checking only. make check passes under that build, and removing the scope in rt_sigprocmask makes test-signal abort there. It is a held-lock count, not a pairwise order validator.
prlimit64 forwarded every limit but NOFILE to host setrlimit, so a guest RLIMIT_CPU or RLIMIT_FSIZE landed on elfuse itself and RLIMIT_RSS overwrote RLIMIT_AS, which macOS defines as one resource. The pid argument was discarded, only NOFILE crossed fork, and RLIMIT_LOCKS through RLIMIT_RTTIME were EINVAL where Linux accepts all sixteen resource numbers. The sixteen limits live in a guest table. Ten are seeded from host getrlimit at boot; macOS has no counterpart for the other six, so they start from the Linux defaults: INIT_RLIMITS, and for SIGPENDING the NPROC value, which is what fork_init assigns. Each process maps its table from a file named by host pid, so prlimit64 on another fork-family member changes the limits that member reads, and a pid outside the family is ESRCH. A thread id of another member is ESRCH too, where Linux would reach its process, and so is a child that has exited, answered at once. The table rides the fork IPC header in place of the two NOFILE fields, which moves the protocol magic to ELFR. execve keeps the table because it keeps the process. A site that enforces a limit reads one slot with no lock, so the pointer to the table is atomic and is switched to the mapped file under rlimit_lock, which the seeding holds from its first slot until the table is published. /proc/self/limits prints the table from one snapshot, so RSS and AS print separately, nothing printed is a host value, and every row is from the same instant. Raising a hard limit takes guest euid 0. Linux also compares the caller's credentials with the target's; the guest ids of another member are not visible to the caller, so that comparison is not made.
No other process can read the rlimits of an elfuse process, so a guest limit that reached host setrlimit is invisible from outside. With ELFUSE_HOST_RLIMIT_CHECK set, each elfuse process records its host limits at start and, when one has moved by exit, writes a line to stderr and exits 86. test-rlimit-unchanged-host runs test-proc-limits under the check, which sets every tracked limit in a parent and in fork children. A fork child publishes its exit status before its atexit handlers run, so the test reads stderr as well as the exit status.
A guest RLIMIT_FSIZE was stored and never consulted. Each call that can grow a regular file now does what the reference kernel does, measured on it: - write, pwrite64, writev, pwritev and pwritev2 with RWF_APPEND follow generic_write_check_limits: a write that starts at or past the soft limit fails with EFBIG and SIGXFSZ, and one that crosses it is cut. An O_APPEND descriptor is judged at end of file, pwrite included. - ftruncate, truncate and fallocate follow inode_newsize_ok and refuse only to grow a file past the limit. A descriptor not open for writing is refused for that first (do_sys_ftruncate, vfs_fallocate). truncate checks and truncates through one descriptor, so a rename cannot put a different file under the second call. - copy_file_range checks the whole request first (generic_copy_file_checks): EFBIG and SIGXFSZ at or past the limit even for a zero length, otherwise the length is cut with no signal. - sendfile and splice are cut at the limit and also raise SIGXFSZ, because the kernel offers the rest of the input to the file's write path, which refuses it. No signal is sent when the input ends at the limit, or has nothing to give yet: an empty pipe that still has a writer waits, or answers EAGAIN when it does not block. The read is sized to what the limit leaves, so a pipe keeps the bytes that did not fit. SIGXFSZ is queued for the thread that made the request, not for the process: Linux sends it with send_sig to current, so it stays pending on a writer that has it blocked. The inline write path in syscall_dispatch calls host write directly, so a write takes the slow path while a limit is set. With no limit each site costs one load of the soft value.
A thread started when RLIMIT_CPU first becomes finite sleeps in wall time and samples CLOCK_PROCESS_CPUTIME_ID, which counts a vCPU thread's guest time as user time. It follows check_process_timers: SIGKILL at the hard limit, SIGXCPU at the soft limit, and the soft limit then moves up a second so the signal repeats. A guest with no CPU limit has no such thread and pays one load per SIGUSR2. A host CPU-time timer would need no thread, but with setitimer(ITIMER_PROF) armed hv_vcpu_run makes no guest progress: a probe that completes 557034 syscalls in 0.5 s completes none, and the vCPU thread sits in hv_trap. Host RLIMIT_CPU stalls it the same way, which is what a guest setrlimit(RLIMIT_CPU) did before the limits moved into guest state. prlimit64 from another process sends the target SIGUSR2 so its preemption thread has the limit looked at again.
032dd83 to
2ba2fb0
Compare
mmap, mremap and brk refuse a request that would take the mapped total past RLIMIT_AS, or the private writable total past RLIMIT_DATA, the way may_expand_vm does. Both totals are summed from the region array under mmap_lock on each call, so they cannot drift from the table, and a MAP_FIXED request is not charged for what it maps over. With neither limit set the check is two loads. A table that has lost entries is summed as it stands, so a limit is then short by the lost records; refusing instead would fail every later request of the guest. The shim and its data are left out of both totals: they sit in the runtime infra reserve, which a Linux process has no mapping for. mprotect is held to RLIMIT_DATA too. A private mapping made writable becomes data, and the check follows mprotect_fixup mapping by mapping: one fails when may_expand_vm refuses it as data and would have allowed it as what it was, and the mappings below it in the range have been changed by then. Two details of may_expand_vm come with it. A soft limit of 0 bounds data mappings by the hard limit, the kernel's allowance for Valgrind. brk is exempt from that allowance, because check_data_rlimit holds the break to the soft limit itself. RLIMIT_STACK is stored and not enforced. The stack is a STACK_SIZE region made at bootstrap and execve that never grows: its extent is 8188 KiB after execve under a soft limit of 1 MiB, 8 MiB and 48 MiB alike, and 4 MiB of it is usable under the 1 MiB limit.
prlimit64 let any member of a fork family read and set the limits of any other. Linux compares the caller's real uid and gid with all three uids and gids of the target and returns EPERM on a mismatch unless the caller holds CAP_SYS_RESOURCE (check_prlimit_permission). The target's ids were not visible across processes, which is why the check was left out. A process now keeps them in its published rlimit table and republishes them where the shim's identity cache is republished: after a successful set*id call and after an execve that changes them. The check reads them under the same file lock as the limits. The tests need two sets of ids, which only root can make, so the matrix reruns test-proc-limits under --fakeroot; the reference lane already runs it as root.
The guest rlimit table is seeded from host getrlimit, and macOS caps the hard stack limit at MAXSSIZ, 64 MiB less 16 KiB. A guest therefore could not raise RLIMIT_STACK past that, where a Linux process starts with the hard limit unlimited and ulimit -s unlimited is routine. The cap describes host thread stacks. The guest stack is guest memory and the limit is guest state, so the cap bounds nothing there. A host that sits at the cap now seeds the guest hard limit as unlimited; a host whose user lowered it below the cap still passes the lower value through.
execve accepted 2 MiB of argument and environment strings whatever the stack limit was. That is what Linux allows under the default 8 MiB limit, but the kernel derives it: a quarter of the soft RLIMIT_STACK, at least ARG_MAX (128 KiB) and at most 6 MiB, with the argv and envp pointers counted in (bprm_stack_limits). A guest that raised its stack limit to pass a larger command line got E2BIG here, and one with a small limit was allowed more than Linux gives it. sys_execve now computes the same bound, counts the filename and the pointer arrays against it, and refuses strings the limit itself could not hold, which is how Linux answers E2BIG under a 64 KiB limit. The filename is sized as the path that is copied, after /proc/self/exe is resolved, and a script exec stops counting the argv[0] it drops (remove_arg_zero). The 6 MiB ceiling still fits the 8 MiB window the stack is built in. Measured on the reference kernel at limits of 64 KiB, 1 MiB, 8 MiB, 64 MiB and unlimited; the new test checks three of them, with an empty environment so the result depends on the arguments alone. test-exec-limits expects E2BIG at 2.2 MB, which holds only under an 8 MiB limit, and re-executes itself forever when that exec succeeds. It now sets the limit it assumes: the make that ships with macOS raises the soft stack limit of its children to 64 MiB.
The stack was a fixed 8 MiB region mapped whole at execve, so RLIMIT_STACK bounded nothing: 4 MiB of stack was usable under a 1 MiB limit, and no limit let it pass 8 MiB. It now behaves as on Linux. execve maps the argument block plus 128 KiB and leaves the rest without page-table entries. An access to a page under the stack region grows the region down to that page when the grown size fits RLIMIT_STACK and RLIMIT_AS and keeps 1 MiB clear of an accessible mapping below (expand_downwards, acct_stack_growth); otherwise the fault is a SIGSEGV. A lowered limit takes nothing away and stops the next growth. The region's backing address moves down with its start, so madvise and mprotect on the grown part act on the pages they name. Linux grows the stack for a kernel access too, so read(2) into a stack buffer the guest has not touched succeeds, and so does writing a signal frame under the stack pointer. Host code reaches guest memory with locks held that rank below mmap_lock, so that path takes the lock with trylock only and gives up after one second. The window between the break and the stack was premapped, which left no fault to grow on. It is unmapped at execve, a shrinking break gives its blocks back, and a break or stack that comes within two blocks of the other cuts the block mapping between them down to pages. The break still never enters the 8 MiB window, so a default stack keeps its room; past the window the stack is bounded by the heap below it, about 110 MiB in the default layout, where Linux has no such bound. The stack-guard record is gone, and test-brk-stack, which unmapped it to show the break stops at an address and not at a record, now checks that the break leaves the window free and that the stack can grow into it afterwards.
A host read or write of guest memory that lands under the stack takes mmap_lock to grow it. It only ever tried for the lock, for up to one second, because some callers hold a lock ranked below mmap_lock, and exec_handoff_reset takes sig_lock under it. So a caller that held nothing still got EFAULT when another thread kept mmap_lock that long. The caller now says which case it is in. The lock order in internal.h ranks a guest access with mmap_lock. A caller that touches guest memory under a lower lock brackets the access with mmap_lock_nowait_begin and mmap_lock_nowait_end, and only there does the growth fall back to trying. Every other access waits. The scopes are in the futex word reads under a bucket lock, timerfd_settime, getdents64, waitid, the FUSE device read and the usbdevfs entry lock. Signal code needs no scope, because it copies to and from the guest with sig_lock released, in the order Linux uses. get_signal copies the action and applies SA_RESETHAND under siglock, and setup_rt_frame, with the rseq abort, runs after siglock is dropped; rt_sigaction and rt_sigprocmask read the new value first and write the old one last. As in Linux, a bad old-value pointer is then EFAULT with the new action or mask already in place. Nothing at run time decides whether to wait. make check-locks builds with -DELFUSE_LOCK_ASSERT, which force-includes debug/lock-assert.h to count the mutexes and rwlocks each thread holds, and asserts that none is held where a thread waits on mmap_lock or reaches guest memory outside a scope. Verified: make check and make check-locks pass. With the rseq abort moved back under sig_lock, test-rseq aborts on the assertion under make check-locks.
The DC ZVA emulation probes the block with AT S1E0W and then zeroes it with four stores at EL1. A sibling vCPU can unmap or protect the page between the probe and a store. That abort was outside the recovery ranges, so it reached restore_and_bad and halted the VM. The emulation now keeps the trap's ELR and SPSR in a slot under its frame, and handle_el1_data_abort_recover takes an abort inside the stores back to the DC ZVA itself. It traps again, the probe fails, and the guest gets the fault it would have taken at EL0. Verified: DC ZVA is not trapped on the development host, so this ran on a build that clears SCTLR.DZE. A thread issuing DC ZVA on a page that a sibling flips between PROT_NONE and read-write halted the VM in 3 of 3 runs before, and ran 4 seconds each in 3 of 3 runs after.
With a full region table, unmapping the middle of a mapping drops the record of its tail while the pages stay mapped. If that mapping sits in the window under the stack, the neighbor scan in mem_stack_grow no longer sees it, and growth zeroed the tail and made it stack. When the table is stale, growth now also asks the page tables, through guest_range_any_mapped, whether any page of the range it would take is mapped, and refuses if one is. A mapping with no access has no pages to find and is still not seen. Verified: test-rlimit-stack puts a mapping in the window, fills the table, unmaps all but its first and last page, and expects EFAULT for getrandom into the page under the last one with that page's contents kept. It fails without the check and passes on Linux 6.18.
A fixed mapping above guest_size was held to RLIMIT_AS and RLIMIT_DATA before sys_mmap_high_va looked at its descriptor, so a request with a bad descriptor that was also over the limit got ENOMEM. Linux answers EBADF, as the paths below guest_size already do. Not run: the path needs a Rosetta guest, and no test here reaches it.
prlimit64 on another pid took kill(host_pid, 0) as proof that the table file belongs to the process under that pid. A process that was killed leaves its file, and once the pid is reused the call wrote the stale table, returned 0, and for RLIMIT_CPU sent SIGUSR2 to whatever holds the pid now. The table now records the start time of the process that published it, and the exchange is refused with ESRCH unless the process under the host pid started at that time. The namespace registry already tells a reused pid apart the same way. Not run against a reused pid, which cannot be arranged from a test. test-proc-limits, whose remote cases need the stamp to match, passes.
The text said make check-locks aborts when a thread waits on mmap_lock with a lower lock held. The assertion is in mmap_lock_acquire, before any wait, and it also refuses a caller inside a nowait scope; a guest access is allowed while mmap_lock is owned. The lock order comment said nothing decides at run time, though mem_stack_grow_for_host does follow the scope the caller declared.
A PT_LOAD segment was tracked from p_vaddr to p_vaddr + p_memsz, so /proc/self/maps printed an unaligned file offset (0x7f880 where Linux prints 0x7f000) and the region totals behind VmSize, RLIMIT_AS and RLIMIT_DATA fell short of the pages Linux charges. elf_map maps a segment from the page holding p_vaddr to the page past its end, with the offset moved back by the same amount, and a page two segments share goes to the later mapping. guest_region_add_elf_segment does the same for both the bootstrap and the execve path. VmSize also counted [shim] and [shim-data], which RLIMIT_AS leaves out because a Linux process has neither. It now sums what RLIMIT_AS bounds, so it equals the guest mappings /proc/self/maps lists: 828 kB for a static binary where it read 2879 kB.
The vDSO page at VDSO_BASE was mapped in the page tables at bootstrap and execve but had no region record, so /proc/self/maps had no [vdso] line where every Linux process has one, and the mmap allocator did not see the page as taken. It is now tracked as [vdso], r-x, on both paths.
ba4cc0f to
c7e4572
Compare
There was a problem hiding this comment.
1 issue found across 11 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/core/bootstrap.c">
<violation number="1" location="src/core/bootstrap.c:128">
P2: Bootstrap adds `[vdso]` after PT_LOAD regions without resolving overlap, so a low-linked image covering `VDSO_BASE` leaves overlapping region records; exec handles the same case in the opposite order. Reserve or reject the fixed vDSO range before registering ELF segments, and keep both paths consistent.</violation>
</file>
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack
View guided diff | Re-trigger cubic
| /* The vDSO page, which Linux lists as [vdso]. Tracking it also keeps a | ||
| * hinted mmap from being placed over it. | ||
| */ | ||
| guest_region_add(g, VDSO_BASE, VDSO_BASE + VDSO_SIZE, |
There was a problem hiding this comment.
P2: Bootstrap adds [vdso] after PT_LOAD regions without resolving overlap, so a low-linked image covering VDSO_BASE leaves overlapping region records; exec handles the same case in the opposite order. Reserve or reject the fixed vDSO range before registering ELF segments, and keep both paths consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/core/bootstrap.c, line 128:
<comment>Bootstrap adds `[vdso]` after PT_LOAD regions without resolving overlap, so a low-linked image covering `VDSO_BASE` leaves overlapping region records; exec handles the same case in the opposite order. Reserve or reject the fixed vDSO range before registering ELF segments, and keep both paths consistent.</comment>
<file context>
@@ -123,6 +122,13 @@ static void register_runtime_regions(guest_t *g, size_t shim_bin_len)
+ /* The vDSO page, which Linux lists as [vdso]. Tracking it also keeps a
+ * hinted mmap from being placed over it.
+ */
+ guest_region_add(g, VDSO_BASE, VDSO_BASE + VDSO_SIZE,
+ LINUX_PROT_READ | LINUX_PROT_EXEC, LINUX_MAP_PRIVATE, 0,
+ "[vdso]");
</file context>
There was a problem hiding this comment.
The overlap needs a PT_LOAD that covers VDSO_BASE (0xF000), and such an image does not run under elfuse before or after this change: vdso_build writes the vDSO over the image’s text at VDSO_BASE, and if the image starts at 0x0, the unmapped null page also removes its ELF header. I checked with a static binary linked at 0x0. It faults at 0x40 on this branch and on ba4cc0f, while Linux runs it when run as root. Rejecting or reserving the range here would only change how that image fails. The underlying gap is the fixed VDSO_BASE; running low-linked images is tracked in #440.
A guest
setrlimitreached hostsetrlimitfor every resource but NOFILE, so a guest limit constrained elfuse itself and nothing was enforced the way Linux enforces it: a finite RLIMIT_CPU stopped the vCPU from making progress until the watchdog fired, RSS and AS shared one macOS slot, and only NOFILE crossed fork. This series keeps all sixteen Linux resources in a guest table with no host call behind it, carries the table through fork and execve, and makesprlimit64reach another fork-family pid with the kernel's credential check.The limits are then enforced where Linux enforces them: FSIZE on write, truncate, fallocate, sendfile, copy_file_range and splice, with SIGXFSZ sent to the thread that wrote; CPU from sampled process CPU time; AS and DATA on brk, mmap, mremap and mprotect; STACK by growing the stack on demand, which also sizes what execve accepts as arguments.
/proc/self/limitsreads the same table.Each behavior was measured on the reference kernel first (Linux 6.18 in the qemu lane), and the tests assert what it returned, so the same test binaries pass in both lanes.
What a reviewer may want to know before the diff:
elfuse-rlim-<hostpid>in the per-user temp directory, which is howprlimit64reaches another process.[stack-guard]record is gone and the window between the break and the stack is unmapped at execve, sotests/test-brk-stack.cchanges with it.ELFUSE_HOST_RLIMIT_CHECKis a test hook in the binary: with it set, a process exits 86 when a host limit has moved.test-rlimit-unchanged-hostruns a guest under it.rt_sigactionandrt_sigprocmaskcopy to and from the guest withsig_lockreleased, in the order Linux uses, because such a copy can grow the stack and so wait formmap_lock. A bad old-value pointer is then EFAULT with the new action or mask already in place, as in Linux.VmSizeleave out[shim]and[shim-data], which a Linux process does not have. ELF segments are tracked at page granularity, aself_mapmaps them, and the vDSO page is listed as[vdso], soVmSizeequals what/proc/self/mapslists for the guest.Left out: MEMLOCK, NPROC, CORE and the six resources macOS has no counterpart for (LOCKS through RTTIME) are stored and reported but not enforced. The stack cannot grow past the heap below it, about 110 MiB in the default layout, where Linux has no such bound.
Verified on
c7e4572, rebased onto8612033:make check-format,make check,make check-locksandmake verifypass;make test-matrix-elfuse-aarch64gives 302 passed, 0 failed, 10 skipped;make test-matrix-qemu-aarch64gives 278 passed, 1 failed, 33 skipped, the failure atest-simd-clonetimeout on the reference kernel that passed on three reruns.Summary by cubic
Keeps all sixteen Linux resource limits as guest state in a single table, so a guest
setrlimitno longer constrains the host process: previously every limit but NOFILE hit hostsetrlimit, and LOCKS through RTTIME returnedEINVALwhere Linux accepts all sixteen resources. The table is carried through fork and execve,prlimit64reaches another fork-family pid with the kernel's credential check andEPERMon a mismatch, and/proc/self/limitsreads the same table. A host at macOS's MAXSSIZ cap seeds the guest hard stack limit as unlimited, since the cap bounds host threads, not guest memory.Enforcement
Note for reviewers
ELFQtoELFR; the header now carries the rlimit table.[stack-guard]record is gone and the break-to-stack gap is unmapped at execve.mmap_lockto grow it; callers holding a lower-ranked lock bracket the access withmmap_lock_nowait_begin/end, andmake check-locksasserts none is held where a thread waits onmmap_lock.guest_sizechecks its descriptor before the limits.prlimit64with a thread id of another process is ESRCH, where Linux reaches the process, and a remote table is refused unless the publisher's pid and start time match so a reused pid cannot read a stale one.[vdso], so/proc/self/mapsprints page-aligned offsets and a VmSize that equals the guest mappings.ELFUSE_HOST_RLIMIT_CHECKis a test hook that makes a process exit 86 when a host limit has moved, proving guest limits never touch the host.Written for commit c7e4572. Summary will update on new commits.