From 07e7797a8be67111268a3f6d21b7966c1ab61a08 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Mon, 17 Aug 2026 14:18:17 +0200 Subject: [PATCH 1/2] test(fs): pin recursive mkdir options decoding --- .../issue_8122_fs_mkdir_options_object.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 crates/perry/tests/issue_8122_fs_mkdir_options_object.rs diff --git a/crates/perry/tests/issue_8122_fs_mkdir_options_object.rs b/crates/perry/tests/issue_8122_fs_mkdir_options_object.rs new file mode 100644 index 0000000000..c88cefd444 --- /dev/null +++ b/crates/perry/tests/issue_8122_fs_mkdir_options_object.rs @@ -0,0 +1,68 @@ +//! Regression test for the fs options-object crash fixed alongside #8122. +//! +//! `mkdirSync(path, { recursive: true })` passes an object where the runtime +//! also accepts a string mode. Before #8204, the generic string helper accepted +//! any plausible NaN-boxed pointer and read the options object's ShapeId as a +//! `StringHeader::byte_len`. ShapeIds have the high bit set, so the runtime +//! attempted a roughly 2 GiB string copy and crashed in `memcpy`. +//! +//! Claude Code exercises this exact call shape during normal startup, while its +//! `--version` fast path exits before touching the filesystem. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +const SOURCE: &str = r#" +import * as fs from "node:fs"; + +const root = ".perry-issue-8122-mkdir"; +fs.mkdirSync(root + "/a/b", { recursive: true }); +const ok = fs.statSync(root + "/a/b").isDirectory(); +console.log(ok ? "PASS" : "FAIL"); +fs.rmSync(root, { recursive: true, force: true }); +"#; + +fn compile(dir: &Path, entry: &Path, output: &Path) { + let compile = Command::new(perry_bin()) + .current_dir(dir) + .arg("compile") + .arg(entry) + .arg("-o") + .arg(output) + .arg("--no-cache") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); +} + +#[test] +fn recursive_mkdir_options_object_is_not_decoded_as_a_string() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, SOURCE).expect("write entry"); + + compile(dir.path(), &entry, &output); + + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + let stdout = String::from_utf8_lossy(&run.stdout); + assert!( + run.status.success() && stdout.contains("PASS"), + "recursive mkdir with an options object failed (status {:?})\nstdout:\n{}\nstderr:\n{}", + run.status, + stdout, + String::from_utf8_lossy(&run.stderr) + ); +} From 04237514718e059c6756860329a6bbd86ffc72f2 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Mon, 17 Aug 2026 14:19:41 +0200 Subject: [PATCH 2/2] chore: add PR 8309 changelog fragment --- changelog.d/8309-mkdir-options-object-regression.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8309-mkdir-options-object-regression.md diff --git a/changelog.d/8309-mkdir-options-object-regression.md b/changelog.d/8309-mkdir-options-object-regression.md new file mode 100644 index 0000000000..46f6e6c5a7 --- /dev/null +++ b/changelog.d/8309-mkdir-options-object-regression.md @@ -0,0 +1 @@ +Add an end-to-end regression test for recursive `fs.mkdirSync` calls that pass an options object.