Repository navigation
Conversation
bun.exe linked with /STACK:0x1200000,0x200000. The second operand is SizeOfStackCommit in the PE header. Windows charges it against the system commit limit when a thread starts, for every thread that passes no commit size of its own, and does not start a thread it cannot charge it for. That is every WTF thread, every Rust std thread, mimalloc, zstd, sqlite and the threads the OS adds to the process. JSC's GC helper and JIT threads exit when idle and start again later, and WTF aborts when one does not start. - Link with /STACK:0x1200000,0x1000. One page is the linker's default, and what node.exe and WebKit's own executables ship. The stack is committed as it is touched. - verify-binary.ts checks SizeOfStackReserve and SizeOfStackCommit of bun.exe and of the .bin shim against binary-expectations.ts. - The MemoryPressure watcher and the IO pool reserve 2 MiB on Windows. They asked for 64 KB and 512 KB, less than the old commit size. Co-authored-by: Shelley Vohr <2036040+codebytere@users.noreply.github.com>
Two test files that fail on purpose on Windows and print measurements: commit per thread start, real stack sizes, startup time and page faults, an idle server, a process at a commit limit, and unprobed regexp frames on JIT threads. Each compares the executable under test with a copy whose SizeOfStackCommit is set back to 2 MB. To be removed before this PR is ready.
|
Updated 5:24 AM PT - Oct 15th, 2026
✅ @robobun, your commit e083680107c81183d924a98949618e60a6ada95f passed in 🧪 To try this PR locally: bunx bun-pr 45094That installs a local version of the PR into your bun-45094 --bun |
|
Status: ready for review.
|
…to small stacks The two stack-commit-measure test files did their work on the Windows test machines (Server 2019 x64, Windows 11 arm64). The numbers are in the PR description. One of them goes into the comment on WINDOWS_STACK_COMMIT: under the 2 MB commit, a thread that asked for a 64 KB, 512 KB or 2 MB reserve got a 2 MB stack that was committed in full, with no guard page.
…ows-stack-commit # Conflicts: # scripts/build/verify-binary.ts
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughThe Windows build now configures an 18 MB PE stack reserve and a one-page commit. Binary checks and tests cover these values and per-thread commit charges. The memory-pressure watcher and IO thread pool also use updated stack sizes. ChangesWindows Stack Sizing
Suggested reviewers: Priority: ⬆️ High Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Beyond the inline finding, I also checked the two Rust stack-size bumps: the MemoryPressure watcher (src/runtime/node/memory_pressure.rs:457) and the IO pool (src/threading/io_thread_pool.rs:34) now request 2 MiB, which on Windows is a reservation only and matches the stack they effectively had under the old 2 MB commit floor, so neither loses usable stack. The shim's expected 0x100000/0x1000 in binary-expectations.ts matches lld-link's defaults for a link that passes no /STACK, so that check is not a new CI hazard.
Extended reasoning...
The change switches the Windows link to /STACK:0x1200000,0x1000 (one page of initial commit per thread), adds a PE header reserve/commit check to verify-binary.ts with literal expectations for bun.exe and the shim, raises the Windows stack request of two Rust threads to 2 MiB, and adds a cross-config lint test plus a Windows-only test that reads the PE header and measures commit per thread start. It touches no auth, crypto, or input-parsing surface. A human look is warranted because the inline finding is a real behavioral trade-off the author documents (Yarr frames on JIT worklist threads can now fault where the base ran), which is a product decision rather than a code defect.
| }, | ||
| { | ||
| flag: ["/STACK:0x1200000,0x200000", "/errorlimit:0"], | ||
| flag: [`/STACK:${WINDOWS_STACK_RESERVE},${WINDOWS_STACK_COMMIT}`, "/errorlimit:0"], |
There was a problem hiding this comment.
🔴 Windows users whose script has a large constant regexp tested against a constant string get an access-violation crash (exit 0xC0000005) that the base build does not have. The /STACK:...,0x1000 operand at scripts/build/flags.ts:1044 leaves a JIT worklist thread with a 4 KB committed stack plus one guard page, and Yarr's JIT prologue moves the stack pointer by the whole frame without a probe when DFGStrengthReductionPhase folds the match on that thread. Fix: before shipping a one-page header, make every thread that runs JIT-generated Yarr code safe for frames above one page: give the JIT worklist threads a commit size of their own (WTF Thread::create) or add a stack probe to the Yarr prologue, so the guard page cannot be skipped.
Why this was flagged
A script with a constant regexp of many capture groups, e.g. /^(a?){N}b$/ written out, tested in a hot loop against a constant subject, so the DFG constant-folds the match on a JIT worklist thread via RegExp::matchConcurrently. With scripts/build/flags.ts:1044 now emitting /STACK:0x1200000,0x1000 (WINDOWS_STACK_COMMIT at flags.ts:53), that thread is created by WTF with size 0 and starts with 4096 bytes committed and a single guard page; nothing pre-commits a JIT thread's stack (preCommitStackMemory only runs on JS threads). The Yarr JIT prologue subtracts the full frame from the stack pointer without a probe, so a frame larger than about 16 KB lands past the guard page and the process dies with 0xC0000005. On the base the header commits 0x200000 bytes, so the same frame lands on committed pages and the script runs. The hardening/stack checks in scripts/build/verify-binary.ts:636-644 only confirm the header values and cannot catch this. The PR description measures 15.6 KB passing and 31.3 KB, 46.9 KB and 125 KB failing on both x64 and arm64.
Verification: On Windows, a script whose hot function tests a large constant regexp against a constant string, so the DFG folds the match on a JIT worklist thread. scripts/build/flags.ts:1044 now emits /STACK:${WINDOWS_STACK_RESERVE},${WINDOWS_STACK_COMMIT} with WINDOWS_STACK_COMMIT = "0x1000" (flags.ts:53), replacing /STACK:0x1200000,0x200000. JIT-generated Yarr code emits no __chkstk-style probe.
There was a problem hiding this comment.
This is correct. It is the second bullet under Downsides, and the numbers quoted here are the ones measured for this PR.
The gap is in JSC. For a match on a compiler thread, Yarr takes its stack limit from the reserve of the thread, and nothing commits or probes the frame. Upstream JSC has the same gap with its own one-page header. The 2 MB commit covered it for frames below about 1.9 MB.
Both fixes named here are changes in oven-sh/WebKit:
- A commit size of their own for the JIT worklist threads puts the large request back on the threads of one of the two crash groups this PR is about (BUN-2V2T is
JITWorklist::wakeThreads). - A probe in the Yarr prologue, or a stack limit taken from the committed stack so that the DFG does not fold the match, fixes it where it is. That fix is separate work, with a Windows test for the 31.3 KB frame.
The order is a decision for the maintainer:
- This PR after the WebKit bump that carries the Yarr fix: no new failure.
- This PR first: until that bump, a match that the DFG folds crashes when its Yarr frame is above about 16 KB. The largest frame in 4,219 regexes from packages is 6 KB, and the match needs a constant subject in hot code.
Problem
WTFCrashWithInfounderWTF::Thread::create, exit code0xC0000409(561 Sentry events in 90 days)./STACK:0x1200000,0x200000(scripts/build/flags.ts:1020) setsSizeOfStackCommit. Windows charges it when any thread starts, and refuses the thread when it cannot.Fix
/STACK:0x1200000,0x1000, as node.exe and WebKit do. A thread start commits 28 to 57 KB, not 2.1 MB.verify-binary.tschecks both header fields of bun.exe and the.binshim on every CI link.test/internal/source-lints/windows-cross-config.test.ts, and on Windowstest/js/bun/binary/tls-segment-size.test.ts. Both fail with the old flag.Background
Downsides
0xC0000005): a regexp match that the DFG folds on a JIT thread, when its Yarr frame is large. 15.6 KB passes, 31.3 KB fails. The largest of 4,219 real regexes needs 6 KB.Notes
How the Windows numbers were taken. On the CI test machines (Windows Server 2019 x64 and Windows 11 arm64, 4 cores each, build 124918). "4k" is the bun.exe of this PR. "2m" is a copy of it with the 8 bytes of
SizeOfStackCommitset back to0x200000: the same code, linked the way main links it. On a stub, a link with,0x200000and this edit give the same file.Commit per thread start (32 threads parked in
Sleep,PrivateUsagedelta / 32):CreateThreadsize 0STACK_SIZE_PARAM_IS_A_RESERVATIONThe test in this PR printed 50,176 / 44,416 (x64) and 45,952 / 49,920 (arm64).
What a thread gets (reserve KB / initial commit KB, from the TEB of a thread created suspended, same on x64 and arm64):
Under the 2 MB commit, a thread that asked for 2 MB or less got a 2 MB stack that was committed in full, with no guard page. That covers the MemoryPressure watcher, the IO pool and every Rust thread with the default size. They have a guard page again.
The reported failure, under a job object commit limit. The process limits itself to its current commit plus a headroom.
Bun.spawnreports the low byte of a Windows exit code: 9 is0xC0000409, the code of the report, and 3 is bun's crash handler.Idle
Bun.servecommit, MB (PrivateUsage; at start / 5 s / 35 s / after one fetch / after a GC):Startup (CreateProcessW to exit, median of interleaved runs. "noise" is a second copy of 2m against 2m.)
--version(20)-e 0(30)empty.js(40)--version(20)-e 0(30)empty.js(40)The cost is per JS thread and comes from JSC's pre-commit loop (
preCommitStackMemory). Each of the first 512 pages was a plain page fault and is now a guard-page move. The number of faults does not change. A process that starts no VM (--version) does not pay.Regexp frames on JIT threads.
DFGStrengthReductionPhaserunsRegExp::matchConcurrentlyon a JIT worklist thread when the pattern and the subject are constants. The Yarr prologue moves the stack pointer by the whole frame without a probe, and nothing commits a JIT thread's stack ahead of it. Script:/^(a?){N}b$/written out,.test("aaab")in a loop of 30,000, run as a file.0xC0000005)Same on x64 and arm64. Upstream JSC has the same gap with its own one-page header. A commit of 64 KB in the header was the fallback if a frame of real size had failed. It did not. The fix in JSC (a limit from the committed stack, or a probe in the Yarr prologue) is separate work.
Merge order. The regexp crash goes away with a fix in JSC (a probe in the Yarr prologue, or a stack limit taken from the committed stack). After the WebKit bump that carries it, this PR adds no new failure. Before it, the window is a folded match with a Yarr frame above about 16 KB.
Other checks.
bun-profile 18874368 bytes of stack reserved, 4096 committed per thread,bun-shim-impl 1048576 bytes of stack reserved, 4096 committed per thread, on the x64 and the aarch64 build.SizeOfStackCommit./STACK:0x1200000and/STACK:0x1200000,0x1000give the same file.bun build --compilekeeps the template's stack fields (checked with stub templates: 4096 in gives 4096 out, 2097152 in gives 2097152 out). An executable compiled from an older downloaded template keeps 2 MB.RangeErrordepth of one recursive function: 53,199 frames (x64) and 53,206 (arm64) with both headers. Runs where the JIT tiers up on the way go deeper with both.verify-binary.tson a stub linked with the old flag:SizeOfStackCommit 2097152, expected 4096, exit 1. It starts 3 child processes before and after.Sentry.
WTFCrashWithInfounderWTF::Thread::createunderWTF::AutomaticThread::start: BUN-2V38 (GC helper pool, fromJSC::Heap::runBeginPhase) and BUN-2V2T (JIT worklist). The helper threads exit when idle and start again, so an idle process dies at a GC timer tick.Other designs. Only surviving a refused start (oven-sh/WebKit#569, #39864) is needed too, but the 2 MB request stays on every thread. A commit size in WTF's
Thread::createleaves the main thread, Rust std, mimalloc, zstd, sqlite and the OS's threads at 2 MB.History. The commit operand has no recorded reason. It was 2 MB in the first Windows build work, 1 MB in #6938, and 2 MB again in #15985 by a commit named "Double the thread stack size on Windows". The operand is the initial commit. The stack size (the reserve) stayed 18 MB.
Related, not in this PR.
os error 1455. A native addon's threads take bun.exe's header commit too.flags.tsentry for ASan. Whichever lands second keeps that arm and branches the expectation oncfg.asan.no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/binary/tls-segment-size.test.ts, test/internal/source-lints/windows-cross-config.test.ts