Running with Miri
SkillDocs & knowledgeRun the codebase under Miri with native FFI support to detect undefined behavior in Rust-C interop. Use this when asked to check for UB or verify memory safety.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Running with Miri skill
What this skill tells your AI
The instructions your AI receives, as published by davidhozic/mujoco-rs in .claude/skills/miri/SKILL.md and read by ahel’s review.
Run the codebase under Miri's experimental FFI native-lib support to detect undefined behavior in Rust code interoperating with MuJoCo C FFI.
-
Build MuJoCo with Miri support: Navigate to the MuJoCo source directory (e.g.,
mujoco/) and build with the Miri allocator hooks enabled. We use a configuration similar to the official build environment but with shared libraries enabled for Miri:cd mujoco cmake -S . -B build \ -DCMAKE_BUILD_TYPE:STRING=Release \ -DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=ON \ -DMUJOCO_BUILD_EXAMPLES:BOOL=OFF \ -DMUJOCO_BUILD_TESTS=OFF \ -DBUILD_SHARED_LIBS:BOOL=ON \ -G Ninja \ -DCMAKE_C_COMPILER:STRING=clang \ -DCMAKE_CXX_COMPILER:STRING=clang++ \ -DMUJOCO_HARDEN:BOOL=ON \ -DMUJOCO_MIRI_SUPPORT:BOOL=ON cmake --build build --parallel --target mujoco -
Select the example and prepare a temporary minimal patch: Pick the example requested by the user and patch it directly with the smallest possible temporary change so Miri can run.
Use this pattern:
- Do not add long-lived
#[cfg(miri)]/#[cfg(not(miri))]scaffolding. - Apply only the minimal temporary edits needed (for example, call
mujoco_rs::miri::install_allocator(), simplify/shorten runtime loop, avoid viewer/GL-only paths). - Keep edits tightly scoped to the selected example and preserve behavior as much as possible.
- Do not add long-lived
-
Setup environment and run the selected example: Run under Miri using the built library. Replace
<EXAMPLE_NAME>andX.Y.Z:# Set paths and Miri flags (using build/lib64/ or build/lib/ depending on your system) # Replace X.Y.Z with the MuJoCo version from Cargo.toml (e.g. +mj-3.9.0 -> 3.9.0) export MUJOCO_DYNAMIC_LINK_DIR=$(realpath build/lib64/) && \ export LD_LIBRARY_PATH=$(realpath build/lib64/) && \ export MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-native-lib=$(realpath build/lib64/libmujoco.so.X.Y.Z) -Zmiri-permissive-provenance -Zmiri-symbolic-alignment-check -Zmiri-deterministic-concurrency -Zmiri-backtrace=full -Zmiri-report-progress -Zmiri-tree-borrows -Zmiri-tree-borrows-implicit-writes -Zmiri-address-reuse-rate=1 -Zmiri-user-relevant-crates=mujoco_rs -Zmiri-track-alloc-accesses" && \ cd .. && \ cargo +nightly miri run --example <EXAMPLE_NAME> --features <REQUIRED_FEATURES>- Do not pipe the Miri command through
tail,head, orgrepwhile streaming; those can buffer and delay visible output.
For targeted deep-dive tracing, extend
MIRIFLAGSwith:-Zmiri-track-alloc-id=...-Zmiri-track-pointer-tag=...
- Do not pipe the Miri command through
-
Verify results and clean up:
- Check for any
error: Undefined Behaviorreports. - The warning about "sharing memory with a native function called via FFI" is expected when calling
mujoco_rs::miri::install_allocator(). - Treat the example edits as temporary: always revert them after investigation.
- Do not commit persistent Miri-only wiring in user-facing examples.
- Check for any
[!NOTE]
- Global Allocator: When
MUJOCO_MIRI_SUPPORTis enabled,mujoco/src/user/user_miri.ccoverridesoperator new/deleteso that every internal C++ allocation goes throughmju_malloc.mujoco_rs::miri::install_allocator()then registers a pool of separate Rust allocations, and MuJoCo takes one whole chunk per block. Miri therefore knows the bounds of every MuJoCo block on its own, and reports a read or a write past a block. One shared buffer would hide that, because the overrun would stay inside the same Miri allocation.- Pool size:
CHUNK_CLASSESinsrc/miri.rssets the chunk sizes and counts, and it is the only thing to tune. A class that runs out spills into the next size up, so only an empty pool fails, withMIRI_LOG: ... no free chunk holds. Raise a count and rerun; no C rebuild is needed. Miri run time grows with the total pool size, so keep the pool near the live high-water mark.- New flags:
-Zmiri-tree-borrows-implicit-writesmakes Tree Borrows stricter, and-Zmiri-address-reuse-rate=1maximizes use-after-free detection. Both cost nothing measurable here.- Leaks: the pool is a deliberate leak, and
install_allocatormarks each chunk with themiri_static_rootintrinsic, so the leak checker stays on for every other allocation. Do not add-Zmiri-ignore-leaks.- Native call limits: a native call supports only integer and pointer arguments and returns.
install_logging_hook()therefore cannot run under Miri, because its handler returns[i8; 1024].- Verbose output: The default flags intentionally increase log volume (
backtrace=full,report-progress, alloc access tracking) to maximize diagnosability.- Provenance:
-Zmiri-permissive-provenanceis essential because MuJoCo (a C library) manages its own memory, which Rust then accesses.- Strict provenance:
-Zmiri-strict-provenanceis not compatible with native FFI calls; do not use it for MuJoCo-backed runs.- Test harness: use
cargo +nightly miri run --example <NAME>, notcargo +nightly miri test. Miri does interpret the standard test harness, and each test reports UB correctly once it callsmujoco_rs::miri::install_allocator()first (without it, the first MuJoCo pointer fails with[noalloc] has no provenance), but every test process that loads MuJoCo dies with SIGSEGV at exit, after Miri prints its verdict. A clean run and a failing run therefore both exit non-zero, socargo miri testcannot serve as a pass/fail gate.cargo miri run --exampleexits 0 on a clean run.- Detection limits, measured by fault injection. Caught: reads and writes past a MuJoCo allocation, Tree Borrows aliasing on MuJoCo memory, alignment, invalid
booland enum values, null and dangling references, uninitialised reads. Missed: an overrun under 64 bytes (mju_mallocrounds every block up to 64, so the overrun stays inside the allocation), a wrong stride inside one MuJoCo buffer (every model array shares one block), use after free of a MuJoCo block (a freed chunk stays a live Rust allocation), and anything whose only effect is inside C. Run/asanfor those four; it catches all of them except the sub-64-byte overrun.
Deliverable -- HTML report
This skill diagnoses UB; it does not apply code fixes (the temporary example edits are reverted),
so its findings go into a self-contained HTML report, not just terminal output. Write/overwrite
mujoco-rs-miri-report.html at the repo root (scope: this run). It must be standalone (inline
<style>), ASCII-only, and match the shared report aesthetic used by /verify
(mujoco-rs-verify-report.html) and mujoco-rs-memory-safety-audit.html: ivory canvas, coral
accent, warm near-black ink, Georgia serif headings, rounded pill badges, white cards,
hairline-border tables. Reuse that styling.
Contents:
- A header noting the nightly toolchain and the key
MIRIFLAGS(e.g.-Zmiri-tree-borrows,-Zmiri-native-lib=...,-Zmiri-permissive-provenance) and which example(s) were run. - A run table:
Example | Features | Result (clean / UB pill) | Notes. - One finding card per
error: Undefined Behavior: the UB kind (e.g. out-of-bounds access, invalid use of uninitialized memory, aliasing/Tree-Borrows violation), the offendingfile:line, and a concise excerpt of the Miri diagnostic and backtrace (relevant frames only, not the full verbose log). - Note the expected, benign "sharing memory with a native function called via FFI" warning so it
is not mistaken for a finding. If every run is clean, state that with a
Cleanpill.
After writing the file, present a brief plain-text summary (which examples were clean, which hit UB).
Signals
- GitHub stars
- 78
- Forks
- 10
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
miri- Source
- github.com/davidhozic/mujoco-rs