Compare commits

..

23 Commits

Author SHA1 Message Date
Claude Bot
24b4559f86 fix(stacktrace): use Interpreter::getStackTrace for async continuation frames
Replace the manual StackVisitor walk in Error.captureStackTrace with
JSC's Interpreter::getStackTrace, which properly handles async
continuation frames (frames from functions suspended at await points
higher up the async call chain).

The previous implementation manually walked the synchronous call stack
using StackVisitor::visit(), which missed async continuation frames.
This caused issues with libraries like NX that use
Error.captureStackTrace with custom Error.prepareStackTrace to detect
recursion by inspecting CallSite objects in async call chains.

Closes #25695

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 00:14:38 +00:00
Jarred Sumner
337a9f7f2b fix(module): prevent crash when resolving bun:main before entry_po… (#27027)
…int.generate()

`ServerEntryPoint.source` defaults to `undefined`, and accessing its
`.contents` or `.path.text` fields before `generate()` has been called
causes a segfault. This happens when `bun:main` is resolved in contexts
where `entry_point.generate()` is skipped (HTML entry points) or never
called (test runner).

Add a `generated` flag to `ServerEntryPoint` and guard both access
sites:
- `getHardcodedModule()` in ModuleLoader.zig (returns null instead of
crashing)
- `_resolve()` in VirtualMachine.zig (falls through to normal
resolution)

### What does this PR do?

### How did you verify your code works?

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-14 12:11:41 -08:00
robobun
38f41dccdf fix(crypto): fix segfault in X509Certificate.issuerCertificate getter (#27025) 2026-02-14 05:58:41 -08:00
robobun
883e43c371 update(crypto): update root certificates to NSS 3.119; eliminate external HTTPS in tests (#27020) 2026-02-14 02:47:13 -08:00
Dylan Conway
cd17934207 fix(stripANSI): handle SIMD false positives for non-ANSI control chars (#27021)
The SIMD fast path in `findEscapeCharacter` matches bytes in `0x10-0x1F`
and `0x90-0x9F`, but `consumeANSI` only handles actual ANSI escape
introducers (`0x1b`, `0x9b`, `0x9d`, `0x90`, `0x98`, `0x9e`, `0x9f`).
For non-ANSI bytes like `0x16` (SYN), `consumeANSI` returns the same
pointer it received, causing an infinite loop in release builds and an
assertion failure in debug builds.

When a SIMD false positive is detected (`consumeANSI` returns the same
pointer), the byte is preserved in the output and scanning advances past
it.

Fixes #27014
2026-02-14 02:01:25 -08:00
robobun
f6d4ff6779 fix(http): validate statusMessage for CRLF to prevent HTTP response splitting (#26949)
## Summary

- Fixes HTTP response splitting vulnerability where `res.statusMessage`
could contain CRLF characters that were written directly to the socket,
allowing injection of arbitrary HTTP headers and response body
- Adds native-layer validation in `NodeHTTPResponse.zig` `writeHead()`
to reject status messages containing control characters (matching
Node.js's `checkInvalidHeaderChar` behavior)
- The `writeHead(code, msg)` API already validated via JS-side
`checkInvalidHeaderChar`, but direct property assignment
(`res.statusMessage = userInput`) followed by `res.end()` or
`res.write()` bypassed all validation

## Test plan

- [x] Verified vulnerability is reproducible: attacker can inject
`Set-Cookie` headers via `res.statusMessage = "OK\r\nSet-Cookie:
admin=true"`
- [x] Verified fix throws `ERR_INVALID_CHAR` TypeError when CRLF is
present in status message
- [x] Added 4 new tests covering: property assignment + `res.end()`,
property assignment + `res.write()`, explicit `writeHead()` rejection,
and valid status message passthrough
- [x] Tests fail with `USE_SYSTEM_BUN=1` (confirming they detect the
vulnerability) and pass with `bun bd test`
- [x] Existing Node.js compat test
`test-http-status-reason-invalid-chars.js` still passes
- [x] All 14 HTTP security tests pass
- [x] Full `node-http.test.ts` suite passes (77 pass, 1 pre-existing
skip, 1 pre-existing proxy failure)

🤖 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-14 00:25:07 -08:00
robobun
2c173529fa fix(test): eliminate external HTTPS fetches and renew expired TLS certs (#27013)
## Summary

- **HTMLRewriter test**: Replaced `fetch("https://www.example.com/")`
with a local HTTP content server, eliminating dependency on external
HTTPS and system CA certificates
- **HTTPS agent test**: Replaced `https.request("https://example.com/")`
with a local TLS server using self-signed certs from harness
- **Expired certs**: Regenerated self-signed certificates in
`test/js/bun/http/fixtures/` and `test/regression/fixtures/` (were
expired since Sep 2024, now valid until 2036)

## Root cause

Tests fetching external HTTPS URLs (`https://example.com`,
`https://www.example.com`) fail on CI environments (Alpine Linux,
Windows) that lack system CA certificate bundles, producing
`UNABLE_TO_GET_ISSUER_CERT_LOCALLY` errors. This affected:
- `test/js/workerd/html-rewriter.test.js` (HTMLRewriter: async
replacement using fetch + Bun.serve)
-
`test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts`

## Test plan

- [x] `bun bd test test/js/workerd/html-rewriter.test.js` - 41 pass, 0
fail
- [x] `bun bd test test/js/workerd/html-rewriter.test.js -t "async
replacement using fetch"` - 1 pass
- [x] `bun bd
test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts`
- exits 0
- [x] `bun bd test test/js/bun/http/serve-listen.test.ts` - 27 pass, 0
fail (uses renewed certs)

🤖 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-13 20:45:06 -08:00
Dylan Conway
243fa45bec fix(shell): prevent use-after-free in builtin OutputTask callbacks inside $() (#26987)
## Summary

- Fixes use-after-free (ASAN use-after-poison) when shell builtins
(`ls`, `touch`, `mkdir`, `cp`) run inside command substitution `$(...)`
and encounter errors (e.g., permission denied)
- The `output_waiting` and `output_done` counters in the builtin exec
state went out of sync because `output_waiting` was only incremented for
async IO operations, while `output_done` was always incremented
- In command substitution, stdout is `.pipe` (sync) and stderr is `.fd`
(async), so a single OutputTask completing both writes could satisfy the
done condition while another OutputTask's async stderr write was still
pending in the IOWriter

The fix moves `output_waiting += 1` before the `needsIO()` check in all
four affected builtins so it's always incremented, matching
`output_done`.

## Test plan

- [x] `echo $(ls /tmp/*)` — no ASAN errors (previously crashed with
use-after-poison)
- [x] `echo $(touch /root/a /root/b ...)` — no ASAN errors
- [x] `echo $(mkdir /root/a /root/b ...)` — no ASAN errors
- [x] `ls /nonexistent` — still reports error and exits with code 1
- [x] `echo $(ls /tmp)` — still captures output correctly
- [x] Existing shell test suite: 292 pass, 52 fail (pre-existing), 83
todo — no regressions

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:40:49 -08:00
Dylan Conway
c19dcb3181 fix(shell): reject non-finite seq args and handle empty condexpr args (#26993)
## Summary
- **`seq inf` / `seq nan` / `seq -inf` hang**: `std.fmt.parseFloat`
accepts non-finite float values like `inf`, `nan`, `-inf`, but the loop
`while (current <= this._end)` never terminates when `_end` is infinity.
Now rejects non-finite values after parsing.
- **`[[ -d "" ]]` out-of-bounds panic**: Empty string expansion produces
no args in the args list, but `doStat()` unconditionally accesses
`args.items[0]`. Now checks `args.items.len == 0` before calling
`doStat()` and returns exit code 1 (path doesn't exist).

## Test plan
- [x] `seq inf`, `seq nan`, `seq -inf` return exit code 1 with "invalid
argument" instead of hanging
- [x] `[[ -d "" ]]` and `[[ -f "" ]]` return exit code 1 instead of
panicking
- [x] `seq 3` still works normally (produces 1, 2, 3)
- [x] `[[ -d /tmp ]]`, `[[ -f /etc/hostname ]]` still work correctly
- [x] Tests pass with `bun bd test`, seq tests fail with
`USE_SYSTEM_BUN=1`

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:01:40 -08: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
60 changed files with 2279 additions and 1202 deletions

View File

@@ -23196,557 +23196,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust ECC Root-01"
#
# Issuer: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Serial Number:43:70:82:77:cf:4d:5d:34:f1:ca:ae:32:2f:37:f7:f4:7f:75:a0:9e
# Subject: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:35:43 2021
# Not Valid After : Sat Apr 28 17:35:42 2046
# Fingerprint (SHA-256): 11:43:7C:DA:7B:B4:5E:41:36:5F:45:B3:9A:38:98:6B:0D:E0:0D:EF:34:8E:0C:7B:B0:87:36:33:80:0B:C3:8B
# Fingerprint (SHA1): 07:86:C0:D8:DD:8E:C0:80:98:06:98:D0:58:7A:EF:DE:A6:CC:A2:5D
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-01"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\103\160\202\167\317\115\135\064\361\312\256\062\057\067
\367\364\177\165\240\236
END
CKA_VALUE MULTILINE_OCTAL
\060\202\002\035\060\202\001\243\240\003\002\001\002\002\024\103
\160\202\167\317\115\135\064\361\312\256\062\057\067\367\364\177
\165\240\236\060\012\006\010\052\206\110\316\075\004\003\003\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061\060
\036\027\015\062\061\060\064\062\070\061\067\063\065\064\063\132
\027\015\064\066\060\064\062\070\061\067\063\065\064\062\132\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061\060
\166\060\020\006\007\052\206\110\316\075\002\001\006\005\053\201
\004\000\042\003\142\000\004\113\066\351\256\127\136\250\160\327
\320\217\164\142\167\303\136\172\252\345\266\242\361\170\375\002
\176\127\335\221\171\234\154\271\122\210\124\274\057\004\276\270
\315\366\020\321\051\354\265\320\240\303\360\211\160\031\273\121
\145\305\103\234\303\233\143\235\040\203\076\006\013\246\102\104
\205\021\247\112\072\055\351\326\150\057\110\116\123\053\007\077
\115\275\271\254\167\071\127\243\102\060\100\060\017\006\003\125
\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006\003
\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006\003
\125\035\016\004\026\004\024\216\007\142\300\120\335\306\031\006
\000\106\164\004\367\363\256\175\165\115\060\060\012\006\010\052
\206\110\316\075\004\003\003\003\150\000\060\145\002\061\000\234
\063\337\101\343\043\250\102\066\046\227\065\134\173\353\333\113
\370\252\213\163\125\025\134\254\170\051\017\272\041\330\304\240
\330\321\003\335\155\321\071\075\304\223\140\322\343\162\262\002
\060\174\305\176\210\323\120\365\036\045\350\372\116\165\346\130
\226\244\065\137\033\145\352\141\232\160\043\265\015\243\233\222
\122\157\151\240\214\215\112\320\356\213\016\313\107\216\320\215
\021
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust ECC Root-01"
# Issuer: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Serial Number:43:70:82:77:cf:4d:5d:34:f1:ca:ae:32:2f:37:f7:f4:7f:75:a0:9e
# Subject: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:35:43 2021
# Not Valid After : Sat Apr 28 17:35:42 2046
# Fingerprint (SHA-256): 11:43:7C:DA:7B:B4:5E:41:36:5F:45:B3:9A:38:98:6B:0D:E0:0D:EF:34:8E:0C:7B:B0:87:36:33:80:0B:C3:8B
# Fingerprint (SHA1): 07:86:C0:D8:DD:8E:C0:80:98:06:98:D0:58:7A:EF:DE:A6:CC:A2:5D
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-01"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\007\206\300\330\335\216\300\200\230\006\230\320\130\172\357\336
\246\314\242\135
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\072\100\247\374\003\214\234\070\171\057\072\242\154\266\012\026
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\103\160\202\167\317\115\135\064\361\312\256\062\057\067
\367\364\177\165\240\236
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust ECC Root-02"
#
# Issuer: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Serial Number:28:fd:99:60:41:47:a6:01:3a:ca:14:7b:1f:ef:f9:68:08:83:5d:7d
# Subject: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:44:54 2021
# Not Valid After : Sat Apr 28 17:44:53 2046
# Fingerprint (SHA-256): 2F:FB:7F:81:3B:BB:B3:C8:9A:B4:E8:16:2D:0F:16:D7:15:09:A8:30:CC:9D:73:C2:62:E5:14:08:75:D1:AD:4A
# Fingerprint (SHA1): 3C:3F:EF:57:0F:FE:65:93:86:9E:A0:FE:B0:F6:ED:8E:D1:13:C7:E5
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-02"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\050\375\231\140\101\107\246\001\072\312\024\173\037\357
\371\150\010\203\135\175
END
CKA_VALUE MULTILINE_OCTAL
\060\202\002\034\060\202\001\243\240\003\002\001\002\002\024\050
\375\231\140\101\107\246\001\072\312\024\173\037\357\371\150\010
\203\135\175\060\012\006\010\052\206\110\316\075\004\003\003\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062\060
\036\027\015\062\061\060\064\062\070\061\067\064\064\065\064\132
\027\015\064\066\060\064\062\070\061\067\064\064\065\063\132\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062\060
\166\060\020\006\007\052\206\110\316\075\002\001\006\005\053\201
\004\000\042\003\142\000\004\170\060\201\350\143\036\345\353\161
\121\017\367\007\007\312\071\231\174\116\325\017\314\060\060\013
\217\146\223\076\317\275\305\206\275\371\261\267\264\076\264\007
\310\363\226\061\363\355\244\117\370\243\116\215\051\025\130\270
\325\157\177\356\154\042\265\260\257\110\105\012\275\250\111\224
\277\204\103\260\333\204\112\003\043\031\147\152\157\301\156\274
\006\071\067\321\210\042\367\243\102\060\100\060\017\006\003\125
\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006\003
\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006\003
\125\035\016\004\026\004\024\346\030\165\377\357\140\336\204\244
\365\106\307\336\112\125\343\062\066\171\365\060\012\006\010\052
\206\110\316\075\004\003\003\003\147\000\060\144\002\060\046\163
\111\172\266\253\346\111\364\175\122\077\324\101\004\256\200\103
\203\145\165\271\205\200\070\073\326\157\344\223\206\253\217\347
\211\310\177\233\176\153\012\022\125\141\252\021\340\171\002\060
\167\350\061\161\254\074\161\003\326\204\046\036\024\270\363\073
\073\336\355\131\374\153\114\060\177\131\316\105\351\163\140\025
\232\114\360\346\136\045\042\025\155\302\207\131\320\262\216\152
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust ECC Root-02"
# Issuer: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Serial Number:28:fd:99:60:41:47:a6:01:3a:ca:14:7b:1f:ef:f9:68:08:83:5d:7d
# Subject: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:44:54 2021
# Not Valid After : Sat Apr 28 17:44:53 2046
# Fingerprint (SHA-256): 2F:FB:7F:81:3B:BB:B3:C8:9A:B4:E8:16:2D:0F:16:D7:15:09:A8:30:CC:9D:73:C2:62:E5:14:08:75:D1:AD:4A
# Fingerprint (SHA1): 3C:3F:EF:57:0F:FE:65:93:86:9E:A0:FE:B0:F6:ED:8E:D1:13:C7:E5
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-02"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\074\077\357\127\017\376\145\223\206\236\240\376\260\366\355\216
\321\023\307\345
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\131\260\104\325\145\115\270\134\125\031\222\002\266\321\224\262
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\050\375\231\140\101\107\246\001\072\312\024\173\037\357
\371\150\010\203\135\175
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust RSA Root-01"
#
# Issuer: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Serial Number:3e:03:49:81:75:16:74:31:8e:4c:ab:d5:c5:90:29:96:c5:39:10:dd
# Subject: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 16:45:54 2021
# Not Valid After : Sat Apr 28 16:45:53 2046
# Fingerprint (SHA-256): 02:BD:F9:6E:2A:45:DD:9B:F1:8F:C7:E1:DB:DF:21:A0:37:9B:A3:C9:C2:61:03:44:CF:D8:D6:06:FE:C1:ED:81
# Fingerprint (SHA1): 6D:0A:5F:F7:B4:23:06:B4:85:B3:B7:97:64:FC:AC:75:F5:33:F2:93
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-01"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\061
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\076\003\111\201\165\026\164\061\216\114\253\325\305\220
\051\226\305\071\020\335
END
CKA_VALUE MULTILINE_OCTAL
\060\202\005\154\060\202\003\124\240\003\002\001\002\002\024\076
\003\111\201\165\026\164\061\216\114\253\325\305\220\051\226\305
\071\020\335\060\015\006\011\052\206\110\206\367\015\001\001\013
\005\000\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\061\060\036\027\015\062\061\060\064\062\070\061\066\064\065
\065\064\132\027\015\064\066\060\064\062\070\061\066\064\065\065
\063\132\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\061\060\202\002\042\060\015\006\011\052\206\110\206\367\015
\001\001\001\005\000\003\202\002\017\000\060\202\002\012\002\202
\002\001\000\260\110\145\243\015\035\102\343\221\155\235\204\244
\141\226\022\302\355\303\332\043\064\031\166\366\352\375\125\132
\366\125\001\123\017\362\314\214\227\117\271\120\313\263\001\104
\126\226\375\233\050\354\173\164\013\347\102\153\125\316\311\141
\262\350\255\100\074\272\271\101\012\005\117\033\046\205\217\103
\265\100\265\205\321\324\161\334\203\101\363\366\105\307\200\242
\204\120\227\106\316\240\014\304\140\126\004\035\007\133\106\245
\016\262\113\244\016\245\174\356\370\324\142\003\271\223\152\212
\024\270\160\370\056\202\106\070\043\016\164\307\153\101\267\320
\051\243\235\200\260\176\167\223\143\102\373\064\203\073\163\243
\132\041\066\353\107\372\030\027\331\272\146\302\223\244\217\374
\135\244\255\374\120\152\225\254\274\044\063\321\275\210\177\206
\365\365\262\163\052\217\174\257\010\362\032\230\077\251\201\145
\077\301\214\211\305\226\060\232\012\317\364\324\310\064\355\235
\057\274\215\070\206\123\356\227\237\251\262\143\224\027\215\017
\334\146\052\174\122\121\165\313\231\216\350\075\134\277\236\073
\050\215\203\002\017\251\237\162\342\054\053\263\334\146\227\000
\100\320\244\124\216\233\135\173\105\066\046\326\162\103\353\317
\300\352\015\334\316\022\346\175\070\237\005\047\250\227\076\351
\121\306\154\005\050\301\002\017\351\030\155\354\275\234\006\324
\247\111\364\124\005\153\154\060\361\353\003\325\352\075\152\166
\302\313\032\050\111\115\177\144\340\372\053\332\163\203\201\377
\221\003\275\224\273\344\270\216\234\062\143\315\237\273\150\201
\261\204\133\257\066\277\167\356\035\177\367\111\233\122\354\322
\167\132\175\221\235\115\302\071\055\344\272\202\370\157\362\116
\036\017\116\346\077\131\245\043\334\075\207\250\050\130\050\321
\361\033\066\333\117\304\377\341\214\133\162\214\307\046\003\047
\243\071\012\001\252\300\262\061\140\203\042\241\117\022\011\001
\021\257\064\324\317\327\256\142\323\005\007\264\061\165\340\015
\155\127\117\151\207\371\127\251\272\025\366\310\122\155\241\313
\234\037\345\374\170\250\065\232\237\101\024\316\245\264\316\224
\010\034\011\255\126\345\332\266\111\232\112\352\143\030\123\234
\054\056\303\002\003\001\000\001\243\102\060\100\060\017\006\003
\125\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006
\003\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006
\003\125\035\016\004\026\004\024\067\135\246\232\164\062\302\302
\371\307\246\025\020\131\270\344\375\345\270\155\060\015\006\011
\052\206\110\206\367\015\001\001\013\005\000\003\202\002\001\000
\257\247\317\336\377\340\275\102\215\115\345\042\226\337\150\352
\175\115\052\175\320\255\075\026\134\103\347\175\300\206\350\172
\065\143\361\314\201\310\306\013\350\056\122\065\244\246\111\220
\143\121\254\064\254\005\073\127\000\351\323\142\323\331\051\325
\124\276\034\020\221\234\262\155\376\131\375\171\367\352\126\320
\236\150\124\102\217\046\122\342\114\337\057\227\246\057\322\007
\230\250\363\140\135\113\232\130\127\210\357\202\345\372\257\154
\201\113\222\217\100\232\223\106\131\313\137\170\026\261\147\076
\102\013\337\050\331\260\255\230\040\276\103\174\321\136\032\011
\027\044\215\173\135\225\351\253\301\140\253\133\030\144\200\373
\255\340\006\175\035\312\131\270\363\170\051\147\306\126\035\257
\266\265\164\052\166\241\077\373\165\060\237\224\136\073\245\140
\363\313\134\014\342\016\311\140\370\311\037\026\212\046\335\347
\047\177\353\045\246\212\275\270\055\066\020\232\261\130\115\232
\150\117\140\124\345\366\106\023\216\210\254\274\041\102\022\255
\306\112\211\175\233\301\330\055\351\226\003\364\242\164\014\274
\000\035\277\326\067\045\147\264\162\213\257\205\275\352\052\003
\217\314\373\074\104\044\202\342\001\245\013\131\266\064\215\062
\013\022\015\353\047\302\375\101\327\100\074\162\106\051\300\214
\352\272\017\361\006\223\056\367\234\250\364\140\076\243\361\070
\136\216\023\301\263\072\227\207\077\222\312\170\251\034\257\320
\260\033\046\036\276\160\354\172\365\063\230\352\134\377\053\013
\004\116\103\335\143\176\016\247\116\170\003\225\076\324\055\060
\225\021\020\050\056\277\240\002\076\377\136\131\323\005\016\225
\137\123\105\357\153\207\325\110\315\026\246\226\203\341\337\263
\006\363\301\024\333\247\354\034\213\135\220\220\015\162\121\347
\141\371\024\312\257\203\217\277\257\261\012\131\135\334\134\327
\344\226\255\133\140\035\332\256\227\262\071\331\006\365\166\000
\023\370\150\114\041\260\065\304\334\125\262\311\301\101\132\034
\211\300\214\157\164\240\153\063\115\265\001\050\375\255\255\211
\027\073\246\232\204\274\353\214\352\304\161\044\250\272\051\371
\010\262\047\126\065\062\137\352\071\373\061\232\325\031\314\360
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust RSA Root-01"
# Issuer: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Serial Number:3e:03:49:81:75:16:74:31:8e:4c:ab:d5:c5:90:29:96:c5:39:10:dd
# Subject: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 16:45:54 2021
# Not Valid After : Sat Apr 28 16:45:53 2046
# Fingerprint (SHA-256): 02:BD:F9:6E:2A:45:DD:9B:F1:8F:C7:E1:DB:DF:21:A0:37:9B:A3:C9:C2:61:03:44:CF:D8:D6:06:FE:C1:ED:81
# Fingerprint (SHA1): 6D:0A:5F:F7:B4:23:06:B4:85:B3:B7:97:64:FC:AC:75:F5:33:F2:93
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-01"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\155\012\137\367\264\043\006\264\205\263\267\227\144\374\254\165
\365\063\362\223
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\016\264\025\274\207\143\135\135\002\163\324\046\070\150\163\330
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\076\003\111\201\165\026\164\061\216\114\253\325\305\220
\051\226\305\071\020\335
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust RSA Root-02"
#
# Issuer: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Serial Number:54:16:bf:3b:7e:39:95:71:8d:d1:aa:00:a5:86:0d:2b:8f:7a:05:4e
# Subject: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:16:43 2021
# Not Valid After : Sat Apr 28 17:16:42 2046
# Fingerprint (SHA-256): FF:E9:43:D7:93:42:4B:4F:7C:44:0C:1C:3D:64:8D:53:63:F3:4B:82:DC:87:AA:7A:9F:11:8F:C5:DE:E1:01:F1
# Fingerprint (SHA1): EA:B0:E2:52:1B:89:93:4C:11:68:F2:D8:9A:AC:22:4C:A3:8A:57:AE
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-02"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\062
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\124\026\277\073\176\071\225\161\215\321\252\000\245\206
\015\053\217\172\005\116
END
CKA_VALUE MULTILINE_OCTAL
\060\202\005\154\060\202\003\124\240\003\002\001\002\002\024\124
\026\277\073\176\071\225\161\215\321\252\000\245\206\015\053\217
\172\005\116\060\015\006\011\052\206\110\206\367\015\001\001\013
\005\000\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\062\060\036\027\015\062\061\060\064\062\070\061\067\061\066
\064\063\132\027\015\064\066\060\064\062\070\061\067\061\066\064
\062\132\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\062\060\202\002\042\060\015\006\011\052\206\110\206\367\015
\001\001\001\005\000\003\202\002\017\000\060\202\002\012\002\202
\002\001\000\341\372\016\373\150\000\022\310\115\325\254\042\304
\065\001\073\305\124\345\131\166\143\245\177\353\301\304\152\230
\275\062\215\027\200\353\135\272\321\142\075\045\043\031\065\024
\351\177\211\247\033\142\074\326\120\347\064\225\003\062\261\264
\223\042\075\247\342\261\355\346\173\116\056\207\233\015\063\165
\012\336\252\065\347\176\345\066\230\242\256\045\236\225\263\062
\226\244\053\130\036\357\077\376\142\064\110\121\321\264\215\102
\255\140\332\111\152\225\160\335\322\000\342\314\127\143\002\173
\226\335\111\227\133\222\116\225\323\371\313\051\037\030\112\370
\001\052\322\143\011\156\044\351\211\322\345\307\042\114\334\163
\206\107\000\252\015\210\216\256\205\175\112\351\273\063\117\016
\122\160\235\225\343\174\155\226\133\055\075\137\241\203\106\135
\266\343\045\270\174\247\031\200\034\352\145\103\334\221\171\066
\054\164\174\362\147\006\311\211\311\333\277\332\150\277\043\355
\334\153\255\050\203\171\057\354\070\245\015\067\001\147\047\232
\351\063\331\063\137\067\241\305\360\253\075\372\170\260\347\054
\237\366\076\237\140\340\357\110\351\220\105\036\005\121\170\032
\054\022\054\134\050\254\015\242\043\236\064\217\005\346\242\063
\316\021\167\023\324\016\244\036\102\037\206\315\160\376\331\056
\025\075\035\273\270\362\123\127\333\314\306\164\051\234\030\263
\066\165\070\056\017\124\241\370\222\037\211\226\117\273\324\356
\235\351\073\066\102\265\012\073\052\324\144\171\066\020\341\371
\221\003\053\173\040\124\315\015\031\032\310\101\062\064\321\260
\231\341\220\036\001\100\066\265\267\372\251\345\167\165\244\042
\201\135\260\213\344\047\022\017\124\210\306\333\205\164\346\267
\300\327\246\051\372\333\336\363\223\227\047\004\125\057\012\157
\067\305\075\023\257\012\000\251\054\213\034\201\050\327\357\206
\061\251\256\362\156\270\312\152\054\124\107\330\052\210\056\257
\301\007\020\170\254\021\242\057\102\360\067\305\362\270\126\335
\016\142\055\316\055\126\176\125\362\247\104\366\053\062\364\043
\250\107\350\324\052\001\170\317\152\303\067\250\236\145\322\054
\345\372\272\063\301\006\104\366\346\317\245\015\247\146\010\064
\212\054\363\002\003\001\000\001\243\102\060\100\060\017\006\003
\125\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006
\003\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006
\003\125\035\016\004\026\004\024\107\320\347\261\042\377\235\054
\365\331\127\140\263\261\261\160\225\357\141\172\060\015\006\011
\052\206\110\206\367\015\001\001\013\005\000\003\202\002\001\000
\206\151\261\115\057\351\237\117\042\223\150\216\344\041\231\243
\316\105\123\033\163\104\123\000\201\141\315\061\343\010\272\201
\050\050\172\222\271\266\250\310\103\236\307\023\046\115\302\330
\345\125\234\222\135\120\330\302\053\333\376\346\250\227\317\122
\072\044\303\145\144\134\107\061\243\145\065\023\303\223\271\367
\371\121\227\273\244\360\142\207\305\326\006\323\227\203\040\251
\176\273\266\041\302\245\015\204\000\341\362\047\020\203\272\335
\003\201\325\335\150\303\146\020\310\321\166\264\263\157\051\236
\000\371\302\051\365\261\223\031\122\151\032\054\114\240\213\340
\025\232\061\057\323\210\225\131\156\345\304\263\120\310\024\010
\112\233\213\023\203\261\244\162\262\073\166\063\101\334\334\252
\246\007\157\035\044\022\237\310\166\275\057\331\216\364\054\356
\267\322\070\020\044\066\121\057\343\134\135\201\041\247\332\273
\116\377\346\007\250\376\271\015\047\154\273\160\132\125\172\023
\351\361\052\111\151\307\137\207\127\114\103\171\155\072\145\351
\060\134\101\356\353\167\245\163\022\210\350\277\175\256\345\304
\250\037\015\216\034\155\120\002\117\046\030\103\336\217\125\205
\261\013\067\005\140\311\125\071\022\004\241\052\317\161\026\237
\066\121\111\277\160\073\236\147\234\373\173\171\311\071\034\170
\254\167\221\124\232\270\165\012\201\122\227\343\146\141\153\355
\076\070\036\226\141\125\341\221\124\214\355\214\044\037\201\311
\020\232\163\231\053\026\116\162\000\077\124\033\370\215\272\213
\347\024\326\266\105\117\140\354\226\256\303\057\002\116\135\235
\226\111\162\000\262\253\165\134\017\150\133\035\145\302\137\063
\017\036\017\360\073\206\365\260\116\273\234\367\352\045\005\334
\255\242\233\113\027\001\276\102\337\065\041\035\255\253\256\364
\277\256\037\033\323\342\073\374\263\162\163\034\233\050\220\211
\023\075\035\301\000\107\011\226\232\070\033\335\261\317\015\302
\264\104\363\226\225\316\062\072\217\064\234\340\027\307\136\316
\256\015\333\207\070\345\077\133\375\233\031\341\061\101\172\160
\252\043\153\001\341\105\114\315\224\316\073\236\055\347\210\002
\042\364\156\350\310\354\326\074\363\271\262\327\167\172\254\173
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust RSA Root-02"
# Issuer: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Serial Number:54:16:bf:3b:7e:39:95:71:8d:d1:aa:00:a5:86:0d:2b:8f:7a:05:4e
# Subject: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:16:43 2021
# Not Valid After : Sat Apr 28 17:16:42 2046
# Fingerprint (SHA-256): FF:E9:43:D7:93:42:4B:4F:7C:44:0C:1C:3D:64:8D:53:63:F3:4B:82:DC:87:AA:7A:9F:11:8F:C5:DE:E1:01:F1
# Fingerprint (SHA1): EA:B0:E2:52:1B:89:93:4C:11:68:F2:D8:9A:AC:22:4C:A3:8A:57:AE
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-02"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\352\260\342\122\033\211\223\114\021\150\362\330\232\254\042\114
\243\212\127\256
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\341\051\371\142\173\166\342\226\155\363\324\327\017\256\037\252
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\124\026\277\073\176\071\225\161\215\321\252\000\245\206
\015\053\217\172\005\116
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "D-Trust SBR Root CA 1 2022"
#

View File

@@ -3182,96 +3182,6 @@ static struct us_cert_string_t root_certs[] = {
"MvHVI5TWWA==\n"
"-----END CERTIFICATE-----",.len=869},
/* CommScope Public Trust ECC Root-01 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMwTjELMAkG\n"
"A1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1Ymxp\n"
"YyBUcnVzdCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNaFw00NjA0MjgxNzM1NDJaME4x\n"
"CzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQ\n"
"dWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16o\n"
"cNfQj3Rid8NeeqrltqLxeP0CflfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOc\n"
"w5tjnSCDPgYLpkJEhRGnSjot6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMB\n"
"Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggq\n"
"hkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg2NED3W3R\n"
"OT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liWpDVfG2XqYZpwI7UNo5uSUm9poIyNStDuiw7L\n"
"R47QjRE=\n"
"-----END CERTIFICATE-----",.len=792},
/* CommScope Public Trust ECC Root-02 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMwTjELMAkG\n"
"A1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1Ymxp\n"
"YyBUcnVzdCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRaFw00NjA0MjgxNzQ0NTNaME4x\n"
"CzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQ\n"
"dWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l\n"
"63FRD/cHB8o5mXxO1Q/MMDALj2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/u\n"
"bCK1sK9IRQq9qEmUv4RDsNuESgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMB\n"
"Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggq\n"
"hkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/nich/m35r\n"
"ChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs73u1Z/GtMMH9ZzkXpc2AVmkzw5l4lIhVtwodZ\n"
"0LKOag==\n"
"-----END CERTIFICATE-----",.len=792},
/* CommScope Public Trust RSA Root-01 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQELBQAwTjEL\n"
"MAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1\n"
"YmxpYyBUcnVzdCBSU0EgUm9vdC0wMTAeFw0yMTA0MjgxNjQ1NTRaFw00NjA0MjgxNjQ1NTNa\n"
"ME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29w\n"
"ZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3QtMDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n"
"AoICAQCwSGWjDR1C45FtnYSkYZYSwu3D2iM0GXb26v1VWvZVAVMP8syMl0+5UMuzAURWlv2b\n"
"KOx7dAvnQmtVzslhsuitQDy6uUEKBU8bJoWPQ7VAtYXR1HHcg0Hz9kXHgKKEUJdGzqAMxGBW\n"
"BB0HW0alDrJLpA6lfO741GIDuZNqihS4cPgugkY4Iw50x2tBt9Apo52AsH53k2NC+zSDO3Oj\n"
"WiE260f6GBfZumbCk6SP/F2krfxQapWsvCQz0b2If4b19bJzKo98rwjyGpg/qYFlP8GMicWW\n"
"MJoKz/TUyDTtnS+8jTiGU+6Xn6myY5QXjQ/cZip8UlF1y5mO6D1cv547KI2DAg+pn3LiLCuz\n"
"3GaXAEDQpFSOm117RTYm1nJD68/A6g3czhLmfTifBSeolz7pUcZsBSjBAg/pGG3svZwG1KdJ\n"
"9FQFa2ww8esD1eo9anbCyxooSU1/ZOD6K9pzg4H/kQO9lLvkuI6cMmPNn7togbGEW682v3fu\n"
"HX/3SZtS7NJ3Wn2RnU3COS3kuoL4b/JOHg9O5j9ZpSPcPYeoKFgo0fEbNttPxP/hjFtyjMcm\n"
"AyejOQoBqsCyMWCDIqFPEgkBEa801M/XrmLTBQe0MXXgDW1XT2mH+VepuhX2yFJtocucH+X8\n"
"eKg1mp9BFM6ltM6UCBwJrVbl2rZJmkrqYxhTnCwuwwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\n"
"AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUN12mmnQywsL5x6YVEFm45P3luG0wDQYJ\n"
"KoZIhvcNAQELBQADggIBAK+nz97/4L1CjU3lIpbfaOp9TSp90K09FlxD533Ahuh6NWPxzIHI\n"
"xgvoLlI1pKZJkGNRrDSsBTtXAOnTYtPZKdVUvhwQkZyybf5Z/Xn36lbQnmhUQo8mUuJM3y+X\n"
"pi/SB5io82BdS5pYV4jvguX6r2yBS5KPQJqTRlnLX3gWsWc+QgvfKNmwrZggvkN80V4aCRck\n"
"jXtdlemrwWCrWxhkgPut4AZ9HcpZuPN4KWfGVh2vtrV0KnahP/t1MJ+UXjulYPPLXAziDslg\n"
"+MkfFoom3ecnf+slpoq9uC02EJqxWE2aaE9gVOX2RhOOiKy8IUISrcZKiX2bwdgt6ZYD9KJ0\n"
"DLwAHb/WNyVntHKLr4W96ioDj8z7PEQkguIBpQtZtjSNMgsSDesnwv1B10A8ckYpwIzqug/x\n"
"BpMu95yo9GA+o/E4Xo4TwbM6l4c/ksp4qRyv0LAbJh6+cOx69TOY6lz/KwsETkPdY34Op054\n"
"A5U+1C0wlREQKC6/oAI+/15Z0wUOlV9TRe9rh9VIzRamloPh37MG88EU26fsHItdkJANclHn\n"
"YfkUyq+Dj7+vsQpZXdxc1+SWrVtgHdqul7I52Qb1dgAT+GhMIbA1xNxVssnBQVocicCMb3Sg\n"
"azNNtQEo/a2tiRc7ppqEvOuM6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw\n"
"-----END CERTIFICATE-----",.len=1935},
/* CommScope Public Trust RSA Root-02 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIIFbDCCA1SgAwIBAgIUVBa/O345lXGN0aoApYYNK496BU4wDQYJKoZIhvcNAQELBQAwTjEL\n"
"MAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1\n"
"YmxpYyBUcnVzdCBSU0EgUm9vdC0wMjAeFw0yMTA0MjgxNzE2NDNaFw00NjA0MjgxNzE2NDJa\n"
"ME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29w\n"
"ZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3QtMDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n"
"AoICAQDh+g77aAASyE3VrCLENQE7xVTlWXZjpX/rwcRqmL0yjReA61260WI9JSMZNRTpf4mn\n"
"G2I81lDnNJUDMrG0kyI9p+Kx7eZ7Ti6Hmw0zdQreqjXnfuU2mKKuJZ6VszKWpCtYHu8//mI0\n"
"SFHRtI1CrWDaSWqVcN3SAOLMV2MCe5bdSZdbkk6V0/nLKR8YSvgBKtJjCW4k6YnS5cciTNxz\n"
"hkcAqg2Ijq6FfUrpuzNPDlJwnZXjfG2WWy09X6GDRl224yW4fKcZgBzqZUPckXk2LHR88mcG\n"
"yYnJ27/aaL8j7dxrrSiDeS/sOKUNNwFnJ5rpM9kzXzehxfCrPfp4sOcsn/Y+n2Dg70jpkEUe\n"
"BVF4GiwSLFworA2iI540jwXmojPOEXcT1A6kHkIfhs1w/tkuFT0du7jyU1fbzMZ0KZwYszZ1\n"
"OC4PVKH4kh+Jlk+71O6d6Ts2QrUKOyrUZHk2EOH5kQMreyBUzQ0ZGshBMjTRsJnhkB4BQDa1\n"
"t/qp5Xd1pCKBXbCL5CcSD1SIxtuFdOa3wNemKfrb3vOTlycEVS8KbzfFPROvCgCpLIscgSjX\n"
"74Yxqa7ybrjKaixUR9gqiC6vwQcQeKwRoi9C8DfF8rhW3Q5iLc4tVn5V8qdE9isy9COoR+jU\n"
"KgF4z2rDN6ieZdIs5fq6M8EGRPbmz6UNp2YINIos8wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\n"
"AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUR9DnsSL/nSz12Vdgs7GxcJXvYXowDQYJ\n"
"KoZIhvcNAQELBQADggIBAIZpsU0v6Z9PIpNojuQhmaPORVMbc0RTAIFhzTHjCLqBKCh6krm2\n"
"qMhDnscTJk3C2OVVnJJdUNjCK9v+5qiXz1I6JMNlZFxHMaNlNRPDk7n3+VGXu6TwYofF1gbT\n"
"l4MgqX67tiHCpQ2EAOHyJxCDut0DgdXdaMNmEMjRdrSzbymeAPnCKfWxkxlSaRosTKCL4BWa\n"
"MS/TiJVZbuXEs1DIFAhKm4sTg7GkcrI7djNB3NyqpgdvHSQSn8h2vS/ZjvQs7rfSOBAkNlEv\n"
"41xdgSGn2rtO/+YHqP65DSdsu3BaVXoT6fEqSWnHX4dXTEN5bTpl6TBcQe7rd6VzEojov32u\n"
"5cSoHw2OHG1QAk8mGEPej1WFsQs3BWDJVTkSBKEqz3EWnzZRSb9wO55nnPt7eck5HHisd5FU\n"
"mrh1CoFSl+NmYWvtPjgelmFV4ZFUjO2MJB+ByRCac5krFk5yAD9UG/iNuovnFNa2RU9g7Jau\n"
"wy8CTl2dlklyALKrdVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670\n"
"v64fG9PiO/yzcnMcmyiQiRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17Org3bhzjl\n"
"P1v9mxnhMUF6cKojawHhRUzNlM47ni3niAIi9G7oyOzWPPO5std3eqx7\n"
"-----END CERTIFICATE-----",.len=1935},
/* Telekom Security TLS ECC Root 2020 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQG\n"

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

@@ -1140,14 +1140,14 @@ export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *c
fn getHardcodedModule(jsc_vm: *VirtualMachine, specifier: bun.String, hardcoded: HardcodedModule) ?ResolvedSource {
analytics.Features.builtin_modules.insert(hardcoded);
return switch (hardcoded) {
.@"bun:main" => .{
.@"bun:main" => if (jsc_vm.entry_point.generated) .{
.allocator = null,
.source_code = bun.String.cloneUTF8(jsc_vm.entry_point.source.contents),
.specifier = specifier,
.source_url = specifier,
.tag = .esm,
.source_code_needs_deref = true,
},
} else null,
.@"bun:internal-for-testing" => {
if (!Environment.isDebug) {
if (!is_allowed_to_use_internal_testing_apis)

View File

@@ -1616,7 +1616,7 @@ fn _resolve(
if (strings.eqlComptime(std.fs.path.basename(specifier), Runtime.Runtime.Imports.alt_name)) {
ret.path = Runtime.Runtime.Imports.Name;
return;
} else if (strings.eqlComptime(specifier, main_file_name)) {
} else if (strings.eqlComptime(specifier, main_file_name) and jsc_vm.entry_point.generated) {
ret.result = null;
ret.path = jsc_vm.entry_point.source.path.text;
return;

View File

@@ -468,6 +468,17 @@ pub fn writeHead(this: *NodeHTTPResponse, globalObject: *jsc.JSGlobalObject, cal
return globalObject.ERR(.HTTP_HEADERS_SENT, "Stream already started", .{}).throw();
}
// Validate status message does not contain invalid characters (defense-in-depth
// against HTTP response splitting). Matches Node.js checkInvalidHeaderChar:
// rejects any char not in [\t\x20-\x7e\x80-\xff].
if (status_message_slice.len > 0) {
for (status_message_slice.slice()) |c| {
if (c != '\t' and (c < 0x20 or c == 0x7f)) {
return globalObject.ERR(.INVALID_CHAR, "Invalid character in statusMessage", .{}).throw();
}
}
}
do_it: {
if (status_message_slice.len == 0) {
if (HTTPStatusText.get(@intCast(status_code))) |status_message| {

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

@@ -22,6 +22,7 @@
#include <wtf/IterationStatus.h>
#include <JavaScriptCore/CodeBlock.h>
#include <JavaScriptCore/FunctionCodeBlock.h>
#include <JavaScriptCore/Interpreter.h>
#include "ErrorStackFrame.h"
@@ -114,9 +115,9 @@ JSCStackTrace JSCStackTrace::fromExisting(JSC::VM& vm, const WTF::Vector<JSC::St
void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, JSC::JSCell* owner, JSC::JSValue caller, WTF::Vector<JSC::StackFrame>& stackTrace, size_t stackTraceLimit)
{
size_t framesCount = 0;
bool belowCaller = false;
// Compute the number of frames to skip by walking the stack to find the caller.
// We need this first pass because Interpreter::getStackTrace uses framesToSkip
// as a count of visible (non-private) frames to skip.
int32_t skipFrames = 0;
WTF::String callerName {};
@@ -129,29 +130,15 @@ void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, J
callerName = callerFunctionInternal->name();
}
size_t totalFrames = 0;
if (!callerName.isEmpty()) {
JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus {
if (isImplementationVisibilityPrivate(visitor)) {
return WTF::IterationStatus::Continue;
}
framesCount += 1;
skipFrames += 1;
// skip caller frame and all frames above it
if (!belowCaller) {
skipFrames += 1;
if (visitor->functionName() == callerName) {
belowCaller = true;
return WTF::IterationStatus::Continue;
}
}
totalFrames += 1;
if (totalFrames > stackTraceLimit) {
if (visitor->functionName() == callerName) {
return WTF::IterationStatus::Done;
}
@@ -163,95 +150,34 @@ void JSCStackTrace::getFramesForCaller(JSC::VM& vm, JSC::CallFrame* callFrame, J
return WTF::IterationStatus::Continue;
}
framesCount += 1;
// skip caller frame and all frames above it
if (!belowCaller) {
auto callee = visitor->callee();
skipFrames += 1;
if (callee.isCell() && callee.asCell() == caller) {
belowCaller = true;
return WTF::IterationStatus::Continue;
}
}
totalFrames += 1;
if (totalFrames > stackTraceLimit) {
skipFrames += 1;
auto callee = visitor->callee();
if (callee.isCell() && callee.asCell() == caller) {
return WTF::IterationStatus::Done;
}
return WTF::IterationStatus::Continue;
});
} else if (caller.isEmpty() || caller.isUndefined()) {
// Skip the first frame.
JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus {
if (isImplementationVisibilityPrivate(visitor)) {
return WTF::IterationStatus::Continue;
}
framesCount += 1;
if (!belowCaller) {
skipFrames += 1;
belowCaller = true;
}
totalFrames += 1;
if (totalFrames > stackTraceLimit) {
return WTF::IterationStatus::Done;
}
return WTF::IterationStatus::Continue;
});
// Skip the first frame (captureStackTrace itself).
skipFrames = 1;
}
size_t i = 0;
totalFrames = 0;
stackTrace.reserveInitialCapacity(framesCount);
JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus {
// Skip native frames
if (isImplementationVisibilityPrivate(visitor)) {
return WTF::IterationStatus::Continue;
// Use Interpreter::getStackTrace which handles async continuation frames
// (frames from functions suspended at await points higher up the async call chain).
// This is critical for compatibility with V8's behavior where Error.captureStackTrace
// includes suspended async frames in the CallSite array.
WTF::Vector<JSC::StackFrame> rawStackTrace;
vm.interpreter.getStackTrace(owner, rawStackTrace, skipFrames, stackTraceLimit);
// Filter out private/internal implementation frames to match the behavior
// of the previous StackVisitor-based approach.
stackTrace.reserveInitialCapacity(rawStackTrace.size());
for (auto& frame : rawStackTrace) {
if (!isImplementationVisibilityPrivate(frame)) {
stackTrace.append(frame);
}
// Skip frames if needed
if (skipFrames > 0) {
skipFrames--;
return WTF::IterationStatus::Continue;
}
totalFrames += 1;
if (totalFrames > stackTraceLimit) {
return WTF::IterationStatus::Done;
}
if (visitor->isNativeCalleeFrame()) {
auto* nativeCallee = visitor->callee().asNativeCallee();
switch (nativeCallee->category()) {
case NativeCallee::Category::Wasm: {
stackTrace.append(StackFrame(visitor->wasmFunctionIndexOrName()));
break;
}
case NativeCallee::Category::InlineCache: {
break;
}
}
#if USE(ALLOW_LINE_AND_COLUMN_NUMBER_IN_BUILTINS)
} else if (!!visitor->codeBlock())
#else
} else if (!!visitor->codeBlock() && !visitor->codeBlock()->unlinkedCodeBlock()->isBuiltinFunction())
#endif
stackTrace.append(StackFrame(vm, owner, visitor->callee().asCell(), visitor->codeBlock(), visitor->bytecodeIndex()));
else
stackTrace.append(StackFrame(vm, owner, visitor->callee().asCell()));
i++;
return (i == framesCount) ? WTF::IterationStatus::Done : WTF::IterationStatus::Continue;
});
}
}
JSCStackTrace JSCStackTrace::getStackTraceForThrownValue(JSC::VM& vm, JSC::JSValue thrownValue)

View File

@@ -560,6 +560,8 @@ JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_infoAccess, (JSGlobalObject * g
return JSValue::encode(jsUndefined());
BUF_MEM* bptr = bio;
if (!bptr)
return JSValue::encode(jsUndefined());
return JSValue::encode(undefinedIfEmpty(jsString(vm, String::fromUTF8(std::span(bptr->data, bptr->length)))));
}
@@ -602,20 +604,10 @@ JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_issuerCertificate, (JSGlobalObj
return {};
}
auto issuerCert = thisObject->view().getIssuer();
if (!issuerCert)
return JSValue::encode(jsUndefined());
auto bio = issuerCert.get();
BUF_MEM* bptr = nullptr;
BIO_get_mem_ptr(bio, &bptr);
std::span<const uint8_t> span(reinterpret_cast<const uint8_t*>(bptr->data), bptr->length);
auto* zigGlobalObject = defaultGlobalObject(globalObject);
auto* structure = zigGlobalObject->m_JSX509CertificateClassStructure.get(zigGlobalObject);
auto jsIssuerCert = JSX509Certificate::create(vm, structure, globalObject, span);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsIssuerCert);
// issuerCertificate is only available when the certificate was obtained from
// a TLS connection with a peer certificate chain. For certificates parsed
// directly from PEM/DER data, it is always undefined (matching Node.js behavior).
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_publicKey, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))

View File

@@ -42,6 +42,13 @@ static std::optional<WTF::String> stripANSI(const std::span<const Char> input)
// Append everything before the escape sequence
result.append(std::span { start, escPos });
const auto newPos = ANSI::consumeANSI(escPos, end);
if (newPos == escPos) {
// No ANSI found
result.append(std::span { escPos, escPos + 1 });
start = escPos + 1;
continue;
}
ASSERT(newPos > start);
ASSERT(newPos <= end);
foundANSI = true;

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

@@ -150,6 +150,7 @@ pub const ClientEntryPoint = struct {
pub const ServerEntryPoint = struct {
source: logger.Source = undefined,
generated: bool = false,
pub fn generate(
entry: *ServerEntryPoint,
@@ -230,6 +231,7 @@ pub const ServerEntryPoint = struct {
entry.source = logger.Source.initPathString(name, code);
entry.source.path.text = name;
entry.source.path.namespace = "server-entry";
entry.generated = true;
}
};

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

@@ -23,7 +23,6 @@ var print_every_i: usize = 0;
// we always rewrite the entire HTTP request when write() returns EAGAIN
// so we can reuse this buffer
var shared_request_headers_buf: [256]picohttp.Header = undefined;
var shared_request_headers_overflow: ?[]picohttp.Header = null;
// this doesn't need to be stack memory because it is immediately cloned after use
var shared_response_headers_buf: [256]picohttp.Header = undefined;
@@ -606,32 +605,7 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
var header_entries = this.header_entries.slice();
const header_names = header_entries.items(.name);
const header_values = header_entries.items(.value);
// The maximum number of headers is the user-provided headers plus up to
// 6 extra headers that may be added below (Connection, User-Agent,
// Accept, Host, Accept-Encoding, Content-Length/Transfer-Encoding).
const max_headers = header_names.len + 6;
const static_buf_len = shared_request_headers_buf.len;
// Use the static buffer for the common case, dynamically allocate for overflow.
// The overflow buffer is kept around for reuse to avoid repeated allocations.
var request_headers_buf: []picohttp.Header = if (max_headers <= static_buf_len)
&shared_request_headers_buf
else blk: {
if (shared_request_headers_overflow) |overflow| {
if (overflow.len >= max_headers) {
break :blk overflow;
}
bun.default_allocator.free(overflow);
shared_request_headers_overflow = null;
}
const buf = bun.default_allocator.alloc(picohttp.Header, max_headers) catch
// On allocation failure, fall back to the static buffer and
// truncate headers rather than writing out of bounds.
break :blk @as([]picohttp.Header, &shared_request_headers_buf);
shared_request_headers_overflow = buf;
break :blk buf;
};
var request_headers_buf = &shared_request_headers_buf;
var override_accept_encoding = false;
var override_accept_header = false;
@@ -693,32 +667,43 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
else => {},
}
if (header_count >= request_headers_buf.len) break;
request_headers_buf[header_count] = .{
.name = name,
.value = this.headerStr(header_values[i]),
};
// header_name_hashes[header_count] = hash;
// // ensure duplicate headers come after each other
// if (header_count > 2) {
// var head_i: usize = header_count - 1;
// while (head_i > 0) : (head_i -= 1) {
// if (header_name_hashes[head_i] == header_name_hashes[header_count]) {
// std.mem.swap(picohttp.Header, &header_name_hashes[header_count], &header_name_hashes[head_i + 1]);
// std.mem.swap(u64, &request_headers_buf[header_count], &request_headers_buf[head_i + 1]);
// break;
// }
// }
// }
header_count += 1;
}
if (!override_connection_header and !this.flags.disable_keepalive and header_count < request_headers_buf.len) {
if (!override_connection_header and !this.flags.disable_keepalive) {
request_headers_buf[header_count] = connection_header;
header_count += 1;
}
if (!override_user_agent and header_count < request_headers_buf.len) {
if (!override_user_agent) {
request_headers_buf[header_count] = getUserAgentHeader();
header_count += 1;
}
if (!override_accept_header and header_count < request_headers_buf.len) {
if (!override_accept_header) {
request_headers_buf[header_count] = accept_header;
header_count += 1;
}
if (!override_host_header and header_count < request_headers_buf.len) {
if (!override_host_header) {
request_headers_buf[header_count] = .{
.name = host_header_name,
.value = this.url.host,
@@ -726,33 +711,31 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
header_count += 1;
}
if (!override_accept_encoding and !this.flags.disable_decompression and header_count < request_headers_buf.len) {
if (!override_accept_encoding and !this.flags.disable_decompression) {
request_headers_buf[header_count] = accept_encoding_header;
header_count += 1;
}
if (header_count < request_headers_buf.len) {
if (body_len > 0 or this.method.hasRequestBody()) {
if (this.flags.is_streaming_request_body) {
if (add_transfer_encoding and this.flags.upgrade_state == .none) {
request_headers_buf[header_count] = chunked_encoded_header;
header_count += 1;
}
} else {
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = std.fmt.bufPrint(&this.request_content_len_buf, "{d}", .{body_len}) catch "0",
};
if (body_len > 0 or this.method.hasRequestBody()) {
if (this.flags.is_streaming_request_body) {
if (add_transfer_encoding and this.flags.upgrade_state == .none) {
request_headers_buf[header_count] = chunked_encoded_header;
header_count += 1;
}
} else if (original_content_length) |content_length| {
} else {
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = content_length,
.value = std.fmt.bufPrint(&this.request_content_len_buf, "{d}", .{body_len}) catch "0",
};
header_count += 1;
}
} else if (original_content_length) |content_length| {
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = content_length,
};
header_count += 1;
}
return picohttp.Request{

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

@@ -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

@@ -266,8 +266,8 @@ pub const ShellCpOutputTask = OutputTask(Cp, .{
const ShellCpOutputTaskVTable = struct {
pub fn writeErr(this: *Cp, childptr: anytype, errbuf: []const u8) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -279,8 +279,8 @@ const ShellCpOutputTaskVTable = struct {
}
pub fn writeOut(this: *Cp, childptr: anytype, output: *OutputSrc) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stdout.enqueue(childptr, output.slice(), safeguard);
}
_ = this.bltn().writeNoIO(.stdout, output.slice());

View File

@@ -175,8 +175,8 @@ pub const ShellLsOutputTask = OutputTask(Ls, .{
const ShellLsOutputTaskVTable = struct {
pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) ?Yield {
log("ShellLsOutputTaskVTable.writeErr(0x{x}, {s})", .{ @intFromPtr(this), errbuf });
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -190,8 +190,8 @@ const ShellLsOutputTaskVTable = struct {
pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) ?Yield {
log("ShellLsOutputTaskVTable.writeOut(0x{x}, {s})", .{ @intFromPtr(this), output.slice() });
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stdout.enqueue(childptr, output.slice(), safeguard);
}
log("ShellLsOutputTaskVTable.writeOut(0x{x}, {s}) no IO", .{ @intFromPtr(this), output.slice() });

View File

@@ -129,8 +129,8 @@ pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{
const ShellMkdirOutputTaskVTable = struct {
pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -142,8 +142,8 @@ const ShellMkdirOutputTaskVTable = struct {
}
pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
const slice = output.slice();
log("THE SLICE: {d} {s}", .{ slice.len, slice });
return this.bltn().stdout.enqueue(childptr, slice, safeguard);

View File

@@ -46,12 +46,14 @@ pub fn start(this: *@This()) Yield {
const maybe1 = iter.next().?;
const int1 = std.fmt.parseFloat(f32, bun.sliceTo(maybe1, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int1)) return this.fail("seq: invalid argument\n");
this._end = int1;
if (this._start > this._end) this.increment = -1;
const maybe2 = iter.next();
if (maybe2 == null) return this.do();
const int2 = std.fmt.parseFloat(f32, bun.sliceTo(maybe2.?, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int2)) return this.fail("seq: invalid argument\n");
this._start = int1;
this._end = int2;
if (this._start < this._end) this.increment = 1;
@@ -60,6 +62,7 @@ pub fn start(this: *@This()) Yield {
const maybe3 = iter.next();
if (maybe3 == null) return this.do();
const int3 = std.fmt.parseFloat(f32, bun.sliceTo(maybe3.?, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int3)) return this.fail("seq: invalid argument\n");
this._start = int1;
this.increment = int2;
this._end = int3;

View File

@@ -132,8 +132,8 @@ pub const ShellTouchOutputTask = OutputTask(Touch, .{
const ShellTouchOutputTaskVTable = struct {
pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -145,8 +145,8 @@ const ShellTouchOutputTaskVTable = struct {
}
pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
const slice = output.slice();
log("THE SLICE: {d} {s}", .{ slice.len, slice });
return this.bltn().stdout.enqueue(childptr, slice, safeguard);

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

@@ -168,6 +168,8 @@ fn commandImplStart(this: *CondExpr) Yield {
.@"-d",
.@"-f",
=> {
// Empty string expansion produces no args; the path doesn't exist.
if (this.args.items.len == 0) return this.parent.childDone(this, 1);
this.state = .waiting_stat;
return this.doStat();
},

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

@@ -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

@@ -416,7 +416,8 @@ describe("bundler", () => {
const db = new Database("test.db");
const query = db.query(\`select "Hello world" as message\`);
if (query.get().message !== "Hello world") throw "fail from sqlite";
const icon = await fetch("https://bun.sh/favicon.ico").then(x=>x.arrayBuffer())
const icon = new Uint8Array(256);
for (let i = 0; i < 256; i++) icon[i] = i;
if(icon.byteLength < 100) throw "fail from icon";
if (typeof getRandomSeed() !== 'number') throw "fail from bun:jsc";
const server = serve({

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

@@ -634,3 +634,42 @@ test.concurrent("bun serve files with correct Content-Type headers", async () =>
// The process will be automatically cleaned up by 'await using'
}
});
test("importing bun:main from HTML entry preload does not crash", async () => {
const dir = tempDirWithFiles("html-entry-bun-main", {
"index.html": /*html*/ `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body><h1>Hello</h1></body>
</html>
`,
"preload.mjs": /*js*/ `
try {
await import("bun:main");
} catch {}
// Signal that preload ran successfully without crashing
console.log("PRELOAD_OK");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.mjs", "index.html", "--port=0"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const decoder = new TextDecoder();
let text = "";
for await (const chunk of proc.stdout) {
text += decoder.decode(chunk, { stream: true });
if (text.includes("http://")) break;
}
expect(text).toContain("PRELOAD_OK");
proc.kill();
await proc.exited;
});

View File

@@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIzOJskt6VkEJY
XKSJv/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwV
x16Q0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+
UXUOzSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb
8MsDmT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo
1EHvYSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1J
oEUjrLKtAgMBAAECggEACInVNhaiqu4infZGVMy0rXMV8VwSlapM7O2SLtFsr0nK
XUmaLK6dvGzBPKK9dxdiYCFzPlMKQTkhzsAvYFWSmm3tRmikG+11TFyCRhXLpc8/
ark4vD9Io6ZkmKUmyKLwtXNjNGcqQtJ7RXc7Ga3nAkueN6JKZHqieZusXVeBGQ70
YH1LKyVNBeJggbj+g9rqaksPyNJQ8EWiNTJkTRQPazZ0o1VX/fzDFyr/a5npFtHl
4BHfafv9o1Xyr70Kie8CYYRJNViOCN+ylFs7Gd3XRaAkSkgMT/7DzrHdEM2zrrHK
yNg2gyDVX9UeEJG2X5UtU0o9BVW7WBshz/2hqIUHoQKBgQC8zsRFvC7u/rGr5vRR
mhZZG+Wvg03/xBSuIgOrzm+Qie6mAzOdVmfSL/pNV9EFitXt1yd2ROo31AbS7Evy
Bm/QVKr2mBlmLgov3B7O/e6ABteooOL7769qV/v+yo8VdEg0biHmsfGIIXDe3Lwl
OT0XwF9r/SeZLbw1zfkSsUVG/QKBgQC5fANM3Dc9LEek+6PHv5+eC1cKkyioEjUl
/y1VUD00aABI1TUcdLF3BtFN2t/S6HW0hrP3KwbcUfqC25k+GDLh1nM6ZK/gI3Yn
IGtCHxtE3S6jKhE9QcK/H+PzGVKWge9SezeYRP0GHJYDrTVTA8Kt9HgoZPPeReJl
+Ss9c8ThcQKBgECX6HQHFnNzNSufXtSQB7dCoQizvjqTRZPxVRoxDOABIGExVTYt
umUhPtu5AGyJ+/hblEeU+iBRbGg6qRzK8PPwE3E7xey8MYYAI5YjL7YjISKysBUL
AhM6uJ6Jg/wOBSnSx8xZ8kzlS+0izUda1rjKeprCSArSp8IsjlrDxPStAoGAEcPr
+P+altRX5Fhpvmb/Hb8OTif8G+TqjEIdkG9H/W38oP0ywg/3M2RGxcMx7txu8aR5
NjI7zPxZFxF7YvQkY3cLwEsGgVxEI8k6HLIoBXd90Qjlb82NnoqqZY1GWL4HMwo0
L/Rjm6M/Rwje852Hluu0WoIYzXA6F/Q+jPs6nzECgYAxx4IbDiGXuenkwSF1SUyj
NwJXhx4HDh7U6EO/FiPZE5BHE3BoTrFu3o1lzverNk7G3m+j+m1IguEAalHlukYl
rip9iUISlKYqbYZdLBoLwHAfHhszdrjqn8/v6oqbB5yR3HXjPFUWJo0WJ2pqJp56
ZshgmQQ/5Khoj6x0/dMPSg==
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCt7iqkEIco372h
v19q0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQ
dReplpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo7
3paywPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIj
NqM5fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zX
WLpUk6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUn
KfKLns9LAgMBAAECggEAAacPHM2G7GBIm/9rCr6tvihNgD8M685zOOZAqGYn9CqY
cYHC4gtF/L2U6CBj2pNAoCwo3LXUkD+6r7MYKXAgqQg3HTCM4rwFbhD1rU8FVHfh
OL0QwwZ2ut95DVdjoxTAlEN9ZcdSFc//llMJ1cF8lxoVvKFc4cv3uCI2mcaJk858
iABfJLl3yfdv1xtpAuOfXf66sXbAmn5NQfN0qTEg2iOdgb4BUee5Wb35MakDQb6+
/s7/bWB+ublZzYt12ChIh1jkBBHaGyQ8mFnPj99ZAJdFjAzi6ydoJ0a2rCVY7Ugs
bkhnzDUtAaHKxo9JXaqIwbUaVFkX8dDhbg82dJrWUQKBgQDb7hNR0bJFW845N19M
74p2PM+0dIiVzwxAg4E2dXDVe39awO/tw8Vu1o1+NPFhWAzGcidP7pAHmPEgRTVO
7LA2P3CDXpkAEx5E0QW6QWZGqHfSa3+P1AvetvAV+OxtlDphcNeLApY16TUVOKZg
SZlxW2e0dZylbHewgLBTIV9wUQKBgQDKdML+JD18WfenPeowsw8HzKdaw01iGiV1
fvTjEXu6YxPPynWFMuj5gjBQodXM2vv0EsQBAPKYfe0nzRFL2kNuYs7TLoaNxqkp
DNfJ2Ww5OSg7Mp76XgppeKKlsXLyUMYHHrDh6MRi5jvWtiHRpaNmV3cHMRs22c+B
cqKP5Zma2wKBgCPNnS2Lsrbh3C+qWQRgVq0q9zFMa1PgEgGKpwVjlwvaAACZOjX9
0e1aVkx+d/E98U55FPdJQf9Koa58NdJ0a7dZGor4YnYFpr7TPFh2/xxvnpoN0AVt
IsWOCIW7MVohcGOeiChkMmnyXibnQwaX1LgEhlx1bRvtDYsZWBsgarYRAoGAARvo
oYnDSHYZtDHToZapg2pslEOzndD02ZLrdn73BYtbZWz/fc5MlmlPKHHqgOfGL40W
w8akjY9LCEfIS3kTm3wxE9kSZZ5r+MyYNgPZ4upcPQ7G7iortm4xveSd85PbsdhK
McKbqMsIEuIGh2Z34ayi+0galQ9WYqglGdKxJ7cCgYEAuSPBHa+en0xaraZNRvMk
OfV9Su/wrpR3TXSeo0E1mZHLwq1JwulpfO1SjxTH5uOJtG0tusl122wfm0KjrXUO
vG5/It+X4u1Nv9oWj+z1+EV4fQrQ/Coqcc1r+5w1yzfURkKlHh74jbK5Yy/KfXrE
eqbbJD40tKhY8ho15D3iCSo=
-----END PRIVATE KEY-----

View File

@@ -1,23 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID5jCCAs6gAwIBAgIUN7coIsdMcLo9amZfkwogu0YkeLEwDQYJKoZIhvcNAQEL
MIIEDDCCAvSgAwIBAgIUbddWE2woW5e96uC4S2fd2M0AsFAwDQYJKoZIhvcNAQEL
BQAwfjELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVN0YXRlMREwDwYDVQQHDAhMb2Nh
dGlvbjEaMBgGA1UECgwRT3JnYW5pemF0aW9uIE5hbWUxHDAaBgNVBAsME09yZ2Fu
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA5MjExNDE2
MjNaFw0yNDA5MjAxNDE2MjNaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjAyMTMyMzEx
MjlaFw0zNjAyMTEyMzExMjlaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
ZTERMA8GA1UEBwwITG9jYXRpb24xGjAYBgNVBAoMEU9yZ2FuaXphdGlvbiBOYW1l
MRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzOJskt6VkEJYXKSJ
v/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwVx16Q
0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+UXUO
zSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb8MsD
mT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo1EHv
YSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1JoEUj
rLKtAgMBAAGjXDBaMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggrBgEFBQcD
ATAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNzx4Rfs9m8XR5ML0WsI
sorKmB4PMA0GCSqGSIb3DQEBCwUAA4IBAQB87iQy8R0fiOky9WTcyzVeMaavS3MX
iTe1BRn1OCyDq+UiwwoNz7zdzZJFEmRtFBwPNFOe4HzLu6E+7yLFR552eYRHlqIi
/fiLb5JiZfPtokUHeqwELWBsoXtU8vKxViPiLZ09jkWOPZWo7b/xXd6QYykBfV91
usUXLzyTD2orMagpqNksLDGS3p3ggHEJBZtRZA8R7kPEw98xZHznOQpr26iv8kYz
ZWdLFoFdwgFBSfxePKax5rfo+FbwdrcTX0MhbORyiu2XsBAghf8s2vKDkHg2UQE8
haonxFYMFaASfaZ/5vWKYDTCJkJ67m/BtkpRafFEO+ad1i1S61OjfxH4
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt7iqkEIco372hv19q
0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQdRep
lpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo73pay
wPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIjNqM5
fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zXWLpU
k6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUnKfKL
ns9LAgMBAAGjgYEwfzAdBgNVHQ4EFgQUQCpSY7ODhdyD6pdZHvfHoWRXWsIwHwYD
VR0jBBgwFoAUQCpSY7ODhdyD6pdZHvfHoWRXWsIwDwYDVR0TAQH/BAUwAwEB/zAs
BgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJ
KoZIhvcNAQELBQADggEBAGKTIzGQsOqfD0+x15F2cu7FKjIo1ua0OiILAhPqGX65
kGcetjC/dJip2bGnw1NjG9WxEJNZ4YcsGrwh9egfnXXmfHNL0wzx/LTo2oysbXsN
nEj+cmzw3Lwjn/ywJc+AC221/xrmDfm3m/hMzLqncnj23ZAHqkXTSp5UtSMs+UDQ
my0AJOvsDGPVKHQsAX3JDjKHaoVJn4YqpHcIGmpjrNcQSvwUocDHPcC0ywco6SgF
Ylzy2bwWWdPd9Cz9JkAMb95nWc7Rwf/nxAqCjJFzKEisvrx7VZ+QSVI0nqJzt8V1
pbtWYH5gMFVstU3ghWdSLbAk4XufGYrIWAlA5mqjQ4o=
-----END CERTIFICATE-----

View File

@@ -221,8 +221,14 @@ describe.concurrent(() => {
["''", "''"],
['""', '""'],
])("test proxy env, http_proxy=%s https_proxy=%s", async (http_proxy, https_proxy) => {
using localServer = Bun.serve({
port: 0,
fetch() {
return new Response("OK");
},
});
const { exited, stderr: stream } = Bun.spawn({
cmd: [bunExe(), "-e", 'await fetch("https://example.com")'],
cmd: [bunExe(), "-e", `await fetch("${localServer.url}")`],
env: {
...bunEnv,
http_proxy: http_proxy,

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

@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// Regression test for use-after-poison in builtin OutputTask callbacks
// inside command substitution $().
//
// The bug: output_waiting was only incremented for async writes but
// output_done was always incremented, so when stdout is sync (.pipe
// in cmdsub) the counter check `output_done >= output_waiting` fires
// prematurely, calling done() and freeing the builtin while IOWriter
// callbacks are still pending.
//
// Repro requires many ls tasks with errors — listing many entries
// alongside non-existent paths reliably triggers the ASAN
// use-after-poison.
describe("builtins in command substitution with errors should not crash", () => {
test("ls with errors in command substitution", async () => {
// Create a temp directory with many files to produce output,
// and include non-existent paths to produce errors.
const files: Record<string, string> = {};
for (let i = 0; i < 50; i++) {
files[`file${i}.txt`] = `content${i}`;
}
using dir = tempDir("shell-cmdsub", files);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
await $\`echo $(ls $TEST_DIR/* /nonexistent_path_1 /nonexistent_path_2)\`;
console.log("done");
`,
],
env: { ...bunEnv, TEST_DIR: String(dir) },
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("done");
expect(exitCode).toBe(0);
});
});

View File

@@ -0,0 +1,85 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("seq inf does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq inf\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test("seq nan does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq nan\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test("seq -inf does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq -- -inf\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test('[[ -d "" ]] does not crash', async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`[[ -d "" ]]\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
}, 10_000);
test('[[ -f "" ]] does not crash', async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`[[ -f "" ]]\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
}, 10_000);

View File

@@ -1,7 +1,7 @@
import { spawnSync } from "bun";
import { constants, Database, SQLiteError } from "bun:sqlite";
import { describe, expect, it } from "bun:test";
import { existsSync, readdirSync, realpathSync, writeFileSync } from "fs";
import { readdirSync, realpathSync } from "fs";
import { bunEnv, bunExe, isMacOS, isMacOSVersionAtLeast, isWindows, tempDirWithFiles } from "harness";
import { tmpdir } from "os";
import path from "path";
@@ -846,13 +846,15 @@ it("db.transaction()", () => {
// this bug was fixed by ensuring FinalObject has no more than 64 properties
it("inlineCapacity #987", async () => {
const path = tmpbase + "bun-987.db";
if (!existsSync(path)) {
const arrayBuffer = await (await fetch("https://github.com/oven-sh/bun/files/9265429/logs.log")).arrayBuffer();
writeFileSync(path, arrayBuffer);
}
const db = new Database(path);
const db = new Database(":memory:");
// Create schema matching the original regression test (media + logs tables)
db.exec(`
CREATE TABLE media (id INTEGER PRIMARY KEY, mid TEXT, name TEXT, url TEXT, duration INTEGER);
CREATE TABLE logs (mid INTEGER, duration INTEGER, start INTEGER, did TEXT, vid TEXT);
INSERT INTO media VALUES (1, 'm1', 'Test Media', 'http://test', 120);
INSERT INTO logs VALUES (1, 60, 1654100000, 'd1', 'v1');
INSERT INTO logs VALUES (1, 45, 1654200000, 'd2', 'v2');
`);
const query = `SELECT
media.mid,

View File

@@ -1,7 +1,20 @@
import { tls } from "harness";
import https from "node:https";
using server = Bun.serve({
port: 0,
tls,
fetch() {
return new Response("OK");
},
});
const { promise, resolve, reject } = Promise.withResolvers();
const client = https.request("https://example.com/", { agent: false });
const client = https.request(`https://localhost:${server.port}/`, {
agent: false,
ca: tls.cert,
rejectUnauthorized: true,
});
client.on("error", reject);
client.on("close", resolve);
client.end();

View File

@@ -212,10 +212,16 @@ describe("async context passes through", () => {
expect(s.getStore()).toBe(undefined);
});
test("fetch", async () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK");
},
});
const s = new AsyncLocalStorage<string>();
await s.run("value", async () => {
expect(s.getStore()).toBe("value");
const response = await fetch("https://bun.sh") //
const response = await fetch(server.url) //
.then(r => {
expect(s.getStore()).toBe("value");
return true;

View File

@@ -5,7 +5,7 @@
*
* A handful of older tests do not run in Node in this file. These tests should be updated to run in Node, or deleted.
*/
import { bunEnv, bunExe, exampleSite, randomPort } from "harness";
import { bunEnv, bunExe, exampleSite, randomPort, tls as tlsCert } from "harness";
import { createTest } from "node-harness";
import { EventEmitter, once } from "node:events";
import nodefs, { unlinkSync } from "node:fs";
@@ -1081,9 +1081,19 @@ describe("node:http", () => {
});
test("should not decompress gzip, issue#4397", async () => {
using server = Bun.serve({
port: 0,
tls: tlsCert,
fetch() {
const body = Bun.gzipSync(Buffer.from("<html>Hello</html>"));
return new Response(body, {
headers: { "content-encoding": "gzip" },
});
},
});
const { promise, resolve } = Promise.withResolvers();
https
.request("https://bun.sh/", { headers: { "accept-encoding": "gzip" } }, res => {
.request(server.url, { ca: tlsCert.cert, headers: { "accept-encoding": "gzip" } }, res => {
res.on("data", function cb(chunk) {
resolve(chunk);
res.off("data", cb);
@@ -1632,6 +1642,75 @@ describe("HTTP Server Security Tests - Advanced", () => {
});
});
describe("Response Splitting Protection", () => {
test("rejects CRLF in statusMessage set via property assignment followed by res.end()", async () => {
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Error>();
server.on("request", (req, res) => {
res.statusCode = 200;
res.statusMessage = "OK\r\nSet-Cookie: admin=true";
try {
res.end("body");
} catch (e: any) {
resolveError(e);
res.statusMessage = "OK";
res.end("safe");
}
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
const err = await errorPromise;
expect((err as any).code).toBe("ERR_INVALID_CHAR");
// The injected Set-Cookie header must NOT appear in the response
expect(response).not.toInclude("Set-Cookie: admin=true");
});
test("rejects CRLF in statusMessage set via property assignment followed by res.write()", async () => {
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Error>();
server.on("request", (req, res) => {
res.statusCode = 200;
res.statusMessage = "OK\r\nX-Injected: evil";
try {
res.write("chunk");
} catch (e: any) {
resolveError(e);
res.statusMessage = "OK";
res.end("safe");
}
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
const err = await errorPromise;
expect((err as any).code).toBe("ERR_INVALID_CHAR");
expect(response).not.toInclude("X-Injected: evil");
});
test("rejects CRLF in statusMessage passed to writeHead()", async () => {
server.on("request", (req, res) => {
expect(() => {
res.writeHead(200, "OK\r\nX-Injected: evil");
}).toThrow(/Invalid character in statusMessage/);
res.writeHead(200, "OK");
res.end("safe");
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
expect(response).not.toInclude("X-Injected");
expect(response).toInclude("safe");
});
test("allows valid statusMessage without control characters", async () => {
server.on("request", (req, res) => {
res.statusCode = 200;
res.statusMessage = "Everything Is Fine";
res.end("ok");
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
expect(response).toInclude("200 Everything Is Fine");
expect(response).toInclude("ok");
});
});
test("Server should not crash in clientError is emitted when calling destroy", async () => {
await using server = http.createServer(async (req, res) => {
res.end("Hello World");

View File

@@ -821,60 +821,74 @@ for (const nodeExecutable of [nodeExe(), bunExe()]) {
expect(typeof settings.maxHeaderListSize).toBe("number");
expect(typeof settings.maxHeaderSize).toBe("number");
};
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
client.on("error", reject);
expect(client.connecting).toBeTrue();
expect(client.alpnProtocol).toBeUndefined();
expect(client.encrypted).toBeTrue();
expect(client.closed).toBeFalse();
expect(client.destroyed).toBeFalse();
expect(client.originSet.length).toBe(1);
expect(client.pendingSettingsAck).toBeTrue();
assertSettings(client.localSettings);
expect(client.remoteSettings).toBeNull();
const headers = { ":path": "/" };
const req = client.request(headers);
expect(req.closed).toBeFalse();
expect(req.destroyed).toBeFalse();
// we always asign a stream id to the request
expect(req.pending).toBeFalse();
expect(typeof req.id).toBe("number");
expect(req.session).toBeDefined();
expect(req.sentHeaders).toEqual({
":authority": "www.example.com",
":method": "GET",
":path": "/",
":scheme": "https",
const h2Server = http2.createSecureServer({ ...TLS_CERT, allowHTTP1: false });
h2Server.on("stream", (stream, headers) => {
stream.respond({ ":status": 200 });
stream.end("OK");
});
expect(req.sentTrailers).toBeUndefined();
expect(req.sentInfoHeaders.length).toBe(0);
expect(req.scheme).toBe("https");
let response_headers = null;
req.on("response", (headers, flags) => {
response_headers = headers;
});
req.resume();
req.on("end", () => {
resolve();
});
await promise;
expect(response_headers[":status"]).toBe(200);
const settings = client.remoteSettings;
const localSettings = client.localSettings;
assertSettings(settings);
assertSettings(localSettings);
expect(settings).toEqual(client.remoteSettings);
expect(localSettings).toEqual(client.localSettings);
client.destroy();
expect(client.connecting).toBeFalse();
expect(client.alpnProtocol).toBe("h2");
expect(client.pendingSettingsAck).toBeFalse();
expect(client.destroyed).toBeTrue();
expect(client.closed).toBeTrue();
expect(req.closed).toBeTrue();
expect(req.destroyed).toBeTrue();
expect(req.rstCode).toBe(http2.constants.NGHTTP2_NO_ERROR);
const { promise: listenPromise, resolve: listenResolve } = Promise.withResolvers();
h2Server.listen(0, () => listenResolve());
await listenPromise;
const serverAddress = h2Server.address();
const serverUrl = `https://localhost:${serverAddress.port}`;
try {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect(serverUrl, TLS_OPTIONS);
client.on("error", reject);
expect(client.connecting).toBeTrue();
expect(client.alpnProtocol).toBeUndefined();
expect(client.encrypted).toBeTrue();
expect(client.closed).toBeFalse();
expect(client.destroyed).toBeFalse();
expect(client.originSet.length).toBe(1);
expect(client.pendingSettingsAck).toBeTrue();
assertSettings(client.localSettings);
expect(client.remoteSettings).toBeNull();
const headers = { ":path": "/" };
const req = client.request(headers);
expect(req.closed).toBeFalse();
expect(req.destroyed).toBeFalse();
// we always asign a stream id to the request
expect(req.pending).toBeFalse();
expect(typeof req.id).toBe("number");
expect(req.session).toBeDefined();
expect(req.sentHeaders).toEqual({
":authority": `localhost:${serverAddress.port}`,
":method": "GET",
":path": "/",
":scheme": "https",
});
expect(req.sentTrailers).toBeUndefined();
expect(req.sentInfoHeaders.length).toBe(0);
expect(req.scheme).toBe("https");
let response_headers = null;
req.on("response", (headers, flags) => {
response_headers = headers;
});
req.resume();
req.on("end", () => {
resolve();
});
await promise;
expect(response_headers[":status"]).toBe(200);
const settings = client.remoteSettings;
const localSettings = client.localSettings;
assertSettings(settings);
assertSettings(localSettings);
expect(settings).toEqual(client.remoteSettings);
expect(localSettings).toEqual(client.localSettings);
client.destroy();
expect(client.connecting).toBeFalse();
expect(client.alpnProtocol).toBe("h2");
expect(client.pendingSettingsAck).toBeFalse();
expect(client.destroyed).toBeTrue();
expect(client.closed).toBeTrue();
expect(req.closed).toBeTrue();
expect(req.destroyed).toBeTrue();
expect(req.rstCode).toBe(http2.constants.NGHTTP2_NO_ERROR);
} finally {
h2Server.close();
}
});
it("ping events should work", async () => {
await using server = await nodeEchoServer(paddingStrategy);

View File

@@ -1,87 +0,0 @@
import { describe, expect, test } from "bun:test";
import { once } from "node:events";
import { createServer } from "node:net";
describe("fetch with many headers", () => {
test("should not crash or corrupt memory with more than 256 headers", async () => {
// Use a raw TCP server to avoid uws header count limits on the server side.
// We just need to verify that the client sends the request without crashing.
await using server = createServer(socket => {
let data = "";
socket.on("data", (chunk: Buffer) => {
data += chunk.toString();
// Wait for the end of HTTP headers (double CRLF)
if (data.includes("\r\n\r\n")) {
// Count headers (lines between the request line and the blank line)
const headerSection = data.split("\r\n\r\n")[0];
const lines = headerSection.split("\r\n");
// First line is the request line (GET / HTTP/1.1), rest are headers
const headerCount = lines.length - 1;
const body = String(headerCount);
const response = ["HTTP/1.1 200 OK", `Content-Length: ${body.length}`, "Connection: close", "", body].join(
"\r\n",
);
socket.write(response);
socket.end();
}
});
}).listen(0);
await once(server, "listening");
const port = (server.address() as any).port;
// Build 300 unique custom headers (exceeds the 256-entry static buffer)
const headers = new Headers();
const headerCount = 300;
for (let i = 0; i < headerCount; i++) {
headers.set(`x-custom-${i}`, `value-${i}`);
}
const res = await fetch(`http://localhost:${port}/`, { headers });
const receivedCount = parseInt(await res.text(), 10);
expect(res.status).toBe(200);
// The server should receive our custom headers plus default ones
// (host, connection, user-agent, accept, accept-encoding = 5 extra)
expect(receivedCount).toBeGreaterThanOrEqual(headerCount);
});
test("should handle exactly 256 user headers without issues", async () => {
await using server = createServer(socket => {
let data = "";
socket.on("data", (chunk: Buffer) => {
data += chunk.toString();
if (data.includes("\r\n\r\n")) {
const headerSection = data.split("\r\n\r\n")[0];
const lines = headerSection.split("\r\n");
const headerCount = lines.length - 1;
const body = String(headerCount);
const response = ["HTTP/1.1 200 OK", `Content-Length: ${body.length}`, "Connection: close", "", body].join(
"\r\n",
);
socket.write(response);
socket.end();
}
});
}).listen(0);
await once(server, "listening");
const port = (server.address() as any).port;
const headers = new Headers();
const headerCount = 256;
for (let i = 0; i < headerCount; i++) {
headers.set(`x-custom-${i}`, `value-${i}`);
}
const res = await fetch(`http://localhost:${port}/`, { headers });
const receivedCount = parseInt(await res.text(), 10);
expect(res.status).toBe(200);
expect(receivedCount).toBeGreaterThanOrEqual(headerCount);
});
});

View File

@@ -577,19 +577,27 @@ describe("fetch", () => {
});
it.concurrent('redirect: "follow"', async () => {
using target = Bun.serve({
port: 0,
tls,
fetch() {
return new Response("redirected!");
},
});
using server = Bun.serve({
port: 0,
fetch(req) {
return new Response(null, {
status: 302,
headers: {
Location: "https://example.com",
Location: target.url.href,
},
});
},
});
const response = await fetch(`http://${server.hostname}:${server.port}`, {
redirect: "follow",
tls: { ca: tls.cert },
});
expect(response.status).toBe(200);
expect(response.headers.get("location")).toBe(null);
@@ -734,8 +742,15 @@ it.concurrent("simultaneous HTTPS fetch", async () => {
});
it.concurrent("website with tlsextname", async () => {
// irony
await fetch("https://bun.sh", { method: "HEAD" });
using server = Bun.serve({
port: 0,
tls,
fetch() {
return new Response("OK");
},
});
const resp = await fetch(server.url, { method: "HEAD", tls: { ca: tls.cert } });
expect(resp.status).toBe(200);
});
function testBlobInterface(blobbyConstructor: { (..._: any[]): any }, hasBlobFn?: boolean) {

View File

@@ -30,94 +30,110 @@ async function createServer(cert: TLSOptions, callback: (port: number) => Promis
describe.concurrent("fetch-tls", () => {
it("can handle multiple requests with non native checkServerIdentity", async () => {
async function request() {
let called = false;
const result = await fetch("https://www.example.com", {
keepalive: false,
tls: {
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
called = true;
return tls.checkServerIdentity(hostname, cert);
},
},
}).then((res: Response) => res.blob());
expect(result?.size).toBeGreaterThan(0);
expect(called).toBe(true);
}
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(request());
}
await Promise.all(promises);
});
it("fetch with valid tls should not throw", async () => {
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
const result = await fetch(url, { keepalive: false }).then((res: Response) => res.blob());
expect(result?.size).toBeGreaterThan(0);
});
await Promise.all(promises);
});
it("fetch with valid tls and non-native checkServerIdentity should work", async () => {
for (const isBusy of [true, false]) {
let count = 0;
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
await fetch(url, {
await createServer(CERT_LOCALHOST_IP, async port => {
async function request() {
let called = false;
const result = await fetch(`https://localhost:${port}`, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
expect(url).toContain(hostname);
called = true;
return tls.checkServerIdentity(hostname, cert);
},
},
}).then((res: Response) => res.blob());
});
if (isBusy) {
const start = performance.now();
while (performance.now() - start < 500) {}
expect(result?.size).toBeGreaterThan(0);
expect(called).toBe(true);
}
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(request());
}
await Promise.all(promises);
expect(count).toBe(2);
}
});
});
it("fetch with valid tls should not throw", async () => {
await createServer(CERT_LOCALHOST_IP, async port => {
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
const promises = urls.map(async url => {
const result = await fetch(url, { keepalive: false, tls: { ca: validTls.cert } }).then((res: Response) =>
res.blob(),
);
expect(result?.size).toBeGreaterThan(0);
});
await Promise.all(promises);
});
});
it("fetch with valid tls and non-native checkServerIdentity should work", async () => {
let count = 0;
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
await fetch(url, {
keepalive: false,
tls: {
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
expect(url).toContain(hostname);
throw new Error("CustomError");
},
},
});
await createServer(CERT_LOCALHOST_IP, async port => {
for (const isBusy of [true, false]) {
let count = 0;
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
const promises = urls.map(async url => {
await fetch(url, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
return tls.checkServerIdentity(hostname, cert);
},
},
}).then((res: Response) => res.blob());
});
if (isBusy) {
const start = performance.now();
while (performance.now() - start < 500) {}
}
await Promise.all(promises);
expect(count).toBe(2);
}
});
});
it("fetch with valid tls and non-native checkServerIdentity that throws should reject", async () => {
await createServer(CERT_LOCALHOST_IP, async port => {
let count = 0;
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
const promises = urls.map(async url => {
await fetch(url, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
throw new Error("CustomError");
},
},
});
});
const start = performance.now();
while (performance.now() - start < 1000) {}
expect((await Promise.allSettled(promises)).every(p => p.status === "rejected")).toBe(true);
expect(count).toBe(2);
});
const start = performance.now();
while (performance.now() - start < 1000) {}
expect((await Promise.allSettled(promises)).every(p => p.status === "rejected")).toBe(true);
expect(count).toBe(2);
});
it("fetch with rejectUnauthorized: false should not call checkServerIdentity", async () => {
let count = 0;
await createServer(CERT_LOCALHOST_IP, async port => {
let count = 0;
await fetch("https://example.com", {
keepalive: false,
tls: {
rejectUnauthorized: false,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
return tls.checkServerIdentity(hostname, cert);
await fetch(`https://localhost:${port}`, {
keepalive: false,
tls: {
rejectUnauthorized: false,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
return tls.checkServerIdentity(hostname, cert);
},
},
},
}).then((res: Response) => res.blob());
expect(count).toBe(0);
}).then((res: Response) => res.blob());
expect(count).toBe(0);
});
});
it("fetch with self-sign tls should throw", async () => {
@@ -152,20 +168,23 @@ describe.concurrent("fetch-tls", () => {
});
it("fetch with checkServerIdentity failing should throw", async () => {
try {
await fetch(`https://example.com`, {
keepalive: false,
tls: {
checkServerIdentity() {
return new Error("CustomError");
await createServer(CERT_LOCALHOST_IP, async port => {
try {
await fetch(`https://localhost:${port}`, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity() {
return new Error("CustomError");
},
},
},
}).then((res: Response) => res.blob());
}).then((res: Response) => res.blob());
expect.unreachable();
} catch (e: any) {
expect(e.message).toBe("CustomError");
}
expect.unreachable();
} catch (e: any) {
expect(e.message).toBe("CustomError");
}
});
});
it("fetch with self-sign certificate tls + rejectUnauthorized: false should not throw", async () => {

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

@@ -109,13 +109,22 @@ describe("HTMLRewriter", () => {
await gcTick();
let content;
{
using contentServer = Bun.serve({
port: 0,
fetch(req) {
return new Response("<h1>Hello from content server</h1>", {
headers: { "Content-Type": "text/html" },
});
},
});
using server = Bun.serve({
port: 0,
fetch(req) {
return new HTMLRewriter()
.on("div", {
async element(element) {
content = await fetch("https://www.example.com/").then(res => res.text());
content = await fetch(`http://localhost:${contentServer.port}/`).then(res => res.text());
element.setInnerContent(content, { html: true });
},
})

View File

@@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIzOJskt6VkEJY
XKSJv/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwV
x16Q0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+
UXUOzSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb
8MsDmT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo
1EHvYSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1J
oEUjrLKtAgMBAAECggEACInVNhaiqu4infZGVMy0rXMV8VwSlapM7O2SLtFsr0nK
XUmaLK6dvGzBPKK9dxdiYCFzPlMKQTkhzsAvYFWSmm3tRmikG+11TFyCRhXLpc8/
ark4vD9Io6ZkmKUmyKLwtXNjNGcqQtJ7RXc7Ga3nAkueN6JKZHqieZusXVeBGQ70
YH1LKyVNBeJggbj+g9rqaksPyNJQ8EWiNTJkTRQPazZ0o1VX/fzDFyr/a5npFtHl
4BHfafv9o1Xyr70Kie8CYYRJNViOCN+ylFs7Gd3XRaAkSkgMT/7DzrHdEM2zrrHK
yNg2gyDVX9UeEJG2X5UtU0o9BVW7WBshz/2hqIUHoQKBgQC8zsRFvC7u/rGr5vRR
mhZZG+Wvg03/xBSuIgOrzm+Qie6mAzOdVmfSL/pNV9EFitXt1yd2ROo31AbS7Evy
Bm/QVKr2mBlmLgov3B7O/e6ABteooOL7769qV/v+yo8VdEg0biHmsfGIIXDe3Lwl
OT0XwF9r/SeZLbw1zfkSsUVG/QKBgQC5fANM3Dc9LEek+6PHv5+eC1cKkyioEjUl
/y1VUD00aABI1TUcdLF3BtFN2t/S6HW0hrP3KwbcUfqC25k+GDLh1nM6ZK/gI3Yn
IGtCHxtE3S6jKhE9QcK/H+PzGVKWge9SezeYRP0GHJYDrTVTA8Kt9HgoZPPeReJl
+Ss9c8ThcQKBgECX6HQHFnNzNSufXtSQB7dCoQizvjqTRZPxVRoxDOABIGExVTYt
umUhPtu5AGyJ+/hblEeU+iBRbGg6qRzK8PPwE3E7xey8MYYAI5YjL7YjISKysBUL
AhM6uJ6Jg/wOBSnSx8xZ8kzlS+0izUda1rjKeprCSArSp8IsjlrDxPStAoGAEcPr
+P+altRX5Fhpvmb/Hb8OTif8G+TqjEIdkG9H/W38oP0ywg/3M2RGxcMx7txu8aR5
NjI7zPxZFxF7YvQkY3cLwEsGgVxEI8k6HLIoBXd90Qjlb82NnoqqZY1GWL4HMwo0
L/Rjm6M/Rwje852Hluu0WoIYzXA6F/Q+jPs6nzECgYAxx4IbDiGXuenkwSF1SUyj
NwJXhx4HDh7U6EO/FiPZE5BHE3BoTrFu3o1lzverNk7G3m+j+m1IguEAalHlukYl
rip9iUISlKYqbYZdLBoLwHAfHhszdrjqn8/v6oqbB5yR3HXjPFUWJo0WJ2pqJp56
ZshgmQQ/5Khoj6x0/dMPSg==
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCt7iqkEIco372h
v19q0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQ
dReplpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo7
3paywPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIj
NqM5fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zX
WLpUk6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUn
KfKLns9LAgMBAAECggEAAacPHM2G7GBIm/9rCr6tvihNgD8M685zOOZAqGYn9CqY
cYHC4gtF/L2U6CBj2pNAoCwo3LXUkD+6r7MYKXAgqQg3HTCM4rwFbhD1rU8FVHfh
OL0QwwZ2ut95DVdjoxTAlEN9ZcdSFc//llMJ1cF8lxoVvKFc4cv3uCI2mcaJk858
iABfJLl3yfdv1xtpAuOfXf66sXbAmn5NQfN0qTEg2iOdgb4BUee5Wb35MakDQb6+
/s7/bWB+ublZzYt12ChIh1jkBBHaGyQ8mFnPj99ZAJdFjAzi6ydoJ0a2rCVY7Ugs
bkhnzDUtAaHKxo9JXaqIwbUaVFkX8dDhbg82dJrWUQKBgQDb7hNR0bJFW845N19M
74p2PM+0dIiVzwxAg4E2dXDVe39awO/tw8Vu1o1+NPFhWAzGcidP7pAHmPEgRTVO
7LA2P3CDXpkAEx5E0QW6QWZGqHfSa3+P1AvetvAV+OxtlDphcNeLApY16TUVOKZg
SZlxW2e0dZylbHewgLBTIV9wUQKBgQDKdML+JD18WfenPeowsw8HzKdaw01iGiV1
fvTjEXu6YxPPynWFMuj5gjBQodXM2vv0EsQBAPKYfe0nzRFL2kNuYs7TLoaNxqkp
DNfJ2Ww5OSg7Mp76XgppeKKlsXLyUMYHHrDh6MRi5jvWtiHRpaNmV3cHMRs22c+B
cqKP5Zma2wKBgCPNnS2Lsrbh3C+qWQRgVq0q9zFMa1PgEgGKpwVjlwvaAACZOjX9
0e1aVkx+d/E98U55FPdJQf9Koa58NdJ0a7dZGor4YnYFpr7TPFh2/xxvnpoN0AVt
IsWOCIW7MVohcGOeiChkMmnyXibnQwaX1LgEhlx1bRvtDYsZWBsgarYRAoGAARvo
oYnDSHYZtDHToZapg2pslEOzndD02ZLrdn73BYtbZWz/fc5MlmlPKHHqgOfGL40W
w8akjY9LCEfIS3kTm3wxE9kSZZ5r+MyYNgPZ4upcPQ7G7iortm4xveSd85PbsdhK
McKbqMsIEuIGh2Z34ayi+0galQ9WYqglGdKxJ7cCgYEAuSPBHa+en0xaraZNRvMk
OfV9Su/wrpR3TXSeo0E1mZHLwq1JwulpfO1SjxTH5uOJtG0tusl122wfm0KjrXUO
vG5/It+X4u1Nv9oWj+z1+EV4fQrQ/Coqcc1r+5w1yzfURkKlHh74jbK5Yy/KfXrE
eqbbJD40tKhY8ho15D3iCSo=
-----END PRIVATE KEY-----

View File

@@ -1,23 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID5jCCAs6gAwIBAgIUN7coIsdMcLo9amZfkwogu0YkeLEwDQYJKoZIhvcNAQEL
MIIEDDCCAvSgAwIBAgIUbddWE2woW5e96uC4S2fd2M0AsFAwDQYJKoZIhvcNAQEL
BQAwfjELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVN0YXRlMREwDwYDVQQHDAhMb2Nh
dGlvbjEaMBgGA1UECgwRT3JnYW5pemF0aW9uIE5hbWUxHDAaBgNVBAsME09yZ2Fu
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA5MjExNDE2
MjNaFw0yNDA5MjAxNDE2MjNaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjAyMTMyMzEx
MjlaFw0zNjAyMTEyMzExMjlaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
ZTERMA8GA1UEBwwITG9jYXRpb24xGjAYBgNVBAoMEU9yZ2FuaXphdGlvbiBOYW1l
MRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzOJskt6VkEJYXKSJ
v/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwVx16Q
0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+UXUO
zSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb8MsD
mT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo1EHv
YSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1JoEUj
rLKtAgMBAAGjXDBaMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggrBgEFBQcD
ATAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNzx4Rfs9m8XR5ML0WsI
sorKmB4PMA0GCSqGSIb3DQEBCwUAA4IBAQB87iQy8R0fiOky9WTcyzVeMaavS3MX
iTe1BRn1OCyDq+UiwwoNz7zdzZJFEmRtFBwPNFOe4HzLu6E+7yLFR552eYRHlqIi
/fiLb5JiZfPtokUHeqwELWBsoXtU8vKxViPiLZ09jkWOPZWo7b/xXd6QYykBfV91
usUXLzyTD2orMagpqNksLDGS3p3ggHEJBZtRZA8R7kPEw98xZHznOQpr26iv8kYz
ZWdLFoFdwgFBSfxePKax5rfo+FbwdrcTX0MhbORyiu2XsBAghf8s2vKDkHg2UQE8
haonxFYMFaASfaZ/5vWKYDTCJkJ67m/BtkpRafFEO+ad1i1S61OjfxH4
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt7iqkEIco372hv19q
0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQdRep
lpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo73pay
wPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIjNqM5
fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zXWLpU
k6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUnKfKL
ns9LAgMBAAGjgYEwfzAdBgNVHQ4EFgQUQCpSY7ODhdyD6pdZHvfHoWRXWsIwHwYD
VR0jBBgwFoAUQCpSY7ODhdyD6pdZHvfHoWRXWsIwDwYDVR0TAQH/BAUwAwEB/zAs
BgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJ
KoZIhvcNAQELBQADggEBAGKTIzGQsOqfD0+x15F2cu7FKjIo1ua0OiILAhPqGX65
kGcetjC/dJip2bGnw1NjG9WxEJNZ4YcsGrwh9egfnXXmfHNL0wzx/LTo2oysbXsN
nEj+cmzw3Lwjn/ywJc+AC221/xrmDfm3m/hMzLqncnj23ZAHqkXTSp5UtSMs+UDQ
my0AJOvsDGPVKHQsAX3JDjKHaoVJn4YqpHcIGmpjrNcQSvwUocDHPcC0ywco6SgF
Ylzy2bwWWdPd9Cz9JkAMb95nWc7Rwf/nxAqCjJFzKEisvrx7VZ+QSVI0nqJzt8V1
pbtWYH5gMFVstU3ghWdSLbAk4XufGYrIWAlA5mqjQ4o=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,134 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// GitHub Issue #25695: Error.captureStackTrace with custom prepareStackTrace
// does not include async continuation frames (awaiter frames), causing NX's
// recursion detection to fail and leading to infinite recursion.
test("Error.captureStackTrace includes async continuation frames in CallSite array", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
let callCount = 0;
function getCallSites() {
const prepareStackTraceBackup = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stackTraces) => stackTraces;
const errorObject = {};
Error.captureStackTrace(errorObject);
const trace = errorObject.stack;
Error.prepareStackTrace = prepareStackTraceBackup;
trace.shift();
return trace;
}
function preventRecursion() {
const stackframes = getCallSites().slice(2);
const found = stackframes.some((f) => {
return f.getFunctionName() === 'outerAsync';
});
if (found) {
throw new Error('Loop detected');
}
}
async function outerAsync() {
callCount++;
if (callCount > 5) {
throw new Error('Safety limit');
}
preventRecursion();
await new Promise(resolve => setTimeout(resolve, 1));
const result = await middleAsync();
return result;
}
async function middleAsync() {
return await innerAsync();
}
async function innerAsync() {
return await outerAsync();
}
try {
await outerAsync();
console.log("BUG:" + callCount);
} catch (e) {
if (e.message === 'Loop detected') {
console.log("OK:" + callCount);
} else {
console.log("FAIL:" + e.message);
}
}
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should detect the recursion, not hit the safety limit
expect(stdout.trim()).toStartWith("OK:");
expect(exitCode).toBe(0);
});
test("Error.captureStackTrace async frames have correct function names", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
function getCallSites() {
const backup = Error.prepareStackTrace;
Error.prepareStackTrace = (_, sites) => sites;
const obj = {};
Error.captureStackTrace(obj);
const trace = obj.stack;
Error.prepareStackTrace = backup;
return trace;
}
let captured = null;
async function alphaAsync() {
await new Promise(resolve => setTimeout(resolve, 1));
const result = await betaAsync();
return result;
}
async function betaAsync() {
return await gammaAsync();
}
async function gammaAsync() {
// Capture the stack trace inside the innermost async function
captured = getCallSites().map(s => s.getFunctionName()).filter(Boolean);
return 42;
}
await alphaAsync();
// The captured stack should include gammaAsync's callers (betaAsync, alphaAsync)
// via async continuation frames
const hasGamma = captured.includes('gammaAsync');
const hasBeta = captured.includes('betaAsync');
const hasAlpha = captured.includes('alphaAsync');
console.log(JSON.stringify({ hasGamma, hasBeta, hasAlpha, frames: captured }));
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const result = JSON.parse(stdout.trim());
// gammaAsync is the current frame (synchronous), should always be present
expect(result.hasGamma).toBe(true);
// betaAsync and alphaAsync are async continuation frames
expect(result.hasBeta).toBe(true);
expect(result.hasAlpha).toBe(true);
expect(exitCode).toBe(0);
});

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,8 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/27014
test("Bun.stripANSI does not hang on non-ANSI control characters", () => {
const s = "\u0016zo\u00BAd\u0019\u00E8\u00E0\u0013?\u00C1+\u0014d\u00D3\u00E9";
const result = Bun.stripANSI(s);
expect(result).toBe(s);
});

View File

@@ -0,0 +1,31 @@
import { expect, test } from "bun:test";
import { X509Certificate } from "crypto";
import { readFileSync } from "fs";
import { join } from "path";
const certPem = readFileSync(join(import.meta.dir, "../../js/node/test/fixtures/keys/agent1-cert.pem"));
test("issuerCertificate should return undefined for directly-parsed certificates without crashing", () => {
const cert = new X509Certificate(certPem);
// issuerCertificate is only populated for certificates obtained from TLS
// connections with a peer certificate chain. For directly parsed certs,
// it should be undefined (matching Node.js behavior).
expect(cert.issuerCertificate).toBeUndefined();
});
test("X509Certificate properties should not crash on valid certificates", () => {
const cert = new X509Certificate(certPem);
// These should all work without segfaulting
expect(cert.subject).toBeDefined();
expect(cert.issuer).toBeDefined();
expect(cert.validFrom).toBeDefined();
expect(cert.validTo).toBeDefined();
expect(cert.fingerprint).toBeDefined();
expect(cert.fingerprint256).toBeDefined();
expect(cert.fingerprint512).toBeDefined();
expect(cert.serialNumber).toBeDefined();
expect(cert.raw).toBeInstanceOf(Uint8Array);
expect(cert.ca).toBe(false);
});

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"');
});
});