jdx/mise · error · eyre::Report

malformed Mach-O in {}

Error message

malformed Mach-O in {}

What it means

The Mach-O relocation pass found a 64-bit LE header whose sizeofcmds field declares a load-command table larger than the file slice itself. The file is truncated or corrupt, and patching is refused before any offsets are trusted.

Source

Thrown at src/system/packages/brew/macho.rs:74

            .position(|w| w == r.placeholder)
        {
            out.splice(pos..pos + r.placeholder.len(), r.value.iter().cloned());
        }
    }
    out
}

/// Patch one 64-bit LE Mach-O slice in place. Returns whether it changed.
fn patch_slice(slice: &mut [u8], replacements: &[Replacement], path: &Path) -> Result<bool> {
    if slice.len() < HEADER_SIZE_64 || u32_at(slice, 0) != MH_MAGIC_64_LE {
        // not a 64-bit LE Mach-O (32-bit or big-endian) — nothing modern on
        // arm64 macOS; leave it to the caller's generic byte-level pass
        return Ok(false);
    }
    let ncmds = u32_at(slice, 16) as usize;
    let sizeofcmds = u32_at(slice, 20) as usize;
    if HEADER_SIZE_64 + sizeofcmds > slice.len() {
        bail!("malformed Mach-O in {}", path.display());
    }

    // upper bound for growing the load-command table: the first byte of
    // section data (everything between sizeofcmds and there is padding)
    let lc_end = HEADER_SIZE_64 + sizeofcmds;
    let mut first_data = slice.len();
    {
        let mut off = HEADER_SIZE_64;
        for _ in 0..ncmds {
            if off + 8 > lc_end {
                bail!("malformed load command table in {}", path.display());
            }
            let cmd = u32_at(slice, off);
            let cmdsize = u32_at(slice, off + 4) as usize;
            if cmdsize < 8 || off + cmdsize > lc_end {
                bail!("malformed load command in {}", path.display());
            }
            if cmd == LC_SEGMENT_64 {

View on GitHub (pinned to 9dcfcaa0dc)

Solutions

  1. Delete the cached bottle and re-pour, verifying checksums
  2. Run otool -hv / file on the artifact — tools that agree it is malformed confirm corruption rather than a parser bug
  3. If standard tools parse it fine, report the file upstream with its header bytes
Defensive patterns

Strategy: validation

Validate before calling

// Header sanity gate before Mach-O patching.
fn macho_header_ok(slice: &[u8]) -> bool {
    slice.len() >= 32
        && u32::from_le_bytes(slice[0..4].try_into().unwrap()) == 0xfeedfacf
        && 32 + u32::from_le_bytes(slice[20..24].try_into().unwrap()) as usize <= slice.len()
}

Type guard

fn is_patchable_macho64(slice: &[u8]) -> bool {
    slice.len() >= 32
        && u32::from_le_bytes(slice[0..4].try_into().unwrap()) == 0xfeedfacf
        && (32usize + u32_at(slice, 20) as usize) <= slice.len()
}

Try / catch

// Per-artifact: propagate with the file path attached so the failing bottle
// is identifiable in the pour log.
patch_macho(&mut content, &replacements, &path)
    .wrap_err_with(|| format!("relocating {}", path.display()))?;

Prevention

When it happens

Trigger: patch_slice (via patch_macho on a bottle artifact): slice.len() >= 32 and magic 0xfeedfacf, but HEADER_SIZE_64 + sizeofcmds > slice.len(). Typical for a half-downloaded bottle, a fat-binary slice clipped by truncation, or a non-Mach-O file that happens to start with the magic.

Common situations: Interrupted bottle download leaving truncated binaries; corrupted cache; slicing bugs in fat-binary handling of unusual files.

Understand the failure class

Related errors


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