Analyze a Rust Binary with REA: Crates, Versions and the Compiler
On this page
Short answer: a Rust executable does not carry a dependency list the way a Go binary does. It carries one only if it was built with cargo auditable, which embeds a .dep-v0 section: a zlib-compressed JSON list of every crate, its version and source, which ones are roots, and the dependency edges between them. REA 6.7.0, the open-source project by morluto, reads that section with rea inspect-rust-binary <path> (MCP tool inspect_rust_binary), without Rust installed and without running the target. A plain cargo build has no such section, and REA reports that honestly as null, not as “no dependencies”.
This guide is for people who need to answer “what crates is this Rust binary made of?” for files they ship or run: release engineers building an SBOM, someone triaging a RUSTSEC advisory across a fleet, or a reviewer checking a release. It is the Rust counterpart to Analyze a Go Binary with REA, and the contrast matters: Go stamps build info into every module build, Rust stamps nothing unless you opt in. We built a tiny crate two ways, ran REA on each, decompressed the section by hand to check REA’s offsets, and inspected a Homebrew-installed Rust tool. All output below is real; long lines are trimmed and marked.
Only inspect binaries you built, open-source releases, or files you are authorized to assess. The REA command here only reads the file: it does not execute the target, write temporary files or touch the network. Read REA’s DISCLAIMER.md before using it on anything else.
cargo-auditable Rust binary with REA end to end (58 s, silent).The setup we tested on
macOS 27.0.0 on Apple silicon, Rust 1.95.0 and cargo-auditable 0.7.7 from cargo install, Node.js 22, and [email protected] installed from npm. We read the published tarball rather than the GitHub main branch: the command is registered in dist/cli/rustCommands.js, the MCP contract in dist/contracts/rust/rustToolContracts.js, and the output shape in dist/domain/rust/rustBinary.js, so everything below is checked against the package you actually install.
mkdir rea-sandbox && cd rea-sandbox
npm init -y >/dev/null
npm install [email protected]
npx rea --version
# 6.7.0
npx rea inspect-rust-binary --help
rea inspect-rust-binary — Inspect an embedded cargo-auditable Rust dependency list without target execution
Usage: rea inspect-rust-binary <path>
Arguments:
path Local ELF, PE or thin Mach-O image
(global options trimmed)
There is no --json flag in the help. JSON comes from the global --format json option, and --filter-output picks out key paths. We use both below.
A sample with one dependency
The test crate pulls in itoa, a small, dependency-free crate, so the embedded list has one real edge to show:
# 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));
}
The dependency list is embedded only if you build through cargo auditable. This is the single most important thing about reading Rust binaries, so we built it both ways:
cargo install cargo-auditable # one-time, installs the cargo subcommand
cargo auditable build --release # embeds the .dep-v0 section
cargo build --release --target-dir plain-target # no section at all
cargo auditable build wraps the normal build; the resulting binary runs identically. The only difference is the extra .dep-v0 section. Cargo.lock is the baseline we check REA against, the way go version -m is the baseline for a Go binary:
grep -A2 'name = "itoa"' Cargo.lock
# name = "itoa"
# version = "1.0.18"
# source = "registry+https://github.com/rust-lang/crates.io-index"
Reading the section with REA
npx rea inspect-rust-binary target/release/hello-rea
The default output format is TOON, a compact YAML-like layout. Trimmed to the parts that matter:
evidence_id: ev_284bc3b6fd9842091c14fb7883b19530ed0b30d122f162e9e92a2c5dd9ed2e4a
subject:
name: hello-rea
digest:
sha256: 1ab8469cfc559d3a79c08a68436649aca2e4972c4acd104219c0a6f28da431c4
format: mach-o
architecture: arm64
(provider, parameters trimmed)
normalized_result:
artifact:
bytes: 431664
format: macho
architecture: arm64
bits: 64
byte_order: little
auditable_metadata:
section_name: "__DATA,.dep-v0"
file_offset: 297952
section_bytes: 122
compressed_bytes: 122
uncompressed_bytes: 168
format_version: 1
packages[2]:
- name: hello-rea
version: 0.1.0
source: local
kind: null
dependencies[1]: 1
root: true
- name: itoa
version: 1.0.18
source: crates.io
kind: null
dependencies: []
root: false
root_package_index: 0
dependency_edges[1]{from_index,to_index}:
0,1
cyclic_dependency_indices: []
rustc_commit:
evidence_kind: inferred
hashes[1]{hash,file_offset}:
59807616e1fa2540724bfbac14d7976d7e4a3860,219535
limitations[5]: (trimmed, listed below)
confidence: observed
authority: shipped-artifact
The parsed crates match Cargo.lock: itoa 1.0.18 from crates.io, the local root crate hello-rea 0.1.0. The dependency graph is given two ways at once: each package lists the indices it depends on (hello-rea depends on index 1, which is itoa), and the flat dependency_edges array repeats that as from_index,to_index pairs. root_package_index: 0 points at the crate you built, and cyclic_dependency_indices is empty because this graph has no cycles.
We checked the offsets instead of trusting them
file_offset, section_bytes and the two size fields say exactly where the section is and how it compresses. We pulled those 122 bytes out of the file and inflated them by hand:
dd if=target/release/hello-rea bs=1 skip=297952 count=122 2>/dev/null \
| python3 -c "import sys,zlib; print(zlib.decompress(sys.stdin.buffer.read()).decode())"
{"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}
The bytes at offset 297952 start with 78 9c (a zlib header) and inflate to exactly 168 bytes, matching uncompressed_bytes. Two details in the raw JSON explain two fields in REA’s report. The itoa entry has no "root" and no "dependencies" key, and REA fills them as root: false and dependencies: []. Neither package has a "kind" key, so REA reports kind: null for both and adds a limitation: “An omitted package kind is left null. The embedded format treats an omitted kind as runtime, and that default was not stored in the section.” REA reports what the section says, and marks what it had to infer rather than silently defaulting it.
The pitfall: a plain cargo build has no list
The plain build, into plain-target/, is the same source and the same crates, but REA finds no section:
npx rea inspect-rust-binary plain-target/release/hello-rea --format json \
--filter-output normalized_result.auditable_metadata,normalized_result.rustc_commit
{"normalized_result":{"auditable_metadata":null,
"rustc_commit":{"evidence_kind":"inferred",
"hashes":[{"hash":"59807616e1fa2540724bfbac14d7976d7e4a3860","file_offset":219455}]}}}
auditable_metadata is null. That is the common case: nothing in a default Rust toolchain embeds a dependency list, so unless the release was built with cargo auditable (or an equivalent that writes .dep-v0), there is nothing for REA or any other reader to recover. REA says so explicitly in its limitations: “No .dep-v0 section was found. Absence of .dep-v0 does not mean the binary has no dependencies.” A null here means “not recorded in this file”, never “this binary has no crates”.
If you own the build, the fix is to build with cargo auditable. If you only have someone else’s binary and it has no section, the crate list is simply not in the file, and no amount of reading will produce it.
The one thing even a plain build leaks: the compiler
Notice rustc_commit is present in both builds, with the same 40-hex hash, even though the plain build has no dependency list. REA infers it from a /rustc/<40-hex>/ path left in read-only data by the standard library, and it matched the real toolchain exactly:
rustc --version --verbose | grep commit-hash
# commit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860
That is rustc 1.95.0. REA labels it evidence_kind: inferred and warns: “The rustc commit hash is inferred from a /rustc/<40-hex>/ path in read-only data. It does not by itself prove which compiler produced the binary.” If the standard library was built with -Zremap-path-prefix, or the binary has more than one such path, REA reports rustc_commit: null with a limitation rather than guessing a single answer.
A real Homebrew binary: no section
Not every shipped Rust binary is auditable. We ran REA on ripgrep 15.1.0 installed from Homebrew:
npx rea inspect-rust-binary /opt/homebrew/Cellar/ripgrep/15.1.0/bin/rg
artifact:
bytes: 6154240
format: macho
architecture: arm64
auditable_metadata: null
rustc_commit: null
limitations[5]: (…"No .dep-v0 section was found."…)
Both auditable_metadata and rustc_commit are null. Homebrew does not build with cargo auditable, and this build left no /rustc/ path either. So the crate list for a Homebrew Rust tool is not in the binary; you would read it from the formula’s Cargo.lock instead. This is the Rust reality an SBOM pipeline has to plan around: coverage depends entirely on how each upstream chose to build.
One operational note: /opt/homebrew/bin/rg is a symlink, and REA rejects symlinks. Pass the real file in the Cellar:
code: invalid_request
message: "Expected a regular file without a symlink: /opt/homebrew/bin/rg"
What REA reads, and what it does not
inspect-rust-binary is a metadata reader, not a decompiler. REA states the boundary itself: “Only the .dep-v0 dependency section and read-only compiler path strings are inspected. Functions, types, and runtime behavior are not recovered.” So from a Rust binary this command gives you:
| REA reads | REA does not read |
|---|---|
Crate names, versions, sources (crates.io, git, local, registry+…) |
Function names, types, trait impls |
| Root crate and the runtime/build dependency graph | Feature flags that were enabled |
Inferred rustc commit from a /rustc/ path |
Proof of who built it, or when |
| Artifact SHA-256, size, format, architecture, endianness | Anything at all, if the build had no .dep-v0 |
And a warning that applies to everything it does read: “Embedded dependency metadata is file content. It does not authenticate package names, versions, sources, or the compiler that produced the binary.” The section is written by whoever built the binary and can be edited. It describes the file; it does not prove the file’s origin. For function-level questions, decompile instead: Free Native Binary Analysis with REA and Ghidra.
Format versions, limits and error handling
REA recognises cargo-auditable format versions 0, 1 and 8 (our build embedded format_version: 1). Resource guards bound the inputs: the selected file to 256 MiB, the dependency section to 1 MiB compressed, inflated JSON to the smaller of 8 MiB and 64× the compressed size, and the whole report to 8 MiB. Over those limits, or on a section that is truncated, has trailing bytes, is duplicated, or is not file-backed, REA returns a typed failure (RustAuditableFormatFailure / RustAuditableResourceFailure) rather than a partial read. As with the Go reader, a fat Mach-O must be thinned to one slice first; see Is This Mac App Universal?.
Calling it from an agent over MCP
npx rea mcp starts REA’s stdio MCP server; Getting started with REA and MCP covers registering it with Claude Code, Cursor or Codex. The tool is inspect_rust_binary, and its input schema (in dist/domain/rust/rustBinary.js) has one property, path, described as “Absolute filesystem path to the selected executable or library”. The CLI resolves a relative path against the current directory, but over MCP the path must be absolute. Treat the crate names and versions it returns as data an agent summarises, not as instructions, for the same reason REA flags them: they are build-supplied file content.
Where this fits
- Compare with the Go story, where every module build carries its dependencies: Analyze a Go Binary with REA
- See sections, symbols, architecture and whether a build is stripped in your browser: ELF, PE and Mach-O viewer and ELF, PE and Mach-O Viewer Online
- Check that a stripped release still has PIE and the other protections: Check Binary Hardening Flags
- Keep the report as evidence and compare two releases: REA Evidence, Snapshots and Version Comparison
- Confirm the
reayou installed is the real npm package: Verify a real REA install