rust-intel-cc 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ # Rust Intel — Security (crypto, timing attacks, error/path/command/SQL injection)
2
+
3
+ > Module of the **rust-intel** skill. Core — operating mode, blocking protocol, enforcement tiers, the trigger table, version pins, and the category→module map — lives in `SKILL.md`. This module holds the category bodies for §B12, §B24, §C2. Tier labels (🔴/🟡/🟢; A–F) and all cross-references are preserved verbatim.
4
+ > **Tiers in this module:** §B12 🔴 · §B24 🔴 · §C2 🟡. Derived from SKILL.md → Enforcement tiers (canonical).
5
+ > **Audit semantics:** 🔴 = report every occurrence; 🟡 = write-time discipline — report only load-bearing/non-obvious cases; 🟢 = clippy's, don't hand-report. Audit the *artifact* (a BANNED pattern present, a REQUIRED code artifact absent); process-REQUIREMENTs ("propose first", "ask the user") are not auditable findings.
6
+
7
+ ---
8
+
9
+ ## §B12. Cryptographic code (silent insecurity)
10
+
11
+ **The trap**: cryptographic code generated by LLMs has a unique failure profile. A large fraction of LLM-generated crypto Rust fails to compile at all, and of the code that *does* compile, a large share of the vulnerabilities present go undetected by static analyzers. Crypto code looks right, runs, passes round-trip tests (encrypt → decrypt yields original) — and is still catastrophically insecure.
12
+
13
+ **Why this happens**: cryptography requires *protocol-level* reasoning the LLM does not do. Encrypt-then-decrypt round-trip is the canonical test, and it passes for any non-broken cipher regardless of whether the key, nonce, or mode is sound. The bugs live at a level orthogonal to functional correctness.
14
+
15
+ **Specifically dangerous patterns**:
16
+ - **Nonce reuse**: hardcoded nonce, nonce derived from a counter that resets, nonce equal to the message ID. Reusing a nonce with the same key in AES-GCM or ChaCha20-Poly1305 is catastrophic — recovers plaintext or forges authentication.
17
+ - **API hallucination in crypto crates**: invented methods on `ring`, `rust-crypto`, `aes-gcm`, `chacha20poly1305`. Crypto-API names look interchangeable to the LLM but have very different security properties. (Note `rust-crypto` itself is **unmaintained since 2016** — RUSTSEC-2022-0011 — and must never be *proposed*; it appears in this list only as a name LLMs hallucinate methods on. Use the maintained `RustCrypto` org crates — `aes-gcm`, `sha2`, etc. — instead.)
18
+ - **Weak parameter choices**: ECB mode (which the LLM may select because it's "simpler"), 64-bit nonces with random generation (birthday-bound collision), insufficient PBKDF2 iterations.
19
+ - **Mixing primitives across security levels**: using SHA-1 alongside AES-256, or pairing a strong cipher with a weak MAC.
20
+ - **Custom crypto code**: hand-rolling any cryptographic primitive in Rust. Almost always wrong.
21
+
22
+ **REQUIRED**:
23
+ - I do not write cryptographic code beyond *direct calls to well-known high-level APIs* (e.g., `aes_gcm::Aes256Gcm::new(key).encrypt(nonce, plaintext)`).
24
+ - For any crypto-touching task, I propose the design in plain text first and ask the user to confirm the threat model before writing code.
25
+ - Nonces are generated via a CSPRNG — prefer `rand::rngs::OsRng` for keys and security-critical nonces, **never** hardcoded, never reused, never derived from a counter without explicit cryptographic justification.
26
+ - Default to high-level libraries (`age`, `ring`, `rustls`) over low-level primitives.
27
+ - For password hashing: `argon2`, not bare PBKDF2 or — under any circumstances — plain SHA-256.
28
+ - I surface every line of crypto code in the post-flight summary with extra prominence. For *custom or protocol-level* crypto (anything beyond a direct documented call) I recommend mandatory human cryptographer review — it is the one place that warrants it. For a direct call to a well-known high-level AEAD/KDF API used per its docs, the bar is surface-it plus verify the params (key length, nonce width, algorithm, iteration count), not a mandatory cryptographer.
29
+
30
+ **BANNED**:
31
+ - Writing custom encryption/decryption logic.
32
+ - Implementing cryptographic primitives (block ciphers, hash functions, KDFs) by hand.
33
+ - Using `SmallRng`, `StdRng`, or any seedable RNG for security-sensitive randomness — use `OsRng` (or `getrandom`) directly. The rule is "OS-backed entropy for keys, nonces, salts", not a literal call name: in `rand` 0.8.x the default RNG accessor is `thread_rng()`, in 0.9+ it is `rng()`. Both are CSPRNGs per the `rand` security guarantees, but `OsRng` is the safer default when seeding chains are part of the threat model. State the `rand` version assumed.
34
+ - Storing crypto keys in source code, environment variables read at compile time, or anywhere they end up in the binary.
35
+ - Comparing secret material (API tokens, MAC tags, password hashes, OTP codes) with `==` — this is a timing side channel. See §B24 for the rule and the `subtle::ConstantTimeEq` / `constant_time_eq` crates.
36
+ - `#[derive(Debug)]` (or manual `impl Debug`) on a struct with fields named `password`, `secret`, `api_key`, `token`, `private_key`, `seed`, `mnemonic`, `cookie` — any secret material that ends up printed in a log/trace via `{:?}`. Wrap secrets in a newtype that implements `Debug` as `"<redacted>"`, or use the `secrecy` crate's `SecretBox<T>`. Scope by *role*, not just the name: the target is secret material used for auth/crypto. Do **not** flag a field merely named `token`/`key`/`seed` in a non-secret role — a lexer/parser `Token`, a map/cache `key`, a keyboard-event `key`, or a deterministic-PRNG `seed` are not secrets. Require a second signal (a crypto/auth context, or nearby `secrecy`/`zeroize`) before redacting.
37
+ - JWT verification accepting the `none` algorithm. Always pin allowed algorithms (`HS256`, `RS256`, etc.) explicitly; `jsonwebtoken::Validation::new(Algorithm::HS256)` rather than the default which accepts whatever the token claims.
38
+ - AEAD encryption with a nonce length other than the algorithm's specified width (96 bits / 12 bytes for AES-GCM and ChaCha20-Poly1305). LLM-generated `let nonce = [0u8; 16];` for AES-GCM compiles but rejects at runtime — or worse, silently truncates depending on the crate version.
39
+
40
+ **Additional REQUIRED**:
41
+ - For any type holding key material, use `zeroize`'s `#[derive(Zeroize, ZeroizeOnDrop)]` — do *not* hand-roll a zeroing `Drop`: the compiler may keep an optimized-away copy on the stack and can elide the manual write.
42
+
43
+ ## §B24. Timing attacks via `==` on secrets
44
+
45
+ **The trap**: `==` on `[u8]`, `Vec<u8>`, `String`, `&str` short-circuits on the first byte mismatch. For non-secret data this is fine. For *secret* comparisons — API tokens, password hashes, MAC tags, OTP codes, session keys, anything an attacker controls one side of — the runtime difference between "first byte wrong" and "first ten bytes right, eleventh wrong" leaks information about the secret one byte at a time. Compiles, runs, passes functional equality tests, and is silently exploitable from across a network on any code path that an attacker can probe repeatedly.
46
+
47
+ **BANNED**:
48
+ - `if user_token == expected_token { ... }` for any secret comparison: API tokens, password-after-hash, MAC tags, OTP codes, session identifiers, HMAC outputs.
49
+ - `Vec<u8> == Vec<u8>` or `&[u8] == &[u8]` for secret material.
50
+ - `String::eq` / `str::eq` on bearer tokens, JWT signatures, or any string the client controls during authentication.
51
+ - Rolling your own constant-time equality with a manual XOR loop *without* `std::hint::black_box` and a `#[inline(never)]` attribute — the compiler may optimize the early-exit back in.
52
+
53
+ **REQUIRED**:
54
+ - Use `subtle::ConstantTimeEq` — `x.ct_eq(&y)` returns `subtle::Choice` (a constant-time-friendly bool surrogate); convert to native `bool` via `bool::from(choice)` or `choice.into()`. **Never** branch on `Choice` directly; the entire point of `Choice` is to keep the comparison branch-free until the explicit conversion. The `constant_time_eq` crate is a smaller alternative — `constant_time_eq::constant_time_eq(a, b) -> bool` directly. Either is acceptable for any secret comparison.
55
+ - For MAC verification specifically, prefer the crypto crate's built-in `verify` / `verify_slice` over manual `==` on the output. (`hmac::Mac::verify_slice` and `aes_gcm::Aes256Gcm::decrypt` both incorporate constant-time comparison; rolling your own is the bug.)
56
+ - Surface every `==` / `!=` on `&[u8]`, `Vec<u8>`, or `String` in the same code path as a secret in the post-flight summary, with the recommendation to switch to a constant-time primitive.
57
+ - Scope note: this targets comparisons where one operand is a secret an attacker can probe (a forgeable tag/token). Constant-time compare is the correct default, but it is not "catastrophic" the way nonce reuse is — and it does **not** apply to comparing a request field against a *public* constant: don't flag `algo == "HS256"` or a header-name check.
58
+
59
+ ## §C2. Error handling discipline
60
+
61
+ **The trap**: `anyhow::Error` in library crates poisons downstream error handling. `unwrap()` and `expect()` in non-test code is a runtime panic waiting to happen. The `?` operator silently loses context if `From` impls are too eager.
62
+
63
+ **REQUIRED**:
64
+ - In **published library crates** (anything shipped to crates.io with a `pub` API that other authors consume): use `thiserror` for typed errors, never `anyhow::Error` in public APIs. Each `pub fn` returning `Result` has a typed error. The cost of `anyhow` here is paid by every downstream caller who loses the ability to match on specific error variants.
65
+ - For **internal/workspace libraries** (not published, only used within the same workspace by the same team): `anyhow::Error` in `pub fn` is acceptable if the team agrees, but make it a deliberate choice — once a library moves toward publication, the migration to typed errors becomes painful.
66
+ - In **binary** crates (`main.rs` and friends): `anyhow::Error` is acceptable for top-level handlers and CLI surfaces.
67
+ - `unwrap()` is allowed only when (a) it is statically impossible to fail and I have a comment explaining why, or (b) in tests. Same for `expect()`.
68
+ - `?` is fine when the conversion is meaningful; if it loses context, use `.map_err(|e| MyError::Context { source: e, info: ... })` instead.
69
+ - `panic!`, `todo!`, `unimplemented!`, `unreachable!` are surfaced in the summary with justification.
70
+
71
+ **BANNED**:
72
+ - `anyhow::Result<T>` in a `pub` API of a published library crate.
73
+ - `.unwrap()` on `Mutex::lock()` in production code (the panic message is unhelpful; use `.expect("description")` minimum, or handle the poison case).
74
+ - Silent `let _ = result;` to discard errors. If discarding is intentional, comment why.
75
+ - `Result<T, Box<dyn Error>>` (or `Result<T, Box<dyn Error + Send + Sync>>`) as the return type of any `pub fn` in a published library crate. Callers cannot match on the error variant — every error becomes an opaque blob. For libraries, define a concrete error enum (typically via `thiserror`). Binary `main` and CLI handlers are excepted (same as `anyhow` above) — there is no downstream caller to match. Internal/workspace code may use `Box<dyn Error>` or `anyhow::Error` as a deliberate trade-off.
76
+ - Reflexive `#[from]` on every error variant. `#[from] io::Error` makes every `?` on an I/O operation collapse into one variant — the resulting error can no longer say *which* operation failed (the config read? the socket write? the temp-file flush?). It compiles, tests pass, and production logs become "I/O error" with no call-site context. Use `#[from]` only where the source type already uniquely identifies the failure; otherwise carry context with `#[source]` plus an explicit `.map_err(|e| MyError::ConfigRead(e))` at each call site, or use `anyhow::Context::context` in binary code.
77
+ - `std::env::var("X").unwrap()` / `.expect(...)` — panics at startup both when the variable is *missing* and when its value is *not valid UTF-8* (common for paths on Windows / non-UTF8 locales). For values that may be non-UTF8 use `std::env::var_os`; for missing-but-optional config, handle the `Err(VarError::NotPresent)` with a default instead of panicking.
78
+ - `base.join(user_segment)` where `user_segment` may be absolute. `Path::join` with an absolute argument (`/etc/passwd`, `C:\…`, or a leading `/`) **discards `base` entirely** and returns the joined path — a path-traversal / write-to-wrong-place hazard when any segment is attacker-controlled. It compiles, tests on relative names pass, and production reads/writes outside the intended directory. The set of segments that drop (or partially drop) `base` is **broader than `is_absolute()`** and is not fully captured by `has_root()` either: on a Windows target a bare separator like `\windows` has `is_absolute() == false` yet still discards a drive-relative `base` (its first component is `Component::RootDir`), while a UNC path `\\server\share` parses as `[Component::Prefix, Component::RootDir]` (so `is_absolute()` and `has_root()` are both `true` there). The robust, OS-independent guard is to inspect the **first component** and reject a leading `Component::Prefix` **or** `Component::RootDir` (this catches `/etc/passwd`, `C:\…`, `\windows`, and `\\server\share` alike) — *not* `is_absolute()` alone. Path parsing is target-OS-specific: prefixes exist only on Windows targets. The robust recipe is a sequence, not an either/or: (1) reject a leading `Component::Prefix`/`Component::RootDir` (above) **and** any `Component::ParentDir` (`..`) in the untrusted segment; (2) join; (3) if the path will be *read*, additionally `canonicalize` and re-verify `result.starts_with(base)` to defeat a symlink inside `base` pointing out. Do not rely on `canonicalize` alone — it errors on a not-yet-created path (the write case) and is unnecessary once `..` and roots are rejected. **Threat-model caveat (CWE-367):** step (3) closes the *static* symlink case (a pre-existing link inside `base` pointing out), but it is **not race-free** against an attacker who can swap a component between the `canonicalize` and the subsequent `open` (a privileged process operating over an attacker-writable directory). State the threat model: when the directory tree is not mutable by an untrusted party during the operation, the static check is sufficient; when it is, the check + `open` is a TOCTOU, and the race-free answer is component-wise `openat` + `O_NOFOLLOW` (or Linux `openat2(RESOLVE_BENEATH)`) — in Rust, the `cap-std` crate.
79
+ - `Command::new("sh").arg("-c").arg(format!("… {user} …"))` — or any subprocess spawned *through a shell* with untrusted data interpolated into the command string: classic **OS command injection** (RCE). The shell re-parses `;`, `|`, `$()`, backticks, `&&` in the injected value. Adjacent: **argument injection** — an untrusted value beginning with `-` is consumed as a *flag* by the target program (`--output=…`, `-o /etc/…`), changing its behavior even without a shell. It compiles, the happy path passes, and production is a remote-code-execution / arg-smuggling hole.
80
+ - Never build a shell command line from untrusted input. Spawn the program directly — `Command::new("convert").arg(user_path)` — so the OS performs no shell parsing and each `.arg()` is exactly one argv element. Insert a literal `--` argument before the first positional user-controlled value to stop it being read as a flag, and reject/validate values beginning with `-`. Allowlist the executable path; never let untrusted input choose the program. If a shell is genuinely unavoidable, escape via a vetted crate (`shell-escape`), never `format!`.
81
+ - `sqlx::query(&format!("SELECT … WHERE id = {id}"))` — or any SQL assembled by string-formatting untrusted input (`sqlx::query_as(&format!(…))`, `diesel::sql_query(format!(…))`, a hand-built `WHERE`/`ORDER BY` fragment): **SQL injection**. The interpolated value is parsed as SQL, not bound as data. It compiles, passes tests against a local DB on benign inputs, and is a data-exfiltration / data-loss hole in production. The `sqlx::query!` *macro* validates the SQL at compile time but does **not** make `format!`-interpolation safe — a formatted string has no placeholders left for it to bind.
82
+ - Pass every untrusted value as a **bound parameter**, never as formatted SQL: `sqlx::query("SELECT … WHERE id = $1").bind(id)`, the compile-checked `sqlx::query!("… WHERE id = $1", id)`, or Diesel's typed DSL / `diesel::sql_query(...).bind(...)`. For genuinely dynamic SQL (a variable `IN`-list, an optional filter) build it with `sqlx::QueryBuilder` (it binds as it pushes) rather than concatenating input into the string. A column or table name that must be dynamic cannot be a bound parameter — **allowlist** it against a fixed set, never interpolate it raw.
@@ -0,0 +1,127 @@
1
+ # Rust Intel — Semantic Conformance
2
+
3
+ > Module of the **rust-intel** skill. Core lives in `SKILL.md`. This module holds the category bodies for §F1–§F4 — defects of *meaning*: code that compiles, passes its own tests and clippy, and implements the wrong thing. These categories are not found by pattern-matching; see SKILL.md → "Tier F — how to review for meaning".
4
+ > **Tiers in this module:** §F1 🔴/🟡 · §F2 🔴/🟡 · §F3 🟡 (🔴 when attacker-extendable) · §F4 🟡. Derived from SKILL.md → Enforcement tiers (canonical); the conditional-🔴 rule is restated in the "Tier F — how to review for meaning" section there.
5
+ > **Audit semantics:** unlike Tiers A–E, the auditable artifact here is often an *absence* (a spec-mandated state not handled, a round-trip test not present, a documented guarantee not enforced). An absence finding must name the reference it is absent *from* — "the spec requires X (section/source), the code has no path for X" — never a bare "looks incomplete".
6
+
7
+ ---
8
+
9
+ ## §F1. Spec / reference conformance — *self-consistent, green, and wrong*
10
+
11
+ **The trap**: the code names an external source of truth — an RFC, a file format, a protocol, "a port of <upstream>" — and then diverges from it: a wire field emitted in the wrong order or width, an endianness flipped, a length prefix counting bytes where the spec counts elements, a state machine handling the common transitions and silently absorbing the rest in `_ =>`, an error mapped to the wrong protocol code. Every internal test passes, because the tests were written against the same misreading. The bug surfaces only against a *real* counterpart — by then the wrong format may already be persisted or deployed on both ends.
12
+
13
+ **BANNED**:
14
+ - Implementing a named spec/format from memory of how it "usually works", without the reference text (or reference implementation) at hand. A protocol detail recalled is a protocol detail guessed.
15
+ - A `match` on a protocol state / opcode / message-type with a `_ => ` (or `_ => unreachable!()` / silent-ignore) arm covering values the spec defines and assigns behavior to. Wildcards are for values the spec itself leaves reserved — and then the spec's mandated reserved-value behavior (reject? ignore? echo?) is what the arm must implement, with a comment citing the section.
16
+ - Verifying conformance by round-tripping against your own implementation only (`assert_eq!(decode(encode(x)), x)` proves inverse-consistency — §F4 — not conformance; both halves can share the same misreading).
17
+ - "Compatible with X" in docs/README when no test exchanges bytes with X itself, X's reference implementation, or X's published test vectors.
18
+ - Silently extending or restricting the spec ("we also accept...", "we never send...") without a `// DEVIATION from <spec §>: <what and why>` comment. Undocumented deviation is indistinguishable from a bug.
19
+
20
+ **REQUIRED**:
21
+ - Cite the reference at the implementation site: `// RFC 9293 §3.5.2` / a link to the upstream function being ported. Uncited conformance claims are unreviewable.
22
+ - Test against the *external* oracle wherever one exists: published test vectors, golden byte-fixtures captured from the reference implementation, or an interop test against the real counterpart. Fixtures must be captured/derived from the reference, not generated by the code under test (that is the §D1a circular oracle).
23
+ - For state machines: enumerate the spec's states and transitions in a table (comment or doc) and ensure each row has a code path and each spec-mandated rejection is tested. The enumeration is the review artifact; "looks complete" is not.
24
+ - On review (audit mode): list spec-mandated behaviors first, tick off against the code, report the unticked. See SKILL.md → Tier F stance, rule 3.
25
+
26
+ **Example — compiles, green, wrong**:
27
+ ```rust
28
+ /// Encodes a length-prefixed frame "per the wire spec".
29
+ fn encode_frame(payload: &[u8]) -> Vec<u8> {
30
+ let mut out = Vec::with_capacity(payload.len() + 2);
31
+ out.extend_from_slice(&(payload.len() as u16).to_le_bytes()); // spec says big-endian
32
+ out.extend_from_slice(payload);
33
+ out
34
+ }
35
+ // decode_frame reads to_le_bytes too — round-trip test green, clippy silent,
36
+ // interop with any conforming peer: every frame length is byte-swapped.
37
+ ```
38
+
39
+ ## §F2. The project's own documented guarantees — *the bug is only visible after reading the README*
40
+
41
+ **The trap**: the project's README / SECURITY.md / design doc states guarantees in prose — "tokens are never logged", "all input on this port is untrusted", "writes are durable after `commit` returns", "events are delivered in order per key" — and a diff, locally reasonable, violates one: a new `debug!("{request:?}")` where the request holds a token; a parser on the untrusted port that trusts a length field; a fast path that returns before `fsync`. No category in Tiers A–E fires, because each guarantee is project-specific. The defect exists only in the gap between the code and a document the reviewer didn't open.
42
+
43
+ **BANNED**:
44
+ - Reviewing or modifying code in a project with stated guarantees without first extracting those guarantees into an explicit checklist (SKILL.md → Tier F stance, rule 4). This is a process ban with an auditable artifact: a Tier-F review that cites no project doc has not performed §F2.
45
+ - Weakening a documented guarantee "internally" (the doc still promises it) — e.g. adding a config flag that disables an invariant the README states unconditionally — without updating the doc in the same change.
46
+ - Treating data as trusted because the *code path* looks internal, when the *docs* classify its origin as untrusted (the doc, not the call graph, defines the trust boundary).
47
+ - New logging/telemetry/error messages formatting types that the project's docs classify as sensitive (see also §B12 Debug-leak — that category catches `password`/`token` field *names*; this one catches what the *docs* declare sensitive regardless of name).
48
+
49
+ **REQUIRED**:
50
+ - Before generating or reviewing code in such a project: read the guarantee-bearing docs, list the guarantees touched by this change, and state in the response which were checked ("guarantees checked: ordering-per-key (unaffected), no-token-logging (touched — see line N)").
51
+ - When a change makes a documented guarantee false, the doc change ships in the same diff — or the code change is rejected.
52
+ - When the docs and the existing code already disagree, that is a finding in itself (report it; don't silently pick a side).
53
+
54
+ **Example — compiles, green, wrong**:
55
+ ```rust
56
+ // README: "the bridge treats everything received from the client socket as
57
+ // untrusted and never allocates based on client-supplied sizes."
58
+ let n = read_u32(&mut client).await?; // client-supplied
59
+ let mut buf = Vec::with_capacity(n as usize); // violates the documented guarantee
60
+ // §B7's trigger needs the reviewer to *know* `n` is untrusted; only the README says so.
61
+ ```
62
+
63
+ ## §F3. Resource lifecycle at the boundary and on the error path — *the happy path cleans up; the `?` path leaks*
64
+
65
+ **The trap**: connection/stream/file handlers are written happy-path-first: cleanup (shutdown, flush, unregister, decrement, abort the paired task) sits at the function tail, and every early `?` / `return` / `break` skips it. Or EOF handling: `read` returning `Ok(0)` is looped on (busy-spin) or treated as an error instead of a half-close; the write side is never `shutdown()`, so the peer waits forever. Or no read deadline on an untrusted remote: a peer that connects and sends nothing pins a task, a buffer, and an FD indefinitely. Tests are green because the test peer is well-behaved, fast, and always closes politely. Production peers are none of these.
66
+
67
+ **BANNED**:
68
+ - Cleanup that runs only on the `Ok` path — any teardown reachable from fewer paths than the acquisition. If the resource needs teardown, it needs an RAII guard (§B4) or a single exit funnel; "I'll remember to clean up before each `return`" does not survive the next `?` someone adds.
69
+ - `.read()` / `read_exact` / `next()` on a stream from an untrusted peer without an enclosing `tokio::time::timeout` or an idle-deadline mechanism. (Connect-then-silence is the cheapest DoS there is. 🔴 when peer-extendable.)
70
+ - Treating `read() == Ok(0)` as anything other than EOF: retrying it (busy-loop), erroring on it when the protocol allows half-close, or tearing down the write direction that may still have data to flush.
71
+ - In a bidirectional byte proxy: an unbounded buffer between the two directions (§B14 applies — the slow side must backpressure the fast side, not grow a Vec), and copy loops that don't propagate shutdown — when one direction EOFs, the proxy must `shutdown()` the corresponding write side, not just drop the loop.
72
+ - Spawning a per-connection sibling task (heartbeat, writer half) without aborting/joining it on every exit path of the main handler — the `Err` path included (§B21 twin, scoped to the connection lifecycle).
73
+
74
+ **REQUIRED**:
75
+ - For every acquired boundary resource, name the release point and verify it is reached from *every* exit: each `?`, each `select!` arm losing the race (§B3/§B23), panic (does Drop suffice?). The review move is path enumeration, not pattern recognition.
76
+ - Idle/read deadlines on all untrusted-peer reads; document the chosen timeout and what happens on expiry.
77
+ - Explicit EOF policy per stream: on `Ok(0)` — flush and `shutdown()` write side? close both? Document it; "whatever the code happens to do" is where the bug lives.
78
+ - Tests must include the rude peer: connects and stalls, sends a partial frame and dies, half-closes mid-stream. An in-memory duplex that always behaves is the §D1a stub-oracle trap.
79
+
80
+ **Example — compiles, green, wrong**:
81
+ ```rust
82
+ async fn handle(mut client: TcpStream) -> io::Result<()> {
83
+ let upstream = TcpStream::connect(UPSTREAM).await?;
84
+ register_conn(&client).await; // metrics: conn count +1
85
+ let creds = read_handshake(&mut client).await?; // early `?`: upstream leaked
86
+ proxy(client, upstream, creds).await?; // until Drop, conn count never -1:
87
+ unregister_conn().await; // skipped on every error path
88
+ Ok(())
89
+ }
90
+ // Tests use a polite in-memory peer: handshake always succeeds, count always balanced.
91
+ // In prod, a scanner that connects and disconnects drifts the gauge upward forever.
92
+ ```
93
+
94
+ ## §F4. Property obligations for inverse pairs — *encode/decode written together, never proven inverse*
95
+
96
+ **The trap**: the codebase ships an inverse pair — `encode`/`decode`, `parse`/`Display`, `to_bytes`/`from_bytes`, `encrypt`/`decrypt`, `compress`/`decompress` — and tests each half on a handful of literals, never asserting the round-trip law over the input domain. The halves drift: an escape added to `Display` that `parse` doesn't strip, a field appended to `encode` that `decode` skips, padding `encrypt` adds that `decrypt` truncates wrong on boundary sizes. Each half's unit tests stay green; only the *composition* on an unanticipated input is broken. (§B20 mandates this for serde; this category generalizes it to every hand-written inverse pair — where no framework reminds you, so it's most often missing.)
97
+
98
+ **BANNED**:
99
+ - Shipping one half of an inverse pair without a round-trip test of the composition (`decode(encode(x)) == x`, or for parse/Display: `parse(display(x)) == x`) in the same change. The pair is one unit; testing the halves separately tests two functions and zero laws.
100
+ - Round-trip over literals only, when the input domain has structure: empty input, max-length, every escape-worthy character, boundary sizes (block size ± 1 for crypto/compression), non-ASCII/UTF-8 multi-byte (§B28), `Option`/default-collapsing values (§B20). Hand-picked literals encode the author's blind spots — `proptest` doesn't have them.
101
+ - Asserting the round-trip in the wrong direction only. `decode(encode(x)) == x` (full domain of `x`) and `encode(decode(b)) == b` (canonical-form question — often *legitimately false* when multiple encodings normalize) are different laws; know which one your format promises and test that one. Don't let a failing canonical-form test get "fixed" by weakening the real law.
102
+ - Letting a round-trip property substitute for §F1 conformance: round-trip proves the pair agrees with *itself*, not with the spec. Both are required for a wire format; neither implies the other.
103
+
104
+ **REQUIRED**:
105
+ - One `proptest`/`quickcheck` round-trip property per inverse pair, named for the law (`prop_decode_encode_roundtrip`), generating over the realistic input domain — shipped in the same change as the pair (Pre-flight item 9).
106
+ - For fallible decode: also a corpus of *invalid* inputs asserting `Err` (a decoder that never rejects accepts garbage — the negative control, §D1a).
107
+ - For `FromStr`/`Display` pairs: the round-trip property *is* the API contract users assume (`x.to_string().parse() == Ok(x)`); breaking it is a semver-relevant behavior change — flag inline.
108
+ - When the pair is intentionally non-inverse (lossy compression, normalizing parser), state the actual law in a doc comment and test *that* (`parse(display(parse(s))) == parse(s)` — idempotence) instead of silently testing nothing.
109
+
110
+ **Example — compiles, green, wrong**:
111
+ ```rust
112
+ impl Display for Tag { // "key=value;key=value"
113
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
114
+ write!(f, "{}={}", self.key, self.value) // value may contain '=' or ';'
115
+ }
116
+ }
117
+ impl FromStr for Tag {
118
+ fn from_str(s: &str) -> Result<Self, ParseError> {
119
+ let (k, v) = s.split_once('=').ok_or(ParseError)?; // first '=' wins
120
+ Ok(Tag { key: k.into(), value: v.into() })
121
+ }
122
+ }
123
+ // Unit tests: Display tested on ("env", "prod"), FromStr on "env=prod". Both green.
124
+ // Tag { key: "k", value: "a=b" } round-trips to value "a=b"... but
125
+ // Tag { key: "k", value: "x;y" } corrupts the *containing* list format — and
126
+ // no test composes the two halves over generated values, so nothing ever fails.
127
+ ```
@@ -0,0 +1,112 @@
1
+ # Rust Intel — Testing, CI & Measurement
2
+
3
+ > Module of the **rust-intel** skill. Core — operating mode, blocking protocol, enforcement tiers, the trigger table, version pins, and the category→module map — lives in `SKILL.md`. This module holds the category bodies for §D1 (a), §D2, §D3, §D4, §D5, §E6. Tier labels (🔴/🟡/🟢; A–F) and all cross-references are preserved verbatim.
4
+ > **Tiers in this module:** §D1 🟡 · §D1a 🟡 · §D2 🟡 · §D3 🟡 · §D4 🟡 · §D5 🟡 · §E6 🟡/🟢. Derived from SKILL.md → Enforcement tiers (canonical).
5
+ > **Audit semantics:** 🔴 = report every occurrence; 🟡 = write-time discipline — report only load-bearing/non-obvious cases; 🟢 = clippy's, don't hand-report. Audit the *artifact* (a BANNED pattern present, a REQUIRED code artifact absent); process-REQUIREMENTs ("propose first", "ask the user") are not auditable findings.
6
+
7
+ ---
8
+
9
+ ## §D1. Tests that pass by luck
10
+
11
+ **The trap**: `thread::sleep(Duration::from_millis(N))` to "wait for the async work to finish" races: the work happens to complete in `< N ms` on the developer's machine, the test passes, CI runs slower (or faster) and the test flakes. `#[should_panic]` without `expected = "..."` catches *any* panic — including a panic in test setup before the system-under-test was even called — and the test author reads "should panic" as proof that the SUT panicked, when in fact it might have been the test scaffolding. Tests that only assert "no panic" (`do_thing(); assert!(true);`) prove the syscall ran, not that it produced the correct postcondition.
12
+
13
+ **BANNED**:
14
+ - `std::thread::sleep` or `tokio::time::sleep` (real) to synchronize a test with async work — race condition, flaky CI.
15
+ - `#[should_panic]` without `#[should_panic(expected = "specific message substring")]` — any panic, anywhere in the test, makes it green.
16
+ - Tests that assert no postcondition: `let _ = do_thing(); assert!(true);` — proves only that `do_thing` returned without panicking, which is the weakest possible assertion.
17
+ - Tests that compare two outputs derived from the same buggy intermediate (e.g., `assert_eq!(serialize(x), serialize(parse(serialize(x))))` does not prove `parse` is correct, only that it is idempotent on serialize output).
18
+ - `assert_eq!(a, b)` where `a: f32` or `f64` and the values are computed (not literal). Floating-point exact equality flakes between debug/release builds, between architectures (SSE vs AVX vs NEON), and across compiler versions due to reassociation. Use `approx::assert_relative_eq!(a, b, epsilon = …)` or `assert_abs_diff_eq!`, or write the comparison manually as `(a - b).abs() < eps`.
19
+ - A test whose mock/fake always returns `Ok`/success and never reproduces the failure modes the real dependency exhibits (timeouts, partial reads, `5xx`, connection resets). A green test against a happy-path-only mock proves behavior against fiction, not against the dependency. Mock the error paths too, or use a fake that can be told to fail.
20
+ - **Vacuous tests / coverage theater.** A test that asserts a value against its own definition (`assert_eq!(MAX_RETRIES, 3)`), re-checks what the compiler, `std`, or a `#[derive]` already guarantees (that `Clone` clones; that a getter returns what the setter just set with no logic in between), or exercises a *dependency's* behavior instead of your own. It lifts the coverage number and the confidence while being structurally unable to fail for any reason that matters — and every refactor pays to update a test that never could have caught a bug. Delete it, or rewrite it to assert a postcondition that *could* break. This is the silent twin of the happy-path mock: there the fake cannot fail, here the assertion cannot. **Exception — contract pins are not vacuous:** a constant encoding an *external* contract is worth pinning precisely because changing it is a breaking change you want caught — a type's layout/size for FFI (`assert_eq!(size_of::<Header>(), 16)`, §B25), a wire-protocol magic number or opcode, a serialized golden/snapshot (catches §B20 field-presence drift). "Does this still equal what the outside world depends on" is a real test; "does this constant equal itself" is not.
21
+ - `#[ignore]` left on a test "temporarily" to make the suite green. An ignored test is invisible to `cargo test` and rots silently — CI stays green because the test never runs. If a test must be ignored, gate it behind a named feature or document the re-enable condition.
22
+ - Tests that share mutable global state (a `static` cell, a fixed-name temp file, a hard-coded port, an env var) and pass only because of run order. `cargo test` runs tests in parallel threads by default; shared state makes them flake or clobber each other. Isolate per-test state (unique temp dirs/ports, `serial_test` for unavoidable globals).
23
+
24
+ **REQUIRED**:
25
+ - For async timing in tests, use `tokio::time::pause()` / `tokio::time::advance(Duration::from_secs(N))` — virtual time that the runtime under your control. Or use explicit synchronization (`tokio::sync::Notify`, `oneshot::channel`) signalled by the async work itself.
26
+ - Always pin `expected = "..."` substring in `#[should_panic(expected = "...")]`. The substring should be specific enough that a panic from elsewhere in the test setup does not coincidentally match.
27
+ - Every test asserts a postcondition involving the system-under-test's *observable* state — a return value, a side effect on a passed-in mock, a state transition in a fake — not just absence of panic.
28
+ - For non-deterministic systems, use `proptest` / `quickcheck` to generate inputs, and explicitly state the property being tested in the test name.
29
+ - For an `interval`-driven background task under `start_paused`: tokio auto-advances paused time only when nothing else is ready, so a *spawned* task that races other futures ticks non-deterministically. Drive it explicitly — `advance(period)` once per tick you need (the first `interval` tick fires immediately), with `yield_now().await` between advances so the spawned task runs between virtual-time steps; asserting right after a single `advance` races the spawn. (A lone interval with no competing futures auto-advances on `tick().await` and needs none of this.)
30
+
31
+ ## §D1a. Oracle validity — *the test is green because the oracle is the code*
32
+
33
+ **The trap**: §D1 covers tests that assert too little; this sub-section covers tests whose *source of truth* is wrong. Four shapes: (1) **the circular oracle** — the test (or its fixture) was written by reading the implementation, so it pins current behavior, bugs included; values generated by the code can never disagree with it. (2) **the world-erasing stub** — an in-memory fake of an I/O trait that delivers each write as exactly one read: real TCP fragments, coalesces, and partially reads (§C4), so a framing bug that only manifests on a split length-prefix is structurally unobservable through the stub. (3) **no negative control** — nobody checked that the test *fails* when the fix is reverted or the bug reintroduced; a test that passes both with and without the change under test is not evidence for it. (4) **the façade fitted to the test** — the dual of (1): there the test is written from the code, here the code is written from the test. Given the goal "make this test pass," a feature whose tests probe only a *declaration* (a config key, a magic byte, a header line, a status string) gets an implementation that emits exactly that and nothing behind it. Unlike (1)–(3) the test is valid and non-vacuous — it merely under-specifies the feature, and an LLM optimizing for green lives in the gap (Goodhart on the test suite).
34
+
35
+ **BANNED**:
36
+ - Golden/expected values produced by running the code under test and pasting its output ("snapshot-blessing" a brand-new implementation). Snapshots are valid for *regression* (pinning behavior already validated against an external reference) — not as the initial proof of correctness. The expected value must come from the spec, the reference implementation, a hand computation, or an independent oracle.
37
+ - An in-memory transport stub as the *only* test path for code whose correctness depends on transport realities: fragmentation/partial reads (§C4), interleaving, backpressure (§B14), peer stall/abort (§F3). The stub is fine for logic tests — it is banned as the sole evidence for framing/streaming code. Make the stub adversarial (split writes at every byte boundary; `proptest` over chunkings) or add one real-socket test.
38
+ - A bugfix PR whose test passes on the pre-fix code. Run the new test against the unfixed code once (mentally or actually); if it doesn't go red, it doesn't test the fix.
39
+ - Asserting *that* a collaborator was called (mock `.times(1)` and nothing else) where the contract is about *what* was passed or what state resulted — execution is not correctness.
40
+ - Treating a *declaration-only* test as evidence that a behavioral feature works. Field-observed at scale: a 360k-LOC LLM-generated Rust port of Git "supported" SHA-256 because every SHA-256 test only asserted that `init` wrote `extensions.objectformat=sha256` to the config — none of them committed or logged in that repo — so the model wrote the config key and kept doing SHA-1 internally; 99.3% of tests green, the feature absent in practice. The declaration test itself is fine (a contract pin, §D1's "contract pins are not vacuous" carve-out); *relying on it as behavioral evidence* is the defect.
41
+
42
+ **REQUIRED**:
43
+ - For every test, be able to name its oracle and the oracle's independence from the implementation: spec section, reference vector, hand-derived value, algebraic property (§F4). "The code's own output" is not on the list for new code.
44
+ - For stream/framing code: at least one test exercising adversarial chunking (write the encoded bytes one byte at a time; split the length prefix across reads).
45
+ - Negative controls where cheap: for a parser, invalid-input → `Err` cases (the decoder that never rejects); for a fix, the reproducer-turned-test that demonstrably failed before the fix (state it in the test's doc comment: `// red before <commit/fix>`).
46
+ - On review (audit mode): for each test backing a risky change, ask the counterfactual — *what mutation of the code would this test catch?* If the honest answer is "none that matters", report the test as non-evidence (same severity as the untested code path it pretends to cover). For a *feature* (not just a code path), sharpen the counterfactual against the **façade-fitted-to-the-test** shape: *would a stub that emits exactly the observables this suite checks — and nothing else — still pass?* If yes, the suite specifies a façade, not the feature; add at least one end-to-end test that **uses** the feature (for SHA-256: init → commit → read the object back), not only one that checks it was announced.
47
+
48
+ ## §D2. Integration vs unit test placement drift
49
+
50
+ **The trap**: unit tests in `#[cfg(test)] mod tests { ... }` inside `src/lib.rs` (or sibling modules) reference *crate-internal* paths — `crate::internal::do_thing()` — that work because they share the crate. Integration tests in `tests/*.rs` only see the crate's public API; they cannot reach `pub(crate)` items. When the LLM (or a refactor) moves a unit test into `tests/`, the import paths suddenly resolve differently, and the test either fails to compile, or — more insidiously — silently calls a different `do_thing` from a re-export and passes for the wrong reason.
51
+
52
+ **BANNED**:
53
+ - Moving a test from `#[cfg(test)] mod tests` to `tests/` (or vice versa) without checking that every import resolves to the same item.
54
+ - Integration tests in `tests/` that reach for `pub(crate)` items by re-exporting them through a public module just to make the test compile — this expands the public API for testing convenience and creates a semver hazard.
55
+ - Mixing both styles in one crate without a stated convention — readers cannot tell whether a `tests` directory file is the public-API exerciser or the leaked-internals tester.
56
+
57
+ **REQUIRED**:
58
+ - Unit tests for *private items* live next to the impl (`#[cfg(test)] mod tests { ... }` in the same file or sibling module). They can see `pub(crate)` and below.
59
+ - Integration tests in `tests/` exercise *only* the public API (`pub` items). They are the contract-test layer.
60
+ - If integration tests genuinely need internals, expose them under `pub(crate) mod test_support` (visible to other workspace crates if they share lineage) or gate them behind `#[cfg(feature = "test-support")]` so the feature is explicit and removable.
61
+ - Document the convention in `CONTRIBUTING.md` or the workspace `README`. Without a written convention, drift is inevitable.
62
+
63
+ ## §D3. Test/prod divergence: build profile, scale, and concurrency
64
+
65
+ **The trap**: `cargo test` runs the **debug** profile, toy data sizes, and (per test) often a single task — production runs **release**, real sizes, and real concurrency. Each axis hides a bug class: debug-vs-release — integer overflow panics in test, silently wraps in prod, and `debug_assert!` guards vanish (**§B26** owns the fixes; §D3 is the testing-side enforcement, plus the two axes below); scale — an O(n²) (§E3/§C4), a recursion depth (§B7), an allocation pattern (§E2) invisible at n=10; concurrency — a TOCTOU (§B13) or lock-order inversion (§B9) that a single-threaded test can never interleave into existence.
66
+
67
+ **BANNED**:
68
+ - Relying on a test-suite pass as evidence about overflow/`debug_assert!` behavior of the release binary — the profiles disagree by default (§B26 has the fixes: `overflow-checks = true` in release, or per-site `checked_*`).
69
+ - Testing code documented to handle "large" / "unbounded" / "attacker-sized" input only at toy sizes. At least one test at the documented scale boundary (max frame size, max depth, the size that makes O(n²) visible — n=10⁴–10⁵ is usually enough to turn quadratic into a timeout).
70
+ - Testing concurrency-bearing code (shared map, lazy init, multi-lock) exclusively through single-task tests. Use `loom` for lock/atomic protocols (§B9/§B13), or at minimum a stress test with real task parallelism and a yield-heavy schedule.
71
+ - Letting per-test timeouts or `#[ignore]` quietly absorb a scale problem ("the big test was slow so it's ignored") — that is §D1's ignored-test rot plus a buried §E3 finding.
72
+
73
+ **REQUIRED**:
74
+ - Run the test suite in release at least in CI (`cargo test --release` as a separate job) when the crate does arithmetic on untrusted/accumulating values or uses `debug_assert!` for anything load-bearing — it is the cheap way to make the tested and shipped configurations overlap.
75
+ - One boundary-scale test per documented size limit; one adversarial-depth test per recursive parser (§B7).
76
+ - For code whose correctness claim is concurrency ("thread-safe", "lock-free", "concurrent map"), the claim defines the test: `loom` model or multi-thread stress — a single-threaded green suite is silent on the claim, not supportive of it.
77
+
78
+ ## §D4. Filtering test-runner output through `grep`/`head` hides hangs
79
+
80
+ **The trap**: an agent or CI script pipes `cargo test` / `cargo nextest` through `grep`/`head` ("show me only the failures") to save context. Two silent losses follow: (1) the filter discards the per-test `SLOW`/`TIMEOUT`/`stalled` lines — the *only* lines that name a hanging test — because they are not `FAIL` lines, so a deadlock becomes invisible; (2) without `set -o pipefail` the pipeline exits with the *filter's* status (`0` when nothing matched), masking the runner's non-zero exit, so a failing or hung run reads as green. The `pipefail` foot-gun is not Rust-specific, but the agentic reflex of appending `| grep` to every test run is — and it blinds the agent to exactly the hang it most needs to see.
81
+
82
+ **BANNED**:
83
+ - `cargo test … | grep …` / `… | head` as the command whose exit status gates "tests passed", without `set -o pipefail` — the filter's status, not the runner's, then decides green/red, and `grep`-no-match returns `0`.
84
+ - Concluding "no failures" from a filtered stream that drops `SLOW`/`TIMEOUT`/`stalled` lines — absence of `FAIL` in a filtered view is not absence of a hang.
85
+ - Raising a per-test timeout (or adding `#[ignore]`, §D1) to make a `SLOW`/`TIMEOUT` line disappear — that masks a deadlock instead of fixing it.
86
+
87
+ **REQUIRED**:
88
+ - Gate on the runner directly, or `tee` full output to a file and grep the **file** — never gate on a live pipe. When a pipe is unavoidable, `set -o pipefail` first so the runner's exit status is the one that counts.
89
+ - Treat any `SLOW`/`TIMEOUT`/`stalled` line as a deadlock bug: reproduce it under parallel load (`--test-threads`, repeated runs) and fix the root cause (§B9 lock order, §B13 TOCTOU, §B3a coordinator livelock), not the symptom.
90
+
91
+ ## §D5. Windows: a hung test process wedges the next link (LNK1104)
92
+
93
+ **The trap**: on Windows a running `.exe` is locked against deletion and overwrite. When a test hangs and the harness kills the *thread* but not the *process*, the zombie lingers holding its own hashed test binary; the next build's link step then fails `LNK1104: cannot open file '…exe'` — and one flaky hang becomes a build cascade that looks unrelated to the test that caused it. This is a CI/ops failure mode rather than a code defect, but it is the direct Windows fallout of the §D4 hang, so it is enforced alongside it.
94
+
95
+ **BANNED**:
96
+ - "Fixing" a recurring `LNK1104` on a test/bench binary by retrying the build or rebooting the runner, without reaping the zombie process and fixing the underlying hang — the cascade recurs on the next hang.
97
+
98
+ **REQUIRED**:
99
+ - The root fix is the hang itself (§D4 — eliminate the deadlock). Zombie-reaping is defense-in-depth, not a substitute for it.
100
+ - On Windows CI, reap stray hashed test/bench binaries (`<crate>-<hex>.exe` — never the dash-named runtime binary) at the start of each run, so a leftover process from a prior run cannot wedge this one.
101
+
102
+ ---
103
+
104
+ ## §E6. Measure before you spend — *The cost lives in the system under load, not in the line you are reading.*
105
+
106
+ - **The discipline**: §E1–§E5 are not a mandate to optimize everything — they are a map of where systemic cost hides. Two are worth fixing on sight: algorithmic complexity (§E3) and obvious waste `clippy::perf` flags (§E2). The rest is profile-gated: confirm the hot path before trading clarity for speed.
107
+ - **The tools**: a flame graph (`cargo flamegraph`, `perf`) for CPU; an allocation profiler (`dhat`, `heaptrack`) for §E2; `tokio-console` for async stalls and lock waits (§E1, §E4); `criterion` to prove a change is faster and guard against regression. Optimize what the profile shows, not what the diff looks like.
108
+ - **Lock the win.** When a measurement justifies an optimization, guard it with a `criterion` benchmark in CI that fails on regression — a one-time result becomes a standing invariant. Without it the next refactor silently gives the speed back, and a §E regression is as invisible to `cargo test` as any Tier B bug. Bench the few paths you actually optimized, not everything — that *is* this discipline, not a contradiction of it (a benchmark of cold or trivial code is its own coverage theater, §D1).
109
+ - **Leave it when**: always, until a measurement or clear algorithmic argument justifies the change. A micro-optimization on a cold path is the noise this document exists to prevent — the 🟡/🟢 discipline, applied to speed.
110
+ - 🟡 — the binding law of this tier.
111
+
112
+ ---
@@ -0,0 +1,106 @@
1
+ # Rust Intel — Unsafe, UB, Memory & FFI
2
+
3
+ > Module of the **rust-intel** skill. Core — operating mode, blocking protocol, enforcement tiers, the trigger table, version pins, and the category→module map — lives in `SKILL.md`. This module holds the category bodies for §B5, §B7, §B18 (and §B18a), §B25. Tier labels (🔴/🟡/🟢; A–F) and all cross-references are preserved verbatim.
4
+ > **Tiers in this module:** §B5 🔴 · §B7 🟡 · §B18 🔴 · §B18a 🔴 · §B25 🔴. Derived from SKILL.md → Enforcement tiers (canonical).
5
+ > **Audit semantics:** 🔴 = report every occurrence; 🟡 = write-time discipline — report only load-bearing/non-obvious cases; 🟢 = clippy's, don't hand-report. Audit the *artifact* (a BANNED pattern present, a REQUIRED code artifact absent); process-REQUIREMENTs ("propose first", "ask the user") are not auditable findings.
6
+
7
+ ---
8
+
9
+ ## §B5. Unsafe that looks safe (high UB rate in small-N studies)
10
+
11
+ **The trap**: code passes review and tests because UB doesn't manifest on typical inputs. In the small-N audit cited in [`docs/sources.md`](docs/sources.md), out of 40 LLM-generated `unsafe` blocks: 13 were UB on any input, 9 were UB on specific inputs (alignment, OOB, Stacked Borrows violations), 18 were correct — i.e. 22/40 (~55%) exhibited UB. The *exact rate* is directional (small sample, not stratified by model or domain), but the *pattern* — that LLM-generated `unsafe` is significantly more dangerous than LLM-generated safe code — is consistent across every published audit to date. Treat any LLM-generated `unsafe` block as high-risk until proven otherwise via miri + manual invariant audit.
12
+
13
+ **BANNED**:
14
+ - `std::ptr::read(p)` / `*p` / `&*p` where the source pointer's alignment is not statically known to match `T`. Use `std::ptr::read_unaligned` / `std::ptr::write_unaligned` instead (`<[T]>::align_to::<U>` is itself `unsafe` and requires the same `Pod`-style invariants as `transmute` — do not list it in the "safe alternatives" bucket).
15
+ - `transmute` between types whose layouts aren't both `#[repr(C)]` (or another pinned repr — `#[repr(transparent)]`, `#[repr(u32)]`, `#[repr(packed)]`, etc.). The default layout (`#[repr(Rust)]`) does not guarantee field order, padding, or stability across compiler versions, so the bytes may not be reinterpretable as the target type — the `transmute` is UB in practice. The attribute `#[repr(Rust)]` itself is stable (it is the default and can be written explicitly); the *layout it implies* is unspecified.
16
+ - Any `unsafe` block without `// SAFETY:` preceding it that names every invariant.
17
+ - Creating a `&mut T` from a `*mut T` while another reference to the same data still exists (Stacked Borrows violation, caught by miri).
18
+ - `mem::uninitialized::<T>()` — deprecated since Rust 1.39 (November 2019) precisely because it is **instant UB for any type with invariants**. The function returns an "undef" value the optimizer is free to assume is initialized; for `bool`, `&T`, `Box<T>`, `NonZero*`, and any enum with restricted discriminants, the call is UB on the very next read. Use `MaybeUninit::<T>::uninit()`.
19
+ - `mem::zeroed::<T>()` for any `T` whose all-zero bit pattern is not a valid value: `bool`, `&T`, `&mut T`, `Box<T>`, `NonZero*`, function pointers, enums whose discriminants do not include 0, and `#[repr(transparent)]` wrappers over any of the above. The function compiles for *every* `T` regardless of whether zero is a valid bit pattern; the compiler will not stop the misuse.
20
+ - Marking a public function `pub fn` when its contract actually requires invariants from the caller. If the caller must uphold something for safety, it is `pub unsafe fn`.
21
+
22
+ **REQUIRED**:
23
+ - `// SAFETY:` comment listing each invariant in the form `// SAFETY: ptr is valid for reads of size_of::<T>(), is properly aligned (allocated via Layout::new::<T>()), and outlives this borrow.`
24
+ - Add miri to CI for files containing `unsafe`: `cargo +nightly miri test`. Yes, 10× slower; the one UB caught pays for it.
25
+ - Default to safe abstractions (`zerocopy::FromBytes`, `bytemuck::Pod`/`bytemuck::cast_slice`, `slice::chunks_exact`) before reaching for raw pointers. (`bytes::Bytes` is a refcounted *buffer container*, not a safe-transmute abstraction — don't confuse the two.)
26
+ - Note: `<[T]>::align_to::<U>` is itself `unsafe` and requires the same `Pod`-style invariants as `transmute`; use it only when `bytemuck`/`zerocopy` aren't available, and with a `// SAFETY:` block per the rule above.
27
+ - **`MaybeUninit<T>` discipline**: for any value initialized piecewise (field-by-field) or by an FFI/`unsafe` call writing into Rust memory, use `MaybeUninit::<T>::uninit()` (or `MaybeUninit::<T>::zeroed()` when zero is a valid bit pattern for `T`), write every byte/field, then call `.assume_init()`. Never call `.assume_init()` while any portion of `T` is still undef. The legacy `mem::uninitialized` / `mem::zeroed` paths are BANNED above for the same reason: they bypass this discipline and produce UB on the first read.
28
+ - **Strict provenance**: when a pointer genuinely encodes its address as an integer (pointer tagging, free-lists, XOR-linked structures), use the strict-provenance API (`ptr.with_addr(addr)`, `ptr.addr()`, `ptr.map_addr(|a| …)`, `ptr.expose_provenance()`, `core::ptr::with_exposed_provenance::<T>(addr)`, `core::ptr::without_provenance::<T>(addr)`) rather than `ptr as usize` / `usize as *const T` round-trips — the cast form loses provenance that miri can otherwise verify, and is a warning under strict-provenance lints / unsound under sanitizers. For an ordinary pointer cast with no address↔integer round-trip, plain `as` is idiomatic and fine — this is not a blanket "replace every cast". (All of these are stable since Rust 1.84.)
29
+ - **`slice::from_raw_parts(data, len)` invariants** — list before calling: `data` is non-null, properly aligned for `T`, points to `len` consecutive properly-initialized `T`s, the memory it points to is not mutated for the lifetime of the returned slice, and `len * size_of::<T>() <= isize::MAX`. Mirror for `from_raw_parts_mut`, plus exclusive access. Every call site states each invariant in its `// SAFETY:` block.
30
+ - For FFI: every `extern "C"` function takes/returns `#[repr(C)]` types only. `String` and `Vec` cannot cross the FFI boundary; use `CString` / `*const c_char` for strings, and for buffers decompose to `(ptr, len, cap)`: on a project with MSRV ≥ 1.93 prefer `Vec::into_raw_parts` (stable as of Rust 1.93 if confirmed by your toolchain); below 1.93 use the manual decomposition via `ManuallyDrop<Vec<T>>` (stable since 1.0). See §B25 for the full FFI ownership discipline.
31
+
32
+ **The unsafe→safe boundary: guard value-invariants, prove relational ones.** When `unsafe` produces a value that flows into safe code, split what must hold about it into two kinds — they need opposite defenses:
33
+ - **Value-invariants** (a bit pattern, an integer range, an enum discriminant, a length, an address's alignment, UTF-8 well-formedness): runtime-checkable. The `unsafe` must emit the value through a **total fallible constructor** — `&[u8] → Result<Typed, _>` — that runs the check and returns `Err` on bad input. It must **never panic on adversarial input** (no `unwrap`/`expect`/`assert!` at the boundary — that turns a soundness check into a remote DoS; see §B7/§C2). Canon: `str::from_utf8`, `char::from_u32`, `TryFrom` (§B26), `zerocopy::TryFromBytes`. Crucial ordering: the guard runs on the **raw representation (bytes) *before* the typed value is minted** — `validate(&[u8]) -> Result<&T>`, never `let t: T = transmute(bytes); check(t)`. The second form's *read* of an invalid `T` (an invalid `bool`, a bad discriminant, a dangling `&`) is itself the UB you were trying to guard against. For types where *every* bit pattern is valid (`bytemuck::Pod`) there is nothing to check — the compile-time `Pod` bound is the guard.
34
+ - **Relational invariants** (lifetime validity, aliasing/exclusivity, pointer provenance, "points to `N` initialized `T`s", `Send`/`Sync`): **not** runtime-checkable. There is no `fn is_not_dangling(p: *const T) -> bool` — to "check" you would dereference, which is the use-after-free. So there is **no downstream guard**; the only defense is the *upstream* proof discharged before the `unsafe` (the `// SAFETY:` contract) plus the type's variance and marker traits (see §B18, §B18a). Do not pretend a downstream check exists — for this class it cannot.
35
+
36
+ ## §B7. Large stack allocations and arena pitfalls
37
+
38
+ **The trap**: `[u8; 1_048_576]` on the stack overflows in debug builds. `Box::new([0u8; N])` constructs on the stack first and may overflow before placement-new optimizations kick in (release-mode dependent, never reliable).
39
+
40
+ **The right threshold to think in**: not page size (4 KiB), but the actual stack budget of the executing thread. Default stack budgets are **8 MiB on Linux for the main thread**, **2 MiB on `std::thread::spawn`-ed threads**, and **2 MiB on the tokio worker thread a task runs on** — tasks are *not* given their own stack, they share the worker's, so a deep async call chain competes for that one budget (configurable via `Builder::stack_size` and `tokio::runtime::Builder::thread_stack_size` respectively, but the defaults are the budget LLM-generated code actually runs against). Practical **default guideline**: keep routine stack values below ~64 KiB; this is a guideline, not a hard line — a single large value on a fresh main-thread frame with a 2–8 MiB budget is fine. It escalates to a real defect (see BANNED) when the large frame sits under recursion, a deep call chain, or a spawned task on a reduced stack, where the cumulative depth — not the single frame — overflows.
41
+
42
+ **BANNED**:
43
+ - `[T; N]` whose total size is a meaningful fraction of available stack: a large array (well past the ~64 KiB guideline, or any size that is a real fraction of the thread's budget) passed/returned by value through a function chain, **and especially** one that recurs or lands on a spawned task's reduced stack — there the per-frame size multiplied by depth overflows. A single large array on one shallow main-thread frame is the guideline case above, not an automatic ban.
44
+ - `Box::new([0u8; N])` for any N at risk — this is the trap: the array is built on the stack first and *then* moved to the heap, so the stack-overflow window is unchanged from the by-value form. Use `vec![0u8; N].into_boxed_slice()` or `Box::<[u8]>::new_uninit_slice(N)` instead.
45
+ - Recursive functions with large local arrays.
46
+ - Unbounded recursion **depth** over input you don't control — a recursive-descent parser, tree/JSON/expression walk with no depth limit overflows the stack on deeply-nested input. A stack overflow is `SIGSEGV`/abort, **not** a catchable panic, so it is a clean DoS vector for parsers of untrusted data. (Distinct from the frame-size trap above: here each frame is small but the depth is unbounded.)
47
+ - `Vec::with_capacity(n)` / `vec![0u8; n]` / `String::with_capacity(n)` / `Vec::reserve(n)` where `n` is a length/count from untrusted input (a wire length-prefix, a `Content-Length`, a parsed count field). An attacker sends `n = u32::MAX` and the single preallocation exhausts memory → OOM/abort, a clean remote DoS. Distinct from the stack traps above: the allocation is on the heap but the *size* is attacker-chosen. Compiles; tests on small `n` pass.
48
+
49
+ **REQUIRED for heap-allocated buffers**:
50
+ - `vec![0u8; N].into_boxed_slice()` — guaranteed heap, zero-initialized, stable Rust.
51
+ - `Box::<[u8]>::new_uninit_slice(N)` (stable since Rust 1.82, October 2024) + `.assume_init()` — when zero-initialization is wasted work and you will fully overwrite the buffer. `assume_init` is `unsafe`; gate it with a `// SAFETY:` block per §B5. **Only** sound when every byte is provably written before any read — e.g. `read_exact` of exactly `N` bytes. For the common `socket.read(&mut buf)` pattern that returns `n < N`, the uninitialized tail must never be sliced/read (that is UB); use the zero-initialized `vec![0u8; N]` form for partial-read buffers.
52
+ - `bytes::BytesMut::zeroed(N)` for buffers headed into `tokio::io`.
53
+ - For recursion over untrusted/unbounded input, enforce an explicit depth limit (a `depth: u32` parameter checked against a max), or rewrite iteratively with an explicit `Vec` stack.
54
+ - Clamp any allocation size derived from untrusted input to a sane maximum before allocating (`if n > MAX_LEN { return Err(...) }`), and prefer incremental growth or `Read::take(limit)` / `Iterator::take(limit)` over preallocating to a wire-supplied size. Reserve `with_capacity(n)` for an `n` you computed or bounded yourself.
55
+
56
+ ## §B18. Manual `unsafe impl Send` / `unsafe impl Sync`
57
+
58
+ **The trap**: a type contains a `*const T`, a `*mut T`, a raw FFI handle, or a `Rc<T>` field, and `tokio::spawn` rejects it with a `Send` bound error. The LLM's reflexive fix is `unsafe impl Send for MyType {}` — and that compiles, and tests pass under low concurrency. Under contention, the un-synchronized access races. This is one of the most reliably-wrong fixes the LLM makes: it converts a correct compile-time refusal into a runtime data race.
59
+
60
+ **BANNED**:
61
+ - `unsafe impl Send for MyStruct {}` or `unsafe impl Sync for MyStruct {}` without a `// SAFETY:` block that names the synchronization invariant (which lock, atomic, or external invariant makes the impl sound).
62
+ - Manual `Send`/`Sync` for a type containing `*mut T` without proving that aliasing is controlled by external synchronization (e.g., the pointer is only ever read after the producing thread has joined, or access is guarded by an external `Mutex`).
63
+ - Manual `Send` for a type whose field is `Rc<T>` "because the Rc is never cloned across threads in practice" — if the field is morally `Arc`, fix the field, do not lie via `unsafe impl Send`.
64
+
65
+ **REQUIRED**:
66
+ - Every `unsafe impl Send` / `unsafe impl Sync` cites the synchronization primitive or invariant that makes the impl sound, in a `// SAFETY:` block per §B5. If there is no primitive — no `Mutex`, no atomic ordering, no thread-local lifetime restriction — the impl is wrong; refactor the type instead.
67
+ - For `*const T` / `*mut T` fields, `NonNull<T>` is a better field type for the null-niche and variance, but note it is `!Send`/`!Sync` exactly like the raw pointer — it does **not** make the type `Send`/`Sync` and does not close the race. The actual fix is to wrap the handle in an `Arc<Mutex<RawHandle>>` (or other real synchronization) and impl `Send`/`Sync` on the wrapper with the lock as the cited invariant — or, for the producer-then-consumer case, cite an explicit happens-before (the producing thread is `join`ed before any read).
68
+ - Surface every manual `unsafe impl Send` / `unsafe impl Sync` in the post-flight summary.
69
+
70
+ ## §B18a. Variance and `PhantomData` soundness in raw-pointer wrappers
71
+
72
+ **The trap**: a hand-written type holding a `*const T` / `*mut T` / `NonNull<T>` (or otherwise built on internal `unsafe`) gets its **variance**, its **drop-check**, or its **auto-traits** wrong via the wrong `PhantomData` — or via no `PhantomData` at all. This is the relational-invariant class of §B5 made concrete: the hole is **not in any `unsafe` block**. There is no `unsafe` token at the site that decides variance — variance is implied by the type's fields and chosen *silently* by the compiler, so a block with a flawless `// SAFETY:` comment can still be unsound because the **type declaration** is wrong. A per-block `// SAFETY:` (§B5) cannot catch it; that is why this needs its own check. In *safe* Rust this is impossible — the compiler always derives variance correctly; the hazard exists only once a raw pointer enters the type.
73
+
74
+ **Why it bites**: if a wrapper that hands out `&mut T` is **covariant** in `T` (because it stores `*const T`, or `PhantomData<&'a T>`, or `PhantomData<fn() -> T>`), a caller can substitute a shorter or longer lifetime where the type required invariance — producing a dangling `&mut` and a use-after-free **with no `unsafe` at the call site**. Symmetrically, wrong drop-check (a missing `PhantomData<T>` on a type that owns a `T` through a raw pointer) lets a `Drop` read freed data; and a raw pointer is `!Send`/`!Sync` by default, so a wrong `unsafe impl Send`/`Sync` lies about it (that half is §B18).
75
+
76
+ **REQUIRED** for any type holding a raw pointer / `NonNull` / using internal `unsafe`:
77
+ - Choose `PhantomData` to match the *real* ownership and access:
78
+ - `PhantomData<T>` — **covariant** in `T`, plus drop-check ownership (the compiler assumes the type may drop a `T`, so `T` must outlive it). Use when the type **owns** the data behind the pointer but does not need invariance — i.e. it never hands out `&mut T` through a covariance-sensitive path. This is the `Vec<T>` / `Box<T>` pattern.
79
+ - `PhantomData<&'a T>` — **covariant** in both `T` and `'a`; a shared, read-only view. The default starting point for non-owning wrappers.
80
+ - `PhantomData<&'a mut T>` — **invariant** in `T`, covariant in `'a`. Use when the type hands out `&mut T` or otherwise requires that callers cannot substitute a sub/supertype of `T`. This is the critical choice that prevents the covariance→UAF described above.
81
+ - If you need both ownership (drop-check) **and** invariance, use both: `PhantomData<T>` for the drop-check + `PhantomData<fn(T)>` (contravariant in `T`) or `PhantomData<Cell<T>>` (invariant in `T`) to force invariance. `PhantomData<T>` alone is **not** invariant — using it when invariance is required is the exact unsoundness this section exists to prevent.
82
+ - `PhantomData<fn(T) -> T>` — invariant in `T`, `Send + Sync` regardless of `T`, no drop-check. Correct only for the rare marker case; never as a reflex.
83
+ - State the chosen variance and *why* in the type's doc / `// SAFETY:` comment — variance is an invisible part of the type's contract, so write it down like any other invariant.
84
+ - Where variance is load-bearing for soundness, add a `compile_fail` doctest that attempts the illegal lifetime substitution, so a future edit that flips variance is caught by the test suite, not by a user's UAF.
85
+ - `NonNull<T>` is the right field type for the null-niche, but note it is `!Send`/`!Sync` **and covariant** in `T` — it does not make the variance decision for you. Prefer audited safe abstractions (`bytemuck`/`zerocopy`) over re-deriving variance by hand wherever they fit.
86
+
87
+ 🔴 — surface every hand-rolled raw-pointer wrapper's variance / `PhantomData` / auto-trait decision in the post-flight summary, alongside the §B18 manual-`Send`/`Sync` items.
88
+
89
+ ## §B25. Panic and ownership across `extern "C"` ABI
90
+
91
+ **The trap**: the LLM writes a Rust function callable from C (or returns a Rust-owned value through FFI), tests it on the happy path, and ships. The panic path is never exercised, the C-side `free()` path is never exercised, and the `cap`-mismatched `from_raw_parts` is never exercised. Everything compiles, the happy-path tests pass, and the bug surfaces under load as heap corruption (silent, worst), a process abort with no Rust stack (visible, but unhelpful), or a leak that grows for days. The compiler does not catch any of these — the `unsafe` boundary is precisely where its guarantees end.
92
+
93
+ **BANNED**:
94
+ - `extern "C" fn` body that can panic without being wrapped in `std::panic::catch_unwind`. Pre-1.81 a panic crossing an `extern "C"` boundary was UB; since Rust 1.81 the default is to abort the process — neither is what an FFI caller expects. Convert panics to a stable error code at the boundary.
95
+ - Passing a `Box<T>`, `Vec<T>`, `String`, `Rc<T>`, or `Arc<T>` directly as a parameter or return value of an `extern "C"` function — these types have no stable ABI and the layout can change between compiler versions. Cross the boundary as `*mut T`, `(*mut u8, usize, usize)`, or `*const c_char`.
96
+ - `Box::from_raw(ptr)` on a pointer not originally produced by `Box::into_raw` in the same Rust binary with the same global allocator. Reclaiming a `malloc`'d pointer through `Box::from_raw`, or freeing a `Box::into_raw`'d pointer with C's `free()`, is allocator-mismatch UB.
97
+ - `Vec::from_raw_parts(ptr, len, cap)` where `cap` does not match the value the source `Vec` was decomposed with — the eventual deallocation passes the wrong size to the allocator and corrupts the heap. Same hazard for `String::from_raw_parts`.
98
+ - Returning a raw pointer from an `extern "C"` function without an accompanying `extern "C" fn drop_T(p: *mut T)` (or equivalent) that the documented contract requires the C side to call. "The caller will know to free it" is wishful thinking that compiles and tests fine.
99
+ - `#[no_mangle]` on functions that are not actually FFI entry points. Each such symbol is a candidate for silent linker collisions across crates.
100
+
101
+ **REQUIRED**:
102
+ - Wrap every panic-capable Rust function callable from C with `std::panic::catch_unwind`; translate `Err(payload)` into a stable error code (a tagged union, a `-1` sentinel, an out-parameter for `*mut PanicInfo`) that the C side can match on. Document the encoding on the function's doc comment. **Precondition:** `catch_unwind` catches only an *unwinding* panic — under `panic = "abort"` the process aborts before the catch runs, so the boundary is unprotected; the FFI guard assumes `panic = "unwind"` (and you may compile this crate with `panic = "unwind"` even when the rest aborts). `catch_unwind` requires an `UnwindSafe` closure — most FFI bodies are, but if you reach for `AssertUnwindSafe`, confirm no caller observes broken invariants after the catch rather than wrapping blindly.
103
+ - For every Rust-owned type `T` that crosses the boundary, ship a paired `extern "C" fn rust_drop_T(p: *mut T)` performing `unsafe { let _ = Box::from_raw(p); }`. Document the contract on both functions: caller owns the pointer until it calls `rust_drop_T`; the C side must not call `free()` on this pointer, ever.
104
+ - For `Vec<T>` crossing the boundary, decompose with `ManuallyDrop<Vec<T>>` (MSRV-safe) or `Vec::into_raw_parts` (MSRV ≥ 1.93, if confirmed by your toolchain) and ship the tuple as three values, plus a paired free function. Document that the C side must pass *all three* back unchanged to release the buffer.
105
+ - For every `#[repr(C)]` struct crossing the boundary, verify the layout against the C header. On nightly: `cargo +nightly rustc --lib -- -Zprint-type-sizes` prints field-by-field sizes and offsets for every type in the crate. On stable, write a unit test that asserts `std::mem::size_of::<MyStruct>()`, `std::mem::align_of::<MyStruct>()`, and `std::mem::offset_of!(MyStruct, field)` against the values expected by the C side. If you use `bindgen`, pin its output (commit the generated file) so changes show up in diff review. Field order, padding, and alignment must match the C side byte-for-byte.
106
+ - Add miri to CI for every file containing `extern "C"` blocks, exactly as §B5 requires for any `unsafe` — but note miri **interprets Rust, it does not run native code**: it cannot execute a real `extern "C"` call into a C library (it aborts with "unsupported operation: can't call foreign function" unless a shim exists). Point miri at the **Rust-side** of the boundary (pointer provenance, `#[repr(C)]` layout, the `from_raw_parts` / `Box::from_raw` ownership invariants) and isolate the actual foreign call behind `#[cfg(not(miri))]` (or a `#[cfg(miri)]` mock) so CI exercises what miri can verify instead of failing on the unsupported call.