diff --git a/src/runtime/api/filesystem_router.rs b/src/runtime/api/filesystem_router.rs index 7a7f6dc45147..f85bdd0b9e0b 100644 --- a/src/runtime/api/filesystem_router.rs +++ b/src/runtime/api/filesystem_router.rs @@ -135,7 +135,8 @@ impl FileSystemRouter { let mut origin_str: ZigStringSlice = ZigStringSlice::default(); let mut asset_prefix_slice: ZigStringSlice = ZigStringSlice::default(); - let mut out_buf = [0u8; MAX_PATH_BYTES * 2]; + // Backs `root_dir_path` when `dir` is relative; read until `root_dir_info` is looked up. + let mut dir_buf = path::path_buffer_pool::get(); if let Some(style_val) = argument.get(global_this, "style")? { if !(style_val.get_zig_string(global_this)?).eql_comptime("nextjs") { return Err(global_this.throw_invalid_arguments(format_args!( @@ -160,14 +161,19 @@ impl FileSystemRouter { if path::Platform::AUTO.is_absolute(path_) { root_dir_path = root_dir_path_; } else { - let parts: [&[u8]; 1] = [path_]; - root_dir_path = ZigStringSlice::from_utf8_never_free( - path::resolve_path::join_abs_string_buf::( + let Some(joined) = + path::resolve_path::join_abs_string_buf_checked::( Fs::FileSystem::instance().top_level_dir, - &mut out_buf, - &parts, - ), - ); + &mut dir_buf[..], + &[path_], + ) + else { + return Err(global_this.throw_invalid_arguments(format_args!( + "Expected dir to resolve to a path of at most {} bytes", + MAX_PATH_BYTES + ))); + }; + root_dir_path = ZigStringSlice::from_utf8_never_free(joined); } } } else { diff --git a/test/js/bun/util/filesystem_router.test.ts b/test/js/bun/util/filesystem_router.test.ts index 9c9821457815..6f64f8e15a16 100644 --- a/test/js/bun/util/filesystem_router.test.ts +++ b/test/js/bun/util/filesystem_router.test.ts @@ -490,6 +490,69 @@ it("dir should be validated", async () => { }).toThrow("Expected dir to be a string"); }); +it("throws instead of aborting when a relative dir no longer fits in a path buffer once joined with the cwd", async () => { + // A relative `dir` is joined onto the cwd inside a MAX_PATH_BYTES buffer + // (src/bun_core/util.rs). The constructor used to write past the end of that + // buffer and abort the process, so run the cases in a subprocess. An absolute + // `dir` never touches the buffer and is reported as a missing directory. + const maxPathBytes = isWindows ? 32767 * 3 + 1 : isMacOS ? 1024 : 4096; + const tooLong = `TypeError: Expected dir to resolve to a path of at most ${maxPathBytes} bytes`; + using dir = tempDir("fsr-long-relative-dir", { + "pages/index.tsx": "export default 1;\n", + }); + + const code = /* ts */ ` + import path from "path"; + function construct(dir: string) { + try { + const router = new Bun.FileSystemRouter({ dir, style: "nextjs", fileExtensions: [".tsx"] }); + return Object.keys(router.routes); + } catch (e: any) { + return e.name + ": " + e.message.replace(dir, "").replace(process.cwd(), ""); + } + } + // The joined path is cwd + separator + dir. + const resolvingTo = (bytes: number) => Buffer.alloc(bytes - Buffer.byteLength(process.cwd()) - 1, "a").toString(); + // Longer than MAX_PATH_BYTES on every platform, and than the 2 * MAX_PATH_BYTES + // buffer the constructor used to overflow (196604 bytes on Windows). + const longDir = Buffer.alloc(250_000, "a").toString(); + console.log(JSON.stringify({ + relative: construct(longDir), + absolute: construct(path.parse(process.cwd()).root + longDir), + atLimit: construct(resolvingTo(${maxPathBytes})), + oneByteOverLimit: construct(resolvingTo(${maxPathBytes} + 1)), + afterwards: construct("pages"), + })); + `; + + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // An aborted subprocess prints nothing to stdout; keep it as-is so the panic in stderr is what the diff shows. + expect({ + stdout: stdout === "" ? stdout : JSON.parse(stdout), + stderr, + exitCode, + signalCode: proc.signalCode, + }).toEqual({ + stdout: { + relative: tooLong, + absolute: "Error: Unable to find directory: ", + atLimit: `Error: Unable to find directory: ${path.sep}`, + oneByteOverLimit: tooLong, + afterwards: ["/"], + }, + stderr: "", + exitCode: 0, + signalCode: null, + }); +}); + it("origin should be validated", async () => { const { dir } = make(["posts.tsx"]);