Safe Rust rules out memory corruption and data races, but not panics, leaks, logic bugs or integer wraparound in release builds. Keep unsafe code in small private modules with a SAFETY comment on every block, wrap each foreign call in a safe function that checks pointers, lengths, lifetimes and ownership, and test it under Miri and AddressSanitizer. Enable overflow-checks for release, never let a panic cross an FFI boundary, and gate crates with cargo-audit and cargo-deny in CI.
unsafedoes not switch off the borrow checker; it allows a short list of operations whose preconditions you must prove in aSAFETYcomment.- A safe function that lets any safe caller cause undefined behavior is unsound, even if no caller does so today.
- Release builds wrap on integer overflow unless
overflow-checks = true, and integer casts withastruncate silently in every build. - Since Rust 1.81 a panic that reaches an
extern "C"function aborts the process; return error codes across FFI instead. - Miri finds undefined behavior only on the paths your tests run and cannot execute foreign code; cover FFI with AddressSanitizer.
- Build scripts and procedural macros run at build time with your privileges; the August 2026 arrayref compromise delivered its payload through a build script.
| Setting | Value | Note | Source |
|---|---|---|---|
edition | "2024" | Rust 1.85+. Requires unsafe extern blocks and #[unsafe(no_mangle)], makes unsafe_op_in_unsafe_fn warn, and marks std::env::set_var unsafe. | Rust 2024 edition guide |
[lints.rust] unsafe_code | "forbid" | In every crate that needs no unsafe code. The [lints] table needs Cargo 1.74 or later. | Cargo Book: the [lints] section |
[lints.rust] unsafe_op_in_unsafe_fn | "deny" | Each unsafe operation inside an unsafe fn needs its own unsafe block and SAFETY comment. | Rust 2024 edition guide |
[lints.clippy] undocumented_unsafe_blocks, multiple_unsafe_ops_per_block, missing_safety_doc | "deny" | In crates that contain unsafe code. Clippy checks that the comment exists, not that it is true. | Clippy lint list (clippy 0.1.99) |
[profile.release] overflow-checks | true | Cargo's release default is false, which wraps silently. | Cargo Book: profiles |
| Toolchain | Rust 1.96 or later | Fixes Cargo CVE-2026-5222 and CVE-2026-5223, which affect third-party registries. 1.99.0 is current. | Rust security advisories, 25 May 2026 |
cargo audit | --deny warnings | Also fails on unmaintained, unsound and yanked crates, not only on vulnerabilities. | cargo-audit 0.22.2 --help |
cargo-deny [advisories] | unsound = "all", yanked = "deny", unmaintained = "workspace" | Vulnerability advisories always fail; the unsound default covers direct dependencies only. | cargo-deny 0.20 advisories configuration |
cargo-deny [sources] | unknown-registry = "deny", unknown-git = "deny" | List crates.io and any private registry in allow-registry. | cargo-deny 0.20 sources configuration |
cargo +nightly miri testpasses for every crate with unsafe code, and eachSAFETYprecondition has a test that pushes a hostile value through the safe API.cargo clippy --all-targets -- -D warningspasses with the unsafe lints at deny, and crates without unsafe code declareunsafe_code = "forbid".- The FFI tests pass on nightly with
RUSTFLAGS=-Zsanitizer=addressand the C library linked in. - A release-build test that adds past
u32::MAXpanics, which provesoverflow-checksis on. cargo audit --deny warningsandcargo deny check advisories bans sourcespass on the committedCargo.lock, and CI builds use--locked.- No job that runs
cargo mirior writes a sharedtarget/cache has secrets in its environment.
- Applies to
- Rust 1.85+ for the 2024 edition; 1.96+ for the Cargo registry fixes (tested 1.99.0)
- Miri Nightly only (tested 2026-10-09); environment-leak fix from nightly 2026-09-22
- cargo-audit 0.22.2 tested
- cargo-deny 0.20.2 tested
- cargo-geiger 0.13.0 tested
Rust crates with unsafe code or foreign-function interfaces. Safe Rust prevents memory errors only while every unsafe block upholds its invariants.
- Part of
- Workflow: Fix a finding
Learning goals and prerequisites
What you will learn
- Explain which bugs safe Rust rules out and which it leaves to the developer.
- Review an unsafe block or FFI wrapper for soundness, lifetimes, ownership and panics.
- Configure lints, profiles, Miri and sanitizers that enforce the review rules.
- Gate dependencies with cargo-audit and cargo-deny and respond to a compromised crate.
- Plan the replacement of a C component behind its existing interface.
Before you start
Rust developers, reviewers and security engineers who own crates with unsafe code or FFI boundaries, or who are moving C components to Rust
- Read Rust code that uses references, slices, Option and Result.
- Know what a raw pointer and a C function prototype are.
- Run cargo build, test and clippy.
In safe Rust the compiler and standard library rule out the bugs that dominate C and C++ (/domains/appsec/platform-security/memory-safety-c-cpp/): out-of-bounds access, use after free, double free, reads of uninitialized memory and data races. The Rust Reference states the guarantee as a list of undefined behaviors that safe code cannot cause, including dangling or misaligned pointer access, breaking the aliasing rules, producing an invalid value and unwinding through a function that does not allow it. Code is sound when no safe caller can trigger any of them. Everything in the table below is still possible in safe code.
| Not prevented in safe code | What happens | Control |
|---|---|---|
| Integer overflow | Panics in debug builds; wraps in release builds unless overflow-checks is on | checked_* arithmetic, try_from, overflow-checks = true |
| Panics | unwrap, expect, indexing and division by zero on bad input end the thread or the process | Return Result from parsers; no panics across FFI |
| Leaks and deadlocks | Leaking memory, deadlocking and exiting without running destructors are all safe | Resource limits, timeouts and tests |
| Logic and injection bugs | CVE-2024-24576: until Rust 1.77.2, untrusted arguments to Windows batch files run through Command could execute shell commands | The same input handling as in any language |
| Undefined behavior in unsafe and foreign code | Safe callers inherit it | Sound wrappers, Miri and sanitizers |
unsafe does not switch off the borrow checker. It allows a short list of operations whose rules the compiler cannot check. The main ones are dereferencing a raw pointer, calling an unsafe or foreign function, using a mutable or external static, reading a union field and implementing an unsafe trait; since the 2024 edition, declaring an extern block and applying attributes such as no_mangle need unsafe too. Each operation has preconditions that the surrounding code must establish.
Make those preconditions reviewable. Put a // SAFETY: comment on every unsafe block that says why each precondition holds, give every public unsafe fn a # Safety section that states what the caller must guarantee, and keep invariants behind private fields in a small module so outside code cannot break them. The Rustonomicon's rule is that unsafe code may trust the specific safe code it depends on, but not safe code its callers supply. smallvec broke that rule: insert_many trusted the lower bound of an iterator's size_hint, and an iterator that yielded more items caused a heap buffer overflow (RUSTSEC-2021-0003, CVE-2021-25900).
A SAFETY comment is only as good as the invariant behind it, and other code can break the invariant later. CVE-2025-68260, in the Linux kernel's Rust Binder driver, came from an unsafe linked-list removal whose comment assumed a node was in its owner's list or in none. But Node::release moved the nodes to a local list and dropped the lock before walking it, so another thread's removal raced on the same pointers, corrupted them and crashed the kernel. Linux 6.18.1 has the fix.
This method is safe to call and unsound: any caller can pass an index past the end, and get_unchecked then reads out of bounds. In a debug build the standard library's precondition checks, which follow the debug-assertions setting since Rust 1.78, aborted the test with "unsafe precondition(s) violated". The same test passed silently in a release build.
/// A record: type (1 byte), length (2 bytes, big-endian), payload.
pub struct Record {
buf: Vec<u8>,
}
impl Record {
pub fn new(buf: Vec<u8>) -> Self {
Record { buf }
}
/// Returns payload byte `i`.
pub fn payload_byte(&self, i: usize) -> u8 {
// SAFETY: none. Any safe caller can pass an `i` past the end, so this
// safe function is unsound: it exposes undefined behavior to safe code.
unsafe { *self.buf.get_unchecked(3 + i) }
}
}The fixed type validates the record in its only constructor, so buf.len() == 3 + payload length holds for every value of the type. The checked accessor needs no unsafe code at all. The unchecked accessor is an unsafe fn whose # Safety section hands the remaining obligation to the caller, and its SAFETY comment cites both facts it relies on.
/// A record: type (1 byte), length (2 bytes, big-endian), payload.
pub struct Record {
buf: Vec<u8>,
}
impl Record {
/// Rejects buffers whose declared length does not match what arrived.
pub fn parse(buf: Vec<u8>) -> Option<Self> {
let declared = usize::from(u16::from_be_bytes([*buf.get(1)?, *buf.get(2)?]));
(buf.len().checked_sub(3)? == declared).then_some(Record { buf })
}
pub fn payload_len(&self) -> usize {
self.buf.len() - 3
}
/// Returns payload byte `i`, or `None` when `i` is out of range.
pub fn payload_byte(&self, i: usize) -> Option<u8> {
self.buf.get(3usize.checked_add(i)?).copied()
}
/// Returns payload byte `i` without a bounds check.
///
/// # Safety
///
/// `i` must be less than the payload length.
pub unsafe fn payload_byte_unchecked(&self, i: usize) -> u8 {
debug_assert!(i < self.buf.len() - 3);
// SAFETY: `parse` guarantees buf.len() == 3 + payload length, and the
// caller guarantees i < payload length, so 3 + i < buf.len().
unsafe { *self.buf.get_unchecked(3 + i) }
}
}Put these settings in the manifest of each crate that is allowed to contain unsafe code, and set unsafe_code = "forbid" in the [lints.rust] table of every other crate. In testing, each lint stopped the build on a probe file: an unsafe block without a comment, a block with two unsafe operations, a public unsafe fn without # Safety, and a raw-pointer dereference in an unsafe fn without its own block. Clippy checks that a SAFETY comment exists, not that it is true; the unsound method above passed every lint.
[lints.rust]
unsafe_op_in_unsafe_fn = "deny" # warns by default in edition 2024; make it an error
[lints.clippy]
undocumented_unsafe_blocks = "deny" # every unsafe block needs a // SAFETY: comment
multiple_unsafe_ops_per_block = "deny" # one proof obligation per block
missing_safety_doc = "deny" # every pub unsafe fn documents its # Safety contract
[profile.release]
overflow-checks = true # panic on arithmetic overflow instead of wrapping silentlyMiri interprets your tests and reports undefined behavior as it happens: out-of-bounds and use-after-free access, uninitialized reads, misalignment, invalid values, data races and, experimentally, aliasing violations. It reported "Undefined Behavior" at the get_unchecked call in the release-mode test that passed natively.
Know its limits. Miri checks only the executions your tests drive, so a clean run is not a proof of soundness, and it cannot run foreign code. Mark FFI tests #[cfg(not(miri))] and run them under AddressSanitizer on nightly instead; that caught the dangling pointer in the next example. If CI caches target/ where pull requests can read it, keep secrets away from the job that runs Miri: until the nightly of 2026-09-22, Miri copied every environment variable into target/.
rustup +nightly component add miri
cargo +nightly miri test # every test that does not call C
cargo +nightly miri test --release # no debug precondition checks; Miri still checks
# error: Undefined Behavior: `assume` called with `false`
# --> src/record_vuln.rs:15:19
# FFI tests: Miri cannot run C, so use AddressSanitizer (nightly; name your target)
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target aarch64-apple-darwin
# ERROR: AddressSanitizer: heap-use-after-free ... READ of size 3
# #0 ... in strlen
# #1 ... in legacy_greetingMost FFI bugs are ownership and lifetime mistakes that the type system cannot see across the boundary. Here the CString is dropped at the end of its let statement, so C reads freed memory; a NULL return is dereferenced; and memory from C's malloc is released through Rust's allocator, which is undefined behavior even where both happen to call free. rustc warns only about the first, through its dangling_pointers_from_temporaries lint. A native test printed "hello, " followed by garbage; under AddressSanitizer it reported a heap-use-after-free in strlen inside the C function.
#![allow(clippy::multiple_unsafe_ops_per_block)] // silencing the lint is the first red flag
use std::ffi::{CStr, CString, c_char};
unsafe extern "C" {
fn legacy_greeting(name: *const c_char) -> *mut c_char;
}
pub fn greeting(name: &str) -> String {
let c_name = CString::new(name).unwrap().as_ptr(); // the CString is dropped at this `;`
// SAFETY: wrong on every count; see the fixed version.
unsafe {
let p = legacy_greeting(c_name); // C reads freed memory
let s = CStr::from_ptr(p).to_string_lossy().into_owned(); // UB if C returned NULL
drop(CString::from_raw(p)); // frees C memory with Rust's allocator
s
}
}Wrap each foreign function once, in a safe function whose signature rules out misuse: slices instead of pointer and length pairs, &str in, String out and Option for failure. Hold memory that C owns as a raw pointer only between the call and the matching C free function. The declarations in an unsafe extern block are promises the compiler cannot check, so generate them from the C headers with bindgen, or generate the header from Rust with cbindgen, and review both sides together.
use std::ffi::{CStr, CString, c_char, c_int};
// These declarations must match csrc/legacy.c exactly; the compiler cannot check them.
unsafe extern "C" {
fn legacy_checksum(buf: *const u8, len: usize, out: *mut u32) -> c_int;
fn legacy_greeting(name: *const c_char) -> *mut c_char;
fn legacy_free(p: *mut c_char);
}
pub fn checksum(buf: &[u8]) -> Option<u32> {
let mut out = 0u32;
// SAFETY: buf is valid for reads of buf.len() bytes and out is a valid u32
// for the whole call; legacy_checksum keeps neither pointer.
let rc = unsafe { legacy_checksum(buf.as_ptr(), buf.len(), &mut out) };
(rc == 0).then_some(out)
}
pub fn greeting(name: &str) -> Option<String> {
let c_name = CString::new(name).ok()?; // an interior NUL is an error, not a panic
// SAFETY: c_name is NUL-terminated and lives until the end of this function.
let p = unsafe { legacy_greeting(c_name.as_ptr()) };
if p.is_null() {
return None;
}
// SAFETY: p is non-null and NUL-terminated; C owns it until legacy_free.
let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
// SAFETY: p came from legacy_greeting and is freed once, by the allocator that made it.
unsafe { legacy_free(p) };
Some(s)
}When C calls Rust, the function receives raw pointers and must build the safe types itself. Check for NULL before slice::from_raw_parts, which is undefined behavior on a null pointer even when the length is 0. Never let a panic escape: since Rust 1.81 a panic that reaches an extern "C" function aborts the process, and catch_unwind around the call does not stop it; in testing the process exited with SIGABRT. Return an error code instead, and use the "C-unwind" ABI only where both sides expect unwinding. A C program linked against this function got 2, -1 and -1 for a valid record, a lying length and NULL.
use crate::record::Record;
/// Returns the payload length of a well-formed record, or -1.
///
/// # Safety
///
/// `buf` must be null or point to `len` bytes that stay readable for the whole call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn record_payload_len(buf: *const u8, len: usize) -> i64 {
if buf.is_null() || len > isize::MAX as usize {
return -1; // from_raw_parts requires a non-null pointer, even when len is 0
}
// SAFETY: buf is non-null, len fits in isize, and the caller guarantees the bytes.
let bytes = unsafe { std::slice::from_raw_parts(buf, len) };
// No unwrap or indexing: a panic here would abort the whole host process.
match Record::parse(bytes.to_vec()) {
Some(r) => i64::try_from(r.payload_len()).unwrap_or(-1),
None => -1,
}
}Cargo's release profile sets overflow-checks = false, so u32::MAX + 2 evaluated to 1 in a release build and panicked in a debug build. Wrapping is defined in Rust, but a wrapped length that sizes a buffer or crosses into C is the same bug as in C. Use checked_add and checked_mul for values from input, wrapping_* where wrapping is intended, and try_from instead of as, which truncates silently in every build. overflow-checks = true turns any case you missed into a panic.
A panic is not memory corruption, but on attacker-controlled input it is a denial of service. Division by zero, slicing past the end, unwrap and expect all panic. With panic = "abort", or across an extern "C" boundary, the whole process exits; with the default unwind strategy the panic unwinds only its own thread, and a std::sync::Mutex it held becomes poisoned. In parsers, return errors, consider Clippy's restriction lints unwrap_used, indexing_slicing and arithmetic_side_effects, and fuzz with cargo-fuzz (/domains/appsec/testing/parser-fuzzing/ shows the harness pattern).
/// Header and body lengths come from the wire.
pub fn frame_len_unchecked(header: u32, body: u32) -> u32 {
header + body // panics in debug builds; wraps in release unless overflow-checks = true
}
pub fn frame_len(header: u32, body: u32) -> Option<u32> {
header.checked_add(body)
}
pub fn wire_len(len: usize) -> Option<u16> {
u16::try_from(len).ok() // `len as u16` would silently turn 70_000 into 4_464
}Every dependency runs code on the build machine before your tests do, because build scripts and procedural macros execute with your privileges. On 2026-08-20, arrayref 0.3.10, a new release of a popular crate, was published from its maintainer's account with a new dependency, proc-macro1, whose build script downloaded a payload; two other crates by the same author were changed too. crates.io removed arrayref 0.3.10 after 86 minutes. RustSec counted 2,285 downloads and noted that most users had older versions pinned in their lockfiles (RUSTSEC-2026-0260). The Rust project believes the maintainer's computer or credentials were compromised. In September it warned of attacks that lure crate owners into video calls about jobs or projects, a style it said is known to be used by North Korea, and said arrayref had been compromised through similar attacks, without knowing whether they were one campaign.
Controls, in the order they pay off:
- Commit
Cargo.lockand build with--locked, so a new release enters only through a reviewed lockfile change. - Run
cargo auditandcargo denyin CI. With the configuration below, a crate that depended on smallvec 1.6.0 failed on RUSTSEC-2021-0003, and wildcard and git dependencies failed the bans and sources checks. - Delay new versions. Cargo's
min-publish-agesetting is still nightly-only in Rust 1.99, so set the delay in your update bot. - Review what each dependency brings. crates.io shows the source that was actually published, which can differ from the repository, and
cargo geigercounts unsafe Rust per crate; it does not scan C files that build scripts compile. - Publish your own crates through crates.io Trusted Publishing, and turn off token-based publishing for them.
- Keep the toolchain current: Rust 1.96 fixed two Cargo vulnerabilities that affect third-party registries (CVE-2026-5222 and CVE-2026-5223).
# cargo deny check advisories bans sources (cargo-deny 0.20)
[advisories]
# RustSec vulnerability advisories always fail the check.
unsound = "all" # also fail on unsound-API advisories in transitive dependencies
yanked = "deny"
unmaintained = "workspace" # fail when a direct dependency is flagged unmaintained
ignore = [
# { id = "RUSTSEC-0000-0000", reason = "the affected function is never called" },
]
[bans]
wildcards = "deny" # no `*` version requirements
multiple-versions = "warn"
deny = [
# { crate = "openssl", reason = "standardized on rustls" },
]
[sources]
unknown-registry = "deny" # only crates.io, plus registries listed in allow-registry
unknown-git = "deny" # no git dependencies unless listed in allow-git
allow-registry = ["https://github.com/rust-lang/crates.io-index"]This script ran clean on the fixed crate. Run it without secrets in the environment. cargo geiger exits non-zero when it finds files it cannot scan, so treat its output as a report, not a gate.
#!/bin/sh
# Run from the workspace root on every pull request. Needs: stable + nightly (miri),
# cargo-audit, cargo-deny; cargo-geiger for the report-only step.
set -eu
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
cargo test --locked --release # release profile: overflow-checks = true
cargo +nightly miri test --locked # FFI tests are #[cfg(not(miri))]
cargo audit --deny warnings # RustSec: vulnerable, unsound, yanked, unmaintained
cargo deny check advisories bans sources
cargo geiger --output-format Ascii || true # report only: review jumps in unsafe countsAndroid's numbers show where the gain comes from. Google wrote new code in memory-safe languages instead of rewriting old C++, and in 2025 memory-safety bugs fell below 20 percent of Android's vulnerabilities for the first time. With about 5 million lines of Rust and one memory-safety bug, caught before release, Google estimates 0.2 such vulnerabilities per million lines of Rust against about 1,000 per million in its historical C and C++, and says about 4 percent of that Rust is inside unsafe blocks.
To replace a C component, pick one with a narrow interface that parses untrusted input, and implement it in Rust behind the same C signature, as record_payload_len does, so callers do not change. Run the old and new versions side by side and compare their results, as CISA's roadmap guidance suggests, and fuzz both with the same corpus. Keep the C-side defenses while C remains: Google's near-miss, a buffer overflow in unsafe Rust in CrabbyAVIF (CVE-2025-48530), never shipped, and Google found that Android's Scudo allocator would have made it non-exploitable, because Scudo puts guard pages around large allocations.
Reviews of unsafe and FFI code usually miss one of these.
- Marking a function safe because today's callers pass valid arguments.
SAFETYcomments that restate the operation instead of proving its preconditions.- Trusting a length, pointer or ownership claim from C, or a trait implementation that a caller supplies.
- Testing only debug builds, where overflow panics and precondition checks hide what release builds do.
- Treating a clean Miri run as coverage for FFI code it cannot execute.
- Running
cargo build,checkorteston a project you do not trust: its build scripts and procedural macros run on your machine. Audit an untrusted lockfile withcargo audit --filefrom another directory.
Guided practice
A pull request adds thumb, a crate that C code calls to size thumbnail buffers before it allocates them; thumb in turn calls the existing C decoder to read the image header. Review the Rust side.
Supplied scenario material
// thumb: C code calls thumb_size(); Rust calls the C decoder's decode_header().
use std::ffi::{CStr, c_char, c_int};
unsafe extern "C" {
fn decode_header(buf: *const u8, len: usize, w: *mut u32, h: *mut u32) -> c_int;
}
pub fn dimensions(buf: &[u8]) -> (u32, u32) {
let (mut w, mut h) = (0u32, 0u32);
unsafe { decode_header(buf.as_ptr(), buf.len(), &mut w, &mut h) };
(w, h)
}
/// Bytes the C caller must allocate for an RGBA thumbnail no larger than max_side.
#[unsafe(no_mangle)]
pub extern "C" fn thumb_size(buf: *const u8, len: usize, max_side: u32) -> u32 {
let data = unsafe { std::slice::from_raw_parts(buf, len) };
let (w, h) = dimensions(data);
let scale = w.max(h) / max_side;
(w / scale) * (h / scale) * 4
}
pub fn label(name: *const c_char) -> String {
unsafe { CStr::from_ptr(name) }.to_str().unwrap().to_owned()
}1. Find the unsound functions
Name each safe function that a safe caller could use to cause undefined behavior, and say how.
Expected result and reasoning
thumb_sizeandlabelare safe functions that dereference caller-supplied raw pointers, so a dangling or null pointer makesslice::from_raw_partsorCStr::from_ptrundefined behavior. Both must becomeunsafe fns with# Safetysections, orlabelshould take&CStr. The unsafe block indimensionshas noSAFETYcomment and ignoresdecode_header's return code.2. Trace a small image
C calls
thumb_sizefor a valid 100 × 80 image withmax_side = 256. Follow the arithmetic.Expected result and reasoning
scale = 100 / 256 = 0, sow / scalepanics with"attempt to divide by zero"in every build. The panic reaches anextern "C"function, so the process aborts with"panic in a function that cannot unwind": any small image takes down the host program.3. Trace a huge image
The header says 40,000 × 40,000 and
max_side = 40,000. Compute the result in a debug build and in a release build with Cargo's default profile.Expected result and reasoning
scale = 1, so the result is 40,000 × 40,000 × 4 = 6,400,000,000, which exceedsu32::MAX. A debug build panics with"attempt to multiply with overflow"and aborts; a release build wraps to 2,105,032,704, so C allocates about 2.1 GB for 6.4 GB of pixels and overflows the heap when it writes them.4. Check the error path
decode_headerreturns -1 for a truncated header. What doesthumb_sizedo?Expected result and reasoning
dimensionsignores the return code and returns(0, 0), soscaleis 0 and the division panics, aborting the host again. Without the panic, a size of 0 would reach C as if it were valid.5. Write the fix and the gates
Give corrected signatures and the build and CI settings that would have caught each problem.
Expected result and reasoning
pub unsafe extern "C" fn thumb_size(...) -> u64, returning0for a null pointer, a failed decode,max_side == 0or any failedchecked_divorchecked_mul;dimensionsreturnsOption<(u32, u32)>;labeltakes&CStrand usesto_string_lossy. Settings:overflow-checks = true, the four unsafe lints at deny, a Miri test of the arithmetic with a stub decoder, an AddressSanitizer run of the FFI path and a cargo-fuzz target forthumb_size.
Paper exercise. The traces were reproduced with Rust 1.99.0 on macOS against a C stub of decode_header; a real decoder may fail differently on malformed headers.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- The Rust Reference: Behavior considered undefined (soundness)
- The Rust Reference: Behavior not considered unsafe (integer overflow)
- The Rustonomicon: How Safe and Unsafe Interact
- The Rustonomicon: FFI and unwinding
- The Cargo Book: profiles, overflow-checks
- Miri: an undefined behavior detection tool for Rust (README)
- CVE-2025-68260: rust_binder race condition on death_list
- Rust Blog: Supply chain attack on arrayref (20 August 2026)
- cargo-deny: advisories configuration
- Google: Rust in Android: move fast and fix things (13 November 2025)
- Rust Blog: Be alert: targeted attacks on prominent Rustaceans (17 September 2026)
- RustSec: RUSTSEC-2026-0260, arrayref 0.3.10 removed for a malicious dependency
Reference information
- SDLC phase
- Development
- CAPEC attack patterns