[Bug 299835] [linuxulator] linux_mremap() returns ENOMEM for unmapped ranges, causing an infinite kernel syscall loop in musl's pthread_getattr_np()
Date: Fri, 23 Oct 2026 08:53:58 UTC
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=299835
Bug ID: 299835
Summary: [linuxulator] linux_mremap() returns ENOMEM for
unmapped ranges, causing an infinite kernel syscall
loop in musl's pthread_getattr_np()
Product: Base System
Version: 15.1-RELEASE
Hardware: amd64
OS: Any
Status: New
Severity: Affects Many People
Priority: ---
Component: kern
Assignee: bugs@FreeBSD.org
Reporter: amutu@amutu.com
Description
linux_mremap() rejects every request that grows a mapping with ENOMEM before
validating the
address range. Linux distinguishes the two cases: an unmapped source range
yields EFAULT, while
ENOMEM is only returned when the mapping exists but cannot be grown in place
(no MREMAP_MAYMOVE).
musl libc depends on that distinction. pthread_getattr_np() derives the
main-thread stack size by
growing a single page and retrying while errno == ENOMEM
(src/thread/pthread_getattr_np.c):
} else { /* main thread: t->stack == NULL */
char *p = (void *)libc.auxv;
size_t l = PAGE_SIZE;
p += -(uintptr_t)p & PAGE_SIZE-1;
a->_a_stackaddr = (uintptr_t)p;
while (mremap(p-l-PAGE_SIZE, PAGE_SIZE, 2*PAGE_SIZE, 0)==MAP_FAILED &&
errno==ENOMEM)
l += PAGE_SIZE;
a->_a_stacksize = l;
}
Because FreeBSD never returns anything but ENOMEM for this call, the loop never
terminates: the
process spins in the kernel at roughly 3 million mremap(2) calls per second on
a single CPU,
while all other threads sit idle in futex(2)/epoll(2).
Any dynamically linked musl program that queries stack attributes hits this. In
practice this makes
Alpine-based (musl) Linux containers unusable under the Linuxulator: podman run
--os=linux on
ghcr.io/gethomepage/homepage (Alpine + Node.js 22) hangs at 100% CPU on the
first node -e
invocation, while node --version completes normally because it never creates a
V8 isolate.
Steps to reproduce
# on FreeBSD 15.1, arm64 or amd64, podman + ocijail
sudo podman run --rm --os=linux ghcr.io/gethomepage/homepage:latest node
--version
# v22.23.3 (fine, exits)
sudo podman run --rm --os=linux ghcr.io/gethomepage/homepage:latest node -e
"console.log(1)"
# never returns; one thread pinned at ~100% CPU
A minimal reproducer, derived from the code path above (not independently
tested by the reporter):
/* build against musl, run under the Linuxulator */
#include <pthread.h>
int main(void) {
pthread_attr_t a;
pthread_attr_init(&a);
return pthread_getattr_np(pthread_self(), &a);
}
Observed evidence
$ sudo procstat -t <pid>
PID TID COMM TDNAME CPU PRI STATE WCHAN
9848 100453 DelayedTaskSche node 2 197 run - <-- spinning in
user mode
9848 100802 DelayedTaskSche node -1 115 sleep kqread <-- libuv epoll,
healthy
9848 100803 DelayedTaskSche - -1 115 sleep futex <-- worker
threads, healthy
$ sudo dtrace -n 'fbt::linux_*:entry /pid==9848/ { @[probefunc] = count(); }
tick-3s { exit(0); }'
linux_mremap 3991267
linux_set_syscall_retval 3991267
Return value, sampled from fbt::linux_mremap:return (arg1): 6,186,469 calls in
2 seconds, all
returning 12 (ENOMEM).
The call site was pinned with td_frame->tf_elr (0x467fd3f4, inside mremap+0x2c
in
ld-musl-aarch64.so.1) and confirmed by disassembling pthread_getattr_np:
62fb8: mov w3, #0 ; flags = 0, no MREMAP_MAYMOVE
62fbc: bl 0x3e3c8 <mremap>
62fc0: cmn x0, #0x1 ; MAP_FAILED?
62fc4: b.ne 0x62fd8 ; no -> leave the loop
62fc8: bl __errno_location
62fcc: ldr w0, [x0]
62fd0: cmp w0, #0xc ; ENOMEM?
62fd4: b.eq 0x62fa4 ; yes -> retry forever
Fix
Return EFAULT when the source range is not fully mapped, and reserve ENOMEM for
"mapped but
cannot grow". Accessibility is checked with useracc(), which takes the vm_map
read lock itself.
--- a/sys/compat/linux/linux_misc.c
+++ b/sys/compat/linux/linux_misc.c
@@ -62,6 +62,7 @@
#include <security/mac/mac_framework.h>
#include <vm/pmap.h>
+#include <vm/vm_extern.h>
#include <vm/vm_map.h>
#include <vm/swap_pager.h>
@@ -324,8 +325,19 @@ linux_mremap(struct thread *td, struct linux_mremap_args
*args)
args->old_len = round_page(args->old_len);
if (args->new_len > args->old_len) {
+ /*
+ * Linux returns EFAULT when the range to be remapped is not
+ * mapped at all, and ENOMEM only when it is mapped but cannot
+ * be grown in place. Callers depend on that distinction:
+ * musl's pthread_getattr_np() measures the main thread stack
by
+ * growing a one page mapping and retries while errno is
ENOMEM.
+ * Returning ENOMEM for every growth request makes that loop
+ * spin in the kernel forever.
+ */
td->td_retval[0] = 0;
- return (ENOMEM);
+ if (useracc((void *)args->addr, args->old_len, VM_PROT_READ))
+ return (ENOMEM);
+ return (EFAULT);
}
Note: vm_map_check_protection() must not be used here — it expects the caller
to hold the vm_map
lock and will corrupt/deadlock the map if called unlocked (this was verified
the hard way; the first
revision of the patch produced a hung mmap(2) waiting on the vm_map sx lock).
useracc() is the
locked variant.
Scope / limitations
This patch does not implement growth support for mremap(2); it only makes the
errno contract
match Linux so that musl-based programs terminate their probe. Callers that
genuinely need to grow a
mapping (e.g. musl realloc() on an mmap'ed chunk) still receive ENOMEM and fall
back to
malloc()+copy, whereas Linux would grow in place. Implementing real growth (and
honouring
MREMAP_MAYMOVE/MREMAP_FIXED, both of which are accepted by the flag check but
otherwise unused)
is left as future work.
Affected versions checked: releng/15.1 and main contain identical code.
Test plan
- Reproducer above: before the patch one thread spins at ~100% CPU
indefinitely; after the patch the
probe terminates after 9 iterations.
- dtrace trace of linux_mremap after the patch shows the expected Linux-like
sequence:
E addr=fffffffe4000 old=4096 new=8192 fl=0 -> err=12 (ENOMEM) /* range mapped
*/
...
E addr=fffffffdc000 old=4096 new=8192 fl=0 -> err=14 (EFAULT) /* range
unmapped, loop exits */
- podman run --rm --os=linux <alpine-based image> node -e "console.log('ok')"
prints ok and exits 0.
--
You are receiving this mail because:
You are the assignee for the bug.