rea-reverse

What Crates Are in a Rust Binary? Reading cargo-auditable Data

Written for REA 6.7.0Published by rea-reverseUpdated 3 min read
On this page

Only if it was built with cargo auditable. Unlike Go, a normal cargo build stores no dependency list in the executable. cargo auditable build adds a .dep-v0 section holding zlib-compressed JSON: every crate, its version and source, which one is the root, and the dependency edges. Dump that section and inflate it, and you have the crate list; if the section is absent, the list is not in the file at all.

We tested on macOS 27 (Apple silicon) with Rust 1.95.0 and cargo-auditable 0.7.7, on a tiny crate built for this article and on Homebrew’s ripgrep 15.1.0. All output below is real.

Watch: inspecting a cargo-auditable Rust binary with REA end to end (58 s, silent).
Analyze a Rust Binary with REA: Crates, Versions and the CompilerSilent walkthrough with English captions.

Build the same crate both ways

The test crate depends on itoa, which has no dependencies of its own:

# Cargo.toml
[package]
name = "hello-rea"
version = "0.1.0"
edition = "2021"

[dependencies]
itoa = "1.0"
// src/main.rs
fn main() {
    let mut buf = itoa::Buffer::new();
    println!("hello from rust {}", buf.format(42u32));
}
cargo install cargo-auditable                      # one-time
cargo auditable build --release                    # with .dep-v0
cargo build --release --target-dir plain-target    # without

The two binaries were 431,664 and 431,584 bytes; the auditable one runs identically and only carries the extra 122-byte section. Cargo.lock is the ground truth to check against:

grep -A2 'name = "itoa"' Cargo.lock
#	name = "itoa"
#	version = "1.0.18"
#	source = "registry+https://github.com/rust-lang/crates.io-index"

Extract the list with llvm-objcopy and zlib

On Mach-O the section is __DATA,.dep-v0; on ELF and PE it is .dep-v0. otool -l shows where it is:

$ otool -l target/release/hello-rea | grep -B1 -A5 'sectname .dep-v0'
Section
  sectname .dep-v0
   segname __DATA
      addr 0x0000000100048be0
      size 0x000000000000007a
    offset 297952

Dump it and inflate it:

llvm-objcopy --dump-section __DATA,.dep-v0=dep.z target/release/hello-rea /dev/null
python3 -c "import zlib,json;print(json.dumps(json.loads(zlib.decompress(open('dep.z','rb').read()))))"
{"packages": [{"name": "hello-rea", "version": "0.1.0", "source": "local", "dependencies": [1], "root": true}, {"name": "itoa", "version": "1.0.18", "source": "crates.io"}], "format": 1}

(For ELF, objcopy --dump-section .dep-v0=dep.z <binary> /dev/null does the same.) dependencies are indices into packages, so hello-rea depends on index 1, itoa. Keys that are omitted have defaults: itoa has no root (false), no dependencies (none), and neither package has kind, which the format treats as a runtime dependency. Build-only dependencies appear with "kind": "build". Feature flags are not recorded.

cargo audit bin <binary> from cargo-audit and SBOM tools such as syft also read this section; we did not test them here.

A plain cargo build has nothing to extract

$ llvm-objcopy --dump-section __DATA,.dep-v0=plain.z plain-target/release/hello-rea /dev/null
llvm-objcopy: error: 'plain-target/release/hello-rea': section '__DATA,.dep-v0' not found

Same source, same crates, no list. Nothing in the default toolchain embeds one, so for someone else’s binary without .dep-v0 the crate list is simply not recoverable from metadata. If you own the build, switch the release job to cargo auditable build.

The one thing a plain build still leaks is the compiler. The standard library leaves /rustc/<commit>/ paths in read-only data:

$ strings -a plain-target/release/hello-rea | grep -m1 -o '/rustc/[0-9a-f]\{40\}'
/rustc/59807616e1fa2540724bfbac14d7976d7e4a3860
$ rustc --version --verbose | grep commit-hash
commit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860

That hash is rustc 1.95.0. It is a hint, not proof: a standard library built with remapped source paths leaves no such path, and the ripgrep build below has none.

A real Homebrew binary: ripgrep has neither

otool -l /opt/homebrew/Cellar/ripgrep/15.1.0/bin/rg | grep -c dep-v0      # 0
strings -a /opt/homebrew/Cellar/ripgrep/15.1.0/bin/rg | grep -c /rustc/    # 0

The 6,154,240-byte rg has no .dep-v0 and no /rustc/ path. Homebrew does not build with cargo auditable, so the crate list for a Homebrew Rust tool has to come from the formula’s Cargo.lock, not the binary. That is what an SBOM pipeline for Rust has to plan around: coverage depends on how each upstream chose to build.

The same data as structured JSON

REA, a third-party open-source tool, wraps this in rea inspect-rust-binary <path> (MCP tool inspect_rust_binary). We ran [email protected] from npm on the same two builds and on ripgrep. For the auditable build it reported section_name: "__DATA,.dep-v0", file_offset: 297952, compressed_bytes: 122, uncompressed_bytes: 168, format_version: 1, both packages with itoa 1.0.18 from crates.io, an edge list 0,1, and a rustc_commit of 59807616e1fa… labelled evidence_kind: inferred. The bytes at that offset start with 78 9c and inflate to exactly 168 bytes, so the offsets are right. It reports an omitted kind as null rather than silently defaulting it.

For the plain build it returned auditable_metadata: null with the limitation “Absence of .dep-v0 does not mean the binary has no dependencies.” For ripgrep both auditable_metadata and rustc_commit were null. Two gotchas: /opt/homebrew/bin/rg is a symlink and REA rejects it (Expected a regular file without a symlink), so pass the Cellar path; and, as with its Go reader, a fat Mach-O must be thinned with lipo -thin first. Over MCP the path must be absolute.

Whatever reads it, the section is file content written by whoever built the binary and can be edited; it describes the file but does not authenticate it, and it recovers no functions or types. For code-level questions, decompile instead, as in native binary analysis with Ghidra. Go does this by default, compare finding a Go binary’s modules.