Skip to content
Open
9 changes: 9 additions & 0 deletions packages/bun-usockets/src/crypto/root_certs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 14 additions & 1 deletion src/libarchive/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2235,6 +2241,13 @@ impl Archiver {
}
retries_remaining -= 1;
}

if is_sparse && !cfg!(windows) {
let _ = bun_sys::ftruncate(
*file_handle,
i64::try_from(size).expect("int cast"),
);
}
}
}
_ => {}
Expand Down
9 changes: 5 additions & 4 deletions src/runtime/ffi/FFIObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,12 +464,13 @@ fn ptr_(global_this: &JSGlobalObject, value: JSValue, byte_offset: Option<JSValu

let bytei64 = off.to_int64();
if bytei64 < 0 {
addr = addr.saturating_sub(usize::try_from(-bytei64).expect("int cast"));
addr = addr.saturating_sub(usize::try_from(bytei64.unsigned_abs()).expect("int cast"));
Comment thread
Jarred-Sumner marked this conversation as resolved.
} else {
Comment thread
claude[bot] marked this conversation as resolved.
addr += usize::try_from(bytei64).expect("int cast");
addr = addr.saturating_add(usize::try_from(bytei64).expect("int cast"));
}

if addr > 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"));
}
}
Expand Down Expand Up @@ -537,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"));
}
Expand Down
48 changes: 47 additions & 1 deletion test/js/bun/archive.test.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -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"));
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);
}
});
});

describe("extract with glob patterns", () => {
Expand Down
13 changes: 13 additions & 0 deletions test/js/bun/ffi/ffi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,19 @@ 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, -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");
});

if (ok) {
describe("run ffi", () => {
ffiRunner(false);
Expand Down
107 changes: 106 additions & 1 deletion test/js/node/tls/test-use-system-ca.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { spawn } from "bun";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, isWindows, tempDir, tls as tlsCert } from "harness";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join, parse } from "node:path";

describe("--use-system-ca", () => {
test("flag loads system certificates", async () => {
Expand Down Expand Up @@ -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);
});
});
Loading