Rust UB Risk Audit - make unsafe assumptions explicit
Use this skill to review Rust code for undefined behavior risk at the points
where the compiler cannot enforce the full contract: unsafe code, FFI, raw
pointers, aliasing, layout assumptions, lifetime extension, concurrency, and
dependency boundaries. The goal is not to prove the program safe. The goal is to
produce a precise inventory, identify broken or undocumented safety contracts,
and leave the project with fixes and verification steps that match the actual
risk.
Critical Constraints
- Treat every
unsafe use as a contract with evidence, not as a style issue.
- Do not clear a risk because the code "looks normal"; tie the verdict to an
invariant, a caller guarantee, a test result, or a narrower refactor.
- Keep static reasoning and dynamic tool results separate. Miri, sanitizers,
fuzzing, and tests can expose defects, but passing them does not prove that
every aliasing, lifetime, or FFI contract is valid.
- Prefer reducing the unsafe surface before adding comments. A smaller unsafe
block with checked inputs is stronger than a paragraph explaining why a wide
unsafe region should be okay.
- Report uncertainty as an audit gap. Unknown ABI ownership, missing header
contracts, untested feature flags, or unreviewed native libraries are findings
when they can change safety.
When To Use
Use this skill for:
- Reviewing unsafe Rust blocks, unsafe functions, unsafe traits, or unsafe impls.
- Auditing FFI boundaries, exported C ABI functions, callbacks, and native
library wrappers.
- Investigating raw pointer dereferences, pointer casts, transmute, packed
fields, unions,
MaybeUninit, ManuallyDrop, or layout-sensitive code.
- Checking
Send, Sync, Unpin, pinning, atomics, locks, and interior
mutability where unsoundness can become a data race or invalid reference.
- Reviewing dependency boundaries where a crate imports unsafe abstractions,
generated bindings, native code, or feature-dependent behavior.
Do not use this skill as a general Rust lint pass. If no unsafe, FFI, layout,
or concurrency boundary exists, say that and limit the report to the evidence
that established the low-risk scope.
Inputs To Gather
Inspect only the repository and artifacts relevant to the requested audit:
- Rust source, build scripts, generated bindings, examples, and tests.
Cargo.toml, Cargo.lock, workspace configuration, feature flags, and target
cfg gates.
- FFI headers, bindgen configuration, native build scripts, and wrapper docs.
- Existing safety comments, design notes, issue history, and prior crash reports
when they are in scope for the task.
- CI, sanitizer, Miri, fuzzing, Loom, or stress-test outputs if already present.
If generated code is committed, audit the committed surface and identify the
generator and source contract. If generated code is not committed, audit the
generation command and the checked-in input that drives it.
Quick Start
- Define the audit boundary: crate, workspace, module, feature set, platform,
and whether dependencies or native libraries are in scope.
- Build an unsafe inventory with file paths, symbols, and risk categories.
- For each unsafe boundary, write the safety contract in plain language:
preconditions, ownership, aliasing, lifetimes, layout, threading, panic or
unwind behavior, and caller obligations.
- Compare the contract to the code that establishes it. Mark missing checks,
invalid assumptions, and undocumented caller requirements.
- Run targeted verification commands where available. Record skipped commands
with the reason.
- Deliver a risk-ranked report with evidence, fixes, and residual gaps.
Inventory Commands
Use commands like these from the repository root, adjusting for workspace shape:
rg -n "\bunsafe\b|extern \"|#\[no_mangle\]|#\[export_name|repr\(|transmute|from_raw|into_raw|MaybeUninit|ManuallyDrop|UnsafeCell|NonNull|Send|Sync" .
cargo metadata --format-version 1 --no-deps
cargo tree -e features
If the repository has multiple crates or generated output, record which paths
were included and which were excluded. Do not let a broad rg result replace
manual review; it is only an index.
Audit Procedure
1. Scope The Boundary
State the exact audit target before judging findings:
- Workspace member or crate name.
- Cargo features and target triples that change unsafe code paths.
- Public API, internal API, FFI boundary, or dependency boundary.
- Whether native libraries, generated bindings, proc macros, or build scripts
are in scope.
- Verification commands that can run in the current environment.
If scope is ambiguous, choose the smallest defensible boundary and name what
remains unaudited.
2. Build The Unsafe Inventory
Capture each relevant item:
unsafe fn, unsafe block, unsafe trait, unsafe impl, and extern block.
- Raw pointer creation, cast, arithmetic, dereference, or conversion into a
reference.
- Ownership transfer through
Box::from_raw, Vec::from_raw_parts,
CString::from_raw, handles, descriptors, and custom allocators.
- Layout-sensitive code using
repr(C), repr(transparent), repr(packed),
unions, discriminants, transmute, or byte casting.
- Initialization and drop-sensitive code using
MaybeUninit, ManuallyDrop,
mem::zeroed, ptr::read, ptr::write, or drop_in_place.
- Concurrency-sensitive code using
UnsafeCell, atomics, lock-free structures,
callback threads, unsafe Send, or unsafe Sync.
For each item, assign one or more categories: FFI, aliasing, lifetime, layout,
initialization, drop, concurrency, dependency, or public API contract.
3. Write The Safety Contract
Every unsafe boundary needs an explicit contract. Record:
- What the caller must guarantee before entering the boundary.
- What the boundary validates itself before executing unsafe operations.
- Which pointer ranges are valid, aligned, initialized, and non-null.
- Whether unique or shared access exists, and what prevents incompatible aliases.
- How long references, buffers, callbacks, and handles remain valid.
- Which thread may call it and which thread may drop returned values.
- What happens on panic, error, cancellation, partial initialization, and drop.
- Which representation, endianness, and ABI assumptions must hold.
Missing contracts are findings when callers cannot infer the requirements from
types alone.
4. Check Pointer Aliasing And Lifetimes
Look for these failure modes:
- A mutable reference exists while another live reference or raw pointer can
observe the same memory in an incompatible way.
- A raw pointer is converted to a reference without proving alignment,
initialization, non-nullness, and valid lifetime.
- Slices are built from pointer and length pairs without checking allocation
provenance, length overflow, or element initialization.
- References are stored beyond the lifetime of the source object, callback,
temporary, stack frame, or foreign allocation.
- Pinned data can move after a self-reference, intrusive link, or external
pointer observes its address.
- Drop order invalidates memory while another value still points into it.
Prefer fixes that move checks before unsafe operations and keep references
short-lived.
5. Check FFI Boundaries
For each imported or exported function, review:
- ABI string, symbol name, calling convention, target cfg, and link attributes.
- Ownership transfer for pointers, strings, slices, handles, and buffers.
- Nullability, alignment, length units, terminators, and encoding.
- Allocation and deallocation pairing across language or library boundaries.
- Error reporting, errno-like state, sentinel values, and partial writes.
- Callback lifetime, reentrancy, thread affinity, and user data pointers.
- Panic and unwind behavior across the boundary.
- Header or upstream contract drift, especially when bindings are generated.
Use wrapper types to encode ownership and lifetime when possible. Keep raw FFI
types at the edge and convert into checked Rust types before wider use.
6. Check Layout And Initialization Assumptions
Review each layout-dependent operation:
repr(C) and repr(transparent) match the external contract they claim.
repr(packed) fields are not borrowed in a way that creates unaligned
references.
- Transmute and byte-casting preserve validity, alignment, size, and inhabited
value constraints.
- Zeroed memory is valid for the type being created.
MaybeUninit paths initialize every field before assume-init and correctly
drop only initialized fields on error.
- Unions and discriminants are guarded by a tag or external invariant.
- Endianness and pointer-width assumptions are explicit when data crosses
process, file, or network boundaries.
When a layout assumption exists only in a comment, prefer a compile-time check,
type wrapper, or test that fails when the assumption changes.
7. Check Concurrency And Interior Mutability
Focus on places where Rust's normal sharing rules are bypassed:
- Unsafe
Send and unsafe Sync impls have a type-level invariant that holds
for all fields and all feature combinations.
UnsafeCell access is synchronized or constrained so incompatible access
cannot happen concurrently.
- Atomics use orderings that match the data dependency being protected.
- Lock-free structures handle reclamation, ABA risk, and destructor timing.
- Foreign callbacks do not race with Rust drops, shutdown, or reconfiguration.
- Thread-local state and global mutable state are initialized and torn down in a
defined order.
Flag "works under this scheduler" arguments unless the code has a deterministic
ownership, synchronization, or state-machine reason.
8. Check Dependency Boundaries
Dependencies can import safety contracts that the local crate must uphold:
- Crates that expose unsafe APIs, FFI bindings, native build steps, or feature
gates that change representation or threading behavior.
- Wrapper crates whose safety depends on an upstream C library version.
- Byte-casting, serialization, memory-map, SIMD, allocator, async runtime, or
lock-free crates used near unsafe boundaries.
- Public APIs that pass local types into dependency unsafe functions.
Record whether the local crate validates the dependency's preconditions or
delegates them to callers. If dependency behavior is version-sensitive, cite the
checked version from Cargo.lock or mark the version unknown.
9. Run Targeted Verification
Choose checks that fit the risk surface and repository support:
cargo test --workspace --all-features
cargo test --workspace --no-default-features
cargo miri test
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu
Only run nightly, Miri, sanitizer, fuzzing, or model-checking commands when the
toolchain and target make sense. If a command cannot run, include the attempted
command and the blocker in the report.
Finding Severity
Use severity to communicate fix order:
- Critical: a reachable path can create invalid references, data races, invalid
drops, cross-ABI unwind, or memory corruption with plausible inputs.
- High: the safety contract is likely violated for a supported API, feature, or
platform, even if a concrete crash was not reproduced.
- Medium: a contract is under-specified or insufficiently checked, and misuse is
plausible from local callers or documented public APIs.
- Low: hardening, documentation, or tests would reduce future risk, but current
code has a credible invariant.
- Info: inventory, tool output, or scope notes without an immediate defect.
Do not downgrade a finding because a tool did not reproduce it. Downgrade only
when the code or type system establishes the missing invariant.
Output Specification
Return a concise audit report:
- Scope and environment: crate, features, platform assumptions, commands run.
- Unsafe inventory: table or bullets grouped by file and category.
- Findings: severity, location, risk, evidence, recommended fix, and
verification step.
- Clean areas: boundaries reviewed with no issue found and why.
- Gaps: skipped paths, missing contracts, unavailable tools, or dependencies
not audited.
For each finding, include enough local evidence for a maintainer to reproduce
the reasoning without rereading the whole audit.
Review Checklist
Before finishing, verify:
- Every unsafe block, unsafe function, unsafe trait, unsafe impl, and extern
boundary in scope appears in the inventory.
- Pointer dereferences have alignment, initialization, provenance, bounds, and
lifetime evidence.
- Aliasing and lifetime assumptions are enforced by types, checks, or a narrow
unsafe contract.
- FFI ownership, nullability, allocation pairing, callbacks, and unwind behavior
are explicit.
- Layout assumptions have a representation guarantee or a failing check.
- Concurrency invariants cover unsafe
Send, unsafe Sync, atomics, shared
mutable state, and foreign callbacks.
- Dependency safety contracts are either locally validated or deliberately
delegated with documented caller obligations.
- Verification commands and blockers are recorded accurately.
1---2name: rust-ub-risk-audit3description: Use when auditing Rust UB risks in unsafe, FFI, raw pointers, layout, or concurrency. Triggers:4---5
6# Rust UB Risk Audit - make unsafe assumptions explicit
7
8Use this skill to review Rust code for undefined behavior risk at the points
9where the compiler cannot enforce the full contract: `unsafe` code, FFI, raw
10pointers, aliasing, layout assumptions, lifetime extension, concurrency, and
11dependency boundaries. The goal is not to prove the program safe. The goal is to
12produce a precise inventory, identify broken or undocumented safety contracts,
13and leave the project with fixes and verification steps that match the actual
14risk.
15
16## Critical Constraints
17
18- Treat every `unsafe` use as a contract with evidence, not as a style issue.
19- Do not clear a risk because the code "looks normal"; tie the verdict to an
20 invariant, a caller guarantee, a test result, or a narrower refactor.
21- Keep static reasoning and dynamic tool results separate. Miri, sanitizers,
22 fuzzing, and tests can expose defects, but passing them does not prove that
23 every aliasing, lifetime, or FFI contract is valid.
24- Prefer reducing the unsafe surface before adding comments. A smaller unsafe
25 block with checked inputs is stronger than a paragraph explaining why a wide
26 unsafe region should be okay.
27- Report uncertainty as an audit gap. Unknown ABI ownership, missing header
28 contracts, untested feature flags, or unreviewed native libraries are findings
29 when they can change safety.
30
31## When To Use
32
33Use this skill for:
34
35- Reviewing unsafe Rust blocks, unsafe functions, unsafe traits, or unsafe impls.
36- Auditing FFI boundaries, exported C ABI functions, callbacks, and native
37 library wrappers.
38- Investigating raw pointer dereferences, pointer casts, transmute, packed
39 fields, unions, `MaybeUninit`, `ManuallyDrop`, or layout-sensitive code.
40- Checking `Send`, `Sync`, `Unpin`, pinning, atomics, locks, and interior
41 mutability where unsoundness can become a data race or invalid reference.
42- Reviewing dependency boundaries where a crate imports unsafe abstractions,
43 generated bindings, native code, or feature-dependent behavior.
44
45Do not use this skill as a general Rust lint pass. If no unsafe, FFI, layout,
46or concurrency boundary exists, say that and limit the report to the evidence
47that established the low-risk scope.
48
49## Inputs To Gather
50
51Inspect only the repository and artifacts relevant to the requested audit:
52
53- Rust source, build scripts, generated bindings, examples, and tests.
54- `Cargo.toml`, `Cargo.lock`, workspace configuration, feature flags, and target
55 cfg gates.
56- FFI headers, bindgen configuration, native build scripts, and wrapper docs.
57- Existing safety comments, design notes, issue history, and prior crash reports
58 when they are in scope for the task.
59- CI, sanitizer, Miri, fuzzing, Loom, or stress-test outputs if already present.
60
61If generated code is committed, audit the committed surface and identify the
62generator and source contract. If generated code is not committed, audit the
63generation command and the checked-in input that drives it.
64
65## Quick Start
66
671. Define the audit boundary: crate, workspace, module, feature set, platform,
68 and whether dependencies or native libraries are in scope.
692. Build an unsafe inventory with file paths, symbols, and risk categories.
703. For each unsafe boundary, write the safety contract in plain language:
71 preconditions, ownership, aliasing, lifetimes, layout, threading, panic or
72 unwind behavior, and caller obligations.
734. Compare the contract to the code that establishes it. Mark missing checks,
74 invalid assumptions, and undocumented caller requirements.
755. Run targeted verification commands where available. Record skipped commands
76 with the reason.
776. Deliver a risk-ranked report with evidence, fixes, and residual gaps.
78
79## Inventory Commands
80
81Use commands like these from the repository root, adjusting for workspace shape:
82
83```bash
84rg -n "\bunsafe\b|extern \"|#\[no_mangle\]|#\[export_name|repr\(|transmute|from_raw|into_raw|MaybeUninit|ManuallyDrop|UnsafeCell|NonNull|Send|Sync" .
85cargo metadata --format-version 1 --no-deps
86cargo tree -e features
87```
88
89If the repository has multiple crates or generated output, record which paths
90were included and which were excluded. Do not let a broad `rg` result replace
91manual review; it is only an index.
92
93## Audit Procedure
94
95### 1. Scope The Boundary
96
97State the exact audit target before judging findings:
98
99- Workspace member or crate name.
100- Cargo features and target triples that change unsafe code paths.
101- Public API, internal API, FFI boundary, or dependency boundary.
102- Whether native libraries, generated bindings, proc macros, or build scripts
103 are in scope.
104- Verification commands that can run in the current environment.
105
106If scope is ambiguous, choose the smallest defensible boundary and name what
107remains unaudited.
108
109### 2. Build The Unsafe Inventory
110
111Capture each relevant item:
112
113- `unsafe fn`, unsafe block, unsafe trait, unsafe impl, and extern block.
114- Raw pointer creation, cast, arithmetic, dereference, or conversion into a
115 reference.
116- Ownership transfer through `Box::from_raw`, `Vec::from_raw_parts`,
117 `CString::from_raw`, handles, descriptors, and custom allocators.
118- Layout-sensitive code using `repr(C)`, `repr(transparent)`, `repr(packed)`,
119 unions, discriminants, transmute, or byte casting.
120- Initialization and drop-sensitive code using `MaybeUninit`, `ManuallyDrop`,
121 `mem::zeroed`, `ptr::read`, `ptr::write`, or `drop_in_place`.
122- Concurrency-sensitive code using `UnsafeCell`, atomics, lock-free structures,
123 callback threads, unsafe `Send`, or unsafe `Sync`.
124
125For each item, assign one or more categories: FFI, aliasing, lifetime, layout,
126initialization, drop, concurrency, dependency, or public API contract.
127
128### 3. Write The Safety Contract
129
130Every unsafe boundary needs an explicit contract. Record:
131
132- What the caller must guarantee before entering the boundary.
133- What the boundary validates itself before executing unsafe operations.
134- Which pointer ranges are valid, aligned, initialized, and non-null.
135- Whether unique or shared access exists, and what prevents incompatible aliases.
136- How long references, buffers, callbacks, and handles remain valid.
137- Which thread may call it and which thread may drop returned values.
138- What happens on panic, error, cancellation, partial initialization, and drop.
139- Which representation, endianness, and ABI assumptions must hold.
140
141Missing contracts are findings when callers cannot infer the requirements from
142types alone.
143
144### 4. Check Pointer Aliasing And Lifetimes
145
146Look for these failure modes:
147
148- A mutable reference exists while another live reference or raw pointer can
149 observe the same memory in an incompatible way.
150- A raw pointer is converted to a reference without proving alignment,
151 initialization, non-nullness, and valid lifetime.
152- Slices are built from pointer and length pairs without checking allocation
153 provenance, length overflow, or element initialization.
154- References are stored beyond the lifetime of the source object, callback,
155 temporary, stack frame, or foreign allocation.
156- Pinned data can move after a self-reference, intrusive link, or external
157 pointer observes its address.
158- Drop order invalidates memory while another value still points into it.
159
160Prefer fixes that move checks before unsafe operations and keep references
161short-lived.
162
163### 5. Check FFI Boundaries
164
165For each imported or exported function, review:
166
167- ABI string, symbol name, calling convention, target cfg, and link attributes.
168- Ownership transfer for pointers, strings, slices, handles, and buffers.
169- Nullability, alignment, length units, terminators, and encoding.
170- Allocation and deallocation pairing across language or library boundaries.
171- Error reporting, errno-like state, sentinel values, and partial writes.
172- Callback lifetime, reentrancy, thread affinity, and user data pointers.
173- Panic and unwind behavior across the boundary.
174- Header or upstream contract drift, especially when bindings are generated.
175
176Use wrapper types to encode ownership and lifetime when possible. Keep raw FFI
177types at the edge and convert into checked Rust types before wider use.
178
179### 6. Check Layout And Initialization Assumptions
180
181Review each layout-dependent operation:
182
183- `repr(C)` and `repr(transparent)` match the external contract they claim.
184- `repr(packed)` fields are not borrowed in a way that creates unaligned
185 references.
186- Transmute and byte-casting preserve validity, alignment, size, and inhabited
187 value constraints.
188- Zeroed memory is valid for the type being created.
189- `MaybeUninit` paths initialize every field before assume-init and correctly
190 drop only initialized fields on error.
191- Unions and discriminants are guarded by a tag or external invariant.
192- Endianness and pointer-width assumptions are explicit when data crosses
193 process, file, or network boundaries.
194
195When a layout assumption exists only in a comment, prefer a compile-time check,
196type wrapper, or test that fails when the assumption changes.
197
198### 7. Check Concurrency And Interior Mutability
199
200Focus on places where Rust's normal sharing rules are bypassed:
201
202- Unsafe `Send` and unsafe `Sync` impls have a type-level invariant that holds
203 for all fields and all feature combinations.
204- `UnsafeCell` access is synchronized or constrained so incompatible access
205 cannot happen concurrently.
206- Atomics use orderings that match the data dependency being protected.
207- Lock-free structures handle reclamation, ABA risk, and destructor timing.
208- Foreign callbacks do not race with Rust drops, shutdown, or reconfiguration.
209- Thread-local state and global mutable state are initialized and torn down in a
210 defined order.
211
212Flag "works under this scheduler" arguments unless the code has a deterministic
213ownership, synchronization, or state-machine reason.
214
215### 8. Check Dependency Boundaries
216
217Dependencies can import safety contracts that the local crate must uphold:
218
219- Crates that expose unsafe APIs, FFI bindings, native build steps, or feature
220 gates that change representation or threading behavior.
221- Wrapper crates whose safety depends on an upstream C library version.
222- Byte-casting, serialization, memory-map, SIMD, allocator, async runtime, or
223 lock-free crates used near unsafe boundaries.
224- Public APIs that pass local types into dependency unsafe functions.
225
226Record whether the local crate validates the dependency's preconditions or
227delegates them to callers. If dependency behavior is version-sensitive, cite the
228checked version from `Cargo.lock` or mark the version unknown.
229
230### 9. Run Targeted Verification
231
232Choose checks that fit the risk surface and repository support:
233
234```bash
235cargo test --workspace --all-features
236cargo test --workspace --no-default-features
237cargo miri test
238RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu
239```
240
241Only run nightly, Miri, sanitizer, fuzzing, or model-checking commands when the
242toolchain and target make sense. If a command cannot run, include the attempted
243command and the blocker in the report.
244
245## Finding Severity
246
247Use severity to communicate fix order:
248
249- Critical: a reachable path can create invalid references, data races, invalid
250 drops, cross-ABI unwind, or memory corruption with plausible inputs.
251- High: the safety contract is likely violated for a supported API, feature, or
252 platform, even if a concrete crash was not reproduced.
253- Medium: a contract is under-specified or insufficiently checked, and misuse is
254 plausible from local callers or documented public APIs.
255- Low: hardening, documentation, or tests would reduce future risk, but current
256 code has a credible invariant.
257- Info: inventory, tool output, or scope notes without an immediate defect.
258
259Do not downgrade a finding because a tool did not reproduce it. Downgrade only
260when the code or type system establishes the missing invariant.
261
262## Output Specification
263
264Return a concise audit report:
265
2661. Scope and environment: crate, features, platform assumptions, commands run.
2672. Unsafe inventory: table or bullets grouped by file and category.
2683. Findings: severity, location, risk, evidence, recommended fix, and
269 verification step.
2704. Clean areas: boundaries reviewed with no issue found and why.
2715. Gaps: skipped paths, missing contracts, unavailable tools, or dependencies
272 not audited.
273
274For each finding, include enough local evidence for a maintainer to reproduce
275the reasoning without rereading the whole audit.
276
277## Review Checklist
278
279Before finishing, verify:
280
281- Every unsafe block, unsafe function, unsafe trait, unsafe impl, and extern
282 boundary in scope appears in the inventory.
283- Pointer dereferences have alignment, initialization, provenance, bounds, and
284 lifetime evidence.
285- Aliasing and lifetime assumptions are enforced by types, checks, or a narrow
286 unsafe contract.
287- FFI ownership, nullability, allocation pairing, callbacks, and unwind behavior
288 are explicit.
289- Layout assumptions have a representation guarantee or a failing check.
290- Concurrency invariants cover unsafe `Send`, unsafe `Sync`, atomics, shared
291 mutable state, and foreign callbacks.
292- Dependency safety contracts are either locally validated or deliberately
293 delegated with documented caller obligations.
294- Verification commands and blockers are recorded accurately.