Ohos aarch64 exec fix - #9
Conversation
|
Hi! I'm the It looks like you correctly set up a CI job that uses the autofix.ci GitHub Action, but the autofix.ci GitHub App has not been installed for this repository. This means that autofix.ci unfortunately does not have the permissions to fix this pull request. If you are the repository owner, please install the app and then restart the CI workflow! 😃 |
springmin
left a comment
There was a problem hiding this comment.
- Extract duplicated $PWD fallback into a shared helper
Location: src/runtime/cli/filter_run.rs:113-119, run_command.rs:278-284, run_command.rs:2195-2201
The same cwd=="/" ? HOME : cwd fallback appears 3 times. Extract into a shared helper:
#[cfg(target_env = "ohos")]
fn ohos_safe_pwd(cwd: &[u8]) -> &[u8] {
if cwd == b"/" || cwd.is_empty() {
bun_core::env_var::HOME::get().unwrap_or(cwd)
} else {
cwd
}
}
- Suggestion split filter_run.rs into two commits for clearer changelog and review.
Location: src/runtime/cli/filter_run.rs
The file mixes two independent fixes:
(a) Set $PWD in child env to avoid shell-init getcwd errors
(b) Mark BufferedReader fd as SOCKET|NONBLOCKING|POLLABLE to prevent on_reader_error loop
These have different root causes and should be reviewed independently.
e779379 to
cbe5de8
Compare
- Fix errno-to-exitcode negative cast in shell builtins echo/which: Use unsigned_abs() before casting to ExitCode (u16) to avoid negative errno values wrapping incorrectly. - Add OHOS getcwd workaround (run_command.rs, filter_run.rs): Set $PWD env var so bash verifies CWD via stat() instead of getcwd(), which fails on hmdfs/tmpfs. Extracted into unified ohos_set_pwd() helper to avoid duplication across 3 call sites. - Add ESPIPE to preadv2/pwritev2 RWF_NOWAIT fallback (sys/lib.rs): Pipe/FIFO fds return ESPIPE on positional I/O; disable RWF and fall back to plain read()/write(). - Fix is_executable_file directory false-positive (c-bindings.cpp): access(X_OK) succeeds on directories (x bit = traversal), so add stat() + S_ISREG check to verify regular file. - Mark BufferedReader as nonblocking+socket in filter_run.rs: Aligns with Bun.spawn (SubprocessPipeReader) read strategy; fixes OHOS infinite loop in on_reader_error.
cbe5de8 to
dc4d198
Compare
…sweep (oven-sh#32729) ### Crash ``` ASSERTION FAILED: vm().currentThreadIsHoldingAPILock() => vm().heap.mutatorState() != MutatorState::Sweeping vendor/WebKit/Source/JavaScriptCore/runtime/JSCell.cpp(179) : bool JSC::JSCell::validateIsNotSweeping() const ``` Backtrace (from a release-asan build with asserts): ``` #3 JSC::JSCell::validateIsNotSweeping() #4 JSC::JSCell::classInfo() const #5 WTF::uncheckedDowncast<WebCore::JSResumableFetchSink>(JSValue const&) #6 ResumableFetchSinkPrototype__ondrainSetCachedValue #7 bun_runtime::webcore::fetch::fetch_tasklet::FetchTasklet::ignore_remaining_response_body #8 JSC::WeakBlock::sweep() <- inside GC sweep (Weak finalizer) #9 JSC::WeakSet::sweep() #10 JSC::PreciseAllocation::sweep() #12 JSC::Heap::finalize() oven-sh#21 JSC::LocalAllocator::allocateSlowCase oven-sh#23 JSC::ErrorInstance::create <- ordinary allocation kicked off GC ``` Found by the syscall fault-injection fuzzer's client-side grammar scenario (fetch/node:http with abort + transient errno on the client socket). Reproduces ~4/5 under `BUN_JSC_collectContinuously=1`. ### Cause `FetchTasklet::on_response_finalize` is the `WeakRefOwner<FetchResponse>::finalize` callback and runs inside `WeakBlock::sweep` while `MutatorState == Sweeping`. When the response body is `Locked` without a pending promise or stream it calls `ignore_remaining_response_body()`, which called: - `ResumableSink::detach_js()`: writes the sink wrapper's cached `ondrain` / `oncancel` / `stream` slots via the generated `ResumableFetchSinkPrototype__*SetCachedValue` helpers. Each does `uncheckedDowncast<JSResumableFetchSink>(thisValue)`, which reaches `JSCell::classInfo()` and then issues a write barrier on the wrapper cell. - `clear_stream_handlers()`: reaches `ReadableStreamTag__tagged` -> `object->inherits<JSReadableStream>()` (guarded today, but one boolean away). Calling `classInfo()` on any cell while the mutator is sweeping is forbidden: the cell's `Structure` may already have been swept. Assert builds catch it; release builds corrupt the heap. ### Fix Thread a `from_finalizer` flag through `ignore_remaining_response_body`. When `true` (the `on_response_finalize` caller) skip `detach_js()` and `clear_stream_handlers()`; only native state is touched. The sink's JS-side detach still happens from `clear_sink()` in `FetchTasklet::deinit()`, which runs as an event-loop `ConcurrentTask` outside any sweep, so nothing leaks. The `on_stream_cancelled_callback` caller (reader `.cancel()`, runs from JS on the event loop) passes `false` and keeps the immediate detach. Also corrects the `ResumableSink::detach_js` doc comment that claimed finalizer safety. ### Verification New test at `test/js/web/fetch/fetch-response-finalizer-sweep.test.ts`: a child process under `BUN_JSC_collectContinuously=1` does 12 iterations of `fetch()` with a user-constructed `ReadableStream` body (so the sink takes the JS route with a Strong `js_this`) against a raw TCP server that sends headers + a partial chunked body and never terminates it, then drops the `Response` unconsumed and runs `Bun.gc(true)`. Without the fix (`bun bd`, src/ stashed): ``` exitCode: 134 stderr: ASSERTION FAILED: vm().currentThreadIsHoldingAPILock() => vm().heap.mutatorState() != MutatorState::Sweeping ``` With the fix: `stdout: "ok"`, `exitCode: 0`. `test/js/web/fetch/fetch-backpressure.test.ts` (exercises the `on_stream_cancelled_callback` path) passes unchanged. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…oven-sh#32742) ### What does this PR do? Fixes a use-after-free in the HTTP client's CONNECT proxy tunnel, caught by ASAN: ``` READ of size 8 at 0x61e00001fe80 thread T6 #0 Option<RefPtr<ProxyTunnel>>::as_ref #1 proxy_tunnel::on_close ProxyTunnel.rs:525 #2 SSLWrapper::trigger_close_callback uws/lib.rs:833 #3 SSLWrapper::handle_reading uws/lib.rs:1053 ... freed by thread T6 here (same stack, same `handle_reading` call): #5 AsyncHTTP::on_async_http_callback_raw AsyncHTTP.rs:819 #7 HTTPClient::send_progress_update_without_stage_check #9 proxy_tunnel::on_data ProxyTunnel.rs:350 #11 SSLWrapper::trigger_data_callback uws/lib.rs:824 #12 SSLWrapper::handle_reading uws/lib.rs:1046 ``` `SSLWrapper::handle_reading` flushes pending decrypted bytes to the data callback, then runs the close callback, guarded only by `closed_notified`: 1. The flushed data callback completes a keep-alive response through the tunnel. A fatal TLS record error sets only `fatal_error` — none of the shutdown flags — so the wrapper passed `tunnel_poolable`'s `!is_shutdown()` check and the tunnel was handed to the keep-alive pool. Nothing called `wrapper.shutdown()`, so `closed_notified` was never latched. Dispatching the final result then freed the `ThreadlocalAsyncHTTP` that embeds the `HTTPClient`. 2. The guard (`ssl.is_none() || closed_notified()`) passes. 3. `trigger_close_callback()` invokes `on_close(handlers.ctx)` with `ctx` pointing at the freed client. The pooling branch is the only terminal path that doesn't go through `close_proxy_tunnel(true)` → `wrapper.shutdown()` → `closed_notified`, which is the latch the read loop relies on. `SSLWrapper::shutdown` already special-cases the *close_notify* flavor of this for exactly that reason; the fatal-error flavor never reaches `shutdown()`. The fix is one predicate: a tunnel whose wrapper has a fatal error or pending unconsumed input/output is not poolable. That routes it through the orderly teardown that latches `closed_notified`, and the pending-I/O half closes the same hole for a tunnel pooled from a mid-loop data callback while more decrypted bytes or queued output remain. Both are also required for the pool to be correct on its own terms — a poisoned or dirty TLS session must not be handed to the next request. ### How did you verify your code works? New regression test in `test/js/bun/http/proxy.test.ts` (next to the existing close_notify sibling): an HTTPS keep-alive response through a CONNECT proxy with a corrupt TLS record appended to the same TCP burst, followed by a second request that can only complete if the HTTP client thread survived the first. Against an unfixed ASAN debug build the fixture aborts every run: ``` ==20981==ERROR: AddressSanitizer: heap-use-after-free on address 0x61e00001fe80 READ of size 8 at 0x61e00001fe80 thread T6 ... exit=134 ``` With this change it prints `4096 200 200` and exits 0 with no ASAN report. `test/js/bun/http/proxy.test.ts` (49/49), `fetch-proxy-connect-tunnel-split-envelope.test.ts`, `fetch-proxy-tls-intern-race.test.ts`, and `fetch-keepalive.test.ts` all pass.
…h#33186) ### Repro ```sh printf '{"name":"x","version":"1.0.0"}' > package.json bun pm pkg set 'contributors[0]=alice' ``` On a release build (1.4.0 and current `main`) this exits 0 and writes freed heap bytes into `package.json` as the property key: ```json { "name": "x", "version": "1.0.0", "P\x01\x00\x00\x00tors": { "\x00": "alice" } } ``` Depending on what was in the freed allocation the result is often not valid JSON at all. Any `bun pm pkg set` key path containing `[index]` hits it. Under ASAN it is a deterministic `heap-use-after-free`: ``` ERROR: AddressSanitizer: heap-use-after-free READ of size 1 #0 bun_js_printer::write_pre_quoted_string_inner src/js_printer/lib.rs:1014 #7 PmPkgCommand::save_package_json src/runtime/cli/pm_pkg_command.rs:909 freed by thread T0 here: #7 <Box<[u8]> as Drop>::drop #12 PmPkgCommand::set_value src/runtime/cli/pm_pkg_command.rs:661 previously allocated by thread T0 here: #10 <Box<[u8]> as From<&[u8]>>::from #11 PmPkgCommand::parse_key_path src/runtime/cli/pm_pkg_command.rs:583 ``` <details> <summary>full ASAN report</summary> ``` ================================================================= ==16563==ERROR: AddressSanitizer: heap-use-after-free on address 0x73423c7c0670 at pc 0x00000f583cc5 bp 0x7fff2667e950 sp 0x7fff2667e948 READ of size 1 at 0x73423c7c0670 thread T0 #0 0x00000f583cc4 in _RINvCs59Hqei94dXF_14bun_js_printer29write_pre_quoted_string_innerINtB2_16StdWriterAdapterQINtB2_6WriterNtB2_12BufferWriterEEKVNtNtB2_8Encoding4Utf8UECsgBGN0jRPILJ_11bun_bundler /workspace/bun/src/js_printer/lib.rs:1014:79 #1 0x00000ebda439 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_characters_utf8 /workspace/bun/src/js_printer/lib.rs:2641:21 #2 0x00000ebdb7a7 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_characters_e_string /workspace/bun/src/js_printer/lib.rs:4546:22 #3 0x00000ebdb238 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_string_literal_e_string /workspace/bun/src/js_printer/lib.rs:3018:18 #4 0x00000ebd2be2 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_property /workspace/bun/src/js_printer/lib.rs:4807:34 #5 0x00000ebc0166 in <bun_js_printer::__gated_printer::Printer<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>, false, false, false, true, false>>::print_expr /workspace/bun/src/js_printer/lib.rs:3962:38 #6 0x00000ee574c1 in bun_js_printer::print_json::<&mut bun_js_printer::Writer<bun_js_printer::BufferWriter>> /workspace/bun/src/js_printer/lib.rs:8071:13 #7 0x00000c05c270 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::save_package_json /workspace/bun/src/runtime/cli/pm_pkg_command.rs:909:25 #8 0x00000c060cfb in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:330:13 #9 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32 #10 0x00000bf919d0 in <bun_runtime::cli::package_manager_command::PackageManagerCommand>::exec /workspace/bun/src/runtime/cli/package_manager_command.rs:704:13 #11 0x00000c3fbb87 in bun_runtime::cli::command::exec_pm /workspace/bun/src/runtime/cli/mod.rs:1591:34 #12 0x00000c3f2b86 in bun_runtime::cli::command::start /workspace/bun/src/runtime/cli/mod.rs:1309:43 #13 0x00000bfad16c in bun_runtime::cli::cli::start /workspace/bun/src/runtime/cli/mod.rs:573:27 #14 0x00000bb3c034 in main /workspace/bun/src/bun_bin/lib.rs:230:5 #15 0x77223ccc7ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 oven-sh#16 0x77223ccc7d64 in __libc_start_main csu/../csu/libc-start.c:360:3 oven-sh#17 0x0000099d1d1d in __wrap___libc_start_main /workspace/bun/build/debug/../../src/jsc/bindings/workaround-missing-symbols.cpp:487:12 0x73423c7c0670 is located 0 bytes inside of 12-byte region [0x73423c7c0670,0x73423c7c067c) freed by thread T0 here: #0 0x000007ae192a in free crtstuff.c #1 0x00000bb3c5a7 in <std::alloc::System as core::alloc::global::GlobalAlloc>::dealloc /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:48:18 #2 0x00000bb3be9a in __rustc::__rust_dealloc /workspace/bun/src/bun_bin/lib.rs:56:15 #3 0x00001258b05f in alloc::alloc::dealloc_nonnull /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:128:14 #4 0x0000125872fe in <alloc::alloc::Global>::deallocate_impl_runtime /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:229:22 #5 0x000012586364 in <alloc::alloc::Global>::deallocate_impl /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:344:9 #6 0x00001258d79c in <alloc::alloc::Global as core::alloc::Allocator>::deallocate /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:462:23 #7 0x000012582946 in <alloc::boxed::Box<[u8]> as core::ops::drop::Drop>::drop /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1956:24 #8 0x000012572e44 in core::ptr::drop_in_place::<alloc::boxed::Box<[u8]>> /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1 #9 0x000011f8d429 in core::ptr::drop_in_place::<[alloc::boxed::Box<[u8]>]> /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1 #10 0x00000ef6b73a in <alloc::vec::Vec<alloc::boxed::Box<[u8]>> as core::ops::drop::Drop>::drop /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:4258:13 #11 0x00000ef69e64 in core::ptr::drop_in_place::<alloc::vec::Vec<alloc::boxed::Box<[u8]>>> /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:809:1 #12 0x00000c061846 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::set_value /workspace/bun/src/runtime/cli/pm_pkg_command.rs:661:5 #13 0x00000c061038 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:325:13 #14 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32 #15 0x00000bf919d0 in <bun_runtime::cli::package_manager_command::PackageManagerCommand>::exec /workspace/bun/src/runtime/cli/package_manager_command.rs:704:13 oven-sh#16 0x00000c3fbb87 in bun_runtime::cli::command::exec_pm /workspace/bun/src/runtime/cli/mod.rs:1591:34 oven-sh#17 0x00000c3f2b86 in bun_runtime::cli::command::start /workspace/bun/src/runtime/cli/mod.rs:1309:43 oven-sh#18 0x00000bfad16c in bun_runtime::cli::cli::start /workspace/bun/src/runtime/cli/mod.rs:573:27 oven-sh#19 0x00000bb3c034 in main /workspace/bun/src/bun_bin/lib.rs:230:5 oven-sh#20 0x77223ccc7ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 previously allocated by thread T0 here: #0 0x000007ae1bc8 in malloc crtstuff.c #1 0x00000bb3c520 in <std::alloc::System as core::alloc::global::GlobalAlloc>::alloc /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/alloc/unix.rs:14:22 #2 0x00000bb3be30 in __rustc::__rust_alloc /workspace/bun/src/bun_bin/lib.rs:56:15 #3 0x00001258b335 in alloc::alloc::alloc /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:101:9 #4 0x000012586b81 in <alloc::alloc::Global>::alloc_impl_runtime /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:210:73 #5 0x0000125862b6 in <alloc::alloc::Global>::alloc_impl /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:332:9 #6 0x00001258d86a in <alloc::alloc::Global as core::alloc::Allocator>::allocate /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs:449:14 #7 0x00001257dbd3 in <alloc::boxed::Box<[u8]>>::try_clone_from_ref_in /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:881:29 #8 0x00001257da49 in <alloc::boxed::Box<[u8]>>::clone_from_ref_in /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:840:15 #9 0x00001257d3f4 in <alloc::boxed::Box<[u8]>>::clone_from_ref /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:793:9 #10 0x000012581e34 in <alloc::boxed::Box<[u8]> as core::convert::From<&[u8]>>::from /root/.rustup/toolchains/nightly-2026-05-06-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed/convert.rs:77:9 #11 0x00000c05a47f in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::parse_key_path /workspace/bun/src/runtime/cli/pm_pkg_command.rs:583:37 #12 0x00000c061608 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::set_value /workspace/bun/src/runtime/cli/pm_pkg_command.rs:643:30 #13 0x00000c061038 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec_set /workspace/bun/src/runtime/cli/pm_pkg_command.rs:325:13 #14 0x00000c05d333 in <bun_runtime::cli::pm_pkg_command::PmPkgCommand>::exec /workspace/bun/src/runtime/cli/pm_pkg_command.rs:73:32 ``` </details> ### Cause `parse_key_path` returned a `Vec<Box<[u8]>>`, and `set_value` / `set_nested` inserted those boxed segments into the manifest AST by reference: `E::Object::put` constructs `EString::init(key)`, whose documented contract is that `key` is arena-owned (it records the slice, it does not copy it). The vector is a local of `set_value`, so it dropped before `exec_set` reached `save_package_json`, and the JSON printer then read the dangling keys. The non-bracket path in `set_value` did not have the bug: it borrowed its segments straight out of the argv key, which outlives the whole command. The bracket path differed only by the unnecessary boxing. ### Fix `parse_key_path` now returns `Vec<&[u8]>`. Every segment is a literal sub-slice of the input key, so nothing ever needed owning. With the boxing gone, `set_value`'s separate non-bracket branch and its `set_nested_simple` helper (which existed only to avoid the allocation) were exact duplicates of the bracket path, so they are deleted and all keys route through `parse_key_path` + `set_nested`. `set_nested_simple`'s trailing `root.put(current_key, nested)` was a no-op: `ExprData::EObject` is a `StoreRef` handle, so mutating the copy returned by `root.get()` already mutates the stored object, and the put re-stores the same handle. Dropping it with the function changes nothing (and the prior bracket path, `set_nested`, never had it). Intentionally not changed here: `set 'contributors[0]=alice'` produces `"contributors": {"0": "alice"}`, an object keyed by the digit string, rather than the array npm's `pkg set` creates, and `set 'array[]=x'` still errors with `InvalidPath` instead of appending. Both are the npm compat gap tracked in oven-sh#22035, which is separate from the memory safety of the key names and is not closed by this PR. ### Verification New test in `test/cli/install/bun-pm-pkg.test.ts` reparses the written file and asserts the exact object. Without the fix it fails on release (`SyntaxError: JSON Parse error: Invalid escape character x`) and on the ASAN debug build (the child aborts on the use-after-free). With the fix the full `bun-pm-pkg.test.ts` suite passes (74 pass, 0 fail).
…ry rewrite (oven-sh#34271) `test/js/bun/util/filesystem_router.test.ts` went red on alpine x64 in build [73276](https://buildkite.com/bun/bun/builds/73276): the `reload() while Bun.build() resolves the same directory` subprocess segfaulted in `bust_dir_cache_recursive`, inlined from `NonNull::new`. ## Cause `RealFS::entries_at` (`src/resolver/lib.rs`) replaces a cached `DirEntry` in place when the caller's resolver generation is newer than the cached listing's. The replacement at `*e_ptr = new_entry` drops the old `DirEntry`, which drops its `data: StringHashMap<*mut Entry>` and frees the hashmap's bucket allocation. The function's comment says `entries_mutex held by caller`, but that is only true on one of the five paths that reach it: `dir_info_uncached`, when entered from `dir_info_cached_miss`. The other callers (`finalize_result`, `handle_esm_resolution`, `load_index_with_extension`, `Transpiler::run_env_loader`) all reach `entries_at` after `dir_info_cached_maybe_log` has already returned and released both `RESOLVER_MUTEX` and `entries_mutex`. `FileSystemRouter::reload()` and `RouteLoader::load` iterate the same `DirEntry.data` map under `entries_mutex` (the snapshot pattern oven-sh#33056 introduced for exactly this kind of concurrent rewrite). With `entries_at`'s rewrite unsynchronized, a `Bun.build()` on the bundler thread can drop the map while `reload()` on the JS thread is mid-iteration. The generation mismatch is what makes `entries_at` enter its rewrite branch, so the window only opens once the bundle thread has processed at least one batch (it bumps its own generation after every queue drain); every subsequent `Bun.build()` then re-reads any directory that `reload()` just refreshed to generation 0. ASAN catches it as a heap-use-after-free with the two sides of the race laid out exactly: ``` READ of size 16 (thread T0): #6 HashMap::values #7 StringHashMap<*mut Entry>::values src/collections/array_hash_map.rs:1864 #8 FileSystemRouter::bust_dir_cache_recursive src/runtime/api/filesystem_router.rs:395 #9 FileSystemRouter::bust_dir_cache src/runtime/api/filesystem_router.rs:451 #10 FileSystemRouter::reload src/runtime/api/filesystem_router.rs:476 freed by thread T11 (Bundler): #11 drop_in_place<bun_resolver::fs_full::DirEntry> #12 bun_resolver::fs::RealFS::entries_at src/resolver/lib.rs:1639 #13 DirInfo::get_entries_ref src/resolver/dir_info.rs:266 #14 Resolver::finalize_result src/resolver/resolver.rs:1714 #15 Resolver::resolve_and_auto_install src/resolver/resolver.rs:1485 ... oven-sh#23 BundleThread::generate_in_new_thread src/bundler/BundleThread.rs:276 previously allocated by thread T0: oven-sh#17 HashMap::reserve oven-sh#18 Resolver::dir_info_cached_miss src/resolver/resolver.rs:4591 oven-sh#19 Resolver::dir_info_cached_maybe_log src/resolver/resolver.rs:4201 oven-sh#20 Resolver::read_dir_info src/resolver/resolver.rs:4118 oven-sh#21 FileSystemRouter::reload src/runtime/api/filesystem_router.rs:492 ``` (The use side is sometimes `RouteLoader::load` at `src/router/lib.rs:816` instead; same map, same lock.) This has been the shape of `entries_at` since the Rust port; oven-sh#33056 narrowed the race by snapshotting under the lock but assumed the rewrite side already held it. ## Fix `entries_at` now takes `entries_mutex` itself, matching `read_directory_with_iterator` which already does. The one call path that reaches it with the lock already held (`dir_info_cached_miss` -> `dir_info_uncached` -> `parent_.get_entries_ref`) routes through a new `entries_at_locked` / `get_entries_ref_locked` pair so the non-recursive mutex is not re-entered. That path is the only one that passes a non-`None` parent to `dir_info_uncached`; the other caller (`dir_info_for_resolution`) passes `None`, so the parent branch containing the accessor never runs there. ## Test The existing concurrency test now awaits one `Bun.build()` first, so the bundle thread's generation is already past zero when the concurrent rounds start, and then runs forty reload/build rounds instead of one. That is the shape that reaches the stale-generation rewrite at all; the original single-round fixture usually completes with every build still on generation 0. The race is scheduling-dependent. Pinning the fixture to a single core reproduces the ASAN use-after-free on roughly 3 in 10 runs against an unpatched debug build and 0 in 15 with this change; with all 16 cores available the unpatched build reproduces at roughly 1 in 30. The assertions are otherwise the same as before, so the test continues to cover the behavior oven-sh#33056 added. Also ran the full `filesystem_router.test.ts`, `test/bundler/bun-build-api.test.ts` (including the thousands-of-builds test that exercises the generation path heavily), `test/js/bun/resolve/resolve.test.ts`, `test/cli/hot/hot.test.ts`, `test/cli/watch/watch.test.ts`, `test/bake/framework-router.test.ts`, and `bun run rust:check-all` (10/10 targets). <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/filesystem_router.test.ts <!-- robobun:evidence:end -->
…e drain (oven-sh#34278) ## Problem `test/js/node/test/parallel/test-worker-stdio-flush.js` went red on the `debian 13 x64-asan` lane of [build 73374](https://buildkite.com/bun/bun/builds/73374) with: ``` ==18202==ERROR: LeakSanitizer: detected memory leaks Direct leak of 32 byte(s) in 1 object(s) allocated from: #9 ConcurrentTask::new src/event_loop/ConcurrentTask.rs:305 #10 ConcurrentTask::create src/event_loop/ConcurrentTask.rs:319 #12 bun_jsc::virtual_machine_exports::queue_task_concurrently src/jsc/virtual_machine_exports.rs:140 #13 ScriptExecutionContext::postTaskConcurrently src/jsc/bindings/ScriptExecutionContext.cpp:266 #14 ScriptExecutionContext::postTaskTo src/jsc/bindings/ScriptExecutionContext.cpp:125 #15 MessagePortPipe::scheduleDrain src/jsc/bindings/webcore/MessagePortPipe.cpp:74 oven-sh#16 MessagePort::postMessage src/jsc/bindings/webcore/MessagePort.cpp:143 ``` The leaked allocation is a `ConcurrentTask` (and the `EventLoopTask` it wraps) left in an exiting worker's `concurrent_tasks` queue after the queue has been drained for the last time. ## Cause `WebWorker::shutdown()` runs `process.on('exit')` handlers, then drains the worker's concurrent queue via `release_queued_tasks_for_shutdown()`, then enters `WebWorker__teardownJSCVM` which (first thing) calls `ctx->markTerminating()`. `ScriptExecutionContext::postTaskTo` already refuses to enqueue onto a terminating context, but between the drain and the flag flip there is a short window where a cross-thread poster still sees `isTerminating() == false` and enqueues. In the failing test the worker writes to `process.stdout` inside its `exit` handler. The parent's captured-stdout reader acks each chunk with `port.postMessage(true)` (`src/js/node/worker_threads.ts` `makePortReadable._read`), which routes through `MessagePortPipe::scheduleDrain` to `postTaskTo(workerCtxId, ...)`. When the ack lands in that window it is pushed onto the worker's `concurrent_tasks`; nothing drains it again, and the worker's VM box is `dealloc`'d raw, so LSan reports the `ConcurrentTask` as a direct leak. The window is a few assignments plus one FFI call wide, so it hits probabilistically; the `release-asan` build is fast enough to line up occasionally, debug essentially never. The ordering was introduced in oven-sh#31216; oven-sh#29917 described the same gap ("a task posted between this drain and `removeFromContextsMap()` inside `teardownJSCVM` still leaks") but left it open. ## Fix - `ScriptExecutionContext::markTerminating()` now takes `allScriptExecutionContextsMapLock`, the same lock `postTaskTo` holds across its `isTerminating()` check and `postTaskConcurrently()` enqueue. That makes the flag flip a proper fence against concurrent posters: any `postTaskTo` critical section either runs entirely before `markTerminating()` (its task is visible to the subsequent drain) or entirely after (it observes `true` and drops). - `WebWorker::shutdown()` calls the new `extern "C" ScriptExecutionContext__markTerminating` immediately before `release_queued_tasks_for_shutdown()`, closing the window. The later `markTerminating()` inside `WebWorker__teardownJSCVM` is now redundant but harmless. No behaviour change for `process.on('exit')` itself: that runs before the new call, so a parent ack posted while the handler is running is still enqueued and then freed by the drain (never executed, same as before). Only posts that would have landed after the drain are now dropped instead of leaked. ## Verification The gap is too narrow to reproduce unassisted against a debug build: 200 iterations of the Node test with the CI LSan env, and 150 worker shutdowns with 64 Atomics-synchronized MessagePorts each, all pass on an unpatched `bun bd`. Widening the gap with a temporary `std::thread::sleep(5ms)` between `release_queued_tasks_for_shutdown()` and `WebWorker__teardownJSCVM` makes it deterministic: | build | `test-worker-stdio-flush.js` under LSan | 200-port Atomics-synchronized probe | | --- | --- | --- | | unpatched + 5 ms sleep | 5/5 leak (`32 byte(s) ConcurrentTask`) | 5/5 leak | | this PR + 5 ms sleep | 10/10 clean | 5/5 clean | | this PR (no sleep) | 50/50 clean | clean | `test/js/node/worker_threads/worker-shutdown-post-leak.test.ts` runs the worker-stdio-on-exit scenario under `detect_leaks=1` as an ASAN-lane guard (in a fresh file so it actually runs; `worker_destruction.test.ts` is ASAN-quarantined via `test/expectations.txt`). The race is not observable on the debug gate without `src/` instrumentation, so the fail-before half will not fire there; `test-worker-stdio-flush.js` on the release-asan lane remains the primary signal. Related: oven-sh#31216 (introduced the ordering), oven-sh#29917 (described but left the remaining window). <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/worker_threads/worker-shutdown-post-leak.test.ts <!-- robobun:evidence:end -->
…ed (oven-sh#36247) ## What `test/js/bun/http/bun-serve-html.test.ts` segfaults on `windows-aarch64` after oven-sh#36175 landed (builds 84162, 84194; one earlier sighting in 83933): ``` panic(main thread): Segmentation fault at address 0x48 Features: ... dev_server(14) ... ``` Symbolicated in oven-sh#36214 as `AsyncFSTask<Access>::run_from_js_thread` with `self = null`, i.e. a zeroed `ConcurrentTask` was dispatched. ## Cause `DevServer.watcher_atomics.events[*].concurrent_task` is the intrusive MPSC node the watcher thread links into `EventLoop.concurrent_tasks` when it submits a hot-reload event. It was an inline field of `DevServer`, so `server.stop()` → `drop(Box<DevServer>)` freed it while it was still linked. The next `tick_concurrent` then read `.next`/`.task`/`.auto_delete` from freed memory. ASAN on Linux confirms: ``` heap-use-after-free: ConcurrentTask::get_next (unbounded_queue.rs) ← BatchIterator::next ← EventLoop::tick_concurrent_with_count freed by: Box<DevServer>::drop ← NewServer::deinit_if_we_can ← NewServer::stop ← dispose_from_js (using server) ``` On release builds the freed block reads back as zeros, so the copied `Task` is `{tag: 0, ptr: null}`; tag 0 is `task_tag::Access`, whose `run_from_js_thread` loads `self.result` at offset `0x48`. The bug is latent and platform-agnostic. oven-sh#36175 exposed it because the CI runner now spawns the napi addon prebuild in the background while serial tests run; that writes under the watched project root, so the `jsx-runtime` DevServers in this test file now reliably receive a hot-reload event between the last `await fetch` and `using server` disposal. ## Fix `watcher_atomics` is now a `NonNull<WatcherAtomics>` owned via `bun_core::heap::into_raw`, so the allocation can outlive `DevServer` and every queued pointer keeps allocation-root provenance. `watcher_acquire_event`, `watcher_release_and_submit_event` and `recycle_event_from_dev_server` take `*mut Self` and derive the returned `*mut HotReloadEvent` (and the linked `concurrent_task` node) from that root pointer via raw place projections rather than from a `&mut WatcherAtomics` reborrow. `Drop for DevServer` reads `next_event` after `Watcher::shutdown` has serialised out the watcher thread (which guarantees it is stable): - `DONE`: nothing is queued; clear and `heap::destroy` as before. - otherwise: a `concurrent_task` is still linked (or its `Task` is already in the drain FIFO). Null `owner` on every event and leave the allocation alive. `HotReloadEvent::run` checks `owner.is_null()` first; when set it reclaims the allocation via the new `atomics` backref and returns without touching the dead `DevServer`. The `# Safety` contracts on `run` and the `BakeHotReloadEvent` dispatch arm are updated to describe the null-owner case. ## Test `test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts` creates a development server, bundles once so `app.js` is watched, synchronously rewrites `app.js`, spins briefly without yielding so the watcher thread can enqueue, disposes the server, then yields. Ten iterations. In a separate file because the React-bundling cases in `bun-serve-html.test.ts` already exceed the default per-test timeout under a debug+ASAN build on `main`. <details><summary>fail-before (debug+ASAN, src/ at main)</summary> ``` ==25521==ERROR: AddressSanitizer: heap-use-after-free on address 0x79315e4743e8 READ of size 8 at 0x79315e4743e8 thread T0 #2 <ConcurrentTask as Node>::get_next unbounded_queue.rs:82 #3 BatchIterator<ConcurrentTask>::next unbounded_queue.rs:135 #4 EventLoop::tick_concurrent_with_count event_loop.rs:507 0x79315e4743e8 is located 488 bytes inside of 16512-byte region freed by thread T0 here: #9 Box<DevServer>::drop #12 NewServer<false,true>::deinit_if_we_can mod.rs:1770 #13 NewServer<false,true>::stop mod.rs:1665 #14 NewServer<false,true>::dispose_from_js server_body.rs:2584 ``` </details> Passes with the fix in ~2.4s under debug+ASAN (also on a local `windows-aarch64` debug build, where the original `bun-serve-html.test.ts` is now 19/19); `test/bake/deinitialization.test.ts` still green. Supersedes the producer half of oven-sh#36214 (which adds a sentinel for the same zeroed-task symptom). <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 4 · 6 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 failed, 2 skipped $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-html.test.ts test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts bun test v1.4.0 (5f6622f) test/js/bun/http/bun-serve-html.test.ts: waitForServer /tmp/html-css-js_ObkyZk { "/": "/tmp/html-css-js_ObkyZk/index.html", "/dashboard": "/tmp/html-css-js_ObkyZk/dashboard.html", } [0.12ms] bundle index.html 1.09 KB [0.05ms] bundle dashboard.html 1.27 KB (pass) serve html [630.67ms] waitForServer /tmp/bun-serve-html-txt_5C6B7a { "/": "/tmp/bun-serve-html-txt_5C6B7a/index.html", } [0.15ms] bundle index.html 0.40 KB HASH efbnbska (pass) serve plugins > basic plugin [556.20ms] waitForServer /tmp/html-css-js-failing-plugin_OPRhwb { "/": "/tmp/html-css-js-failing-plugin_OPRhwb/index.html", } error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_OPRhwb/styles.css:0 error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_OPRhwb/styles.css:0 (pass) serve plugins > serve html with failing plugin [491.35ms] waitForServer /tmp/html-css-js-empty-plugins_biqnN6 { "/": "/tmp/htm ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (96ff7ec) test/js/bun/http/bun-serve-html.test.ts: waitForServer /tmp/html-css-js_ZmgkBG { "/": "/tmp/html-css-js_ZmgkBG/index.html", "/dashboard": "/tmp/html-css-js_ZmgkBG/dashboard.html", } [0.00ms] bundle index.html 1.09 KB [0.00ms] bundle dashboard.html 1.27 KB (pass) serve html [25.17ms] waitForServer /tmp/bun-serve-html-txt_uWNogT { "/": "/tmp/bun-serve-html-txt_uWNogT/index.html", } [0.00ms] bundle index.html 0.40 KB HASH efbnbska (pass) serve plugins > basic plugin [17.25ms] waitForServer /tmp/html-css-js-failing-plugin_Kd1a8p { "/": "/tmp/html-css-js-failing-plugin_Kd1a8p/index.html", } error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_Kd1a8p/styles.css:0 error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_Kd1a8p/styles.css:0 (pass) serve plugins > serve html with failing plugin [16.33ms] waitForServer /tmp/html-css-js-empty-plugins_Ecz7qJ { "/": "/tmp/html-css-js-empty-plugins_Ecz7qJ/index.html", } [0.00ms] bundle index.html 0.71 KB (pass) serve plugins > empty plugin array [13.23ms] Waiting for server waitForServer /tmp/html-css-js-concurrent-plugins_l7wQg5 { "/": "/tmp/ ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: 2 skipped $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/http/bun-serve-html.test.ts test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts bun test v1.4.0 (5f6622f) test/js/bun/http/bun-serve-html.test.ts: waitForServer /tmp/html-css-js_CpXhx4 { "/": "/tmp/html-css-js_CpXhx4/index.html", "/dashboard": "/tmp/html-css-js_CpXhx4/dashboard.html", } [0.09ms] bundle index.html 1.09 KB [0.05ms] bundle dashboard.html 1.27 KB (pass) serve html [588.64ms] waitForServer /tmp/bun-serve-html-txt_rjZL4e { "/": "/tmp/bun-serve-html-txt_rjZL4e/index.html", } [0.15ms] bundle index.html 0.40 KB HASH efbnbska (pass) serve plugins > basic plugin [552.11ms] waitForServer /tmp/html-css-js-failing-plugin_D8NJ2O { "/": "/tmp/html-css-js-failing-plugin_D8NJ2O/index.html", } error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_D8NJ2O/styles.css:0 error: Plugin failed intentionally at /tmp/html-css-js-failing-plugin_D8NJ2O/styles.css:0 (pass) serve plugins > serve html with failing plugin [505.55ms] waitForServer /tmp/html-css-js-empty-plugins_vK0DVI { "/": "/tmp/htm ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 673ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/7] gen bake.{client,server,error}.js -> bake.client.js, bake.server.js, bake.error.js [2/7] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited [2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/runtime/bake/DevServer.rs | 63 +++- src/runtime/bake/dev_server/lifecycle.rs | 12 +- src/runtime/bake/dev_server/mod.rs | 401 +++++++++++---------- src/runtime/dispatch.rs | 10 +- .../http/bun-serve-html-hot-reload-drop.test.ts | 82 +++++ test/js/bun/http/bun-serve-html.test.ts | 10 +- 6 files changed, 374 insertions(+), 204 deletions(-) ``` </details> **gate history** · 3 passed · 2 rejected · iteration 4 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/runtime/bake/DevServer.rs 10 13 0 src/runtime/bake/dev_server/lifecycle.rs 5 9 0 src/runtime/bake/dev_server/mod.rs 13 12 0 src/runtime/dispatch.rs 3 2 0 test/js/bun/http/bun-serve-html-hot-reload-drop.test.ts 1 5 0 test/js/bun/http/bun-serve-html.test.ts 6 9 0 ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
…rier (oven-sh#36337) `JSNativeStreamSourceAdapter::m_controller` was a `JSC::Weak<JSReadableStreamDefaultController>`. When the native pull promise is rejected (socket fault on a fetch body) the adapter is queued as the `onNativePullRejected` reaction context, which roots the **adapter** but not the **controller**: the adapter's only edge to it was the `Weak`. `FetchTasklet` releases both native `Strong<>`s to the body stream before that microtask drains, so a GC in between can leave the entire consumer graph (`controller -> stream -> reader -> pipe op -> destination -> writer -> readyPromise`) white. The subsequent error cascade then enqueues the pipe's writes-drained shutdown deferral against a corpse `op`, and `performPipeShutdownAction(AbortDestination)` dereferences a swept `readyPromise`: ``` ASSERTION FAILED: result JSObject.h(583) JSGlobalObject *JSC::JSObject::realm() const #5 JSC::JSObject::realm() #6 JSC::JSPromise::rejectPromise #7 JSC::JSPromise::reject #8 Bun::WebStreams::writableStreamDefaultWriterEnsureReadyPromiseRejected #9 Bun::WebStreams::writableStreamStartErroring #10 Bun::WebStreams::writableStreamAbort #11 WebCore::performPipeShutdownAction (AbortDestination) #12 WebCore::JSStreamPipeToOperation::onWritesFinishedForShutdown ``` On builds without the assert the same path is a silent write into freed/reused promise memory. ## Fix Hold `m_controller` as a visited internal field so a queued adapter roots the controller directly. The edge is cleared on every terminal path (`nativeSourcePullRejected`, `nativeSourceCallClose`, `nativeSourceCancel`); `controller->algorithmContext` is cleared by `readableStreamDefaultControllerClearAlgorithms`, so the abandoned case is an ordinary intra-heap cycle mark-sweep collects. `NewSource::this_jsvalue` is only `Strong` during FileReader I/O, where pinning the consumer graph is the correct behavior anyway. With the `Weak` gone the adapter no longer needs a destructor, so it is now a `JSInternalFieldObjectImpl<5>`: the five JSValue members (handle, pendingView, closer, drainValue, controller) are internal fields visited by the base class, with typed accessors at call sites. The scalar members (chunkSize, flag bitfield, text-decode state) stay as plain members. ## Verification `native-source-onclose-leak.test.ts` (the partial-read + `releaseLock` abandonment tests for Blob/fetch/File sources) continues to pass, confirming the cycle does not pin. `streams.test.js`, `pipeTo-signal-leak.test.ts`, `compression.test.ts`, `blob.test.ts` all pass. The crash itself is 0/1800 standalone; it reproduces ~1/3 only under a fault-injected tracer replay. `pipeTo-shutdown-gc.test.ts` exercises the shape (native body source, socket fault mid-stream, fire-and-forget `pipeTo` under `collectContinuously`, `AbortDestination` shutdown arm) as a regression surface. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/streams/pipeTo-shutdown-gc.test.ts <!-- robobun:evidence:end -->
…p_until (oven-sh#41149) Fuzzilli found an ASAN stack-buffer-overflow in `run_tasks` -> `Log::add_msg` -> `Vec::push` during runtime auto-install. ## What happened `enqueue_dependency_to_root` blocks in `sleep_until`. `sleep_until` ticks the JS event loop between `is_done` polls. Each poll calls `run_tasks`, which reads `manager.log` to log manifest 4xx errors. The event loop tick can run module transpilation or `AsyncModule::resume_loading_module`. Both swap the VM's log pointers (`jsc_vm.log`, `transpiler.log`, `resolver.log`, `linker.log`, `pm.log`) and restore them on exit. They do not save from the same source: - `resolve_maybe_needs_trailing_slash` saves `jsc_vm.log` and swaps every pointer except `transpiler.log`. - `transpile_source_code_inner` saves `transpiler.log` and restores `pm.log` to it. - `AsyncModule::resume_loading_module` saves `jsc_vm.log` but restores `transpiler.log` to it. When a module transpile runs during a resolve's `sleep_until` tick, its restore sets `pm.log` to the old `transpiler.log`, not the resolve's scoped log. The 404 diagnostics from `run_tasks` then land in the wrong log. With more interleaving across calls `pm.log` ends up at a dead stack `Log`, and the next `run_tasks` poll reads it. ``` READ of size 8 at 0x7ffd98a149c0 thread T0 #0 RawVecInner::capacity #1 Log::add_msg (lib.rs:2369) #3 Log::add_error_fmt (lib.rs:2032) #4 run_tasks (runTasks.rs:494) #5 Closure::is_done (PackageManagerEnqueue.rs:534) #7 AnyEventLoop::tick_raw (AnyEventLoop.rs:148) #8 PackageManager::sleep_until (PackageManager.rs:1063) #9 enqueue_dependency_to_root (PackageManagerEnqueue.rs:571) ... oven-sh#18 resolve_maybe_needs_trailing_slash (VirtualMachine.rs:4259) Address is located in stack of thread T0 in frame #0 to_js_host_call (host_fn.rs:677) [144, 152) 'scope' <== Memory access at offset 160 overflows this variable [176, 240) 'scope_storage' ``` ## Fix - Swap and restore `transpiler.log` with the other log pointers in `resolve_maybe_needs_trailing_slash`. It can no longer drift from `jsc_vm.log` across a resolve. - Snapshot `pm.log` on entry to `enqueue_dependency_to_root`'s `sleep_until`. Re-assert it before each `run_tasks` poll. `run_tasks` always sees the caller's log, whatever the event loop tick did to it. ## Test The test runs a 404 registry in the parent process on `port: 0`. The child queues `require()` calls with `setImmediate`, so they run during `sleep_until`'s event-loop tick and trigger `transpile_source_code_inner`'s log swap. Without the fix, the 404 errors land in the VM log and print to stderr at exit. With the fix, stderr is empty. Verified on current `main` (6f27257): the test fails without the source change and passes with it. Supersedes oven-sh#31120, which the stale bot closed after 90 days. Same change, rebased onto current main; moved to this branch so the fix can be tracked. <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 6 · 3 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts bun test v1.4.1 (a6c4cc2) test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts: (pass) repeated failing auto-install resolves at varying stack depth don't read a dangling pm.log [1075.94ms] 114 | stderr: "pipe", 115 | }); 116 | 117 | const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); 118 | 119 | expect(stderr).toBe(""); ^ error: expect(received).toBe(expected) - "" + "error: GET http://localhost:37987/autoinstall-missing-pkg-0 - 404 + + error: GET http://localhost:37987/autoinstall-missing-pkg-1 - 404 + + error: GET http://localhost:37987/autoinstall-missing-pkg-2 - 404 + + error: GET http://localhost:37987/autoinstall-missing-pkg-3 - 404 + + error: GET http://localhost:37987/autoinstall-missing-pkg-4 - 404 + + error: GET http://localhost:37987/autoinstall-missing-pkg-5 - 404 + + error: GET http://localhost:37987/autoinstall-missing-pkg-6 - 404 + + error: GET http:// ... (truncated) release without fix: 1 FAILED bun test v1.4.1-canary.1 (a6c4cc2) test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts: (pass) repeated failing auto-install resolves at varying stack depth don't read a dangling pm.log [22.63ms] 114 | stderr: "pipe", 115 | }); 116 | 117 | const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); 118 | 119 | expect(stderr).toBe(""); ^ error: expect(received).toBe(expected) - "" + "error: GET http://localhost:35697/autoinstall-missing-pkg-0 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-1 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-2 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-3 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-4 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-5 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-6 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-7 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-8 - 404 + + error: GET http://localhost:35697/autoinstall-missing-pkg-9 - ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts bun test v1.4.1 (a6c4cc2) test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts: (pass) repeated failing auto-install resolves at varying stack depth don't read a dangling pm.log [1104.72ms] (pass) module transpile during auto-install's event-loop tick doesn't desync pm.log [689.71ms] 2 pass 0 fail 8 expect() calls Ran 2 tests across 1 file. [3.73s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 590ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/141] gen generated_host_exports.rs generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 244 extern-C blocks audited [2/141] gen JS modules (bundle-modules) Preprocess modules (8046ms) Bundle modules (666ms) Postprocesss modules (969ms) Bundle Functions (629ms) Generate Code (40ms) [10.36s] Bundled "src/js" for production 2594 kb 197 internal modules 13 native modules 50 internal functions across 16 files [2/141] cargo bun_runtime → libbun_runtime.a �[1m�[92m Compiling�[0m bun_output_tags v0.0.0 (/workspace/bun/src/bun_output_tags) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_parsers v0.0.0 (/workspace/bun/src/parsers) �[1m�[92m Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install) �[1m�[92m Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc) �[1m�[92m Compiling�[0m bun_dispatch v0.0.0 (/workspace/bun/src/dispatch) �[1m�[92m Compiling�[0m bun_jsc_macros v0.0.0 (/workspace/bun/src/jsc_macros) ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` .../PackageManager/PackageManagerEnqueue.rs | 6 ++ src/jsc/VirtualMachine.rs | 5 ++ .../resolve-autoinstall-log-dangling.test.ts | 65 ++++++++++++++++++++++ 3 files changed, 76 insertions(+) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 6 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/install/PackageManager/PackageManagerEnqueue.rs 1 2 19 src/jsc/VirtualMachine.rs 5 3 19 …js/bun/resolve/resolve-autoinstall-log-dangling.test.ts 3 9 18 ``` </details> <!-- robobun:evidence:end -->
Fix is_executable_file directory false positive on OHOS
Fix OHOS shell-init getcwd error by setting $PWD in child env across run_command.rs and filter_run.rs, and mark BufferedReader stdout/stderr as socket+nonblocking to prevent on_reader_error infinite loops
Fixes test/cli/install/bun-run.test.ts