Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
04eda02
Tighten input validation, bounds checking, and object lifetime handli…
Jarred-Sumner Jun 29, 2026
75388d0
Run cargo fmt
Jarred-Sumner Jun 29, 2026
0253e0d
Address review feedback: cover sibling code paths, remove newly dead …
Jarred-Sumner Jun 29, 2026
44fc770
Compute UTF-8 lengths of invalid UTF-16 exactly and skip the dealloca…
Jarred-Sumner Jun 29, 2026
8601143
Add regression tests across the touched subsystems
Jarred-Sumner Jun 29, 2026
4107b11
glob: scanning through a self-referential directory symlink now compl…
Jarred-Sumner Jun 29, 2026
b54c4f5
node:http: accept the request host values Node accepts
Jarred-Sumner Jun 29, 2026
d959468
Make the new regression tests deterministic across platforms and proc…
Jarred-Sumner Jun 29, 2026
aeb6893
glob: bound the followed-link ancestor scan to the live walk stack
Jarred-Sumner Jun 29, 2026
0b2767f
fetch: track the decompressed-body limit across the whole response
Jarred-Sumner Jun 29, 2026
67b8beb
node:http2: scope stream access to the dispatch guard and honor the H…
Jarred-Sumner Jun 29, 2026
003f664
http: cross-reference the two Host validators and pin CONNECT framing…
Jarred-Sumner Jun 29, 2026
7521af3
pack: apply only the non-overridable default ignores before "files" i…
Jarred-Sumner Jun 29, 2026
f306c90
valkey: derive the subscription reply kinds from the push-message table
Jarred-Sumner Jun 29, 2026
8249bf7
md: degrade reference-link expansion to text when the output budget i…
Jarred-Sumner Jun 29, 2026
f650d8f
Make the record conversion fast path observe properties in specificat…
Jarred-Sumner Jun 29, 2026
b8fe3f5
node:url: stop the legacy protocol tables from inheriting Object.prot…
Jarred-Sumner Jun 29, 2026
f5c15bd
macros: serialize an own __proto__ key as a computed property
Jarred-Sumner Jun 29, 2026
e87fb97
fs: copy Windows directory junctions with the unprivileged symlink he…
Jarred-Sumner Jun 29, 2026
36a089d
wrapAnsi: recompute the row width when the appended word can join the…
Jarred-Sumner Jun 29, 2026
936b6c6
tests: cover multi-wildcard target expansion limits and empty compres…
Jarred-Sumner Jun 29, 2026
c4fd1a6
Keep fetch response decompression unbounded
Jarred-Sumner Jun 29, 2026
8f74717
node:url browser fallback: give the protocol lookup tables a null pro…
Jarred-Sumner Jun 29, 2026
63061c2
glob, s3: address clippy lints (large_types_passed_by_value, or_fun_c…
Jarred-Sumner Jun 30, 2026
2845c7a
json, toml, yaml, css-module loaders: mark an own __proto__ key as co…
Jarred-Sumner Jun 30, 2026
0c9585e
node_fs: rewrite UNC reparse targets to their absolute form when copying
Jarred-Sumner Jun 30, 2026
41c08e0
wrapAnsi: recompute the row width when a no-space seam can join clusters
Jarred-Sumner Jun 30, 2026
b0bb980
pack: let "files" re-include .hg like npm
Jarred-Sumner Jun 30, 2026
6f9e880
h2: validate the teardown error code before any stream is referenced;…
Jarred-Sumner Jun 30, 2026
2c3bafb
http2: frame the HEAD and close-delimited allowHTTP1 fallback respons…
Jarred-Sumner Jun 30, 2026
ea423ac
tests: cover record fast-path structure transitions and the high Host…
Jarred-Sumner Jun 30, 2026
db95dc8
s3: format guess_bucket fallback expressions
Jarred-Sumner Jun 30, 2026
449115d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 30, 2026
6b2a4f3
tests: derive expected stack lines at runtime and retry refused parit…
Jarred-Sumner Jun 30, 2026
6047fa0
completions: extract the script name at the key boundary instead of s…
Jarred-Sumner Jun 30, 2026
6f4fb88
wrapAnsi: classify the wrap seam by the word's first visible codepoint
Jarred-Sumner Jun 30, 2026
d5134c9
resolver: adapt the path length guards to the refactored resolution d…
Jarred-Sumner Jul 1, 2026
b01f5ec
glob, resolver: derive followed-link identity from data the walk alre…
Jarred-Sumner Jul 1, 2026
2f41950
Merge branch 'main' into claude/security-round-11
Jarred-Sumner Jul 3, 2026
2e5eb28
Bun.serve: keep the request.url host fallback and drop the parser-lev…
Jarred-Sumner Jul 4, 2026
b42fbd3
install: drop the lockfile script normalization on load and the unuse…
Jarred-Sumner Jul 4, 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
5 changes: 4 additions & 1 deletion .github/workflows/update-vendor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ jobs:

- name: Update version if needed
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
env:
PACKAGE: ${{ matrix.package }}
LATEST_TAG: ${{ steps.check-version.outputs.latest }}
run: |
set -euo pipefail
bun -e 'await Bun.write("test/vendor.json", JSON.stringify((await Bun.file("test/vendor.json").json()).map(v=>{if(v.package===process.argv[1])v.tag=process.argv[2];return v;}), null, 2) + "\n")' ${{ matrix.package }} ${{ steps.check-version.outputs.latest }}
bun -e 'await Bun.write("test/vendor.json", JSON.stringify((await Bun.file("test/vendor.json").json()).map(v=>{if(v.package===process.argv[1])v.tag=process.argv[2];return v;}), null, 2) + "\n")' "$PACKAGE" "$LATEST_TAG"

- name: Create Pull Request
if: success() && steps.check-version.outputs.current != steps.check-version.outputs.latest
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 17 additions & 6 deletions completions/bun.bash
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ _read_scripts_in_package_json() {
scripts="${scripts//@(\"|\')/}";
readarray -td, scripts <<<"${scripts}";
for completion in "${scripts[@]}"; do
package_json_compreply+=( "${completion%:*}" );
[[ "${completion}" =~ ^[[:space:]]*([[:alnum:]@/:._-]+)[[:space:]]*: ]] && package_json_compreply+=( "${BASH_REMATCH[1]}" );
done
COMPREPLY+=( $(compgen -W "${package_json_compreply[*]}" -- "${cur_word}") );
}
Expand All @@ -59,9 +59,16 @@ _read_scripts_in_package_json() {
( "${COMPREPLY[*]}" =~ ${re_prev_script} && -n "${COMP_WORDS[2]}" ) || \
( "${COMPREPLY[*]}" =~ ${re_comp_word_script} )
]] && {
local re_script=$(echo ${package_json_compreply[@]} | sed 's/[^ ]*/(&)/g');
local new_reply=$(echo "${COMPREPLY[@]}" | sed -E "s/$re_script//");
COMPREPLY=( $(compgen -W "${new_reply}" -- "${cur_word}") );
local filtered_reply=();
local reply_word script_name keep;
for reply_word in "${COMPREPLY[@]}"; do
keep=1;
for script_name in "${package_json_compreply[@]}"; do
[[ "${reply_word}" == "${script_name}" ]] && { keep=""; break; };
done
[[ -n "${keep}" ]] && filtered_reply+=( "${reply_word}" );
done
COMPREPLY=( "${filtered_reply[@]}" );
replaced_script="${prev}";
}
}
Expand Down Expand Up @@ -175,8 +182,12 @@ _bun_completions() {
# the previous word is not part of the allowed completion
# the previous word is not an argument to the last two option
[[ -z "${cur_word}" ]] && {
declare -A comp_reply_associative="( $(echo ${COMPREPLY[@]} | sed 's/[^ ]*/[&]=&/g') )";
[[ -z "${comp_reply_associative[${prev}]}" ]] && {
local prev_in_reply="";
local reply_word;
for reply_word in "${COMPREPLY[@]}"; do
[[ "${reply_word}" == "${prev}" ]] && { prev_in_reply=1; break; };
done
[[ -z "${prev_in_reply}" ]] && {
local re_prev_prev="(^| )${COMP_WORDS[(( COMP_CWORD - 2 ))]}($| )";
local global_option_with_extra_args="--bunfile --server-bunfile --config --port --cwd --public-dir --jsx-runtime --platform --loader";
[[
Expand Down
24 changes: 13 additions & 11 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,8 @@ _bun() {
cmd)
local -a scripts_list
IFS=$'\n' scripts_list=($(SHELL=zsh bun getcompletes i))
scripts="scripts:scripts:((${scripts_list//:/\\\\:}))"
scripts="scripts:scripts:compadd -a scripts_list"
local -a files_list
IFS=$'\n' files_list=($(SHELL=zsh bun getcompletes j))

main_commands=(
Expand All @@ -761,7 +762,7 @@ _bun() {
'help\:"Show all supported flags and commands" '
)
main_commands=($main_commands)
_alternative "$scripts" "args:command:(($main_commands))" "files:files:(($files_list))"
_alternative "$scripts" "args:command:(($main_commands))" "files:files:compadd -a files_list"

;;
args)
Expand Down Expand Up @@ -942,12 +943,12 @@ _bun_list_bunfig_toml() {
}

_bun_run_param_script_completion() {
local -a scripts_list
local -a scripts_list bins
IFS=$'\n' scripts_list=($(SHELL=zsh bun getcompletes s))
IFS=$'\n' bins=($(SHELL=zsh bun getcompletes b))

_alternative "scripts:scripts:((${scripts_list//:/\\\\:}))"
_alternative "bin:bin:((${bins//:/\\\\:}))"
_alternative "scripts:scripts:compadd -a scripts_list"
_alternative "bin:bin:compadd -a bins"
_alternative "files:file:_files -g '*.(js|ts|jsx|tsx|wasm)'"
}

Expand All @@ -958,8 +959,8 @@ _bun_link_param_package_completion() {
global_node_modules=$install_dir/install/global/node_modules

local -a packages_full_path=(${global_node_modules}/*(N))
packages=$(echo $packages_full_path | tr ' ' '\n' | xargs basename)
_alternative "dirs:directory:(($packages))"
local -a packages=(${packages_full_path:t})
_alternative "dirs:directory:compadd -a packages"
}

_bun_remove_param_package_completion() {
Expand All @@ -969,10 +970,11 @@ _bun_remove_param_package_completion() {

# TODO: move to "bun getcompletes"
if [ -f "package.json" ]; then
local dependencies=$(jq -r '.dependencies | keys[]' package.json)
local dev_dependencies=$(jq -r '.devDependencies | keys[]' package.json)
_alternative "deps:dependency:(($dependencies))"
_alternative "deps:dependency:(($dev_dependencies))"
local -a dependencies dev_dependencies
IFS=$'\n' dependencies=($(jq -r '.dependencies | keys[]' package.json))
IFS=$'\n' dev_dependencies=($(jq -r '.devDependencies | keys[]' package.json))
_alternative "deps:dependency:compadd -a dependencies"
_alternative "deps:dependency:compadd -a dev_dependencies"
fi
}

Expand Down
2 changes: 1 addition & 1 deletion dockerhub/alpine/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ RUN apk --no-cache add ca-certificates curl dirmngr gpg gpg-agent unzip \
-fsSLO \
--compressed \
--retry 5 \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& gpg --batch --verify --output SHASUMS256.txt SHASUMS256.txt.asc \
|| (echo "error: failed to verify: $tag" && exit 1) \
&& grep " bun-linux-$build.zip\$" SHASUMS256.txt | sha256sum -c - \
|| (echo "error: failed to verify: $tag" && exit 1) \
Expand Down
2 changes: 1 addition & 1 deletion dockerhub/debian-slim/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ RUN apt-get update -qq \
-fsSLO \
--compressed \
--retry 5 \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& gpg --batch --verify --output SHASUMS256.txt SHASUMS256.txt.asc \
|| (echo "error: failed to verify: $tag" && exit 1) \
&& grep " bun-linux-$build.zip\$" SHASUMS256.txt | sha256sum -c - \
|| (echo "error: failed to verify: $tag" && exit 1) \
Expand Down
2 changes: 1 addition & 1 deletion dockerhub/debian/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ RUN apt-get update -qq \
-fsSLO \
--compressed \
--retry 5 \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& gpg --batch --verify --output SHASUMS256.txt SHASUMS256.txt.asc \
|| (echo "error: failed to verify: $tag" && exit 1) \
&& grep " bun-linux-$build.zip\$" SHASUMS256.txt | sha256sum -c - \
|| (echo "error: failed to verify: $tag" && exit 1) \
Expand Down
2 changes: 1 addition & 1 deletion dockerhub/distroless/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ RUN apt-get update -qq \
-fsSLO \
--compressed \
--retry 5 \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& gpg --batch --verify --output SHASUMS256.txt SHASUMS256.txt.asc \
|| (echo "error: failed to verify: $tag" && exit 1) \
&& grep " bun-linux-$build.zip\$" SHASUMS256.txt | sha256sum -c - \
|| (echo "error: failed to verify: $tag" && exit 1) \
Expand Down
3 changes: 2 additions & 1 deletion packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChildProcess, spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { EventEmitter } from "node:events";
import { AddressInfo, createServer, Socket } from "node:net";
import * as path from "node:path";
Expand Down Expand Up @@ -2813,7 +2814,7 @@ function nextId(): number {
}

export function getRandomId() {
return Math.random().toString(36).slice(2);
return randomBytes(16).toString("hex");
}

export function normalizeWindowsPath(winPath: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { connect } from "node:net";
import { networkInterfaces } from "node:os";
import { WebSocketDebugAdapter } from "./adapter.js";
import { getRandomId, WebSocketDebugAdapter } from "./adapter.js";
import { TCPSocketSignal } from "./signal.js";
import { SourceMap } from "./sourcemap.js";

Expand Down Expand Up @@ -67,6 +67,16 @@ test("only forwards inspector events from known protocol domains to the adapter"
expect(heapEvents).toEqual([{ collection: { type: "full", startTime: 0, endTime: 1 } }]);
});

test("getRandomId returns a distinct 32-character lowercase hex string on every call", () => {
const ids = new Set<string>();
for (let i = 0; i < 256; i++) {
const id = getRandomId();
expect(id).toMatch(/^[0-9a-f]{32}$/);
ids.add(id);
}
expect(ids.size).toBe(256);
});

test("TCPSocketSignal accepts connections only on the loopback interface", async () => {
// Same construction the VS Code extension uses (diagnostics.ts createSignal).
const signal = new TCPSocketSignal(0);
Expand Down
17 changes: 11 additions & 6 deletions packages/bun-release/src/npm/install.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isAbsolute, relative } from "path";
import { unzipSync } from "zlib";
import { debug, error } from "../console";
import { fetch } from "../fetch";
Expand Down Expand Up @@ -107,12 +108,16 @@ async function downloadBun(platform: Platform, dst: string): Promise<void> {
const size = parseInt(str(offset + 124, 12), 8);
offset += 512;
if (!isNaN(size)) {
write(join(dst, name), buffer.subarray(offset, offset + size));
if (name === platform.exe) {
try {
chmod(join(dst, name), 0o755);
} catch (error) {
debug("chmod failed", error);
const entryPath = join(dst, name);
const entryName = relative(dst, entryPath);
if (entryName && !entryName.startsWith("..") && !isAbsolute(entryName)) {
write(entryPath, buffer.subarray(offset, offset + size));
if (name === platform.exe) {
try {
chmod(entryPath, 0o755);
} catch (error) {
debug("chmod failed", error);
}
}
}
offset += (size + 511) & ~511;
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ struct us_socket_t *us_socket_adopt(struct us_socket_t *s, struct us_socket_grou
s->flags.adopted = 1;
/* Tell the event loop what is the new socket so we can route subsequent events */
s->prev = new_s;
if (s->ssl) {
us_internal_ssl_socket_relocated(loop, s, new_s);
}
}
if (c) {
c->connecting_head = new_s;
Expand Down
12 changes: 12 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,18 @@ static void ssl_release_spill(struct us_loop_t *loop, struct us_socket_t *s) {
}
}

void us_internal_ssl_socket_relocated(struct us_loop_t *loop, struct us_socket_t *old_s,
struct us_socket_t *new_s) {
struct loop_ssl_data *loop_ssl_data = (struct loop_ssl_data *)loop->data.ssl_data;
if (!loop_ssl_data) return;
if (loop_ssl_data->ssl_spill_owner == old_s) {
loop_ssl_data->ssl_spill_owner = new_s;
}
if (loop_ssl_data->ssl_last_fatal_error_owner == (void *)old_s) {
loop_ssl_data->ssl_last_fatal_error_owner = (void *)new_s;
}
}

static int BIO_s_custom_read(BIO *bio, char *dst, int length) {
struct loop_ssl_data *loop_ssl_data = (struct loop_ssl_data *)BIO_get_data(bio);

Expand Down
1 change: 1 addition & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ void us_internal_socket_after_open(us_socket_r s, int error);
void us_internal_ssl_attach(us_socket_r s, struct ssl_ctx_st *ssl_ctx, int is_client, const char *sni, struct us_listen_socket_t *listener);
/* SSL_free(s->ssl); s->ssl = NULL. Idempotent. */
void us_internal_ssl_detach(us_socket_r s);
void us_internal_ssl_socket_relocated(us_loop_r loop, us_socket_r old_s, us_socket_r new_s);

/* TLS-layer event hooks. loop.c calls these instead of us_dispatch_* when
* s->ssl != NULL; they decrypt/encrypt and re-dispatch the plaintext. */
Expand Down
18 changes: 9 additions & 9 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,15 @@ namespace uWS
/* RFC 9112 6.3
* If a message is received with both a Transfer-Encoding and a Content-Length header field,
Comment thread
alii marked this conversation as resolved.
* the Transfer-Encoding overrides the Content-Length. */
if (transferEncoding.has) {
if (isConnectRequest) {
// This only serves to mark that the connect request read all headers
// and can start emitting data. Don't try to parse remaining data as HTTP -
// it's pipelined data that we've already captured in req->head.
remainingStreamingBytes = STATE_IS_CHUNKED;
// Mark remaining data as consumed and break - it's not HTTP
consumedTotal += length;
break;
} else if (transferEncoding.has) {
/* We already validated that chunked is last if present, before calling the handler */
remainingStreamingBytes = STATE_IS_CHUNKED;
/* If consume minimally, we do not want to consume anything but we want to mark this as being chunked */
Expand Down Expand Up @@ -1013,14 +1021,6 @@ namespace uWS
return HttpParserResult::success(consumedTotal, returnedUser);
}
}
} else if(isConnectRequest) {
// This only serves to mark that the connect request read all headers
// and can start emitting data. Don't try to parse remaining data as HTTP -
// it's pipelined data that we've already captured in req->head.
remainingStreamingBytes = STATE_IS_CHUNKED;
// Mark remaining data as consumed and break - it's not HTTP
consumedTotal += length;
break;
} else {
/* If we came here without a body; emit an empty data chunk to signal no data */
void *returnedUser = dataHandler(user, {}, true);
Expand Down
2 changes: 1 addition & 1 deletion packages/bun-vscode/src/features/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ class FileDebugSession extends DebugSession {
}

async initialize() {
const uniqueId = this.sessionId ?? Math.random().toString(36).slice(2);
const uniqueId = this.sessionId ?? getRandomId();
const url =
process.platform === "win32"
? `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`
Expand Down
26 changes: 19 additions & 7 deletions packages/bun-vscode/src/features/lockfile/lockfile.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,35 @@ function styleSection(section: string) {

function styleLine(line: string) {
if (line.startsWith("#")) {
return `<span class="mtk5">${line}</span>`;
return `<span class="mtk5">${escapeHtml(line)}</span>`;
}

const parts = line.trim().split(" ");
if (line.startsWith(" ")) {
return `<span><span class="mtk1">&nbsp;&nbsp;&nbsp;&nbsp;${parts[0]}&nbsp;</span><span class="mtk16">${parts[1]}</span></span>`;
return `<span><span class="mtk1">&nbsp;&nbsp;&nbsp;&nbsp;${escapeHtml(parts[0])}&nbsp;</span><span class="mtk16">${escapeHtml(parts[1])}</span></span>`;
}
if (line.startsWith(" ")) {
const leftPart = `<span class="mtk6">&nbsp;&nbsp;${parts[0]}&nbsp;</span>`;
const leftPart = `<span class="mtk6">&nbsp;&nbsp;${escapeHtml(parts[0])}&nbsp;</span>`;

if (parts.length === 1) return `<span>${leftPart}</span>`;

if (parts[1].startsWith('"http://') || parts[1].startsWith('"https://'))
return `<span>${leftPart}<span class="mtk12 detected-link">${parts[1]}</span></span>`;
if (parts[1].startsWith('"')) return `<span>${leftPart}<span class="mtk16">${parts[1]}</span></span>`;
return `<span>${leftPart}<span class="mtk12 detected-link">${escapeHtml(parts[1])}</span></span>`;
if (parts[1].startsWith('"')) return `<span>${leftPart}<span class="mtk16">${escapeHtml(parts[1])}</span></span>`;

return `<span>${leftPart}<span class="mtk6">${parts[1]}</span></span>`;
return `<span>${leftPart}<span class="mtk6">${escapeHtml(parts[1])}</span></span>`;
}
return `<span class="mtk1">${line}&nbsp;</span>`;
return `<span class="mtk1">${escapeHtml(line)}&nbsp;</span>`;
}

const htmlEscapes: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
};

function escapeHtml(text: string | undefined): string {
return String(text).replace(/[&<>"']/g, character => htmlEscapes[character]);
}
Loading
Loading