Compare commits

...

19 Commits

Author SHA1 Message Date
Claude Bot
32ee722ad4 fix(socket): resolve HTTP streaming hangs on Windows
Three fixes to address issue #27010 where HTTP requests using the Node.js
http module would hang on Windows after receiving the first data chunk:

1. Force writable poll re-registration on libuv/Windows: Add
   us_poll_change_force() that unconditionally restarts uv_poll to work
   around WSAPoll not reliably delivering writable events after partial
   writes. Previously, only kqueue had explicit re-registration.

2. Enable repeat-recv on Windows: The recv loop optimization that reads
   multiple times when large data is available was completely disabled
   on Windows. This caused each recv to require a full event loop
   iteration, degrading streaming performance.

3. Fix bsd_write2 partial write handling on Windows: The two-send
   fallback (since Windows lacks writev) now properly propagates errors
   from the first send call instead of passing negative values through.

Closes #27010

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-13 23:15:34 +00:00
Jarred Sumner
c57af9df38 Fix websocket proxy ping crash (#26995)
### What does this PR do?

The `sendPong` fix alone wasn't sufficient. The bug only manifests with
**wss:// through HTTP proxy** (not ws://), because only that path uses
`initWithTunnel` with a detached socket.

**Two bugs were found and fixed:**

1. **`sendPong`/`sendCloseWithBody` socket checks**
(`src/http/websocket_client.zig`): Replaced `socket.isClosed() or
socket.isShutdown()` with `!this.hasTCP()` as originally proposed. Also
guarded `shutdownRead()` against detached sockets.

2. **Spurious 1006 during clean close** (`src/http/websocket_client.zig`
+ `WebSocketProxyTunnel.zig`): When `sendCloseWithBody` calls
`clearData()`, it shuts down the proxy tunnel. The tunnel's `onClose`
callback was calling `ws.fail(ErrorCode.ended)` which dispatched a 1006
abrupt close, overriding the clean 1000 close already in progress. Fixed
by adding `tunnel.clearConnectedWebSocket()` before `tunnel.shutdown()`
so the callback is a no-op.

### How did you verify your code works?

- `USE_SYSTEM_BUN=1`: Fails with `Unexpected close code: 1006`
- `bun bd test`: Passes with clean 1000 close
- Full proxy test suite: 25 pass, 4 skip, 0 fail
- Related fragmented/close tests: all passing

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 14:49:40 -08:00
SUZUKI Sosuke
3debd0a2d2 perf(structuredClone): remove unnecessary zero-fill for Int32 and Double array fast paths (#26989)
## What

Replace two-step `Vector<uint8_t>` zero-initialization + `memcpy` with
direct `std::span` copy construction in the `structuredClone` Int32 and
Double array fast paths.

## Why

The previous code allocated a zero-filled buffer and then immediately
overwrote it with `memcpy`. By constructing the `Vector` directly from a
`std::span`, we eliminate the redundant zero-fill and reduce the
operation to a single copy.

### Before
```cpp
Vector<uint8_t> buffer(byteSize, 0);
memcpy(buffer.mutableSpan().data(), data, byteSize);
```

### After
```cpp
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
```

## Test

`bun bd test test/js/web/structured-clone-fastpath.test.ts` — 91/92 pass
(1 pre-existing flaky memory test unrelated to this change).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:50:54 -08:00
robobun
7afead629c fix(linker): defer dynamic import() of unknown node: modules to runtime (#26981)
## Summary

- Defer resolution of dynamic `import()` of unknown `node:` modules
(like `node:sqlite`) to runtime instead of failing at transpile time
- Fix use-after-poison in `addResolveError` by always duping `line_text`
from the source so Location data outlives the arena

Fixes #25707

## Root cause

When a CJS file is `require()`d, Bun's linker eagerly resolves all
import records, including dynamic `import()` expressions. For unknown
`node:` prefixed modules, `whenModuleNotFound` was only deferring
`.require` and `.require_resolve` to runtime — `.dynamic` imports fell
through to the error path, causing the entire `require()` to fail.

This broke Next.js builds with turbopack + `cacheComponents: true` +
Better Auth, because Kysely's dialect detection code uses
`import("node:sqlite")` inside a try/catch that gracefully handles the
module not being available.

Additionally, when the resolve error was generated, the
`Location.line_text` was a slice into arena-allocated source contents.
The arena is reset before `processFetchLog` processes the error, causing
a use-after-poison when `Location.clone` tries to dupe the freed memory.

## Test plan

- [x] New regression test in `test/regression/issue/25707.test.ts` with
3 cases:
- CJS require of file with `import("node:sqlite")` inside try/catch
(turbopack pattern)
  - CJS require of file with bare `import("node:sqlite")` (no try/catch)
  - Runtime error produces correct `ERR_UNKNOWN_BUILTIN_MODULE` code
- [x] Test fails with `USE_SYSTEM_BUN=1` (system bun v1.3.9)
- [x] Test passes with `bun bd test`
- [x] No ASAN use-after-poison crash on debug build


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-12 14:47:31 -08:00
Jarred Sumner
9a72bbfae2 Update CLAUDE.md 2026-02-12 14:25:05 -08:00
robobun
7a801fcf93 fix(ini): prevent OOB read and UB on truncated/invalid UTF-8 in INI parser (#26947)
## Summary

- Fix out-of-bounds read in the INI parser's `prepareStr` function when
a multi-byte UTF-8 lead byte appears at the end of a value with
insufficient continuation bytes
- Fix undefined behavior when bare continuation bytes (0x80-0xBF) cause
`utf8ByteSequenceLength` to return 0, hitting an `unreachable` branch
(UB in ReleaseFast builds)
- Add bounds checking before accessing `val[i+1]`, `val[i+2]`,
`val[i+3]` in both escaped and non-escaped code paths

The vulnerability could be triggered by a crafted `.npmrc` file
containing truncated UTF-8 sequences. In release builds, this could
cause OOB heap reads (potential info leak) or undefined behavior.

## Test plan

- [x] Added 9 tests covering truncated 2/3/4-byte sequences, bare
continuation bytes, and escaped contexts
- [x] All 52 INI parser tests pass (`bun bd test
test/js/bun/ini/ini.test.ts`)
- [x] No regressions in npmrc tests (failures are pre-existing Verdaccio
connectivity issues)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 00:28:44 -08:00
robobun
44541eb574 fix(sql): reject null bytes in connection parameters to prevent protocol injection (#26952)
## Summary

- Reject null bytes in `username`, `password`, `database`, and `path`
connection parameters for both PostgreSQL and MySQL to prevent wire
protocol parameter injection
- Both the Postgres and MySQL wire protocols use null-terminated strings
in their startup/handshake messages, so embedded null bytes in these
fields act as field terminators, allowing injection of arbitrary
protocol parameters (e.g. `search_path` for schema hijacking)
- The fix validates these fields immediately after UTF-8 conversion and
throws `InvalidArguments` error with a clear message if null bytes are
found

## Test plan

- [x] New test
`test/regression/issue/postgres-null-byte-injection.test.ts` verifies:
- Null bytes in username are rejected with an error before any data is
sent
- Null bytes in database are rejected with an error before any data is
sent
- Null bytes in password are rejected with an error before any data is
sent
  - Normal connections without null bytes still work correctly
- [x] Test verified to fail with `USE_SYSTEM_BUN=1` (unfixed bun) and
pass with `bun bd test` (fixed build)
- [x] Existing SQL tests pass (`adapter-env-var-precedence.test.ts`,
`postgres-stringbuilder-assertion-aggressive.test.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 00:27:00 -08:00
robobun
993be3f931 fix(plugin): set virtualModules to nullptr after delete in clearAll (#26940)
## Summary

- Fix double-free in `Bun.plugin.clearAll()` by setting `virtualModules
= nullptr` after `delete`
- In `jsFunctionBunPluginClear` (`BunPlugin.cpp:956`), `delete
global->onLoadPlugins.virtualModules` freed the pointer without
nullifying it. When the `OnLoad` destructor later runs (during Worker
termination or VM destruction), it checks `if (virtualModules)` — the
dangling non-null pointer passes the check and is deleted again,
corrupting the heap allocator.

## Test plan

- [ ] New test
`test/regression/issue/plugin-clearall-double-free.test.ts` spawns a
subprocess that registers a virtual module, calls
`Bun.plugin.clearAll()`, and exits with `BUN_DESTRUCT_VM_ON_EXIT=1` to
trigger the destructor path
- [ ] Verified the test fails on the system bun (pre-fix) with `pas
panic: deallocation did fail ... Alloc bit not set`
- [ ] Verified the test passes with the debug build (post-fix)
- [ ] Existing plugin tests (`test/js/bun/plugin/plugins.test.ts`) all
pass (29/29)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-11 23:14:43 -08:00
robobun
a68393926b fix(ws): handle fragmented pong frames and validate control frame size (#26944)
## Summary

- Fix WebSocket client pong frame handler to properly handle payloads
split across TCP segments, preventing frame desync that could cause
protocol confusion
- Add missing RFC 6455 Section 5.5 validation: control frame payloads
must not exceed 125 bytes (pong handler lacked this check, unlike ping
and close handlers)

## Details

The pong handler (lines 652-663) had two issues:

1. **Frame desync on fragmented delivery**: When a pong payload was
split across TCP segments (`data.len < receive_body_remain`), the
handler consumed only the available bytes but unconditionally reset
`receive_state = .need_header` and `receive_body_remain = 0`. The
remaining payload bytes in the next TCP delivery were then
misinterpreted as WebSocket frame headers.

2. **Missing payload length validation**: Unlike the ping handler (line
615) and close handler (line 680), the pong handler did not validate the
7-bit payload length against the RFC 6455 limit of 125 bytes for control
frames.

The fix models the pong handler after the existing ping handler pattern:
track partial delivery state with a `pong_received` boolean, buffer
incoming data into `ping_frame_bytes`, and only reset to `need_header`
after the complete payload has been consumed.

## Test plan

- [x] New test `websocket-pong-fragmented.test.ts` verifies:
- Fragmented pong delivery (50-byte payload split into 2+48 bytes) does
not cause frame desync, and a subsequent text frame is received
correctly
- Pong frames with >125 byte payloads are rejected as invalid control
frames
- [x] Test fails with `USE_SYSTEM_BUN=1` (reproduces the bug) and passes
with `bun bd test`
- [x] Existing WebSocket tests pass: `websocket-client.test.ts`,
`websocket-close-fragmented.test.ts`,
`websocket-client-short-read.test.ts`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 23:12:28 -08:00
robobun
e8a5f23385 fix(s3): reject CRLF characters in header values to prevent header injection (#26942)
## Summary

- Fixes HTTP header injection vulnerability in S3 client where
user-controlled options (`contentDisposition`, `contentEncoding`,
`type`) were passed to HTTP headers without CRLF validation
- Adds input validation at the JS-to-Zig boundary in
`src/s3/credentials.zig` that throws a `TypeError` if `\r` or `\n`
characters are detected
- An attacker could previously inject arbitrary headers (e.g.
`X-Amz-Security-Token`) by embedding `\r\n` in these string fields

## Test plan

- [x] Added `test/regression/issue/s3-header-injection.test.ts` with 6
tests:
  - CRLF in `contentDisposition` throws
  - CRLF in `contentEncoding` throws
  - CRLF in `type` (content-type) throws
  - Lone CR in `contentDisposition` throws
  - Lone LF in `contentDisposition` throws
  - Valid `contentDisposition` without CRLF still works correctly
- [x] Tests fail with `USE_SYSTEM_BUN=1` (confirming vulnerability
exists in current release)
- [x] Tests pass with `bun bd test` (confirming fix works)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 23:02:39 -08:00
robobun
16b3e7cde7 fix(libarchive): use normalized path in mkdiratZ to prevent directory traversal (#26956)
## Summary

- Fix path traversal vulnerability in tarball directory extraction on
POSIX systems where `mkdiratZ` used the un-normalized `pathname` (raw
from tarball) instead of the normalized `path` variable, allowing `../`
components to escape the extraction root via kernel path resolution
- The Windows directory creation, symlink creation, and file creation
code paths already correctly used the normalized path — only the two
POSIX `mkdiratZ` calls were affected (lines 463 and 469)
- `bun install` is not affected because npm mode skips directory
entries; affected callers include `bun create`, GitHub tarball
extraction, and `compile_target`

## Test plan

- [x] Added regression test that crafts a tarball with
`safe_dir/../../escaped_dir/` directory entry and verifies it cannot
create directories outside the extraction root
- [x] Verified test **fails** with system bun (vulnerable) and
**passes** with debug build (fixed)
- [x] Full `archive.test.ts` suite passes (99/99 tests)
- [x] `symlink-path-traversal.test.ts` continues to pass (3/3 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 22:47:41 -08:00
robobun
4c32f15339 fix(sql): use constant-time comparison for SCRAM server signature (#26937)
## Summary

- Replace `bun.strings.eqlLong` with BoringSSL's `CRYPTO_memcmp` for
SCRAM-SHA-256 server signature verification in the PostgreSQL client
- The previous comparison (`eqlLong`) returned early on the first
mismatching byte, potentially leaking information about the expected
server signature via timing side-channel
- `CRYPTO_memcmp` is already used elsewhere in the codebase for
constant-time comparisons (CSRF tokens, `crypto.timingSafeEqual`,
KeyObject comparison)

## Test plan

- [x] `bun bd` compiles successfully
- [ ] Existing SCRAM-SHA-256 integration tests in
`test/js/sql/sql.test.ts` pass (require Docker/PostgreSQL)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 22:45:47 -08:00
robobun
635034ee33 fix(shell): use-after-free in runFromJS when setupIOBeforeRun fails (#26920)
## Summary

- Fixes #26918 — segfault at address `0x28189480080` caused by
use-after-free in the shell interpreter
- When `setupIOBeforeRun()` fails (e.g., stdout handle unavailable on
Windows), the `runFromJS` error path called `deinitFromExec()` which
directly freed the GC-managed interpreter object with
`allocator.destroy(this)`. When the GC later swept and called
`deinitFromFinalizer()` on the already-freed memory, it caused a
segfault.
- Replaced `deinitFromExec()` with `derefRootShellAndIOIfNeeded(true)`
which properly cleans up runtime resources (IO handles, shell
environment) while leaving final object destruction to the GC finalizer
— matching the pattern already used in `finish()`.

## Test plan

- [x] Added regression test in `test/regression/issue/26918.test.ts`
that verifies the shell interpreter handles closed stdout gracefully
without crashing
- [x] Test passes with `bun bd test test/regression/issue/26918.test.ts`
- [ ] The actual crash is primarily reproducible on Windows where stdout
handles can be truly unavailable — CI Windows tests should validate the
fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-11 17:51:10 -08:00
robobun
3e792d0d2e fix(test): write JUnit reporter outfile when --bail triggers early exit (#26852)
## Summary
- When `--bail` caused an early exit after a test failure, the JUnit
reporter output file (`--reporter-outfile`) was never written because
`Global.exit()` was called before the normal completion path
- Extracted the JUnit write logic into a `writeJUnitReportIfNeeded()`
method on `CommandLineReporter` and call it in both bail exit paths
(test failure and unhandled rejection) as well as the normal completion
path

Closes #26851

## Test plan
- [x] Added regression test `test/regression/issue/26851.test.ts` with
two cases:
  - Single failing test file with `--bail` produces JUnit XML output
- Multiple test files where bail triggers on second file still writes
the report
- [x] Verified test fails with system bun (`USE_SYSTEM_BUN=1`)
- [x] Verified test passes with `bun bd test`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 17:41:45 -08:00
robobun
b7d505b6c1 deflake: make HMR rapid edits test event-driven (#26890)
## Summary
- Add `expectMessageEventually(value)` to the bake test harness `Client`
class — waits for a specific message to appear, draining any
intermediate messages that arrived before it
- Rewrite "hmr handles rapid consecutive edits" test to use raw
`Bun.write` + sleep for intermediate edits and `expectMessageEventually`
for the final assertion, avoiding flaky failures when HMR batches
updates non-deterministically across platforms

Fixes flaky failure on Windows where an extra "render 10" message
arrived after `expectMessage` consumed its expected messages but before
client disposal.

## Test plan
- [x] `bun bd test test/bake/dev-and-prod.test.ts` — all 12 tests pass
- [x] Ran the specific test multiple times to confirm no flakiness

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-11 16:05:25 -08:00
Dylan Conway
50e478dcdc fix(crypto): correct method and constructor names mangled by number renamer (#26876)
## Problem

The bundler's number renamer was mangling `.name` properties on crypto
class prototype methods and constructors:

- `hash.update.name` → `"update2"` instead of `"update"`
- `verify.verify.name` → `"verify2"` instead of `"verify"`
- `cipher.update.name` → `"update3"` instead of `"update"`
- `crypto.Hash.name` → `"Hash2"` instead of `"Hash"`

### Root causes

1. **Named function expressions on prototypes** collided with other
bindings after scope flattening (e.g. `Verify.prototype.verify =
function verify(...)` collided with the imported `verify`)
2. **Block-scoped constructor declarations** (`Hash`, `Hmac`) got
renamed when the bundler hoisted them out of block scope
3. **Shared function declarations** in the Cipher/Decipher block all got
numeric suffixes (`update3`, `final2`, `setAutoPadding2`, etc.)

## Fix

- Use `Object.assign` with object literals for prototype methods —
object literal property keys correctly infer `.name` and aren't subject
to the renamer
- Remove unnecessary block scopes around `Hash` and `Hmac` constructors
so they stay at module level and don't get renamed
- Inline `Cipheriv` methods and copy references to `Decipheriv`

## Tests

Added comprehensive `.name` tests for all crypto classes: Hash, Hmac,
Sign, Verify, Cipheriv, Decipheriv, DiffieHellman, ECDH, plus factory
functions and constructor names.
2026-02-10 23:06:22 -08:00
robobun
e8f73601c0 fix(compile): use remaining buffer in standalone POSIX write loop (#26882)
## What does this PR do?

Fixes the write loop in `StandaloneModuleGraph.inject()` for POSIX
targets (the `else` branch handling ELF/Linux standalone binaries) to
pass `remain` instead of `bytes` to `Syscall.write()`.

## Problem

The write loop that appends the bundled module graph to the end of the
executable uses a standard partial-write retry pattern, but passes the
full `bytes` buffer on every iteration instead of the remaining portion:

```zig
var remain = bytes;
while (remain.len > 0) {
    switch (Syscall.write(cloned_executable_fd, bytes)) {   // bug: should be 'remain'
        .result => |written| remain = remain[written..],
        ...
    }
}
```

If a partial write occurs, the next iteration re-writes from the start
of the buffer instead of continuing where it left off, corrupting the
output binary. The analogous read loop elsewhere in the same file
already correctly uses `remain`.

## Fix

One-character change: `bytes` → `remain` in the `Syscall.write()` call.

## How did you verify your code works?

- `bun bd` compiles successfully
- `bun bd test test/bundler/bun-build-compile.test.ts` — 4/4 pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alistair Smith <alistair@anthropic.com>
2026-02-10 23:04:46 -08:00
robobun
ba6e84fecd fix(compile): seek to start of file before EXDEV cross-device copy (#26883)
## What does this PR do?

Fixes `bun build --compile` producing an all-zeros binary when the
output directory is on a different filesystem than the temp directory.
This is common in Docker containers, Gitea runners, and other
environments using overlayfs.

## Problem

When `inject()` finishes writing the modified executable to the temp
file, the file descriptor's offset is at EOF. If the subsequent
`renameat()` to the output path fails with `EXDEV` (cross-device — the
temp file and output dir are on different filesystems), the code falls
back to `copyFileZSlowWithHandle()`, which:

1. Calls `fallocate()` to pre-allocate the output file to the correct
size (filled with zeros)
2. Calls `bun.copyFile(in_handle, out_handle)` — but `in_handle`'s
offset is at EOF
3. `copy_file_range` / `sendfile` / `read` all use the current file
offset (EOF), read 0 bytes, and return immediately
4. Result: output file is the correct size but entirely zeros

This explains user reports of `bun build --compile
--target=bun-darwin-arm64` producing invalid binaries that `file`
identifies as "data" rather than a Mach-O executable.

## Fix

Seek the input fd to offset 0 in `copyFileZSlowWithHandle` before
calling `bun.copyFile`.

## How did you verify your code works?

- `bun bd` compiles successfully
- `bun bd test test/bundler/bun-build-compile.test.ts` — 6/6 pass
- Added tests that verify compiled binaries have valid executable
headers and produce correct output
- Manually verified cross-compilation: `bun build --compile
--target=bun-darwin-arm64` produces a valid Mach-O binary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-10 22:32:31 -08:00
SUZUKI Sosuke
e29e830a25 perf(path): use pre-built Structure for path.parse() result objects (#26865)
## Summary

Optimize `path.parse()` by caching a pre-built JSC Structure for the
result object. Instead of creating a new empty object and performing 5
`putDirect` calls (each triggering a Structure transition), we now use
`constructEmptyObject` with the cached Structure and write values
directly via `putDirectOffset`.

## What changed

- **`ZigGlobalObject.h/cpp`**: Added `m_pathParsedObjectStructure` as a
`LazyPropertyOfGlobalObject<Structure>` with fixed property offsets for
`{root, dir, base, ext, name}`.
- **`Path.cpp`**: Added `PathParsedObject__create` extern "C" factory
function that constructs the object using the pre-built Structure and
`putDirectOffset`.
- **`path.zig`**: Replaced `toJSObject()` implementation to call the C++
factory function instead of `createEmptyObject` + 5x `.put()`.
- **`bench/snippets/path-parse.mjs`**: Added benchmark for
`path.parse()`.

This follows the same pattern used by `JSSocketAddressDTO` and
`m_jsonlParseResultStructure`.

## Benchmark results (Apple M4 Max)

| Benchmark | Before (1.3.9) | After | Speedup |
|---|---|---|---|
| `posix.parse("/home/user/dir/file.txt")` | 266.71 ns | 119.62 ns |
**2.23x** |
| `posix.parse("/home/user/dir/")` | 239.10 ns | 91.46 ns | **2.61x** |
| `posix.parse("file.txt")` | 232.55 ns | 89.20 ns | **2.61x** |
| `posix.parse("/root")` | 246.75 ns | 92.68 ns | **2.66x** |
| `posix.parse("")` | 152.19 ns | 20.72 ns | **7.34x** |
| `win32.parse("/home/user/dir/file.txt")` | 260.08 ns | 118.12 ns |
**2.20x** |
| `win32.parse("/home/user/dir/")` | 234.35 ns | 93.47 ns | **2.51x** |
| `win32.parse("file.txt")` | 224.19 ns | 80.56 ns | **2.78x** |
| `win32.parse("/root")` | 241.20 ns | 88.23 ns | **2.73x** |
| `win32.parse("")` | 160.39 ns | 24.20 ns | **6.63x** |

**~2.2x–2.8x faster** for typical paths, **~7x faster** for empty
strings.

## GC Safety

- `LazyPropertyOfGlobalObject<Structure>` is automatically visited via
`FOR_EACH_GLOBALOBJECT_GC_MEMBER`.
- JSValues created by `createUTF8ForJS` are protected by JSC's
conservative stack scanning during the factory function call.

## Test

All 116 existing path tests pass (`bun bd test test/js/node/path/`).

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-10 22:32:08 -08:00
39 changed files with 1993 additions and 215 deletions

View File

@@ -0,0 +1,18 @@
import { posix, win32 } from "path";
import { bench, run } from "../runner.mjs";
const paths = ["/home/user/dir/file.txt", "/home/user/dir/", "file.txt", "/root", ""];
paths.forEach(p => {
bench(`posix.parse(${JSON.stringify(p)})`, () => {
globalThis.abc = posix.parse(p);
});
});
paths.forEach(p => {
bench(`win32.parse(${JSON.stringify(p)})`, () => {
globalThis.abc = win32.parse(p);
});
});
await run();

View File

@@ -810,11 +810,19 @@ ssize_t bsd_write2(LIBUS_SOCKET_DESCRIPTOR fd, const char *header, int header_le
#else
ssize_t bsd_write2(LIBUS_SOCKET_DESCRIPTOR fd, const char *header, int header_length, const char *payload, int payload_length) {
ssize_t written = bsd_send(fd, header, header_length);
if (written < 0) {
return written;
}
if (written == header_length) {
ssize_t second_write = bsd_send(fd, payload, payload_length);
if (second_write > 0) {
written += second_write;
} else if (second_write < 0) {
/* First write succeeded but second failed with error.
* Return the header bytes written so the caller knows
* partial progress was made. */
}
/* If second_write == 0 (would-block), also just return header_length */
}
return written;
}

View File

@@ -495,6 +495,12 @@ void us_poll_change(struct us_poll_t *p, struct us_loop_t *loop, int events) {
}
}
/* On epoll/kqueue, force is the same as regular change since the kernel
* handles level/edge triggering correctly. */
void us_poll_change_force(struct us_poll_t *p, struct us_loop_t *loop, int events) {
us_poll_change(p, loop, events);
}
void us_poll_stop(struct us_poll_t *p, struct us_loop_t *loop) {
int old_events = us_poll_events(p);
int new_events = 0;

View File

@@ -115,6 +115,18 @@ void us_poll_change(struct us_poll_t *p, struct us_loop_t *loop, int events) {
}
}
/* Like us_poll_change, but always calls uv_poll_start even if events haven't changed.
* This is needed on Windows where WSAPoll may not reliably re-trigger writable events
* after a partial write without an explicit poll restart. */
void us_poll_change_force(struct us_poll_t *p, struct us_loop_t *loop, int events) {
if(!p->uv_p) return;
p->poll_type =
us_internal_poll_type(p) |
((events & LIBUS_SOCKET_READABLE) ? POLL_TYPE_POLLING_IN : 0) |
((events & LIBUS_SOCKET_WRITABLE) ? POLL_TYPE_POLLING_OUT : 0);
uv_poll_start(p->uv_p, events, poll_cb);
}
void us_poll_stop(struct us_poll_t *p, struct us_loop_t *loop) {
if(!p->uv_p) return;
uv_poll_stop(p->uv_p);

View File

@@ -399,6 +399,9 @@ void us_poll_start(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
/* Returns 0 if successful */
int us_poll_start_rc(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
void us_poll_change(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
/* Like us_poll_change but unconditionally restarts the poll even if events match.
* Needed on Windows where WSAPoll may miss writable events without explicit restart. */
void us_poll_change_force(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
void us_poll_stop(us_poll_r p, struct us_loop_t *loop) nonnull_fn_decl;
/* Return what events we are polling for */

View File

@@ -418,9 +418,11 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
if (!s->flags.last_write_failed || us_socket_is_shut_down(0, s)) {
us_poll_change(&s->p, loop, us_poll_events(&s->p) & LIBUS_SOCKET_READABLE);
} else {
#ifdef LIBUS_USE_KQUEUE
/* Kqueue one-shot writable needs to be re-registered */
us_poll_change(&s->p, loop, us_poll_events(&s->p) | LIBUS_SOCKET_WRITABLE);
#if defined(LIBUS_USE_KQUEUE) || defined(LIBUS_USE_LIBUV)
/* Kqueue: one-shot writable needs to be re-registered.
* Libuv/Windows: WSAPoll may not reliably deliver writable events
* after a partial write without re-registration. Force it. */
us_poll_change_force(&s->p, loop, us_poll_events(&s->p) | LIBUS_SOCKET_WRITABLE);
#endif
}
}
@@ -509,8 +511,8 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
if(s && s->flags.adopted && s->prev) {
s = s->prev;
}
// loop->num_ready_polls isn't accessible on Windows.
#ifndef WIN32
// loop->num_ready_polls isn't accessible on Windows.
// rare case: we're reading a lot of data, there's more to be read, and either:
// - the socket has hung up, so we will never get more data from it (only applies to macOS, as macOS will send the event the same tick but Linux will not.)
// - the event loop isn't very busy, so we can read multiple times in a row
@@ -529,6 +531,20 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
}
}
#undef LOOP_ISNT_VERY_BUSY_THRESHOLD
#else
// On Windows, we don't have num_ready_polls but we still need to
// drain available data to avoid requiring a full event loop iteration
// for each recv. This is critical for streaming performance.
if (
s && length >= (LIBUS_RECV_BUFFER_LENGTH - 24 * 1024) && length <= LIBUS_RECV_BUFFER_LENGTH &&
!us_socket_is_closed(0, s)
) {
repeat_recv_count++;
// Limit to 10 iterations to avoid starving other sockets
if (repeat_recv_count <= 10) {
continue;
}
}
#endif
} else if (!length) {
eof = 1; // lets handle EOF in the same place

View File

@@ -10,3 +10,295 @@ Conventions:
- Prefer `@import` at the **bottom** of the file, but the auto formatter will move them so you don't need to worry about it.
- **Never** use `@import()` inline inside of functions. **Always** put them at the bottom of the file or containing struct. Imports in Zig are free of side-effects, so there's no such thing as a "dynamic" import.
- You must be patient with the build.
## Prefer Bun APIs over `std`
**Always use `bun.*` APIs instead of `std.*`.** The `bun` namespace (`@import("bun")`) provides cross-platform wrappers that preserve OS error info and never use `unreachable`. Using `std.fs`, `std.posix`, or `std.os` directly is wrong in this codebase.
| Instead of | Use |
| ------------------------------------------------------------ | ------------------------------------ |
| `std.fs.File` | `bun.sys.File` |
| `std.fs.cwd()` | `bun.FD.cwd()` |
| `std.posix.open/read/write/stat/mkdir/unlink/rename/symlink` | `bun.sys.*` equivalents |
| `std.fs.path.join/dirname/basename` | `bun.path.join/dirname/basename` |
| `std.mem.eql/indexOf/startsWith` (for strings) | `bun.strings.eql/indexOf/startsWith` |
| `std.posix.O` / `std.posix.mode_t` / `std.posix.fd_t` | `bun.O` / `bun.Mode` / `bun.FD` |
| `std.process.Child` | `bun.spawnSync` |
| `catch bun.outOfMemory()` | `bun.handleOom(...)` |
## `bun.sys` — System Calls (`src/sys.zig`)
All return `Maybe(T)` — a tagged union of `.result: T` or `.err: bun.sys.Error`:
```zig
const fd = switch (bun.sys.open(path, bun.O.RDONLY, 0)) {
.result => |fd| fd,
.err => |err| return .{ .err = err },
};
// Or: const fd = try bun.sys.open(path, bun.O.RDONLY, 0).unwrap();
```
Key functions (all take `bun.FileDescriptor`, not `std.posix.fd_t`):
- `open`, `openat`, `openA` (non-sentinel) → `Maybe(bun.FileDescriptor)`
- `read`, `readAll`, `pread``Maybe(usize)`
- `write`, `pwrite`, `writev``Maybe(usize)`
- `stat`, `fstat`, `lstat``Maybe(bun.Stat)`
- `mkdir`, `unlink`, `rename`, `symlink`, `chmod`, `fchmod`, `fchown``Maybe(void)`
- `readlink`, `getFdPath`, `getcwd``Maybe` of path slice
- `getFileSize`, `dup`, `sendfile`, `mmap`
Use `bun.O.RDONLY`, `bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC`, etc. for open flags.
### `bun.sys.File` (`src/sys/File.zig`)
Higher-level file handle wrapping `bun.FileDescriptor`:
```zig
// One-shot read: open + read + close
const bytes = switch (bun.sys.File.readFrom(bun.FD.cwd(), path, allocator)) {
.result => |b| b,
.err => |err| return .{ .err = err },
};
// One-shot write: open + write + close
switch (bun.sys.File.writeFile(bun.FD.cwd(), path, data)) {
.result => {},
.err => |err| return .{ .err = err },
}
```
Key methods:
- `File.open/openat/makeOpen``Maybe(File)` (`makeOpen` creates parent dirs)
- `file.read/readAll/write/writeAll` — single or looped I/O
- `file.readToEnd(allocator)` — read entire file into allocated buffer
- `File.readFrom(dir_fd, path, allocator)` — open + read + close
- `File.writeFile(dir_fd, path, data)` — open + write + close
- `file.stat()`, `file.close()`, `file.writer()`, `file.reader()`
### `bun.FD` (`src/fd.zig`)
Cross-platform file descriptor. Use `bun.FD.cwd()` for cwd, `bun.invalid_fd` for sentinel, `fd.close()` to close.
### `bun.sys.Error` (`src/sys/Error.zig`)
Preserves errno, syscall tag, and file path. Convert to JS: `err.toSystemError().toErrorInstance(globalObject)`.
## `bun.strings` — String Utilities (`src/string/immutable.zig`)
SIMD-accelerated string operations. Use instead of `std.mem` for strings.
```zig
// Searching
strings.indexOf(haystack, needle) // ?usize
strings.contains(haystack, needle) // bool
strings.containsChar(haystack, char) // bool
strings.indexOfChar(haystack, char) // ?u32
strings.indexOfAny(str, comptime chars) // ?OptionalUsize (SIMD-accelerated)
// Comparison
strings.eql(a, b) // bool
strings.eqlComptime(str, comptime literal) // bool — optimized
strings.eqlCaseInsensitiveASCII(a, b, comptime true) // 3rd arg = check_len
// Prefix/Suffix
strings.startsWith(str, prefix) // bool
strings.endsWith(str, suffix) // bool
strings.hasPrefixComptime(str, comptime prefix) // bool — optimized
strings.hasSuffixComptime(str, comptime suffix) // bool — optimized
// Trimming
strings.trim(str, comptime chars) // strip from both ends
strings.trimSpaces(str) // strip whitespace
// Encoding conversions
strings.toUTF8Alloc(allocator, utf16) // ![]u8
strings.toUTF16Alloc(allocator, utf8) // !?[]u16
strings.toUTF8FromLatin1(allocator, latin1) // !?Managed(u8)
strings.firstNonASCII(slice) // ?u32
```
Bun handles UTF-8, Latin-1, and UTF-16/WTF-16 because JSC uses Latin-1 and UTF-16 internally. Latin-1 is NOT UTF-8 — bytes 128-255 are single chars in Latin-1 but invalid UTF-8.
### `bun.String` (`src/string.zig`)
Bridges Zig and JavaScriptCore. Prefer over `ZigString` in new code.
```zig
const s = bun.String.cloneUTF8(utf8_slice); // copies into WTFStringImpl
const s = bun.String.borrowUTF8(utf8_slice); // no copy, caller keeps alive
const utf8 = s.toUTF8(allocator); // ZigString.Slice
defer utf8.deinit();
const js_value = s.toJS(globalObject);
// Create a JS string value directly from UTF-8 bytes:
const js_str = try bun.String.createUTF8ForJS(globalObject, utf8_slice);
```
## `bun.path` — Path Manipulation (`src/resolver/resolve_path.zig`)
Use instead of `std.fs.path`. Platform param: `.auto` (current platform), `.posix`, `.windows`, `.loose` (both separators).
```zig
// Join paths — uses threadlocal buffer, result must be copied if it needs to persist
bun.path.join(&.{ dir, filename }, .auto)
bun.path.joinZ(&.{ dir, filename }, .auto) // null-terminated
// Join into a caller-provided buffer
bun.path.joinStringBuf(&buf, &.{ a, b }, .auto)
bun.path.joinStringBufZ(&buf, &.{ a, b }, .auto) // null-terminated
// Resolve against an absolute base (like Node.js path.resolve)
bun.path.joinAbsString(cwd, &.{ relative_path }, .auto)
bun.path.joinAbsStringBufZ(cwd, &buf, &.{ relative_path }, .auto)
// Path components
bun.path.dirname(path, .auto)
bun.path.basename(path)
// Relative path between two absolute paths
bun.path.relative(from, to)
bun.path.relativeAlloc(allocator, from, to)
// Normalize (resolve `.` and `..`)
bun.path.normalizeBuf(path, &buf, .auto)
// Null-terminate a path into a buffer
bun.path.z(path, &buf) // returns [:0]const u8
```
Use `bun.PathBuffer` for path buffers: `var buf: bun.PathBuffer = undefined;`
For pooled path buffers (avoids 64KB stack allocations on Windows):
```zig
const buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(buf);
```
## URL Parsing
Prefer `bun.jsc.URL` (WHATWG-compliant, backed by WebKit C++) over `bun.URL.parse` (internal, doesn't properly handle errors or invalid URLs).
```zig
// Parse a URL string (returns null if invalid)
const url = bun.jsc.URL.fromUTF8(href_string) orelse return error.InvalidURL;
defer url.deinit();
url.protocol() // bun.String
url.pathname() // bun.String
url.search() // bun.String
url.hash() // bun.String (includes leading '#')
url.port() // u32 (maxInt(u32) if not set, otherwise u16 range)
// NOTE: host/hostname are SWAPPED vs JS:
url.host() // hostname WITHOUT port (opposite of JS!)
url.hostname() // hostname WITH port (opposite of JS!)
// Normalize a URL string (percent-encode, punycode, etc.)
const normalized = bun.jsc.URL.hrefFromString(bun.String.borrowUTF8(input));
if (normalized.tag == .Dead) return error.InvalidURL;
defer normalized.deref();
// Join base + relative URLs
const joined = bun.jsc.URL.join(base_str, relative_str);
defer joined.deref();
// Convert between file paths and file:// URLs
const file_url = bun.jsc.URL.fileURLFromString(path_str); // path → file://
const file_path = bun.jsc.URL.pathFromFileURL(url_str); // file:// → path
```
## MIME Types (`src/http/MimeType.zig`)
```zig
const MimeType = bun.http.MimeType;
// Look up by file extension (without leading dot)
const mime = MimeType.byExtension("html"); // MimeType{ .value = "text/html", .category = .html }
const mime = MimeType.byExtensionNoDefault("xyz"); // ?MimeType (null if unknown)
// Category checks
mime.category // .javascript, .css, .html, .json, .image, .text, .wasm, .font, .video, .audio, ...
mime.category.isCode()
```
Common constants: `MimeType.javascript`, `MimeType.json`, `MimeType.html`, `MimeType.css`, `MimeType.text`, `MimeType.wasm`, `MimeType.ico`, `MimeType.other`.
## Memory & Allocators
**Use `bun.default_allocator` for almost everything.** It's backed by mimalloc.
`bun.handleOom(expr)` converts `error.OutOfMemory` into a crash without swallowing other errors:
```zig
const buf = bun.handleOom(allocator.alloc(u8, size)); // correct
// NOT: allocator.alloc(u8, size) catch bun.outOfMemory() — could swallow non-OOM errors
```
## Environment Variables (`src/env_var.zig`)
Type-safe, cached environment variable accessors via `bun.env_var`:
```zig
bun.env_var.HOME.get() // ?[]const u8
bun.env_var.CI.get() // ?bool
bun.env_var.BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS.get() // u64 (has default: 30)
```
## Logging (`src/output.zig`)
```zig
const log = bun.Output.scoped(.MY_FEATURE, .visible); // .hidden = opt-in via BUN_DEBUG_MY_FEATURE=1
log("processing {d} items", .{count});
// Color output (convenience wrappers auto-detect TTY):
bun.Output.pretty("<green>success<r>: {s}\n", .{msg});
bun.Output.prettyErrorln("<red>error<r>: {s}", .{msg});
```
## Spawning Subprocesses (`src/bun.js/api/bun/process.zig`)
Use `bun.spawnSync` instead of `std.process.Child`:
```zig
switch (bun.spawnSync(&.{
.argv = argv,
.envp = null, // inherit parent env
.cwd = cwd,
.stdout = .buffer, // capture
.stderr = .inherit, // pass through
.stdin = .ignore,
.windows = if (bun.Environment.isWindows) .{
.loop = bun.jsc.EventLoopHandle.init(bun.jsc.MiniEventLoop.initGlobal(env, null)),
},
}) catch return) {
.err => |err| { /* bun.sys.Error */ },
.result => |result| {
defer result.deinit();
const stdout = result.stdout.items;
if (result.status.isOK()) { ... }
},
}
```
Options: `argv: []const []const u8`, `envp: ?[*:null]?[*:0]const u8` (null = inherit), `argv0: ?[*:0]const u8`. Stdio: `.inherit`, `.ignore`, `.buffer`.
## Common Patterns
```zig
// Read a file
const contents = switch (bun.sys.File.readFrom(bun.FD.cwd(), path, allocator)) {
.result => |bytes| bytes,
.err => |err| { globalObject.throwValue(err.toSystemError().toErrorInstance(globalObject)); return .zero; },
};
// Create directories recursively
bun.makePath(dir.stdDir(), sub_path) catch |err| { ... };
// Hashing
bun.hash(bytes) // u64 — wyhash
bun.hash32(bytes) // u32
```

View File

@@ -935,7 +935,7 @@ pub const StandaloneModuleGraph = struct {
var remain = bytes;
while (remain.len > 0) {
switch (Syscall.write(cloned_executable_fd, bytes)) {
switch (Syscall.write(cloned_executable_fd, remain)) {
.result => |written| remain = remain[written..],
.err => |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to write to temporary file\n{f}", .{err});

View File

@@ -954,6 +954,7 @@ BUN_DEFINE_HOST_FUNCTION(jsFunctionBunPluginClear, (JSC::JSGlobalObject * global
global->onResolvePlugins.namespaces.clear();
delete global->onLoadPlugins.virtualModules;
global->onLoadPlugins.virtualModules = nullptr;
return JSC::JSValue::encode(JSC::jsUndefined());
}

View File

@@ -114,6 +114,25 @@ static JSC::JSObject* createPath(JSGlobalObject* globalThis, bool isWindows)
} // namespace Zig
extern "C" JSC::EncodedJSValue PathParsedObject__create(
JSC::JSGlobalObject* globalObject,
JSC::EncodedJSValue root,
JSC::EncodedJSValue dir,
JSC::EncodedJSValue base,
JSC::EncodedJSValue ext,
JSC::EncodedJSValue name)
{
auto* global = JSC::jsCast<Zig::GlobalObject*>(globalObject);
auto& vm = JSC::getVM(globalObject);
JSC::JSObject* result = JSC::constructEmptyObject(vm, global->pathParsedObjectStructure());
result->putDirectOffset(vm, 0, JSC::JSValue::decode(root));
result->putDirectOffset(vm, 1, JSC::JSValue::decode(dir));
result->putDirectOffset(vm, 2, JSC::JSValue::decode(base));
result->putDirectOffset(vm, 3, JSC::JSValue::decode(ext));
result->putDirectOffset(vm, 4, JSC::JSValue::decode(name));
return JSC::JSValue::encode(result);
}
namespace Bun {
JSC::JSValue createNodePathBinding(Zig::GlobalObject* globalObject)

View File

@@ -2067,6 +2067,30 @@ void GlobalObject::finishCreation(VM& vm)
init.set(structure);
});
this->m_pathParsedObjectStructure.initLater(
[](const Initializer<Structure>& init) {
// { root, dir, base, ext, name } — path.parse() result
Structure* structure = init.owner->structureCache().emptyObjectStructureForPrototype(
init.owner, init.owner->objectPrototype(), 5);
PropertyOffset offset;
structure = Structure::addPropertyTransition(init.vm, structure,
Identifier::fromString(init.vm, "root"_s), 0, offset);
RELEASE_ASSERT(offset == 0);
structure = Structure::addPropertyTransition(init.vm, structure,
Identifier::fromString(init.vm, "dir"_s), 0, offset);
RELEASE_ASSERT(offset == 1);
structure = Structure::addPropertyTransition(init.vm, structure,
Identifier::fromString(init.vm, "base"_s), 0, offset);
RELEASE_ASSERT(offset == 2);
structure = Structure::addPropertyTransition(init.vm, structure,
Identifier::fromString(init.vm, "ext"_s), 0, offset);
RELEASE_ASSERT(offset == 3);
structure = Structure::addPropertyTransition(init.vm, structure,
init.vm.propertyNames->name, 0, offset);
RELEASE_ASSERT(offset == 4);
init.set(structure);
});
this->m_pendingVirtualModuleResultStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::PendingVirtualModuleResult::createStructure(init.vm, init.owner, init.owner->objectPrototype()));

View File

@@ -567,6 +567,7 @@ public:
V(public, LazyClassStructure, m_JSHTTPParserClassStructure) \
\
V(private, LazyPropertyOfGlobalObject<Structure>, m_jsonlParseResultStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_pathParsedObjectStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_pendingVirtualModuleResultStructure) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_nativeMicrotaskTrampoline) \
@@ -702,6 +703,7 @@ public:
void reload();
JSC::Structure* jsonlParseResultStructure() { return m_jsonlParseResultStructure.get(this); }
JSC::Structure* pathParsedObjectStructure() { return m_pathParsedObjectStructure.get(this); }
JSC::Structure* pendingVirtualModuleResultStructure() { return m_pendingVirtualModuleResultStructure.get(this); }
// We need to know if the napi module registered itself or we registered it.

View File

@@ -5954,16 +5954,14 @@ ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalOb
auto* data = array->butterfly()->contiguous().data();
if (!containsHole(data, length)) {
size_t byteSize = sizeof(JSValue) * length;
Vector<uint8_t> buffer(byteSize, 0);
memcpy(buffer.mutableSpan().data(), data, byteSize);
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
return SerializedScriptValue::createInt32ArrayFastPath(WTF::move(buffer), length);
}
} else {
auto* data = array->butterfly()->contiguousDouble().data();
if (!containsHole(data, length)) {
size_t byteSize = sizeof(double) * length;
Vector<uint8_t> buffer(byteSize, 0);
memcpy(buffer.mutableSpan().data(), data, byteSize);
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
return SerializedScriptValue::createDoubleArrayFastPath(WTF::move(buffer), length);
}
}

View File

@@ -71,13 +71,12 @@ fn PathParsed(comptime T: type) type {
name: []const T = "",
pub fn toJSObject(this: @This(), globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
var jsObject = jsc.JSValue.createEmptyObject(globalObject, 5);
jsObject.put(globalObject, jsc.ZigString.static("root"), try bun.String.createUTF8ForJS(globalObject, this.root));
jsObject.put(globalObject, jsc.ZigString.static("dir"), try bun.String.createUTF8ForJS(globalObject, this.dir));
jsObject.put(globalObject, jsc.ZigString.static("base"), try bun.String.createUTF8ForJS(globalObject, this.base));
jsObject.put(globalObject, jsc.ZigString.static("ext"), try bun.String.createUTF8ForJS(globalObject, this.ext));
jsObject.put(globalObject, jsc.ZigString.static("name"), try bun.String.createUTF8ForJS(globalObject, this.name));
return jsObject;
const root = try bun.String.createUTF8ForJS(globalObject, this.root);
const dir = try bun.String.createUTF8ForJS(globalObject, this.dir);
const base = try bun.String.createUTF8ForJS(globalObject, this.base);
const ext = try bun.String.createUTF8ForJS(globalObject, this.ext);
const name_val = try bun.String.createUTF8ForJS(globalObject, this.name);
return PathParsedObject__create(globalObject, root, dir, base, ext, name_val);
}
};
}
@@ -2748,6 +2747,14 @@ pub fn resolveJS_T(comptime T: type, globalObject: *jsc.JSGlobalObject, allocato
}
extern "c" fn Process__getCachedCwd(*jsc.JSGlobalObject) jsc.JSValue;
extern "c" fn PathParsedObject__create(
*jsc.JSGlobalObject,
jsc.JSValue,
jsc.JSValue,
jsc.JSValue,
jsc.JSValue,
jsc.JSValue,
) jsc.JSValue;
pub fn resolve(globalObject: *jsc.JSGlobalObject, isWindows: bool, args_ptr: [*]jsc.JSValue, args_len: u16) bun.JSError!jsc.JSValue {
var arena = bun.ArenaAllocator.init(bun.default_allocator);

View File

@@ -948,6 +948,7 @@ pub const CommandLineReporter = struct {
this.printSummary();
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ this.jest.bail, if (this.jest.bail == 1) "" else "s" });
Output.flush();
this.writeJUnitReportIfNeeded();
Global.exit(1);
}
},
@@ -970,6 +971,20 @@ pub const CommandLineReporter = struct {
Output.printStartEnd(bun.start_time, std.time.nanoTimestamp());
}
/// Writes the JUnit reporter output file if a JUnit reporter is active and
/// an outfile path was configured. This must be called before any early exit
/// (e.g. bail) so that the report is not lost.
pub fn writeJUnitReportIfNeeded(this: *CommandLineReporter) void {
if (this.reporters.junit) |junit| {
if (this.jest.test_options.reporter_outfile) |outfile| {
if (junit.current_file.len > 0) {
junit.endTestSuite() catch {};
}
junit.writeToFile(outfile) catch {};
}
}
}
pub fn generateCodeCoverage(this: *CommandLineReporter, vm: *jsc.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool) !void {
if (comptime !reporters.text and !reporters.lcov) {
return;
@@ -1772,12 +1787,7 @@ pub const TestCommand = struct {
Output.prettyError("\n", .{});
Output.flush();
if (reporter.reporters.junit) |junit| {
if (junit.current_file.len > 0) {
junit.endTestSuite() catch {};
}
junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {};
}
reporter.writeJUnitReportIfNeeded();
if (vm.hot_reload == .watch) {
vm.runWithAPILock(jsc.VirtualMachine, vm, runEventLoopForWatch);
@@ -1920,6 +1930,7 @@ pub const TestCommand = struct {
if (reporter.jest.bail == reporter.summary().fail) {
reporter.printSummary();
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ reporter.jest.bail, if (reporter.jest.bail == 1) "" else "s" });
reporter.writeJUnitReportIfNeeded();
vm.exit_handler.exit_code = 1;
vm.is_shutting_down = true;

View File

@@ -27,6 +27,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
ping_frame_bytes: [128 + 6]u8 = [_]u8{0} ** (128 + 6),
ping_len: u8 = 0,
ping_received: bool = false,
pong_received: bool = false,
close_received: bool = false,
close_frame_buffering: bool = false,
@@ -120,6 +121,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
this.clearReceiveBuffers(true);
this.clearSendBuffers(true);
this.ping_received = false;
this.pong_received = false;
this.ping_len = 0;
this.close_frame_buffering = false;
this.receive_pending_chunk_len = 0;
@@ -136,6 +138,10 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
// Set to null FIRST to prevent re-entrancy (shutdown can trigger callbacks)
if (this.proxy_tunnel) |tunnel| {
this.proxy_tunnel = null;
// Detach the websocket from the tunnel before shutdown so the
// tunnel's onClose callback doesn't dispatch a spurious 1006
// after we've already handled a clean close.
tunnel.clearConnectedWebSocket();
tunnel.shutdown();
tunnel.deref();
}
@@ -650,14 +656,38 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
if (data.len == 0) break;
},
.pong => {
const pong_len = @min(data.len, @min(receive_body_remain, this.ping_frame_bytes.len));
if (!this.pong_received) {
if (receive_body_remain > 125) {
this.terminate(ErrorCode.invalid_control_frame);
terminated = true;
break;
}
this.ping_len = @truncate(receive_body_remain);
receive_body_remain = 0;
this.pong_received = true;
}
const pong_len = this.ping_len;
this.dispatchData(data[0..pong_len], .Pong);
if (data.len > 0) {
const total_received = @min(pong_len, receive_body_remain + data.len);
const slice = this.ping_frame_bytes[6..][receive_body_remain..total_received];
@memcpy(slice, data[0..slice.len]);
receive_body_remain = total_received;
data = data[slice.len..];
}
const pending_body = pong_len - receive_body_remain;
if (pending_body > 0) {
// wait for more data - pong payload is fragmented across TCP segments
break;
}
const pong_data = this.ping_frame_bytes[6..][0..pong_len];
this.dispatchData(pong_data, .Pong);
data = data[pong_len..];
receive_state = .need_header;
receive_body_remain = 0;
receiving_type = last_receive_data_type;
this.pong_received = false;
if (data.len == 0) break;
},
@@ -884,7 +914,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
}
fn sendPong(this: *WebSocket, socket: Socket) bool {
if (socket.isClosed() or socket.isShutdown()) {
if (!this.hasTCP()) {
this.dispatchAbruptClose(ErrorCode.ended);
return false;
}
@@ -916,14 +946,17 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
body_len: usize,
) void {
log("Sending close with code {d}", .{code});
if (socket.isClosed() or socket.isShutdown()) {
if (!this.hasTCP()) {
this.dispatchAbruptClose(ErrorCode.ended);
this.clearData();
return;
}
// we dont wanna shutdownRead when SSL, because SSL handshake can happen when writting
// For tunnel mode, shutdownRead on the detached socket is a no-op; skip it.
if (comptime !ssl) {
socket.shutdownRead();
if (this.proxy_tunnel == null) {
socket.shutdownRead();
}
}
var final_body_bytes: [128 + 8]u8 = undefined;
var header = @as(WebsocketHeader, @bitCast(@as(u16, 0)));

View File

@@ -253,6 +253,13 @@ pub fn setConnectedWebSocket(this: *WebSocketProxyTunnel, ws: *WebSocketClient)
this.#upgrade_client = .{ .none = {} };
}
/// Clear the connected WebSocket reference. Called before tunnel shutdown during
/// a clean close so the tunnel's onClose callback doesn't dispatch a spurious
/// abrupt close (1006) after the WebSocket has already sent a clean close frame.
pub fn clearConnectedWebSocket(this: *WebSocketProxyTunnel) void {
this.#connected_websocket = null;
}
/// SSLWrapper callback: Called with encrypted data to send to network
fn writeEncrypted(this: *WebSocketProxyTunnel, encrypted_data: []const u8) void {
log("writeEncrypted: {} bytes", .{encrypted_data.len});

View File

@@ -291,25 +291,32 @@ pub const Parser = struct {
}
},
else => {
try unesc.appendSlice(switch (bun.strings.utf8ByteSequenceLength(c)) {
1 => brk: {
break :brk &[_]u8{ '\\', c };
switch (bun.strings.utf8ByteSequenceLength(c)) {
0, 1 => try unesc.appendSlice(&[_]u8{ '\\', c }),
2 => if (val.len - i >= 2) {
try unesc.appendSlice(&[_]u8{ '\\', c, val[i + 1] });
i += 1;
} else {
try unesc.appendSlice(&[_]u8{ '\\', c });
},
2 => brk: {
defer i += 1;
break :brk &[_]u8{ '\\', c, val[i + 1] };
3 => if (val.len - i >= 3) {
try unesc.appendSlice(&[_]u8{ '\\', c, val[i + 1], val[i + 2] });
i += 2;
} else {
try unesc.append('\\');
try unesc.appendSlice(val[i..val.len]);
i = val.len - 1;
},
3 => brk: {
defer i += 2;
break :brk &[_]u8{ '\\', c, val[i + 1], val[i + 2] };
4 => if (val.len - i >= 4) {
try unesc.appendSlice(&[_]u8{ '\\', c, val[i + 1], val[i + 2], val[i + 3] });
i += 3;
} else {
try unesc.append('\\');
try unesc.appendSlice(val[i..val.len]);
i = val.len - 1;
},
4 => brk: {
defer i += 3;
break :brk &[_]u8{ '\\', c, val[i + 1], val[i + 2], val[i + 3] };
},
// this means invalid utf8
else => unreachable,
});
}
},
}
@@ -342,25 +349,30 @@ pub const Parser = struct {
try unesc.append('.');
}
},
else => try unesc.appendSlice(switch (bun.strings.utf8ByteSequenceLength(c)) {
1 => brk: {
break :brk &[_]u8{c};
else => switch (bun.strings.utf8ByteSequenceLength(c)) {
0, 1 => try unesc.append(c),
2 => if (val.len - i >= 2) {
try unesc.appendSlice(&[_]u8{ c, val[i + 1] });
i += 1;
} else {
try unesc.append(c);
},
2 => brk: {
defer i += 1;
break :brk &[_]u8{ c, val[i + 1] };
3 => if (val.len - i >= 3) {
try unesc.appendSlice(&[_]u8{ c, val[i + 1], val[i + 2] });
i += 2;
} else {
try unesc.appendSlice(val[i..val.len]);
i = val.len - 1;
},
3 => brk: {
defer i += 2;
break :brk &[_]u8{ c, val[i + 1], val[i + 2] };
4 => if (val.len - i >= 4) {
try unesc.appendSlice(&[_]u8{ c, val[i + 1], val[i + 2], val[i + 3] });
i += 3;
} else {
try unesc.appendSlice(val[i..val.len]);
i = val.len - 1;
},
4 => brk: {
defer i += 3;
break :brk &[_]u8{ c, val[i + 1], val[i + 2], val[i + 3] };
},
// this means invalid utf8
else => unreachable,
}),
},
}
}

View File

@@ -199,18 +199,18 @@ function Sign(algorithm, options): void {
}
$toClass(Sign, "Sign", Writable);
Sign.prototype._write = function _write(chunk, encoding, callback) {
this.update(chunk, encoding);
callback();
};
Sign.prototype.update = function update(data, encoding) {
return this[kHandle].update(this, data, encoding);
};
Sign.prototype.sign = function sign(options, encoding) {
return this[kHandle].sign(options, encoding);
};
Object.assign(Sign.prototype, {
_write: function (chunk, encoding, callback) {
this.update(chunk, encoding);
callback();
},
update: function (data, encoding) {
return this[kHandle].update(this, data, encoding);
},
sign: function (options, encoding) {
return this[kHandle].sign(options, encoding);
},
});
crypto_exports.Sign = Sign;
crypto_exports.sign = sign;
@@ -237,9 +237,11 @@ $toClass(Verify, "Verify", Writable);
Verify.prototype._write = Sign.prototype._write;
Verify.prototype.update = Sign.prototype.update;
Verify.prototype.verify = function verify(options, signature, sigEncoding) {
return this[kHandle].verify(options, signature, sigEncoding);
};
Object.assign(Verify.prototype, {
verify: function (options, signature, sigEncoding) {
return this[kHandle].verify(options, signature, sigEncoding);
},
});
crypto_exports.Verify = Verify;
crypto_exports.verify = verify;
@@ -250,82 +252,76 @@ function createVerify(algorithm, options?) {
crypto_exports.createVerify = createVerify;
{
function Hash(algorithm, options?): void {
if (!new.target) {
return new Hash(algorithm, options);
}
const handle = new _Hash(algorithm, options);
this[kHandle] = handle;
LazyTransform.$apply(this, [options]);
function Hash(algorithm, options?): void {
if (!new.target) {
return new Hash(algorithm, options);
}
$toClass(Hash, "Hash", LazyTransform);
Hash.prototype.copy = function copy(options) {
const handle = new _Hash(algorithm, options);
this[kHandle] = handle;
LazyTransform.$apply(this, [options]);
}
$toClass(Hash, "Hash", LazyTransform);
Object.assign(Hash.prototype, {
copy: function (options) {
return new Hash(this[kHandle], options);
};
Hash.prototype._transform = function _transform(chunk, encoding, callback) {
},
_transform: function (chunk, encoding, callback) {
this[kHandle].update(this, chunk, encoding);
callback();
};
Hash.prototype._flush = function _flush(callback) {
},
_flush: function (callback) {
this.push(this[kHandle].digest(null, false));
callback();
};
Hash.prototype.update = function update(data, encoding) {
},
update: function (data, encoding) {
return this[kHandle].update(this, data, encoding);
};
Hash.prototype.digest = function digest(outputEncoding) {
},
digest: function (outputEncoding) {
return this[kHandle].digest(outputEncoding);
};
},
});
crypto_exports.Hash = Hash;
crypto_exports.createHash = function createHash(algorithm, options) {
return new Hash(algorithm, options);
};
}
crypto_exports.Hash = Hash;
crypto_exports.createHash = function createHash(algorithm, options) {
return new Hash(algorithm, options);
};
{
function Hmac(hmac, key, options?): void {
if (!new.target) {
return new Hmac(hmac, key, options);
}
const handle = new _Hmac(hmac, key, options);
this[kHandle] = handle;
LazyTransform.$apply(this, [options]);
function Hmac(hmac, key, options?): void {
if (!new.target) {
return new Hmac(hmac, key, options);
}
$toClass(Hmac, "Hmac", LazyTransform);
Hmac.prototype.update = function update(data, encoding) {
const handle = new _Hmac(hmac, key, options);
this[kHandle] = handle;
LazyTransform.$apply(this, [options]);
}
$toClass(Hmac, "Hmac", LazyTransform);
Object.assign(Hmac.prototype, {
update: function (data, encoding) {
return this[kHandle].update(this, data, encoding);
};
Hmac.prototype.digest = function digest(outputEncoding) {
},
digest: function (outputEncoding) {
return this[kHandle].digest(outputEncoding);
};
Hmac.prototype._transform = function _transform(chunk, encoding, callback) {
},
_transform: function (chunk, encoding, callback) {
this[kHandle].update(this, chunk, encoding);
callback();
};
Hmac.prototype._flush = function _flush(callback) {
},
_flush: function (callback) {
this.push(this[kHandle].digest());
callback();
};
},
});
crypto_exports.Hmac = Hmac;
crypto_exports.createHmac = function createHmac(hmac, key, options) {
return new Hmac(hmac, key, options);
};
}
crypto_exports.Hmac = Hmac;
crypto_exports.createHmac = function createHmac(hmac, key, options) {
return new Hmac(hmac, key, options);
};
crypto_exports.getHashes = getHashes;
@@ -390,62 +386,6 @@ crypto_exports.createECDH = function createECDH(curve) {
return decoder;
}
function setAutoPadding(ap) {
this[kHandle].setAutoPadding(ap);
return this;
}
function getAuthTag() {
return this[kHandle].getAuthTag();
}
function setAuthTag(tagbuf, encoding) {
this[kHandle].setAuthTag(tagbuf, encoding);
return this;
}
function setAAD(aadbuf, options) {
this[kHandle].setAAD(aadbuf, options);
return this;
}
function _transform(chunk, encoding, callback) {
this.push(this[kHandle].update(chunk, encoding));
callback();
}
function _flush(callback) {
try {
this.push(this[kHandle].final());
} catch (e) {
callback(e);
return;
}
callback();
}
function update(data, inputEncoding, outputEncoding) {
const res = this[kHandle].update(data, inputEncoding);
if (outputEncoding && outputEncoding !== "buffer") {
this._decoder = getDecoder(this._decoder, outputEncoding);
return this._decoder.write(res);
}
return res;
}
function final(outputEncoding) {
const res = this[kHandle].final();
if (outputEncoding && outputEncoding !== "buffer") {
this._decoder = getDecoder(this._decoder, outputEncoding);
return this._decoder.end(res);
}
return res;
}
function Cipheriv(cipher, key, iv, options): void {
if (!new.target) {
return new Cipheriv(cipher, key, iv, options);
@@ -457,13 +397,52 @@ crypto_exports.createECDH = function createECDH(curve) {
}
$toClass(Cipheriv, "Cipheriv", LazyTransform);
Cipheriv.prototype.setAutoPadding = setAutoPadding;
Cipheriv.prototype.getAuthTag = getAuthTag;
Cipheriv.prototype.setAAD = setAAD;
Cipheriv.prototype._transform = _transform;
Cipheriv.prototype._flush = _flush;
Cipheriv.prototype.update = update;
Cipheriv.prototype.final = final;
Object.assign(Cipheriv.prototype, {
setAutoPadding: function (ap) {
this[kHandle].setAutoPadding(ap);
return this;
},
getAuthTag: function () {
return this[kHandle].getAuthTag();
},
setAAD: function (aadbuf, options) {
this[kHandle].setAAD(aadbuf, options);
return this;
},
_transform: function (chunk, encoding, callback) {
this.push(this[kHandle].update(chunk, encoding));
callback();
},
_flush: function (callback) {
try {
this.push(this[kHandle].final());
} catch (e) {
callback(e);
return;
}
callback();
},
update: function (data, inputEncoding, outputEncoding) {
const res = this[kHandle].update(data, inputEncoding);
if (outputEncoding && outputEncoding !== "buffer") {
this._decoder = getDecoder(this._decoder, outputEncoding);
return this._decoder.write(res);
}
return res;
},
final: function (outputEncoding) {
const res = this[kHandle].final();
if (outputEncoding && outputEncoding !== "buffer") {
this._decoder = getDecoder(this._decoder, outputEncoding);
return this._decoder.end(res);
}
return res;
},
});
function Decipheriv(cipher, key, iv, options): void {
if (!new.target) {
@@ -476,13 +455,18 @@ crypto_exports.createECDH = function createECDH(curve) {
}
$toClass(Decipheriv, "Decipheriv", LazyTransform);
Decipheriv.prototype.setAutoPadding = setAutoPadding;
Decipheriv.prototype.setAuthTag = setAuthTag;
Decipheriv.prototype.setAAD = setAAD;
Decipheriv.prototype._transform = _transform;
Decipheriv.prototype._flush = _flush;
Decipheriv.prototype.update = update;
Decipheriv.prototype.final = final;
Object.assign(Decipheriv.prototype, {
setAutoPadding: Cipheriv.prototype.setAutoPadding,
setAuthTag: function (tagbuf, encoding) {
this[kHandle].setAuthTag(tagbuf, encoding);
return this;
},
setAAD: Cipheriv.prototype.setAAD,
_transform: Cipheriv.prototype._transform,
_flush: Cipheriv.prototype._flush,
update: Cipheriv.prototype.update,
final: Cipheriv.prototype.final,
});
crypto_exports.Cipheriv = Cipheriv;
crypto_exports.Decipheriv = Decipheriv;

View File

@@ -460,13 +460,13 @@ pub const Archiver = struct {
if (comptime Environment.isWindows) {
try bun.MakePath.makePath(u16, dir, path);
} else {
std.posix.mkdiratZ(dir_fd, pathname, @intCast(mode)) catch |err| {
std.posix.mkdiratZ(dir_fd, path, @intCast(mode)) catch |err| {
// It's possible for some tarballs to return a directory twice, with and
// without `./` in the beginning. So if it already exists, continue to the
// next entry.
if (err == error.PathAlreadyExists or err == error.NotDir) continue;
bun.makePath(dir, std.fs.path.dirname(path_slice) orelse return err) catch {};
std.posix.mkdiratZ(dir_fd, pathname, 0o777) catch {};
std.posix.mkdiratZ(dir_fd, path, 0o777) catch {};
};
}
},

View File

@@ -222,7 +222,7 @@ pub const Linker = struct {
if (comptime is_bun) {
// make these happen at runtime
if (import_record.kind == .require or import_record.kind == .require_resolve) {
if (import_record.kind == .require or import_record.kind == .require_resolve or import_record.kind == .dynamic) {
return false;
}
}

View File

@@ -927,7 +927,9 @@ pub const Log = struct {
err: anyerror,
) OOM!void {
@branchHint(.cold);
return try addResolveErrorWithLevel(log, source, r, allocator, fmt, args, import_kind, false, .err, err);
// Always dupe the line_text from the source to ensure the Location data
// outlives the source's backing memory (which may be arena-allocated).
return try addResolveErrorWithLevel(log, source, r, allocator, fmt, args, import_kind, true, .err, err);
}
pub fn addResolveErrorWithTextDupe(

View File

@@ -221,7 +221,11 @@ pub const S3Credentials = struct {
defer str.deref();
if (str.tag != .Empty and str.tag != .Dead) {
new_credentials._contentDispositionSlice = str.toUTF8(bun.default_allocator);
new_credentials.content_disposition = new_credentials._contentDispositionSlice.?.slice();
const slice = new_credentials._contentDispositionSlice.?.slice();
if (containsNewlineOrCR(slice)) {
return globalObject.throwInvalidArguments("contentDisposition must not contain newline characters (CR/LF)", .{});
}
new_credentials.content_disposition = slice;
}
} else {
return globalObject.throwInvalidArgumentTypeValue("contentDisposition", "string", js_value);
@@ -236,7 +240,11 @@ pub const S3Credentials = struct {
defer str.deref();
if (str.tag != .Empty and str.tag != .Dead) {
new_credentials._contentTypeSlice = str.toUTF8(bun.default_allocator);
new_credentials.content_type = new_credentials._contentTypeSlice.?.slice();
const slice = new_credentials._contentTypeSlice.?.slice();
if (containsNewlineOrCR(slice)) {
return globalObject.throwInvalidArguments("type must not contain newline characters (CR/LF)", .{});
}
new_credentials.content_type = slice;
}
} else {
return globalObject.throwInvalidArgumentTypeValue("type", "string", js_value);
@@ -251,7 +259,11 @@ pub const S3Credentials = struct {
defer str.deref();
if (str.tag != .Empty and str.tag != .Dead) {
new_credentials._contentEncodingSlice = str.toUTF8(bun.default_allocator);
new_credentials.content_encoding = new_credentials._contentEncodingSlice.?.slice();
const slice = new_credentials._contentEncodingSlice.?.slice();
if (containsNewlineOrCR(slice)) {
return globalObject.throwInvalidArguments("contentEncoding must not contain newline characters (CR/LF)", .{});
}
new_credentials.content_encoding = slice;
}
} else {
return globalObject.throwInvalidArgumentTypeValue("contentEncoding", "string", js_value);
@@ -1150,6 +1162,12 @@ const CanonicalRequest = struct {
}
};
/// Returns true if the given slice contains any CR (\r) or LF (\n) characters,
/// which would allow HTTP header injection if used in a header value.
fn containsNewlineOrCR(value: []const u8) bool {
return strings.indexOfAny(value, "\r\n") != null;
}
const std = @import("std");
const ACL = @import("./acl.zig").ACL;
const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions;

View File

@@ -1154,7 +1154,7 @@ pub const Interpreter = struct {
_ = callframe; // autofix
if (this.setupIOBeforeRun().asErr()) |e| {
defer this.#deinitFromExec();
defer this.#derefRootShellAndIOIfNeeded(true);
const shellerr = bun.shell.ShellErr.newSys(e);
return try throwShellErr(&shellerr, .{ .js = globalThis.bunVM().event_loop });
}

View File

@@ -422,6 +422,19 @@ pub fn createInstance(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFra
break :brk b.allocatedSlice();
};
// Reject null bytes in connection parameters to prevent protocol injection
// (null bytes act as field terminators in the MySQL wire protocol).
inline for (.{ .{ username, "username" }, .{ password, "password" }, .{ database, "database" }, .{ path, "path" } }) |entry| {
if (entry[0].len > 0 and std.mem.indexOfScalar(u8, entry[0], 0) != null) {
bun.default_allocator.free(options_buf);
tls_config.deinit();
if (tls_ctx) |tls| {
tls.deinit(true);
}
return globalObject.throwInvalidArguments(entry[1] ++ " must not contain null bytes", .{});
}
}
const on_connect = arguments[9];
const on_close = arguments[10];
const idle_timeout = arguments[11].toInt32();

View File

@@ -680,6 +680,20 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
break :brk b.allocatedSlice();
};
// Reject null bytes in connection parameters to prevent Postgres startup
// message parameter injection (null bytes act as field terminators in the
// wire protocol's key\0value\0 format).
inline for (.{ .{ username, "username" }, .{ password, "password" }, .{ database, "database" }, .{ path, "path" } }) |entry| {
if (entry[0].len > 0 and std.mem.indexOfScalar(u8, entry[0], 0) != null) {
bun.default_allocator.free(options_buf);
tls_config.deinit();
if (tls_ctx) |tls| {
tls.deinit(true);
}
return globalObject.throwInvalidArguments(entry[1] ++ " must not contain null bytes", .{});
}
}
const on_connect = arguments[9];
const on_close = arguments[10];
const idle_timeout = arguments[11].toInt32();
@@ -1626,7 +1640,10 @@ pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.enum_litera
// This will usually start with "v="
const comparison_signature = final.data.slice();
if (comparison_signature.len < 2 or !bun.strings.eqlLong(server_signature, comparison_signature[2..], true)) {
if (comparison_signature.len < 2 or
server_signature.len != comparison_signature.len - 2 or
BoringSSL.c.CRYPTO_memcmp(server_signature.ptr, comparison_signature[2..].ptr, server_signature.len) != 0)
{
debug("SASLFinal - SASL Server signature mismatch\nExpected: {s}\nActual: {s}", .{ server_signature, comparison_signature[2..] });
this.fail("The server did not return the correct signature", error.SASL_SIGNATURE_MISMATCH);
} else {

View File

@@ -4092,6 +4092,12 @@ pub fn copyFileZSlowWithHandle(in_handle: bun.FileDescriptor, to_dir: bun.FileDe
_ = std.os.linux.fallocate(out_handle.cast(), 0, 0, @intCast(stat_.size));
}
// Seek input to beginning — the caller may have written to this fd,
// leaving the file offset at EOF. copy_file_range / sendfile / read
// all use the current offset when called with null offsets.
// Ignore errors: the fd may be non-seekable (e.g. a pipe).
_ = setFileOffset(in_handle, 0);
switch (bun.copyFile(in_handle, out_handle)) {
.err => |e| return .{ .err = e },
.result => {},

View File

@@ -260,14 +260,35 @@ devTest("hmr handles rapid consecutive edits", {
await Bun.sleep(1);
}
// Wait event-driven for "render 10" to appear. Intermediate renders may
// be skipped (watcher coalescing) and the final render may fire multiple
// times (duplicate reloads), so we just listen for any occurrence.
const finalRender = "render 10";
while (true) {
const message = await client.getStringMessage();
if (message === finalRender) break;
if (typeof message === "string" && message.includes("HMR_ERROR")) {
throw new Error("Unexpected HMR error message: " + message);
}
}
await new Promise<void>((resolve, reject) => {
const check = () => {
for (const msg of client.messages) {
if (typeof msg === "string" && msg.includes("HMR_ERROR")) {
cleanup();
reject(new Error("Unexpected HMR error message: " + msg));
return;
}
if (msg === finalRender) {
cleanup();
resolve();
return;
}
}
};
const cleanup = () => {
client.off("message", check);
};
client.on("message", check);
// Check messages already buffered.
check();
});
// Drain all buffered messages — intermediate renders and possible
// duplicates of the final render are expected and harmless.
client.messages.length = 0;
const hmrErrors = await client.js`return globalThis.__hmrErrors ? [...globalThis.__hmrErrors] : [];`;
if (hmrErrors.length > 0) {

View File

@@ -121,4 +121,71 @@ describe("Bun.build compile", () => {
});
});
describe("compiled binary validity", () => {
test("output binary has valid executable header", async () => {
using dir = tempDir("build-compile-valid-header", {
"app.js": `console.log("hello");`,
});
const outfile = join(dir + "", "app-out");
const result = await Bun.build({
entrypoints: [join(dir + "", "app.js")],
compile: {
outfile,
},
});
expect(result.success).toBe(true);
// Read the first 4 bytes and verify it's a valid executable magic number
const file = Bun.file(result.outputs[0].path);
const header = new Uint8Array(await file.slice(0, 4).arrayBuffer());
if (isMacOS) {
// MachO magic: 0xCFFAEDFE (little-endian)
expect(header[0]).toBe(0xcf);
expect(header[1]).toBe(0xfa);
expect(header[2]).toBe(0xed);
expect(header[3]).toBe(0xfe);
} else if (isLinux) {
// ELF magic: 0x7F 'E' 'L' 'F'
expect(header[0]).toBe(0x7f);
expect(header[1]).toBe(0x45); // 'E'
expect(header[2]).toBe(0x4c); // 'L'
expect(header[3]).toBe(0x46); // 'F'
} else if (isWindows) {
// PE magic: 'M' 'Z'
expect(header[0]).toBe(0x4d); // 'M'
expect(header[1]).toBe(0x5a); // 'Z'
}
});
test("compiled binary runs and produces expected output", async () => {
using dir = tempDir("build-compile-runs", {
"app.js": `console.log("compile-test-output");`,
});
const outfile = join(dir + "", "app-run");
const result = await Bun.build({
entrypoints: [join(dir + "", "app.js")],
compile: {
outfile,
},
});
expect(result.success).toBe(true);
await using proc = Bun.spawn({
cmd: [result.outputs[0].path],
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("compile-test-output");
expect(exitCode).toBe(0);
});
});
// file command test works well

View File

@@ -611,6 +611,82 @@ describe("Bun.Archive", () => {
// Very deep paths might fail on some systems - that's acceptable
}
});
test("directory entries with path traversal components cannot escape extraction root", async () => {
// Manually craft a tar archive containing directory entries with "../" traversal
// components in their pathnames. This tests that the extraction code uses the
// normalized path (which strips "..") rather than the raw pathname from the tarball.
function createTarHeader(
name: string,
size: number,
type: "0" | "5", // 0=file, 5=directory
): Uint8Array {
const header = new Uint8Array(512);
const enc = new TextEncoder();
header.set(enc.encode(name).slice(0, 100), 0);
header.set(enc.encode(type === "5" ? "0000755 " : "0000644 "), 100);
header.set(enc.encode("0000000 "), 108);
header.set(enc.encode("0000000 "), 116);
header.set(enc.encode(size.toString(8).padStart(11, "0") + " "), 124);
const mtime = Math.floor(Date.now() / 1000)
.toString(8)
.padStart(11, "0");
header.set(enc.encode(mtime + " "), 136);
header.set(enc.encode(" "), 148); // checksum placeholder
header[156] = type.charCodeAt(0);
header.set(enc.encode("ustar"), 257);
header[262] = 0;
header.set(enc.encode("00"), 263);
let checksum = 0;
for (let i = 0; i < 512; i++) checksum += header[i];
header.set(enc.encode(checksum.toString(8).padStart(6, "0") + "\0 "), 148);
return header;
}
const blocks: Uint8Array[] = [];
const enc = new TextEncoder();
// A legitimate directory
blocks.push(createTarHeader("safe_dir/", 0, "5"));
// A directory entry with traversal: "safe_dir/../../escaped_dir/"
// After normalization this becomes "escaped_dir" (safe),
// but the raw pathname resolves ".." via the kernel in mkdirat.
blocks.push(createTarHeader("safe_dir/../../escaped_dir/", 0, "5"));
// A normal file
const content = enc.encode("hello");
blocks.push(createTarHeader("safe_dir/file.txt", content.length, "0"));
blocks.push(content);
const pad = 512 - (content.length % 512);
if (pad < 512) blocks.push(new Uint8Array(pad));
// End-of-archive markers
blocks.push(new Uint8Array(1024));
const totalLen = blocks.reduce((s, b) => s + b.length, 0);
const tarball = new Uint8Array(totalLen);
let offset = 0;
for (const b of blocks) {
tarball.set(b, offset);
offset += b.length;
}
// Create a parent directory so we can check if "escaped_dir" appears outside extractDir
using parentDir = tempDir("archive-traversal-parent", {});
const extractPath = join(String(parentDir), "extract");
const { mkdirSync, existsSync } = require("fs");
mkdirSync(extractPath, { recursive: true });
const archive = new Bun.Archive(tarball);
await archive.extract(extractPath);
// The "escaped_dir" should NOT exist in the parent directory (outside extraction root)
const escapedOutside = join(String(parentDir), "escaped_dir");
expect(existsSync(escapedOutside)).toBe(false);
// The "safe_dir" should exist inside the extraction directory
expect(existsSync(join(extractPath, "safe_dir"))).toBe(true);
// The normalized "escaped_dir" may or may not exist inside extractPath
// (depending on whether normalization keeps it), but it must NOT be outside
});
});
describe("Archive.write()", () => {

View File

@@ -489,6 +489,61 @@ brr = 3
"zr": ["deedee"],
});
});
describe("truncated/invalid utf-8", () => {
test("bare continuation byte (0x80) should not crash", () => {
// 0x80 is a continuation byte without a leading byte
// utf8ByteSequenceLength returns 0, which must not hit unreachable
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0x80])]).toString("latin1");
// Should not crash - just parse gracefully
expect(() => parse(ini)).not.toThrow();
});
test("truncated 2-byte sequence at end of value", () => {
// 0xC0 is a 2-byte lead byte, but there's no continuation byte following
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xc0])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("truncated 3-byte sequence at end of value", () => {
// 0xE0 is a 3-byte lead byte, but only 0 continuation bytes follow
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xe0])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("truncated 3-byte sequence with 1 continuation byte at end", () => {
// 0xE0 is a 3-byte lead byte, but only 1 continuation byte follows
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xe0, 0x80])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("truncated 4-byte sequence at end of value", () => {
// 0xF0 is a 4-byte lead byte, but only 0 continuation bytes follow
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xf0])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("truncated 4-byte sequence with 1 continuation byte at end", () => {
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xf0, 0x80])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("truncated 4-byte sequence with 2 continuation bytes at end", () => {
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xf0, 0x80, 0x80])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("truncated 2-byte sequence in escaped context", () => {
// Backslash followed by a 2-byte lead byte at end of value
const ini = Buffer.concat([Buffer.from("key = \\"), Buffer.from([0xc0])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
test("bare continuation byte in escaped context", () => {
const ini = Buffer.concat([Buffer.from("key = \\"), Buffer.from([0x80])]).toString("latin1");
expect(() => parse(ini)).not.toThrow();
});
});
});
const wtf = {

View File

@@ -504,6 +504,110 @@ describe("Hash", () => {
expect(hash.update.name).toBe("update");
expect(hash.digest.name).toBe("digest");
expect(hash.copy.name).toBe("copy");
expect(hash._transform.name).toBe("_transform");
expect(hash._flush.name).toBe("_flush");
});
});
describe("Hmac", () => {
it("should have correct method names", () => {
const hmac = crypto.createHmac("sha256", "key");
expect(hmac.update.name).toBe("update");
expect(hmac.digest.name).toBe("digest");
expect(hmac._transform.name).toBe("_transform");
expect(hmac._flush.name).toBe("_flush");
});
});
describe("Sign", () => {
it("should have correct method names", () => {
const sign = crypto.createSign("sha256");
expect(sign.update.name).toBe("update");
expect(sign.sign.name).toBe("sign");
expect(sign._write.name).toBe("_write");
});
});
describe("Verify", () => {
it("should have correct method names", () => {
const verify = crypto.createVerify("sha256");
expect(verify.update.name).toBe("update");
expect(verify.verify.name).toBe("verify");
expect(verify._write.name).toBe("_write");
});
});
describe("Cipheriv", () => {
it("should have correct method names", () => {
const cipher = crypto.createCipheriv("aes-256-cbc", Buffer.alloc(32), Buffer.alloc(16));
expect(cipher.update.name).toBe("update");
expect(cipher.final.name).toBe("final");
expect(cipher.setAutoPadding.name).toBe("setAutoPadding");
expect(cipher.getAuthTag.name).toBe("getAuthTag");
expect(cipher.setAAD.name).toBe("setAAD");
expect(cipher._transform.name).toBe("_transform");
expect(cipher._flush.name).toBe("_flush");
});
});
describe("Decipheriv", () => {
it("should have correct method names", () => {
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.alloc(32), Buffer.alloc(16));
expect(decipher.update.name).toBe("update");
expect(decipher.final.name).toBe("final");
expect(decipher.setAutoPadding.name).toBe("setAutoPadding");
expect(decipher.setAuthTag.name).toBe("setAuthTag");
expect(decipher.setAAD.name).toBe("setAAD");
expect(decipher._transform.name).toBe("_transform");
expect(decipher._flush.name).toBe("_flush");
});
});
describe("DiffieHellman", () => {
it("should have correct method names", () => {
const dh = crypto.createDiffieHellman(512);
expect(dh.generateKeys.name).toBe("generateKeys");
expect(dh.computeSecret.name).toBe("computeSecret");
expect(dh.getPrime.name).toBe("getPrime");
expect(dh.getGenerator.name).toBe("getGenerator");
expect(dh.getPublicKey.name).toBe("getPublicKey");
expect(dh.getPrivateKey.name).toBe("getPrivateKey");
expect(dh.setPublicKey.name).toBe("setPublicKey");
expect(dh.setPrivateKey.name).toBe("setPrivateKey");
});
});
describe("ECDH", () => {
it("should have correct method names", () => {
const ecdh = crypto.createECDH("prime256v1");
expect(ecdh.generateKeys.name).toBe("generateKeys");
expect(ecdh.computeSecret.name).toBe("computeSecret");
expect(ecdh.getPublicKey.name).toBe("getPublicKey");
expect(ecdh.getPrivateKey.name).toBe("getPrivateKey");
expect(ecdh.setPublicKey.name).toBe("setPublicKey");
expect(ecdh.setPrivateKey.name).toBe("setPrivateKey");
});
});
describe("crypto module", () => {
it("should have correct factory function names", () => {
expect(crypto.createHash.name).toBe("createHash");
expect(crypto.createHmac.name).toBe("createHmac");
expect(crypto.createSign.name).toBe("createSign");
expect(crypto.createVerify.name).toBe("createVerify");
expect(crypto.createCipheriv.name).toBe("createCipheriv");
expect(crypto.createDecipheriv.name).toBe("createDecipheriv");
expect(crypto.createDiffieHellman.name).toBe("createDiffieHellman");
expect(crypto.createECDH.name).toBe("createECDH");
expect(crypto.hash.name).toBe("hash");
expect(crypto.pbkdf2.name).toBe("pbkdf2");
});
it("should have correct constructor names", () => {
expect(crypto.Hash.name).toBe("Hash");
expect(crypto.Hmac.name).toBe("Hmac");
expect(crypto.Sign.name).toBe("Sign");
expect(crypto.Verify.name).toBe("Verify");
});
});

View File

@@ -0,0 +1,222 @@
import { TCPSocketListener } from "bun";
import { describe, expect, test } from "bun:test";
const hostname = "127.0.0.1";
const MAX_HEADER_SIZE = 16 * 1024;
function doHandshake(
socket: any,
handshakeBuffer: Uint8Array,
data: Uint8Array,
): { buffer: Uint8Array; done: boolean } {
const newBuffer = new Uint8Array(handshakeBuffer.length + data.length);
newBuffer.set(handshakeBuffer);
newBuffer.set(data, handshakeBuffer.length);
if (newBuffer.length > MAX_HEADER_SIZE) {
socket.end();
throw new Error("Handshake headers too large");
}
const dataStr = new TextDecoder("utf-8").decode(newBuffer);
const endOfHeaders = dataStr.indexOf("\r\n\r\n");
if (endOfHeaders === -1) {
return { buffer: newBuffer, done: false };
}
if (!dataStr.startsWith("GET")) {
throw new Error("Invalid handshake");
}
const magic = /Sec-WebSocket-Key:\s*(.*)\r\n/i.exec(dataStr);
if (!magic) {
throw new Error("Missing Sec-WebSocket-Key");
}
const hasher = new Bun.CryptoHasher("sha1");
hasher.update(magic[1].trim());
hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
const accept = hasher.digest("base64");
socket.write(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
`Sec-WebSocket-Accept: ${accept}\r\n` +
"\r\n",
);
socket.flush();
return { buffer: newBuffer, done: true };
}
function makeTextFrame(text: string): Uint8Array {
const payload = new TextEncoder().encode(text);
const len = payload.length;
let header: Uint8Array;
if (len < 126) {
header = new Uint8Array([0x81, len]);
} else if (len < 65536) {
header = new Uint8Array([0x81, 126, (len >> 8) & 0xff, len & 0xff]);
} else {
throw new Error("Message too large for this test");
}
const frame = new Uint8Array(header.length + len);
frame.set(header);
frame.set(payload, header.length);
return frame;
}
describe("WebSocket", () => {
test("fragmented pong frame does not cause frame desync", async () => {
let server: TCPSocketListener | undefined;
let client: WebSocket | undefined;
let handshakeBuffer = new Uint8Array(0);
let handshakeComplete = false;
try {
const { promise, resolve, reject } = Promise.withResolvers<void>();
server = Bun.listen({
socket: {
data(socket, data) {
if (handshakeComplete) {
// After handshake, we just receive client frames (like close) - ignore them
return;
}
const result = doHandshake(socket, handshakeBuffer, new Uint8Array(data));
handshakeBuffer = result.buffer;
if (!result.done) return;
handshakeComplete = true;
// Build a pong frame with a 50-byte payload, but deliver it in two parts.
// Pong opcode = 0x8A, FIN=1
const pongPayload = new Uint8Array(50);
for (let i = 0; i < 50; i++) pongPayload[i] = 0x41 + (i % 26); // 'A'-'Z' repeated
const pongFrame = new Uint8Array(2 + 50);
pongFrame[0] = 0x8a; // FIN + Pong opcode
pongFrame[1] = 50; // payload length
pongFrame.set(pongPayload, 2);
// Part 1 of pong: header (2 bytes) + first 2 bytes of payload = 4 bytes
// This leaves 48 bytes of pong payload undelivered.
const pongPart1 = pongFrame.slice(0, 4);
// Part 2: remaining 48 bytes of pong payload
const pongPart2 = pongFrame.slice(4);
// A text message to send after the pong completes.
const textFrame = makeTextFrame("hello after pong");
// Send part 1 of pong
socket.write(pongPart1);
socket.flush();
// After a delay, send part 2 of pong + the follow-up text message
setTimeout(() => {
// Concatenate part2 + text frame to simulate them arriving together
const combined = new Uint8Array(pongPart2.length + textFrame.length);
combined.set(pongPart2);
combined.set(textFrame, pongPart2.length);
socket.write(combined);
socket.flush();
}, 50);
},
},
hostname,
port: 0,
});
const messages: string[] = [];
client = new WebSocket(`ws://${server.hostname}:${server.port}`);
client.addEventListener("error", event => {
reject(new Error("WebSocket error"));
});
client.addEventListener("close", event => {
// If the connection closes unexpectedly due to frame desync, the test should fail
reject(new Error(`WebSocket closed unexpectedly: code=${event.code} reason=${event.reason}`));
});
client.addEventListener("message", event => {
messages.push(event.data as string);
if (messages.length === 1) {
// We got the text message after the fragmented pong
try {
expect(messages[0]).toBe("hello after pong");
resolve();
} catch (err) {
reject(err);
}
}
});
await promise;
} finally {
client?.close();
server?.stop(true);
}
});
test("pong frame with payload > 125 bytes is rejected", async () => {
let server: TCPSocketListener | undefined;
let client: WebSocket | undefined;
let handshakeBuffer = new Uint8Array(0);
let handshakeComplete = false;
try {
const { promise, resolve, reject } = Promise.withResolvers<void>();
server = Bun.listen({
socket: {
data(socket, data) {
if (handshakeComplete) return;
const result = doHandshake(socket, handshakeBuffer, new Uint8Array(data));
handshakeBuffer = result.buffer;
if (!result.done) return;
handshakeComplete = true;
// Send a pong frame with a 126-byte payload (invalid per RFC 6455 Section 5.5)
// Control frames MUST have a payload length of 125 bytes or less.
// Use 2-byte extended length encoding since 126 > 125.
// But actually, the 7-bit length field in byte[1] can encode 0-125 directly.
// For 126, the server must use the extended 16-bit length.
// However, control frames with >125 payload are invalid regardless of encoding.
const pongFrame = new Uint8Array(4 + 126);
pongFrame[0] = 0x8a; // FIN + Pong
pongFrame[1] = 126; // Signals 16-bit extended length follows
pongFrame[2] = 0; // High byte of length
pongFrame[3] = 126; // Low byte of length = 126
// Fill payload with arbitrary data
for (let i = 0; i < 126; i++) pongFrame[4 + i] = 0x42;
socket.write(pongFrame);
socket.flush();
},
},
hostname,
port: 0,
});
client = new WebSocket(`ws://${server.hostname}:${server.port}`);
client.addEventListener("error", () => {
// Expected - the connection should error due to invalid control frame
resolve();
});
client.addEventListener("close", () => {
// Also acceptable - connection closes due to protocol error
resolve();
});
client.addEventListener("message", () => {
reject(new Error("Should not receive a message from an invalid pong frame"));
});
await promise;
} finally {
client?.close();
server?.stop(true);
}
});
});

View File

@@ -398,6 +398,71 @@ describe("WebSocket wss:// through HTTP proxy (TLS tunnel)", () => {
expect(messages).toContain("hello via tls tunnel");
gc();
});
test("server-initiated ping survives through TLS tunnel proxy", async () => {
// Regression test: sendPong checked socket.isClosed() on the detached tcp
// field instead of using hasTCP(). For wss:// through HTTP proxy, the
// WebSocket uses initWithTunnel which sets tcp = detached (all I/O goes
// through proxy_tunnel). Detached sockets return true for isClosed(), so
// sendPong would immediately dispatch a 1006 close instead of sending the
// pong through the tunnel.
using pingServer = Bun.serve({
port: 0,
tls: {
key: tlsCerts.key,
cert: tlsCerts.cert,
},
fetch(req, server) {
if (server.upgrade(req)) return;
return new Response("Expected WebSocket", { status: 400 });
},
websocket: {
message(ws, message) {
if (String(message) === "ready") {
// Send a ping after the client confirms it's connected.
// On the buggy code path, this triggers sendPong on the detached
// socket → dispatchAbruptClose → 1006.
ws.ping();
// Follow up with a text message. If the client receives this,
// the connection survived the ping/pong exchange.
ws.send("after-ping");
}
},
},
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
const ws = new WebSocket(`wss://127.0.0.1:${pingServer.port}`, {
proxy: `http://127.0.0.1:${proxyPort}`,
tls: { rejectUnauthorized: false },
});
ws.onopen = () => {
ws.send("ready");
};
ws.onmessage = event => {
if (String(event.data) === "after-ping") {
ws.close(1000);
}
};
ws.onclose = event => {
if (event.code === 1000) {
resolve();
} else {
reject(new Error(`Unexpected close code: ${event.code}`));
}
};
ws.onerror = event => {
reject(event);
};
await promise;
gc();
});
});
describe("WebSocket through HTTPS proxy (TLS proxy)", () => {

View File

@@ -0,0 +1,113 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/25707
// Dynamic import() of non-existent node: modules inside CJS files should not
// fail at transpile/require time. They should be deferred to runtime so that
// try/catch can handle the error gracefully.
test("require() of CJS file containing dynamic import of non-existent node: module does not fail at load time", async () => {
using dir = tempDir("issue-25707", {
// Simulates turbopack-generated chunks: a CJS module with a factory function
// containing import("node:sqlite") inside a try/catch that is never called
// during require().
"chunk.js": `
module.exports = [
function factory(exports) {
async function detect(e) {
if ("createSession" in e) {
let c;
try {
({DatabaseSync: c} = await import("node:sqlite"))
} catch(a) {
if (null !== a && "object" == typeof a && "code" in a && "ERR_UNKNOWN_BUILTIN_MODULE" !== a.code)
throw a;
}
}
}
exports.detect = detect;
}
];
`,
"main.js": `
// This require() should not fail even though chunk.js contains import("node:sqlite")
const factories = require("./chunk.js");
console.log("loaded " + factories.length + " factories");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("loaded 1 factories");
expect(exitCode).toBe(0);
});
test("require() of CJS file with bare dynamic import of non-existent node: module does not fail at load time", async () => {
// The dynamic import is NOT inside a try/catch, but it's still a dynamic import
// that should only be resolved at runtime, not at transpile time
using dir = tempDir("issue-25707-bare", {
"lib.js": `
module.exports = async function() {
const { DatabaseSync } = await import("node:sqlite");
return DatabaseSync;
};
`,
"main.js": `
const fn = require("./lib.js");
console.log("loaded");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("loaded");
expect(exitCode).toBe(0);
});
test("dynamic import of non-existent node: module in CJS rejects at runtime with correct error", async () => {
using dir = tempDir("issue-25707-runtime", {
"lib.js": `
module.exports = async function() {
try {
const { DatabaseSync } = await import("node:sqlite");
return "resolved";
} catch (e) {
return "caught: " + e.code;
}
};
`,
"main.js": `
const fn = require("./lib.js");
fn().then(result => console.log(result));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("caught: ERR_UNKNOWN_BUILTIN_MODULE");
expect(exitCode).toBe(0);
});

View File

@@ -0,0 +1,77 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
test("--bail writes JUnit reporter outfile", async () => {
using dir = tempDir("bail-junit", {
"fail.test.ts": `
import { test, expect } from "bun:test";
test("failing test", () => { expect(1).toBe(2); });
`,
});
const outfile = join(String(dir), "results.xml");
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--bail", "--reporter=junit", `--reporter-outfile=${outfile}`, "fail.test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
// The test should fail and bail
expect(exitCode).not.toBe(0);
// The JUnit report file should still be written despite bail
const file = Bun.file(outfile);
expect(await file.exists()).toBe(true);
const xml = await file.text();
expect(xml).toContain("<?xml");
expect(xml).toContain("<testsuites");
expect(xml).toContain("</testsuites>");
expect(xml).toContain("failing test");
});
test("--bail writes JUnit reporter outfile with multiple files", async () => {
using dir = tempDir("bail-junit-multi", {
"a_pass.test.ts": `
import { test, expect } from "bun:test";
test("passing test", () => { expect(1).toBe(1); });
`,
"b_fail.test.ts": `
import { test, expect } from "bun:test";
test("another failing test", () => { expect(1).toBe(2); });
`,
});
const outfile = join(String(dir), "results.xml");
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--bail", "--reporter=junit", `--reporter-outfile=${outfile}`],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
// The test should fail and bail
expect(exitCode).not.toBe(0);
// The JUnit report file should still be written despite bail
const file = Bun.file(outfile);
expect(await file.exists()).toBe(true);
const xml = await file.text();
expect(xml).toContain("<?xml");
expect(xml).toContain("<testsuites");
expect(xml).toContain("</testsuites>");
// Both the passing and failing tests should be recorded
expect(xml).toContain("passing test");
expect(xml).toContain("another failing test");
});

View File

@@ -0,0 +1,134 @@
import { expect, test } from "bun:test";
import http from "node:http";
// Regression test for https://github.com/oven-sh/bun/issues/27010
// HTTP requests hanging on Windows when making multiple concurrent large
// streaming GET requests using the Node.js http module.
test("multiple concurrent streaming HTTP requests complete without hanging", async () => {
const TWO_MIB = 2 * 1024 * 1024;
const CHUNK_SIZE = 64 * 1024;
const ZERO_CHUNK = new Uint8Array(CHUNK_SIZE);
// Start a streaming HTTP server
using server = Bun.serve({
port: 0,
fetch(req) {
const url = new URL(req.url);
if (url.pathname !== "/stream") {
return new Response("OK");
}
let sent = 0;
const stream = new ReadableStream({
pull: async controller => {
const remaining = TWO_MIB - sent;
if (remaining <= 0) {
controller.close();
return;
}
// Small delay to simulate realistic streaming
await new Promise(resolve => setTimeout(resolve, 1));
const n = Math.min(remaining, ZERO_CHUNK.byteLength);
controller.enqueue(n === ZERO_CHUNK.byteLength ? ZERO_CHUNK : ZERO_CHUNK.subarray(0, n));
sent += n;
},
});
return new Response(stream, {
headers: {
"content-type": "application/octet-stream",
"cache-control": "no-store",
},
});
},
});
const url = `http://localhost:${server.port}/stream`;
const NUM_WORKERS = 3;
const NUM_ITERATIONS = 2;
function downloadOnce(): Promise<number> {
return new Promise((resolve, reject) => {
const req = http.get(url, res => {
let total = 0;
res.on("data", (chunk: Buffer) => {
total += chunk.length;
});
res.on("end", () => {
resolve(total);
});
res.on("error", (err: Error) => {
reject(err);
});
});
req.on("error", (err: Error) => {
reject(err);
});
});
}
async function workerLoop(): Promise<void> {
for (let iter = 0; iter < NUM_ITERATIONS; iter++) {
const bytes = await downloadOnce();
expect(bytes).toBe(TWO_MIB);
}
}
const promises: Promise<void>[] = [];
for (let w = 0; w < NUM_WORKERS; w++) {
promises.push(workerLoop());
}
await Promise.all(promises);
}, 30_000);
test("streaming HTTP response delivers all chunks via node:http", async () => {
const TOTAL_SIZE = 512 * 1024; // 512KB
const CHUNK_SIZE = 16 * 1024; // 16KB chunks
using server = Bun.serve({
port: 0,
fetch(req) {
let sent = 0;
const stream = new ReadableStream({
pull: async controller => {
if (sent >= TOTAL_SIZE) {
controller.close();
return;
}
// Create a chunk with incrementing byte pattern for verification
const n = Math.min(TOTAL_SIZE - sent, CHUNK_SIZE);
const chunk = new Uint8Array(n);
chunk.fill((sent / CHUNK_SIZE) & 0xff);
controller.enqueue(chunk);
sent += n;
},
});
return new Response(stream);
},
});
const url = `http://localhost:${server.port}/`;
const result = await new Promise<Buffer>((resolve, reject) => {
const req = http.get(url, res => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => {
chunks.push(chunk);
});
res.on("end", () => {
resolve(Buffer.concat(chunks));
});
res.on("error", reject);
});
req.on("error", reject);
});
expect(result.length).toBe(TOTAL_SIZE);
// Verify chunk pattern integrity
for (let i = 0; i < TOTAL_SIZE / CHUNK_SIZE; i++) {
const offset = i * CHUNK_SIZE;
const expectedByte = i & 0xff;
expect(result[offset]).toBe(expectedByte);
expect(result[offset + CHUNK_SIZE - 1]).toBe(expectedByte);
}
});

View File

@@ -0,0 +1,187 @@
import { SQL } from "bun";
import { expect, test } from "bun:test";
import net from "net";
test("postgres connection rejects null bytes in username", async () => {
let serverReceivedData = false;
const server = net.createServer(socket => {
serverReceivedData = true;
socket.destroy();
});
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
const port = (server.address() as net.AddressInfo).port;
try {
const sql = new SQL({
hostname: "127.0.0.1",
port,
username: "alice\x00search_path\x00evil_schema,public",
database: "testdb",
max: 1,
idleTimeout: 1,
connectionTimeout: 2,
});
await sql`SELECT 1`;
expect.unreachable();
} catch (e: any) {
expect(e.message).toContain("null bytes");
} finally {
server.close();
}
// The server should never have received any data because the null byte
// should be rejected before the connection is established.
expect(serverReceivedData).toBe(false);
});
test("postgres connection rejects null bytes in database", async () => {
let serverReceivedData = false;
const server = net.createServer(socket => {
serverReceivedData = true;
socket.destroy();
});
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
const port = (server.address() as net.AddressInfo).port;
try {
const sql = new SQL({
hostname: "127.0.0.1",
port,
username: "alice",
database: "testdb\x00search_path\x00evil_schema,public",
max: 1,
idleTimeout: 1,
connectionTimeout: 2,
});
await sql`SELECT 1`;
expect.unreachable();
} catch (e: any) {
expect(e.message).toContain("null bytes");
} finally {
server.close();
}
expect(serverReceivedData).toBe(false);
});
test("postgres connection rejects null bytes in password", async () => {
let serverReceivedData = false;
const server = net.createServer(socket => {
serverReceivedData = true;
socket.destroy();
});
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
const port = (server.address() as net.AddressInfo).port;
try {
const sql = new SQL({
hostname: "127.0.0.1",
port,
username: "alice",
password: "pass\x00search_path\x00evil_schema",
database: "testdb",
max: 1,
idleTimeout: 1,
connectionTimeout: 2,
});
await sql`SELECT 1`;
expect.unreachable();
} catch (e: any) {
expect(e.message).toContain("null bytes");
} finally {
server.close();
}
expect(serverReceivedData).toBe(false);
});
test("postgres connection does not use truncated path with null bytes", async () => {
// The JS layer's fs.existsSync() rejects paths containing null bytes,
// so the path is dropped before reaching the native layer. Verify that a
// path with null bytes doesn't silently connect via a truncated path.
let serverReceivedData = false;
const server = net.createServer(socket => {
serverReceivedData = true;
socket.destroy();
});
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
const port = (server.address() as net.AddressInfo).port;
try {
const sql = new SQL({
hostname: "127.0.0.1",
port,
username: "alice",
database: "testdb",
path: "/tmp\x00injected",
max: 1,
idleTimeout: 1,
connectionTimeout: 2,
});
await sql`SELECT 1`;
} catch {
// Expected to fail
} finally {
server.close();
}
// The path had null bytes so it should have been dropped by the JS layer,
// falling back to TCP where it hits our mock server (not a truncated Unix socket).
expect(serverReceivedData).toBe(true);
});
test("postgres connection works with normal parameters (no null bytes)", async () => {
// Verify that normal connections without null bytes still work.
// Use a mock server that sends an auth error so we can verify the
// startup message is sent correctly.
let receivedData = false;
const server = net.createServer(socket => {
socket.once("data", () => {
receivedData = true;
const errMsg = Buffer.from("SFATAL\0VFATAL\0C28000\0Mauthentication failed\0\0");
const len = errMsg.length + 4;
const header = Buffer.alloc(5);
header.write("E", 0);
header.writeInt32BE(len, 1);
socket.write(Buffer.concat([header, errMsg]));
socket.destroy();
});
});
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
const port = (server.address() as net.AddressInfo).port;
try {
const sql = new SQL({
hostname: "127.0.0.1",
port,
username: "alice",
database: "testdb",
max: 1,
idleTimeout: 1,
connectionTimeout: 2,
});
await sql`SELECT 1`;
} catch {
// Expected - mock server sends auth error
} finally {
server.close();
}
// Normal parameters should connect fine - the server should receive data
expect(receivedData).toBe(true);
});

View File

@@ -0,0 +1,148 @@
import { S3Client } from "bun";
import { describe, expect, test } from "bun:test";
// Test that CRLF characters in S3 options are rejected to prevent header injection.
// See: HTTP Header Injection via S3 Content-Disposition Value
describe("S3 header injection prevention", () => {
test("contentDisposition with CRLF should throw", () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK", { status: 200 });
},
});
const client = new S3Client({
accessKeyId: "test-key",
secretAccessKey: "test-secret",
endpoint: server.url.href,
bucket: "test-bucket",
});
expect(() =>
client.write("test-file.txt", "Hello", {
contentDisposition: 'attachment; filename="evil"\r\nX-Injected: value',
}),
).toThrow(/CR\/LF/);
});
test("contentEncoding with CRLF should throw", () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK", { status: 200 });
},
});
const client = new S3Client({
accessKeyId: "test-key",
secretAccessKey: "test-secret",
endpoint: server.url.href,
bucket: "test-bucket",
});
expect(() =>
client.write("test-file.txt", "Hello", {
contentEncoding: "gzip\r\nX-Injected: value",
}),
).toThrow(/CR\/LF/);
});
test("type (content-type) with CRLF should throw", () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK", { status: 200 });
},
});
const client = new S3Client({
accessKeyId: "test-key",
secretAccessKey: "test-secret",
endpoint: server.url.href,
bucket: "test-bucket",
});
expect(() =>
client.write("test-file.txt", "Hello", {
type: "text/plain\r\nX-Injected: value",
}),
).toThrow(/CR\/LF/);
});
test("contentDisposition with only CR should throw", () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK", { status: 200 });
},
});
const client = new S3Client({
accessKeyId: "test-key",
secretAccessKey: "test-secret",
endpoint: server.url.href,
bucket: "test-bucket",
});
expect(() =>
client.write("test-file.txt", "Hello", {
contentDisposition: "attachment\rinjected",
}),
).toThrow(/CR\/LF/);
});
test("contentDisposition with only LF should throw", () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK", { status: 200 });
},
});
const client = new S3Client({
accessKeyId: "test-key",
secretAccessKey: "test-secret",
endpoint: server.url.href,
bucket: "test-bucket",
});
expect(() =>
client.write("test-file.txt", "Hello", {
contentDisposition: "attachment\ninjected",
}),
).toThrow(/CR\/LF/);
});
test("valid contentDisposition without CRLF should not throw", async () => {
const { promise: requestReceived, resolve: onRequestReceived } = Promise.withResolvers<Headers>();
using server = Bun.serve({
port: 0,
async fetch(req) {
onRequestReceived(req.headers);
return new Response("OK", { status: 200 });
},
});
const client = new S3Client({
accessKeyId: "test-key",
secretAccessKey: "test-secret",
endpoint: server.url.href,
bucket: "test-bucket",
});
// Valid content-disposition values should not throw synchronously.
// The write may eventually fail because the mock server doesn't speak S3 protocol,
// but the option parsing should succeed and a request should be initiated.
expect(() =>
client.write("test-file.txt", "Hello", {
contentDisposition: 'attachment; filename="report.pdf"',
}),
).not.toThrow();
const receivedHeaders = await requestReceived;
expect(receivedHeaders.get("content-disposition")).toBe('attachment; filename="report.pdf"');
});
});