Rust Unsafe Boundary Audit - make every unsafe edge small, named, and tested
Use this skill when a Rust change, crate, or repository needs a focused audit of
unsafe blocks, raw pointer handling, layout assumptions, or foreign-function
boundaries. The goal is not to forbid unsafe code; it is to make each unsafe
operation local, justified, mechanically guarded where possible, and covered by
tests or tooling that exercise the contract.
Critical Constraints
- Treat every
unsafe block as a proof obligation. The audit must state the
invariant that makes the operation sound, who must uphold it, and how the code
prevents invalid callers from reaching the operation.
- FFI boundaries must document ownership, lifetime, nullability, threading,
allocator, layout, and panic/unwind behavior. Missing contract text is a
finding even when the implementation looks correct.
- Prefer shrinking the unsafe surface before adding comments. Safe wrappers,
typed handles, length-checked slices, and sealed modules are stronger than
broad caller obligations.
- Do not approve unsafe code from tool output alone. Tooling is evidence, not a
substitute for checking aliasing, initialization, provenance, and lifetime
assumptions in the source.
- Keep findings behavior-specific. A useful finding names the exact operation,
the invariant that can fail, a realistic failure mode, and the smallest
corrective action.
Read Before Auditing
Inspect only the project under review and its in-repo contracts:
Cargo.toml, workspace layout, feature flags, build scripts, and target
configuration that can change unsafe behavior.
- Modules containing
unsafe, extern, repr(C), raw pointers, manual
allocation, inline assembly, generated bindings, or platform-specific code.
- Public APIs that wrap unsafe internals, especially APIs accepting pointers,
file descriptors, handles, lengths, callbacks, or ownership transfer.
- Tests, fuzz targets, Miri configuration, sanitizer jobs, Clippy settings, and
CI workflows that claim to validate unsafe contracts.
- Existing safety comments, architecture notes, FFI headers, C ABI docs, and
generated binding metadata.
If generated bindings are present, audit the handwritten boundary that calls
them. Do not spend the main review on generated code unless the generation
options or wrapper policy are part of the risk.
Boundary Inventory
Build a concise inventory before judging individual sites. Search broadly, then
deduplicate by boundary:
rg -n '\bunsafe\b|extern[[:space:]]+"C"|no_mangle|export_name|repr\(C\)|repr\(transparent\)|from_raw|into_raw|as_ptr|as_mut_ptr|NonNull|MaybeUninit|ManuallyDrop|transmute|slice::from_raw_parts|CStr::from_ptr|CString::from_raw|libc::|windows_sys::|asm!' .
For each boundary, record:
| Field |
What to capture |
| Site |
File, function, and public entry path. |
| Operation |
The unsafe action: dereference, slice construction, FFI call, layout cast, allocation transfer, callback, concurrency primitive, or assembly. |
| Required invariant |
The condition that must be true for soundness. |
| Enforcer |
Type system, checked wrapper, runtime guard, caller contract, external library, or none. |
| Evidence |
Unit test, property test, fuzz target, Miri run, sanitizer run, platform CI, or manual reasoning. |
| Verdict |
Pass, fix required, needs owner decision, or out of scope. |
Group repeated unsafe blocks behind the same abstraction. The audit should make
the module's boundary shape clear, not bury the reader in duplicate line items.
Safety Invariant Checks
For each inventory item, test the contract against these questions.
Pointer and slice validity:
- Can null pointers, dangling pointers, unaligned pointers, or incorrect lengths
reach the unsafe operation?
- Is provenance preserved when converting through integers, byte buffers, or
foreign handles?
- Are zero-length slices handled without requiring a non-null data pointer where
Rust requires one?
Aliasing and mutability:
- Can
&mut T or mutable slices alias any other live reference?
- Does interior mutability use the right primitive and document synchronization
or single-thread assumptions?
- Are
Send and Sync implementations justified by the data they protect, not
by the absence of compiler errors?
Initialization, layout, and drop:
- Is every byte read from initialized memory?
- Are
repr(C) and repr(transparent) assumptions matched to the actual ABI
requirement?
- Are
MaybeUninit, ManuallyDrop, ptr::read, and ptr::write paired with a
clear drop story for success and failure paths?
Lifetime and ownership:
- Does any pointer, callback, or handle outlive the Rust value it depends on?
- Is ownership transfer across FFI one-way and paired with the correct release
function?
- Are borrowed buffers protected from mutation or deallocation by the foreign
side while Rust references exist?
FFI and ABI behavior:
- Are nullability, length units, string encoding, struct packing, and enum values
stated at the boundary?
- Can a Rust panic or foreign exception cross an ABI that does not permit
unwinding?
- Does the code use the same allocator family for allocation and release?
- Are callbacks reentrant, thread-safe, and cancellation-safe according to the
contract?
Integer and platform assumptions:
- Are pointer-sized values, signedness, truncation, endian behavior, and
alignment different on supported targets?
- Do feature flags or target-specific modules bypass guards used on the primary
platform?
Surface Reduction Procedure
When a boundary fails, prefer fixes in this order:
- Encode the invariant in a safe type: non-zero length, owned handle, checked
pointer wrapper, lifetime-bearing reference, or enum with valid variants.
- Move the unsafe operation into the smallest private function that can check
its inputs immediately before the operation.
- Replace caller promises with runtime validation where validation is cheap and
complete.
- Split construction from use so invalid states cannot be represented after
initialization.
- Add a
SAFETY: comment beside the unsafe block that names the invariant and
points to the guard that enforces it.
- If the invariant cannot be encoded or checked, document the public
unsafe
API contract and require call-site evidence for every caller.
Reject broad module-level safety comments that do not tie a specific unsafe
operation to specific guards. A reader should be able to stand on the unsafe
line and see why it is sound.
Validation Evidence
Match checks to the risk instead of running a generic omnibus gate:
cargo test for normal behavior and regression coverage.
cargo test --all-features when feature flags alter unsafe paths.
cargo miri test for undefined-behavior-sensitive code when the crate and
dependencies support Miri.
- Address, leak, or thread sanitizers for pointer lifetime, allocation, and data
race risks on targets where those checks are available.
- Fuzz or property tests for parsers, length arithmetic, byte decoding, and FFI
input adapters.
- ABI/layout assertions for
repr(C) structs, exported symbols, and handles
consumed outside Rust.
- Negative tests for rejected null pointers, invalid lengths, double-free
attempts, callback misuse, panic containment, and mismatched ownership.
Record commands exactly as run, the platform, important feature flags, and any
tests that were skipped. If a tool cannot run, say why and compensate with a
targeted source review or narrower test.
Report Shape
Produce a short audit report in this order:
- Scope: crate, modules, commit range, target platforms, and excluded areas.
- Unsafe inventory: grouped boundary table with invariant and enforcer columns.
- Findings: severity, file/function, failed invariant, failure mode, fix.
- Surface-reduction plan: concrete wrappers, type changes, module moves, or
call-site contract changes.
- Validation evidence: commands run, results, platform, skipped checks, and why.
- Residual risk: assumptions that still depend on callers, foreign code,
platform ABIs, generated bindings, or unavailable tooling.
- Verdict: pass, pass with follow-up, block until fixed, or needs owner
decision.
Finding Standards
A finding is actionable when it includes:
- The exact unsafe operation or FFI edge.
- The invariant that is missing, unenforced, or contradicted by the code.
- A plausible path from safe or external input to undefined behavior, memory
corruption, data race, leak, panic across FFI, or ABI mismatch.
- The narrowest fix that removes or encodes the unsafe obligation.
- The test or tooling evidence that should prove the fix.
Avoid findings that only say "unsafe code exists" or "add more tests." The
audit exists to prove or improve the boundary, not to count unsafe keywords.
Quality Rubric
The audit passes when all hold:
- Every unsafe boundary has a named invariant and an identified enforcer.
- Public safe APIs cannot trigger undefined behavior by passing ordinary invalid
input.
- Public unsafe APIs state caller obligations in Rust terms and, for FFI, ABI
terms.
- The unsafe surface is private and minimal unless public unsafe is essential to
the crate's purpose.
- Validation evidence targets the specific invariants under review.
- Residual risk is explicit enough for maintainers to accept, schedule, or block.
1---2name: rust-unsafe-boundary-audit-23description: Use when auditing Rust unsafe blocks and FFI boundaries, invariants, tests, and tooling. Triggers:4---5
6# Rust Unsafe Boundary Audit - make every unsafe edge small, named, and tested
7
8Use this skill when a Rust change, crate, or repository needs a focused audit of
9`unsafe` blocks, raw pointer handling, layout assumptions, or foreign-function
10boundaries. The goal is not to forbid unsafe code; it is to make each unsafe
11operation local, justified, mechanically guarded where possible, and covered by
12tests or tooling that exercise the contract.
13
14## Critical Constraints
15
16- Treat every `unsafe` block as a proof obligation. The audit must state the
17 invariant that makes the operation sound, who must uphold it, and how the code
18 prevents invalid callers from reaching the operation.
19- FFI boundaries must document ownership, lifetime, nullability, threading,
20 allocator, layout, and panic/unwind behavior. Missing contract text is a
21 finding even when the implementation looks correct.
22- Prefer shrinking the unsafe surface before adding comments. Safe wrappers,
23 typed handles, length-checked slices, and sealed modules are stronger than
24 broad caller obligations.
25- Do not approve unsafe code from tool output alone. Tooling is evidence, not a
26 substitute for checking aliasing, initialization, provenance, and lifetime
27 assumptions in the source.
28- Keep findings behavior-specific. A useful finding names the exact operation,
29 the invariant that can fail, a realistic failure mode, and the smallest
30 corrective action.
31
32## Read Before Auditing
33
34Inspect only the project under review and its in-repo contracts:
35
36- `Cargo.toml`, workspace layout, feature flags, build scripts, and target
37 configuration that can change unsafe behavior.
38- Modules containing `unsafe`, `extern`, `repr(C)`, raw pointers, manual
39 allocation, inline assembly, generated bindings, or platform-specific code.
40- Public APIs that wrap unsafe internals, especially APIs accepting pointers,
41 file descriptors, handles, lengths, callbacks, or ownership transfer.
42- Tests, fuzz targets, Miri configuration, sanitizer jobs, Clippy settings, and
43 CI workflows that claim to validate unsafe contracts.
44- Existing safety comments, architecture notes, FFI headers, C ABI docs, and
45 generated binding metadata.
46
47If generated bindings are present, audit the handwritten boundary that calls
48them. Do not spend the main review on generated code unless the generation
49options or wrapper policy are part of the risk.
50
51## Boundary Inventory
52
53Build a concise inventory before judging individual sites. Search broadly, then
54deduplicate by boundary:
55
56```sh
57rg -n '\bunsafe\b|extern[[:space:]]+"C"|no_mangle|export_name|repr\(C\)|repr\(transparent\)|from_raw|into_raw|as_ptr|as_mut_ptr|NonNull|MaybeUninit|ManuallyDrop|transmute|slice::from_raw_parts|CStr::from_ptr|CString::from_raw|libc::|windows_sys::|asm!' .
58```
59
60For each boundary, record:
61
62| Field | What to capture |
63| --- | --- |
64| Site | File, function, and public entry path. |
65| Operation | The unsafe action: dereference, slice construction, FFI call, layout cast, allocation transfer, callback, concurrency primitive, or assembly. |
66| Required invariant | The condition that must be true for soundness. |
67| Enforcer | Type system, checked wrapper, runtime guard, caller contract, external library, or none. |
68| Evidence | Unit test, property test, fuzz target, Miri run, sanitizer run, platform CI, or manual reasoning. |
69| Verdict | Pass, fix required, needs owner decision, or out of scope. |
70
71Group repeated unsafe blocks behind the same abstraction. The audit should make
72the module's boundary shape clear, not bury the reader in duplicate line items.
73
74## Safety Invariant Checks
75
76For each inventory item, test the contract against these questions.
77
78Pointer and slice validity:
79
80- Can null pointers, dangling pointers, unaligned pointers, or incorrect lengths
81 reach the unsafe operation?
82- Is provenance preserved when converting through integers, byte buffers, or
83 foreign handles?
84- Are zero-length slices handled without requiring a non-null data pointer where
85 Rust requires one?
86
87Aliasing and mutability:
88
89- Can `&mut T` or mutable slices alias any other live reference?
90- Does interior mutability use the right primitive and document synchronization
91 or single-thread assumptions?
92- Are `Send` and `Sync` implementations justified by the data they protect, not
93 by the absence of compiler errors?
94
95Initialization, layout, and drop:
96
97- Is every byte read from initialized memory?
98- Are `repr(C)` and `repr(transparent)` assumptions matched to the actual ABI
99 requirement?
100- Are `MaybeUninit`, `ManuallyDrop`, `ptr::read`, and `ptr::write` paired with a
101 clear drop story for success and failure paths?
102
103Lifetime and ownership:
104
105- Does any pointer, callback, or handle outlive the Rust value it depends on?
106- Is ownership transfer across FFI one-way and paired with the correct release
107 function?
108- Are borrowed buffers protected from mutation or deallocation by the foreign
109 side while Rust references exist?
110
111FFI and ABI behavior:
112
113- Are nullability, length units, string encoding, struct packing, and enum values
114 stated at the boundary?
115- Can a Rust panic or foreign exception cross an ABI that does not permit
116 unwinding?
117- Does the code use the same allocator family for allocation and release?
118- Are callbacks reentrant, thread-safe, and cancellation-safe according to the
119 contract?
120
121Integer and platform assumptions:
122
123- Are pointer-sized values, signedness, truncation, endian behavior, and
124 alignment different on supported targets?
125- Do feature flags or target-specific modules bypass guards used on the primary
126 platform?
127
128## Surface Reduction Procedure
129
130When a boundary fails, prefer fixes in this order:
131
1321. Encode the invariant in a safe type: non-zero length, owned handle, checked
133 pointer wrapper, lifetime-bearing reference, or enum with valid variants.
1342. Move the unsafe operation into the smallest private function that can check
135 its inputs immediately before the operation.
1363. Replace caller promises with runtime validation where validation is cheap and
137 complete.
1384. Split construction from use so invalid states cannot be represented after
139 initialization.
1405. Add a `SAFETY:` comment beside the unsafe block that names the invariant and
141 points to the guard that enforces it.
1426. If the invariant cannot be encoded or checked, document the public `unsafe`
143 API contract and require call-site evidence for every caller.
144
145Reject broad module-level safety comments that do not tie a specific unsafe
146operation to specific guards. A reader should be able to stand on the unsafe
147line and see why it is sound.
148
149## Validation Evidence
150
151Match checks to the risk instead of running a generic omnibus gate:
152
153- `cargo test` for normal behavior and regression coverage.
154- `cargo test --all-features` when feature flags alter unsafe paths.
155- `cargo miri test` for undefined-behavior-sensitive code when the crate and
156 dependencies support Miri.
157- Address, leak, or thread sanitizers for pointer lifetime, allocation, and data
158 race risks on targets where those checks are available.
159- Fuzz or property tests for parsers, length arithmetic, byte decoding, and FFI
160 input adapters.
161- ABI/layout assertions for `repr(C)` structs, exported symbols, and handles
162 consumed outside Rust.
163- Negative tests for rejected null pointers, invalid lengths, double-free
164 attempts, callback misuse, panic containment, and mismatched ownership.
165
166Record commands exactly as run, the platform, important feature flags, and any
167tests that were skipped. If a tool cannot run, say why and compensate with a
168targeted source review or narrower test.
169
170## Report Shape
171
172Produce a short audit report in this order:
173
1741. Scope: crate, modules, commit range, target platforms, and excluded areas.
1752. Unsafe inventory: grouped boundary table with invariant and enforcer columns.
1763. Findings: severity, file/function, failed invariant, failure mode, fix.
1774. Surface-reduction plan: concrete wrappers, type changes, module moves, or
178 call-site contract changes.
1795. Validation evidence: commands run, results, platform, skipped checks, and why.
1806. Residual risk: assumptions that still depend on callers, foreign code,
181 platform ABIs, generated bindings, or unavailable tooling.
1827. Verdict: pass, pass with follow-up, block until fixed, or needs owner
183 decision.
184
185## Finding Standards
186
187A finding is actionable when it includes:
188
189- The exact unsafe operation or FFI edge.
190- The invariant that is missing, unenforced, or contradicted by the code.
191- A plausible path from safe or external input to undefined behavior, memory
192 corruption, data race, leak, panic across FFI, or ABI mismatch.
193- The narrowest fix that removes or encodes the unsafe obligation.
194- The test or tooling evidence that should prove the fix.
195
196Avoid findings that only say "unsafe code exists" or "add more tests." The
197audit exists to prove or improve the boundary, not to count unsafe keywords.
198
199## Quality Rubric
200
201The audit passes when all hold:
202
203- Every unsafe boundary has a named invariant and an identified enforcer.
204- Public safe APIs cannot trigger undefined behavior by passing ordinary invalid
205 input.
206- Public unsafe APIs state caller obligations in Rust terms and, for FFI, ABI
207 terms.
208- The unsafe surface is private and minimal unless public unsafe is essential to
209 the crate's purpose.
210- Validation evidence targets the specific invariants under review.
211- Residual risk is explicit enough for maintainers to accept, schedule, or block.