Skip to content

deps: update highway to 1.4.0 - #6

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
deps/update-highway
Open

deps: update highway to 1.4.0#6
github-actions[bot] wants to merge 1 commit into
mainfrom
deps/update-highway

Conversation

@github-actions

Copy link
Copy Markdown

What does this PR do?

Updates highway to version 1.4.0

Compare: google/highway@ac0d5d2...2607d3b

Auto-updated by this workflow

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`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant