deps: update highway to 1.4.0 - #6
Open
github-actions[bot] wants to merge 1 commit into
Open
Conversation
igorls
pushed a commit
that referenced
this pull request
Apr 29, 2026
…en-sh#29856) ## Problem `onUpgrade` in `src/bun.js/api/server.zig` reads `Sec-WebSocket-Protocol` / `Sec-WebSocket-Extensions` from the user's upgrade headers via `FetchHeaders.fastGet`, then immediately calls `fastRemove` on the same entry so the header isn't written twice. `fastGet` returns a `ZigString` whose pointer borrows directly from the header map entry's `WTF::StringImpl` buffer (no ref taken). `fastRemove` erases the map entry, dropping the last reference and freeing that `StringImpl` when nothing else holds one. The dangling `ZigString` is then dereferenced later in `toSlice()` and the freed bytes are written to the socket via `resp.upgrade()`. Introduced in 12243b9 (oven-sh#26118). ## Repro ```js using server = Bun.serve({ port: 0, websocket: { message() {} }, fetch(req, server) { const h = new Headers(); h.append("Sec-WebSocket-Protocol", "a".repeat(128)); h.append("Sec-WebSocket-Protocol", "tail"); // map now solely owns the combined StringImpl if (server.upgrade(req, { headers: h })) return; return new Response("no", { status: 400 }); }, }); await fetch(server.url, { headers: { Upgrade: "websocket", Connection: "Upgrade", "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", "Sec-WebSocket-Version": "13", "Sec-WebSocket-Protocol": "x", }, }); ``` With `Malloc=1` (routes bmalloc through the system allocator) under an ASAN build: ``` ==ERROR: AddressSanitizer: heap-use-after-free #6 ZigString.toSlice src/bun.js/bindings/ZigString.zig:677 #7 server.onUpgrade src/bun.js/api/server.zig:1020 ``` ## Fix Clone the header value into an owned `ZigString.Slice` via `toSliceClone` **before** calling `fastRemove`, and point the `ZigString` at the owned buffer. The owned slice is freed by `defer` at scope exit, so the later `toSlice()` / `resp.upgrade()` read valid memory. Applied to both the `Bun.serve` Request path and the `node:http` upgrade path. ## Verification - New regression test in `test/js/bun/websocket/websocket-server.test.ts` double-appends `Sec-WebSocket-Protocol` so the combined `makeString` result is solely owned by the header map, and runs the subprocess with `Malloc=1` so ASAN observes the `StringImpl` allocation. - `git stash -- src/ && bun bd test <file> -t 'does not use-after-free'` → **FAIL** (`AddressSanitizer: heap-use-after-free`) - `git stash pop && bun bd test <file> -t 'does not use-after-free'` → **PASS** - `test/regression/issue/3613.test.ts` (original oven-sh#26118 fix) still passes. Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls
pushed a commit
that referenced
this pull request
May 6, 2026
…ion getters (oven-sh#30078) ## Problem `server.upgrade(req, opts)` reads `Sec-WebSocket-Key` / `Sec-WebSocket-Protocol` / `Sec-WebSocket-Extensions` from `request.getFetchHeaders()` via `FetchHeaders.fastGet`, which returns a `ZigString` that **borrows** directly from the header map entry's `StringImpl` (`bindings.cpp` `WebCore__FetchHeaders__fastGet_` → `Zig::toZigString(StringView)`, no ref taken). It then invokes the `opts.data` / `opts.headers` getters — arbitrary user JS — and only afterwards passes those borrowed slices to `resp.upgrade()`. A getter that mutates `req.headers` (e.g. `req.headers.set('sec-websocket-key', ...)`) drops the sole ref on the original `StringImpl` (`HTTPHeaderMap::set` does a `RefPtr` assignment), freeing it. `resp.upgrade()` then reads freed memory for the key/protocol/extensions. ```js Bun.serve({ fetch(req, server) { req.headers; // materialize FetchHeaders server.upgrade(req, { get data() { req.headers.set('sec-websocket-key', 'x'); // frees the borrowed StringImpl return undefined; }, }); }, websocket: { message() {} }, }); ``` The re-entrancy guard after the getters only checks `isAbortedOrEnded() / didUpgradeWebSocket()`, not header mutation. The `opts.headers` path was already defensively cloning with `toSliceClone` (because `fastRemove` there frees the backing); the `request.headers` path was missed. ## Fix Clone `sec_websocket_key` / `protocol` / `extensions` into owned `ZigString.Slice` storage immediately after reading them from `request.getFetchHeaders()`, so the bytes stay valid across the option getters and `resp.upgrade()`. The `opts.headers` override path reuses the same owned slots (freeing the previous clone first). ## Verification New test in `test/js/bun/websocket/websocket-server-upgrade-reentrant.test.ts` spawns a subprocess with `Malloc=1` (routes bmalloc → system heap so ASAN observes `StringImpl` frees) and has an `opts.data` getter overwrite all three `Sec-WebSocket-*` headers. **Before** (src/ stashed, `bun bd test`): ``` ==ERROR: AddressSanitizer: heap-use-after-free #3 uWS::HttpResponse<false>::upgrade ... HttpResponse.h:269 #6 server.zig:1076 (resp.upgrade call) (fail) server.upgrade() clones Sec-WebSocket-* from request.headers before running option getters ``` **After**: all three tests in the file pass. Also fails on `USE_SYSTEM_BUN=1` (release, no ASAN) — with `Malloc=1` the system allocator reuses the freed slot and the WebSocket client rejects the handshake (bad `Sec-WebSocket-Accept` / mismatched protocol). Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls
pushed a commit
that referenced
this pull request
May 6, 2026
…es (oven-sh#30077) ## What When a chunked (or HTTP/3) request body exceeds `maxRequestBodySize`, `onBufferedBodyChunk` writes the 413 directly on the raw uWS response: ```zig resp.writeStatus("413 Payload Too Large"); resp.endWithoutBody(comptime !http3); ``` `internalEnd` → `markDone()` nulls `onAborted`, so when the socket closes no abort ever fires to detach `ctx.resp` or release the base ref. `this.resp` is left pointing at a completed response whose socket is about to be freed by `us_internal_free_closed_sockets`. If the fetch handler returned a pending Promise: - **resolve**: `handleResolve` → `isAbortedOrEnded()` is false (`this.resp != null`) → `render()` → `runCorkedWithType` corks the freed socket → **heap-use-after-free** (ASAN trace below). - **reject**: `handleReject` reads `resp.hasResponded()` off freed memory, sees `true`, skips the error handler, and returns without ever releasing the base ref → **RequestContext leaks** (`server.pendingRequests` never returns to 0). ## Fix Route through `this.endWithoutBody()` (the `RequestContext` wrapper) instead of the raw `resp.endWithoutBody()`. That path does `detachResponse()` (nulls `this.resp`, clears `onData`/`onAborted`/`onTimeout`) and `deref()` (releases the base ref), matching every other end path in this file. The body promise is rejected with the specific `"Request body exceeded maxRequestBodySize"` error *before* `endWithoutBody()` so `endRequestStreaming()` doesn't overwrite it with a generic `ConnectionClosed`. `has_written_status` is set so any later `renderMissing`/`renderMetadata` knows the status line is already committed. ## Repro ``` ==ERROR: AddressSanitizer: heap-use-after-free #0 us_socket_group socket.c:77 #1 uWS::AsyncSocket<false>::getLoopData() AsyncSocket.h:69 #2 uWS::AsyncSocket<false>::isCorked() AsyncSocket.h:141 #3 uWS::HttpResponse<false>::cork(...) HttpResponse.h:647 #4 uws_res_cork libuwsockets.cpp:1740 #5 ...runCorkedWithType Response.zig:299 #6 ...doRenderBlob RequestContext.zig:1942 ... oven-sh#11 ...handleResolve RequestContext.zig:220 oven-sh#12 ...onResolve RequestContext.zig:154 freed by: #1 us_poll_free epoll_kqueue.c:73 #2 us_internal_free_closed_sockets loop.c:305 ``` ## Test `test/js/bun/http/serve-pending-promise-abort-leak.test.ts` — new case sends a raw `Transfer-Encoding: chunked` POST exceeding `maxRequestBodySize` with a handler that holds its resolve/reject, waits for the socket to be reclaimed, then settles the Promise. Asserts `pendingRequests` returns to 0 for both paths, the body was rejected with the right message, and a follow-up request still works. Without the fix: ASAN heap-use-after-free on the resolve path; on release builds the reject path shows `pendingAfterReject: 1` (leak). Co-authored-by: robobun <robobun@users.noreply.github.com>
igorls
pushed a commit
that referenced
this pull request
May 6, 2026
…worker panic, never retry (oven-sh#30216) ## What `bun test --isolate` / `--parallel` crashes when a test file loads a native addon whose deferred napi finalizers outlive the file. The `--parallel` coordinator then silently retries the file once, which masks the panic and lets the run exit 0. Fixes oven-sh#30205, oven-sh#30191. Supersedes oven-sh#30214 (same NapiEnv fix, but without the coordinator change, the `cleanup_hooks` retarget, or a test that actually reproduces on unpatched `main`). ## Reproduction ```sh git clone https://github.com/workglow-dev/libs && cd libs bun i && bun run build:packages bun test --timeout=30000 --parallel=4 packages/test/src/test/{util,task}/*.test.ts ``` On `main` (d484fd6), 3–4 workers crash per run with either ``` ASSERTION FAILED: isMarked(cell) JavaScriptCore/heap/Heap.cpp:1232 : void JSC::Heap::addToRememberedSet(const JSCell *) ``` or (when the slot is already being reallocated) ``` ASSERTION FAILED: m_cellState == CellState::DefinitelyWhite JavaScriptCore/JSCellInlines.h:69 : JSC::JSCell::JSCell(VM &, Structure *) ``` and in release builds the segfaults at `0x68` / `0xD0` reported in oven-sh#30205. ## Root cause Frame-pointer walk from the assertion: ``` #3 Bun::NapiHandleScope::open(Zig::GlobalObject*, bool) #4 NapiHandleScope__open #6 napi.Finalizer.run #7 napi.NapiFinalizerTask.runOnJSThread oven-sh#10 event_loop.tick oven-sh#11 event_loop.waitForPromise oven-sh#13 VirtualMachine.loadEntryPointForTestRunner ← next test file ``` `NapiEnv::m_globalObject` is a raw `Zig::GlobalObject*`. For non-experimental addons (`nm_version != NAPI_VERSION_EXPERIMENTAL`, which is ~every real-world addon — sharp, better-sqlite3, etc.), `napi_wrap`/`napi_create_external` finalizers are **deferred** to the event loop as `NapiFinalizerTask` rather than run inside GC sweep. Objects rooted on the old global (module graph, `globalThis.*`) only become collectable when `Zig__GlobalObject__createForTestIsolation` runs `gcUnprotect(oldGlobal)`. The `DeferGC` from oven-sh#29573 ends at that function's `}`, so the next GC runs there, collects those objects, and enqueues their finalizers. Those tasks then run on the very next `eventLoop().tick()` — inside `loadEntryPointForTestRunner`'s `waitForPromise` for file N+1. `Finalizer.run` opens a `NapiHandleScope` via `env->globalObject()`, which reads `NapiHandleScopeImplStructure()` off the dead cell and writes `m_currentNapiHandleScopeImpl` on it → write barrier on an unmarked cell. The `--parallel` coordinator's `reapWorker` then re-queued the file once (`retries[idx] < 1`) into a fresh worker with no stale `NapiEnv`, which passed — so the run reported 0 fail despite multiple Bun panics in the log. ## Fix **NapiEnv retarget** (`ZigGlobalObject.cpp`, `napi.h`): `Zig__GlobalObject__createForTestIsolation` now calls `newGlobal->adoptNapiEnvsForTestIsolation(oldGlobal)` before `gcUnprotect`. Each `NapiEnv::m_globalObject` is repointed at the new global and the `Ref<NapiEnv>`s are moved over, so late finalizers open handle scopes on a live global and the envs stay owned after the old global is swept. `VirtualMachine.swapGlobalForTestIsolation` also repoints `rare_data.cleanup_hooks[*].globalThis` so `CleanupHook.eql()` stays accurate. **No retry, abort on panic** (`Coordinator.zig`): removed the per-file retry. A worker that dies mid-file is counted as one failure. If it died by a fatal signal (SIGILL/SIGTRAP/SIGABRT/SIGBUS/SIGFPE/SIGSEGV/SIGSYS — Bun's own `@trap()`, a JSC/WTF assertion, or native-addon crash), the whole run aborts with `error: a test worker process crashed with <SIG> while running <file>`. `process.exit()` / SIGKILL are still just a per-file failure and the run continues. ## Verification - `test/regression/issue/30205.test.ts` — 4 tests. Adds a tiny non-experimental addon (`isolate_finalizer_addon.c`) and a fixture pattern (`Bun.gc(true)` + module-scope `await 0` + objects rooted on `globalThis`) that crashes **8/8** on unpatched `main` and passes 8/8 with this change. - `workglow-dev/libs` full 201-file unit suite: 3× clean `--parallel=4` runs (was 3–4 crashes/run). - Gate: `git stash -- src/ && bun bd test test/regression/issue/30205.test.ts` → 3/4 fail; with fix → 4/4 pass. - `test/cli/test/isolation.test.ts`, `test/regression/issue/29519.test.ts` → pass (one pre-existing unrelated timeout in isolation.test.ts, same as oven-sh#29573). - `test/cli/test/parallel.test.ts` → all tests I touched pass; the 3 timing-sensitive scale-up/work-steal tests that fail in this container fail identically on unmodified `main`. --------- Co-authored-by: robobun <robobun@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
igorls
pushed a commit
that referenced
this pull request
May 31, 2026
…s, http (oven-sh#30722) Hardens 36 reachable security findings across the runtime, package manager, parsers, HTTP client/server, and SQL drivers. Three auto-applied fixes (oven-sh#61 SSL exception leak, oven-sh#68 YAML merge dedup, oven-sh#104 archive overwrite precheck) were dropped: oven-sh#61 introduced a use-after-free, oven-sh#68 stored a non-`'static` byte view in a `'static` field, and oven-sh#104 added dead gating that did not close the traversal. ### Memory safety / lifetime - #2 — Dangling proxy slice across reentrant JS getter — copy `process.env` proxy href to an owned `Vec` before reentrant getters can free the env map (`Blob.rs`) - oven-sh#15 — Rollback restores dangling editor name pointer — preserve and restore `name_storage` on `detect_editor` failure (`BunObject.rs`) - oven-sh#81 — Reentrant reconnect frees live handlers — only free previous handlers when `active_connections == 0` (`Listener.rs`) - oven-sh#110 — Async randomFill uses stale resizable buffer pointer — fill a worker-owned scratch buffer; copy back on the JS thread after re-validating bounds (`node_crypto_binding.rs`) - oven-sh#119 — Null zero-length slice UB in DOMJIT fast path — use `ffi::slice` which tolerates `(null, 0)` (`Crypto.rs`) - oven-sh#67 — Raw serialization reads struct padding bytes — add explicit `_padding_*` fields with `offset_of!` proof asserts (`npm.rs`) - oven-sh#74 — TLS rejection path leaks websocket refcount — route SSL/auth failures through `self.fail()` which clears `outgoing_websocket` (`websocket_client.rs`) - oven-sh#108 — FD-backed fetch body leaks duplicated descriptor — close `opened_fd` unconditionally after `read_file` (`fetch.rs`) ### Untrusted-input bounds / panics - oven-sh#10 — Invalid lockfile tag causes panic DoS — replace `unreachable!()` with logged error + `Tag::Uninitialized` (`dependency.rs`) - oven-sh#20 — Unchecked lockfile string offsets cause OOB slice — bounds-check non-inline `String` pointers against `ctx.buffer` (`dependency.rs`) - oven-sh#91 — Panic on unvalidated resolution tag — validate `ResolutionTag` discriminants on lockfile load (`Package.rs`) - oven-sh#24 — Unwrap panic on unexpected 304 response — return `UnexpectedNotModified` when no cached manifest exists (`npm.rs`) - oven-sh#44 — UDP port getter unwrap panic on transient state — return `undefined` when `socket` is `None` (`udp_socket.rs`) - oven-sh#36 — Close reason length mismatch causes panic — clamp `body_len` to 125 and bail on overlong UTF-8 transcode (`websocket_client.rs`) - oven-sh#100 — Windows pipe name length panic DoS — `debug_assert` → real bounds check (`Listener.rs`) - oven-sh#60 / oven-sh#111 — Windows shim stack buffer overflows — bounds-check argument and filename writes against `BUF1_LEN`/`BUF2_U16_LEN` before `copy_nonoverlapping` (`bun_shim_impl.rs`) - oven-sh#76 / oven-sh#101 — Unchecked bin name/entry name copies — bounds-check before slicing into `abs_dest_buf` (`bin.rs`) - oven-sh#79 — `if` keyword misclassification causes parser panic — require a delimiter token before classifying (`shell_parser/parse.rs`) - oven-sh#32 — Bounds check occurs after UTF-16 write — pre-flight key/value lengths before `convert_utf8_to_utf16_in_buffer` (`env_loader.rs`) - oven-sh#95 — PBKDF2 digest validation allows panic-only algorithm — reject digests with no `EVP_MD` (`PBKDF2.rs`) ### DoS / resource caps - oven-sh#17 — Unbounded recursion on deep TOML dotted keys — cap dotted-key segments at 512 (`toml.rs`) - oven-sh#39 — Unbounded brace expansion preallocation — cap expansion count at 65536 in `Bun.$` and `Bun.braces` (`BunObject.rs`, `Expansion.rs`) - oven-sh#31 — SCRAM PBKDF2 parameters accepted from server — clamp iteration count to `[4096, 10M]`, salt length to `[1, 1024]` (`PostgresSQLConnection.rs`) ### Auth / injection / traversal - oven-sh#19 — Cleartext password sent after TLS downgrade — require `TLSStatus::SslOk`, not just `ssl_mode != Disable` (`MySQLConnection.rs`) - oven-sh#83 — Strict TLS request reuses lax-verified pooled socket — track `established_with_reject_unauthorized` and refuse pool reuse for strict callers (`HTTPContext.rs`, `lib.rs`, `ClientSession.rs`) - oven-sh#73 — IPv6 loopback prefix auth bypass — exact-match `::1` instead of `starts_with` (`server_body.rs`) - oven-sh#56 — Unsanitized filename injects response headers — reject `\r`/`\n`/NUL/`"` in `content-disposition` filenames (`RequestContext.rs`) - oven-sh#43 — Missing CRLF checks for signed host/auth headers — also validate `region`, `access_key_id`, and `host` (`s3_signing/credentials.rs`) - oven-sh#34 — Bucket slash enables S3 host confusion — reject buckets containing `/` (`s3_signing/credentials.rs`) - oven-sh#25 — Lexical symlink check permits extraction escape — track created symlinks during extraction and refuse paths that traverse them (`libarchive/lib.rs`) - oven-sh#71 — bunx executes untrusted temp-cache binary — `lstat` cached binary; refuse symlinks and other-uid files (`bunx_command.rs`) ### Permission hygiene - #6 — Bin target chmod always sets mode 0777 — `0o777 & !umask` instead of `umask | 0o777` (`bin.rs`) - oven-sh#23 — Process umask cleared and never restored — restore umask after probing it in `ensure_umask` (`bin.rs`) ### Parser correctness - oven-sh#22 — Sign-prefixed scalar misparsed as infinity — fix Zig→Rust `&&`/`||` precedence transliteration (`yaml.rs`)
igorls
pushed a commit
that referenced
this pull request
May 31, 2026
…er (oven-sh#31333) ### Problem Fuzzing found a second transpiler stack overflow (`sig:SIGSEGV:nostack`): ~600 nested `{` blocks crash the process. ```js new Bun.Transpiler({ loader: "tsx", target: "bun", minifyWhitespace: true, deadCodeElimination: true }) .transformSync("{".repeat(600) + 'class Test1 { static "prop1" = 0; }' + "}".repeat(600)); ``` oven-sh#31242 guarded the **expression** recursion (`visit_expr_in_out`, `print_expr`, DCE helpers), but the **statement** recursion was left unguarded. Nested blocks stay under `MAX_STMT_DEPTH` (1000) in `parse_stmt`, then the visit pass recurses through `visit_stmts → visit_and_append_stmt → s_block → visit_stmts` with no stack check — each level stacks several multi-KB frames, so a few hundred levels exhaust the thread's stack (reproduces at depth 800 on a debug build's 8 MB main stack; smaller stacks crash at 600): ``` #5 visit_stmts src/js_parser/visit/mod.rs:1280 #6 s_block src/js_parser/visit/visit_stmt.rs:1627 #7 visit_and_append_stmt src/js_parser/visit/visit_stmt.rs:108 #8 visit_stmts src/js_parser/visit/mod.rs:1336 ... (repeats until SIGSEGV) ``` ### Fix Guard the statement recursion the same way the expression recursion already is: - `visit_and_append_stmt` now checks `stack_check.is_safe_to_recurse()` (plus the `reported_stack_overflow` fast-path) and reports "Maximum call stack size exceeded" instead of descending, mirroring `visit_expr_in_out`. - `print_stmt` and `print_if` (which self-recurses for `else if` chains without passing through `print_stmt`) get the same guard `print_expr`/`print_binding` already have, so a deep AST printed on a thread with less stack headroom errors instead of overflowing. - Removed the `MAX_STMT_DEPTH`/`parse_stmt_depth` hard cap from `parse_stmt` (review feedback): recursion depth in every phase is now governed by `StackCheck` alone, matching the Zig parser. - Guarded `hoist_symbols` the same way: it walks the scope tree before the visit pass at the full depth the parser allowed, and was only kept safe previously by the now-removed cap (the 15k-deep `lots-of-for-loop.js` fixture overflowed it in release builds otherwise). With this, every arbitrarily-nestable AST recursion (statements, expressions, bindings) is stack-checked in all three phases (parse, visit, print); deep inputs throw a catchable `Maximum call stack size exceeded` error. ### Verification New test `deeply nested statement blocks error instead of crashing the process` in `test/bundler/transpiler/transpiler.test.js` transpiles nested-block and `else if`-chain shapes at depths 600/800/990 (below the parse-time cap, deep enough to overflow an unguarded visitor) in a subprocess and asserts it exits cleanly. - Without the fix: the subprocess dies with SIGSEGV at depth 800+ (debug build), so the test fails. - With the fix: `bun bd test test/bundler/transpiler/transpiler.test.js` → 147 pass, 0 fail; the repro above now throws `Maximum call stack size exceeded`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Updates highway to version 1.4.0
Compare: google/highway@ac0d5d2...2607d3b
Auto-updated by this workflow