Skip to content

Bump WebKit (oven-sh/WebKit#946 preview): a RegExp match that the DFG folds on a JIT thread stays within the committed stack on Windows - #47416

Draft
robobun wants to merge 9 commits into
mainfrom
robobun/63d2abac/regexp-fold-jit-thread-stack
Draft

robobun wants to merge 9 commits into
mainfrom
robobun/63d2abac/regexp-fold-jit-thread-stack

Conversation

@robobun

@robobun robobun commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #45094

Problem

  • On Windows, a RegExp match that the DFG folds on a JIT thread can end the process with exit code 0xC0000005. bun.exe does that for a frame above its 2 MB of stack commit.
  • RegExp JIT code moves the stack pointer a whole frame without a probe. JavaScriptCore takes the JIT thread's limit from the stack reserve (yarr/YarrMatchingContextHolder.h:68).
  • windows: commit one page of stack per thread and check the stack sizes of the linked exe #45094 reduces the commit (scripts/build/flags.ts:1137) to one page. Then a frame of 20 KB is enough.

Fix

Background

Downsides

Notes

The pin, and what must happen before this merges

The preview build is the engine that main pins (8c6c963e3c49) plus the commits of oven-sh/WebKit#946 up to 6672ab7320. That is the last commit before that pull request merged WebKit main into its branch. main here does not pin WebKit main yet: #47112 does that, and it updates the 383 test files that depend on the engine. A preview release goes away when its pull request merges or closes, so this pull request is a draft.

  1. [JSC] Keep a compiler thread's RegExp fold within the committed stack on Windows, and fold only a match that was answered WebKit#946 merges.
  2. main here pins WebKit main. WebKit main is ahead of the pin: it has the upstream upgrade of Upgrade to upstream WebKit e8f704815e94 WebKit#923, which Upgrade WebKit to e8f704815e94 #47112 brings here, and [JSC] AOT: handlers at the end of the stack, text constants in flow, samples in native code WebKit#934.
  3. This pull request moves WEBKIT_VERSION to a commit of WebKit main that has [JSC] Keep a compiler thread's RegExp fold within the committed stack on Windows, and fold only a match that was answered WebKit#946. If another pull request already did that, only the test is left.

#45094 names this limit as what removes its one new failure. The order of the two is for a maintainer to decide. The test here does not depend on #45094: it sets the stack commit of a copy of bun.exe itself.

The test

test/js/bun/jsc/regexp-fold-jit-thread-stack.test.ts is for Windows only. It has two tests. Each runs a script file in which the event loop calls a function until numberOfDFGCompiles says that the DFG has compiled it. So nothing inlines the function, and the JIT thread has finished with it before the script goes on. The match in each function has a constant pattern and a constant string. Each test expects one line for each function, an empty stderr and exit code 0. The loop has no limit of its own: a function that the DFG never compiles ends in the timeout of the test.

  1. bun.exe as built. The pattern has 140,000 optional terms, which is a frame of 2.24 MB. The test first reads the stack commit from the header of bun.exe and expects less than that. Two BUN_JSC_* variables lower the JIT thresholds, because one call takes about a millisecond.
  2. A copy of bun.exe with 0x1000 in SizeOfStackCommit of its PE header. Four functions: test and exec with a frame of 128 KB, a group that repeats 24 times (a frame of 14 KB, and as much again from the stack for each turn), and a 128 KB frame for a RegExp that has bytecode. For the last one the JIT thread runs the interpreter after its JIT code finds no room.
build JavaScriptCore Windows Server 2019 x64 Windows 11 aarch64
135863 8c6c963e3c49, the pin of main both tests fail: exit code 5, no output the same
135777 preview build 7d7c45d0 of oven-sh/WebKit#946 both pass both pass
135866 preview build 6672ab73, this pin both pass both pass

Exit code 5 is the low byte of 0xC0000005. The process ends on the JIT thread, below the guard pages, where no handler can run. The file takes about 1 second.

The script is a file and not -e: bun -e compiles on the thread that runs JavaScript (JSC::Options::useConcurrentJIT() = false in JSCInitialize).

Earlier forms of the test gave the same results: build 135764 (the pin of main, both tests fail), and builds 135536, 135602 and 135622 (only the copy with one page: fails on the pin of main, passes on the previews b4e7b533 and 2f9450ab).

What folds, before and after

A temporary test file, now removed, printed these numbers on the Windows test machines. The program folds one match in a function that the event loop calls until the DFG has compiled it. One JIT thread. Whether the match is folded is read from the code of the function (BUN_JSC_dumpDFGDisassembly=1). Build 135755 has the pin of main, build 135777 has the preview 7d7c45d0. The test file ran four times in each build, with five runs of each case, so each case has 20 runs on each machine.

bun.exe as built. Its header has 2,097,152 bytes of stack commit.

frame pin of main this engine
1.92 MB (120,000 terms) folds in 20 of 20, on both machines the same
2.24 MB (140,000 terms) exit code 5 in 20 of 20, on both machines not folded in 20 of 20, exit code 0

A copy with one page of stack commit, /^(a?){N}b$/.test("aaab"), 16 bytes of frame for each term. The sizes are 4, 4.8, 6, 7.2, 8.6, 10.2, 12.8, 16, 20, 24, 32, 48 and 128 KB.

function machine pin of main this engine
the first that the JIT thread optimizes x64 folds up to 12.8 KB, 19 of 20 at 16 KB, exit code 5 from 20 KB folds up to 4 KB, 18 of 20 at 4.8 and 6 KB, 3 of 20 at 7.2 KB, not folded from 8.6 KB
the first that the JIT thread optimizes aarch64 folds up to 20 KB, exit code 5 from 24 KB folds up to 7.2 KB, 9 of 20 at 8.6 KB, 10 of 20 at 10.2 KB, not folded from 12.8 KB
a later one x64 folds up to 32 KB, exit code 5 from 48 KB folds up to 20 KB, not folded from 24 KB
a later one aarch64 folds up to 24 KB, 17 of 20 at 32 KB, exit code 5 from 48 KB folds up to 16 KB, 10 of 20 at 20 KB, not folded from 24 KB

Other patterns, one page of stack commit:

match pin of main this engine
/^\p{RGI_Emoji}$/v, a frame of 21.7 KB, first function x64: exit code 5 in 20 of 20. aarch64: in 11 of 20 not folded
the same, a later function folds x64: folds in 16 of 20. aarch64: not folded
a group that repeats, frame of 18 KB (8 turns) or 14 KB (24 turns) exit code 5 in every run not folded
a group that repeats, frame of 3.6 KB, 64 turns folds not folded
a frame of 128 KB, RegExp with bytecode exit code 5 in every run folds: the interpreter answers on the JIT thread

With this engine no run ended with an exit code other than 0.

The first measurements (builds 135536 and 135602, five runs of each case) also covered a copy with 2 MB of stack commit, frames from 0.4 KB, the other operations and the default number of JIT threads. With 2 MB every size up to 128 KB folds in every run with both engines. exec, match, search and replace behave as test does. With the three JIT threads that bun starts by default and one page, 1.6 KB folds in every run and nothing folds from 16 KB. The largest frame among 4,219 regexes of the packages in test/node_modules is 5,984 bytes (measured for #45094).

The engine without the fix folds some frames that the fix gives up: those that end in the guard region, where the kernel commits the stack on a write. oven-sh/WebKit#946 says why the limit is not lower.

The cost of a match that is not folded

jsc of the WebKit branch on Linux x64, against a copy in which the fold is always refused. Instructions in user space, three runs each.

program calls folded not folded per call
/^(a?){375}b$/.test("aaab"), 5.9 KB frame 2,000,000 156 to 166 M 3,628 M 1,734
/^(a?){25}b$/.test("aaab"), 0.4 KB frame 20,000,000 1,144 to 1,150 M 8,109 to 8,111 M 348
/^a+b$/.test("aaab") 20,000,000 1,128 to 1,151 M 2,417 to 2,446 M 65

The second change in that build

On every platform, the RegExp interpreter reports a search that it gives up as no match, and the DFG could fold test() to false for a string that matches. With oven-sh/WebKit#946 a JIT thread folds a match only when the engine answered it. That pull request has three tests for it.

Size

From the bun-webkit builds, 8c6c963e3c against the preview: .text of jsc.exe grows by 1,280 bytes on Windows x64 and by 1,152 on arm64, and .text of the Linux x64 LTO jsc by 768. The match functions of the thread that runs JavaScript have the same instructions in both builds, on Linux and in the Windows libraries.

The size of bun itself is not measured: the build artifacts of CI (S3) could not be downloaded from the machine that wrote this.

…the committed stack (Windows)

The test runs a copy of bun.exe whose PE header commits one page of stack
for each thread. The DFG folds a match of a constant pattern against a
constant string on a JIT thread, and the frame of the match is 128 KB.

With the JavaScriptCore that main pins, the frame is written below the guard
page of that thread and the process ends with exit code 0xC0000005. The
WebKit change that this branch will pin gives the fold up instead.
…ows test machines

A test file that fails on purpose on Windows and prints measurements: for a
match that the DFG folds on a JIT thread, whether it is folded, kept or ends
the process, by the size of its frame, by the stack commit of the executable
and by the order of the compilations. To be removed before this pull request
is ready.
@robobun

robobun commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:03 PM PT - Oct 30th, 2026

❌ @robobun, your commit 65c5d40 has 2 failures in Build #135866 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47416

That installs a local version of the PR into your bun-47416 executable, so you can run:

bun-47416 --bun

@robobun

robobun commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: draft. The diff is the pin and one test file. It waits for oven-sh/WebKit#946 to merge, and for main here to pin WebKit main (#47112).

The build is the JavaScriptCore that main pins (8c6c963e3c49) with two
changes. On Windows the stack limit of a JIT thread that runs RegExp JIT
code is no lower than the stack that the thread has committed. And a JIT
thread whose RegExp JIT code fails gives the fold of the match up. It does
not run the interpreter.

WEBKIT_VERSION must move to the merge commit of that pull request before
this merges: a preview release goes away when its pull request closes.
@robobun robobun changed the title Bump WebKit: a RegExp match that the DFG folds on a JIT thread stays within the committed stack on Windows Bump WebKit (oven-sh/WebKit#946 preview): a RegExp match that the DFG folds on a JIT thread stays within the committed stack on Windows Oct 30, 2026
…sh/WebKit#946 at 2f9450ab

The measurements ran on the Windows test machines in builds 135536 (the
JavaScriptCore that main pins) and 135602 (the preview build b4e7b533).
Their numbers are in the pull request.

The preview build 2f9450ab has the same engine as b4e7b533. The commit
between them changes one test of JavaScriptCore.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant