[Bug 299835] [linuxulator] linux_mremap() returns ENOMEM for unmapped ranges, causing an infinite kernel syscall loop in musl's pthread_getattr_np()

From: <bugzilla-noreply_at_freebsd.org>
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.