Skip to content

module loader: TranspilerJob owns its path text and builds the Path in run() - #47099

Closed
robobun wants to merge 1 commit into
robobun/225b7f1d/free-through-owner-not-shared-viewfrom
robobun/225b7f1d/transpiler-job-owns-path-text
Closed

robobun wants to merge 1 commit into
robobun/225b7f1d/free-through-owner-not-shared-viewfrom
robobun/225b7f1d/transpiler-job-owns-path-text

Conversation

@robobun

@robobun robobun commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Draft: stacked on #47098, and held until #44833 is merged (Notes).

Problem

  • TranspilerJob, the record of one async import, stores a Path<'static> view of a leaked Box<[u8]> and frees the box through Path::text: heap::take(ptr::from_ref::<[u8]>(old_path.text).cast_mut()) (src/jsc/RuntimeTranspilerStore.rs:506). A pointer made from &T may only read, so the free is undefined behaviour.
  • bun_jsc cannot run under Miri. A reduction of this shape gives Undefined Behavior: deallocation through <tag> is forbidden. No crash is known.

Fix

Background

Downsides

  • None found. Checked: 1 malloc and 1 free per import as before, 14 fewer instructions per import, TranspilerJob 376 to 336 B, the block of each VM 32,768 to 28,672 B.
Notes

Why a draft

Proof

  • test/internal/source-lints/self-receiver-reclaim.test.ts with src/ from http: HTTPResponseMetadata owns its buffers and builds its view on demand #47098 lists src/jsc/RuntimeTranspilerStore.rs:506. With this branch it lists nothing.
  • bun_jsc needs JavaScriptCore, so Miri cannot run it. A standalone reduction of the old shape (a Path view of a leaked box, freed through ptr::from_ref(text).cast_mut()) is UB under Tree Borrows and Stacked Borrows. The reduction of the new shape passes under both.

Measurements (release builds of #47098 and of this branch, x86_64 Linux)

#47098 this PR
size_of::<TranspilerJob>() 376 336
size_of::<RuntimeTranspilerStore>() 24,512 21,952
size_of::<VirtualMachine>() 30,720 28,160
mimalloc block of one VirtualMachine 32,768 28,672
malloc + free of the path text per async import 1 + 1 1 + 1
instructions, RuntimeTranspilerStore::transpile 230 225
instructions, RuntimeTranspilerStore::run_from_js_thread 350 338
instructions, drop glue of TranspilerJob 36 40
instructions, TranspilerJob::run_from_worker_thread 571 570
.text of bun-profile (size -A) 83,496,745 83,496,489
  • Sizes: size_of in a const, compiled with the flags of the release build. Block size: mi_usable_size(mi_malloc_aligned(size, 64)) in the release binary. VirtualMachine::init allocates with that size and alignment, one block for the main thread and one for each Worker.
  • Allocations: gdb records the block that transpile allocates for the text and the mi_free call that frees it, over 1,000 imports. Each block is freed one time and none is live at exit. The free moves from run_from_js_thread to the drop glue of the job.
  • Instructions: the instructions that one call executes in the address range of the function (gdb nexti). One async import: -14 in total. transpile has a few more in a call that scans past used slots.
  • test/cli/run/esm-fixture-leak-small.mjs (100,000 imports) on the release builds: 11 MB and 10 MB on http: HTTPResponseMetadata owns its buffers and builds its view on demand #47098, 12 MB and 11 MB on this branch. The limit is 100 MB.

Suites (debug ASAN build)

Not in this change

@robobun

robobun commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:06 PM PT - Oct 27th, 2026

✅ @robobun, your commit a424f27804b53e0dfeda808cd35e0a610f0143a8 passed in Build #133889! 🎉


🧪   To try this PR locally:

bunx bun-pr 47099

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

bun-47099 --bun

@robobun
robobun force-pushed the robobun/225b7f1d/free-through-owner-not-shared-view branch from 7be1dad to 9860424 Compare October 27, 2026 18:43
@robobun
robobun force-pushed the robobun/225b7f1d/transpiler-job-owns-path-text branch from 3604e54 to 21fd929 Compare October 27, 2026 18:43
@robobun

robobun commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Status

…n run()

`TranspilerJob` stored a `Path<'static>` view of a leaked `Box<[u8]>` and
freed the box through `Path::text`. A pointer made from a shared reference
may only read, so the free is undefined behaviour under Tree Borrows and
Stacked Borrows.

- `TranspilerJob` owns `path_text: Box<[u8]>`. It stores no view.
- `run()` borrows the text for the `Path` it parses under. No lifetime is
  erased: `ParseOptions.path` is a `Path<'a>`.
- The drop glue of the slot frees the text, so `reset_for_pool` only
  disables the poll ref.
- The `self-receiver-reclaim` source lint no longer allows an instance in
  this file.
@robobun
robobun force-pushed the robobun/225b7f1d/transpiler-job-owns-path-text branch from 21fd929 to a424f27 Compare October 27, 2026 19:03
@robobun

robobun commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: main has this change since #47893 (commit 9239165). TranspilerJob holds path_text: Box<[u8]>, run() builds the Path from a borrow of it, and reset_for_pool frees nothing. Each hunk of this PR in src/jsc/RuntimeTranspilerStore.rs has its equivalent on main at b9f2ec3. The lint entry for that file goes away in #47098 with its next rebase.

@robobun robobun closed this Nov 4, 2026
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