From 67b843eb2fd16dff906d49241e4544a7d41d050c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:23:44 +0000 Subject: [PATCH 1/5] bunfig: skip or reject config paths that do not fit in a path buffer load_config joined the working directory and "bunfig.toml" (or the --config value) into a stack PathBuffer with the unchecked join, and get_home_config_path did the same with $XDG_CONFIG_HOME / $HOME and ".bunfig.toml". A cwd within 12 bytes of PATH_MAX, a --config value of PATH_MAX bytes or more, or an over-long config home panicked at startup with a slice index out of bounds. Build the paths with join_abs_string_buf_checked, leaving room for the NUL, and length-check the absolute --config arm. A path that does not fit cannot be opened anyway, so it is handled like any other unreadable config: auto-loaded configs (bunfig.toml in the cwd, the global .bunfig.toml) are skipped, and an explicit --config fails with the same ENAMETOOLONG message open() would produce. --- src/bunfig/arguments.rs | 106 ++++++----- test/config/bunfig/bunfig-errors.test.ts | 215 ++++++++++++++++++++++- 2 files changed, 274 insertions(+), 47 deletions(-) diff --git a/src/bunfig/arguments.rs b/src/bunfig/arguments.rs index d0a388d3ef7c..c6076d8ae17f 100644 --- a/src/bunfig/arguments.rs +++ b/src/bunfig/arguments.rs @@ -17,22 +17,48 @@ use crate::bunfig::Bunfig; // ─── bunfig loading ────────────────────────────────────────────────────────── -fn get_home_config_path(buf: &mut PathBuffer) -> Option<&ZStr> { - let paths: [&[u8]; 1] = [b".bunfig.toml"]; +/// `dir/path`, NUL-terminated in `buf`. `dir` (cwd, `$HOME`, ...) and `path` +/// (argv) are unbounded, so the result may not fit; `None` then. No file can be +/// opened at such a path, so callers treat it like any other unreadable config. +fn join_config_path<'buf>( + dir: &[u8], + path: &[u8], + buf: &'buf mut PathBuffer, +) -> Option<&'buf ZStr> { + let max_len = buf.len() - 1; + let len = resolve_path::join_abs_string_buf_checked::( + dir, + &mut buf[..max_len], + &[path], + )? + .len(); + buf[len] = 0; + Some(ZStr::from_buf(&buf[..], len)) +} - if let Some(data_dir) = env_var::XDG_CONFIG_HOME.get() { - return Some(resolve_path::join_abs_string_buf_z::( - data_dir, &mut **buf, &paths, - )); - } +fn get_home_config_path(buf: &mut PathBuffer) -> Option<&ZStr> { + let dir = env_var::XDG_CONFIG_HOME + .get() + .or_else(|| env_var::HOME.get())?; + join_config_path(dir, b".bunfig.toml", buf) +} - if let Some(home_dir) = env_var::HOME.get() { - return Some(resolve_path::join_abs_string_buf_z::( - home_dir, &mut **buf, &paths, - )); +/// An auto-loaded bunfig that cannot be read is treated as absent; one the +/// user asked for is fatal. +fn unreadable_config( + auto_loaded: bool, + err: &bun_sys::Error, + config_path: &[u8], +) -> Result<(), crate::Error> { + if auto_loaded { + return Ok(()); } - - None + bun_core::pretty_errorln!( + "{}\nwhile reading config \"{}\"", + err, + BStr::new(config_path), + ); + Global::exit(1); } fn load_bunfig( @@ -44,17 +70,7 @@ fn load_bunfig( let source = match bun_ast::to_source(config_path, bun_ast::ToSourceOptions { convert_bom: true }) { Ok(s) => s, - Err(err) => { - if auto_loaded { - return Ok(()); - } - bun_core::pretty_errorln!( - "{}\nwhile reading config \"{}\"", - err, - BStr::new(config_path.as_bytes()), - ); - Global::exit(1); - } + Err(err) => return unreadable_config(auto_loaded, &err, config_path.as_bytes()), }; bun_ast::stmt::data::Store::create(); @@ -187,11 +203,12 @@ pub fn load_config( if config_path_.is_empty() { return Ok(()); } - let config_path_len: usize; - if config_path_[0] == b'/' { - config_buf[..config_path_.len()].copy_from_slice(config_path_); - config_buf[config_path_.len()] = 0; - config_path_len = config_path_.len(); + let config_path: Option<&ZStr> = if config_path_[0] == b'/' { + if config_path_.len() < config_buf.len() { + Some(resolve_path::z(config_path_, &mut config_buf)) + } else { + None + } } else { if ctx.args.absolute_working_dir.is_none() { let mut secondbuf = PathBuffer::uninit(); @@ -202,23 +219,20 @@ pub fn load_config( ctx.args.absolute_working_dir = Some(Box::<[u8]>::from(&secondbuf[..cwd_len])); } - // Reshaped for borrowck: `join_abs_string_buf` ties the - // returned slice's lifetime to both `cwd` (borrowed from `ctx.args`) - // and `config_buf`. We only need the length to NUL-terminate and - // re-wrap, so capture `joined.len()` and drop the `ctx` borrow before - // the `&mut ctx` call below. - config_path_len = { - let awd: &[u8] = ctx.args.absolute_working_dir.as_deref().unwrap(); - let parts: [&[u8]; 2] = [awd, config_path_]; - let joined = - resolve_path::join_abs_string_buf::(awd, &mut *config_buf, &parts); - joined.len() - }; - config_buf[config_path_len] = 0; - } - // SAFETY: `config_buf[config_path_len] == 0` (written above on both arms); - // `config_buf` outlives the call. - let config_path = ZStr::from_buf(&config_buf[..], config_path_len); + join_config_path( + ctx.args.absolute_working_dir.as_deref().unwrap(), + config_path_, + &mut config_buf, + ) + }; + let Some(config_path) = config_path else { + return unreadable_config( + auto_loaded, + &bun_sys::Error::from_code(bun_sys::E::ENAMETOOLONG, bun_sys::Tag::open) + .with_path(config_path_), + config_path_, + ); + }; if let Err(err) = load_config_path(cmd, auto_loaded, config_path, ctx) { report_bunfig_load_failure(ctx.log, err); diff --git a/test/config/bunfig/bunfig-errors.test.ts b/test/config/bunfig/bunfig-errors.test.ts index 7f6358af7b18..97e984c2e293 100644 --- a/test/config/bunfig/bunfig-errors.test.ts +++ b/test/config/bunfig/bunfig-errors.test.ts @@ -1,5 +1,7 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; describe.concurrent("bunfig.toml type-mismatch error messages", () => { const cases: [config: string, expected: string][] = [ @@ -30,3 +32,214 @@ describe.concurrent("bunfig.toml type-mismatch error messages", () => { expect(exitCode).not.toBe(0); }); }); + +// bun builds every config path in a stack buffer of MAX_PATH_BYTES (the platform's +// PATH_MAX, see bun_core), so the longest path it can hold is MAX_PATH_BYTES - 1 +// bytes plus the NUL terminator. Paths that do not fit used to overflow the buffer +// (a panic at startup); they must be treated like any other unreadable config. +// +// On Windows the buffer is ~96 KiB, longer than any path, argument or environment +// variable the OS accepts, so the overflow cannot be reached there. +describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path buffer", () => { + const MAX_PATH_BYTES = process.platform === "linux" || process.platform === "android" ? 4096 : 1024; + // A TOML syntax error: every command fails to load it, and the error names the + // path the config was loaded from. + const INVALID_BUNFIG = "[install\n"; + const SEGMENT = Buffer.alloc(200, "d").toString(); + + /** An absolute path below `root` that is exactly `length` bytes long (ASCII only). */ + function pathOfLength(root: string, length: number): string { + let path = root; + // Leave room for the final component, which has to stay under NAME_MAX (255). + while (length - path.length > 256) path = join(path, SEGMENT); + path = join(path, Buffer.alloc(length - path.length - 1, "L").toString()); + expect(path).toHaveLength(length); + return path; + } + + /** Writes an invalid config at `configPath` and returns it. */ + function invalidConfigAt(configPath: string): string { + mkdirSync(dirname(configPath), { recursive: true }); + writeFileSync(configPath, INVALID_BUNFIG); + return configPath; + } + + async function runBun(args: string[], cwd: string, env: Record = {}) { + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + env: { ...bunEnv, ...env }, + cwd, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + type Result = Awaited>; + + function expectLoadedFrom({ stdout, stderr, exitCode }: Result, configPath: string) { + expect(stderr).toContain(`at ${configPath}:`); + expect(stderr).toContain("failed to load bunfig"); + expect(stdout).toBe(""); + expect(exitCode).toBe(1); + } + + function expectNameTooLong({ stdout, stderr, exitCode }: Result, configArg: string) { + expect(stderr.replaceAll(configArg, "")).toBe( + 'ENAMETOOLONG: : File name too long (open())\nwhile reading config ""\n', + ); + expect(stdout).toBe(""); + expect(exitCode).toBe(1); + } + + describe("bunfig.toml auto-loaded from the working directory", () => { + const PRINT_CWD_LENGTH = "console.log(process.cwd().length)"; + + test("is loaded when its path is exactly MAX_PATH_BYTES - 1 bytes", async () => { + using dir = tempDir("bunfig-long-cwd", {}); + const cwd = pathOfLength(String(dir), MAX_PATH_BYTES - "/bunfig.toml".length - 1); + const config = invalidConfigAt(join(cwd, "bunfig.toml")); + expect(config).toHaveLength(MAX_PATH_BYTES - 1); + + expectLoadedFrom(await runBun(["-e", PRINT_CWD_LENGTH], cwd), config); + }); + + // No bunfig.toml exists in these directories: its path would be too long to + // create, and the path is built (and used to overflow) before it is opened. + const skippedCases: [configPathWouldBe: string, cwdLength: number][] = [ + ["exactly MAX_PATH_BYTES bytes, leaving no room for the NUL", MAX_PATH_BYTES - "/bunfig.toml".length], + ["longer than the buffer", MAX_PATH_BYTES - 1], + ]; + + test.each(skippedCases)("bun -e still runs when the bunfig.toml path would be %s", async (_, cwdLength) => { + using dir = tempDir("bunfig-long-cwd", {}); + const cwd = pathOfLength(String(dir), cwdLength); + mkdirSync(cwd, { recursive: true }); + + expect(await runBun(["-e", PRINT_CWD_LENGTH], cwd)).toEqual({ + stdout: `${cwdLength}\n`, + stderr: "", + exitCode: 0, + }); + }); + + test("bun still runs when the bunfig.toml path does not fit", async () => { + using dir = tempDir("bunfig-long-cwd", {}); + const cwdLength = MAX_PATH_BYTES - "/bunfig.toml".length; + const cwd = pathOfLength(String(dir), cwdLength); + mkdirSync(cwd, { recursive: true }); + writeFileSync(join(cwd, "x.cjs"), PRINT_CWD_LENGTH); + + expect(await runBun(["x.cjs"], cwd)).toEqual({ + stdout: `${cwdLength}\n`, + stderr: "", + exitCode: 0, + }); + }); + }); + + describe("--config=", () => { + test("is loaded when the resolved path is exactly MAX_PATH_BYTES - 1 bytes", async () => { + using dir = tempDir("bunfig-long-config", {}); + const config = invalidConfigAt(pathOfLength(String(dir), MAX_PATH_BYTES - 1)); + + expectLoadedFrom(await runBun([`--config=${relative(String(dir), config)}`, "-e", "1"], String(dir)), config); + }); + + test("fails with ENAMETOOLONG when the resolved path is MAX_PATH_BYTES bytes", async () => { + using dir = tempDir("bunfig-long-config", {}); + const configArg = relative(String(dir), pathOfLength(String(dir), MAX_PATH_BYTES)); + + expectNameTooLong(await runBun([`--config=${configArg}`, "-e", "1"], String(dir)), configArg); + }); + + test("fails with ENAMETOOLONG when the argument alone is longer than the buffer", async () => { + using dir = tempDir("bunfig-long-config", {}); + const configArg = Buffer.alloc(MAX_PATH_BYTES + 1000, "a").toString(); + + expectNameTooLong(await runBun([`--config=${configArg}`, "-e", "1"], String(dir)), configArg); + }); + + test("is loaded when a path longer than the buffer normalizes to one that fits", async () => { + using dir = tempDir("bunfig-long-config", { "bunfig.toml": INVALID_BUNFIG }); + const configArg = "x/../".repeat(Math.ceil(MAX_PATH_BYTES / "x/../".length)) + "bunfig.toml"; + expect(configArg.length).toBeGreaterThan(MAX_PATH_BYTES); + + expectLoadedFrom( + await runBun([`--config=${configArg}`, "-e", "1"], String(dir)), + join(String(dir), "bunfig.toml"), + ); + }); + }); + + describe("--config=", () => { + test("is loaded when the path is exactly MAX_PATH_BYTES - 1 bytes", async () => { + using dir = tempDir("bunfig-long-config", {}); + const config = invalidConfigAt(pathOfLength(String(dir), MAX_PATH_BYTES - 1)); + + expectLoadedFrom(await runBun([`--config=${config}`, "-e", "1"], String(dir)), config); + }); + + const tooLongCases: [pathIs: string, configArgBelow: (root: string) => string][] = [ + ["exactly MAX_PATH_BYTES bytes", root => pathOfLength(root, MAX_PATH_BYTES)], + ["longer than the buffer", () => "/" + Buffer.alloc(MAX_PATH_BYTES + 1000, "a").toString()], + ]; + + test.each(tooLongCases)("fails with ENAMETOOLONG when the path is %s", async (_, configArgBelow) => { + using dir = tempDir("bunfig-long-config", {}); + const configArg = configArgBelow(String(dir)); + + expectNameTooLong(await runBun([`--config=${configArg}`, "-e", "1"], String(dir)), configArg); + }); + }); + + // Install commands also read $XDG_CONFIG_HOME/.bunfig.toml (or $HOME/.bunfig.toml). + // `bun pm cache` prints the cache directory once that config has been handled. + describe("global .bunfig.toml", () => { + const CACHE_DIR_MARKER = "/bunfig-global-test-cache-dir"; + const PACKAGE_JSON = { "package.json": JSON.stringify({ name: "bunfig-global-test" }) }; + async function pmCache(cwd: string, env: Record): Promise { + const result = await runBun(["pm", "cache"], cwd, { BUN_INSTALL_CACHE_DIR: CACHE_DIR_MARKER, ...env }); + return { ...result, stdout: result.stdout.trim() }; + } + + test("is loaded when its path is exactly MAX_PATH_BYTES - 1 bytes", async () => { + using dir = tempDir("bunfig-long-global", PACKAGE_JSON); + const configHome = pathOfLength(String(dir), MAX_PATH_BYTES - "/.bunfig.toml".length - 1); + const config = invalidConfigAt(join(configHome, ".bunfig.toml")); + expect(config).toHaveLength(MAX_PATH_BYTES - 1); + + expectLoadedFrom(await pmCache(String(dir), { XDG_CONFIG_HOME: configHome }), config); + }); + + // The directories never exist; only the length of the variable matters. The + // longest value used here still leaves room for the "/.npmrc" that install + // commands append to the same variables afterwards. + const skippedCases: [configPathWouldBe: string, configHomeLength: number][] = [ + ["exactly MAX_PATH_BYTES bytes, leaving no room for the NUL", MAX_PATH_BYTES - "/.bunfig.toml".length], + ["longer than the buffer", MAX_PATH_BYTES - "/.npmrc".length - 1], + ]; + + test.each(skippedCases)("is skipped when its $XDG_CONFIG_HOME path would be %s", async (_, configHomeLength) => { + using dir = tempDir("bunfig-long-global", PACKAGE_JSON); + const configHome = pathOfLength(String(dir), configHomeLength); + + expect(await pmCache(String(dir), { XDG_CONFIG_HOME: configHome })).toEqual({ + stdout: CACHE_DIR_MARKER, + stderr: "", + exitCode: 0, + }); + }); + + test("is skipped when its $HOME path would be exactly MAX_PATH_BYTES bytes", async () => { + using dir = tempDir("bunfig-long-global", PACKAGE_JSON); + const home = pathOfLength(String(dir), MAX_PATH_BYTES - "/.bunfig.toml".length); + + expect(await pmCache(String(dir), { HOME: home, XDG_CONFIG_HOME: undefined })).toEqual({ + stdout: CACHE_DIR_MARKER, + stderr: "", + exitCode: 0, + }); + }); + }); +}); From 920a639d362b1f5a8e642513cdb5a8bb85a88978 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:50:08 +0000 Subject: [PATCH 2/5] test: put the bun pm cache directory inside the temp dir bun pm cache creates the directory named by BUN_INSTALL_CACHE_DIR and falls back to node_modules/.cache when it cannot, so a marker under / only worked when the tests ran as root. --- test/config/bunfig/bunfig-errors.test.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/test/config/bunfig/bunfig-errors.test.ts b/test/config/bunfig/bunfig-errors.test.ts index 97e984c2e293..5f2a52b0e4e3 100644 --- a/test/config/bunfig/bunfig-errors.test.ts +++ b/test/config/bunfig/bunfig-errors.test.ts @@ -196,12 +196,16 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu // Install commands also read $XDG_CONFIG_HOME/.bunfig.toml (or $HOME/.bunfig.toml). // `bun pm cache` prints the cache directory once that config has been handled. describe("global .bunfig.toml", () => { - const CACHE_DIR_MARKER = "/bunfig-global-test-cache-dir"; const PACKAGE_JSON = { "package.json": JSON.stringify({ name: "bunfig-global-test" }) }; - async function pmCache(cwd: string, env: Record): Promise { - const result = await runBun(["pm", "cache"], cwd, { BUN_INSTALL_CACHE_DIR: CACHE_DIR_MARKER, ...env }); + // `bun pm cache` creates the directory it prints, and silently falls back to + // node_modules/.cache when it cannot, so it has to live inside the temp dir. + const cacheDirIn = (dir: string) => join(dir, "install-cache"); + async function pmCache(dir: string, env: Record): Promise { + const result = await runBun(["pm", "cache"], dir, { BUN_INSTALL_CACHE_DIR: cacheDirIn(dir), ...env }); return { ...result, stdout: result.stdout.trim() }; } + /** The global config was skipped and the command went on to print the cache directory. */ + const printedCacheDir = (dir: string): Result => ({ stdout: cacheDirIn(dir), stderr: "", exitCode: 0 }); test("is loaded when its path is exactly MAX_PATH_BYTES - 1 bytes", async () => { using dir = tempDir("bunfig-long-global", PACKAGE_JSON); @@ -224,22 +228,16 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu using dir = tempDir("bunfig-long-global", PACKAGE_JSON); const configHome = pathOfLength(String(dir), configHomeLength); - expect(await pmCache(String(dir), { XDG_CONFIG_HOME: configHome })).toEqual({ - stdout: CACHE_DIR_MARKER, - stderr: "", - exitCode: 0, - }); + expect(await pmCache(String(dir), { XDG_CONFIG_HOME: configHome })).toEqual(printedCacheDir(String(dir))); }); test("is skipped when its $HOME path would be exactly MAX_PATH_BYTES bytes", async () => { using dir = tempDir("bunfig-long-global", PACKAGE_JSON); const home = pathOfLength(String(dir), MAX_PATH_BYTES - "/.bunfig.toml".length); - expect(await pmCache(String(dir), { HOME: home, XDG_CONFIG_HOME: undefined })).toEqual({ - stdout: CACHE_DIR_MARKER, - stderr: "", - exitCode: 0, - }); + expect(await pmCache(String(dir), { HOME: home, XDG_CONFIG_HOME: undefined })).toEqual( + printedCacheDir(String(dir)), + ); }); }); }); From e79c0f84da4e570688097e884f48673add626aa4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:52:07 +0000 Subject: [PATCH 3/5] bunfig: trim the new doc comments --- src/bunfig/arguments.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bunfig/arguments.rs b/src/bunfig/arguments.rs index c6076d8ae17f..dc83e304221a 100644 --- a/src/bunfig/arguments.rs +++ b/src/bunfig/arguments.rs @@ -17,9 +17,7 @@ use crate::bunfig::Bunfig; // ─── bunfig loading ────────────────────────────────────────────────────────── -/// `dir/path`, NUL-terminated in `buf`. `dir` (cwd, `$HOME`, ...) and `path` -/// (argv) are unbounded, so the result may not fit; `None` then. No file can be -/// opened at such a path, so callers treat it like any other unreadable config. +/// `None` when `dir/path` does not fit: nothing could be opened at such a path anyway. fn join_config_path<'buf>( dir: &[u8], path: &[u8], @@ -43,8 +41,6 @@ fn get_home_config_path(buf: &mut PathBuffer) -> Option<&ZStr> { join_config_path(dir, b".bunfig.toml", buf) } -/// An auto-loaded bunfig that cannot be read is treated as absent; one the -/// user asked for is fatal. fn unreadable_config( auto_loaded: bool, err: &bun_sys::Error, From e90fac22dcc0a4e45edf4df7562de30bbc5f28a0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:57:59 +0000 Subject: [PATCH 4/5] test: measure the config path boundaries in UTF-8 bytes The path buffer holds bytes, so a temporary directory with non-ASCII characters shifted every boundary case when lengths were measured in UTF-16 code units. --- test/config/bunfig/bunfig-errors.test.ts | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/test/config/bunfig/bunfig-errors.test.ts b/test/config/bunfig/bunfig-errors.test.ts index 5f2a52b0e4e3..0b28d5740ccf 100644 --- a/test/config/bunfig/bunfig-errors.test.ts +++ b/test/config/bunfig/bunfig-errors.test.ts @@ -47,13 +47,13 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu const INVALID_BUNFIG = "[install\n"; const SEGMENT = Buffer.alloc(200, "d").toString(); - /** An absolute path below `root` that is exactly `length` bytes long (ASCII only). */ + /** An absolute path below `root` whose UTF-8 encoding is exactly `length` bytes long. */ function pathOfLength(root: string, length: number): string { let path = root; // Leave room for the final component, which has to stay under NAME_MAX (255). - while (length - path.length > 256) path = join(path, SEGMENT); - path = join(path, Buffer.alloc(length - path.length - 1, "L").toString()); - expect(path).toHaveLength(length); + while (length - Buffer.byteLength(path) > 256) path = join(path, SEGMENT); + path = join(path, Buffer.alloc(length - Buffer.byteLength(path) - 1, "L").toString()); + expect(Buffer.byteLength(path)).toBe(length); return path; } @@ -93,31 +93,31 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu } describe("bunfig.toml auto-loaded from the working directory", () => { - const PRINT_CWD_LENGTH = "console.log(process.cwd().length)"; + const PRINT_CWD_BYTES = "console.log(Buffer.byteLength(process.cwd()))"; test("is loaded when its path is exactly MAX_PATH_BYTES - 1 bytes", async () => { using dir = tempDir("bunfig-long-cwd", {}); const cwd = pathOfLength(String(dir), MAX_PATH_BYTES - "/bunfig.toml".length - 1); const config = invalidConfigAt(join(cwd, "bunfig.toml")); - expect(config).toHaveLength(MAX_PATH_BYTES - 1); + expect(Buffer.byteLength(config)).toBe(MAX_PATH_BYTES - 1); - expectLoadedFrom(await runBun(["-e", PRINT_CWD_LENGTH], cwd), config); + expectLoadedFrom(await runBun(["-e", PRINT_CWD_BYTES], cwd), config); }); // No bunfig.toml exists in these directories: its path would be too long to // create, and the path is built (and used to overflow) before it is opened. - const skippedCases: [configPathWouldBe: string, cwdLength: number][] = [ + const skippedCases: [configPathWouldBe: string, cwdBytes: number][] = [ ["exactly MAX_PATH_BYTES bytes, leaving no room for the NUL", MAX_PATH_BYTES - "/bunfig.toml".length], ["longer than the buffer", MAX_PATH_BYTES - 1], ]; - test.each(skippedCases)("bun -e still runs when the bunfig.toml path would be %s", async (_, cwdLength) => { + test.each(skippedCases)("bun -e still runs when the bunfig.toml path would be %s", async (_, cwdBytes) => { using dir = tempDir("bunfig-long-cwd", {}); - const cwd = pathOfLength(String(dir), cwdLength); + const cwd = pathOfLength(String(dir), cwdBytes); mkdirSync(cwd, { recursive: true }); - expect(await runBun(["-e", PRINT_CWD_LENGTH], cwd)).toEqual({ - stdout: `${cwdLength}\n`, + expect(await runBun(["-e", PRINT_CWD_BYTES], cwd)).toEqual({ + stdout: `${cwdBytes}\n`, stderr: "", exitCode: 0, }); @@ -125,13 +125,13 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu test("bun still runs when the bunfig.toml path does not fit", async () => { using dir = tempDir("bunfig-long-cwd", {}); - const cwdLength = MAX_PATH_BYTES - "/bunfig.toml".length; - const cwd = pathOfLength(String(dir), cwdLength); + const cwdBytes = MAX_PATH_BYTES - "/bunfig.toml".length; + const cwd = pathOfLength(String(dir), cwdBytes); mkdirSync(cwd, { recursive: true }); - writeFileSync(join(cwd, "x.cjs"), PRINT_CWD_LENGTH); + writeFileSync(join(cwd, "x.cjs"), PRINT_CWD_BYTES); expect(await runBun(["x.cjs"], cwd)).toEqual({ - stdout: `${cwdLength}\n`, + stdout: `${cwdBytes}\n`, stderr: "", exitCode: 0, }); @@ -211,7 +211,7 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu using dir = tempDir("bunfig-long-global", PACKAGE_JSON); const configHome = pathOfLength(String(dir), MAX_PATH_BYTES - "/.bunfig.toml".length - 1); const config = invalidConfigAt(join(configHome, ".bunfig.toml")); - expect(config).toHaveLength(MAX_PATH_BYTES - 1); + expect(Buffer.byteLength(config)).toBe(MAX_PATH_BYTES - 1); expectLoadedFrom(await pmCache(String(dir), { XDG_CONFIG_HOME: configHome }), config); }); From 1a40f6da85eb0757e7ab55c80e433fb4d4f37c72 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:25:21 +0000 Subject: [PATCH 5/5] test: build the repeated path segments with Buffer.alloc --- test/config/bunfig/bunfig-errors.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/config/bunfig/bunfig-errors.test.ts b/test/config/bunfig/bunfig-errors.test.ts index 0b28d5740ccf..dc3a764c4189 100644 --- a/test/config/bunfig/bunfig-errors.test.ts +++ b/test/config/bunfig/bunfig-errors.test.ts @@ -162,7 +162,9 @@ describe.concurrent.skipIf(isWindows)("config paths that do not fit in a path bu test("is loaded when a path longer than the buffer normalizes to one that fits", async () => { using dir = tempDir("bunfig-long-config", { "bunfig.toml": INVALID_BUNFIG }); - const configArg = "x/../".repeat(Math.ceil(MAX_PATH_BYTES / "x/../".length)) + "bunfig.toml"; + const hop = "x/../"; + const hops = Buffer.alloc(Math.ceil(MAX_PATH_BYTES / hop.length) * hop.length, hop).toString(); + const configArg = hops + "bunfig.toml"; expect(configArg.length).toBeGreaterThan(MAX_PATH_BYTES); expectLoadedFrom(