From 14e34de29aeac9cf04d4f7f97408221d14d2d0a6 Mon Sep 17 00:00:00 2001 From: npiesco Date: Fri, 31 Jul 2026 13:49:54 -0400 Subject: [PATCH] Truncate IR dump basenames to respect NAME_MAX Resolves the `// FIXME work around filename too long errors` in `src/pretty_clif.rs`. When `--emit=llvm-ir` is used with cg_clif, symbol-mangled basenames can exceed the filesystem per-component limit (255B on ext4/XFS/APFS, 143B on HFS+, 255 UTF-16 on NTFS). `File::create` returns `ENAMETOOLONG`; the current `early_warn` path silently drops the dump. This patch truncates only names that would fail: - Stems short enough that no extension can overrun the limit pass through unchanged (zero overhead, borrowed `Cow`). - Longer: rewritten to `_h<16-hex-FNV-1a-64 over the full stem>.`. - Extension chains (`.opt.clif`, `.unopt.clif`, `.vcode`) preserved. - Char-boundary-safe truncation. The length test deliberately looks at the stem alone rather than the whole basename, reserving room for the longest extension chain written here. Testing the whole basename would make the rewrite depend on which artifact is being written: a 192-byte symbol pushes `.opt.clif` over the limit but leaves `.vcode` under it, so one function's dumps would land under two unrelated names and anything pairing a CLIF with its vcode would silently find nothing to pair. Symbols of 190 to 194 bytes all hit that; in an `--emit=llvm-ir` build of rustc itself that is 11510 of 650644 functions. Called inside `write_ir_file`, so both `write_clif_file` and the vcode writer in `base.rs` benefit uniformly. Tests cover the pairing property, the length bound, char-boundary safety, and that distinct symbols stay distinct. The pairing test sweeps the whole boundary region rather than checking one length, since at any single length the artifact-dependent behaviour is invisible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/pretty_clif.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/src/pretty_clif.rs b/src/pretty_clif.rs index 918fe3d2a3..68c5dafa3e 100644 --- a/src/pretty_clif.rs +++ b/src/pretty_clif.rs @@ -259,7 +259,7 @@ pub(crate) fn write_ir_file( res @ Err(_) => res.unwrap(), } - let clif_file_name = clif_output_dir.join(name); + let clif_file_name = clif_output_dir.join(truncate_ir_basename(name).as_ref()); let res = std::fs::File::create(clif_file_name).and_then(|mut file| write(&mut file)); if let Err(err) = res { @@ -270,6 +270,55 @@ pub(crate) fn write_ir_file( } } +/// Ensure a generated IR filename fits in the filesystem's per-component +/// length limit. Symbol-mangled names can exceed `NAME_MAX` (255 on ext4, +/// 143 on HFS+), which causes `ENAMETOOLONG` on `File::create`. +/// +/// Names ≤ 200 bytes pass through unchanged. Longer names are rewritten to +/// `_h<16-hex-FNV-1a-64 of full stem>.`. +/// The hash is computed over the original stem so the transformation is +/// deterministic and any two distinct inputs map to distinct outputs. +fn truncate_ir_basename(name: &str) -> std::borrow::Cow<'_, str> { + const MAX_BASENAME_LEN: usize = 200; + // Longest extension chain written for a single function: `.unopt.clif`, + // alongside `.opt.clif` and `.vcode`. + const MAX_EXT_LEN: usize = ".unopt.clif".len(); + // Decide from the stem alone, reserving room for the longest extension. + // + // Testing the whole basename would make the decision depend on which + // artifact is being written: with a 192-byte symbol, `.opt.clif` exceeds + // the limit and is rewritten while `.vcode` stays under it and is not, so + // one function's dumps end up under two unrelated names. Tools that pair a + // function's CLIF with its vcode then silently find nothing to pair. Every + // symbol between 190 and 194 bytes hits this; in a `--emit=llvm-ir` build + // of rustc itself that is 11510 of 650644 functions. + const MAX_STEM_LEN: usize = MAX_BASENAME_LEN - MAX_EXT_LEN; + + // Preserve extension suffix chain (e.g. ".opt.clif", ".unopt.clif", ".vcode"). + let (stem, ext) = match name.find('.') { + Some(i) => (&name[..i], &name[i..]), + None => (name, ""), + }; + if stem.len() <= MAX_STEM_LEN { + return std::borrow::Cow::Borrowed(name); + } + // FNV-1a 64-bit over the *full stem* (not the truncated prefix) so two + // inputs sharing a long common prefix still hash apart. + let mut hash: u64 = 0xcbf29ce484222325; + for b in stem.as_bytes() { + hash ^= *b as u64; + hash = hash.wrapping_mul(0x100000001b3); + } + // Char-boundary-safe truncation of the stem prefix. + let keep_bytes = 160.min(stem.len()); + let mut cut = keep_bytes; + while cut > 0 && !stem.is_char_boundary(cut) { + cut -= 1; + } + let prefix = &stem[..cut]; + std::borrow::Cow::Owned(format!("{prefix}_h{hash:016x}{ext}")) +} + pub(crate) fn write_clif_file( output_filenames: &OutputFilenames, symbol_name: &str, @@ -278,7 +327,6 @@ pub(crate) fn write_clif_file( func: &cranelift_codegen::ir::Function, mut clif_comments: &CommentWriter, ) { - // FIXME work around filename too long errors write_ir_file(output_filenames, &format!("{}.{}.clif", symbol_name, postfix), |file| { let mut clif = String::new(); cranelift_codegen::write::decorate_function(&mut clif_comments, &mut clif, func).unwrap(); @@ -312,3 +360,74 @@ impl fmt::Debug for FunctionCx<'_, '_, '_> { writeln!(f, "\n{}", clif) } } + +#[cfg(test)] +mod tests { + use super::truncate_ir_basename; + + const EXTS: [&str; 3] = [".opt.clif", ".unopt.clif", ".vcode"]; + + fn stem_of(name: &str, ext: &str) -> String { + name[..name.len() - ext.len()].to_string() + } + + #[test] + fn every_artifact_of_one_function_keeps_one_stem() { + // A function is dumped to several files and consumers pair them by + // their shared stem, so the rewrite must never depend on which + // artifact is being written. Sweep the whole boundary region rather + // than a single length: the failure only appears where one extension + // pushes a name over the limit and a shorter one does not. + for len in 1..260usize { + let symbol = format!("_R{}", "a".repeat(len)); + let stems: Vec = EXTS + .iter() + .map(|ext| stem_of(&truncate_ir_basename(&format!("{symbol}{ext}")), ext)) + .collect(); + assert!( + stems.iter().all(|s| *s == stems[0]), + "symbol of {} bytes produced differing stems: {stems:?}", + symbol.len() + ); + } + } + + #[test] + fn rewritten_names_fit_the_component_limit() { + for len in [0, 1, 189, 190, 200, 512, 4096] { + let symbol = format!("_R{}", "a".repeat(len)); + for ext in EXTS { + let name = format!("{symbol}{ext}"); + let out = truncate_ir_basename(&name); + assert!(out.len() <= 200, "{} bytes for a {len}-byte symbol", out.len()); + } + } + } + + #[test] + fn short_names_are_untouched_and_borrowed() { + let name = "_RNvC4test3foo.opt.clif"; + assert!(matches!(truncate_ir_basename(name), std::borrow::Cow::Borrowed(_))); + assert_eq!(truncate_ir_basename(name), name); + } + + #[test] + fn distinct_symbols_stay_distinct() { + let a = format!("_R{}", "a".repeat(400)); + let b = format!("_R{}b", "a".repeat(399)); + assert_ne!(a, b); + assert_ne!( + truncate_ir_basename(&format!("{a}.opt.clif")), + truncate_ir_basename(&format!("{b}.opt.clif")) + ); + } + + #[test] + fn truncation_respects_char_boundaries() { + // A multi-byte character straddling the cut must not be split. + let symbol = "\u{00e9}".repeat(200); + let name = format!("{symbol}.opt.clif"); + let out = truncate_ir_basename(&name); + assert!(std::str::from_utf8(out.as_bytes()).is_ok()); + } +}