Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
by oven-shRust
Last 12 weeks · 1646 commits
5 of 6 standards met
Problem (src/runtime/cli/run_command.rs) held one , formed from the pointer returned, across , every , and , and kept using it afterwards. The JS those calls run reaches the same VM through other pointers: host functions via / / , and via (src/jsc/VirtualMachine.rs). An listener setting the exit code therefore wrote the field through a second while still held its own and read the exit code through it next. That is aliasing UB (Background); / document "keep the borrow short, do not hold it across reentrant JS calls", and itself avoids forming the around its FFI calls for this reason. The same shape drove in src/runtime/bake/production.rs (held across config evaluation and route rendering), in src/runtime/cli/test/parallel/runner.rs (held across every test file the worker runs; the comment above it names the raw pointer it aliases), (held across , then reads the exit code the listeners may have set), ( re-enters through -> while the is live), (called back from inside the test runner's global swap, which holds its own ), , and the two exports (a minted only to feed a parameter nothing reads). Nothing reported any of it: only matched for a fixed list of callback-parameter names, so , , , ... were invisible; and the functions that hand out to a singleton (, , , , ...: 62 functions in 29 files) are inventoried by nothing, since clippy's needs a reference parameter to fire. Fix / : the post-init setup moves into / , which take for the one window where that is true (nothing has run yet) and are called with a call-scoped reborrow. then holds (from , asserted to be the VM returned) and calls the methods through , one borrow per call, which is how already drives a worker's VM. The hot reloader gets the thread-local instead of a pointer cast from the reference; the cold exit helpers take . and (plus / , which only read the VM): the same shared-reference shape. , , , : statement-scoped accesses through the raw pointer, the style uses. : shared reference, per . lose the parameter they never read. Why this is correct: every converted site performs the same reads, writes and calls in the same order; the only thing removed is a borrow spanning the call that re-enters the VM. Each access now starts from the same thread-local / raw pointer the re-entering code starts from, so no exclusive borrow is live while another access happens. The struct-wide move of JS-mutated plain fields to , which documents as its direction, is separate and not claimed here. now matches the shape for any identifier operand. The callback-parameter names stay banned outright; the other 160 sites (81 files) are pinned by operand name in , exact per file, to regenerate, so a new one fails and the list only shrinks. On main's it fails for run_command.rs, production.rs, web_worker.rs, VirtualMachine.rs, runner.rs and EventLoopDelayMonitor.rs; with this diff it passes. New + : every whose return type contains (bare or inside //a tuple, multi-line signatures included; the scanner has a fixture self-test), 62 in 29 files, exact per file. Seeded from main; this PR removes none, so its proof is leave-one-out: deleting the VirtualMachine.rs entry fails with / . There is no runtime test for this: the aliasing has no symptom current compilers produce. The reborrow lint is the fail-before / pass-after test for the conversions. Verified: (263 pass); (1111 pass; the 11 failures are environmental, see details); targeted runs of heap-prof (self-kill profile write), perf_hooks (), test isolation, , bake production (9/9 once the 5s default timeout is raised, the ASAN build here needs 6-11s per test), dev-and-prod, hot, watch, worker_threads, web workers and the process tests; by hand, a worker whose listener sets reports 7, a main-thread listener setting 5 exits 5, and a rejected entry plus a listener setting 9 exits 9 (same as node). Left as they are (inventoried), for separate PRs: (test_command.rs) holds for the whole run and threads it through / / ; (runner.rs:57) feeds the same signatures. Its own operand is a call expression, so it is outside even the generalized lint. / (post-init patching, no FFI), in production.rs (two sites; #38241 rewrites that exit path and keeps the count at two, so the lint passes in either merge order), run_command's x2 () and (opaque ZST handle), and the macro's template, which is the documented design there. The receivers of / / ..., taking from C++, and plus the sql shims. The http entries of the accessor inventory are being removed by #37706 / #37694 and the four buffers by #37616; whichever side lands second reruns . Background Re-entrancy: runs JS; JS calls host functions; host functions get the VM from a thread-local raw pointer () or are handed by C++ (). Any borrow a driver loop holds across therefore coexists with the borrows made inside it. Aliasing rule: a asserts that nothing else touches until the reference's last use. Accessing the memory some other way and then using the again is UB under both Stacked Borrows and Tree Borrows (what Miri checks), and rustc marks parameters , which lets LLVM keep values read through them across calls. A is fine for state behind interior mutability (), and a formed for a single call is no longer in use by the time the callee runs JS. returns a rebuilt from the thread-local pointer (not cast from ) and is documented for exactly this one-call use; is the same pointer, for callees that store one. Source lints: holds grep-style tests over that GitHub Actions runs on every PR without a build. Several (, ) pin the current population of a shape in a JSON inventory that a change must regenerate, so the population can only shrink; both lints here follow that pattern. Local failures in bun bd test test/cli/run/, all environmental / : in the container. uid/gid case: fails identically with the released binary (); the grandchild cannot be spawned with credentials here. : the unresolvable step takes ~18s per spawn on this ASAN build (3.4s total on the release binary against a 5s budget); resolvable runs take 0.4s. leak tests: 20-60s timeouts; the children finish later and print RSS deltas of 7-149 MB, under the 320-400 MB ASAN thresholds. "lazily scales" / "work-stealing": a trivial test file takes 346ms on this build (7ms on release) against the tests' 250ms scale-up budget; both pass on the release binary. "process": expects , unset in the container. "message flood": a worker takes seconds to boot on this build, the test waits 30ms. Every test that timed out at the default 5s (bake production, worker terminate races, inside , the onBeforeExit worker case) passes with .
Problem (src/bun_core/lib.rs) is a safe function whose soundness depended on its caller: it passed 's spare capacity straight to , which writes the whole conversion without being told an output length, and never looked at how much spare capacity there was. Its doc comment asked callers to reserve first. Called on a it writes through the Vec's dangling pointer; called on a Vec with too little spare room it writes past the allocation. Both happened during the Rust port and were patched at the call sites: the comments removed from src/io/PipeWriter.rs and src/bun_core/string/immutable.rs describe the segfault. Every caller on main reserves correctly today, so this is an unsound safe API rather than a crash reachable from JS; it also left five call sites (shell_body.rs, vec_ext.rs, immutable.rs, and two wrappers in lib.rs) each carrying their own copy of the sizing. in the same file has the same shape: once ). The length cannot be verified without the scan the parameter exists to skip (TextEncoder would otherwise scan large strings twice), so the requirement belongs in the signature. Its two callers get SAFETY comments; behavior is unchanged. Tests: test/js/bun/util/convertUTF16ToUTF8Append.test.ts drives the conversion through a new probe in (same shape as the existing ), because the starting spare capacity is not something any JS-reachable caller lets a test choose. For ASCII, 2 and 3 byte, surrogate pair and lone surrogate inputs, short and long enough to reach the vector kernels, it appends to a fresh Vec and after existing bytes with 0, one short, exact, one over, worst case and above worst case spare bytes, through both entry points, and checks the bytes plus that the Vec grew exactly when the encoded text did not fit. Verified: : 56 pass. With only the reserve removed from and the probe kept, the fresh-Vec cases die with a SEGV writing to address 0x1 inside , and the cases with existing bytes trip the assertion after simdutf wrote past the allocation (trace under details). On the released bun the probe does not exist and every case fails. on test/js/web/encoding/{text-encoder.test.js,text-encoder-stream.test.ts,encode-bad-chunks.test.ts}, test/js/bun/util/{toUTF16Alloc,arraybuffersink,filesink}.test.ts, test/js/bun/shell/{lex,parse}.test.ts and bunshell.test.ts , and test/internal/source-lints: all pass. on bun_core, bun_collections, bun_jsc: clean. rustfmt and prettier: clean. Background simdutf's takes no output length. The caller promises holds bytes (a separate SIMD pass over the input; it counts 1 to 3 bytes per BMP code unit and 2 per surrogate code unit). The function returns either SUCCESS with the number of bytes written, or an error with the input position of the first unpaired surrogate, having written the conversion of everything before it. ( here) is the same count but charging 3 bytes per unpaired surrogate, i.e. the length of the output with U+FFFD substituted. It is at least the plain length, and at most 3 bytes per code unit. gives the Vec's uninitialized spare capacity as and then advances the length by the byte count returns. Debug builds assert that count fits in the spare capacity, which is what the prefixed test cases tripped without the fix. A Rust is one whose caller must uphold a condition the compiler cannot check; the section states it and each call site records why it holds. turns an into Bun's out-of-memory crash, which is how the infallible entry point is built on the fallible one. Failure without the sizing (reserve removed, probe kept), debug ASAN build Running only the "after existing bytes" cases: ``` panic: assertion failed: n
Problem fails as soon as a git or tarball dependency's own package.json declares a dependency on a directory (bun 1.4.0 and main, no network needed): No or is written, exit code 1. The same package published to a registry installs fine (, "transitive file dependencies"). Cause: (, arm) joins every folder path onto the directory of the package.json being parsed and stores it relative to the top-level dir. Git and tarball packages are parsed with that function too (, ), but their package.json lives in the install cache, so the stored path points into the cache. The resolver then refuses it as a transitive target outside the declaring package (, ), which is the error above. If the cache sits inside the project ( in bunfig), the path passes that check instead: gets , the install reports success, and the installer, looking for that path under the installed package, links nothing. Registry packages go through , which stores the path exactly as declared, i.e. relative to the package, and that is the form the stub package in and the hoisted installer (, transitive folder branch: install from the declaring package's own directory) are built for. Fix gets an flag, set by next to the existing / . The arm in now only rebases the path when one of the three is set, i.e. for a manifest that lives in the project (root, workspace member, local folder). parses (git, tarball, and the offline auto-install's cached npm manifest in ) fall through to the arm and keep the path as declared. The guard on the match arm is the fix; the rest is the flag and keeping the two existing comments about the two path bases accurate. Why this is correct: a folder path declared by a package that is copied out of the cache is only meaningful relative to wherever that package gets installed, which is exactly what already records for registry packages and what the resolver and installer consume. A dependency and the same tarball published to a registry are the same artifact, so they now resolve to the same lockfile rows and the same layout. Rebasing onto the project dir is only defined for manifests that are in the project, and those three callers keep the old behavior byte for byte (#33159's installer branch for local declarers depends on it). Containment is unchanged: the stub path still goes through at resolve time and the installer's checks at install time, so declared in a tarball is still rejected (and is now reported with the declared path instead of a cache path). A declared folder missing from the archive is tolerated the same way it is for registry packages. compares dependencies by their literal, so the stored form does not feed the lockfile diff, and already loads these rows as declared, so a lockfile written after this change reloads to the same packages. Rows already in a lockfile are never re-resolved: a lockfile produced by the silent variant above keeps its cache-path row until that dependency is resolved again. #38850 (load-time rebase of rows declared by root / workspace / folder packages) skips every other declarer, so it agrees with the form this change produces. Verified with : a package declaring , and a folder absent from the package, installed as a tarball path, a tarball URL and a dependency. Each asserts the stub rows, that only the two existing folders are linked into the package's own , and that of the package reaches the nested folder at runtime; the tarball variants repeat that from . A fourth test pins that declared in a tarball still fails to resolve and links nothing. All four fail on the released build () and pass with this change. in that file gained a option for the git case. Also run: the rest of , and (the #31417 / #32452 / #33159 transitive tests), , ; no changes. Out of scope, each tracked separately, none changed by this PR: dependencies on a are , never touched this arm, and are resolved against the project root for every non-workspace declarer with no containment check (); identical before and after. The isolated linker resolves every transitive folder stub against the project dir, so it fails on a registry package declaring a folder today; git and tarball packages now behave identically to registry packages there. Folder rows created by the pnpm / package-lock importers under a registry or tarball declarer are project-relative and are silently skipped by the hoisted installer; the importers do not go through . #38814 (folder dependencies declared by local packages) and #33106 (escape check for local declarers) change the other branch of the same resolver arm and do not overlap with this change. Background A dependency on a directory is a dependency and resolves to a whose payload is a path string. There are two bases for that string: manifests in the project (root, workspace members, local packages) store it relative to the top-level dir, which the resolver joins onto the cwd; everything installed from the cache stores it as declared, relative to the declaring package. For a declarer that is not the root or a workspace, the resolver does not read the target's package.json (it may not exist on disk until the declarer is installed). It records a stub package with just a name and the folder path (the rows), after checking the path does not leave the declaring package. The hoisted installer later symlinks that folder, file by file, into the declaring package's own ; folder packages are never hoisted. () is the option set passed to : which dependency sections to read plus which kind of manifest this is. , , are the project manifests; is used for everything extracted into the cache (git, tarballs, cached npm packages) and for registry manifests in .
Repository: oven-sh/bun. Description: Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one Stars: 95407, Forks: 4963. Primary language: Rust. Languages: Rust (65.2%), C++ (20.8%), TypeScript (8.8%), C (2.7%), JavaScript (1.7%). Homepage: https://bun.com Topics: bun, bundler, javascript, javascriptcore, jsx, nodejs, npm, react, rust, rust-lang, transpiler, typescript. Latest release: bun-v1.3.14 (3mo ago). Open PRs: 100, open issues: 8105. Last activity: just now. Community health: 100%. Top contributors: Jarred-Sumner, robobun, dylan-conway, nektro, cirospaciari, paperclover, Electroid, alii, colinhacks, pfgithub and others.