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

  1. Clear the bottle cache and re-download, verifying the checksum
  2. Run lipo -info / file on the artifact to confirm whether it is a valid universal binary
  3. 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

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

Related errors


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/204f010986dbfeb3. Report an issue: GitHub.