jdx/mise · error · eyre::Report
malformed fat header in {}
Error message
malformed fat header in {} What it means
A universal (fat) Mach-O begins with a big-endian 0xcafebabe header followed by nfat_arch entries of 20 bytes each. If the file ends before all fat-arch entries can be read, the header is truncated and the file is rejected before any slice is patched.
Source
Thrown at src/system/packages/brew/macho.rs:186
Ok(true)
}
/// Patch load-command path strings in a Mach-O file (thin or fat).
/// Returns whether anything changed.
pub fn patch(content: &mut [u8], replacements: &[Replacement], path: &Path) -> Result<bool> {
if content.len() < 8 {
return Ok(false);
}
let be_magic = u32::from_be_bytes(content[..4].try_into().unwrap());
if be_magic == FAT_MAGIC_BE {
let nfat = u32::from_be_bytes(content[4..8].try_into().unwrap()) as usize;
let mut changed = false;
// collect slice ranges first (fat headers are big-endian)
let mut ranges = vec![];
for i in 0..nfat {
let entry = 8 + i * 20;
if entry + 20 > content.len() {
bail!("malformed fat header in {}", path.display());
}
let offset =
u32::from_be_bytes(content[entry + 8..entry + 12].try_into().unwrap()) as usize;
let size =
u32::from_be_bytes(content[entry + 12..entry + 16].try_into().unwrap()) as usize;
if offset + size > content.len() {
bail!("malformed fat arch in {}", path.display());
}
ranges.push(offset..offset + size);
}
for range in ranges {
changed |= patch_slice(&mut content[range], replacements, path)?;
}
Ok(changed)
} else {
patch_slice(content, replacements, path)
}
}View on GitHub (pinned to 9dcfcaa0dc)
Solutions
- Clear the bottle cache and re-download, verifying the checksum
- Run lipo -info / file on the artifact to confirm whether it is a valid universal binary
- Report upstream if the file validates elsewhere
Defensive patterns
Strategy: validation
Validate before calling
// Fat header gate: file must contain the full nfat_arch entry array.
fn fat_header_ok(content: &[u8]) -> bool {
content.len() >= 8
&& u32::from_be_bytes(content[..4].try_into().unwrap()) == 0xcafebabe
&& {
let nfat = u32::from_be_bytes(content[4..8].try_into().unwrap()) as usize;
8 + nfat * 20 <= content.len()
}
} Type guard
fn is_patchable_fat_macho(content: &[u8]) -> bool {
fat_header_ok(content)
&& (0..u32::from_be_bytes(content[4..8].try_into().unwrap()) as usize).all(|i| {
let e = 8 + i * 20;
let off = u32::from_be_bytes(content[e + 8..e + 12].try_into().unwrap()) as usize;
let size = u32::from_be_bytes(content[e + 12..e + 16].try_into().unwrap()) as usize;
off + size <= content.len()
})
} Try / catch
if let Err(e) = patch_macho(&mut content, &replacements, &path) {
warn!("{}: malformed universal binary, not relocated: {e:#}", path.display());
} Prevention
- Verify bottle checksums before extraction
- Remember 0xcafebabe is also the Java class magic — don't feed jars/classfiles to Mach-O tooling
When it happens
Trigger: patch_macho fat path: for some i in 0..nfat, entry = 8 + i*20 satisfies entry + 20 > content.len(). Caused by truncated downloads, corrupted caches, or a file whose nfat_arch field is garbage (random bytes after the magic).
Common situations: Interrupted bottle downloads; disk or cache corruption; non-Mach-O files that coincidentally begin with 0xcafebabe (e.g. Java class files use the same magic).
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed fat arch in {}
- malformed Mach-O in {}
- malformed load command table in {}
- malformed load command in {}
- cannot relocate {}: not enough padding to grow load commands
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/204f010986dbfeb3.
Report an issue: GitHub.