Repository navigation
Conversation
|
Preview build of 2c80070: |
robobun
force-pushed
the
robobun/635ce8f2/stackbounds-checked-limit
branch
from
October 29, 2026 10:48
30aa4d1 to
4dcee73
Compare
currentThreadStackBoundsInternal() called getrlimit(RLIMIT_STACK) for the main thread and read the limit without a look at the result. A call that fails writes nothing. On Linux a seccomp filter that denies prlimit64() makes it fail, and the bound then came from whatever the stack slot held. With a slot of 0 the bound is one page above the origin, and every recursion check on the main thread says that the stack is used up. The Linux and Darwin arms now start from a limit of RLIM_INFINITY, so a limit that was not read counts as no limit and the size is the 8 MB that WTF takes for one. glibc counts a limit it cannot read as no limit too, when it sizes the stacks of threads. The Linux arm tests for the main thread once. When the read fails and libc gave no stack either (glibc and bionic need the limit for the main thread), the Linux arm returns empty bounds. A limit that can be read gives the bounds it gave before.
complex.yaml runs the new test on Linux twice: prlimit64() fails with EPERM and with ENOSYS. The new helper deny-prlimit64 gives jsc a stack limit of 8 MB, installs the filter with fiddle (no compiler), checks that getrlimit() fails under it, and starts jsc. The entry asks the helper first whether it can do that, and skips itself otherwise. It also skips itself under ASan, whose own start reads the limit, and in a bundle for another machine. The test compiles a regular expression of 10,000 nested groups, which fits in 8 MB, and expects a RangeError for 100,000, which do not. The jsc of 8c6c963 reads a stale stack slot for the limit and ends with SIGSEGV in both runs.
robobun
force-pushed
the
robobun/635ce8f2/stackbounds-checked-limit
branch
from
October 29, 2026 12:25
4dcee73 to
2c80070
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related to oven-sh/bun#42688
Problem
prlimit64()on Linux, every recursion check on Bun's main thread fails:error: Failed to parse package.json: StackOverflow.currentThreadStackBoundsInternal()(Source/WTF/wtf/StackBounds.cpp:185) ignores the result ofgetrlimit(RLIMIT_STACK)and reads arlimitthat nothing wrote. Before [JSC] Ahead-of-time compilation #757,pthread_attr_init()zeroed that stack slot.Fix
RLIM_INFINITY. A limit that was not read counts as no limit, for which WTF takes 8 MB.JSTests/complex/bun-stack-bounds-unreadable-stack-limit.js(new). Thejscof 8c6c963 gets SIGSEGV for EPERM and for ENOSYS.Background
StackBoundsholds a stack's origin (highest address) and bound (lowest usable address). JSC and Bun check the stack pointer against the bound before they recurse.getrlimit()withprlimit64(). glibc and this file's FreeBSD arm treat a failed read as no limit.getrlimitsyscall: more code on a rare path.Downsides
bun --no-orphansunder the filter still stops at an assertion, as in Bun 1.4.4 (Notes).Notes
Status. Draft until Bun is checked against the preview build of this head. The Bun PR follows.
Scope. An earlier version of this PR also kept a thread's bounds in a
thread_local, so that Bun andWTF::Threadget one value. That is a separate change with its own case (bun --no-orphanswith a large environment under a low stack limit, a crash that Bun 1.4.4 also has). It is on the branchrobobun/635ce8f2/stackbounds-one-computationand not part of this PR.What the stale slot held. gdb on Bun canary 1.4.4-canary.1+11c367163 under the filter: the slot of
limitis 0,size -= pagesizewraps, and the bound is one page above the origin (origin 0x7ffffffff000, bound 0x800000000000). In thejscof the same build the slot holds 0x3480e00, and the bound claims 52 MB of stack under an 8 MB limit.Before #757 the main thread ran
newThreadStackBounds()first.pthread_attr_init()zeroed the slot, and glibc'spthread_getattr_np()failed, because it needs the limit for the main thread. The bounds were {0, 0x1000}: no check tripped in Bun, and JSC stopped atRELEASE_ASSERT(stack.contains(m_lastStackTop))inVM::setLastStackTop.Empty bounds. When the read fails and libc gave no stack (the filter, plus
environmoved before the request, so thatendOfMainThreadStack()gives nothing), the Linux arm returns empty bounds. Bun's checks then do not trip and JSC stops at the assertion above. That is what Bun 1.4.4 does there. Without this clause the bound would be counted down from a null origin, and every check would say that the stack is used up. The test is the base that libc returned, so that it also holds for bionic, which leaves a stack size in the attributes when it fails. With a readable limit that case is as on main.bun --no-orphansunder the filter. Bun callssetenv()before JSC asks for the bounds a second time. That request then has no origin, and JSC stops at the assertion. Bun 1.4.4 and Bun main do the same.Harness. The real
StackBounds.cpp, compiled alone with clang 23 -O2 (x64, glibc 2.41, ASLR off), main thread:prlimit64answers EPERM or ENOSYS, or only forRLIMIT_STACKsetenv()before the requestThe bounds are the same as at 8c6c963 in 18 of 18 start modes with a readable limit: default, empty environment, 700 KB of environment, 300 KB of arguments, a symlink, the loader (default, empty, 400 KB, 700 KB), limits of 1024, 4096 and 65536 KB with and without a large environment, and no limit (default, 700 KB, loader).
The test.
complex.yamlrunsjsctwice behind the new helperTools/Scripts/jsc-stress-test-helpers/deny-prlimit64. The helper sets a stack limit of 8 MB, installs the filter with fiddle (no compiler), checks thatgetrlimit()fails under it, and startsjsc. The test holds the bound from both sides: 10,000 nested groups must compile (8 MB hold 34,627 in the x64 LTO build) and 100,000 must throw. It does not pin the 8 MB exactly. The entry skips itself under ASan, whose own start reads the limit and aborts, in a bundle for another machine, and where the helper cannot install the filter (it asks the helper first).Darwin. XNU can fail a system call for a sandboxed process before its handler runs (
unix_syscall,bsd/dev/arm/systemcalls.c, themac_proc_check_syscall_unixbranch). The arm gets the same start value. No macOS lane runs the new test.Not changed here.
newThreadStackBounds()ignores the result ofpthread_getattr_np().size -= pagesizewraps for a limit below one page.