Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c87564e
Hardening follow-up: fix regressions from #46021 and more crashes
Jarred-Sumner Oct 21, 2026
dcbdc0c
node:http2: accept an undefined socket in writeToClosedTransport's type
Jarred-Sumner Oct 21, 2026
0e6421a
node:fs: keep a wasm vector element's view next to its index
Jarred-Sumner Oct 21, 2026
9e6bea9
bindings.cpp: include NodeVM.h
Jarred-Sumner Oct 21, 2026
7cd4931
sqlite: reword two comments
Jarred-Sumner Oct 21, 2026
a4e466a
node:zlib: params() on a finished stream doesn't wait for 'end'
Jarred-Sumner Oct 21, 2026
64a49d9
dev server: a page's output doesn't depend on whether a stylesheet it…
Jarred-Sumner Oct 21, 2026
24d5f89
require(): name the native frame "anonymous" in stack traces again
Jarred-Sumner Oct 21, 2026
d2f6919
shell cp: return the failure as a variant of Check
Jarred-Sumner Oct 21, 2026
2eb4174
shell cp: call Check::fail directly
Jarred-Sumner Oct 21, 2026
53b7985
node:vm: keep the prototype of a context's global on its proxy
Jarred-Sumner Oct 21, 2026
ab663a3
node:vm: store on the global object unless its prototype chain takes …
Jarred-Sumner Oct 21, 2026
6dc565f
node:zlib: params() on a finished stream doesn't wait for a reader
Jarred-Sumner Oct 21, 2026
c20629a
--hot, --watch: an uncaught exception stops the run's timers again, a…
Jarred-Sumner Oct 21, 2026
ca2563d
napi: env cleanup does not wait for async work; its finalizers run af…
Jarred-Sumner Oct 21, 2026
3940ed5
dev server: two stylesheets bundled together both keep their edges to…
Jarred-Sumner Oct 21, 2026
6b2d129
Bun.Image: GIF metadata() reports the decoded frame's size, and a pat…
Jarred-Sumner Oct 21, 2026
4472dc7
node:vm: pass a store on to an object with its own put() in the globa…
Jarred-Sumner Oct 21, 2026
21b425a
node:http2: write() on a stream that can no longer send completes on …
Jarred-Sumner Oct 21, 2026
bed53f0
Bun.Image: a small maxPixels still takes a PNG profile of up to 8 MB;…
Jarred-Sumner Oct 21, 2026
f7d4830
node:http2: take the later completion of write() on a stream that can…
Jarred-Sumner Oct 21, 2026
384a0b2
node:http2: test that a session reports ECONNRESET when its read find…
Jarred-Sumner Oct 21, 2026
d6a0d6b
Tests: skip extension loading where SQLite can't do it, close a serve…
Jarred-Sumner Oct 21, 2026
8186ac5
--hot: a reload after an uncaught exception cancels only the failed r…
Jarred-Sumner Oct 21, 2026
1feb2cd
timers: take the hot run by reference
Jarred-Sumner Oct 21, 2026
808f44d
node:https closes a paused connection after a hangup; a write that wa…
Jarred-Sumner Oct 21, 2026
9dbc3e7
Tests: assert Node's events for writes before secureConnect, in both …
Jarred-Sumner Oct 21, 2026
d6c18c9
Merge branch 'main' into claude/hardening
Jarred-Sumner Oct 21, 2026
e25eb25
node:https, node:http2: connection teardown follows Node
Jarred-Sumner Oct 21, 2026
a329dee
node:http2: res.end() calls its callback when the stream closes befor…
Jarred-Sumner Oct 21, 2026
2b8e452
node:process: the EventEmitter methods the module exports act on proc…
Jarred-Sumner Oct 21, 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: 2 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9476,6 +9476,8 @@ declare module "bun" {
* Reject inputs whose `width × height` exceeds this many pixels. The
* check runs after the header is read but before any pixel buffer is
* allocated, so a tiny file claiming a huge canvas is refused cheaply.
* A lower limit also lowers how much a compressed PNG chunk, such as an
* ICC profile, may inflate to before the image is refused.
* @default 268402689 // 0x3FFF * 0x3FFF, same as Sharp
*/
maxPixels?: number;
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,10 @@ int us_socket_is_closed(us_socket_r s) nonnull_fn_decl;
* from one the peer has already written to or closed. */
int us_socket_queued_input(us_socket_r s) nonnull_fn_decl;

/* TLS sockets only: nothing sets the bit up for a plain one. The kernel rejected a send of TLS records, so the error
* code of the close that follows stands for the lost records, not for a failed read. */
int us_socket_ssl_send_rejected(us_socket_r s) nonnull_fn_decl;

int us_socket_is_ssl_handshake_finished(us_socket_r s) nonnull_fn_decl;
int us_socket_ssl_handshake_callback_has_fired(us_socket_r s) nonnull_fn_decl;
/* TLS ciphertext bytes already sealed for this socket and reported as
Expand Down
6 changes: 4 additions & 2 deletions packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -922,9 +922,11 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
* read_eof is set there is nothing left to drain and deferring would only
* lose the close. Error-flagged events keep the error path. A hangup
* takes the write side down too, so a hangup_closes_unsent socket does
* not wait when the write this event retried failed again. */
* not wait when the write this event retried failed again. A TLS socket
* retries nothing after a rejected send. */
const int eof_deferrable = eof && s && !error && !us_socket_is_closed(s) && !s->read_eof;
const int unsent_is_lost = hangup && s && s->hangup_closes_unsent && s->flags.last_write_failed;
const int unsent_is_lost = hangup && s && s->hangup_closes_unsent &&
(s->flags.last_write_failed || (s->ssl && s->ssl_send_rejected));
if (eof_deferrable && s->flags.is_paused && !unsent_is_lost) {
#ifdef LIBUS_USE_EPOLL
/* EPOLLHUP is unmaskable: leave epoll while paused so it cannot re-fire; the unread tail stays in
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-usockets/src/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ __attribute__((always_inline)) int us_socket_is_established(struct us_socket_t *
return us_internal_poll_type((struct us_poll_t *) s) != POLL_TYPE_SEMI_SOCKET;
}

int us_socket_ssl_send_rejected(struct us_socket_t *s) {
return s->ssl_send_rejected;
}

int us_socket_queued_input(struct us_socket_t *s) {
if (s->flags.is_closed || !us_socket_is_established(s)) {
return LIBUS_QUEUED_INPUT_NONE;
Expand Down
17 changes: 15 additions & 2 deletions packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include <span>
#include <array>
#include <mutex>
#include <openssl/ssl.h>


extern "C" void Bun__NodeHTTP__onReadsResumable(int ssl, struct us_socket_t *s);
Expand Down Expand Up @@ -320,6 +321,18 @@ struct HttpContext {
return us_socket_close(s, 0, nullptr);
}

/* A TLS send that the kernel rejected ends the write side too, and the next writable event closes the socket.
* Like Node, node:https still dispatches the requests that came with this read. */
template <bool IsNodeHttp>
static bool isShutDownByOwner(us_socket_t *s) {
if constexpr (SSL && IsNodeHttp) {
if (us_socket_ssl_send_rejected(s) && !(SSL_get_shutdown((const ssl_st *) us_socket_get_native_handle(s)) & SSL_SENT_SHUTDOWN)) {
return false;
}
}
return us_socket_is_shut_down(s);
}

template <bool IsNodeHttp>
static us_socket_t *onData(us_socket_t *s, char *data, int length) {
// ref the socket to make sure we process it entirely before it is closed
Expand Down Expand Up @@ -590,7 +603,7 @@ struct HttpContext {
}

/* We absolutely have to terminate parsing if shutdown */
if (us_socket_is_shut_down((us_socket_t *) s)) {
if (isShutDownByOwner<IsNodeHttp>((us_socket_t *) s)) {
return nullptr;
}

Expand Down Expand Up @@ -682,7 +695,7 @@ struct HttpContext {
}

/* We absolutely have to terminate parsing if shutdown */
if (us_socket_is_shut_down((us_socket_t *) user)) {
if (isShutDownByOwner<IsNodeHttp>((us_socket_t *) user)) {
/* node:http: this socket can be a tunnel that still reads. Its bytes are not for a body that ended. */
if constexpr (IsNodeHttp) {
if (fin) {
Expand Down
5 changes: 5 additions & 0 deletions src/ast/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ impl ReplaceableExport {
pub fn is_replace(&self) -> bool {
matches!(self, Self::Replace(_))
}

#[inline]
pub fn injects_default(&self) -> bool {
matches!(self, Self::Inject { name, .. } if **name == *crate::ClauseItem::DEFAULT_ALIAS)
}
}

/// Newtype (not a bare alias) so we can hang `get_ptr` (which borrows
Expand Down
38 changes: 24 additions & 14 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2092,9 +2092,8 @@ pub mod bv2_impl {
pub(crate) scb_bitset: Option<DynamicBitSetUnmanaged>,
pub(crate) scb_list: server_component_boundary::Slice<'a>,

/// Files which are imported by JS and inlined in CSS
pub(crate) additional_files_imported_by_js_and_inlined_in_css:
&'a mut DynamicBitSetUnmanaged,
/// Files inlined in CSS whose output path a JS or HTML importer or their own entry point chunk prints
pub(crate) additional_files_needed_outside_css: &'a mut DynamicBitSetUnmanaged,
/// Files which are imported by CSS and inlined in CSS
pub(crate) additional_files_imported_by_css_and_inlined: &'a mut DynamicBitSetUnmanaged,

Expand Down Expand Up @@ -2194,7 +2193,6 @@ pub mod bv2_impl {
}
}

let is_js = self.all_loaders[source_index.get() as usize].is_javascript_like();
let is_css = self.all_loaders[source_index.get() as usize].is_css();

let import_record_list_id = source_index;
Expand Down Expand Up @@ -2245,17 +2243,17 @@ pub mod bv2_impl {
let import_record = &mut self.all_import_records
[import_record_list_id.get() as usize]
.as_mut_slice()[ir_idx];
// Mark if the file is imported by JS and its URL is inlined for CSS
// Mark who imports a file whose URL is inlined for CSS
let is_inlined = import_record.source_index.is_valid()
&& !self.all_urls_for_css
[import_record.source_index.get() as usize]
.is_empty();
if is_js && is_inlined {
self.additional_files_imported_by_js_and_inlined_in_css
.set(import_record.source_index.get() as usize);
} else if is_css && is_inlined {
if is_css && is_inlined {
self.additional_files_imported_by_css_and_inlined
.set(import_record.source_index.get() as usize);
} else if is_inlined {
self.additional_files_needed_outside_css
.set(import_record.source_index.get() as usize);
}

let next_source = import_record.source_index;
Expand Down Expand Up @@ -2350,8 +2348,11 @@ pub mod bv2_impl {
None
};

let mut additional_files_imported_by_js_and_inlined_in_css =
let mut additional_files_needed_outside_css =
DynamicBitSetUnmanaged::init_empty(self.graph.input_files.len())?;
for entry_point in self.graph.entry_points.iter() {
additional_files_needed_outside_css.set(entry_point.get() as usize);
}
let mut additional_files_imported_by_css_and_inlined =
DynamicBitSetUnmanaged::init_empty(self.graph.input_files.len())?;

Expand Down Expand Up @@ -2390,8 +2391,7 @@ pub mod bv2_impl {
all_targets,
scb_bitset,
scb_list,
additional_files_imported_by_js_and_inlined_in_css:
&mut additional_files_imported_by_js_and_inlined_in_css,
additional_files_needed_outside_css: &mut additional_files_needed_outside_css,
additional_files_imported_by_css_and_inlined:
&mut additional_files_imported_by_css_and_inlined,
stack: Vec::new(),
Expand Down Expand Up @@ -2450,9 +2450,9 @@ pub mod bv2_impl {
for (index, url_for_css) in all_urls_for_css.iter().enumerate() {
if !url_for_css.is_empty() {
// We like to inline additional files in CSS if they fit a size threshold
// If we do inline a file in CSS, and it is not imported by JS, then we don't need to copy the additional file into the output directory
// If we do inline a file in CSS, and nothing else prints its path, then we don't need to copy the additional file into the output directory
if additional_files_imported_by_css_and_inlined.is_set(index)
&& !additional_files_imported_by_js_and_inlined_in_css.is_set(index)
&& !additional_files_needed_outside_css.is_set(index)
{
additional_files[index].clear_retaining_capacity();
unique_keys[index] = bun_alloc::AstAlloc::vec().into_boxed_slice();
Expand Down Expand Up @@ -5056,6 +5056,13 @@ pub mod bv2_impl {
// If it's a file namespace, we should run it through the parser like normal.
// The file could be on disk.
if source.path.is_file() {
// The enqueue site left the asset bookkeeping to the plugin's answer.
let index = load.source_index.get() as usize;
if this.graph.input_files.items_loader()[index].should_copy_for_bundling() {
this.graph.input_files.items_side_effects_mut()[index] =
bun_ast::SideEffects::NoSideEffectsPureData;
this.graph.estimated_file_loader_count += 1;
}
Comment thread
Jarred-Sumner marked this conversation as resolved.
Resolved
this.graph.pool().schedule(load.parse_task_mut());
return;
}
Expand Down Expand Up @@ -6050,6 +6057,9 @@ pub mod bv2_impl {
} == 0
{
record.source_index = Index::INVALID;
// The importer isn't bundled again when the stylesheet is fixed, so its
// output must treat this import the same as a stylesheet that didn't fail.
record.path.is_disabled = true;
continue;
}

Expand Down
6 changes: 5 additions & 1 deletion src/bundler/linker_context/doStep5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,11 @@ impl LinkerContext<'_> {
.unwrap()
.get(ref_)
.unwrap();
let use_: &mut SymbolUse = symbol_uses.get_ptr_mut(ref_).unwrap();
// The parser drops the entry if a replaced macro call was the last plain use.
let use_: &mut SymbolUse = symbol_uses
.get_or_put_value(*ref_, SymbolUse::default())
.expect("OOM")
.value_ptr;

// Rare path: this import is a TypeScript enum
if let Some(import_data) = our_imports_to_bind.get(ref_) {
Expand Down
10 changes: 9 additions & 1 deletion src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2929,7 +2929,15 @@ impl<'a> Transpiler<'a> {
};
}
options::Loader::Dataurl | options::Loader::Base64 => {
bun_core::Output::panic(format_args!("TODO: dataurl, base64"));
self.log_mut().add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"loader \"{}\" is not supported with --no-bundle",
<&'static str>::from(loader),
),
);
return Ok(None);
}
options::Loader::Css => {
match self.build_css_output(
Expand Down
7 changes: 4 additions & 3 deletions src/collections/array_hash_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,10 +608,11 @@ impl<K, V, C, A: MapAllocator> ArrayHashMap<K, V, C, A> {
/// the index — keeping its SwissTable grow path off the subsequent
/// `push_entry` loop. No-op when the index hasn't materialised yet (it will
/// be built at the right size by [`rebuild_index`] when the map first
/// crosses [`INDEX_THRESHOLD`], since that reads `self.keys.capacity()`).
/// crosses [`INDEX_THRESHOLD`], since that reads `self.hashes.capacity()`).
/// Not `self.keys.capacity()`, which is `usize::MAX` for a zero-sized `K`.
#[inline]
fn reserve_index_to_capacity(&mut self) {
let cap = self.keys.capacity();
let cap = self.hashes.capacity();
if let Some(index) = self.index.as_deref_mut() {
index_reserve(index, &self.hashes, cap);
}
Expand Down Expand Up @@ -800,7 +801,7 @@ impl<K, V, C, A: MapAllocator> ArrayHashMap<K, V, C, A> {
fn rebuild_index(&mut self) {
self.index = Some(rebuild_index_from_hashes(
&self.hashes,
self.keys.capacity(),
self.hashes.capacity(),
));
}

Expand Down
12 changes: 6 additions & 6 deletions src/collections/vec_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ impl crate::pool::ObjectPoolType for Vec<u8> {

#[derive(Default)]
pub struct OffsetByteList {
pub head: u32,
pub head: usize,
pub byte_list: Vec<u8>,
}

Expand All @@ -546,12 +546,12 @@ impl OffsetByteList {
}

pub fn remaining(&self) -> &[u8] {
&self.byte_list[self.head as usize..]
&self.byte_list[self.head..]
}

pub fn consume(&mut self, bytes: u32) {
pub fn consume(&mut self, bytes: usize) {
self.head = self.head.saturating_add(bytes);
let head = self.head as usize;
let head = self.head;
if head >= self.byte_list.len() {
self.head = 0;
self.byte_list.clear();
Expand All @@ -563,8 +563,8 @@ impl OffsetByteList {
}
}

pub fn len(&self) -> u32 {
self.byte_list.len() as u32 - self.head
pub fn len(&self) -> usize {
self.byte_list.len() - self.head
}

pub fn clear_and_free(&mut self) {
Expand Down
6 changes: 2 additions & 4 deletions src/css/selectors/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,16 +930,14 @@ pub(crate) mod serialize {
dest.write_str(b":host")?;
if let Some(sel) = selector {
dest.write_char(b'(')?;
let ctx = dest.ctx;
serialize_selector(sel, dest, ctx, false)?;
serialize_selector(sel, dest, context, false)?;
dest.write_char(b')')?;
}
return Ok(());
}
Component::Slotted(selector) => {
dest.write_str(b"::slotted(")?;
let ctx = dest.ctx;
serialize_selector(selector, dest, ctx, false)?;
serialize_selector(selector, dest, context, false)?;
dest.write_char(b')')?;
}
_ => {
Expand Down
2 changes: 1 addition & 1 deletion src/install/lockfile/Package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub(crate) fn folder_relative_to_top_level_dir<'a>(
// empty when the package depends on itself
let relative: &[u8] = if relative.is_empty() { b"." } else { relative };
let len = relative.len();
buf[..len].copy_from_slice(relative);
buf.get_mut(..len)?.copy_from_slice(relative);
#[cfg(windows)]
path::dangerously_convert_path_to_posix_in_place::<u8>(&mut buf[..len]);
Some(&buf[..len])
Expand Down
14 changes: 4 additions & 10 deletions src/js/internal/webstreams_adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,25 +120,19 @@ class ReadableFromWeb extends Readable {
_destroy(error, callback) {
if (!this.#closed) {
this.#closed = true;
// As in Node, a cancel() that rejects doesn't error the Readable.
const done = () => callback(error);
var reader = this.#reader;
if (reader) {
this.#reader = undefined;
PromisePrototypeThen.$call(
reader.cancel(error),
() => callback(error),
cancelError => callback(error ?? cancelError),
);
PromisePrototypeThen.$call(reader.cancel(error), done, done);
return;
}
var stream = this.#stream;
if (stream) {
this.#stream = undefined;
if (!this.#takenByResponse(stream)) {
PromisePrototypeThen.$call(
stream.cancel(error),
() => callback(error),
cancelError => callback(error ?? cancelError),
);
PromisePrototypeThen.$call(stream.cancel(error), done, done);
return;
}
}
Expand Down
Loading
Loading