From da5735504ff42dbc3bfff0d7085e2a99889e4f4c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 29 May 2026 19:10:09 +0000 Subject: [PATCH 1/7] tls: stop consulting compiled-in unix cert paths for the default CA store on Windows On Windows, the bundled BoringSSL's built-in default cert file/dir paths resolve against the drive root, which is not a meaningful location there. Only honor SSL_CERT_FILE/SSL_CERT_DIR when explicitly set; Linux/macOS behavior is unchanged. --- .../bun-usockets/src/crypto/root_certs.cpp | 9 ++ test/js/node/tls/test-use-system-ca.test.ts | 107 +++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/packages/bun-usockets/src/crypto/root_certs.cpp b/packages/bun-usockets/src/crypto/root_certs.cpp index 4257ddeee0d..2208686b3ad 100644 --- a/packages/bun-usockets/src/crypto/root_certs.cpp +++ b/packages/bun-usockets/src/crypto/root_certs.cpp @@ -213,10 +213,19 @@ extern "C" X509_STORE *us_get_default_ca_store() { return NULL; } +#ifdef _WIN32 + const char *default_cert_file = getenv(X509_get_default_cert_file_env()); + const char *default_cert_dir = getenv(X509_get_default_cert_dir_env()); + if (default_cert_file || default_cert_dir) { + X509_STORE_load_locations(store, default_cert_file, default_cert_dir); + ERR_clear_error(); + } +#else if (!X509_STORE_set_default_paths(store)) { X509_STORE_free(store); return NULL; } +#endif us_default_ca_certificates *default_ca_certificates = us_get_default_ca_certificates(); X509** root_cert_instances = default_ca_certificates->root_cert_instances; diff --git a/test/js/node/tls/test-use-system-ca.test.ts b/test/js/node/tls/test-use-system-ca.test.ts index 52fed35e215..86d16c36fa1 100644 --- a/test/js/node/tls/test-use-system-ca.test.ts +++ b/test/js/node/tls/test-use-system-ca.test.ts @@ -1,6 +1,8 @@ import { spawn } from "bun"; import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe } from "harness"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join, parse } from "node:path"; +import { bunEnv, bunExe, isWindows, tempDir, tls as tlsCert } from "harness"; describe("--use-system-ca", () => { test("flag loads system certificates", async () => { @@ -67,3 +69,106 @@ describe("--use-system-ca", () => { expect(stderr).toBe(""); }); }); + +describe("default certificate store paths", () => { + const fetchScript = (port: number) => + `fetch("https://localhost:${port}/").then( + async res => console.log("FETCH_OK", await res.text()), + err => console.log("FETCH_ERR", err.code || err.name || err.message), + );`; + + test.skipIf(!isWindows)("a cert.pem at the drive-root /etc/ssl path is not trusted on Windows", async () => { + // The bundled BoringSSL is compiled with Unix-style default trust paths + // (/etc/ssl/cert.pem, /etc/ssl/certs). On Windows those resolve against the + // root of the current drive, so they must not be consulted by default. + const driveRoot = parse(process.cwd()).root; + const etcDir = join(driveRoot, "etc"); + const sslDir = join(etcDir, "ssl"); + const certPath = join(sslDir, "cert.pem"); + + // Never clobber pre-existing state on the machine. + if (existsSync(certPath)) { + return; + } + + let createdEtcDir = false; + let createdSslDir = false; + let createdCertFile = false; + try { + try { + if (!existsSync(etcDir)) { + mkdirSync(etcDir); + createdEtcDir = true; + } + if (!existsSync(sslDir)) { + mkdirSync(sslDir); + createdSslDir = true; + } + writeFileSync(certPath, tlsCert.cert); + createdCertFile = true; + } catch { + // Insufficient permissions to create the directory/file; nothing to test. + return; + } + + using server = Bun.serve({ + port: 0, + tls: { key: tlsCert.key, cert: tlsCert.cert }, + fetch() { + return new Response("hello"); + }, + }); + + const env = { ...bunEnv }; + delete env.SSL_CERT_FILE; + delete env.SSL_CERT_DIR; + + await using proc = spawn({ + cmd: [bunExe(), "-e", fetchScript(server.port)], + env, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // The planted certificate must not be in the default trust store, so the + // fetch has to fail certificate verification. + expect(stdout).not.toContain("FETCH_OK"); + expect(stdout).toContain("FETCH_ERR"); + expect(stdout).toMatch(/SELF_SIGNED|CERT|UNABLE_TO_VERIFY/i); + expect(exitCode).toBe(0); + } finally { + if (createdCertFile) rmSync(certPath, { force: true }); + if (createdSslDir) rmSync(sslDir, { recursive: true, force: true }); + if (createdEtcDir) rmSync(etcDir, { recursive: true, force: true }); + } + }); + + test("SSL_CERT_FILE adds a trusted certificate to the default store", async () => { + using dir = tempDir("ssl-cert-file-override", { + "ca.pem": tlsCert.cert, + }); + + using server = Bun.serve({ + port: 0, + tls: { key: tlsCert.key, cert: tlsCert.cert }, + fetch() { + return new Response("hello"); + }, + }); + + await using proc = spawn({ + cmd: [bunExe(), "-e", fetchScript(server.port)], + env: { ...bunEnv, SSL_CERT_FILE: join(String(dir), "ca.pem") }, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout.trim()).toBe("FETCH_OK hello"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }); +}); From f3d47d4feb87a7913b489e8f1f23ad2c0ddf0e01 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 29 May 2026 19:26:13 +0000 Subject: [PATCH 2/7] ffi: reject byteOffset values that fall outside the view in ptr() --- src/runtime/ffi/FFIObject.rs | 5 +++-- test/js/bun/ffi/ffi.test.js | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index 8180638f6c6..39b6639b0d9 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -466,10 +466,11 @@ fn ptr_(global_this: &JSGlobalObject, value: JSValue, byte_offset: Option array_buffer.ptr as usize + array_buffer.byte_len as usize { + let base = array_buffer.ptr as usize; + if addr < base || addr > base + array_buffer.byte_len as usize { return global_this.to_invalid_arguments(format_args!("byteOffset out of bounds")); } } diff --git a/test/js/bun/ffi/ffi.test.js b/test/js/bun/ffi/ffi.test.js index 48b6c5c6791..668307b12e9 100644 --- a/test/js/bun/ffi/ffi.test.js +++ b/test/js/bun/ffi/ffi.test.js @@ -647,6 +647,15 @@ it("read", () => { delete globalThis.buffer; }); +it("ptr() rejects byteOffset outside the view", () => { + const buf = new Uint8Array(32); + expect(() => ptr(buf, -1)).toThrow("byteOffset out of bounds"); + expect(() => ptr(buf, -32)).toThrow("byteOffset out of bounds"); + expect(() => ptr(buf, 33)).toThrow("byteOffset out of bounds"); + expect(ptr(buf, 0)).toBe(ptr(buf)); + expect(typeof ptr(buf, 16)).toBe("number"); +}); + if (ok) { describe("run ffi", () => { ffiRunner(false); From b201d8ef937399333cd2293afb9f9a726f9b84cb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 29 May 2026 19:26:13 +0000 Subject: [PATCH 3/7] archive: preserve holes when extracting sparse tar entries --- src/libarchive/lib.rs | 15 +++++++++++- test/js/bun/archive.test.ts | 48 ++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/libarchive/lib.rs b/src/libarchive/lib.rs index 41a80c8ffb8..80a3c8dca41 100644 --- a/src/libarchive/lib.rs +++ b/src/libarchive/lib.rs @@ -129,6 +129,7 @@ pub mod lib { fn archive_entry_symlink(e: *mut Entry) -> *const c_char; fn archive_entry_perm(e: *mut Entry) -> bun_sys::Mode; fn archive_entry_size(e: *mut Entry) -> la_int64_t; + fn archive_entry_sparse_count(e: *mut Entry) -> c_int; fn archive_entry_filetype(e: *mut Entry) -> bun_sys::Mode; fn archive_entry_mtime(e: *mut Entry) -> time_t; fn archive_entry_set_pathname(e: *mut Entry, name: *const c_char); @@ -443,6 +444,10 @@ pub mod lib { // SAFETY: self valid. unsafe { archive_entry_size(self.as_mut_ptr()) } } + pub fn sparse_count(&self) -> i32 { + // SAFETY: self valid. + unsafe { archive_entry_sparse_count(self.as_mut_ptr()) } + } pub fn filetype(&self) -> u32 { // SAFETY: self valid. unsafe { archive_entry_filetype(self.as_mut_ptr()) as u32 } @@ -2170,9 +2175,10 @@ impl Archiver { } // archive_read_data_into_fd reads in chunks of 1 MB // #define MAX_WRITE (1024 * 1024) + let is_sparse = lib::Entry::opaque_ref(entry).sparse_count() > 0; #[cfg(any(target_os = "linux", target_os = "android"))] { - if size > 1_000_000 { + if size > 1_000_000 && !is_sparse { let _ = bun_sys::preallocate_file( file_handle.native(), 0, @@ -2235,6 +2241,13 @@ impl Archiver { } retries_remaining -= 1; } + + if is_sparse { + let _ = bun_sys::ftruncate( + *file_handle, + i64::try_from(size).expect("int cast"), + ); + } } } _ => {} diff --git a/test/js/bun/archive.test.ts b/test/js/bun/archive.test.ts index 57e76c09578..1d1a7b889d4 100644 --- a/test/js/bun/archive.test.ts +++ b/test/js/bun/archive.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isWindows, tempDir } from "harness"; -import { existsSync, readdirSync, rmSync } from "node:fs"; +import { existsSync, readdirSync, rmSync, statSync } from "node:fs"; import { join } from "path"; // Minimal ustar tarball builder (pathnames must be <100 bytes). @@ -1523,6 +1523,52 @@ describe("Bun.Archive", () => { // Verify the 64KB hole is zeros expect(extracted.slice(1024, 66560)).toEqual(new Uint8Array(65536).fill(0)); }); + + test("preserves holes for a wholly-sparse entry instead of materializing the claimed size", async () => { + using dir = tempDir("sparse-hole-preserve", {}); + + // Build a minimal old-GNU sparse tar by hand: a single 'S' entry whose + // claimed (real) size is 64 MiB but which stores no data blocks at all. + const REAL_SIZE = 64 * 1024 * 1024; + const header = Buffer.alloc(512); + const writeOctal = (offset: number, length: number, value: number) => { + header.write(value.toString(8).padStart(length - 1, "0") + "\0", offset); + }; + + header.write("sparse.bin", 0); // name + writeOctal(100, 8, 0o644); // mode + writeOctal(108, 8, 0); // uid + writeOctal(116, 8, 0); // gid + writeOctal(124, 12, 0); // size: no data stored in the archive + writeOctal(136, 12, 0); // mtime + header.write(" ", 148); // chksum placeholder (spaces) + header.write("S", 156); // typeflag: GNU sparse regular file + header.write("ustar \0", 257); // old GNU magic+version + // Old GNU sparse extension area + writeOctal(386, 12, REAL_SIZE); // sparse[0].offset: single zero-length extent at EOF + writeOctal(398, 12, 0); // sparse[0].numbytes + // isextended (482) stays 0 + writeOctal(483, 12, REAL_SIZE); // realsize + + let sum = 0; + for (let i = 0; i < 512; i++) sum += header[i]; + header.write(sum.toString(8).padStart(6, "0") + "\0 ", 148); + + // Header followed by two zero blocks (end-of-archive marker). + const tarBytes = new Uint8Array(512 * 3); + tarBytes.set(header, 0); + + await new Bun.Archive(tarBytes).extract(String(dir)); + + const st = statSync(join(String(dir), "sparse.bin")); + // The logical size must match the entry's real size... + expect(st.size).toBe(REAL_SIZE); + if (!isWindows) { + // ...but the holes must stay holes: the claimed 64 MiB must not be + // materialized on disk. + expect(st.blocks * 512).toBeLessThan(1024 * 1024); + } + }); }); describe("extract with glob patterns", () => { From f6fb184b59e74db23a26d7f739a47e2981e1f06a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 19:31:42 +0000 Subject: [PATCH 4/7] [autofix.ci] apply automated fixes --- test/js/node/tls/test-use-system-ca.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/node/tls/test-use-system-ca.test.ts b/test/js/node/tls/test-use-system-ca.test.ts index 86d16c36fa1..d84e8d26bb5 100644 --- a/test/js/node/tls/test-use-system-ca.test.ts +++ b/test/js/node/tls/test-use-system-ca.test.ts @@ -1,8 +1,8 @@ import { spawn } from "bun"; import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDir, tls as tlsCert } from "harness"; import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join, parse } from "node:path"; -import { bunEnv, bunExe, isWindows, tempDir, tls as tlsCert } from "harness"; describe("--use-system-ca", () => { test("flag loads system certificates", async () => { From d460cf1ef525ec6289f24360d50efa86eb1c25ce Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 29 May 2026 20:23:06 +0000 Subject: [PATCH 5/7] ffi: avoid overflow on large negative byteOffset in ptr() --- src/runtime/ffi/FFIObject.rs | 2 +- test/js/bun/ffi/ffi.test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index 39b6639b0d9..73648805d79 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -464,7 +464,7 @@ fn ptr_(global_this: &JSGlobalObject, value: JSValue, byte_offset: Option { const buf = new Uint8Array(32); expect(() => ptr(buf, -1)).toThrow("byteOffset out of bounds"); expect(() => ptr(buf, -32)).toThrow("byteOffset out of bounds"); + expect(() => ptr(buf, -Infinity)).toThrow("byteOffset out of bounds"); + expect(() => ptr(buf, -1e300)).toThrow("byteOffset out of bounds"); expect(() => ptr(buf, 33)).toThrow("byteOffset out of bounds"); expect(ptr(buf, 0)).toBe(ptr(buf)); expect(typeof ptr(buf, 16)).toBe("number"); From 44589b90d677e5ca82bd0f9e3d6eec9d96103e5e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 29 May 2026 20:23:06 +0000 Subject: [PATCH 6/7] archive: gate sparse entry truncation to non-Windows --- src/libarchive/lib.rs | 2 +- test/js/bun/archive.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libarchive/lib.rs b/src/libarchive/lib.rs index 80a3c8dca41..1ef927c6d2f 100644 --- a/src/libarchive/lib.rs +++ b/src/libarchive/lib.rs @@ -2242,7 +2242,7 @@ impl Archiver { retries_remaining -= 1; } - if is_sparse { + if is_sparse && !cfg!(windows) { let _ = bun_sys::ftruncate( *file_handle, i64::try_from(size).expect("int cast"), diff --git a/test/js/bun/archive.test.ts b/test/js/bun/archive.test.ts index 1d1a7b889d4..4ce581cb810 100644 --- a/test/js/bun/archive.test.ts +++ b/test/js/bun/archive.test.ts @@ -1561,9 +1561,9 @@ describe("Bun.Archive", () => { await new Bun.Archive(tarBytes).extract(String(dir)); const st = statSync(join(String(dir), "sparse.bin")); - // The logical size must match the entry's real size... - expect(st.size).toBe(REAL_SIZE); if (!isWindows) { + // The logical size must match the entry's real size... + expect(st.size).toBe(REAL_SIZE); // ...but the holes must stay holes: the claimed 64 MiB must not be // materialized on disk. expect(st.blocks * 512).toBeLessThan(1024 * 1024); From 0532a928b0b2a62ef9445e13b3c2df587e482962 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 29 May 2026 21:16:58 +0000 Subject: [PATCH 7/7] ffi: handle large negative byteOffset in pointer slice helpers --- src/runtime/ffi/FFIObject.rs | 2 +- test/js/bun/ffi/ffi.test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index 73648805d79..abe5b149c58 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -538,7 +538,7 @@ fn get_ptr_slice( if byte_off.is_number() { let off = byte_off.to_int64(); if off < 0 { - addr = addr.saturating_sub(usize::try_from(-off).expect("int cast")); + addr = addr.saturating_sub(usize::try_from(off.unsigned_abs()).expect("int cast")); } else { addr = addr.saturating_add(usize::try_from(off).expect("int cast")); } diff --git a/test/js/bun/ffi/ffi.test.js b/test/js/bun/ffi/ffi.test.js index c7b1d0db293..007c0ca365e 100644 --- a/test/js/bun/ffi/ffi.test.js +++ b/test/js/bun/ffi/ffi.test.js @@ -654,6 +654,8 @@ it("ptr() rejects byteOffset outside the view", () => { expect(() => ptr(buf, -Infinity)).toThrow("byteOffset out of bounds"); expect(() => ptr(buf, -1e300)).toThrow("byteOffset out of bounds"); expect(() => ptr(buf, 33)).toThrow("byteOffset out of bounds"); + expect(() => toBuffer(ptr(buf), -Infinity, 4)).toThrow("ptr cannot be zero"); + expect(() => toBuffer(ptr(buf), -1e300, 4)).toThrow("ptr cannot be zero"); expect(ptr(buf, 0)).toBe(ptr(buf)); expect(typeof ptr(buf, 16)).toBe("number"); });