Skip to content

AOT: stack traces as in bun run, code from disk, workers, embedded files, module mocks, build cost - #48478

Open
Jarred-Sumner wants to merge 12 commits into
mainfrom
claude/aot-stack-traces-workers-and-embedded-files
Open

Jarred-Sumner wants to merge 12 commits into
mainfrom
claude/aot-stack-traces-workers-and-embedded-files

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Bun's side of a round of work on the ahead-of-time compiler (bun build --compile --aot and bun test --aot, both experimental). The JavaScriptCore side is oven-sh/WebKit#1028 and #1030. This follows #48436.

Merge order

oven-sh/WebKit#1030 merges first; scripts/build/deps/webkit.ts pins its preview until then.

What's in it

2,878 files, +62,639 −4,042 against main.

Area Files + −
test: stored programs, test runs and tests 2,561 48,496 740
src/aot: the compiler's front end 142 6,044 1,042
src/sema: the type checker 81 4,027 568
docs 3 20 10
Everything else 91 4,052 1,682

A stored program is a small program that CI compiles with --aot, runs, and compares with bun run. A stored test run does the same with bun test --aot and bun test.

If you don't use --aot

File + − Change
src/standalone_graph/compiled_program.rs, src/standalone_graph/StandaloneModuleGraph.rs and 7 more 545 117 Embedded files: fstat(), open() and lookups answer as on disk.
src/resolver/resolver.rs, src/resolver/compiled_program.rs and 2 more 121 5 Code from disk beside compiled files. Plain Bun asks one boolean
src/jsc/bindings/BunCompiledModules.cpp, src/jsc/bindings/BunCompiledModules.h and 11 more 1375 1266 Modules and mocks of compiled programs; plainer names
src/jsc/bindings/ErrorStackTrace.cpp, src/jsc/bindings/ErrorStackTrace.h and 4 more 57 5 The path that a stack trace prints
src/jsc/bindings/AOTTextCoding.cpp, src/jsc/bindings/webcore/JSTextEncoder.cpp and 10 more 472 48 Entries of compiled code for encode(), decode() and five functions of node:fs; native dictionaries read compiled objects
src/jsc/bindings/BunCompiledObjectEquality.h, src/jsc/bindings/RemovedFeature.cpp and 4 more 145 93 Equality of compiled objects; what --trim removed; data: URLs; delete in a node:vm context over a Proxy
src/jsc/bindings/TestEnvironment.cpp 10 1 The realms of jsdom and happy-dom under --aot --isolate
src/runtime/cli/compile_command/build.rs, src/runtime/cli/compile_command/failure.rs and 10 more 276 48 bun compile, --aot-harden
src/js/ahead-of-time/internal/fs/streams.ts, src/js/ahead-of-time/node/events.ts and 10 more 302 52 The library that compiled programs run
src/bun_alloc/MimallocArena.rs, src/bun_alloc/ast_alloc.rs and 8 more 689 39 Memory, counters and the stack check of the build's front end
scripts/build/deps/webkit.ts 1 1 The pin
scripts/verify-baseline-static/allowlist-x64.txt, scripts/verify-baseline-static/allowlist-x64-windows.txt and 1 more 44 0 Two new Highway functions that run-time dispatch alone calls; the Windows lines are by analogy
src/jsc/bindings/BunSignalActions.cpp 6 4 Signal handlers on Linux
src/runtime/jsc_hooks.rs 9 3 A VM frees its NodeFS, which only compiled code makes

Plain fstatSync() takes 255.5 M instructions in our loop, 256.7 M on main. Plain Bun too, in the optimizing JIT: on x64 a WebAssembly load whose low bytes alone are used returned a value at the end of the memory where it should trap, f32.demote_f64 of a converted 64-bit integer rounded once where it rounds twice, and a signaling NaN stayed signaling through f32.demote_f64 of f64.promote_f32. Fixed.

Executables and builds

  • Two tests of bounds in a row, such as if (v < 3) { a = 5; continue; } if (v > 9) { b = 7; continue; }, took the first's arm for both where both arms are short. Fixed.
  • A value that a class lays over a member of a constructor function below it could be taken for false. --compile-inspector over export * stopped the build. Fixed.
  • Stack traces name a source by its path relative to the folder of the build (./src/inner.ts:2:13, before /$bunfs/root/inner.ts) and report bun run's positions.
  • An ahead-of-time build refuses to write its executable over a file of its program.
  • A file that only an import() names must parse, also where it never runs, as with bun build --compile; before, "sideEffects": false could leave it out.
  • React Compiler: a callback that isn't memoized could be left out of the executable, and calling it ended the program; a component beside an export * could stop the build; a file that is imported only for code that the compiler drops did not run. Fixed.
  • An entry point's own import statements always run, whatever "sideEffects" says.
  • With --aot-allow-code-from-disk, a file from disk finds the files of its packages on disk before compiled ones, and new Function("module", "return import(module)") loads.
  • CommonJS: an ES module that imports a barrel runs its getters, require() of a file whose module.exports is a getter runs it, a module loaded by a fresh require() names its parent.
  • Workers: files that URLs name are found beside any use of import.meta, a start that leads to no file warns (TS100082), and a worker that runs the entry file has import.meta.main false.
  • --asset: a folder outside the root that shares its base name with a folder below it is merged with it. Reads that an asset cannot answer warn (TS100087), as do reads through a symbolic link in its folder (TS100165). On Linux readFile() of an embedded file of at least 1 MB maps its pages.
  • Linux: the four libc signal functions that ask who called them are no longer inlined, so a chained handler gets the kernel's mask.

bun test --aot

  • --isolate with jsdom and happy-dom: what the build compiled runs in their realms. In 41 jsdom test files of a public suite 246 tests pass; before 101, bun test 249.
  • Module mocks: factories that take their original, in chains and cycles, names that a mocked file's module lacks, files that fail behind a mock.

Build cost

  • Less memory and work: packed records, indexes of import sites, quadratic cases in unions, enums, barrels and patterns.

Known issues

Where What differs How it shows State
bun test --aot A test file that mocks, with its original, a module of its own running graph, beside another such mock in a helper, can end with "never finished loading". loud not on main
Executables with --aot-harden=proof-checks A method that is replaced by that of another class stops a build. These can stop one: a replaced generator method that is stepped by hand, a test of a list's length against the length before a push() or pop(), a closure with two captured variables that is called in a loop and, beside it, inside a list callback, row[i] += factor * b[i] in a nest of two loops where factor is undefined in one call, and a getter that switches on a variable of its file that holds a constant. loud not on main
bun test --aot With vi.doMock(), a factory that imports its own file and vi.importActual(), and with a mock of a file that a package has imported before, the answer is wrong as on main, without main's notice. silent as on main
Executables A bare .cjs file loaded with import() runs in sloppy mode when a require() that never runs also names it. silent as on main
Builds A build does not end where some eight or more branches of one if / else chain join onto the same field, or some 24 objects' fields are joined twice each from the next one's, and a file read uses the field. hangs not on main
Executables In a stack trace, where the conversion of an operand throws, or a constructor whose object is kept in a variable, the caller's line names the start of the expression and not the operator. position not on main

Lines of stack traces no longer include import.meta.path, by design. The embedded file lookups on Windows were not run.

CI

The engine is the preview of 3f5b3629. Nothing was built or run on this tree before the push.

…les, module mocks, build cost

On 89452d2, the merge of #48436. 2,584 files, +57,669 -2,633: scripts 1 file +1 -1 . src
295 files +12,098 -1,957 . test 2,284 files +45,549 -664 . docs 3 files +20 -10 . packages 1 file +1
-1. Pins the preview build of oven-sh/WebKit#1028. [skip size check], as in 607519a.

Executables and builds

- Stack traces name a source by its path relative to the folder of the build, as in
  `./src/lib/inner.ts:2:13` instead of `/$bunfs/root/lib/inner.ts`, and report the positions that
  `bun run` reports. What code looks files up by, such as `getFileName()` and import.meta, stays.
- A build that compiles ahead of time refuses to write its executable over a file of its program
  (`--outfile`, `bun compile`, `compile.outfile`).
- An entry point's own import statements always run, whatever "sideEffects" says: the command line
  tools of uuid and vitest printed nothing.
- With `--aot-allow-code-from-disk`: files from disk find files on disk before compiled ones in
  packages that they ask for by name, `new Function("module", "return import(module)")` loads, and
  the library of evaluated code is kept. Found by postcss, @babel/cli, depcheck, knex, webpack and
  prettier.
- import() of a data: URL that the program writes is part of the executable, also under
  `--disallow-code-generation-from-strings=strict`.
- CommonJS: an ES module that imports a barrel runs its getters as `bun run` does, require() of a
  file whose `module.exports` is a getter runs it, a module loaded by a fresh require() names its
  parent.
- Workers: files that URLs name are found beside any use of import.meta, a start that leads to no
  file warns (TS100082), a worker that runs the entry file has import.meta.main false, and a file
  that imports what cannot be resolved warns and reports the error event instead of stopping the
  build.
- `--asset`: a folder outside the root that shares its base name with a folder below it is merged
  with it. Reads that an asset cannot answer warn (TS100087). A read through a symbolic link inside
  an --asset folder is warned of (TS100165), and the warning names the link to pass as an asset of
  its own. On Linux readFile() of an embedded file of at least 1 MB maps its pages.
- On Windows an embedded file is found with a trailing separator, over a.txt/../a.txt and with a
  lower-case drive letter. Three lines of it are compiled on Windows alone and were built by nobody
  yet.
- fs.fstatSync(), lstat() and open() of embedded files answer alike; below an embedded file they
  answer ENOTDIR. Plain fstatSync() costs no more than before.
- A script without an extension takes octal escapes, and one with a byte order mark before `#!` is
  read. A comparison such as `process.argv[1] === __filename` is true where both name one file.
- --trim: the functions that stand in for `Bun.sql`, `Bun.postgres` and `Bun.SQL` are one small
  class with one trap instead of proxies with nine.
- The build no longer warns about a path that is overwritten before anything reads it.
- A very long chain of operators no longer ends the build by a signal: it is checked for the stack.

`bun test --aot`

- `--isolate` with jsdom and happy-dom: what the build compiled runs in their realms. In 41 test
  files of a public suite that use jsdom 246 tests pass, 101 before, 249 in `bun test`.
- Mocks: chains whose factories take their original, originals that import each other in a cycle,
  names that a mocked file's module lacks, files that fail behind a mock. A test file that never
  finishes loading names the mock that it waits for.
- Coverage counts stored fields and emitted enums as in `bun test`.

Build cost

- Less memory: packed layouts and function records, smaller type records and string tables, pages
  that the front end is done with are given back. Less work: indexes of import sites built once a
  file, and quadratic cases in unions, enums, barrels and nested patterns.

Known issues

- With `--aot-harden=proof-checks`, a loop in a loop that ends at a test for `undefined` stops the
  build. Main builds it.

- As on main: in the second pass of `--rerun-each`, an import() that is refused again behind an
  unrelated mock is rejected with another error object, and a CommonJS test file times out when a
  mock factory takes the original of a file that threw. In executables, a bare .cjs file loaded with
  import() runs in sloppy mode when a computed require() can also name it.

- A line of a stack trace no longer includes import.meta.path; that is by design. The embedded file
  lookups on Windows were not run.

Not built as one tree: CI's run of this pull request is its first.
@Jarred-Sumner
Jarred-Sumner requested a review from alii as a code owner November 10, 2026 13:40
@robobun

robobun commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 10:05 PM PT - Nov 10th, 2026

❌ @Jarred-Sumner, your commit 7e51860 has 1 failures in Build #140019 (All Failures):

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #139772
    sizeΔ
    ❌ bun-darwin-aarch64121.58 MB120.47 MB+1.10 MB
    ❌ bun-darwin-x64132.00 MB130.91 MB+1.10 MB
    ❌ bun-linux-aarch64138.62 MB137.50 MB+1.13 MB
    ❌ bun-linux-x64137.90 MB136.84 MB+1.05 MB
    ❌ bun-linux-aarch64-musl124.21 MB123.40 MB+832.0 KB
    ❌ bun-linux-x64-musl126.09 MB125.22 MB+892.0 KB
    ❌ bun-linux-aarch64-android130.43 MB129.49 MB+960.1 KB
    ❌ bun-linux-x64-android138.39 MB137.37 MB+1.02 MB
    ❌ bun-freebsd-x64134.62 MB133.62 MB+1.00 MB
    ❌ bun-freebsd-aarch64137.15 MB136.11 MB+1.04 MB
    ❌ bun-windows-x64147.05 MB145.89 MB+1.16 MB
    ❌ bun-windows-aarch64130.85 MB129.80 MB+1.05 MB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 48478

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

bun-48478 --bun

On e7f2347. 1 file, +2 -0: src 1 file +2 -0.

macOS and FreeBSD did not compile: `stop_using_statx()` in src/sys/lib.rs uses
`SUPPORTS_STATX_ON_LINUX`, which is defined on Linux and Android alone. The function and
`IS_FSTATX_DIRECT` have the same condition now, as every use of them.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the new stop_using_statx in src/sys/lib.rs for a macOS/FreeBSD build break — it is gated on cfg(any(target_os = "linux", target_os = "android")) like its callers, so it is not affected — and the simdutf prefix handling in decodeWithReplacement (src/jsc/bindings/AOTTextCoding.cpp:147-156), which advances by utf16_length_from_utf8 of the valid prefix on error, matching the WebKit TextCodecUTF8 pattern. This change spans ~2,584 files and the bug hunt stopped at its budget rather than running dry, so coverage is partial; the build and test suite were not run as part of this review.

Extended reasoning...

The diff is a ~57k-line change across 2,584 files touching the AOT compiler (src/aot, src/sema), JSC bindings, allocator internals (MimallocArena), node:fs/statx paths, and the WebKit dependency pin. It touches memory-safety-sensitive native code and the removed-feature bindings but no auth, crypto, or injection surface. Five confirmed findings are posted inline, and the hunt exited on max_bugs rather than a dry streak, so approval is not possible; this note only records the two candidate issues that were examined and ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread scripts/build/deps/webkit.ts Resolved
Comment thread src/bun_alloc/MimallocArena.rs Resolved
Comment thread src/jsc/bindings/RemovedFeature.cpp Resolved
…ct for a refused import, tests

On 80771f5. 96 files, +2,668 -1,413: scripts 4 files +45 -1 . src 24 files +1,467 -1,342 . test 68
files +1,156 -70. Pins the preview build of oven-sh/WebKit#1028's commit 348972fa.
[skip size check], as in 607519a.

- `bun test --aot --rerun-each`: in the second pass, an import() that is refused again behind an
  unrelated mock is rejected with the same error object as under `bun test`.
- BunPlugin.cpp and what calls it: 210 plainer names and shorter comments. No logic changes with the
  names. Two functions of the engine change their names with them, so this needs 348972fa.
- scripts/verify-baseline-static: the three allowlists name PackWordsImpl and UnpackWordsImpl, two
  Highway functions that are new in this pull request and are called by run-time dispatch alone. The
  check was red without them. The Windows lines are by analogy with x64.
- The counters of a page's units in MimallocArena.rs are 32 bits wide instead of 16, an atomic that
  the lists for arm64 already name.
- A thread that returned the last block of a page while the page's owner laid another block there
  could clear the new block. Fixed; found in review.
- Findings of the lints: ten of mordant and three of clippy. No behavior changes. Three stored
  records of the lowering are restated, and the lint of names that are compared as text allows the
  namespaces of hoisted imports.
- On macOS a descriptor of an embedded file is known again after its times are set back, and on
  Windows paths of assets are compared with the separators of the platform. Tests that CI showed to
  be wrong are corrected (sizes, positions inside built-ins of builds with assertions, tables of
  three reads, folders without a test, rows of node:fs). Tests of what the engine's commits fix:
  WebAssembly loads at the end of the memory, stores in loops that only a trap ends, calls behind an
  entry test, two kinds of loop under `--aot-harden=proof-checks`, lists of zeros.

Linux: the four libc signal functions that ask who called them are no longer inlined, so a chained
handler gets the mask that the kernel would have set.

Plain Bun too: on x64 a WebAssembly load whose low bytes alone are used returned a value at the end
of the memory where it should trap, in the optimizing JIT. Fixed.

Known issues: in executables, a bare .cjs file loaded with import() runs in sloppy mode when a
computed require() can also name it, as on main. A program whose heap is exhausted can hang in the
collection that runs without helper threads, as on main.
…ings of the review, tests

On 8fa04f2. 93 files, +1,124 -118: scripts 1 file +1 -1 . src 16 files +482 -116 . test 76 files
+641 -1. Pins the preview build of oven-sh/WebKit#1028's commit 83468e92. [skip size check], as in
607519a.

Plain Bun too, in the optimizing JIT: `f32.demote_f64` of a converted 64-bit integer rounded once
where it rounds twice, and a signaling NaN stayed signaling through `f32.demote_f64` of
`f64.promote_f32`. Fixed.

- Findings of the review: `Bun.sql` of an executable without it throws the error of a Proxy's trap
  instead of a second one over it. Exception checks of deep equality are in functions, not lambdas.
  The helpers on paths of embedded files stand in one crate.
- Executables: a `require()` whose computed name the build has followed gives way to an `import()`
  that names the same CommonJS file, so a bare .cjs file that the import loads runs in strict mode
  as in `bun run`.
- Tests of the conversions and of data segments of WebAssembly.

Known issues: in executables, a text that grows by adding elements of the list that it is stored
into, over many turns, can take gigabytes where `bun run` takes about 60 MB, and with
`--aot-harden=proof-checks` a generator that stores into a `Float64Array` in a loop stops the build;
main does neither. A bare .cjs file loaded with import() runs in sloppy mode when a require() that
never runs also names it, as on main. A program whose heap is exhausted can hang in the collection
that runs without helper threads, as on main.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (1):

  • Unresolved: 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment on lines +1570 to +1571
bool isCompiled = sandbox->type() == CompiledObjectType || sandbox->type() == SlimClosureType;
bool deleted = isCompiled ? sandbox->methodTable()->deleteProperty(sandbox, globalObject, propertyName, slot) : sandbox->deleteProperty(sandbox, globalObject, propertyName, slot);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) In a compiled executable, delete x inside a vm context whose sandbox is a Proxy no longer runs the Proxy's deleteProperty trap, and the target keeps the property; the base AOT build honoured it. NodeVMGlobalObject::deleteProperty at src/jsc/bindings/NodeVM.cpp:1570-1571 now takes the method-table path only for CompiledObjectType and SlimClosureType, and every other sandbox is deleted through the static JSObject::deleteProperty, which bypasses ProxyObject's override. Fix: dispatch through sandbox->methodTable()->deleteProperty for every sandbox whose class overrides deleteProperty (at least ProxyObject and JSGlobalProxy), or for all sandboxes, as the base did when Bun::hasCompiledModules(). Pre-existing in bun run, where the same static call was already used.

Why this was flagged

A program compiled ahead of time calls vm.createContext(new Proxy(target, { deleteProperty(t, key) { ... } })) and then vm.runInContext("delete x", ctx). The delete reaches NodeVMGlobalObject::deleteProperty (src/jsc/bindings/NodeVM.cpp:1558). On the base branch that function used sandbox->methodTable()->deleteProperty whenever Bun::hasCompiledModules() was true, so ProxyObject::deleteProperty ran the trap. After the change, src/jsc/bindings/NodeVM.cpp:1570 sets isCompiled only for sandbox->type() == CompiledObjectType or SlimClosureType, and src/jsc/bindings/NodeVM.cpp:1571 otherwise calls sandbox->deleteProperty(sandbox, globalObject, propertyName, slot), the static JSObject::deleteProperty, which never consults the handler or the target. The user's trap is not invoked and the target's property survives, where the base AOT executable invoked the trap. No other guard covers it: Base::deleteProperty on line 1577 only removes the property from the NodeVMGlobalObject itself.

Verification: Base (src/jsc/bindings/NodeVM.cpp:1569) dispatched every sandbox through sandbox->methodTable()->deleteProperty in any compiled executable, so ProxyObject::deleteProperty ran the deleteProperty trap. Head (src/jsc/bindings/NodeVM.cpp:1570-1571) sets isCompiled only for CompiledObjectType or SlimClosureType; for a Proxy sandbox the static JSObject::deleteProperty runs instead, so the trap never runs and the target keeps the property.

Comment thread src/sys/private_copy.rs Resolved
On dd4008c. 69 files, +301 -5: scripts 1 file +1 -1 . src 3 files +24 -4 . test 65 files +276 -0.
Pins the preview build of oven-sh/WebKit#1028's commit 33c7b694. [skip size check], as in 607519a.

A file of at least 1 MB is embedded at a multiple of 16 bytes, so that the Buffer of `readFile()` is
aligned as before.

Tests of texts that grow by long additions, of lists beside stores into views, of mocks below
originals, and of an import() after one whose graph failed to link.

Known issues: in executables, with `--aot-harden=proof-checks` a loop over a list of the file that
calls a function which keeps the list, and a coroutine that passes a parameter on, can stop the
build; main builds them. A bare .cjs file loaded with import() runs in sloppy mode when a require()
that never runs also names it, as on main.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (2):

  • Unresolved: 1 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread src/sys/private_copy.rs
Comment on lines +40 to +58
fn offset_in_executable(bytes: &[u8]) -> Option<usize> {
let mut path = bun_paths::path_buffer_pool::get();
let length = crate::readlink(bun_core::zstr!("/proc/self/exe"), &mut path[..]).ok()?;
let executable = &path[..length];
let begin = bytes.as_ptr().addr();
let end = begin + bytes.len();
let maps = crate::File::read_from(crate::Fd::cwd(), b"/proc/self/maps").ok()?;
bun_core::strings::split(&maps, b"\n").find_map(|line| {
let mut fields = line.splitn(6, |&byte| byte == b' ');
let mut range = bun_core::strings::split(fields.next()?, b"-").map(parse_hex);
let low = range.next()??;
let high = range.next()??;
let protection = fields.next()?;
let offset = parse_hex(fields.next()?)?;
let (_device, _inode) = (fields.next()?, fields.next()?);
let file = fields.next()?.trim_ascii_start();
(low <= begin && end <= high && protection == b"r--p" && file == executable)
.then(|| offset + (begin - low))
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) On Linux, every fs.readFileSync of an embedded file of 1 MiB or more in an --aot executable now reads and parses the whole /proc/self/maps, which the base's single memcpy never did. embedded_file_buffer (src/runtime/node/node_fs.rs:538-544) calls private_copy::map on each call, and map calls offset_in_executable (src/sys/private_copy.rs:40-58), which readlinks /proc/self/exe, reads all of /proc/self/maps and scans every line; nothing caches the answer. Fix: resolve an embedded file's offset in the executable once (per File, or from the ImageFile descriptor and offset the graph already keeps) and reuse it on every read, keeping the copy fallback for bytes outside an r--p mapping.

Why this was flagged

A program compiled with --aot on Linux calls fs.readFileSync(path) (or fs.readFile) with no encoding on an embedded file whose size is at least MIN_MAPPED_BYTES = 1 << 20 (src/sys/private_copy.rs:6); read_file_in_graph (src/runtime/node/node_fs.rs:552-562) hands the bytes to embedded_file_buffer (node_fs.rs:538-549), which calls sys::private_copy::map(contents) on every call. map (private_copy.rs:12-31) calls offset_in_executable (private_copy.rs:40-58), which readlinks /proc/self/exe, reads the entire /proc/self/maps into a Vec and parses each line until it finds the r--p mapping of the executable, then opens /proc/self/exe again and mmaps. No field of File, Graph or a static caches the offset, so the maps scan repeats on every read of the same file. A JSC-based process has hundreds to thousands of VMAs, so generating and parsing /proc/self/maps costs on the order of a millisecond per call, where the base branch (contents.to_vec(), node_fs.rs:545-548) did one 1 MiB memcpy of tens of microseconds.

Verification: On Linux, an --aot program calls fs.readFileSync/readFile with no encoding on an embedded file of at least MIN_MAPPED_BYTES. src/runtime/node/node_fs.rs:538-544 calls sys::private_copy::map(contents) on every call; map calls offset_in_executable (src/sys/private_copy.rs:40-58), which reads /proc/self/maps (line 46). Nothing caches the offset. The base path was contents.to_vec(), a single memcpy.

…ts of the engine's fixes

On 793f226. 50 files, +465 -7: scripts 1 file +1 -1 . src 3 files +13 -5 . test 46 files +451 -1.
Pins the preview build of oven-sh/WebKit#1028's commit 4f3f6314. [skip size check], as in 607519a.

- A worker of a compiled program frees what its functions of node:fs made: CI's leak check reported
  16 bytes a worker.
- `bun build --compile --aot --compile-inspector` over `export *` no longer stops the build.
- Tests of two bounds in a row, of values that a class lays over a member of a constructor function,
  of loops that compare elements with numbers, of mocked files on one ring of imports, and of builds
  under `--aot-harden=proof-checks`. One test no longer asks for an order that `bun test` does not
  keep.

Known issues: with `--aot-harden=proof-checks` a method that is replaced by that of another class
stops a build; main builds it. A bare .cjs file loaded with import() runs in sloppy mode when a
require() that never runs also names it, as on main.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 3 findings from earlier reviews are still open above.

Still open from earlier reviews (3):

  • Unresolved: 2 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

On e9687e9. 7 files, +123 -1: scripts 1 file +1 -1 . test 6 files +122 -0. Pins the preview build
of oven-sh/WebKit#1028's commit 5473c680. [skip size check], as in 607519a.

A variable that a function captures and that a loop of several versions asks for could stop the
build: `const f = make(1);` and a counted loop that sums what `f(a, b)` gives. Fixed.

Two stored programs: of captured variables that counted loops sum, and of results of calls that are
unsigned integers.

Four stored programs of CommonJS and ES modules that load each other are compared with stored
outputs, since `bun run` itself does not always give the same.

Known issues: in `bun test --aot`, a test file that mocks, with its original, a module of its own
running graph, beside another such mock in a helper, can end with "never finished loading"; main
does not. With `--aot-harden=proof-checks` a method that is replaced by that of another class stops
a build, and a replaced generator method that is stepped by hand and some loops over lists of
numbers can stop one; main builds all three. In `bun test --aot`, with `vi.doMock()`, a factory that
imports its own file and `vi.importActual()`, and with a mock of a file that a package has imported
before, the answer is wrong as on main, without main's notice. A bare .cjs file loaded with import()
runs in sloppy mode when a require() that never runs also names it, as on main. With the React
Compiler in the same build, a callback that isn't memoized can be left out of the executable, and
calling it ends the program, as on main.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 3 findings from earlier reviews are still open above.

Still open from earlier reviews (3):

  • Unresolved: 2 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

…omponents

On 0e96f1b. 15 files, +188 -32: src 4 files +107 -32 . test 11 files +81 -0. Pins the same preview
build, of oven-sh/WebKit#1028's commit 5473c680. [skip size check], as in 607519a.

With the React Compiler in the same build, a callback that isn't memoized could be left out of the
executable, and calling it ended the program. Fixed.

With the React Compiler, a component beside an `export *` could stop the build. Fixed.

Two more stored programs of CommonJS and ES modules are compared with stored outputs, since `bun
run` itself does not always give the same.

Known issues: in `bun test --aot`, a test file that mocks, with its original, a module of its own
running graph, beside another such mock in a helper, can end with "never finished loading"; main
does not. With `--aot-harden=proof-checks` a method that is replaced by that of another class stops
a build, and a replaced generator method that is stepped by hand and some loops over lists of
numbers can stop one; main builds all three. In `bun test --aot`, with `vi.doMock()`, a factory that
imports its own file and `vi.importActual()`, and with a mock of a file that a package has imported
before, the answer is wrong as on main, without main's notice. A bare .cjs file loaded with import()
runs in sloppy mode when a require() that never runs also names it, as on main. A build's time
doubles with each conditional join onto one field that a file read uses; main's does not.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 3 findings from earlier reviews are still open above.

Still open from earlier reviews (3):

  • Unresolved: 2 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

On 01881b8. 12 files, +90 -8: src 2 files +57 -7 . test 10 files +33 -1. Pins the same preview
build, of oven-sh/WebKit#1028's commit 5473c680. [skip size check], as in 607519a.

With the React Compiler, a file that is imported only for code that the compiler drops did not run.
Fixed.

A stored program of how builds grow passes; its Todo line goes out.

The tests of tsconfig globs compare fewer paths under Miri, where they took hours.

Known issues: in `bun test --aot`, a test file that mocks, with its original, a module of its own
running graph, beside another such mock in a helper, can end with "never finished loading"; main
does not. With `--aot-harden=proof-checks` a method that is replaced by that of another class stops
a build, and a replaced generator method that is stepped by hand and some loops over lists of
numbers can stop one; main builds all three. In `bun test --aot`, with `vi.doMock()`, a factory that
imports its own file and `vi.importActual()`, and with a mock of a file that a package has imported
before, the answer is wrong as on main, without main's notice. A bare .cjs file loaded with import()
runs in sloppy mode when a require() that never runs also names it, as on main. A build's time
doubles with each conditional join onto one field that a file read uses; main's does not.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (3):

  • Unresolved: 2 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

const hash = (bytes: Uint8Array) => Bun.hash(bytes).toString(16);
const first = readFileSync(path), second = readFileSync(path);
const unchanged = hash(second);
console.log(first.length, first.byteOffset === 0 || first.buffer.byteLength >= first.length, hash(first) === unchanged);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): the second value printed on this line cannot be false, so the "3000001 true true" expectation proves nothing about the mapped buffer's placement. first.byteOffset === 0 || first.buffer.byteLength >= first.length holds for every typed array, since a view never extends past its buffer. Fix: assert a property that the private-copy path can actually get wrong, such as first.byteOffset === 0 alone or first.buffer.byteLength === first.length, so that a Buffer handed out by Buffer::from_private_copy with a wrong offset or length fails the test.

Why this was flagged

The test at test/bundler/compile-aot-run-time.test.ts:210-247 exercises the new private-copy path for embedded files of at least MIN_MAPPED_BYTES (src/runtime/node/node_fs.rs:526-537, src/sys/private_copy.rs:12-31). Its first printed line includes first.byteOffset === 0 || first.buffer.byteLength >= first.length (line 222). For any TypedArray, byteOffset + length <= buffer.byteLength, so the right-hand disjunct is always true and the expression is always true. The expectation "3000001 true true" at line 241 therefore cannot fail on this clause even if from_private_copy (src/jsc/array_buffer.rs:1104-1117) returned a buffer with a wrong byte_len or the JS wrapper was created with a wrong offset. REVIEW.md asks that every assertion be able to fail; this one is vacuous, and the base branch had no such test, so it adds no coverage of the invariant it names.

Verification: nit. Triggering condition: always — the sub-assertion is a tautology for every TypedArray, so it passes regardless of what the private-copy path produces. test/bundler/compile-aot-run-time.test.ts:222 prints first.byteOffset === 0 || first.buffer.byteLength >= first.length; the expectation at line 243 pins it to "3000001 true true". This violates the REVIEW.md rule "Every assertion must be able to fail".

Comment on lines +1338 to +1343
let (born, recorded) = (now.birthtim, opened.birthtim);
if born == now.mtim && (born.sec, born.nsec) < (recorded.sec, recorded.nsec) {
opened.birthtim = born;
}
let made = (opened.dev, opened.ino, opened.nlink, opened.birthtim);
if (now.dev, now.ino, now.nlink, now.birthtim) != made {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): on Linux hosts where statx is unavailable (kernel < 4.11 or a seccomp profile that returns EPERM/ENOSYS), fs.fstatSync of an embedded-file fd stops reporting the embedded path's stats after any fchmod/fchown/futimes on that fd, and silently falls back to the temp copy's dev/ino/nlink forever. The identity check at src/standalone_graph/compiled_program.rs:1342-1343 includes birthtim, but the fstat fallback in stat_descriptor (compiled_program.rs:989) gets birthtim from ctime (src/sys/PosixStat.rs:152-163), which every metadata change alters. Fix: make the descriptor identity independent of fields that the non-statx fallback derives from ctime, e.g. compare birthtim only when statx supplied it, so the embedded stat survives fchmod/futimes on every Linux host.

Why this was flagged

An --aot executable on Linux where statx fails (stop_using_statx has cleared SUPPORTS_STATX_ON_LINUX, src/sys/lib.rs:2279 and 2299) calls fs.openSync on an embedded file; open_embedded_path records the fd with graph.add_descriptor (src/runtime/node/node_fs.rs:649). add_descriptor stores stat_descriptor(fd), and with statx off stat_descriptor uses bun_sys::fstat plus PosixStat::init (compiled_program.rs:989), whose birthtim on Linux is stat_ctime (src/sys/PosixStat.rs:152-163). The program then calls fs.fchmodSync(fd, mode) or fs.futimesSync(fd, ...), which changes the temp copy's ctime. The next fs.fstatSync(fd) reaches graph.fstat; at compiled_program.rs:1342-1345 now.birthtim (new ctime) differs from opened.birthtim, so the slot is set to None and the raw stat of the unnamed temp file is returned, and every later fstat of that fd does the same. With statx available, birthtim is the real btime or a constant zero, so the record survives. The base branch always returned the temp copy's stat, so this only undercuts the new feature on those hosts; no other guard re-registers the descriptor.

Verification: On a Linux host where statx(2) fails, stat_descriptor (src/standalone_graph/compiled_program.rs:985-989) takes bun_sys::fstat(fd), whose birthtim is the ctime (src/sys/PosixStat.rs:152-163). fchmod/fchown/futimens bump ctime on the temp file, so the comparison at compiled_program.rs:1342-1344 mismatches, *slot = None, and every later fs.fstatSync(fd) returns the unnamed temp file's stats.

On 087029c. 6 files, +168 -7: scripts 1 file +1 -1 . test 5 files +167 -6. Pins the preview build
of oven-sh/WebKit#1028's commit 9ad6efd7. [skip size check], as in 607519a.

In a loop, a remainder took the integer behind a sum past 2 ** 53 in place of the rounded number:
where y + i reaches 2 ** 53 + 1, (y + i) % 3 was the remainder of 2 ** 53 + 1. Fixed in the engine;
one stored program.

With `--aot-harden=proof-checks`, a list that is read at its older length behind a change of its
storage stopped a build. Fixed in the engine; two stored programs.

The reader of Mach-O files that a test uses takes only sections of instructions for code: constants
in __TEXT read as instructions.

The scaling row of getters that a helper lays off a table is asked at 4,000 targets, where the peak
of a build of nothing no longer decides the reading.

Known issues: in `bun test --aot`, a test file that mocks, with its original, a module of its own
running graph, beside another such mock in a helper, can end with "never finished loading"; main
does not. With `--aot-harden=proof-checks` a method that is replaced by that of another class stops
a build, a replaced generator method that is stepped by hand can stop one, and so can a test of a
list's length against the length before a push() or pop(); main builds all three. In `bun test
--aot`, with `vi.doMock()`, a factory that imports its own file and `vi.importActual()`, and with a
mock of a file that a package has imported before, the answer is wrong as on main, without main's
notice. A bare .cjs file loaded with import() runs in sloppy mode when a require() that never runs
also names it, as on main. A build does not end where some eight or more branches of one if / else
chain join onto the same field, or some 24 objects' fields are joined twice each from the next
one's, and a file read uses the field; main's does.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no new issues

No new issues were found in this update; 5 findings from earlier reviews are still open above.

Still open from earlier reviews (5):

  • Unresolved: 4 minor or pre-existing, 1 blocking on lines changed since (possibly already fixed).

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Jarred-Sumner added a commit to oven-sh/WebKit that referenced this pull request Nov 11, 2026
…tack traces as in bun run (#1028)

The JavaScriptCore side of a round of work on Bun's ahead-of-time compiler (`bun build --compile
--aot` and `bun test --aot`, both experimental). Bun's side is oven-sh/bun#48478. This follows
#1025.

565 files, +24,718 -4,414: aot/tests 90 files, +3,077 -724; the rest of aot/ 395 files, +18,764
-3,265; outside aot/ 80 files, +2,877 -425. In heap/ one condition and one comment, for compiled
images; nothing in WTF or bmalloc, and nothing outside Source/JavaScriptCore.

Executables

- Two tests of bounds in a row, such as `if (v < 3) { a = 5; continue; } if (v > 9) { b = 7;
  continue; }`, took the first's arm for both where both arms are short. Fixed.

- Stack traces and error messages are closer to `bun run`: positions of constructions, destructuring
  and `await`, names of methods under computed keys, paths relative to the folder of the build. An
  async function that awaits itself through `Promise.all` is in a trace once.

- Modules: `require()` of an ES module whose graph holds a file that cannot be loaded runs the
  CommonJS files in front of it. Errors reach every load of a graph. A file that is imported as text
  and as JSON no longer mixes up its forms. After an imported CommonJS file throws, the next one
  that an import loads is the main module, as under `bun run`.

- A collection of compiled code that has no helper threads could wait for helpers, so a program
  whose heap is exhausted, or that may not start threads, could hang. The collector's thread marks
  there itself; plain Bun is unchanged.

`bun test --aot`

- Mocks: a package module that failed at its first load, chains of mocks whose factories take their
  original, mocks of files that import each other in a cycle or that fail, `resetModules()` inside a
  running graph, mocks below originals, an import() after one whose graph failed to link.

- `--rerun-each`: in the second pass, an import() that is refused again behind an unrelated mock is
  rejected with the same error object as under `bun test`.

WebAssembly in the image

- Frames are shrink-wrapped, the bounds checking register is not saved, direct calls pass the first
  mutable i32 global in a register, functions of Go's compiler are entered behind the switch on
  their parameter, an access that a succeeded one covers needs no check, on x64 comparisons read
  memory. A trap takes no lock.

Plain Bun too

- On x64 a WebAssembly load whose low bytes alone are used returned a value at the end of the memory
  where it should trap, in the optimizing JIT. Fixed.

- In the optimizing JIT: `f32.demote_f64` of a converted 64-bit integer rounded once where it rounds
  twice, and a signaling NaN stayed signaling through `f32.demote_f64` of `f64.promote_f32`. Fixed.

- arm64 with DotProd: `i32x4.relaxed_dot_i8x16_i7x16_add_s(x, x, c)` in the optimizing JIT
  multiplied by the addend where the result took x's register. Fixed.

- The data segments of a WebAssembly module lie in one buffer, each behind a short header:
  esbuild.wasm has 98,450 of them.

- B3: a Phi that follows its own Upsilon in a block is folded; assertion builds poison stale
  Value::owner pointers (#986).

- Corrected assembler operations that nothing calls with such operands: equal registers, negative
  immediates and masks, pairs of floats at far offsets, blinded additions, shifts by 32 and more.
  Small corrections in the x64 assembler and in the register allocator.

Speed (instructions, by our own benchmark rows)

- Async code and generators: an awaited call of a compiled function continues it in place, 687 to
  202 a call; a pipeline of generators is joined, 81 to 35 an element.

- Sorting: `sort((a, b) => a - b)` from 65 numbers on goes by digits, 1,286 to 52 an element for
  20,000 integers; `sort(comparator)` of typed arrays is compiled.

- Strings: substrings, parseInt(), keys of a `Map` that are joined, one character of a string,
  split(), destructuring, TextEncoder and TextDecoder (3,811 to 1,465 a line).

- Loops: lists in fields, rows of rows, nested loops, counters of any kind, views under strides and
  masks, sums of unknown values, BigInts, remainders, method chains, throwing callees.

- Objects: comparisons of one layout, `instanceof`, `Object.hasOwn()` and `Object.keys()` of
  records, getters and setters of host and class objects, static fields, spread copies, constructor
  functions below native classes. Results of functions of node:path and node:fs have types.

Size and build

- Image tables are coded. Constants are compared and loaded in place, and element accesses under a
  constant index take fewer bytes.

- Builds that took quadratic time: chains of classes and interfaces, unions with many tests, barrels
  with many stars under mocks, nested array patterns.

- `--aot-harden=proof-checks` verifies each stage. A frame part is closed under dominance.

`Wire::schemaHash` changes, so this engine and Bun's side go together.

Known issues, right before this commit

- In `bun test --aot`, a test file that mocks, with its original, a module of its own running graph,
  beside another such mock in a helper, can end with "never finished loading".

- With `--aot-harden=proof-checks`, a method that is replaced by that of another class stops a
  build, a replaced generator method that is stepped by hand can stop one, and so can a test of a
  list's length against the length before a push() or pop().

As before

- `bun test --aot`: with `vi.doMock()`, a factory that imports its own file and `vi.importActual()`,
  and with a mock of a file that a package has imported before, the answer is wrong as before,
  without the notice that was given before.

A line of a stack trace names the source relative to the folder of the build, so it no longer
includes `import.meta.path`; that is by design.
On dfb5a20. 1 file, +1 -1: scripts 1 file +1 -1. Pins the merged commit of oven-sh/WebKit#1028,
d837f045. [skip size check], as in 607519a.

Known issues: in `bun test --aot`, a test file that mocks, with its original, a module of its own
running graph, beside another such mock in a helper, can end with "never finished loading"; main
does not. With `--aot-harden=proof-checks` a method that is replaced by that of another class stops
a build, a replaced generator method that is stepped by hand can stop one, and so can a test of a
list's length against the length before a push() or pop(); main builds all three. In `bun test
--aot`, with `vi.doMock()`, a factory that imports its own file and `vi.importActual()`, and with a
mock of a file that a package has imported before, the answer is wrong as on main, without main's
notice. A bare .cjs file loaded with import() runs in sloppy mode when a require() that never runs
also names it, as on main. A build does not end where some eight or more branches of one if / else
chain join onto the same field, or some 24 objects' fields are joined twice each from the next
one's, and a file read uses the field; main's does. In a stack trace, where the conversion of an
operand throws, or a constructor whose object is kept in a variable, the caller's line names the
start of the expression and not the operator; main names the operator.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Still open from earlier reviews (4):

  • Unresolved: 4 minor or pre-existing.

slot(async () => {
using dir = tempDir("aot-sentry", program);
const folder = String(dir);
await installSDK(folder);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional), pre-existing pattern: the new test adds a second live bun install of @ sentry/bun from the public registry, so it fails offline and on a registry hiccup. installSDK at test/aot/cli/sentry-source-map.test.ts:118 runs bun install --ignore-scripts against the real registry, and the new test at :327 calls it again in a fresh temp folder. Fix: serve the SDK from a local in-process registry or the container harness (or a vendored tarball) for both tests, so neither contacts an external host.

Why this was flagged

Both tests in test/aot/cli/sentry-source-map.test.ts build a temp folder whose package.json (line 18) depends on "@ sentry/bun": "11.9.0" and then call installSDK (line 118), which spawns bun install --ignore-scripts with no registry override, so it fetches from the public npm registry. The base branch did this once inside build(); the diff extracts it and the new test "without it, events name the sources by their paths from the folder of the build, and their frames are in app" (lines 321-342) calls installSDK a second time at line 327. REVIEW.md asks that tests never contact external network hosts or live registries. On an offline or rate-limited CI runner the expect at lines 120-123 fails for both tests, and the failure has nothing to do with the stack-trace path change the test is meant to cover. No local registry fixture or skip guard is present in the file.

Verification: nit, pre-existing pattern. The public npm registry is unreachable or flaky when the suite runs. test/aot/cli/sentry-source-map.test.ts:118-124 installSDK spawns [bunExe(), "install", "--ignore-scripts"]; neither the folder nor bunEnv overrides the registry, so the install fetches from the live registry. The new test at line 326 calls await installSDK(folder), adding a second live install to the file.

Comment on lines +204 to +205
return JSC::jsEmptyString(vm);
if (bytes.size() > String::MaxLength)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): a compiled program that decodes a very large ASCII buffer with TextDecoder now aborts the process when the string cannot be allocated, where the base throws a catchable error. tryDecodeUTF8 at src/jsc/bindings/AOTTextCoding.cpp:208 builds the result with String(byteCast(bytes)), whose StringImpl::create path uses fastMalloc and crashes on failure. The 16-bit branch of the same function already uses StringImpl::tryCreateUninitialized and returns nullptr to fall back. Fix: make every allocation in the fast path fallible, e.g. use StringImpl::tryCreateUninitialized for the ASCII branch and the small 16-bit branch too, returning nullptr so the generic decode throws its usual error.

Why this was flagged

The trigger is a TextDecoder.decode(view) call in an --aot executable where view is a Uint8Array of all-ASCII bytes and the process is near its memory limit, so the string allocation fails. At src/jsc/bindings/AOTTextCoding.cpp:208 the ASCII case returns JSC::jsString(vm, String(byteCast(bytes))); WTF::String's span constructor calls StringImpl::create, which allocates with StringImplMalloc::malloc (fastMalloc) and aborts the process when the allocation fails. On the base branch the same call reaches TextDecoder::decode_slice at src/runtime/webcore/TextDecoder.rs:339, which uses heap.try_create_latin1_string; that returns Err(memory_allocation_failed()) (src/jsc/bindgen.rs:137-143), so the program gets a catchable error instead of a crash. The large non-ASCII branch at src/jsc/bindings/AOTTextCoding.cpp:216 uses StringImpl::tryCreateUninitialized and returns nullptr to fall back, so the safeguard exists but does not cover the ASCII branch or the 256-byte 16-bit branch at src/jsc/bindings/AOTTextCoding.cpp:211.

Verification: An --aot compiled program calls TextDecoder.decode on an all-ASCII Uint8Array whose allocation fails because the process is near its memory limit. The ASCII case at src/jsc/bindings/AOTTextCoding.cpp:208 allocates with fastMalloc and CRASHes on a null result. The base path, TextDecoder::decode_slice in src/runtime/webcore/TextDecoder.rs line 339, yields a catchable memory_allocation_failed() error.

On 8452024. 2 files, +24 -1: scripts 1 file +1 -1 . test 1 file +23 -0. Pins the preview build of
oven-sh/WebKit#1030's commit 3f5b3629. [skip size check], as in 607519a.

In a loop, a sum or difference of two values tested only one of them for a boxed int32 where the
type of the other says int32: JSON's 3 beside 0.5 is boxed as a double, and `p.a + p.b[0]` was 3
where both are 3. Fixed in the engine; one stored program.

Known issues: in `bun test --aot`, a test file that mocks, with its original, a module of its own
running graph, beside another such mock in a helper, can end with "never finished loading"; main
does not. With `--aot-harden=proof-checks` a method that is replaced by that of another class stops
a build. These can stop one: a replaced generator method that is stepped by hand, a test of a list's
length against the length before a push() or pop(), a closure with two captured variables that is
called in a loop and, beside it, inside a list callback, and `row[i] += factor * b[i]` in a nest of
two loops where `factor` is undefined in one call; main builds all five. In `bun test --aot`, with
`vi.doMock()`, a factory that imports its own file and `vi.importActual()`, and with a mock of a
file that a package has imported before, the answer is wrong as on main, without main's notice. A
bare .cjs file loaded with import() runs in sloppy mode when a require() that never runs also names
it, as on main. A build does not end where some eight or more branches of one if / else chain join
onto the same field, or some 24 objects' fields are joined twice each from the next one's, and a
file read uses the field; main's does. In a stack trace, where the conversion of an operand throws,
or a constructor whose object is kept in a variable, the caller's line names the start of the
expression and not the operator; main names the operator.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Still open from earlier reviews (6):

  • Unresolved: 6 minor or pre-existing.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants