Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b9d1724
Fix regressions since 1.4.3 in bun install, bunx, node:fs and node:http2
Jarred-Sumner Oct 21, 2026
6bc11f1
Merge branch 'main' into claude/hardening, and address the reviews
Jarred-Sumner Oct 21, 2026
7c2444f
install: an entry removed or changed next to its optional twin is a c…
Jarred-Sumner Oct 21, 2026
fb39dcb
Update brotli to v1.2.1; bunx: pass --ignore-scripts on to the instal…
Jarred-Sumner Oct 21, 2026
861b0f7
http3: enforce the advertised header size limit
Jarred-Sumner Oct 21, 2026
3e4b56e
fetch http3 test: send the header set from Bun.serve
Jarred-Sumner Oct 21, 2026
f4e5088
http3: return the value lsquic documents for closing the connection
Jarred-Sumner Oct 21, 2026
70da7c8
fetch http3: advertise the header size limit that one field can hold
Jarred-Sumner Oct 21, 2026
27bb326
install: accept the optional peer of a 1.4 bun.lockb, and let a file:…
claude Oct 21, 2026
35c3c56
Merge remote-tracking branch 'origin/main' into claude/hardening
claude Oct 21, 2026
e5be1ee
http3: refuse a header that is too long to send, and reset the stream…
Jarred-Sumner Oct 22, 2026
33aaad7
node:http2: the same session teardown on a plain socket; http3: limit…
Jarred-Sumner Oct 22, 2026
c23e644
fetch http3: do not send a written POST or PATCH a second time (from …
Jarred-Sumner Oct 22, 2026
c5fa3b8
node:http2: a server's session on a plain socket reports its error again
Jarred-Sumner Oct 22, 2026
e0d367e
node:http2: a server's session reports its error after its client lef…
Jarred-Sumner Oct 22, 2026
9e44330
node:http2: one helper dispatches an event with its values; http3: th…
Jarred-Sumner Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/pm/overrides.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Add `bar` to the `"overrides"` field in `package.json`. Bun defers to the specif

Bun only reads overrides from the root `package.json`, not from workspace packages. Overrides apply to `peerDependencies` as well.

Every install also checks the lockfile against the overrides whose value is a version or a range. If the lockfile has a version that such an override does not allow (after a merge, or a migration from another package manager), `bun install` resolves that package again, and `bun install --frozen-lockfile` fails and names it. The check leaves these entries as they are:
`bun install` also checks the lockfile against the overrides whose value is a version or a range. If the lockfile has a version that such an override does not allow (after a merge, or a migration from another package manager), `bun install` resolves that package again. `bun install --frozen-lockfile` installs the version in the lockfile. The check leaves these entries as they are:

- a bundled dependency and an optional peer dependency
- a peer or optional dependency that shares its version with a dependency the override does not apply to
Expand Down
7 changes: 4 additions & 3 deletions packages/bun-usockets/src/node_quic_shim.c
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,11 @@ int us_nq_stream_send_headers(lsquic_stream_t *s, const char *buf, size_t len,
if (i >= len) break;
i++;
unsigned char flags = (i < len) ? (unsigned char) buf[i++] : 0;
if (name_len > LSXPACK_MAX_STRLEN || val_len > LSXPACK_MAX_STRLEN)
if (name_len >= LSXPACK_MAX_STRLEN || val_len > LSXPACK_MAX_STRLEN)
return -1;
lsxpack_header_set_offset2(&hdrs[count], buf, name_off, name_len,
val_off, val_len);
/* lsxpack asserts that offsets fit lsxpack_strlen_t, so they count from this pair's start. */
lsxpack_header_set_offset2(&hdrs[count], buf + name_off, 0, name_len,
val_off - name_off, val_len);
if (flags & 1) {
hdrs[count].flags = LSXPACK_NEVER_INDEX;
hdrs[count].indexed_type = 2;
Expand Down
95 changes: 76 additions & 19 deletions packages/bun-usockets/src/quic.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ extern int us_ssl_ctx_set_verify_signature_algorithms(SSL_CTX *ctx);
extern int us_x509_verify_cert(X509_STORE_CTX *ctx, int purpose);
extern struct us_bun_verify_error_t us_ssl_socket_verify_error_from_ssl(SSL *ssl);

extern size_t BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;

#define US_QUIC_READ_BUF (16 * 1024)
/* A header set over max_size or max_fields fails its stream. One over this
* multiple of either closes the connection, as in Http2Context.h, which has
* the same floor for the size. */
#define US_QUIC_HSET_HARD_CAP_FACTOR 2
Comment thread
Jarred-Sumner marked this conversation as resolved.
Resolved
#define US_QUIC_HSET_HARD_CAP_FLOOR (16 * 1024)

/* Incoming header set: contiguous storage + index. Created before the
* stream object exists (lsquic decodes headers ahead of on_new_stream),
Expand All @@ -44,6 +51,11 @@ struct us_quic_hset {
struct us_quic_header_t *headers;
unsigned int count, hcap;
int is_server;
/* Size as RFC 9114 §4.2.2 counts it, and the context's max_header_set_size. */
size_t size, max_size;
/* Fields decoded so far, pseudo-headers and dropped ones included. */
size_t fields, max_fields;
int too_large;
};

struct us_quic_socket_context_s {
Expand All @@ -59,6 +71,8 @@ struct us_quic_socket_context_s {
* us_quic_engine_leave finishes it. */
int shutdown_pending;
int is_client;
/* lsquic only advertises es_max_header_list_size. This is what us_quic_hsi_process enforces. */
unsigned int max_header_set_size;
unsigned int conn_count;
unsigned int conn_ext_size;
/* Stream bytes written since the last process_conns. Once this exceeds
Expand Down Expand Up @@ -468,10 +482,19 @@ static int us_quic_alpn_select(SSL *ssl, const unsigned char **out, unsigned cha

/* ───── header-set interface ───── */

static size_t us_quic_hset_hard_cap(const struct us_quic_hset *h) {
size_t cap = US_QUIC_HSET_HARD_CAP_FACTOR * h->max_size;
return cap > US_QUIC_HSET_HARD_CAP_FLOOR ? cap : US_QUIC_HSET_HARD_CAP_FLOOR;
}

static void *us_quic_hsi_create(void *hsi_ctx, lsquic_stream_t *s, int is_push) {
(void) s; (void) is_push;
struct us_quic_hset *h = (struct us_quic_hset *) us_calloc(1, sizeof(struct us_quic_hset));
if (h) h->is_server = !((us_quic_socket_context_t *) hsi_ctx)->is_client;
if (!h) return NULL;
h->is_server = !((us_quic_socket_context_t *) hsi_ctx)->is_client;
h->max_size = ((us_quic_socket_context_t *) hsi_ctx)->max_header_set_size;
/* Only max_size bounds the fields of a response, as in the HTTP/2 client. */
h->max_fields = h->is_server ? US_QUIC_MAX_REQUEST_FIELDS : h->max_size;
return h;
}

Expand Down Expand Up @@ -499,24 +522,26 @@ static int us_quic_field_is_malformed(const struct lsxpack_header *hdr) {

static struct lsxpack_header *us_quic_hsi_prepare(void *hset_p, struct lsxpack_header *hdr, size_t space) {
struct us_quic_hset *h = (struct us_quic_hset *) hset_p;
if (space > 64 * 1024) return NULL;
unsigned int need = h->len + (unsigned int) space;
/* `space` is lsqpack's estimate and can exceed what the field decodes to. */
size_t need = h->len + space;
if (space > LSXPACK_MAX_STRLEN || need > us_quic_hset_hard_cap(h)) return NULL;
if (need > h->cap) {
unsigned int ncap = h->cap ? h->cap : 512;
size_t ncap = h->cap ? h->cap : 512;
while (ncap < need) ncap *= 2;
char *nb = (char *) us_realloc(h->buf, ncap);
if (!nb) return NULL;
h->buf = nb;
h->cap = ncap;
h->cap = (unsigned int) ncap;
}
if (hdr == NULL) {
hdr = &h->scratch;
lsxpack_header_prepare_decode(hdr, h->buf, h->len, space);
/* Offsets count from this field's start: lsxpack asserts that they fit lsxpack_strlen_t. */
lsxpack_header_prepare_decode(hdr, h->buf + h->len, 0, space);
} else {
/* Resize: lsqpack already wrote part of name/value into the previous
* buffer; only the storage may move. Preserve offsets, repoint buf,
* and report the larger window via val_len. */
hdr->buf = h->buf;
hdr->buf = h->buf + h->len;
hdr->val_len = (lsxpack_strlen_t) space;
}
return hdr;
Expand All @@ -525,6 +550,15 @@ static struct lsxpack_header *us_quic_hsi_prepare(void *hset_p, struct lsxpack_h
static int us_quic_hsi_process(void *hset_p, struct lsxpack_header *hdr) {
struct us_quic_hset *h = (struct us_quic_hset *) hset_p;
if (hdr == NULL) return 0; /* end of headers */
h->size += (size_t) hdr->name_len + hdr->val_len + 32;
h->fields++;
if (h->size > h->max_size || h->fields > h->max_fields) {
/* Drop the field but let lsqpack finish the block, so that it still
* acknowledges the block and the connection stays usable. */
h->too_large = 1;
return h->size > us_quic_hset_hard_cap(h) ||
h->fields > US_QUIC_HSET_HARD_CAP_FACTOR * h->max_fields ? -1 : 0;
}
if (h->is_server && us_quic_field_is_malformed(hdr)) return 1;
if (h->count == h->hcap) {
unsigned int ncap = h->hcap ? h->hcap * 2 : 16;
Expand All @@ -540,7 +574,7 @@ static int us_quic_hsi_process(void *hset_p, struct lsxpack_header *hdr) {
* and :scheme are URI components, not field values, and stay as sent.
* :authority is trimmed because the request exposes it as the Host field. */
const char *name = lsxpack_header_get_name(hdr);
unsigned int val_offset = hdr->val_offset, val_len = hdr->val_len;
unsigned int val_offset = h->len + hdr->val_offset, val_len = hdr->val_len;
int is_pseudo = hdr->name_len > 0 && name[0] == ':';
if (!is_pseudo || (hdr->name_len == 10 && memcmp(name, ":authority", 10) == 0)) {
const char *val = lsxpack_header_get_value(hdr);
Expand All @@ -550,13 +584,13 @@ static int us_quic_hsi_process(void *hset_p, struct lsxpack_header *hdr) {
/* lsxpack wrote name+value into h->buf at h->len; record offsets, then
* advance len so the next header lands after this one. We store offsets
* (cast to pointer-sized) and resolve them after the buffer stops moving. */
h->headers[h->count].name = (const char *)(uintptr_t) hdr->name_offset;
h->headers[h->count].name = (const char *)(uintptr_t) (h->len + hdr->name_offset);
h->headers[h->count].name_len = hdr->name_len;
h->headers[h->count].value = (const char *)(uintptr_t) val_offset;
h->headers[h->count].value_len = val_len;
h->headers[h->count].qpack_index = -1;
h->count++;
h->len = (unsigned int) hdr->val_offset + hdr->val_len + hdr->dec_overhead;
h->len += (unsigned int) hdr->val_offset + hdr->val_len + hdr->dec_overhead;
return 0;
}

Expand Down Expand Up @@ -813,10 +847,10 @@ us_quic_socket_context_t *us_create_quic_socket_context(
lsquic_engine_init_settings(&ctx->settings, LSENG_HTTP_SERVER);
ctx->settings.es_versions = LSQUIC_DF_VERSIONS & LSQUIC_IETF_VERSIONS;
ctx->settings.es_ecn = 0;
/* QPACK can expand small dynamic-table refs into large header lists; cap
* the post-decode size at the same order as uWS H1's MAX_FALLBACK_SIZE so
* a single request can't run hsi_prepare to OOM. */
ctx->settings.es_max_header_list_size = 16 * 1024;
/* Same value as Http2Context::maxHeaderListSize. */
size_t max_header_size = BUN_DEFAULT_MAX_HTTP_HEADER_SIZE ? BUN_DEFAULT_MAX_HTTP_HEADER_SIZE : 64 * 1024;
ctx->max_header_set_size = max_header_size > 0xffffff ? 0xffffff : (unsigned) max_header_size;
ctx->settings.es_max_header_list_size = ctx->max_header_set_size;
ctx->settings.es_init_max_streams_bidi = 100;
/* Static-table-only response encoding: skips the per-header dynamic
* table search (lsqpack_enc_encode + XXH32 + header_out_dynamic_entry
Expand Down Expand Up @@ -1152,8 +1186,10 @@ int us_quic_stream_send_headers(us_quic_stream_t *s,
* so each pair has to be contiguous. The caller hands us arbitrary
* pointers, so flatten here. */
size_t total = 0;
for (unsigned int i = 0; i < count; i++)
for (unsigned int i = 0; i < count; i++) {
if (headers[i].name_len > LSXPACK_MAX_STRLEN || headers[i].value_len > LSXPACK_MAX_STRLEN) return -1;
total += headers[i].name_len + headers[i].value_len;
}

char stackbuf[1024];
char *buf = total <= sizeof(stackbuf) ? stackbuf : (char *) us_malloc(total);
Expand All @@ -1171,8 +1207,9 @@ int us_quic_stream_send_headers(us_quic_stream_t *s,
const struct us_quic_header_t *h = &headers[i];
memcpy(buf + off, h->name, h->name_len);
memcpy(buf + off + h->name_len, h->value, h->value_len);
lsxpack_header_set_offset2(&xh[i], buf, off, h->name_len,
off + h->name_len, h->value_len);
/* lsxpack asserts that offsets fit lsxpack_strlen_t, so they count from this pair's start. */
lsxpack_header_set_offset2(&xh[i], buf + off, 0, h->name_len,
h->name_len, h->value_len);
if (h->qpack_index >= 0) {
xh[i].qpack_index = (uint8_t) h->qpack_index;
xh[i].flags = LSXPACK_QPACK_IDX;
Expand Down Expand Up @@ -1224,17 +1261,30 @@ void lsquic_stream_maybe_reset(struct lsquic_stream *, uint64_t error_code, int)
* the wire-level "I'm cancelling this send" and lets the server treat it
* as a stream-level cancellation rather than a malformed message.
* Sends nothing once lsquic_stream_close/shutdown has run, so call it first. */
void us_quic_stream_reset(us_quic_stream_t *s) {
static void us_quic_stream_reset_with(us_quic_stream_t *s, uint64_t error_code) {
if (!s->stream) return;
/* do_close=0: with no reset due, maybe_reset's own close shuts only the read half. */
lsquic_stream_maybe_reset(s->stream, 0x10C, 0);
lsquic_stream_maybe_reset(s->stream, error_code, 0);
lsquic_stream_close(s->stream);
}

void us_quic_stream_reset(us_quic_stream_t *s) {
us_quic_stream_reset_with(s, 0x10C);
}

/* H3_INTERNAL_ERROR, for a message this endpoint can't send. */
void us_quic_stream_reset_internal_error(us_quic_stream_t *s) {
us_quic_stream_reset_with(s, 0x102);
}

int us_quic_stream_has_unacked(us_quic_stream_t *s) {
return s->stream ? lsquic_stream_has_unacked_data(s->stream) : 0;
}

uint64_t us_quic_stream_reset_code(us_quic_stream_t *s) {
return s->stream ? lsquic_stream_get_error_code(s->stream) : 0;
}

void *us_quic_stream_ext(us_quic_stream_t *s) { return s + 1; }

us_quic_socket_t *us_quic_stream_socket(us_quic_stream_t *s) {
Expand All @@ -1253,6 +1303,10 @@ const struct us_quic_header_t *us_quic_stream_header(us_quic_stream_t *s, unsign
return s->hset && i < s->hset->count ? &s->hset->headers[i] : NULL;
}

int us_quic_stream_headers_too_large(us_quic_stream_t *s) {
return s->hset && s->hset->too_large;
}

void *us_quic_socket_ext(us_quic_socket_t *s) { return s + 1; }
us_quic_socket_context_t *us_quic_socket_context(us_quic_socket_t *s) { return s->ctx; }

Expand Down Expand Up @@ -1352,7 +1406,10 @@ us_quic_socket_context_t *us_create_quic_client_context(
lsquic_engine_init_settings(&ctx->settings, LSENG_HTTP);
ctx->settings.es_versions = (1u << LSQVER_I001);
ctx->settings.es_ecn = 0;
/* One field holds at most LSXPACK_MAX_STRLEN bytes, so a server isn't told it may send more. */
ctx->settings.es_max_header_list_size = 64 * 1024;
/* A server may ignore that setting. Same as the HTTP/2 client's LOCAL_MAX_HEADER_LIST_SIZE. */
ctx->max_header_set_size = 256 * 1024;
ctx->settings.es_ext_http_prio = 0;

struct lsquic_engine_api api;
Expand Down
8 changes: 8 additions & 0 deletions packages/bun-usockets/src/quic.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ void us_quic_stream_flush(us_quic_stream_t *s);
void us_quic_stream_shutdown_read(us_quic_stream_t *s);
void us_quic_stream_close(us_quic_stream_t *s);
void us_quic_stream_reset(us_quic_stream_t *s);
void us_quic_stream_reset_internal_error(us_quic_stream_t *s);
int us_quic_stream_has_unacked(us_quic_stream_t *s);
/* The code of the last RESET_STREAM or STOP_SENDING on the stream, 0 without one.
* on_stream_close can still read it. */
uint64_t us_quic_stream_reset_code(us_quic_stream_t *s);

void *us_quic_stream_ext(us_quic_stream_t *s);
us_quic_socket_t *us_quic_stream_socket(us_quic_stream_t *s);
Expand All @@ -160,6 +164,10 @@ void us_quic_loop_flush_if_pending(struct us_loop_t *loop);
/* Incoming headers — valid from on_stream_headers until on_stream_close. */
unsigned int us_quic_stream_header_count(us_quic_stream_t *s);
const struct us_quic_header_t *us_quic_stream_header(us_quic_stream_t *s, unsigned int i);
/* Fields a request may have, pseudo-headers included. */
#define US_QUIC_MAX_REQUEST_FIELDS 200
/* The header set exceeds the size or the field count this endpoint accepts and is incomplete. */
int us_quic_stream_headers_too_large(us_quic_stream_t *s);

/* Connection accessors */
void *us_quic_socket_ext(us_quic_socket_t *s);
Expand Down
7 changes: 7 additions & 0 deletions packages/bun-uws/src/Http3Context.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace uWS {

static_assert(US_QUIC_MAX_REQUEST_FIELDS == UWS_HTTP_MAX_HEADERS_COUNT, "HTTP/3 has the field limit of HTTP/1 and HTTP/2");

struct Http3Context {

static Http3Context *create(Loop *loop, us_bun_socket_context_options_t options, unsigned idleTimeoutSecs = 0) {
Expand All @@ -34,6 +36,11 @@ struct Http3Context {
if (rd->state != 0) return;
rd->reset();

if (us_quic_stream_headers_too_large(s)) {
res->writeStatus("431 Request Header Fields Too Large")->end();
return;
}

Http3Request req(s);
if (req.getHeader("expect") == "100-continue") res->writeContinue();
cd->router.getUserData() = {res, &req};
Expand Down
7 changes: 5 additions & 2 deletions packages/bun-uws/src/Http3Response.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,11 @@ struct Http3Response {
h.name = base + (uintptr_t) h.name;
h.value = base + (uintptr_t) h.value;
}
us_quic_stream_send_headers((us_quic_stream_t *) this,
d->hdrs.mutableSpan().data(), (unsigned) d->hdrs.size(), endStream);
if (us_quic_stream_send_headers((us_quic_stream_t *) this,
d->hdrs.mutableSpan().data(), (unsigned) d->hdrs.size(), endStream) != 0) {
/* Later writes fail, and on_stream_close reports the abort as it does for a reset by the peer. */
us_quic_stream_reset_internal_error((us_quic_stream_t *) this);
Comment thread
Jarred-Sumner marked this conversation as resolved.
Resolved
Comment thread
Jarred-Sumner marked this conversation as resolved.
Resolved
}
d->hdrBuf.shrink(0);
d->hdrs.shrink(0);
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/brotli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import type { Dependency, DirectBuild } from "../source.ts";

const BROTLI_COMMIT = "028fb5a23661f123017c060daa546b55cf4bde29"; // v1.2.0
const BROTLI_COMMIT = "db24a98aed38a16de78cc05ca15cdd8dc2a0d92d"; // v1.2.1

// prettier-ignore
const SOURCES = [
Expand Down
1 change: 1 addition & 0 deletions src/brotli_sys/brotli_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub enum BrotliDecoderErrorCode2 {
ERROR_FORMAT_PADDING_1 = -14,
ERROR_FORMAT_PADDING_2 = -15,
ERROR_FORMAT_DISTANCE = -16,
ERROR_FORMAT_BLOCK_SWITCH = -17,
ERROR_COMPOUND_DICTIONARY = -18,
ERROR_DICTIONARY_NOT_SET = -19,
ERROR_INVALID_ARGUMENTS = -20,
Expand Down
27 changes: 27 additions & 0 deletions src/clap/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,33 @@ pub struct IgnoredLongFlags {
pub prefixes: &'static [&'static [u8]],
}

impl IgnoredLongFlags {
/// Returns `None` if `--name` isn't skipped. Otherwise returns whether `next`, the argument
/// after it, is its value. `is_own_flag` tells whether the command has a flag of that name.
pub fn value_is_next_arg(
&self,
name: &[u8],
next: &[u8],
is_own_flag: impl Fn(&[u8]) -> bool,
) -> Option<bool> {
if self.options.contains(&name) {
return Some(!next.starts_with(b"-"));
}
if self.flags.contains(&name)
|| name.strip_prefix(b"no-").is_some_and(|positive| {
self.flags.contains(&positive)
|| self.options.contains(&positive)
|| (!self.flags.is_empty() && is_own_flag(positive))
})
{
return Some(next == b"true" || next == b"false");
}
(self.prefixes.iter())
.any(|prefix| name.starts_with(prefix))
.then_some(false)
}
}

// Help/usage/error rendering — none of this is on the cold-start hot chain
// (`bun -p '1+1'` / `bun --version` / plain `bun <file>` never call into the
// help or diagnostics path). Mark every entry point `#[cold]` + `#[inline(never)]`
Expand Down
26 changes: 5 additions & 21 deletions src/clap/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,28 +158,12 @@ where
}

if self.unknown_long_flags_are_errors && !self.iter.last_is_from_bun_options() {
let ignored = self.ignored_long_flags;
let next = self.iter.remain().first().copied().unwrap_or_default();
let value_is_next_arg = if ignored.options.contains(&name) {
!next.starts_with(b"-")
} else if ignored.flags.contains(&name)
|| name.strip_prefix(b"no-").is_some_and(|positive| {
ignored.flags.contains(&positive)
|| ignored.options.contains(&positive)
|| (!ignored.flags.is_empty()
&& params
.iter()
.any(|param| param.names.matches_long(positive)))
})
{
next == b"true" || next == b"false"
} else if ignored
.prefixes
.iter()
.any(|prefix| name.starts_with(prefix))
{
false
} else {
let is_own_flag =
|name: &[u8]| params.iter().any(|param| param.names.matches_long(name));
let Some(value_is_next_arg) =
(self.ignored_long_flags).value_is_next_arg(name, next, is_own_flag)
else {
return Err(self.err(arg, None, Some(name), ArgError::InvalidArgument));
};
if value_is_next_arg && maybe_value.is_none() {
Expand Down
Loading
Loading