Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
b3cbcc2
install: tighten extract folder name handling
Jarred-Sumner May 27, 2026
a57ad00
install: validate off-registry tarballs during yarn.lock migration
Jarred-Sumner May 27, 2026
6c1cd7e
install: source dependency lifecycle scripts from package.json
Jarred-Sumner May 27, 2026
b39ad6b
pm: resolve cache dir from process env in cache rm
Jarred-Sumner May 27, 2026
9a11ac2
install: tighten trusted dependency matching for non-npm packages
Jarred-Sumner May 27, 2026
0b2644b
install: keep registry tokens scoped to their host
Jarred-Sumner May 27, 2026
ac36920
install: include non-npm parents in security scan traversal
Jarred-Sumner May 27, 2026
62fb9a8
pack: validate package name and version fields
Jarred-Sumner May 27, 2026
c38e940
compile: use exclusive creation and tighter modes for output files
Jarred-Sumner May 27, 2026
47f7697
repl: tighten history file permissions
Jarred-Sumner May 27, 2026
3ff07a1
test: add regression coverage for input validation changes
Jarred-Sumner May 27, 2026
93fa7ed
fetch: tighten request header validation
Jarred-Sumner May 27, 2026
3fd8ec0
fetch: bound interim response buffering
Jarred-Sumner May 27, 2026
827a7ae
fetch: tighten connection reuse handling
Jarred-Sumner May 27, 2026
a898b73
node:http2: tighten received header validation
Jarred-Sumner May 27, 2026
ad62e81
node:http2: tighten outbound frame queue handling
Jarred-Sumner May 27, 2026
d9bfccb
node:http2: tighten read buffer handling
Jarred-Sumner May 27, 2026
8d0c582
s3: tighten object key handling
Jarred-Sumner May 27, 2026
39b85d9
server: tighten request target normalization
Jarred-Sumner May 27, 2026
30c7595
websocket: tighten handshake buffer ownership
Jarred-Sumner May 27, 2026
5660958
node:http: tighten request path handling
Jarred-Sumner May 27, 2026
4693f20
uws: tighten remote address handling
Jarred-Sumner May 27, 2026
fd8aaaf
webcore: tighten response status validation
Jarred-Sumner May 27, 2026
87f2486
s3: bound list response parsing
Jarred-Sumner May 27, 2026
f110731
test: add regression coverage for input validation changes
Jarred-Sumner May 27, 2026
efaa5d0
sql: bound column offset handling
Jarred-Sumner May 27, 2026
92a319a
sql: tighten tls option validation
Jarred-Sumner May 27, 2026
1650d46
mysql: bound result metadata handling
Jarred-Sumner May 27, 2026
12cbe1a
node:tls: tighten authorization state validation
Jarred-Sumner May 27, 2026
592062d
io: bound scratch buffer handling
Jarred-Sumner May 27, 2026
86bd9c8
sqlite: tighten database handle validation
Jarred-Sumner May 27, 2026
f77c55f
spawn: bound descriptor range handling
Jarred-Sumner May 27, 2026
036fbe5
password: tighten hash parameter validation
Jarred-Sumner May 27, 2026
b2c7909
tls: tighten read state handling
Jarred-Sumner May 27, 2026
340b3c2
socket: tighten handler reload validation
Jarred-Sumner May 27, 2026
4b22284
socket: tighten handler ownership validation
Jarred-Sumner May 27, 2026
1c1a9ed
test: add regression coverage for input validation changes
Jarred-Sumner May 28, 2026
d1742ba
yaml: bound alias expansion during parsing
Jarred-Sumner May 27, 2026
7f20a00
md: index reference definition lookups
Jarred-Sumner May 27, 2026
e3ab03e
md: cap table column count
Jarred-Sumner May 27, 2026
38c0963
md: bound autolink scanning
Jarred-Sumner May 27, 2026
f0ff344
node:fs: surface overlong watcher paths as errors
Jarred-Sumner May 27, 2026
42bfee3
assert: bound diff size for large operands
Jarred-Sumner May 27, 2026
daa15f1
test: add regression coverage for input validation changes
Jarred-Sumner May 27, 2026
eb50d85
errors: keep stack frame callees alive while formatting
Jarred-Sumner May 28, 2026
ce0f5f7
test: tighten mid-read TLS dispatch coverage
Jarred-Sumner May 28, 2026
af7ceae
[autofix.ci] apply automated fixes
autofix-ci[bot] May 28, 2026
df2edc9
socket: move safety comments next to their unsafe expressions
Jarred-Sumner May 28, 2026
5349bed
http: forward explicit Content-Length on bodyless node:http client re…
Jarred-Sumner May 28, 2026
71165e2
response: drop body for null body statuses instead of throwing
Jarred-Sumner May 28, 2026
756b22e
request: keep query string for absolute-form targets without a path
Jarred-Sumner May 28, 2026
e98ffbc
pm: only resolve the cache directory for display on bun pm cache
Jarred-Sumner May 28, 2026
cb79034
repl: chmod history file after opening
Jarred-Sumner May 28, 2026
339c28a
node:http: normalize https absolute URIs passed in the path option
Jarred-Sumner May 28, 2026
d28599e
sqlite: take registry lock when closing databases at termination
Jarred-Sumner May 28, 2026
0b66d6c
install: keep saved registry token only when the scheme is not downgr…
Jarred-Sumner May 28, 2026
785440a
install: keep loading lifecycle scripts from the binary lockfile
Jarred-Sumner May 28, 2026
f902f02
websocket: release buffered handshake handler when the VM is tearing …
Jarred-Sumner May 28, 2026
25b8e47
mysql: only rebuild cached result shape when column definitions change
Jarred-Sumner May 28, 2026
1d9211a
test: construct large list-objects payload with Buffer.alloc
Jarred-Sumner May 28, 2026
f755710
sqlite: serialize lazy library loading
Jarred-Sumner May 28, 2026
910dc1f
install: dedupe remote tarball URL validation and bound resolution lo…
Jarred-Sumner May 28, 2026
68497c1
test: tighten assertions in install, sqlite, and http tests
Jarred-Sumner May 28, 2026
5c3d87b
fetch: reject bodies on null-body status responses
Jarred-Sumner May 28, 2026
c833175
fetch: keep response construction behavior, restrict the change to th…
Jarred-Sumner May 28, 2026
7963009
sql: abort when explicitly requested TLS is declined
Jarred-Sumner May 28, 2026
b906566
install: keep migrating yarn.lock entries without integrity
Jarred-Sumner May 28, 2026
f295205
node:http: revert client request target handling for absolute URI paths
Jarred-Sumner May 28, 2026
a4d0ca0
test: cover connection header case in http2 server header validation …
Jarred-Sumner May 28, 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
7 changes: 7 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1119,8 +1119,15 @@ struct us_socket_t *us_internal_ssl_on_data(struct us_socket_t *s, char *data, i
read += just_read;

if (read == LIBUS_RECV_BUFFER_LENGTH) {
char *saved_input = loop_ssl_data->ssl_read_input;
unsigned int saved_length = loop_ssl_data->ssl_read_input_length;
unsigned int saved_offset = loop_ssl_data->ssl_read_input_offset;
s = us_dispatch_data(s, loop_ssl_data->ssl_read_output + LIBUS_RECV_BUFFER_PADDING, read);
if (!s || ssl_gone(s)) return NULL;
loop_ssl_data->ssl_read_input = saved_input;
loop_ssl_data->ssl_read_input_length = saved_length;
loop_ssl_data->ssl_read_input_offset = saved_offset;
loop_ssl_data->ssl_socket = s;
read = 0;
goto restart;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/bun-uws/src/HttpContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ struct HttpContext {

/* Route the method and URL */
selectedRouter->getUserData() = {(HttpResponse<SSL> *) s, httpRequest};
if (!selectedRouter->route(httpRequest->getCaseSensitiveMethod(), httpRequest->getUrl())) {
if (!selectedRouter->route(httpRequest->getCaseSensitiveMethod(), httpRequest->getUrlForRouting())) {
/* We have to force close this socket as we have no handler for it */
us_socket_close((us_socket_t *) s, 0, nullptr);
return nullptr;
Expand Down
21 changes: 21 additions & 0 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,27 @@ namespace uWS
return headers->value;
}

std::string_view getUrlForRouting()
{
std::string_view url = getUrl();
if (url.length() && url[0] != '/') {
size_t schemeLength = 0;
if (url.length() >= 7 && strncasecmp(url.data(), "http://", 7) == 0) {
schemeLength = 7;
} else if (url.length() >= 8 && strncasecmp(url.data(), "https://", 8) == 0) {
schemeLength = 8;
}
if (schemeLength) {
size_t pathStart = url.find('/', schemeLength);
if (pathStart == std::string_view::npos) {
return "/";
}
return url.substr(pathStart);
}
}
return url;
}

/* Hack: this should be getMethod */
std::string_view getCaseSensitiveMethod()
{
Expand Down
1 change: 1 addition & 0 deletions src/http/HTTPContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ impl<const SSL: bool> Handler<SSL> {
}

bun_core::scoped_log!(HTTPContext, "Unexpected data on socket");
HTTPContext::<SSL>::terminate_socket(socket);

return;
}
Expand Down
25 changes: 21 additions & 4 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ pub struct Flags {
/// Set after the first H3 retry so a stale-session/GOAWAY race retries
/// once on a fresh connection but never loops.
pub h3_retried: bool,
pub is_node_http_client: bool,
}

impl Default for Flags {
Expand All @@ -233,6 +234,7 @@ impl Default for Flags {
force_http1: false,
force_http3: false,
h3_retried: false,
is_node_http_client: false,
}
}
}
Expand Down Expand Up @@ -768,7 +770,7 @@ use core::ffi::c_uint;

use bstr::BStr;
use bun_boringssl as boringssl;
use bun_collections::ArrayHashMap;
use bun_collections::{ArrayHashMap, VecExt};
use bun_core::StringBuilder;
use bun_core::{FeatureFlags, Global, Output, err};
use bun_core::{OwnedString, String as BunString, Tag as BunStringTag, immutable as strings};
Expand Down Expand Up @@ -2260,7 +2262,10 @@ impl<'a> HTTPClient<'a> {
picohttp::Header::new(CONTENT_LENGTH_HEADER_NAME, value);
header_count += 1;
}
} else if let Some(content_length) = original_content_length {
} else if let Some(content_length) = original_content_length
&& (self.flags.is_node_http_client
|| matches!(bun_core::parse_unsigned::<usize>(content_length, 10), Ok(0)))
{
request_headers_buf[header_count] =
picohttp::Header::new(CONTENT_LENGTH_HEADER_NAME, content_length);
header_count += 1;
Expand Down Expand Up @@ -3183,7 +3188,7 @@ impl<'a> HTTPClient<'a> {
// if less than 16 it will always be a ShortRead
if to_read!().len() < 16 {
bun_core::scoped_log!(fetch, "handleShortRead");
self.handle_short_read::<IS_SSL>(incoming_data, socket, needs_move);
self.handle_short_read::<IS_SSL>(to_read!(), socket, needs_move);
return;
}

Expand All @@ -3206,7 +3211,7 @@ impl<'a> HTTPClient<'a> {
self.close_and_fail::<IS_SSL>(err!(ResponseHeadersTooLarge), socket);
return;
}
self.handle_short_read::<IS_SSL>(incoming_data, socket, needs_move);
self.handle_short_read::<IS_SSL>(to_read!(), socket, needs_move);
return;
}
Err(e) => {
Expand Down Expand Up @@ -3248,6 +3253,13 @@ impl<'a> HTTPClient<'a> {
bun_core::scoped_log!(fetch, "information headers");

self.state.pending_response = None;
if !needs_move {
let remaining = to_read!().len();
let buffer = &mut self.state.response_message_buffer.list;
let consumed = buffer.len().saturating_sub(remaining);
buffer.drain_front(consumed);
to_read = bun_ptr::RawSlice::new(buffer.as_slice());
}
if to_read!().is_empty() {
// we only received 1XX responses, we wanna wait for the next status code
return;
Expand Down Expand Up @@ -4045,6 +4057,11 @@ impl<'a> HTTPClient<'a> {
) -> Result<bool, bun_core::Error> {
debug_assert!(self.state.transfer_encoding == Encoding::Identity);
let content_length = self.state.content_length;
if let Some(len) = content_length
&& incoming_data.len() > len.saturating_sub(self.state.total_body_received)
{
self.state.flags.allow_keepalive = false;
}
// is it exactly as much as we need?
if is_only_buffer
&& let Some(len) = content_length
Expand Down
27 changes: 14 additions & 13 deletions src/http_jsc/websocket_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1840,8 +1840,8 @@ impl<const SSL: bool> WebSocket<SSL> {
ws: NonNull::new(outgoing).map(|p| unsafe { CppWebSocketRef::new(p) }),
}));
// Backref so `handle_data` can drain the buffered slice ahead of
// fresh socket data, and so `deinit()` can reclaim the box if
// process.exit() races ahead of the microtask drain.
// fresh socket data, and so `deinit()` can detach from the box if
// teardown races ahead of the microtask drain.
ws_ref.initial_data_handler = NonNull::new(initial_data);

// Use a higher-priority callback for the initial onData handler
Expand Down Expand Up @@ -2053,18 +2053,19 @@ impl<const SSL: bool> WebSocket<SSL> {
this_ref.clear_data();
// deflate already dropped in clear_data; this is defensive parity with Zig
this_ref.deflate = None;
// The queued microtask normally owns the handler box and clears this
// field via `handle_without_deinit` before freeing it. Reaching
// `deinit()` with the field still set means the microtask never
// drained (process.exit() under the API lock during onopen) and JSC
// is being torn down — reclaim the box so LSan doesn't flag it. In
// normal operation the adopted-socket I/O ref keeps `ref_count > 0`
// until after microtasks drain, so this branch is not hit.
if let Some(handler) = this_ref.initial_data_handler.take() {
// SAFETY: allocated via `heap::into_raw` in init()/init_with_tunnel();
// sole remaining owner — JSC's microtask queue only holds the
// pointer encoded as a JSValue double and will not run again.
drop(unsafe { bun_core::heap::take(handler.as_ptr()) });
// SAFETY: the handler box was allocated via `heap::into_raw` in
// init()/init_with_tunnel() and is normally freed by the queued
// microtask in `InitialDataHandler::handle`; this field still
// being set means that microtask has not run yet, so the box is
// live and the raw field write does not alias any borrow.
unsafe { core::ptr::addr_of_mut!((*handler.as_ptr()).adopted).write(None) };
if this_ref.global_this.bun_vm().is_shutting_down() {
// SAFETY: same allocation as above; the VM is shutting down, so
// the queued microtask can no longer run and this is the sole
// remaining owner of the box.
drop(unsafe { bun_core::heap::take(handler.as_ptr()) });
}
}
bun_core::scoped_log!(alloc, "destroy({}) = {:p}", Self::ALLOC_TYPE_NAME, this);
// SAFETY: this was allocated via heap::alloc in init/init_with_tunnel
Expand Down
12 changes: 11 additions & 1 deletion src/install/PackageManager/PackageManagerOptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,11 +597,21 @@ impl Options {
|| registry_.starts_with(b"http://"))
{
let prev_scope = self.scope.clone();
let prev_url = prev_scope.url.url();
let new_url = bun_url::URL::parse(registry_);
let token = if bun_core::without_trailing_slash(new_url.host)
== bun_core::without_trailing_slash(prev_url.host)
&& (new_url.is_https() || !prev_url.is_https())
{
prev_scope.token
} else {
Box::default()
};
Comment thread
claude[bot] marked this conversation as resolved.
// PORT NOTE: was `std.mem.zeroes(Api.NpmRegistry)`; zeroed slices are
// invalid in Rust — use Default (empty strings) which is semantically equivalent.
let api_registry = Api::NpmRegistry {
url: registry_.into(),
token: prev_scope.token,
token,
..Default::default()
};
self.scope = Npm::registry::Scope::from_api(b"", api_registry, env)?;
Expand Down
55 changes: 17 additions & 38 deletions src/install/PackageManager/security_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,11 +535,6 @@ impl<'a> PackageCollector<'a> {
continue;
}

let dep_res = &pkg_resolutions[dep_pkg_id as usize];
if dep_res.tag != bun_install::resolution::Tag::Npm {
continue;
}

if self.dedupe.get_or_put(dep_pkg_id)?.found_existing {
continue;
}
Expand Down Expand Up @@ -572,11 +567,6 @@ impl<'a> PackageCollector<'a> {
continue;
}

let dep_res = &pkg_resolutions[dep_pkg_id as usize];
if dep_res.tag != bun_install::resolution::Tag::Npm {
continue;
}

if self.dedupe.get_or_put(dep_pkg_id)?.found_existing {
continue;
}
Expand Down Expand Up @@ -609,10 +599,6 @@ impl<'a> PackageCollector<'a> {
if update_pkg_id != req.package_id {
continue;
}
if pkg_resolutions[update_pkg_id as usize].tag != bun_install::resolution::Tag::Npm
{
continue;
}

let mut update_dep_id: DependencyID = invalid_dependency_id;
let mut parent_pkg_id: PackageID = invalid_package_id;
Expand Down Expand Up @@ -682,16 +668,18 @@ impl<'a> PackageCollector<'a> {
let pkg_id = item.pkg_id;
let _ = item.dep_id; // Could be useful in the future for dependency-specific processing

let pkg_path_copy: Box<[PackageID]> = item.pkg_path.clone().into_boxed_slice();
let dep_path_copy: Box<[DependencyID]> = item.dep_path.clone().into_boxed_slice();
if pkg_resolutions[pkg_id as usize].tag == bun_install::resolution::Tag::Npm {
let pkg_path_copy: Box<[PackageID]> = item.pkg_path.clone().into_boxed_slice();
let dep_path_copy: Box<[DependencyID]> = item.dep_path.clone().into_boxed_slice();

self.package_paths.put(
pkg_id,
PackagePath {
pkg_path: pkg_path_copy,
dep_path: dep_path_copy,
},
)?;
self.package_paths.put(
pkg_id,
PackagePath {
pkg_path: pkg_path_copy,
dep_path: dep_path_copy,
},
)?;
}

let pkg_deps = pkg_dependencies[pkg_id as usize];
for _next_dep_id in pkg_deps.begin()..pkg_deps.end() {
Expand All @@ -703,11 +691,6 @@ impl<'a> PackageCollector<'a> {
continue;
}

let next_pkg_res = &pkg_resolutions[next_pkg_id as usize];
if next_pkg_res.tag != bun_install::resolution::Tag::Npm {
continue;
}

if self.dedupe.get_or_put(next_pkg_id)?.found_existing {
continue;
}
Expand Down Expand Up @@ -773,9 +756,9 @@ impl<'a> JSONBuilder<'a> {
json_buf.extend_from_slice(b",\n");
}

// SAFETY: `PackageCollector::collect_packages_from_root` only inserts
// packages whose resolution tag is `Tag::Npm` into `package_paths`,
// so the `npm` union variant is the active field here.
// SAFETY: `PackageCollector::process_queue` only inserts packages
// whose resolution tag is `Tag::Npm` into `package_paths`, so the
// `npm` union variant is the active field here.
let npm = pkg_res.npm();
if dep_id == invalid_dependency_id {
write!(
Expand Down Expand Up @@ -889,14 +872,10 @@ fn attempt_security_scan_with_retry(

// PORT NOTE: destructure `collector` here to release its `&PackageManager`
// borrow before constructing `SecurityScanSubprocess` (which needs `&mut`).
// Only `package_paths` and the dedupe count are read past this point.
let PackageCollector {
dedupe,
package_paths,
..
} = collector;
// Only `package_paths` is read past this point.
let PackageCollector { package_paths, .. } = collector;
let mut package_paths = package_paths;
let packages_scanned = dedupe.count();
let packages_scanned = package_paths.count();

let mut code: Vec<u8> = Vec::new();

Expand Down
25 changes: 13 additions & 12 deletions src/install/TarballStream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,19 +665,20 @@ impl TarballStream {
// Tag::Extract` for streaming tarballs).
let tarball = &self.extract_task.request_extract().tarball;
let (_, basename) = tarball.name_and_basename();
if !tarball.resolution.tag.is_git()
&& tarball.resolution.tag != ResolutionTag::LocalTarball
&& !crate::dependency::is_safe_install_folder_name(&basename[0..basename.len().min(32)])
{
self.invalid_name = true;
return Err(bun_core::err!("InstallFailed"));
}
let truncated_basename = &basename[0..basename.len().min(32)];
let tmpname_suffix: &[u8] =
if crate::dependency::is_safe_install_folder_name(truncated_basename) {
truncated_basename
} else if tarball.resolution.tag.is_git()
|| tarball.resolution.tag == ResolutionTag::LocalTarball
{
b"package"
} else {
self.invalid_name = true;
return Err(bun_core::err!("InstallFailed"));
};
let mut buf = PathBuffer::uninit();
let tmpname = FileSystem::tmpname(
&basename[0..basename.len().min(32)],
&mut buf[..],
bun_core::fast_random(),
)?;
let tmpname = FileSystem::tmpname(tmpname_suffix, &mut buf[..], bun_core::fast_random())?;
// allocator.dupeZ → owned NUL-terminated copy.
self.tmpname = ZBox::from_bytes(tmpname.as_bytes());

Expand Down
42 changes: 21 additions & 21 deletions src/install/extract_tarball.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,29 +243,29 @@ impl ExtractTarball {
// `open_dir_at_windows_a` boundary, not here.
let mut tmpname_buf = PathBuffer::uninit();
let (name, basename) = self.name_and_basename();
if !self.resolution.tag.is_git()
&& self.resolution.tag != ResolutionTag::LocalTarball
&& !bun_install::dependency::is_safe_install_folder_name(
&basename[0..basename.len().min(32)],
)
{
log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"Refusing to install package with invalid name \"{}\"",
bun_fmt::s(name),
),
);
return Err(bun_core::err!("InstallFailed"));
}
let truncated_basename = &basename[0..basename.len().min(32)];
let tmpname_suffix: &[u8] =
if bun_install::dependency::is_safe_install_folder_name(truncated_basename) {
truncated_basename
} else if self.resolution.tag.is_git()
|| self.resolution.tag == ResolutionTag::LocalTarball
{
b"package"
} else {
log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!(
"Refusing to install package with invalid name \"{}\"",
bun_fmt::s(name),
),
);
return Err(bun_core::err!("InstallFailed"));
};

let mut resolved: &'static [u8] = b"";
let tmpname = FileSystem::tmpname(
&basename[0..basename.len().min(32)],
&mut tmpname_buf.0,
bun_core::fast_random(),
)?;
let tmpname =
FileSystem::tmpname(tmpname_suffix, &mut tmpname_buf.0, bun_core::fast_random())?;
{
let extract_destination = match bun_sys::make_path::make_open_path(
tmpdir,
Expand Down
Loading
Loading