rambo 0.1.1

A tool to map ROM collateral damage
Documentation

rambo

CI Release License: MIT Rust 2024

ROM Area Memory Behavior Observer — a CLI for mapping the SRAM collateral damage a Cortex-M boot ROM leaves behind.

rambo connects to a target MCU over SWD using probe-rs, primes its on-chip SRAM with a known pattern, issues a reset, halts the core before it executes any user firmware, and then classifies every block of RAM to show you exactly what the boot ROM clobbered, what it left alone, and what looks suspiciously like leftover data structures.

It is intentionally not a TUI — output is plain stdout with ANSI colors, designed to be readable in a terminal, piped to a file, or pasted into a bug report.

Why?

When you write low-level firmware (bootloaders, secure-boot stages, RTOS ports), it matters which parts of RAM are safe to use at startup. Vendor reference manuals may not document which scratch areas the on-chip ROM uses, and the layout often differs between silicon revisions. rambo answers the question empirically: "if I want my firmware's first instruction to find untouched memory, which addresses can I trust?"

How it works

For each RAM region reported by probe-rs's chip database:

  1. Write a deterministic pattern (addr-as-data: each 32-bit word stores its own address) across the entire region.
  2. Reset the target and halt it immediately on the vector catch, before any user code runs.
  3. Read back the region and classify each block:
    • SAFE — pattern survived intact (ROM did not touch this block).
    • ZERO — block was zeroed.
    • ONES — block was filled with 0xFF.
    • CHANGED — block was rewritten with something else (likely ROM scratch / stack / data structures).
  4. Render a heatmap, a run-length summary, and per-region totals.

Optional modes layer extra analyses on top of this core loop:

  • Fingerprint changed blocks (--fingerprint) — detect constants, dominant values, ascending counters, repeating motifs, address-with-offset patterns, or noise.
  • Dual-pattern sweep (--dual-pattern) — write addr then !addr in two separate reset cycles to distinguish undriven RAM from actively modified RAM.
  • Write-readback (--write-readback) — no reset between write and read; detects aliasing and unmapped windows in reserved address ranges.
  • Stability (--reset-cycles N) — re-read after each of N resets to see whether post-ROM state is deterministic or drifts.

Installation

From source

cargo install --path .

Pre-built binaries

Pre-built binaries for Linux, macOS (Intel + Apple Silicon), and Windows are attached to each tagged release on the Releases page.

System dependencies

probe-rs needs libudev on Linux:

sudo apt-get install -y libudev-dev pkg-config   # Debian/Ubuntu
sudo dnf install -y systemd-devel pkgconf-pkg-config  # Fedora

Quick start

Survey all RAM regions of an MCXA276 with default settings (4 KiB blocks, single reset cycle):

rambo --chip MCXA276

Use a smaller block size and fingerprint anything ROM modified:

rambo --chip MCXA276 --block 0x200 --fingerprint

Decide whether a region is driven or undriven via the dual-pattern sweep:

rambo --chip MCXA276 --dual-pattern

Detect aliasing or unmapped windows in a reserved RAM range (no reset between write and read):

rambo --chip MCXA276 --write-readback

Check determinism across five resets:

rambo --chip MCXA276 --reset-cycles 5

Select a specific debug probe by VID:PID or serial:

rambo --chip MCXA276 --probe 0483:374b

CLI reference

Flag Default Description
--chip <NAME> Required. probe-rs target identifier (e.g. MCXA276).
--block <BYTES> 0x1000 Classification block size. Decimal / 0x / 0o / 0b / _ accepted. Must be a non-zero multiple of 4.
--reset-cycles <N> 1 Number of read-after-reset iterations. N>1 enables stability analysis. Incompatible with --dual-pattern.
--fingerprint off Fingerprint each CHANGED block (bit density, top values, pattern matches).
--dual-pattern off Two-pass addr / !addr sweep to distinguish undriven from modified RAM.
--write-readback off Write then immediately read back (no reset). Reveals aliasing / unmapped windows.
--probe <SEL> first Probe selector, VID:PID or serial.
--json <PATH> off Write a machine-readable JSON report to <PATH> (use - for stdout, which suppresses normal text output).
--expectations <PATH> off Load a "RAM contract" JSON file and evaluate each expectation against the survey. Non-zero exit on failure.

Run rambo --help for the full clap-generated help text.

Classification legend

Class Glyph Meaning
SAFE █ green Pattern survived. ROM did not touch this block.
ZERO █ blue Block was cleared to 0x00000000.
ONES █ magenta Block was filled with 0xFFFFFFFF.
CHANGED █ red Block was rewritten with something else (ROM working data).

The heatmap is laid out as 64 cells per row, where each cell aggregates one block (1 KiB for the survey heatmap, --block bytes elsewhere). Below the heatmap, rambo prints a run-length-encoded list of contiguous regions sharing a class, followed by totals.

Output anatomy

A typical survey output for a single region looks like:

═══ RAM @ 0x20000000 .. 0x2003c000 (240 KiB) ═══

[heatmap, 64 cells per row]

Runs
┌────────────────────────┬─────────┬──────────┐
│ Range                  │ Size    │ Class    │
├────────────────────────┼─────────┼──────────┤
│ 0x20000000..0x20001000 │   4 KiB │ CHANGED  │
│ 0x20001000..0x2003c000 │ 236 KiB │ SAFE     │
└────────────────────────┴─────────┴──────────┘

Totals
  SAFE:    236 KiB
  CHANGED:   4 KiB

CI gate: JSON output and RAM contracts

rambo can emit a stable JSON report (--json) and evaluate a JSON RAM contract (--expectations) against the survey. Together they turn rambo from a one-off diagnostic into a regression gate that catches when a new chip rev, silicon lot, or ROM patch clobbers memory your firmware relies on.

rambo --chip MCXA276 \
      --json report.json \
      --expectations contract.json

Exit code is 0 if every expectation passes, 1 if any fails. Both flags are independent — use --json alone to archive a CI artifact, or --expectations alone for a pass/fail check.

A contract file declares per-range expectations; see examples/expectations.json for a worked example. Each expectation specifies exactly one clause:

Clause Meaning
expect: "safe" Every block in the range must classify as that class.
expect_any_of: ["safe", "zero"] Every block must classify as one of these.
expect_not: "changed" No block in the range may classify as that class.

Ranges must be block-aligned (--block) and fit entirely inside one of the chip's RAM regions; misalignment or out-of-region addresses are rejected before any probe I/O happens, so a typo can never brick a run.

JSON output is wire-stable: schema_version: 1 will keep its current shape, and breaking changes will bump the schema version alongside a release.

Hardware notes

rambo has been smoke-tested against an NXP MCXA276 development board with the on-board MCU-Link probe. Any Cortex-M chip supported by probe-rs should work, but only chips whose target description in probe-rs exposes RAM regions in its memory_map will be useful (which is essentially all of them).

⚠️ The CMSIS-Pack chip descriptions that probe-rs consumes occasionally list code-bus aliases of the system-bus RAM as separate regions. rambo treats every region in the map as independent — if you see the same physical RAM surveyed twice under different addresses, that is why.

Development

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

CI runs the same three checks on every push and PR (Ubuntu/macOS/Windows for tests, Ubuntu for lints). The CI badges at the top of this README reflect the current status of main.

Commit convention

Commits to main MUST follow Conventional Commits. release-plz parses them to compute the next version bump and to generate CHANGELOG.md. Non-conforming commits default to a patch bump and won't appear in the changelog cleanly.

Bump rules differ between 0.x and 1.x (per the next_version crate that release-plz uses):

Commit 0.x bump ≥1.0 bump
fix: / perf: / feat: patch patch / patch / minor
feat!: or BREAKING CHANGE: footer minor major
non-conventional patch patch

On 0.x the minor digit is the breaking axis (per Cargo's semver rules), so feat: deliberately stays on the patch line until you bump to 1.0.0 manually.

Releases

Releases are automated via release-plz:

  1. Merging conventional commits to main causes release-plz to open (or update) a "chore: release" PR that bumps Cargo.toml and updates CHANGELOG.md.
  2. Merging that PR tags vX.Y.Z, creates a GitHub Release, and publishes to crates.io.
  3. The tag also triggers release.yml, which builds per-OS binary archives and uploads them onto the same GitHub Release.

License

MIT © Felipe Balbi