Claude Skill

sota-rust

State-of-the-art Rust engineering (2026) for writing and auditing Rust code. Covers idiomatic ownership and API design, error handling and panic policy, unsafe discipline with Miri, async/tokio (cancellation safety, structured concurrency, graceful shutdown), security and supply

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download martinholovsky-SOTA-skills-skills_sota-rust-ec2abf6.zip · 54 KB
Part of martinholovsky/sota-skills — 39 skills

Install

skills CLI npx skills add https://github.com/martinholovsky/SOTA-skills/tree/main/skills/sota-rust
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install martinholovsky-sota-skills@llmmart
Git git clone https://github.com/martinholovsky/SOTA-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole martinholovsky/sota-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SOTA Rust (2026)

Purpose

This skill encodes the 2026 state of the art for production Rust: the idioms, security posture, performance discipline, and CI baseline expected of an expert Rust codebase. Baseline as of mid-2026: a recent stable Rust toolchain (verify the current release at blog.rust-lang.org), edition 2024 (next edition expected ~2027), tokio still 1.x. It serves two modes — BUILD (write new code to this standard) and AUDIT (find where existing code falls short, with severity and evidence). The detailed rules live in rules/*.md; load only the files relevant to the task (see index below). Every rules file ends with an "Audit checklist" of grep/clippy patterns — use those verbatim in AUDIT mode.

BUILD mode

When writing or modifying Rust code:

  1. Scope the work, load the rules. Pick the relevant rules/ files from the index. Touching async code? Load 04. Adding a dependency, parsing network input, or spawning an external program? Load 05. Writing any unsafe? Load 03 — no exceptions.
  2. Design types first. Newtypes for domain primitives, errors per subsystem (thiserror for libs, anyhow for apps), ownership tree before Arc<Mutex<_>>, public API minimal and borrowed (&str/&[T] params). Parse, don't validate: constructors enforce invariants.
  3. Write to the non-negotiables (bottom of this file) without being asked. They are defaults, not suggestions; deviations carry a written justification at the site (e.g. expect with invariant message, #[allow(lint, reason = "...")]).
  4. Wire the scaffolding with the code, not after: lints in [lints], deny.toml + cargo deny in CI for anything deployed, Miri job if unsafe exists, nextest, MSRV declared and tested, benches for claimed-hot paths. See rules/07 §9 for the CI shape to copy.
  5. Verify before claiming done: cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, cargo nextest run + cargo test --doc, and cargo doc warning-free for libraries. If you wrote unsafe: cargo +nightly miri test over it. If you claimed performance: show the benchmark.
  6. Comment intent at decision points the next reader will question: justified clones, cancel-safety of select! arms, SAFETY comments, channel-capacity choices, poisoning policy.

AUDIT mode

When reviewing or auditing existing Rust:

  1. Recon first: cargo metadata/workspace layout, Cargo.toml profiles and features, CI config, rg 'unsafe' --count-matches, dependency tree (cargo tree -d). This decides which rules files to load and where risk concentrates (network input? unsafe? async service?).
  2. Run the audit checklists at the end of each loaded rules file — they are ordered grep/clippy hunts with pre-calibrated severities.
  3. Validate every finding: read the surrounding code; a grep hit is a lead, not a finding. Confirm reachability (is the unwrap on an attacker-influenced path?) before assigning severity.
  4. Report with the finding format below. Prefer few, true, prioritized findings over volume. Note positive observations where the code is already SOTA (prevents "fixes" that regress good decisions).

Severity conventions

Severity Meaning Examples
Critical Exploitable now, or UB reachable UB, unsound safe API, SQLi/path traversal, authn bypass, unwinding across FFI, secrets in logs+repo
High Exploitable under realistic conditions, or correctness loss attacker-reachable panic/OOM (DoS), wrapped arithmetic on untrusted lengths, cancellation data loss, deadlock (block_on in async, lock across await), unbounded channels fed by network, missing dep-audit in deployed-service CI
Medium Latent defect or eroded defense missing SAFETY comments, no Miri CI on unsafe crate, swallowed errors (.ok(), filter_map(Result::ok)) uncommented, untested MSRV, non-additive features, orphaned spawned tasks
Low Hygiene, idiom, maintainability clone-to-satisfy-borrowck, index loops, missing #[non_exhaustive], missing # Errors docs, blanket #[allow] without reason

Severity scales with reachability (attacker-controlled > user > operator > build-time) and blast radius (process death > request failure > slow).

Finding format

[SEVERITY] short title
  Where: path/to/file.rs:123 (fn name / module)
  What:  the defect, in one or two sentences
  Why:   concrete consequence (exploit path, failure mode, cost)
  Fix:   specific change — code sketch or named pattern from rules/NN
  Effort: trivial | small | medium | large
  Refs:  rules/NN §M; clippy lint or RUSTSEC id if applicable

Group findings by severity, Critical first. End with: checklist coverage (which rules files were applied), what was not reviewed, and quick wins (one-line fixes with outsized value).

Rules index

File Read this when...
rules/01-ownership-and-api-design.md Designing structs/traits/modules/workspaces; fighting the borrow checker; deciding clone vs borrow vs Rc/Arc; newtype, typestate, builder patterns; sealed traits, coherence; comparison-trait (Eq/Ord) invariants; exhaustive matching; iterator-chain idioms
rules/02-errors-and-panics.md Choosing thiserror vs anyhow/eyre; designing error enums; unwrap/expect policy and invariant messages; context discipline; panic policy for servers, FFI, and Drop (no panic in destructors); Option/Result combinator flow; not unwrapping an Option back into a sentinel (unwrap_or(-1), serde(default) on numbers)
rules/03-unsafe-discipline.md Writing or reviewing ANY unsafe; SAFETY comment standards; UB catalog (aliasing, uninit, transmute, FFI lifetimes); Miri/sanitizers/loom in CI; cargo-geiger; soundness review protocol
rules/04-async-tokio.md Anything async: tokio, spawn vs spawn_blocking, Send/Sync bound errors, select! and cancellation safety, JoinSet/TaskTracker, channel selection, locks across await, async traits, graceful shutdown
rules/05-security-supply-chain.md Network-facing or deployed code; adding dependencies; cargo audit/deny/vet; integer overflow on untrusted input; panic-DoS; zeroize/constant-time for secrets; serde hardening (untagged enums, size limits); service-edge defaults; spawning external programs (std::process::Command — argv vs shell, the Windows .bat CVE, a dropped Child that keeps running, deadlines that don't kill, §9)
rules/06-performance.md Performance work or claims: profiling (samply/perf/flamegraph, criterion/divan), allocation reduction (Cow/SmallVec/buffer reuse), accidental clones, iterator fusion, release profile (LTO, codegen-units, panic=abort), PGO
rules/07-tooling-ci.md Setting up or auditing repo scaffolding: clippy policy and pedantic triage, rustfmt, nextest, MSRV declaration+testing, additive feature flags, docs.rs discipline, edition 2024 migration, CI baseline. Test strategy — suite shape, TDD, doubles, test data, flake policy — lives in sota-testing; load it for any build that writes logic. This file owns Rust runner mechanics only.

Top-10 non-negotiables

  1. No unwrap()/bare expect() on production paths. Propagate with ? + context; expect("...") only with a message proving the invariant. An attacker-reachable panic is a DoS. (rules/02)
  2. Every unsafe block has a // SAFETY: comment discharging the called API's documented preconditions, and lives behind a sound safe abstraction. Unsafe code without Miri in CI is unaudited code. (rules/03)
  3. Libraries: thiserror enums with #[source] chains. Applications: anyhow with .context(). Never anyhow::Error in a public lib API; never silently swallowed errors. (rules/02)
  4. Never block the async runtime: no sync I/O, std::thread::sleep, or sustained CPU inside async fn; spawn_blocking or a compute pool. No std::sync::MutexGuard held across .await. (rules/04)
  5. Every select!/timeout/abort path is cancellation-reviewed: futures dropped at any .await; cancel-unsafe ops don't go in select! arms; invariants spanning awaits get drop guards. Spawned tasks are owned (JoinSet/TaskTracker), never orphaned. (rules/04)
  6. Untrusted input gets checked arithmetic, size limits, and depth limits: checked_*/try_into on lengths (release mode wraps silently), body-size caps before parsing, no #[serde(untagged)] or uncapped with_capacity on hostile data. (rules/05)
  7. Supply chain is CI-enforced: cargo deny/cargo audit on PRs + scheduled, Cargo.lock committed, new deps vetted (cargo vet or documented review), git deps pinned by rev. (rules/05)
  8. Secrets are typed (SecretString/Zeroizing), redacted from Debug/logs, and compared in constant time (ct_eq). Key material only from OS randomness. (rules/05)
  9. Don't clone to satisfy the borrow checker; don't take owned params you only read. &str/&[T] in signatures, split borrows, mem::take; newtypes over primitive obsession; exhaustive matches (no lazy _ => on owned enums). (rules/01)
  10. CI gate: fmt + clippy -D warnings (triaged pedantic) + nextest + doctests + MSRV job + feature-matrix check. Performance claims require benchmarks; release profile (LTO/codegen-units/panic strategy) is a deliberate, documented choice. (rules/06, 07)
Files (sota-skills)
  • rules
    • 01-ownership-and-api-design.md 14 KB
      # 01 — Ownership, Borrowing & API Design
      
      Idiomatic ownership is the difference between Rust that fights you and Rust that
      documents itself. These rules cover borrow patterns, smart-pointer selection,
      type-driven design (newtype, typestate, builder), trait design, and code layout.
      
      ## 1. Borrowing & clone discipline
      
      **Never clone to satisfy the borrow checker.** A `.clone()` whose only purpose is
      to silence E0502/E0505 is a design smell: restructure scopes, split borrows, or
      take ownership earlier.
      
      ```rust
      // BAD: clone because `self.items` is borrowed while mutating
      let names: Vec<String> = self.items.iter().map(|i| i.name.clone()).collect();
      for name in names { self.register(&name); }
      
      // GOOD: split the struct so disjoint fields borrow independently
      let (items, registry) = (&self.items, &mut self.registry);
      for item in items { registry.register(&item.name); }
      ```
      
      - Split-borrow via destructuring or accessor methods returning `(&A, &mut B)`.
      - For "clone before loop" patterns over small data, `Copy` types or indices are
        cheaper than cloning `String`/`Vec`.
      - `std::mem::take` / `mem::replace` extract owned values from `&mut self`
        without cloning (leave a cheap default behind).
      - Legitimate clones exist: crossing thread/task boundaries, caching, fan-out.
        Comment intent when a clone looks gratuitous: `// clone: sent to spawned task`.
      
      **Accept the most general borrowed form in APIs:**
      
      ```rust
      // BAD: forces callers to own/allocate
      fn process(s: String, items: Vec<u32>) -> usize { ... }
      
      // GOOD: borrow; caller keeps ownership
      fn process(s: &str, items: &[u32]) -> usize { ... }
      ```
      
      - Parameters: `&str` over `&String`, `&[T]` over `&Vec<T>`, `&Path` over
        `&PathBuf`, `impl AsRef<Path>` / `impl Into<String>` when ergonomics matter
        (but not on hot generic-bloat-sensitive paths — use inner non-generic fn).
      - Take ownership (`String`, `Vec<T>`) only when you store the value. Taking
        `&str` then immediately `.to_owned()` forces a copy the caller may have been
        able to move; prefer `impl Into<String>` there.
      - Return `&str`/`&[T]` borrowed from `self` where lifetimes allow; `Cow<'_, str>`
        when sometimes-owned (see rules/06 for perf framing).
      
      ## 2. Rc/Arc/Box/RefCell: when shared ownership is right
      
      Reach for smart pointers in this order; each step is a justified escalation:
      
      | Need | Tool |
      |---|---|
      | Heap allocation / unsized / recursion | `Box<T>` |
      | Shared ownership, single-threaded | `Rc<T>` |
      | Shared ownership across threads | `Arc<T>` |
      | Shared + interior mutation, single-threaded | `Rc<RefCell<T>>` |
      | Shared + mutation, multi-threaded | `Arc<Mutex<T>>` / `Arc<RwLock<T>>` |
      | Shared, replace-on-write, read-mostly | `Arc<T>` + swap (`arc-swap`) |
      
      - `Arc<Mutex<T>>` everywhere is Java-in-Rust. Prefer **ownership trees**
        with message passing (channels) for cross-task state; share only what is
        genuinely shared (config, caches, connection pools).
      - `Rc::clone(&x)` / `Arc::clone(&x)` (associated form) — makes "refcount bump,
        not deep copy" greppable.
      - Break `Rc`/`Arc` cycles with `Weak` (parent links, observer lists). Audit any
        graph-shaped `Rc<RefCell<...>>` for leaks.
      - `RefCell` panics at runtime on double-borrow; confine it to module-private
        state with documented borrow discipline. Never let `RefCell` borrows escape
        across a callback into user code.
      - Immutable shared config: `Arc<Config>` (no lock). Read-mostly: `RwLock` only
        if writes are rare AND readers are many; otherwise `Mutex` (simpler, no
        writer starvation surprises).
      
      ## 3. Newtype pattern
      
      Wrap primitives that have semantics. `u64` is not a `UserId`.
      
      ```rust
      // BAD
      fn transfer(from: u64, to: u64, amount: u64) -> Result<(), Error>
      
      // GOOD
      #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
      pub struct AccountId(u64);
      #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
      pub struct Cents(u64);
      fn transfer(from: AccountId, to: AccountId, amount: Cents) -> Result<(), Error>
      ```
      
      - Validate at construction: `impl TryFrom<&str> for Email` returning a typed
        error; keep the field private so the only way in is the validated path
        ("parse, don't validate").
      - Derive the full expected set (`Debug, Clone, Copy, PartialEq, Eq, Hash`,
        plus `serde` derives behind a feature) — missing derives are friction that
        pushes callers back to raw primitives.
      - Implement `Deref` to the inner type **only** for true smart-pointer-like
        wrappers; for domain newtypes expose explicit `as_str()`/`get()` instead
        (Deref leaks the abstraction).
      - Newtypes are also the coherence escape hatch: wrap a foreign type to impl a
        foreign trait (orphan rule).
      
      ## 4. Typestate pattern
      
      Encode protocol/state machines in the type system so invalid transitions don't
      compile. Use when misuse is plausible and states are few.
      
      ```rust
      pub struct Connection<S> { stream: TcpStream, _state: PhantomData<S> }
      pub struct Unauthenticated;
      pub struct Authenticated;
      
      impl Connection<Unauthenticated> {
          pub fn authenticate(self, creds: &Creds)
              -> Result<Connection<Authenticated>, AuthError> { ... }
      }
      impl Connection<Authenticated> {
          pub fn query(&mut self, q: &str) -> Result<Rows, QueryError> { ... } // only here
      }
      ```
      
      - Transitions consume `self` and return the next state — the old state is
        unusable afterward, enforced at compile time.
      - Don't typestate everything: runtime enums are right when states are dynamic,
        numerous, or serialized. Typestate shines for build-time-known protocols
        (request builders, handshakes, init-before-use).
      
      ## 5. Builder pattern
      
      Use a builder when a constructor would exceed ~4 params or has many optionals.
      
      ```rust
      let server = Server::builder()
          .bind("0.0.0.0:8080")
          .worker_threads(8)
          .tls(tls_config)            // optional
          .build()?;                  // validation lives here, returns Result
      ```
      
      - `build()` returns `Result<T, BuildError>` if any combination can be invalid;
        infallible builders may return `T`.
      - Owned `self` methods (move-chain) for one-shot builders; `&mut self` methods
        when the builder is configured conditionally across branches. Pick one style
        per builder.
      - Required params: take them in `T::builder(required)` rather than failing at
        `build()` — or use typestate builders for compile-time required-field checks
        (what `typed-builder`/`bon` generate). Consider `bon` (2024+) before
        hand-rolling large builders.
      - `#[non_exhaustive]` on config structs + builder is the semver-safe way to add
        options later.
      
      ## 6. Trait design, sealed traits, coherence
      
      - Keep traits **small and capability-shaped** (`Read`, `Write`, not
        `RepositoryManagerService`). Compose with supertraits sparingly.
      - **Sealed traits** prevent downstream impls so you can add methods without a
        breaking change:
      
      ```rust
      mod sealed { pub trait Sealed {} }
      pub trait Backend: sealed::Sealed {
          fn run(&self) -> Output;
          // adding a defaulted method later is non-breaking: no foreign impls exist
      }
      impl sealed::Sealed for Postgres {}
      impl Backend for Postgres { ... }
      ```
      
      - Orphan rule: you may impl a trait only if you own the trait or the type.
        Workarounds: newtype wrapper, or define a local conversion trait. Never
        paper over it with a blanket `impl<T> From<T>` that will collide later.
      - Prefer generics (`impl Trait` / `<T: Trait>`) for hot paths (monomorphized,
        inlinable); `dyn Trait` for heterogeneous collections, plugin registries, and
        to cut compile time/bloat on cold paths. `Box<dyn Error + Send + Sync>` at
        app boundaries is fine.
      - Return-position `impl Trait` (and RPITIT in traits, stable since 1.75) over
        boxed returns where the concrete type is single. For public traits needing
        dyn-compatibility with async, use `async-trait` or hand-rolled
        `Box<Pin<...>>` returns (see rules/04).
      - Implement standard traits eagerly: `Debug` on everything public (or a manual
        redacting impl for secrets — rules/05), `Default` where a zero-config value
        exists, `Display` + `std::error::Error` on errors, `From` for infallible
        conversions, `TryFrom` for fallible ones. Never impl `Into` directly.
      - **`Eq`/`Ord`/`PartialEq`/`PartialOrd`: `#[derive]` them; hand-write only with
        care.** The std library *assumes* their invariants — `PartialEq` symmetry and
        transitivity, `Eq` reflexivity, `Ord` a total order consistent with `Eq`, and
        `PartialOrd`/`Ord` agreement. A manual impl that breaks them is not a compile
        error but silently corrupts `sort`, `binary_search`, `BinaryHeap`,
        `BTreeMap`/`Set`, and dedup — wrong results, lost entries, or a panic
        (`sort_unstable` may panic on a non-total `Ord`). If you must hand-roll,
        define the minimal method (`Ord::cmp`, then `PartialOrd` via `Some(self.cmp)`)
        so the others stay consistent, and never derive `Ord` on a type whose `f32`/
        `f64` field makes the order partial. (ANSSI `LANG-CMP-INV`/`-DERIVE`)
      
      ## 7. Exhaustive matching & `#[non_exhaustive]`
      
      - Match exhaustively on **your own** enums; avoid `_ =>` arms so the compiler
        flags every new variant at each match site:
      
      ```rust
      // BAD: silently swallows future variants
      match event { Event::Open => ..., _ => {} }
      
      // GOOD: compiler forces a decision when Event grows
      match event { Event::Open => ..., Event::Close => ..., Event::Ping => {} }
      ```
      
      - Mark public enums/structs `#[non_exhaustive]` when variants/fields will grow;
        downstream is then required to write `_` arms and your additions are
        non-breaking. Don't mark closed sets (e.g. `Ordering`-like) — it destroys
        downstream exhaustiveness checking for no gain.
      - `let ... else` for refutable bindings with early return; `matches!()` for
        boolean checks; `if let` chains (stable since 1.88) over nested `if let`;
        `if let` guards in `match` arms (`Some(x) if let Ok(y) = f(x) =>`, stable
        since 1.95) over guard-then-rematch patterns.
      
      ## 8. Iterators & combinators
      
      - Iterator chains over index loops: no bounds-check noise, no off-by-one, fuses
        and optimizes as well or better than manual indexing.
      
      ```rust
      // BAD
      let mut out = Vec::new();
      for i in 0..xs.len() { if xs[i].active { out.push(xs[i].score * 2); } }
      
      // GOOD
      let out: Vec<_> = xs.iter().filter(|x| x.active).map(|x| x.score * 2).collect();
      ```
      
      - Know the collect targets: `Vec<_>`, `String`, `HashMap<_,_>`,
        `Result<Vec<T>, E>` (fail-fast), `itertools::process_results` for streaming.
      - Option/Result combinators over match ladders: `map`, `and_then`, `ok_or_else`,
        `unwrap_or_else`, `?` with `From` conversions. But: a 5-combinator chain that
        needs a comment should be a `match` — clarity wins.
      - Use lazy variants on hot/fallible paths: `unwrap_or_else(|| expensive())`,
        `ok_or_else`, not `unwrap_or(expensive())` (argument always evaluated).
      - Need the index? `.enumerate()`. Need pairs? `.zip()` / `.windows(2)` /
        `.chunks(n)` — or `array_windows::<N>()` (stable 1.94) when a const-size
        `&[T; N]` lets you destructure without bounds checks. Early exit with side
        results: `try_fold` / `find_map`.
      
      ## 9. Module & workspace organization
      
      - Module = privacy boundary, not a filing cabinet. Keep invariant-carrying
        fields private; expose constructors that enforce them. `pub(crate)` over
        `pub` until something external needs it.
      - Re-export the public API at the crate root (`pub use module::Type;`) so the
        internal tree can move without breaking users; keep the public surface flat
        and small.
      - Prefer `foo.rs` + `foo/` subdir over `foo/mod.rs` (one convention per repo).
      - Workspace structure for anything beyond one crate:
      
      ```toml
      # /Cargo.toml
      [workspace]
      members = ["crates/*"]
      resolver = "3"                 # edition 2024 default
      
      [workspace.package]
      edition = "2024"
      rust-version = "1.85"
      license = "MIT OR Apache-2.0"
      
      [workspace.dependencies]       # single source of version truth
      serde = { version = "1", features = ["derive"] }
      tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
      ```
      
        Member crates use `serde = { workspace = true }`. Split crates along compile
        and dependency boundaries (`core` no-heavy-deps, `cli`, `server`), not by
        "layer" for its own sake — each crate is a compile unit and a semver unit.
      - Binary crates: keep `main.rs` thin (arg parsing + call into `lib.rs`) so the
        logic is testable.
      
      ## Audit checklist
      
      - [ ] `rg '\.clone\(\)' -t rust` — review each hit near a `for`/borrow error
            fix; flag clones of `String`/`Vec`/large structs that exist only to
            appease borrowck. `clippy::redundant_clone` (note: known false negatives,
            still run it).
      - [ ] `rg 'fn \w+\((&self, )?\w+: (String|Vec<|PathBuf)' -t rust` — owned params
            that are only read → should borrow.
      - [ ] `rg '&String|&Vec<|&PathBuf|&Box<' -t rust` — double-indirection params
            (`clippy::ptr_arg` catches most).
      - [ ] `rg 'Arc<Mutex<' -t rust` — each one: is shared mutable state actually
            required, or would a channel/owner task work? Check for `Weak` where
            graphs/cycles appear: `rg 'Rc<RefCell<'`.
      - [ ] `rg '_ =>' -t rust` — wildcard arms on local enums lose exhaustiveness;
            `clippy::wildcard_enum_match_arm` (pedantic) to enforce.
      - [ ] Public enums likely to grow without `#[non_exhaustive]`:
            `rg -B2 'pub enum' | rg -v non_exhaustive`.
      - [ ] `rg 'for i in 0\.\.' -t rust` — index loops; check `clippy::needless_range_loop`.
      - [ ] `rg 'unwrap_or\([a-zA-Z_]+\(' -t rust` — eager argument evaluation
            (`clippy::or_fun_call`).
      - [ ] Raw primitive IDs in public signatures: `rg 'fn .*\b(id|user|key)\w*: (u32|u64|i64|String)'`.
      - [ ] Public trait intended to be closed but unsealed — can a downstream crate
            impl it? If yes and that's unintended, seal it.
      - [ ] Hand-written comparison impls: `rg 'impl (PartialEq|Eq|PartialOrd|Ord)' -t rust`
            — verify invariants (total order, consistency with `Eq`, symmetry); a
            broken `Ord` corrupts `sort`/`BinaryHeap`/`BTreeMap` or panics. Prefer
            `#[derive]`. `LANG-CMP-INV`. `clippy::derive_ord_xor_partial_ord`,
            `clippy::non_canonical_partial_ord_impl`.
      - [ ] Multi-crate repo without `[workspace.dependencies]` → version drift;
            `cargo tree -d` to find duplicate dependency versions.
      - [ ] Clippy gates for this file's concerns: `clippy::needless_pass_by_value`,
            `clippy::trivially_copy_pass_by_ref`, `clippy::large_enum_variant`,
            `clippy::rc_buffer`, `clippy::mutable_key_type`.
      
    • 02-errors-and-panics.md 13.5 KB
      # 02 — Error Handling & Panic Policy
      
      Errors are API. Panics are program-integrity violations, not control flow.
      These rules define the library/application split, the unwrap policy, and how
      panics interact with servers and FFI.
      
      ## 1. The library/application split
      
      - **Libraries**: concrete, structured error enums via `thiserror`. Callers must
        be able to match on failure modes without string-parsing.
      - **Applications** (binaries, services, CLIs): `anyhow` (or `eyre` if you want
        custom report handlers/spantrace) — context-rich, cheap to propagate, rendered
        once at the top.
      - Never expose `anyhow::Error` in a library's public API; never build elaborate
        error enums in app code that only ever get printed.
      
      ```rust
      // LIBRARY (thiserror)
      #[derive(Debug, thiserror::Error)]
      pub enum StoreError {
          #[error("key not found: {0}")]
          NotFound(String),
          #[error("serialization failed")]
          Serde(#[from] serde_json::Error),
          #[error("io error accessing {path}")]
          Io { path: PathBuf, #[source] source: std::io::Error },
      }
      
      // APPLICATION (anyhow)
      use anyhow::{Context, Result};
      fn load_config(path: &Path) -> Result<Config> {
          let raw = fs::read_to_string(path)
              .with_context(|| format!("reading config {}", path.display()))?;
          toml::from_str(&raw).context("parsing config TOML")
      }
      ```
      
      ## 2. Designing library error types
      
      - One error enum **per fallible subsystem**, not one giant crate-wide enum where
        every function claims it can fail in 14 ways it can't. If a function has two
        failure modes, its error type should show two.
      - Preserve sources: `#[source]` (or `#[from]`) so `Error::source()` chains work
        and `anyhow` callers get full causality. Don't flatten causes into strings.
      - `#[from]` only when the conversion is unambiguous; if two variants wrap
        `io::Error`, use explicit constructors with context (path, operation) instead.
      - Mark public error enums `#[non_exhaustive]` so adding failure modes is
        non-breaking.
      - Errors must be `Debug + Display + Send + Sync + 'static` (and implement
        `std::error::Error`) — otherwise they won't flow through `anyhow`/`Box<dyn>`.
      - Don't `impl From<MyError> for String` or stringly-type errors. Display is for
        humans; variants are for machines.
      - Big errors slow every `Result` return: keep error types ≤ ~3 words or box the
        payload (`clippy::result_large_err` flags >128 bytes).
      
      ## 3. Context discipline
      
      - Add context **at each boundary where information exists that the callee
        lacked**: file paths, IDs, request params. `?` alone up a 6-frame stack
        yields "No such file or directory" with no idea which file.
      - `with_context(|| ...)` (lazy) over `context(format!(...))` (eager allocation
        on success path).
      - In services, attach machine context to spans (`tracing` fields), human
        context to the error. Log errors **once**, at the handling site, with
        `{:#}`/error-chain rendering — not at every propagation hop (double-logging
        audit smell).
      
      ## 4. unwrap/expect policy
      
      **Production code paths: no `unwrap()`. `expect()` only with an invariant
      message — a message stating why this cannot fail, not what failed.**
      
      ```rust
      // BAD
      let port = env::var("PORT").unwrap();
      let re = Regex::new(pattern).unwrap();           // user-supplied pattern!
      
      // ACCEPTABLE: invariant expect — message states the reason it can't fail
      let re = Regex::new(r"^[a-z]+$").expect("static regex is valid");
      let ts = UNIX_EPOCH.elapsed().expect("system clock before 1970");
      
      // GOOD: fallible inputs propagate
      let port: u16 = env::var("PORT")
          .context("PORT not set")?
          .parse()
          .context("PORT is not a valid u16")?;
      ```
      
      - Convention: messages read as "this holds because…" — `expect("mutex not
        poisoned: no panicking critical sections")`. If you can't write the
        invariant, you don't have one; return an error.
      - `unwrap()` is fine in: tests, benches, examples, doc-tests, build scripts,
        and `const`/static init where failure is a compile-mistake. Gate the rest:
        `#![warn(clippy::unwrap_used)]` (and `clippy::expect_used` in the strictest
        crates), with `#[allow]` + justification at the call site when truly needed.
      - Slicing/indexing (`xs[i]`, `&s[a..b]`) and integer `as` casts are implicit
        unwraps — use `.get(i)`, `s.get(a..b)`, `try_into()` on untrusted values
        (string slicing also panics on non-char-boundary).
      - `unreachable!()` must carry the proof: `unreachable!("len checked > 0 above")`.
      
      ## 5. Panics: when, and what they cost
      
      Panic only for **bugs** — violated invariants, impossible states — never for
      bad input, missing files, network failure, or anything an attacker controls.
      
      - Servers: a panic in a handler must not kill the process, and a reachable
        panic is a DoS primitive (rules/05 §4). Catch at the task boundary —
        `tokio::spawn` already isolates panics into `JoinError`; check
        `JoinError::is_panic()` and log. Axum/tower: `CatchPanicLayer`. But treat
        every caught panic as a bug to fix, not a handled case.
      - `panic = "abort"` in release profiles (rules/06) means no unwinding: no
        `catch_unwind`, panics kill the process. Decide deliberately for servers —
        abort+supervisor-restart is a valid stance, but then panic-freedom of
        handlers is load-bearing.
      - **FFI**: unwinding across `extern "C"` is UB. Wrap Rust callbacks invoked
        from C in `std::panic::catch_unwind` (or use `extern "C-unwind"` only when
        both sides genuinely support it).
      - **`Drop` impls must not panic.** `drop()` itself runs during unwinding from
        another panic; a panic there is a *double panic* that — per the std `Drop`
        docs — "will likely abort the program." So a panicking destructor turns one
        recoverable failure into process death (DoS). Destructors clean up and return
        normally; if a Drop genuinely must signal misuse, gate it on
        `std::thread::panicking()` first. This applies to RAII guards, buffer
        flushers, and `Zeroize`-on-drop types alike. (ANSSI `LANG-DROP-NO-PANIC`)
      - Poisoned mutexes (`std::sync::Mutex`): a panic while holding the lock poisons
        it. Decide policy once: propagate (`lock().expect("not poisoned: …")`) or
        recover (`unwrap_or_else(PoisonError::into_inner)`) — document which.
      - Allocation failure aborts; for memory-bound parsing of untrusted sizes use
        `try_reserve` (rules/05 §6).
      
      ## 6. Option/Result flow patterns
      
      - `?` everywhere it applies, including `Option` in functions returning `Option`.
      - Convert at the edges: `ok_or_else(|| StoreError::NotFound(key.into()))` turns
        `Option` into `Result` exactly where "absence is an error" becomes true —
        not earlier.
      - `let Some(x) = ... else { return ... };` for guard clauses; keeps the happy
        path unindented.
      - Fallible iteration: `collect::<Result<Vec<_>, _>>()` fail-fast; or
        `partition_result` / inspect-and-log per item when partial success is the
        semantics. Choose explicitly — silent `filter_map(Result::ok)` **discards
        errors** and is an audit finding unless commented.
      - Don't `match` on a `Result` just to re-wrap (`Ok(v) => Ok(f(v)), Err(e) =>
        Err(e)`) — that's `.map(f)`. Clippy: `manual_map`, `question_mark`,
        `needless_match`.
      
      ### 6a. Don't unwrap an Option back into a sentinel
      
      Rust makes the in-band sentinel (`sota-architecture` rules/02 §8a) hard to write by
      accident: `str::find` and `Iterator::position` return `Option<usize>`, not `-1`
      (verified, rustc 1.97.1). The way it gets reintroduced is at the *seam*:
      
      - `opt.unwrap_or(-1)`, `unwrap_or_default()` on a numeric `Option`, or
        `unwrap_or(0)` — each discards the type system's absence encoding and hands a
        domain value downstream (verified: `None::<i32>.unwrap_or(-1)` is `-1`). Push the
        `Option` outward instead; collapse it only where the value is *consumed*, and then
        with `match`/`ok_or`, not a magic number.
      - FFI and wire boundaries are where it enters: a C ABI returning `-1`/`errno`, a
        protobuf `int32` with no `optional`, `serde` deserializing a field whose absence
        the schema encodes as a value. Convert **at the boundary** into `Option`/`Result`
        — the anti-corruption-layer rule, applied to a scalar.
      - `#[serde(default)]` on a numeric field silently substitutes `0` for absent. That
        is an in-band sentinel chosen by an attribute; use `Option<T>` unless `0` is
        genuinely the right value.
      - Audit: `grep -rnE 'unwrap_or\(-?[0-9]+\)|unwrap_or_default\(\)' --include='*.rs' .`
        and `serde(default)` on numeric fields.
      
      ## 7. Retryability & error classification
      
      Services need errors classified for *behavior*, not just display. Encode the
      decision the caller must make:
      
      ```rust
      #[derive(Debug, thiserror::Error)]
      pub enum FetchError {
          #[error("transient: {0}")]
          Transient(#[source] anyhow::Error),   // timeouts, 503, conn reset
          #[error("permanent: {0}")]
          Permanent(#[source] anyhow::Error),   // 4xx, validation, auth
      }
      impl FetchError {
          pub fn is_retryable(&self) -> bool { matches!(self, Self::Transient(_)) }
      }
      ```
      
      - Classify at the site that knows (the HTTP client wrapper knows 503 vs 400);
        upper layers consume `is_retryable()` instead of re-inspecting causes.
      - Retries always carry backoff + jitter + a cap (`backon`/`tokio-retry`
        style); retrying permanent errors is load amplification, retrying without
        jitter is a thundering herd.
      - Map the same classification to HTTP/gRPC status at the boundary in ONE
        place (an `IntoResponse`/`From<Error> for Status` impl), not per-handler.
      - Idempotency: only auto-retry operations that are idempotent or carry an
        idempotency key — a retried non-idempotent POST is a correctness bug, not
        resilience.
      
      ## 8. Testing error paths
      
      Error paths are code; untested error paths are where prod incidents live.
      
      - Unit-test that fallible constructors reject bad input with the *right
        variant*: `assert!(matches!(parse(""), Err(ParseError::Empty)))` — not just
        `.is_err()` (a panic-turned-error or wrong variant passes `.is_err()`).
        `assert_matches!`/`debug_assert_matches!` (stable since 1.96) do the same
        and panic with the actual value on mismatch — prefer them in new tests.
      - Test `Display` output of user-facing errors (snapshot with `insta`) —
        error messages are UI and regress silently.
      - Fault injection at trait boundaries: a mock `Storage` whose `get` returns
        `Err(Io(...))` proves the caller's context/retry/cleanup logic. If errors
        can't be injected, the seam is missing (concrete deps where traits/generics
        belong).
      - For panics that are part of a documented contract: `#[should_panic(expected
        = "...")]` pins the message; for `Result`-returning tests use
        `fn test() -> anyhow::Result<()>` and `?` freely.
      
      ## 9. Error reporting at the top
      
      ```rust
      fn main() -> anyhow::Result<()> { ... } // Debug-renders the chain on error
      // or for custom exit codes / human rendering:
      fn main() -> ExitCode {
          if let Err(e) = run() {
              eprintln!("error: {e:#}");      // {:#} renders the full context chain
              return ExitCode::FAILURE;
          }
          ExitCode::SUCCESS
      }
      ```
      
      - Services: render error chain into structured logs
        (`tracing::error!(error = ?err)` or the `err` field shorthand), map to a
        client-safe status — never leak internal error text to HTTP responses
        (info disclosure, rules/05).
      - Install a panic hook that logs through your structured logger
        (`std::panic::set_hook`) so panics in production aren't lost to stderr.
      
      ## Audit checklist
      
      - [ ] Sentinel re-entry (§6a): `rg 'unwrap_or\(-?[0-9]+\)|unwrap_or_default\(\)' -t rust`
            on numeric `Option`s, and `#[serde(default)]` on numeric fields — each discards the
            absence the type system was carrying.
      
      - [ ] `rg '\.unwrap\(\)' -t rust -g '!*test*' -g '!benches/*' -g '!examples/*'`
            — every hit in production paths is a finding; severity scales with input
            reachability (attacker-reachable unwrap = High).
      - [ ] `rg '\.expect\("' -t rust` — messages must state invariants ("valid
            static regex"), not restate the failure ("failed to parse").
      - [ ] `rg '\.unwrap_or_default\(\)|filter_map\(Result::ok\)|\.ok\(\)[;)]' -t rust`
            — silently swallowed errors; require a comment justifying each.
      - [ ] `rg 'panic!|unreachable!|todo!|unimplemented!' -t rust -g '!*test*'` —
            `todo!`/`unimplemented!` in shipped code = High; `panic!` on
            input-derived conditions = High (DoS).
      - [ ] Indexing on untrusted data: `rg '\[[a-z_]+\]' -t rust` near
            parse/decode code; string slicing `&s[`, plus `clippy::indexing_slicing`
            (restriction lint) on parser crates.
      - [ ] Library crates exporting `anyhow::Error`/`Box<dyn Error>` in public
            signatures: `rg 'pub fn .*-> .*(anyhow|Box<dyn Error)' -t rust`.
      - [ ] thiserror enums: variants missing `#[source]`/`#[from]` on wrapped
            errors (broken causality chains); public error enums missing
            `#[non_exhaustive]`.
      - [ ] Double-logging: error logged at propagation site AND handler.
      - [ ] `?` chains with no `.context(...)` anywhere between syscall and `main` —
            undebuggable errors.
      - [ ] FFI: `extern "C"` functions whose bodies can panic without
            `catch_unwind` → UB, Critical.
      - [ ] `Drop` impls that can panic: `rg -A15 'impl Drop' -t rust` then scan for
            `unwrap`/`expect`/`panic!`/indexing/`?` inside `fn drop` — double-panic
            aborts the process (DoS), High. `LANG-DROP-NO-PANIC`.
      - [ ] Retry loops: `rg 'retry|backoff' -t rust -i` — retries without
            classification (retrying 4xx), without jitter/cap, or around
            non-idempotent operations.
      - [ ] Error tests assert variants (`matches!`), not just `.is_err()`; error
            seams injectable (trait boundaries) for fault testing.
      - [ ] Lints to enforce in CI: `clippy::unwrap_used`, `clippy::panic` (servers),
            `clippy::result_large_err`, `clippy::or_fun_call`,
            `clippy::manual_let_else`; consider `#![deny(clippy::unwrap_used)]` at
            crate roots of network-facing crates.
      
    • 03-unsafe-discipline.md 11.9 KB
      # 03 — Unsafe Discipline
      
      `unsafe` does not turn off the rules; it makes **you** the checker. The standard
      is: minimal surface, sound encapsulation, documented invariants, and tooling
      (Miri, sanitizers) that re-checks what the compiler can't.
      
      ## 1. Minimize, then isolate
      
      - First question for any `unsafe`: **is there a safe equivalent?** Most are:
        `split_at_mut`, `MaybeUninit` + `Vec::spare_capacity_mut`, `bytemuck`/`zerocopy`
        for transmutes, `OnceLock`/`LazyLock` for lazy statics, `Cell`/`RefCell` for
        interior mutability, `Pin` APIs instead of raw self-references. The safe
        surface keeps growing — 1.93 stabilized `MaybeUninit` slice APIs
        (`assume_init_ref`/`assume_init_mut`/`write_copy_of_slice`), retiring many
        hand-rolled init loops.
      - Performance claims require receipts: an `unsafe` "optimization"
        (`get_unchecked`, skipped UTF-8 checks) without a benchmark showing the safe
        version is the bottleneck is a finding. Bounds checks usually vanish under
        iterators or a single up-front `assert!(len <= buf.len())` hoisting.
      - Isolate unsafe in **small modules/crates with a safe public API** whose
        soundness can be argued locally. The privacy boundary is the soundness
        boundary: if a `pub` field or safe method can break the invariant your
        unsafe code relies on, the abstraction is unsound *even if no caller does it
        today*.
      
      ```rust
      // Sound abstraction: invariant (`init <= N`) is privately owned,
      // every safe method maintains it, unsafe is locally justifiable.
      pub struct FixedVec<T, const N: usize> {
          buf: [MaybeUninit<T>; N],
          init: usize, // INVARIANT: buf[..init] is initialized
      }
      impl<T, const N: usize> FixedVec<T, N> {
          pub fn push(&mut self, v: T) -> Result<(), T> {
              if self.init == N { return Err(v); }
              self.buf[self.init].write(v);
              self.init += 1;
              Ok(())
          }
          pub fn as_slice(&self) -> &[T] {
              // SAFETY: buf[..init] is initialized (struct invariant, maintained
              // by push/pop; init never exceeds N).
              unsafe { slice::from_raw_parts(self.buf.as_ptr().cast(), self.init) }
          }
      }
      ```
      
      ## 2. SAFETY comments — non-negotiable
      
      **Every `unsafe` block carries a `// SAFETY:` comment proving each obligation
      of the called API is met.** Every `unsafe fn` and unsafe trait impl carries a
      `/// # Safety` doc section stating what callers/implementors must uphold.
      
      ```rust
      // BAD
      let x = unsafe { *ptr };
      
      // GOOD
      // SAFETY: `ptr` comes from Box::into_raw in `Self::new`, is non-null and
      // aligned, and is not freed until Drop; no &mut alias exists because we
      // hold &self and the field is not otherwise exposed.
      let x = unsafe { *ptr };
      ```
      
      - The comment addresses the **specific preconditions** in the unsafe API's
        docs (non-null, aligned, initialized, valid-for-reads, no aliasing, lifetime
        bounds) — not vibes like "this is fine".
      - Enforce mechanically: `#![deny(clippy::undocumented_unsafe_blocks)]`
        (and `clippy::missing_safety_doc`, which is warn-by-default). Edition 2024:
        `unsafe_op_in_unsafe_fn` is warn-by-default — write explicit `unsafe {}`
        blocks inside `unsafe fn` so each obligation site is visible and commented.
      - An `unsafe fn` whose safety contract can't be written in one paragraph has
        the wrong API shape — split it.
      
      ## 3. The UB catalog — what actually bites
      
      Audit unsafe code against these, in observed-frequency order:
      
      1. **Aliasing violations**: constructing two `&mut` to the same data, or a
         `&mut` while a `&` lives — *creating* the reference is UB even if unused.
         Classic source: `&mut *ptr` twice, casting `&T` → `&mut T` (always UB —
         `clippy::cast_ref_to_mut`/compiler `invalid_reference_casting` lint),
         `Vec`/self-referential pointer invalidated by reallocation.
      2. **Uninitialized memory**: `mem::uninitialized()` (deprecated, instant UB
         for most types) and `MaybeUninit::assume_init` before full init. Reading
         uninit bytes is UB even for `u8`. Use `MaybeUninit`, `Vec::spare_capacity_mut`,
         `ptr::write` (not `*ptr = v`, which drops the uninit "old value").
      3. **Transmute abuse**: size/alignment mismatch, invalid bit patterns (`bool`
         not 0/1, invalid enum discriminant, null fn pointer, uninhabited types),
         transmuting `&T` lifetimes, transmuting between repr(Rust) types whose
         layout is unspecified. Prefer `bytemuck::{cast, Pod}` / `zerocopy`
         (derive-checked), `f32::from_bits`, `ptr::cast`. Transmuting to extend a
         lifetime is a soundness hole, full stop.
      4. **Invalid values & ranges**: producing a `str` with invalid UTF-8 via
         `from_utf8_unchecked` on unvalidated input; out-of-range `char`;
         `NonZero*`/`NonNull` holding zero/null.
      5. **FFI lifetimes & ownership**: returning a pointer into a Rust object the C
         side outlives; freeing with the wrong allocator (must round-trip
         `Box::into_raw`/`Box::from_raw`, or expose `mylib_free`); double-free when
         C calls a destructor twice; `CString::new(s).unwrap().as_ptr()` — temporary
         dropped at end of statement, dangling pointer (`temporary_cstring_as_ptr`
         lint). Struct layout across FFI requires `#[repr(C)]`.
      6. **Unwinding across FFI** — see rules/02 §5; UB pre-"C-unwind", abort after.
      7. **Data races**: `unsafe impl Send/Sync` on types containing raw pointers or
         `Cell`-like internals without an argument; `static mut` (deprecated pattern;
         edition 2024 denies `static_mut_refs`) — use `AtomicX`, `OnceLock`,
         `Mutex`, or `SyncUnsafeCell` with justification.
      
      ## 3a. Layout, provenance, and Pin — the subtler contracts
      
      **Layout:** `repr(Rust)` layout is unspecified and may differ between
      compilations — any unsafe code assuming field order/offsets needs `#[repr(C)]`
      (FFI, byte-casting) or `#[repr(transparent)]` (newtype with identical ABI to
      its single field — required for soundly casting `&Wrapper<T>` ↔ `&T`).
      Enum-discriminant tricks need explicit `#[repr(u8)]`-style declarations.
      `bytemuck::Pod`/`zerocopy::FromBytes` derives verify these statically — prefer
      them over manual offset math; for unavoidable offsets use
      `core::mem::offset_of!` (stable), never hand-computed constants.
      
      **Pointer provenance:** a pointer is more than an address. Casting ptr→int→ptr
      strips provenance and is UB-adjacent under strict provenance; round-trip with
      `ptr.with_addr(...)`/`ptr.map_addr(...)` (strict provenance APIs, stable) or
      keep it as a pointer. Pointers derived from a `&T` may only access that `T`'s
      bytes for that borrow's lifetime — offsetting into a sibling field via a field
      reference is UB even if the address is "right". Run Miri with
      `-Zmiri-strict-provenance` to catch the class.
      
      **Pin:** `Pin<&mut T>` promises T won't move again until drop. Unsafe code
      relying on pinning must uphold the drop guarantee (pinned memory must be
      dropped before reuse, can't be deallocated without drop) and never hand out
      `&mut T` from `Pin<&mut T>` for `!Unpin` types except via `map_unchecked_mut`
      with a SAFETY argument that the projection is structural. Hand-rolled
      self-referential types: use `pin-project` (safe projections, checks the rules)
      instead of manual `unsafe` projections — hand-rolled pin projections are a
      recurring soundness-bug source even in expert crates.
      
      **Drop interaction:** `ManuallyDrop` + `ptr::read` patterns (taking ownership
      out of `&mut self` in `Drop`) must guarantee no double-drop on every path
      including panics; `mem::forget` is safe but leaks — unsafe code may NOT rely
      on Drop running for soundness (leakpocalypse rule: `Rc` cycles + `mem::forget`
      make "Drop always runs" a false invariant).
      
      ## 4. Miri, sanitizers, fuzzing — CI for the unchecked
      
      Any crate with non-trivial `unsafe` runs **Miri in CI**:
      
      ```yaml
      # .github/workflows/miri.yml (core job)
      - run: rustup toolchain install nightly --component miri
      - run: cargo +nightly miri test
        env:
          # many-seeds for nondeterminism; strict provenance catches ptr-int abuse
          MIRIFLAGS: "-Zmiri-strict-provenance"
      ```
      
      - Miri checks the (Tree Borrows / Stacked Borrows) aliasing model, init,
        alignment, leaks — but **only on executed paths**: unsafe code without tests
        is unaudited code. Write tests that exercise every unsafe branch.
      - Miri can't run FFI/syscall-heavy paths; for those use sanitizers:
        `RUSTFLAGS="-Zsanitizer=address" cargo +nightly test` (ASan), TSan for
        concurrency claims, and `loom` for testing lock-free/atomic algorithms
        exhaustively.
      - Parsers and any unsafe-touching decoder: fuzz with `cargo fuzz` (libFuzzer)
        — fuzzing + Miri/ASan is the practical soundness net (see rules/05 §6).
      
      ## 5. Supply-chain visibility of unsafe
      
      - `cargo geiger` reports unsafe usage across the dependency tree — use it to
        *direct review attention*, not as a verdict (unsafe ≠ unsound; zero-unsafe ≠
        sound). Heavy-unsafe deps doing things std could do = replace.
      - Prefer audited foundations: `bytemuck`/`zerocopy` over hand transmutes,
        well-known FFI `-sys` crates over bespoke bindings.
      - `#![forbid(unsafe_code)]` in crates that need none — it's a semver-visible
        promise and makes regressions un-mergeable. Workspace-wide:
        `[lints.rust] unsafe_code = "forbid"` with per-crate opt-out.
      - Record unsafe review in `cargo vet` audits (criteria `safe-to-deploy` +
        unsafe review) — rules/05 §2.
      
      ## 6. Soundness review protocol (for AUDIT mode)
      
      For each `unsafe` block, in order:
      
      1. Identify the exact unsafe operations (deref, call, transmute, impl).
      2. List each documented precondition of those operations.
      3. Check the SAFETY comment discharges **all** of them (missing comment =
         automatic finding; wrong comment = worse).
      4. Hunt invariant escapes: can safe code (pub fields, safe methods, trait
         impls, `Deref`, `Drop`, panics mid-modification, reentrancy via callbacks)
         break the invariant the unsafe block assumes? Panic-safety: if user code
         (closures, `T: Clone`, comparators) can panic while your invariant is
         temporarily broken, Drop/unwinding observes broken state → need guard
         objects or `catch_unwind` reasoning.
      5. Check `Send`/`Sync`: any manual `unsafe impl` needs a written argument per
         field; raw pointers suppress auto-derive for a reason.
      6. Confirm Miri runs over this code path in CI; if not, that's a finding
         regardless of how correct the code looks.
      
      ## Audit checklist
      
      - [ ] `rg 'unsafe' -t rust --count-matches` — map the surface first; unsafe
            outside dedicated modules/crates is a structure finding.
      - [ ] Undocumented blocks: `rg -B2 'unsafe \{' -t rust | rg -v 'SAFETY'` (then
            verify by eye); enforce `clippy::undocumented_unsafe_blocks`.
      - [ ] `rg 'transmute' -t rust` — each one: why not `bytemuck`/`zerocopy`/
            `from_bits`/`cast`? Lifetime-extending transmute = Critical.
      - [ ] `rg 'from_utf8_unchecked|get_unchecked|assume_init|set_len' -t rust` —
            verify the stated invariant actually holds on all paths incl. panics.
      - [ ] `rg 'static mut|&mut \*\(|as \*mut' -t rust`; compiler lints
            `static_mut_refs`, `invalid_reference_casting` must be deny.
      - [ ] `rg 'unsafe impl (Send|Sync)' -t rust` — require per-field justification
            comment; absence = High.
      - [ ] FFI: `rg 'extern "C"' -t rust` — check `#[repr(C)]` on crossing types,
            panic containment, allocator pairing (`into_raw`/`from_raw` symmetry),
            `as_ptr()` on temporaries.
      - [ ] Layout assumptions: `rg 'repr\(' -t rust` — byte-casting/FFI types have
            `repr(C)`/`repr(transparent)`; `rg 'as usize as \*|usize as \*' -t rust`
            — int→ptr casts (provenance loss).
      - [ ] `rg 'map_unchecked_mut|Pin::new_unchecked|get_unchecked_mut' -t rust` —
            hand-rolled pin projections; prefer `pin-project`. Drop impls using
            `ptr::read`/`ManuallyDrop` checked for panic-path double-drop.
      - [ ] CI: Miri job exists and isn't `continue-on-error: true`; fuzz targets
            exist for unsafe parsers; `cargo geiger` output reviewed for the tree.
      - [ ] Crates with zero unsafe missing `#![forbid(unsafe_code)]` — Low, but
            free hardening.
      - [ ] Severity calibration: reachable UB = Critical; unsound public API (safe
            code can trigger UB) = Critical even if unexercised; missing SAFETY
            comment = Medium; missing Miri CI on unsafe crate = Medium.
      
    • 04-async-tokio.md 14.6 KB
      # 04 — Async Rust & Tokio
      
      Async Rust's failure modes are quiet: a blocked worker thread, a future dropped
      mid-write, a lock held across `.await`. These rules cover tokio idioms,
      cancellation safety, structured concurrency, channels, and shutdown.
      
      ## 1. Never block the runtime
      
      A tokio worker thread running blocking code stalls **every task scheduled on
      it**. Blocking = sync I/O, heavy CPU (>~100µs per poll), `std::thread::sleep`,
      sync DB drivers, `reqwest::blocking`, sync `zip`/compression, big serde on huge
      payloads, `std::sync::Mutex` under contention.
      
      ```rust
      // BAD: stalls the worker thread
      async fn load(path: PathBuf) -> Result<Config> {
          let raw = std::fs::read_to_string(&path)?;          // sync I/O in async
          let parsed = heavy_parse(&raw);                     // 50ms CPU in async
          Ok(parsed)
      }
      
      // GOOD
      async fn load(path: PathBuf) -> Result<Config> {
          let raw = tokio::fs::read_to_string(&path).await?;  // async I/O
          let parsed = tokio::task::spawn_blocking(move || heavy_parse(&raw))
              .await?;                                         // CPU off-runtime
          Ok(parsed)
      }
      ```
      
      - `spawn` for async work; `spawn_blocking` for blocking-but-bounded work
        (file ops via std, sync clients, password hashing); a dedicated **rayon pool
        or separate runtime** for sustained CPU-parallel work — `spawn_blocking`'s
        pool (default cap 512 threads) is sized for blocking I/O, not compute.
      - `tokio::time::sleep`, never `std::thread::sleep`, in async fns.
      - Detection: `tokio-console` (task poll times), `RUSTFLAGS` +
        `tokio_unstable` task dumps; in review, grep for sync APIs inside `async fn`
        (checklist below). A p99 latency cliff under load with idle CPU is the
        classic blocked-worker signature.
      - `block_on` inside an async context panics or deadlocks
        (`Handle::block_on` on the current runtime, nested `Runtime::block_on`).
        Bridging sync→async from a blocking thread: `Handle::current().block_on`
        from `spawn_blocking` is legal; document why.
      
      ## 2. Decoding Send/Sync bounds errors
      
      "future cannot be sent between threads safely" means a non-`Send` value is
      **held across an `.await`** in a future passed to `tokio::spawn`
      (multi-threaded runtime requires `Send + 'static`).
      
      Fix in priority order:
      
      1. **Shrink the hold**: drop/scope the non-Send value before the await.
      
      ```rust
      // BAD: MutexGuard (often non-Send) and Rc held across await
      let guard = state.lock().unwrap();
      let data = guard.compute();
      remote.push(data).await?;        // guard still alive here
      
      // GOOD: scope ends before await
      let data = { state.lock().unwrap().compute() };
      remote.push(data).await?;
      ```
      
      2. Replace the type: `Rc`→`Arc`, `RefCell`→`Mutex`/atomics, thread-local /
         `dyn Trait` without `+ Send` → bounded version.
      3. For genuinely thread-bound libs (e.g. some FFI/GUI handles), as a last
         resort: `tokio::runtime::LocalRuntime` (stabilized in tokio 1.51, 2026 —
         a whole runtime whose tasks may be `!Send`) or the older
         `tokio::task::LocalSet` + `spawn_local`.
      
      - The compiler note "this value is used across an await" points at the exact
        hold — read it before refactoring.
      - `'static` errors on `spawn`: the future can't borrow from the caller. Move
        owned/`Arc` data in (`async move`), or restructure so the parent awaits the
        child directly (no spawn = borrows fine), or use `JoinSet`/scoped patterns.
      - Library code: add `Send` bounds tests (`fn assert_send<T: Send>(t: T)`) so
        you don't break downstream spawnability silently.
      
      ## 3. Cancellation safety
      
      **Every `.await` is a possible end of your function.** Futures are cancelled by
      being dropped — by `select!`, timeouts, dropped `JoinHandle`s being aborted,
      or a client disconnect dropping the request future (hyper/axum do this).
      
      - `select!` pitfall: the non-winning branches' futures are **dropped** each
        iteration. Recreating a future in a loop loses partial progress:
      
      ```rust
      // BAD: on every tick, read_line future is dropped — buffered partial line lost
      loop {
          tokio::select! {
              line = read_line(&mut reader) => handle(line?),
              _ = interval.tick() => flush().await,
          }
      }
      
      // GOOD: keep the future alive across iterations (pin it once)
      let read_fut = read_line(&mut reader);
      tokio::pin!(read_fut);
      loop {
          tokio::select! {
              line = &mut read_fut => { handle(line?); read_fut.set(read_line(&mut reader)); }
              _ = interval.tick() => flush().await,
          }
      }
      ```
      
      - **Know your cancel-safe primitives — copy the list, do not reason about it.**
        Tokio classifies each operation itself, and two of the ones people most often
        assume are safe are *not*. Transcribed from the `select!` docs
        ([cancellation safety](https://docs.rs/tokio/latest/tokio/macro.select.html#cancellation-safety),
        read 2026-09-16; it is a per-version list, so re-read it for your pinned tokio):
      
        | | operation |
        |---|---|
        | **cancel-safe** | `mpsc::Receiver::recv`, `mpsc::UnboundedReceiver::recv`, `broadcast::Receiver::recv`, **`watch::Receiver::changed`**, `TcpListener::accept`, `UnixListener::accept`, `signal::unix::Signal::recv`, `AsyncReadExt::read` / `read_buf`, `AsyncWriteExt::write` / `write_buf`, `StreamExt::next` (tokio-stream or futures) |
        | **NOT safe — partial I/O, data is lost** | `AsyncReadExt::read_exact`, `read_to_end`, `read_to_string`, `AsyncWriteExt::write_all` |
        | **NOT safe — you lose your place in a fairness queue** | **`Mutex::lock`**, `RwLock::read`, `RwLock::write`, `Semaphore::acquire`, **`Notify::notified`** |
      
        **The two unsafe rows fail differently and the difference decides the fix.** The
        first row loses *bytes*: half a frame is gone and the stream is desynchronised,
        so the repair is to keep the future alive across iterations (`tokio::pin!`,
        above) or to read into a buffer you own. The second row loses *progress*:
        nothing is corrupted and no memory is unsound — the docs' wording is that these
        *"use a queue for fairness and cancellation makes you lose your place in the
        queue"* — so the symptom is starvation of a task that keeps getting cancelled
        and re-queued, not a torn value. Do not report the second row as data loss.
      
        On `watch`: the cancel-safe method is **`changed()`**, not `recv()` — a detail
        worth stating because the sibling channels *do* use `recv()`.
      - State mutations spanning an await are torn by cancellation. Either make the
        critical section await-free, or use a **drop guard** to restore/complete
        invariants:
      
      ```rust
      struct InFlightGuard<'a>(&'a Counter);
      impl Drop for InFlightGuard<'_> { fn drop(&mut self) { self.0.dec(); } }
      // guard decrements even if the request future is dropped mid-await
      ```
      
      - Cooperative cancellation: `CancellationToken` (tokio-util) +
        `token.cancelled()` in `select!`, or `JoinHandle::abort()` (abort only stops
        at await points; CPU loops need explicit checks).
      - Spawned tasks are **not** cancelled when their `JoinHandle` drops — they
        leak unless tracked (see §4) or aborted. Dropping a `JoinSet` *does* abort
        its tasks.
      
      **Cancelling a `timeout` around a child process does not kill the process.** The
      future is dropped, the OS process is not — rules/05 §9 (R9.5) has the measured
      behaviour and `.kill_on_drop(true)`. A spawned process is owned state exactly like a
      spawned task, and cancellation is where that ownership is usually dropped.
      
      ## 4. Structured concurrency
      
      Unsupervised `tokio::spawn` is a goto: errors vanish, panics vanish, shutdown
      can't find it.
      
      - **`JoinSet`** for dynamic groups of homogeneous tasks: collects results,
        propagates panics as `JoinError`, aborts all on drop.
      
      ```rust
      let mut set = tokio::task::JoinSet::new();
      for url in urls { set.spawn(fetch(url)); }
      while let Some(res) = set.join_next().await {
          let body = res??;             // JoinError (panic/abort) then app error
          process(body);
      }
      ```
      
      - **`TaskTracker` + `CancellationToken`** (tokio-util) for service-lifetime
        tasks: `tracker.spawn(...)`, then `tracker.close(); tracker.wait().await`
        on shutdown.
      - Concurrency without spawning (no `'static` needed, same task):
        `join!`/`try_join!` for fixed sets;
        `futures::stream::iter(items).map(work).buffer_unordered(N)` for bounded
        fan-out — **always bound N**; unbounded fan-out over request-derived
        collections is a self-DoS.
      - Every `tokio::spawn` must have an owner that observes its `JoinHandle` (or a
        comment justifying fire-and-forget + its own error logging). Panics in
        spawned tasks are silent until joined.
      
      ## 5. Locks across `.await`
      
      - `std::sync::MutexGuard` is non-Send (compile error on spawn) — but on
        single-future paths it *can* compile and then **deadlock**: task A holds the
        lock, awaits; task B on the same thread polls and blocks on the lock.
      - Decision rule: **short, await-free critical sections → `std::sync::Mutex`
        (or `parking_lot`)**, scoped to drop before any await. Need to hold a lock
        across an await (e.g. exclusive access to a connection through a protocol
        exchange) → `tokio::sync::Mutex` — accept that it's slower and serializes
        tasks.
      - Often the real fix is neither: move owned state into a dedicated task and
        communicate via channels (actor pattern), or use `RwLock`/`arc-swap` for
        read-mostly config.
      - Clippy: `await_holding_lock`, `await_holding_refcell_ref` — deny in CI.
      
      ## 6. Channel selection
      
      | Channel | Shape | Use |
      |---|---|---|
      | `mpsc` | many→one, bounded | work queues, actor inboxes — **default choice** |
      | `oneshot` | one value | request/response, completion signal |
      | `broadcast` | many→many, each gets all | events, pub/sub; lagging receivers get `RecvError::Lagged` — handle it |
      | `watch` | latest-value only | config updates, status, shutdown flag |
      | `mpsc::unbounded` | many→one, unbounded | almost never — unbounded = memory DoS under backpressure |
      
      - **Bounded `mpsc` everywhere by default**; choose capacity deliberately —
        `send().await` backpressure is the feature. `try_send` + explicit
        drop/shed policy on latency-critical producers.
      - Request/response over an actor: send `(payload, oneshot::Sender<Reply>)`.
      - `watch` for shutdown signals predates `CancellationToken`; prefer the token
        in new code.
      - Crossing sync→async: tokio `mpsc::Sender::blocking_send` from sync threads;
        never `block_on(tx.send(...))` inside the runtime.
      
      ## 7. Async traits & API design
      
      - Native `async fn` in traits (stable 1.75): fine for internal/sealed traits;
        **not dyn-compatible** and leaves `Send` of the returned future
        unnameable for generic callers. Public traits used as `dyn` or spawned
        generically: use `#[async_trait]` (boxes, adds `Send` bound by default) or
        return `impl Future + Send` explicitly / `BoxFuture`. The 2026-era
        alternative: `trait-variant` to generate `Send` variants.
      - Don't make functions `async` that never await — sync fn returning a value
        is simpler and callable anywhere (`clippy::unused_async`).
      - Don't expose tokio types in library public APIs unless the crate is
        tokio-specific by design; abstract over `AsyncRead`/`AsyncWrite`
        (tokio or futures versions) where feasible.
      - Tokio remains 1.x (1.52 as of mid-2026; no 2.0) and designates LTS minors
        with ≥1 year of backported fixes (1.47.x until Sep 2026, 1.51.x until Mar
        2027). Stability-critical services can pin an LTS line with tilde syntax:
        `tokio = { version = "~1.51", features = [...] }`.
      
      ## 8. Graceful shutdown
      
      The canonical service shape:
      
      ```rust
      let token = CancellationToken::new();
      let tracker = TaskTracker::new();
      
      // signal handling
      let t = token.clone();
      tokio::spawn(async move {
          tokio::signal::ctrl_c().await.expect("ctrl_c handler installed");
          t.cancel();
      });
      
      // accept loop
      loop {
          tokio::select! {
              _ = token.cancelled() => break,
              conn = listener.accept() => {
                  let (stream, _) = conn?;
                  tracker.spawn(handle(stream, token.clone()));
              }
          }
      }
      
      // drain: stop accepting, let in-flight finish (with deadline), then exit
      tracker.close();
      tokio::select! {
          _ = tracker.wait() => {}
          _ = tokio::time::sleep(DRAIN_TIMEOUT) => warn!("shutdown deadline hit"),
      }
      ```
      
      - Order: stop intake → signal cancellation → drain with deadline → flush
        (logs, metrics, WAL) → exit. Dropping the `Runtime` mid-flight cancels
        everything abruptly — drain first.
      - Handlers must observe the token at long awaits (`select!` with
        `token.cancelled()`) or be cancel-safe end-to-end.
      - Test shutdown: a service that can't exit cleanly under load hides task leaks.
      
      ## Audit checklist
      
      - [ ] Blocking in async: `rg -t rust 'async fn' -A30 | rg 'std::fs::|std::thread::sleep|reqwest::blocking|\.lock\(\)\s*$'`
            — more reliably, grep each: `rg 'thread::sleep|std::fs::(read|write|File)|blocking::' -t rust`
            and check enclosing fn for `async`. High severity in request paths.
      - [ ] `rg 'block_on' -t rust` — any call reachable from async context =
            Critical (deadlock/panic).
      - [ ] `rg 'unbounded_channel|UnboundedSender' -t rust` — each needs a written
            backpressure argument; attacker-fed unbounded channel = High (DoS).
      - [ ] `rg 'tokio::spawn' -t rust` — orphaned handles (result never joined, no
            JoinSet/TaskTracker, no error logging in task) = Medium; panic
            observability gap.
      - [ ] `select!` loops: any branch future recreated per-iteration that buffers
            internally (reads, `write_all`, custom combinators) → cancellation data
            loss = High. Check each `select!` arm against the cancel-safety table above —
            and **rate the two unsafe rows differently**: partial-I/O (`read_exact`,
            `write_all`) loses bytes and desynchronises a stream = High; a fairness-queue
            operation (`Mutex::lock`, `Semaphore::acquire`, `Notify::notified`) loses only
            its place in the queue = starvation risk, **not** data loss, so reporting it
            as corruption is a false finding. `Notified` and `Mutex::lock` are the two most
            often assumed safe; both are on Tokio's unsafe list.
      - [ ] Locks: clippy `await_holding_lock`, `await_holding_refcell_ref`;
            `rg 'tokio::sync::Mutex' -t rust` — verify each actually needs
            hold-across-await, else downgrade to std/parking_lot.
      - [ ] `rg '\.abort\(\)' -t rust` — aborted tasks: is every shared invariant
            abort-safe (drop guards present)?
      - [ ] Fan-out: `rg 'buffer_unordered|buffered\(' -t rust` — bound derived from
            config, not unbounded or request-controlled; loops spawning per item of
            untrusted-size collections.
      - [ ] Shutdown path exists: signal handler, drain deadline, `tracker.close()`
            before `wait()` (close-after-wait hangs forever).
      - [ ] `rg 'async fn' -t rust` + `clippy::unused_async`; public async traits:
            dyn-compat and Send bounds checked for downstream spawnability.
      - [ ] CI lints: `clippy::await_holding_lock`, `clippy::unused_async`,
            `clippy::large_futures` (oversized futures → stack/box them).
      
    • 05-security-supply-chain.md 21.2 KB
      # 05 — Security & Supply Chain
      
      Memory safety is the floor, not the ceiling. Rust services still fall to logic
      bugs, integer wrapping, panic-DoS, malicious dependencies, secret leakage, and
      hostile input. These rules are written for network-facing code; relax
      consciously for offline tools.
      
      ## 1. Dependency auditing in CI — cargo audit & cargo deny
      
      Non-negotiable for anything deployed: advisory + policy checks on every PR and
      on a schedule (new advisories land against old lockfiles).
      
      ```toml
      # deny.toml (core)
      [advisories]
      yanked = "deny"
      # RUSTSEC advisories: deny by default; ignore list requires expiry + reason
      ignore = [
        # { id = "RUSTSEC-2026-0001", reason = "not reachable: feature off", expire = "2026-09-01" }
      ]
      
      [licenses]
      allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "Unicode-3.0"]
      
      [bans]
      multiple-versions = "warn"
      wildcards = "deny"            # no `foo = "*"`
      [[bans.deny]]
      name = "openssl"              # example policy: rustls-only stack
      
      [sources]
      unknown-registry = "deny"
      unknown-git = "deny"          # git deps pinned by rev only, allowlisted
      ```
      
      - `cargo deny check` (advisories, licenses, bans, sources) in PR CI;
        `cargo audit` nightly via cron so existing `Cargo.lock` gets re-checked.
      - Commit `Cargo.lock` for binaries **and** (current guidance) for libraries —
        reproducible CI; `cargo update` is a reviewed PR, not a side effect.
      - Dependabot/Renovate for bumps; review changelogs of security-sensitive deps
        (crypto, TLS, parsing) instead of auto-merging.
      - Minimize the tree first: every dep is attack surface and build time.
        `cargo tree -d` for duplicates; question deps that pull in 50 transitive
        crates for one function.
      
      ## 2. Vetting dependencies — cargo vet / supply-chain hygiene
      
      - `cargo vet`: record audits (`safe-to-deploy`) for each dep version; import
        trusted audit sets (Mozilla, Google, Bytecode Alliance) so you only audit
        the residue. `cargo vet` in CI fails on unaudited new deps — turns "someone
        added a crate" into a reviewed event.
      - New-dep review minimum: maintenance signal, repo matches crates.io package
        (`cargo crev`/inspect tarball — typosquats and repo/package divergence are
        the common attacks), `cargo geiger` unsafe density, build.rs and proc-macros
        (these run code **at build time** — highest-trust tier). Check the crate's
        Security tab on crates.io (since Jan 2026 it surfaces RustSec advisories,
        CVE aliases, and affected ranges at the point of discovery).
      - This is not theoretical: Feb–Mar 2026 saw a coordinated campaign of five
        fake "time utility" crates (`time-sync`, `dnp3times`, `chrono_anchor`, … —
        RUSTSEC-2026-0030/0031/0032/0036) that typosquatted/brandjacked real crates
        and exfiltrated `.env` files from developer and CI machines. Mitigations are
        exactly the above plus secret hygiene: no long-lived credentials in `.env`
        on build machines; rotate anything exposed to an unvetted build.
      - Pin GitHub Actions by SHA, not tag; CI tokens least-privilege
        (`permissions: contents: read` default).
      - **Declared but not reached**: an unused crate is still install/build-time trust
        granted (`build.rs`, proc-macros) for zero function. `cargo machete` runs on
        stable but is deliberately imprecise — false positives for crates used only
        from `build.rs`-generated code and for import names that differ from package
        names (`--with-metadata` fixes the latter); `cargo +nightly udeps` needs
        nightly and documents false *negatives*. Neither settles it alone: prove a
        candidate by removing it in a scratch copy and running the real build, clippy,
        and full suite — `sota-devsecops` rules/10.
      
      ## 3. Integer overflow — release mode wraps
      
      Debug builds panic on overflow; **release builds wrap silently** (unless
      `overflow-checks = true`). Wrapping on attacker-influenced arithmetic is how
      length checks, allocations, and billing go wrong.
      
      ```rust
      // BAD: attacker sends len = u32::MAX; len + 4 wraps to 3, check passes
      if header.len + 4 <= buf.len() as u32 { read(&buf[..header.len as usize]) }
      
      // GOOD: checked arithmetic on untrusted input, fail closed
      let total = header.len.checked_add(4).ok_or(Error::BadLength)?;
      if (total as usize) <= buf.len() { ... }
      ```
      
      - Policy: **untrusted input → `checked_*` / `try_into`**; counters/metrics →
        `saturating_*`; intentional modular arithmetic (hashes, crypto, ring
        buffers) → `wrapping_*` / `Wrapping<T>` so intent is greppable. Bare `+ - *`
        is for values you've already bounded.
      - Set `overflow-checks = true` in the **release profile** for security-critical
        services — the cost is usually <2% and it converts silent corruption into a
        caught panic. (Then see §4: that panic must be contained.)
      - `as` casts truncate silently (`u64 as u32`, `i64 as usize`): use
        `try_from` on untrusted values; clippy `cast_possible_truncation`,
        `cast_sign_loss` (pedantic) on parser/protocol crates.
      
      ## 4. Panics as DoS
      
      Any attacker-reachable panic is a denial-of-service primitive — one request
      kills a worker or (with `panic=abort`) the whole process.
      
      - Hunt the implicit panic surface in request paths: `unwrap`/`expect`
        (rules/02), slice indexing, `&s[a..b]` on non-char boundaries, integer
        division by zero, `with_capacity(attacker_len)` (capacity overflow /
        OOM-abort), recursion depth on nested input (stack overflow — **abort, not
        catchable**; see §6 on serde recursion).
      - Containment at the boundary: per-connection/per-request `tokio::spawn`
        isolates unwinding panics (`JoinError::is_panic`); `CatchPanicLayer` for
        tower stacks. With `panic = "abort"`, containment is gone — pair abort with
        a supervisor (systemd `Restart=always`, k8s) and treat reachable panics as
        Critical, plus rate-limit restarts to blunt crash-loop DoS.
      - Resource-exhaustion siblings of panic-DoS: unbounded channels (rules/04),
        missing request body limits, decompression bombs (cap decompressed size),
        unbounded `read_to_end` on sockets — set explicit limits at every ingest.
      
      ## 5. Secrets in memory — zeroize & constant time
      
      - Wrap key material in `zeroize`/`secrecy`:
      
      ```rust
      use secrecy::{SecretString, ExposeSecret};
      struct DbConfig { url: String, password: SecretString }
      // Debug prints REDACTED; memory zeroized on drop; .expose_secret() is greppable
      ```
      
        Zeroize is best-effort (moves/reallocations copy bytes — avoid resizing
        buffers holding secrets; `Box::pin` long-lived keys), but it shrinks the
        window and kills the "secret in a core dump / Debug log" class.
      - **Don't rely *only* on `Drop` for security erasure.** `Drop` is skipped
        entirely by `mem::forget`, `Box::leak`, reference cycles (`Rc`/`Arc`), a
        panic mid-drop, and `panic = "abort"` / process exit — so Drop-based
        zeroization is a window-shrinker, not a guarantee. Don't structure a security
        argument ("the key is erased after use") on the destructor running; minimize
        the secret's lifetime, avoid leaking/forgetting secret-bearing values, and
        keep Drop impls panic-free (rules/02 §5) so the erasure path isn't skipped by
        an abort. (ANSSI `LANG-DROP-SEC`; soundness corollary in rules/03.)
      - **No `Debug`/`Display`/`Serialize` leaking secrets**: manual `Debug` impls
        redacting sensitive fields; never `#[derive(Debug)]` on a struct holding a
        raw key. Audit `tracing` events for token/password fields.
      - Comparisons of MACs/tokens/password hashes: constant-time only —
        `subtle::ConstantTimeEq` (`a.ct_eq(&b)`), or the comparison built into the
        crypto crate (e.g. `hmac`'s `verify_slice`). `==` on secret bytes is a
        timing oracle.
      - Don't hand-roll crypto: RustCrypto crates, `ring`, `aws-lc-rs`, or libsodium
        bindings; password hashing via `argon2`; randomness via `rand::rngs::OsRng`
        / `getrandom` only (never `SmallRng`/`thread_rng` for key material —
        thread_rng is a CSPRNG but OsRng removes the argument).
      - Env/config: secrets via files or secret managers over env vars where
        possible (`/proc/<pid>/environ` leaks); never in `Cargo.toml`, never
        compiled into the binary (`strings target/release/app | rg -i secret`).
      
      ## 6. Parsing untrusted input — serde hardening
      
      Deserialization is the front door. Rules for any `serde` boundary fed by the
      network:
      
      - **Size-limit before parse**: enforce body/frame limits at the transport
        (axum `DefaultBodyLimit`, manual `Content-Length` + streaming cap) — parsing
        a 2GB JSON body allocates before serde can object.
      - **`deny_unknown_fields`** on security-relevant configs and requests
        (prevents smuggling fields through proxies/validators that the backend
        interprets) — but note it breaks `#[serde(flatten)]` and forward-compat;
        choose per-type.
      - **Untagged enum DoS**: `#[serde(untagged)]` tries each variant in order —
        on deep/nested input this multiplies parse work and produces useless errors;
        worst case is exponential blowup with nested untagged enums. Prefer tagged
        (`#[serde(tag = "type")]`) or manual discriminator dispatch on hostile
        input. Adjacent risk: recursion depth — `serde_json` has a default 128-level
        limit, but `serde_yaml`-style formats and custom `Deserialize` impls may
        not; use `serde_stacker`/explicit depth caps for deeply-nested formats
        (stack overflow = abort = DoS).
      - Validate after parse: serde checks shape, not semantics. Lengths, ranges,
        string charsets via `TryFrom` newtypes (rules/01 §3) or `validator`/`garde`
        — the deserialized type should already be the validated type
        ("parse, don't validate").
      - `Vec` preallocation from attacker-controlled length prefixes
        (`Vec::with_capacity(hdr.count)`): cap or `try_reserve`. Binary formats
        (`bincode` etc.): configure size limits explicitly.
      - Don't deserialize to `Box<dyn Trait>`/arbitrary types via
        `typetag`-style registries from untrusted sources without an allowlist.
      - Fuzz every parser of untrusted bytes: `cargo fuzz` target per format, in
        scheduled CI. Combine with rules/03 sanitizers when the parser has unsafe.
      
      ## 7. Service-edge defaults
      
      - TLS: `rustls` stack by default (memory-safe, modern defaults).
      - Timeouts on **everything**: connect, read, write, total-request, idle
        (`TimeoutLayer`, `tower` middleware). Missing timeouts = slowloris.
      - Error responses: generic client text, full chain only into logs (rules/02
        §9); no `Debug`-formatted internals in HTTP bodies.
      - Path handling on user input: reject `..` traversal — canonicalize then
        verify prefix (`path.canonicalize()?.starts_with(root)`), never just join.
      - SQL via parameterized queries (`sqlx` compile-checked, `diesel`); any
        `format!` into a query string is a finding regardless of current inputs.
      
      ## 8. Release provenance & logging hygiene
      
      - **`cargo auditable`**: embeds the dependency list in the binary so deployed
        artifacts can be scanned against future advisories (`cargo audit bin app`).
        Pair with SBOM generation (`cargo cyclonedx`/`cargo sbom`) where compliance
        requires it. Reproducible-ish builds: pinned toolchain + locked deps +
        `--locked` in release CI (`cargo build --release --locked` — fails instead
        of silently updating the lockfile).
      - Release binaries built in CI from tags, not laptops; artifacts checksummed
        and (where distribution warrants) signed; `--locked` and the pinned
        toolchain make the build attributable to the lockfile that was audited.
      - **Log injection**: user-controlled strings logged raw can forge log lines
        (embedded `\n`) or poison downstream parsers — log via `tracing` structured
        fields (`tracing::info!(user = %name)` escapes on JSON output) rather than
        interpolating into the message; never log full request bodies or headers
        carrying credentials (`Authorization`, `Cookie`) — redact at the middleware
        layer once.
      - Don't log at error level on client mistakes (4xx) — that's an
        alert-fatigue vector that buries real errors; reserve `error!` for
        operator-actionable events.
      
      ## 9. Running external programs — `std::process::Command`
      
      `Command` takes a program plus an argument vector and **there is no shell**: the
      docs are explicit that "shell syntax like quotes, escaped characters, word
      splitting, glob patterns, variable substitution, etc. have no effect". That makes
      the classic injection hard to write by accident and moves the risk somewhere else.
      Behaviour below was **measured on rustc 1.97.1 / tokio 1.53, macOS** unless a doc
      is quoted; re-check the platform-specific items on your target.
      
      **R9.1 — Arguments are never split, by either API.** `.arg("-l -a")` passes the
      single argument `-l -a` (measured), and `.args([..])` merely iterates — neither
      splits on whitespace. The Rust mistake is therefore the *inverse* of the shell
      one: a string assembled as though it were a command line arrives as one nonsense
      argument, and `Command::new("ls -l")` looks for a program literally named `ls -l`.
      That failure is loud and harmless. The dangerous spelling is the one that puts a
      shell back:
      
      ```rust
      // BAD — a shell, and therefore injection, is back
      Command::new("sh").arg("-c").arg(format!("convert {path} out.png"))
      // GOOD — absolute program, argv, end-of-options, no shell
      Command::new("/usr/bin/convert").arg("--").arg(&path).arg("out.png")
      ```
      
      **R9.2 — Argument injection survives argv.** A value beginning with `-` becomes a
      flag. Pin your own flags first, then `--`, then validated operands; prefer a
      library binding to a CLI wrapper for attacker-influenced parameters.
      `sota-sandboxing` rules/04 §5 lists the exec-capable argument gadgets
      (`find -exec`, `tar --checkpoint-action`, `ssh -o ProxyCommand`, …) to check for.
      
      **R9.3 — Windows `.bat`/`.cmd` is a documented exception (CVE-2024-24576).**
      `cmd.exe` and batch files decode their command line non-standardly, so argv-safety
      does not hold there: before **Rust 1.77.2**, passing untrusted arguments to a batch
      file could run arbitrary shell commands. The fix did not make escaping safe — the
      standard library now **returns an `InvalidInput` error when it cannot safely escape
      an argument**, and the current `Command` docs still carry the warning that for
      `cmd.exe` "a malicious argument can potentially run arbitrary shell commands". So:
      MSRV **≥ 1.77.2** for anything that may run on Windows, propagate that
      `InvalidInput` rather than unwrapping it, and treat `CommandExt::raw_arg` as
      trusted-input-only.
      
      **R9.4 — Dropping a `Child` neither kills nor reaps it.** "There is no
      implementation of `Drop` for child processes, so if you do not ensure the `Child`
      has exited then it will continue to run, even after the `Child` handle to the child
      process has gone out of scope" — measured: still alive 600 ms after the handle was
      dropped. The same docs warn that a terminated-but-unwaited process "is still around
      as a *zombie*" and that too many "may exhaust global resources (for example process
      IDs)". So an error path that returns early while holding a `Child` leaks a *running
      process*, not just a handle — and `?` makes that the easy path to write. Own the
      child on every exit route, including the error and cancellation ones.
      
      **R9.5 — `std` has no timeout, and a tokio timeout does not kill the child.**
      Neither `wait()` nor `wait_with_output()` takes a deadline, and `wait_with_output()`
      waits for **EOF on the pipes**, which a grandchild that inherited them can hold open
      indefinitely. Measured, and this is where Rust differs from Go (whose `Wait` blocks
      past context cancellation unless `cmd.WaitDelay` is set):
      `tokio::time::timeout(2s, child.wait_with_output())` **does** fire at 2.0 s under
      exactly that pipe-holding grandchild, so Rust needs no `WaitDelay` equivalent.
      But firing only cancels *your future* — the child and its grandchild keep running.
      Pair the deadline with `.kill_on_drop(true)` (measured: the child is gone 600 ms
      after the handle drops) or kill and `wait()` explicitly. In sync code the
      long-standing option is the `wait-timeout` crate — mature rather than active (last
      release 2025-02, still 0.2.x, ~50M recent downloads), so check its upstream health
      before adopting it (`sota-devsecops` rules/10 §5); otherwise use a supervisor
      thread you actually join. Never abandon a thread parked in `wait()`. Killing the
      direct child does not signal its *group*: for that, spawn it into its own group
      with `process_group(0)` (Unix, stable since **1.64**) and signal the group.
      
      **R9.6 — Output is buffered without a cap.** `output()`/`wait_with_output()`
      collect the child's stdout into a `Vec<u8>` with no limit — measured, 5 MB of
      `/dev/zero` buffered without complaint, and a hostile child can make that
      unbounded. For anything attacker-influenced, take `Stdio::piped()` and read with an
      explicit `.take(MAX)`, or send output to a file or `Stdio::null()`.
      
      **R9.7 — Environment and program resolution both have sharp edges.** The child
      inherits the parent's environment by default. `env_clear()` gives it **zero**
      variables (measured) — but a *bare* program name still resolves, because with
      `PATH` removed `execvp` falls back to an OS-defined default (the docs say typically
      `/bin:/usr/bin`, "not the parent's `PATH`"): measured, bare `uname` still ran while
      a binary present only in the process's working directory did **not**, so that
      fallback did not include `.` here. Do not rely on either half — **pass an absolute
      path**. Relative program paths are worse: the docs call the interpretation relative
      to the parent's cwd versus `current_dir` "platform specific and unstable" and
      recommend `canonicalize`; measured on macOS, `Command::new("./p").current_dir(d)`
      ran `d/p`. Use `uid`/`gid` to drop privilege where relevant (both trigger
      `setgroups(0, NULL)` unless groups are set explicitly, dropping supplementary
      groups). `CommandExt::pre_exec` is `unsafe` for a real reason — the closure runs
      after `fork` in the child, where "normal operations like `malloc`, accessing
      environment variables through `std::env` or acquiring a mutex are not guaranteed to
      work"; keep it async-signal-safe or use a purpose-built crate.
      
      For the isolation the child itself needs — seccomp/Landlock, fd-only interfaces,
      memory budgets — see `sota-sandboxing` rules/04 §5 and rules/02 R7.2a.
      
      
      ## Audit checklist
      
      - [ ] CI has `cargo deny check` (or `cargo audit`) on PRs **and** a scheduled
            run; `deny.toml` ignore entries have reasons + expiry. Missing = High
            for deployed services.
      - [ ] `Cargo.lock` committed; `rg 'git = "' Cargo.toml */Cargo.toml` — git
            deps without `rev =` pin = Medium; wildcard versions = Medium.
      - [ ] `cargo vet` (or documented dep-review process) for new dependencies;
            build.rs / proc-macro deps enumerated and reviewed.
      - [ ] Arithmetic on input: `rg '(len|size|count|offset|idx)\s*[+*-]' -t rust`
            near parsing code — wrapped math on untrusted values = High;
            `rg 'as u(8|16|32)|as usize' -t rust` in protocol code for truncating
            casts; release profile `overflow-checks` decision documented.
      - [ ] Panic surface in handlers: run rules/02 checklist scoped to
            request-reachable code; `rg 'with_capacity\(' -t rust` where the arg
            derives from input = High.
      - [ ] `rg '#\[derive\(.*Debug' -t rust` on structs with `password|secret|key|
            token` fields; `rg '==' -t rust` comparing MACs/tokens (want `ct_eq`);
            secrets not in `SecretString`/`Zeroizing` = Medium-High.
      - [ ] `rg 'thread_rng|SmallRng|StdRng::seed' -t rust` in key/nonce/token
            generation paths → require OsRng/getrandom.
      - [ ] `rg 'untagged' -t rust` on network-facing types = review for DoS;
            `rg 'deny_unknown_fields'` absent on auth/config types = Low-Medium;
            body-size limits present at every ingest (axum `DefaultBodyLimit`,
            manual caps) — absent = High.
      - [ ] `rg 'format!\(.*(SELECT|INSERT|UPDATE|DELETE|WHERE)' -t rust -i` = High;
            `rg '\.join\(' -t rust` on user-supplied path segments without
            canonicalize+prefix check = High.
      - [ ] Fuzz targets exist for each untrusted-input parser; absent on a
            network-facing parser = Medium.
      - [ ] Release CI uses `--locked`; binaries built from tags in CI;
            `cargo auditable` (or SBOM) for deployed artifacts = recommended.
      - [ ] Logs: `rg 'info!|warn!|error!|debug!' -t rust` near auth/headers — no
            `Authorization`/`Cookie`/body logging; user strings as structured
            fields, not message interpolation.
      - [ ] Subprocess: `rg 'Command::new\("(sh|bash|cmd|powershell)"' -t rust` and
            `rg '\.arg\("-c"\)' -t rust` — a shell with any interpolated value = Critical.
            `rg 'Command::new\(' -t rust` for non-absolute program names on
            attacker-reachable paths = Medium (R9.1, R9.7).
      - [ ] Every spawned `Child` is killed **and** waited on every exit path, including
            `?` early-returns and cancellation — `rg 'spawn\(\)' -t rust` and read the
            error paths; a dropped `Child` keeps running and then becomes a zombie
            (R9.4). Deployed code targeting Windows declares MSRV **≥ 1.77.2** (R9.3).
      - [ ] Every subprocess wait has a deadline **and** a kill: a bare
            `wait()`/`wait_with_output()` = High on any attacker-influenced child, and a
            `tokio::time::timeout` without `.kill_on_drop(true)` (or an explicit kill)
            leaves the child running when it fires (R9.5).
      - [ ] `rg 'output\(\)|wait_with_output' -t rust` where the child's output size is
            not bounded by the caller = Medium (R9.6); `rg 'pre_exec' -t rust` — the
            closure must be async-signal-safe (R9.7).
      - [ ] Severity calibration: RCE/memory corruption = Critical; authn/authz
            bypass, SQLi, traversal = Critical/High; attacker-reachable panic or
            unbounded allocation = High; missing CI audit gates = Medium-High;
            hygiene (locks, lints) = Low-Medium.
      
    • 06-performance.md 12.1 KB
      # 06 — Performance
      
      Rust is fast by default; most "slow Rust" is accidental allocation, debug
      builds, or architecture — not missing micro-tricks. The discipline: measure,
      fix the proven bottleneck, re-measure. Never trade soundness or clarity for an
      unmeasured win (see rules/03 §1 on unsafe "optimizations").
      
      ## 1. Measure first — profiling toolbox
      
      - **Always profile release builds** with debug symbols:
      
      ```toml
      [profile.release]
      debug = true            # or: [profile.profiling] inherits = "release", debug = true
      ```
      
        The #1 perf bug report is "Rust is slow" on a debug build (10–100x off).
      - **CPU profiles**: `samply record ./target/release/app` (works macOS+Linux,
        Firefox Profiler UI) or `perf record -g --call-graph dwarf` + `perf report`
        on Linux; `cargo flamegraph` for one-command flamegraphs. Wide flat flames =
        death-by-allocation/memcpy; deep narrow = algorithmic hotspot.
      - **Microbenchmarks**: `criterion` (statistical, regression-detecting) or
        `divan` (lighter, allocation counting). Use `std::hint::black_box` to stop
        the optimizer deleting your benchmark. Bench realistic input sizes —
        O(n²) hides at n=10.
      - **Allocation profiling**: `dhat-rs` (heap profiling as a test harness),
        `heaptrack` (Linux); divan's `AllocProfiler` for per-bench alloc counts.
      - **Async**: tokio-console for task-level stalls (rules/04); `tracing` spans +
        `tracing-timing`/OTel histograms for production latency attribution.
      - Lock contention: `perf lock` / mutex wrappers with metrics; high sys-time +
        low throughput on many cores is the contention signature.
      
      ## 2. Allocation reduction — the usual 80%
      
      Allocations (and the copies feeding them) dominate most non-numeric Rust
      hotspots.
      
      - **Borrow in signatures** (`&str`, `&[T]` — rules/01 §1) so callers don't
        allocate to call you.
      - **Reuse buffers in loops**:
      
      ```rust
      // BAD: fresh String/Vec per iteration
      for record in records { let mut line = String::new(); render(record, &mut line); out.write_all(line.as_bytes())?; }
      
      // GOOD: clear-and-reuse keeps capacity
      let mut line = String::new();
      for record in records {
          line.clear();
          render(record, &mut line);
          out.write_all(line.as_bytes())?;
      }
      ```
      
      - **`with_capacity`** when the size is known (`Vec`, `String`, `HashMap`);
        growth is amortized but reallocation+memcpy of large buffers still hurts.
        (Untrusted sizes: cap it — rules/05 §6.)
      - **`Cow<'_, str>`** for transform-sometimes functions: borrow the common
        case, allocate only when modified. Don't `Cow` everything — it infects
        signatures; use where the borrow-rate is meaningfully high.
      - **`SmallVec`/`ArrayVec`/inline strings (`compact_str`)** only with profile
        evidence that small-collection allocs dominate; `SmallVec` adds branch +
        size costs and is a pessimization when it spills or gets moved a lot.
      - **Avoid intermediate collections**: `collect()` then re-iterate is two
        passes + an allocation; keep it lazy until the terminal op. `format!` into
        `write!(buf, ...)`; `Vec<String>` + `join` into a single fold/`itertools::join`
        when hot; `to_string()` in comparisons (`x.to_string() == y`) never.
      - String building: `push_str`/`write!` over repeated `+`/`format!`.
      - Interning/arenas (`bumpalo`, `typed-arena`, `lasso`) for parser/compiler
        workloads with many small same-lifetime objects — wholesale drop, zero
        per-object free.
      
      ## 3. Accidental clones & copies
      
      - Hot-path `.clone()` of `String`/`Vec`/maps found in a profile: restructure
        ownership (rules/01 §1) — pass borrows down, return owned up, `Arc<str>`/
        `Arc<[T]>` for shared immutable data cloned often (refcount bump vs deep
        copy; also half the size of `Arc<String>`'s double indirection).
      - Large types moved by value are memcpys: 1KB struct passed through 5 call
        frames = 5KB of copying. `clippy::large_types_passed_by_value`,
        `clippy::large_enum_variant` (box the big variant), `clippy::large_stack_arrays`.
        Async: oversized futures (big locals held across await) — `clippy::large_futures`,
        box the future or shrink the locals.
      - Hidden copies: `*slice.to_vec()` where a borrow works, `as_bytes().to_vec()`,
        `iter().cloned()` where `iter()` + borrows suffice (`clippy::cloned_instead_of_copied`
        for Copy types — `copied()` is explicit and free).
      - Derive `Copy` for small (≤ ~16-byte) plain-data types — clone noise gone,
        and the compiler stops you when it grows? It won't — so re-check `Copy`
        types' sizes when fields are added (`static_assertions::assert_eq_size!`).
      
      ## 4. Iterator fusion & loop shape
      
      - Iterator adapter chains compile to single fused loops — no intermediate
        materialization, bounds checks elided. Trust the chain; verify with a
        benchmark when in doubt, not by rewriting to indices.
      - Keep bounds-check elision intact: iterate (`for x in &xs`, `zip`) instead of
        `xs[i]` under a manually-checked index; when indexing is unavoidable, hoist
        one `assert!(n <= xs.len())` so LLVM elides the per-element checks.
      - `extend` over push-in-loop (`vec.extend(iter)` can use size hints and
        specialized memcpy paths); `collect::<Vec<_>>()` from a sized iterator
        preallocates exactly.
      - `chunks_exact`/`array_chunks` over `chunks` in SIMD-able inner loops (the
        exact variant lets LLVM vectorize without remainder branches).
      - Data layout beats instruction tweaks: struct-of-arrays for scanned columns,
        `Vec<T>` over `Vec<Box<T>>` (pointer-chasing kills cache), sort+binary-search
        or `HashMap` with `FxHash`/`ahash` (default SipHash is DoS-resistant but
        slow — switch only for non-attacker-controlled keys, see rules/05).
      - Parallelize embarrassingly-parallel CPU work with `rayon`
        (`par_iter`) — after confirming single-thread is actually optimized;
        parallel O(n²) is still O(n²).
      
      ## 5. Release profile settings
      
      ```toml
      [profile.release]
      lto = "thin"            # near-fat-LTO wins, fraction of the compile cost; "fat" for final binaries if it measures better
      codegen-units = 1       # better codegen, slower builds — for release artifacts
      # opt-level = 3 is default; try "s"/"z" for size-bound targets (embedded/wasm)
      strip = "symbols"       # smaller binaries (keep debug=true in a separate profiling profile)
      
      # panic = "abort"       # smaller/faster, no unwinding — decide per rules/02 §5
                              # (kills catch_unwind containment; not for libs that embed elsewhere)
      ```
      
      - `panic = "abort"`: ~smaller binary, removes landing pads; cost = no panic
        containment (rules/02/05). Choose deliberately for servers; fine for CLIs.
      - `overflow-checks = true` in release for security-sensitive services
        (rules/05 §3) — measure, it's typically <2%.
      - Target-specific codegen for owned deployments:
        `RUSTFLAGS="-C target-cpu=native"` (or a fixed `target-cpu=x86-64-v3`) —
        unlocks AVX2+ vectorization; never for distributed portable binaries.
      - **PGO** for the last 5–15% on hot services: `cargo pgo` (or manual
        `-Cprofile-generate` → run representative load → `-Cprofile-use`); BOLT on
        top for large binaries. Only worth wiring once the easy wins are done.
      - Build-time hygiene: keep one `profiling` profile; don't ship `debug = true`
        symbols accidentally in size-sensitive contexts (use `strip` + split-debuginfo).
      
      ## 6. Allocators & zero-copy I/O
      
      - **Global allocator swap** is the cheapest multithreaded-throughput win in
        alloc-heavy services: `mimalloc` or `tikv-jemallocator` as `#[global_allocator]`
        often yields 5–30% on multithreaded alloc-heavy loads vs system malloc
        (glibc malloc contends; macOS/musl mallocs are slow). Measure with your
        workload; jemalloc additionally gives heap profiling (`jeprof`) in prod.
        musl-target deployments almost always want this (musl malloc is a known
        multithreaded bottleneck).
      - **`bytes::Bytes`** for network payloads: cheaply cloneable, sliceable,
        refcounted views — one recv buffer shared across framing/parsing/handlers
        without copies. Pair with `tokio_util::codec` framing.
      - **Zero-copy parsing**: borrow from the input (`&'a str` fields,
        `#[serde(borrow)]` with `serde_json::from_slice`) instead of owning
        `String` fields — turns deserialization allocations into pointer
        arithmetic; `zerocopy`/`bytemuck` for fixed-layout binary views (rules/03
        for the safety side).
      - **Writes**: unbuffered `write!` to a raw `File`/`TcpStream` syscalls per
        call — wrap in `BufWriter` (and remember to flush; dropped `BufWriter`
        errors are swallowed). `vectored writes` (`write_vectored`) for
        header+body patterns.
      - mmap (`memmap2`) for large read-mostly files beats read-into-Vec; the
        safety caveat (file truncated under you = UB) is real — confine to files
        you control or accept advisory locking.
      
      ## 7. Build-time performance (developer loop)
      
      Slow builds are a perf problem too — they tax every iteration.
      
      - `cargo check`/rust-analyzer for the inner loop, not `cargo build`.
      - Linker: `rust-lld` is already the default on `x86_64-unknown-linux-gnu`
        since Rust 1.90 (opt-out: `-C linker-features=-lld`); `mold` is the further
        Linux upgrade, and manual `-C link-arg=-fuse-ld=lld` pays only on targets
        where lld isn't the default yet.
      - Split heavy generics: generic shells delegating to non-generic inner fns
        (`fn run(p: impl AsRef<Path>) { fn inner(p: &Path) {...} inner(p.as_ref()) }`)
        cut monomorphization bloat (binary size AND compile time).
      - `cargo build --timings` to find the long pole; feature-trim heavy deps
        (`tokio` full vs needed features, `syn` full); move optional integrations
        behind features (rules/07 §5).
      - Workspace split so hot-edit crates are leaves, not roots (rules/01 §9);
        proc-macro and build.rs crates dominate cold builds — audit their cost.
      
      ## 8. Don'ts
      
      - Don't sprinkle `#[inline(always)]` — it defeats the inliner's cost model and
        bloats icache; `#[inline]` only on small cross-crate hot functions (and it's
        unnecessary for generics, which are already monomorphized downstream).
      - Don't micro-optimize before architecture: batching, caching, removing a
        network round-trip, or a better algorithm beats any amount of `SmallVec`.
      - Don't benchmark on laptops with thermal throttling / background noise and
        publish 3% wins; criterion's noise floor on shared CI is ~5%+ — gate
        regressions with thresholds, not single runs.
      - Don't unsafe-away bounds checks without a flamegraph showing them (§1,
        rules/03).
      
      ## Audit checklist
      
      - [ ] Benchmarks exist for claimed-hot code (criterion/divan in `benches/`);
            perf-sensitive PRs include before/after numbers. Claims without
            measurements = Low finding, ask for receipts.
      - [ ] `rg '\.clone\(\)|to_vec\(\)|to_string\(\)|to_owned\(\)' -t rust` scoped
            to loop bodies / per-request paths — review each; clippy
            `redundant_clone`, `cloned_instead_of_copied`, `unnecessary_to_owned`.
      - [ ] `rg 'format!' -t rust` in hot loops (want `write!` into reused buffer);
            `rg 'String::new\(\)|Vec::new\(\)' -t rust` inside loops (want hoisted
            clear-and-reuse or `with_capacity`).
      - [ ] `rg 'collect::<Vec' -t rust` immediately followed by `.iter()`/`into_iter()`
            — needless materialization (`clippy::needless_collect`).
      - [ ] `[profile.release]` reviewed: LTO set, codegen-units decision, strip,
            panic-strategy documented; profiling profile with `debug = true` exists.
      - [ ] `rg 'inline\(always\)' -t rust` — each needs benchmark justification.
      - [ ] Hash maps on hot non-adversarial keys still on SipHash (perf left on
            table) or, inversely, `FxHash/ahash` on attacker-controlled keys
            (HashDoS — High, see rules/05).
      - [ ] `clippy::large_enum_variant`, `large_types_passed_by_value`,
            `large_futures`, `needless_range_loop`, `or_fun_call`, `manual_memcpy`
            enabled; `Vec<Box<T>>`-style pointer-chasing layouts in scanned data.
      - [ ] Alloc-heavy multithreaded service on default allocator — try
            mimalloc/jemalloc with a benchmark; musl deployments especially.
      - [ ] `rg 'File::create|TcpStream' -t rust` write paths without `BufWriter`;
            `rg 'BufWriter' -A20` missing explicit `flush()` before drop.
      - [ ] Network parsing copying into owned `String`/`Vec` where
            `Bytes`/`#[serde(borrow)]` would zero-copy.
      - [ ] CI bench regression gate (criterion + `critcmp`/`cargo bench` artifacts,
            or codspeed/iai-callgrind instruction counting for noise-free CI).
      
    • 07-tooling-ci.md 17.6 KB
      # 07 — Tooling, CI & Crate Hygiene
      
      A SOTA Rust repo is recognizable from its CI config alone: lints are deny,
      tests run under nextest, MSRV is tested not guessed, features are additive,
      and docs build clean. This file defines that baseline.
      
      ## 1. Clippy — policy, not vibes
      
      Configure lints **in Cargo.toml** (`[lints]`, inherited workspace-wide), not
      scattered `#![allow]`s:
      
      ```toml
      # workspace Cargo.toml
      [workspace.lints.rust]
      unsafe_code = "warn"               # forbid in crates that can (rules/03)
      missing_docs = "warn"              # libraries: consider deny
      unused_must_use = "deny"
      
      [workspace.lints.clippy]
      all = { level = "warn", priority = -1 }
      pedantic = { level = "warn", priority = -1 }
      # triaged pedantic opt-outs — each with a reason, reviewed yearly:
      module_name_repetitions = "allow"  # naming convention conflicts
      must_use_candidate = "allow"       # too noisy for app crates
      # hard floors:
      unwrap_used = "deny"               # production crates (rules/02)
      dbg_macro = "deny"
      todo = "deny"
      undocumented_unsafe_blocks = "deny"
      await_holding_lock = "deny"
      
      # member crates: [lints] workspace = true
      ```
      
      - **Pedantic triage**, not pedantic-blanket: enable the group, then `allow`
        specific lints with a comment — this catches new pedantic lints on toolchain
        updates instead of opting out of the future.
      - **Deny-warnings in CI only**: `cargo clippy --all-targets --all-features --
        -D warnings` in CI; locally keep warn so WIP compiles. For build warnings,
        Cargo's `build.warnings = "allow"/"warn"/"deny"` config (stable since Rust
        1.97) is the first-class knob — set `CARGO_BUILD_WARNINGS=deny` in the CI
        environment, not in committed config, so local builds stay on warn.
        Hardcoding `#![deny(warnings)]` in source breaks builds on every new rustc
        lint — don't.
      - Per-site `#[allow(clippy::xyz, reason = "...")]` (lint reasons are stable)
        over module-level allows; an allow without a reason is a finding.
      - Run clippy on the same pinned toolchain as the build (lint sets drift across
        versions).
      
      ### 1a. On a constrained target, a style lint's *premise* may be false
      
      Clippy's advice is near-universally correct because it is written against the
      assumptions of a hosted target: an 8 MB stack, an allocator, a real `std`. In an
      **eBPF program, an embedded or `no_std` crate, a WASM module, a kernel module or an
      interrupt handler**, one of those assumptions is gone, and a lint that encodes it
      silently spends a resource the compiler will not warn you about.
      
      Worked case. `clippy::needless_borrows_for_generic_args` (style, warn-by-default —
      *"taking a reference that is going to be automatically dereferenced"*, verified in
      clippy 0.1.97) fired on a map `insert(&key, &now, 0)` inside a BPF program and
      suggested the owned form. `insert` takes `impl Borrow<K>`, so the owned call
      monomorphises to `Borrow<K> for K` and copies the 52-byte key onto the stack. A BPF
      program gets **512 bytes for the whole call chain** — `MAX_BPF_STACK` in the kernel's
      `include/linux/filter.h` — of which a helper already held 344. The program stopped
      loading:
      
      ```text
      combined stack size of 2 calls is 544. Too large
      ```
      
      (that string is emitted by the kernel verifier, `kernel/bpf/verifier.c`.)
      
      Rules:
      
      - **Before taking a lint suggestion in such a crate, name the resource it spends.**
        `Copy` means "cheap to copy" *on an 8 MB stack*; it says nothing about a 512-byte
        one. The same applies to lints that suggest an owned value, an iterator adaptor, a
        `format!`, or anything that inlines a larger frame.
      - **Do not let the word "mechanical" stand in for the analysis.** *"Mechanical",
        "trivial", "just a rename", "style only"* are classifications that license skipping
        evaluation, and they are applied **before** the evaluation that would justify them.
        Treat them as a prompt to check, not as a conclusion — this is the linguistic tell
        that a decision was made without being made.
      - **Prefer removing the lint's premise over silencing it.** Dropping `Copy` from an
        oversized key type stops the lint firing *and* stops the next contributor
        reintroducing the copy — better than `#[allow]`, which only silences this site. But
        do not then claim the type *enforces* what it merely discourages.
      - **Set the policy at the crate, not the call site.** A constrained crate's
        `[lints.clippy]` should `allow` the specific hosted-assumption lints with a reason
        naming the constraint, so the rest of the group keeps working.
      - **Only a gate that loads or runs the artifact can catch this** — `fmt`, `clippy`
        and lint passes all stop at the compiled object and were green on the broken one.
        `sota-devsecops` rules/09 §2a.
      
      ### 1b. Source shape is not a proxy for compiled behaviour
      
      §1a is a *lint's* premise being false on a constrained target. This is **your own reasoning's**
      premise being false, and it is the harder one because nothing fires.
      
      Rust makes source structure feel like it maps onto machine behaviour — ownership, scopes and
      struct layout usually do. At `opt-level=3` with LTO they need not, and on a target where the
      limit is checked at **load** time there is no compile error to correct you.
      
      Measured on rustc 1.97.1: a plain helper called once from a function disappears entirely at
      `opt-level=3` — the symbol is absent from the emitted assembly, while an `#[inline(never)]`
      neighbour in the same file still shows three call sites (the control that proves the absence
      is real, not a search artefact).
      
      ```console
      $ rustc -C opt-level=0 --emit asm t.rs && grep -c fill t.s   # 2  — the boundary exists
      $ rustc -C opt-level=3 --emit asm t.rs && grep -c fill t.s   # 0  — it does not
      ```
      
      Field-reported consequence: a helper was extracted from an eBPF program specifically so each
      transport would own one large stack local instead of two, and that reasoning was written into
      a commit message as fact. The verifier's own numbers before and after were **identical** —
      `stack depth 136+0+344+0` both times. The refactor was sound; the justification was fiction.
      
      - **A claim about memory layout cites a measurement from the toolchain that enforces the
        limit** — the verifier's report, `-Zprint-type-sizes`, a linker map, `--emit asm`. Write it
        as *"measured X on Y"* or do not write it.
      - **Extracting a function does not create a scope the optimiser must honour.** If you need
        two lifetimes not to overlap, you need something the compiler cannot inline through — a
        separate program, an explicit `#[inline(never)]`, or a different data flow.
      - **The numbers to compare are before and after on the same toolchain**, not one reading and
        an argument. `sota-code-security` rules/15 §2a: a *pass* is not a number.
      
      ## 2. rustfmt — zero-config by default
      
      - `cargo fmt --check` in CI. Default style; a `rustfmt.toml` should contain
        only deliberate deviations (e.g. `imports_granularity = "Crate"`,
        `group_imports = "StdExternalCrate"` — nightly-only options mean fmt runs
        on nightly toolchain in CI if used).
      - Never hand-format against rustfmt; never argue style in review — that's the
        tool's job.
      
      ## 3. Tests & cargo-nextest
      
      - `cargo nextest run` over `cargo test`: process-per-test isolation (one
        test's panic/env pollution can't poison others), better parallelism, flaky
        retries with detection (`--retries N` + reporting), per-test timeouts
        (`slow-timeout` + `terminate-after` — hangs fail instead of stalling CI),
        JUnit output. Note: nextest doesn't run doctests — keep a separate
        `cargo test --doc` step.
      - Test taxonomy: unit tests in-module (`#[cfg(test)]`), integration tests in
        `tests/` (compile as separate crates — each file is a binary; group to keep
        link time sane), doctests on every public API example (they're the only
        examples guaranteed to compile).
      - Property tests (`proptest`) for parsers/serializers/invariant-heavy code;
        snapshot tests (`insta`) for rendered output; `loom` for atomics (rules/03);
        fuzz for untrusted-input parsers (rules/05).
      - Coverage: `cargo llvm-cov nextest` — track trend, don't worship the number.
      
      ## 4. MSRV policy
      
      - Declare it: `rust-version = "1.85"` in `[package]`/`[workspace.package]` —
        cargo refuses to build on older toolchains with a clear error instead of
        cryptic syntax failures.
      - **Test it**: a CI job building with the pinned MSRV toolchain
        (`cargo +1.85 check --all-features`); `cargo msrv verify` / `cargo msrv find`
        to maintain. An untested MSRV claim is false within two dependency bumps —
        note deps' MSRV bumps in *minor* versions can break you; this is what the
        MSRV-aware resolver (Rust 1.84+, `resolver.incompatible-rust-versions =
        "fallback"`) mitigates.
      - Policy in README/CONTRIBUTING: which versions you support and whether an
        MSRV bump is a semver-minor (common convention) — pick one and say it.
      - Applications: pin the toolchain exactly with `rust-toolchain.toml`
        (reproducible builds, same clippy everywhere). Libraries: MSRV floor +
        stable-latest CI matrix.
      
      ## 5. Feature flag hygiene
      
      Features must be **additive**: enabling a feature may only add API/behavior,
      never remove or change it. Cargo unifies features across the graph — if crate
      A needs `foo` without feature X and crate B enables X, A gets X too; mutually
      exclusive features break the ecosystem.
      
      ```toml
      [features]
      default = ["std"]
      std = []
      serde = ["dep:serde", "uuid?/serde"]   # dep: = no implicit feature for optional deps
                                              # ?/ = enable serde on uuid only if uuid is on
      full = ["serde", "metrics"]            # convenience aggregate, still additive
      ```
      
      - `dep:` syntax for optional dependencies (no accidental public
        `features = ["serde"]` exposure of dep names); `pkg?/feat` for weak
        (conditional) feature forwarding.
      - No `no-std`-style **negative features** (`no_std = []` that removes things);
        model it as positive `std` in `default`, with `default-features = false`
        consumers opting in.
      - **Test the matrix**: `cargo hack check --feature-powerset --depth 2` (or at
        minimum: `--no-default-features`, default, `--all-features`) in CI — feature
        combinations nobody compiles are broken combinations.
      - Don't gate public API breaking-ly (a feature that changes a type's layout or
        a function's signature = non-additive = ecosystem breakage).
      - Keep default features lean; heavyweight integrations (TLS stacks, runtimes)
        always optional and documented.
      - cfg dispatch: prefer built-in `cfg_select!` (stable since Rust 1.95,
        compile-time match on cfgs) over adding the `cfg-if` dependency in new code.
      
      ## 6. Documentation discipline (docs.rs)
      
      - Every public item documented; `#![warn(missing_docs)]` on libraries.
        First line = one-sentence summary (it's the item's listing line); then
        details, then `# Examples` (doctested), `# Errors` (when `Result`),
        `# Panics` (every documented panic path — clippy `missing_panics_doc`),
        `# Safety` (every `unsafe fn` — rules/03).
      - Crate root (`lib.rs`) gets the long-form intro: what/why/quickstart —
        `#![doc = include_str!("../README.md")]` keeps README and docs in sync
        (and doctests the README's examples).
      - Feature-gated items: build docs.rs with all features and label them:
      
      ```toml
      [package.metadata.docs.rs]
      all-features = true
      rustdoc-args = ["--cfg", "docsrs"]
      ```
      
      ```rust
      #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]   // renders "Available on feature serde"
      ```
      
      - Intra-doc links (`[`Vec`]`, `[`crate::Config`]`) over bare URLs — checked at
        build (`rustdoc::broken_intra_doc_links` is deny-worthy).
      - CI: `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features` so doc
        rot fails the build.
      
      ## 7. Edition 2024 notes
      
      Current edition as of mid-2026; new code starts here (`edition = "2024"`,
      Rust ≥1.85; verify latest stable at releases.rs). The next edition is expected
      ~2027 on the usual three-year cadence — nothing to migrate toward yet.
      Migration: `cargo fix --edition` then review. Key changes that affect rules in
      this skill:
      
      - `unsafe_op_in_unsafe_fn`: unsafe fns need explicit inner `unsafe {}` blocks
        (rules/03 §2 — good: per-obligation SAFETY comments).
      - `static_mut_refs` denied: references to `static mut` rejected — migrate to
        atomics/`OnceLock`/`Mutex` (rules/03 §3).
      - **RPIT lifetime capture**: return-position `impl Trait` now captures *all*
        in-scope lifetimes by default (was: only named ones) — use
        `+ use<'a, T>` precise-capture syntax to opt out; check existing APIs whose
        returned opaques suddenly borrow more.
      - `unsafe extern` blocks and unsafe attributes (`#[unsafe(no_mangle)]`) —
        FFI declarations are now explicitly trust-me-marked.
      - Temporaries in `if let`/match scrutinees drop sooner (tail-expression
        temporary scope) — re-check `RefCell`/lock guards in conditions.
      - `Future`/`IntoFuture` in prelude; `gen` keyword reserved.
      - Resolver v3 (MSRV-aware) default with edition 2024 (§4).
      
      ## 8. Semver enforcement & release automation
      
      - **`cargo semver-checks`** in CI for published libraries: diffs the public
        API against the last release and fails on undeclared breaking changes
        (removed items, changed signatures, new non-defaulted trait methods, auto
        trait leaks like a type silently becoming `!Send`). Run on release PRs at
        minimum; it catches the breakage humans reliably miss.
      - Semver hazards it can't fully see — review manually: blanket impl
        additions, `#[non_exhaustive]` removal (breaking to *remove*), MSRV bumps
        (declare policy, §4), feature removals/renames (features are public API),
        and doc-promised behavior changes.
      - **Release automation**: `release-plz` (or `cargo-release`) — version bump
        from conventional commits, changelog generation, tag, `cargo publish` with
        `--locked` from CI with a scoped registry token. Manual `cargo publish`
        from laptops drifts from the audited lockfile and skips the gates.
      - Changelog discipline: keep a human-readable CHANGELOG.md (generated or
        curated); "see git log" is not a changelog. Yanked releases
        (`cargo yank`) for published-broken versions — yank doesn't delete, it
        stops *new* resolution.
      - Binaries: `cargo dist` (or equivalent) for multi-target release artifacts,
        checksums, and installers from one config; cross-compilation via `cross`
        or toolchain targets exercised in CI *before* the release tag, not during.
      
      ## 9. The CI baseline (copy this shape)
      
      ```yaml
      jobs:
        check:   # fmt + clippy + doc, pinned stable toolchain
          - cargo fmt --check
          - cargo clippy --workspace --all-targets --all-features -- -D warnings
          - RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --all-features
        test:
          - cargo nextest run --workspace --all-features
          - cargo test --doc --workspace
        msrv:
          - cargo +${MSRV} check --workspace --all-features
        features:
          - cargo hack check --workspace --feature-powerset --depth 2
        supply-chain:        # rules/05
          - cargo deny check
        unsafe-crates-only:  # rules/03
          - cargo +nightly miri test -p crate-with-unsafe
      ```
      
      Plus scheduled: `cargo audit` (new advisories), fuzz jobs, bench regression
      gate (rules/06). Cache with `Swatinem/rust-cache`; pin action SHAs (rules/05).
      
      ## Audit checklist
      
      - [ ] `[lints]` table present and workspace-inherited; `rg '#!\[allow' -t rust`
            — blanket crate-level allows without reasons = Low each, pattern = Medium.
      - [ ] CI runs clippy with `-D warnings` on `--all-targets --all-features`;
            source does NOT hardcode `#![deny(warnings)]`.
      - [ ] **Constrained crates (eBPF, `no_std`, embedded, WASM, kernel) — is any lint
            suggestion taken on a false premise?** (§1a) For each accepted style fix in such a
            crate, name the resource it spends (stack frame, allocation, code size); hosted-
            assumption lints are `allow`ed at the crate with a reason naming the constraint.
            A change described as "mechanical" in one of these crates is unreviewed, not safe.
      - [ ] **Any claim about stack, size or layout that cites the SOURCE rather than a
            measurement?** (§1b) At `opt-level=3` with LTO a plain helper's boundary does not exist
            in the artefact — measured, a single-call helper vanishes entirely — so "I extracted it
            so the locals would not overlap" is not evidence. Cite the enforcing toolchain's own
            number, before and after.
      - [ ] **Does any gate load or run the artifact?** (§1a) `fmt`, `clippy` and lint passes
            all stop at the compiled object; a crate whose failures appear at load or verify
            time is ungated until one gate executes it on the real target.
      - [ ] `cargo fmt --check` green and in CI; `rustfmt.toml` deviations are
            deliberate and few.
      - [ ] Tests: nextest in CI + separate doctest step; per-test timeout
            configured; `rg '#\[ignore\]' -t rust` — ignored tests have reasons.
      - [ ] `rust-version` declared AND exercised by a CI job; binaries have
            `rust-toolchain.toml`. `cargo msrv verify` passes.
      - [ ] Features: `rg 'no[-_](std|default)' Cargo.toml` style negative features
            = Medium (non-additive); optional deps using `dep:`; feature matrix job
            (`cargo hack`) present; `--no-default-features` builds.
      - [ ] Docs: `missing_docs` on lib crates; `# Errors`/`# Panics`/`# Safety`
            sections present (`clippy::missing_errors_doc`, `missing_panics_doc`,
            `missing_safety_doc`); docs.rs metadata for feature-gated crates;
            doc job with `-D warnings`.
      - [ ] Edition: new crates on 2024; pre-2024 crates have a migration note or
            reason; post-migration, re-audit `static mut` and RPIT capture changes.
      - [ ] Published libraries: `cargo semver-checks` in release CI; publishing
            automated (`release-plz`/`cargo-release`) with `--locked`, not from
            laptops; CHANGELOG maintained.
      - [ ] Reproducibility: `Cargo.lock` committed, toolchain pinned, CI action
            SHAs pinned, `rust-cache` keyed correctly (not caching stale clippy).
      - [ ] Quick greps: `rg 'dbg!|println!' -t rust -g '!*test*' -g '!*/bin/*'`
            (debug leftovers in libs); `rg 'FIXME|HACK|XXX' -t rust` triaged.
      
  • SKILL.md 10.4 KB
    ---
    name: sota-rust
    description: >-
      State-of-the-art Rust engineering (2026) for writing and auditing Rust code.
      Covers idiomatic ownership and API design, error handling and panic policy,
      unsafe discipline with Miri, async/tokio (cancellation safety, structured
      concurrency, graceful shutdown), security and supply chain (cargo
      audit/deny/vet, integer overflow, serde hardening, zeroize), performance
      (profiling, allocation reduction, release profiles), and tooling/CI (clippy
      policy, nextest, MSRV, feature hygiene, edition 2024). Use when writing new
      Rust code, reviewing or auditing existing Rust, designing crate APIs,
      debugging borrow checker or Send/Sync errors, or hardening Rust services.
      Triggers: Rust, cargo, crate, tokio, unsafe, lifetime, borrow checker,
      clippy, async Rust, Cargo.toml, thiserror, anyhow, serde, Miri, MSRV,
      std::process::Command, subprocess, spawn a process.
    ---
    
    # SOTA Rust (2026)
    
    ## Purpose
    
    This skill encodes the 2026 state of the art for production Rust: the idioms,
    security posture, performance discipline, and CI baseline expected of an
    expert Rust codebase. Baseline as of mid-2026: a recent stable Rust toolchain (verify the current
    release at blog.rust-lang.org),
    edition 2024 (next edition expected ~2027), tokio still 1.x. It serves two modes — **BUILD** (write new code to this
    standard) and **AUDIT** (find where existing code falls short, with severity
    and evidence). The detailed rules live in `rules/*.md`; load only the files
    relevant to the task (see index below). Every rules file ends with an "Audit
    checklist" of grep/clippy patterns — use those verbatim in AUDIT mode.
    
    ## BUILD mode
    
    When writing or modifying Rust code:
    
    1. **Scope the work, load the rules.** Pick the relevant `rules/` files from
       the index. Touching async code? Load 04. Adding a dependency, parsing
       network input, or spawning an external program? Load 05. Writing any `unsafe`? Load 03 — no exceptions.
    2. **Design types first.** Newtypes for domain primitives, errors per
       subsystem (thiserror for libs, anyhow for apps), ownership tree before
       `Arc<Mutex<_>>`, public API minimal and borrowed (`&str`/`&[T]` params).
       Parse, don't validate: constructors enforce invariants.
    3. **Write to the non-negotiables** (bottom of this file) without being asked.
       They are defaults, not suggestions; deviations carry a written
       justification at the site (e.g. `expect` with invariant message,
       `#[allow(lint, reason = "...")]`).
    4. **Wire the scaffolding with the code**, not after: lints in `[lints]`,
       `deny.toml` + `cargo deny` in CI for anything deployed, Miri job if unsafe
       exists, nextest, MSRV declared and tested, benches for claimed-hot paths.
       See rules/07 §9 for the CI shape to copy.
    5. **Verify before claiming done:** `cargo fmt --check`, `cargo clippy
       --all-targets --all-features -- -D warnings`, `cargo nextest run` +
       `cargo test --doc`, and `cargo doc` warning-free for libraries. If you
       wrote unsafe: `cargo +nightly miri test` over it. If you claimed
       performance: show the benchmark.
    6. **Comment intent at decision points** the next reader will question:
       justified clones, cancel-safety of `select!` arms, SAFETY comments,
       channel-capacity choices, poisoning policy.
    
    ## AUDIT mode
    
    When reviewing or auditing existing Rust:
    
    1. **Recon first:** `cargo metadata`/workspace layout, `Cargo.toml` profiles
       and features, CI config, `rg 'unsafe' --count-matches`, dependency tree
       (`cargo tree -d`). This decides which rules files to load and where risk
       concentrates (network input? unsafe? async service?).
    2. **Run the audit checklists** at the end of each loaded rules file — they
       are ordered grep/clippy hunts with pre-calibrated severities.
    3. **Validate every finding**: read the surrounding code; a grep hit is a
       lead, not a finding. Confirm reachability (is the unwrap on an
       attacker-influenced path?) before assigning severity.
    4. **Report with the finding format below.** Prefer few, true, prioritized
       findings over volume. Note positive observations where the code is already
       SOTA (prevents "fixes" that regress good decisions).
    
    ### Severity conventions
    
    | Severity | Meaning | Examples |
    |---|---|---|
    | **Critical** | Exploitable now, or UB | reachable UB, unsound safe API, SQLi/path traversal, authn bypass, unwinding across FFI, secrets in logs+repo |
    | **High** | Exploitable under realistic conditions, or correctness loss | attacker-reachable panic/OOM (DoS), wrapped arithmetic on untrusted lengths, cancellation data loss, deadlock (`block_on` in async, lock across await), unbounded channels fed by network, missing dep-audit in deployed-service CI |
    | **Medium** | Latent defect or eroded defense | missing SAFETY comments, no Miri CI on unsafe crate, swallowed errors (`.ok()`, `filter_map(Result::ok)`) uncommented, untested MSRV, non-additive features, orphaned spawned tasks |
    | **Low** | Hygiene, idiom, maintainability | clone-to-satisfy-borrowck, index loops, missing `#[non_exhaustive]`, missing `# Errors` docs, blanket `#[allow]` without reason |
    
    Severity scales with **reachability** (attacker-controlled > user > operator >
    build-time) and **blast radius** (process death > request failure > slow).
    
    ### Finding format
    
    ```
    [SEVERITY] short title
      Where: path/to/file.rs:123 (fn name / module)
      What:  the defect, in one or two sentences
      Why:   concrete consequence (exploit path, failure mode, cost)
      Fix:   specific change — code sketch or named pattern from rules/NN
      Effort: trivial | small | medium | large
      Refs:  rules/NN §M; clippy lint or RUSTSEC id if applicable
    ```
    
    Group findings by severity, Critical first. End with: checklist coverage (which
    rules files were applied), what was *not* reviewed, and quick wins (one-line
    fixes with outsized value).
    
    ## Rules index
    
    | File | Read this when... |
    |---|---|
    | [rules/01-ownership-and-api-design.md](rules/01-ownership-and-api-design.md) | Designing structs/traits/modules/workspaces; fighting the borrow checker; deciding clone vs borrow vs Rc/Arc; newtype, typestate, builder patterns; sealed traits, coherence; comparison-trait (`Eq`/`Ord`) invariants; exhaustive matching; iterator-chain idioms |
    | [rules/02-errors-and-panics.md](rules/02-errors-and-panics.md) | Choosing thiserror vs anyhow/eyre; designing error enums; unwrap/expect policy and invariant messages; context discipline; panic policy for servers, FFI, and `Drop` (no panic in destructors); Option/Result combinator flow; **not unwrapping an `Option` back into a sentinel** (`unwrap_or(-1)`, `serde(default)` on numbers) |
    | [rules/03-unsafe-discipline.md](rules/03-unsafe-discipline.md) | Writing or reviewing ANY `unsafe`; SAFETY comment standards; UB catalog (aliasing, uninit, transmute, FFI lifetimes); Miri/sanitizers/loom in CI; cargo-geiger; soundness review protocol |
    | [rules/04-async-tokio.md](rules/04-async-tokio.md) | Anything async: tokio, spawn vs spawn_blocking, Send/Sync bound errors, `select!` and cancellation safety, JoinSet/TaskTracker, channel selection, locks across await, async traits, graceful shutdown |
    | [rules/05-security-supply-chain.md](rules/05-security-supply-chain.md) | Network-facing or deployed code; adding dependencies; cargo audit/deny/vet; integer overflow on untrusted input; panic-DoS; zeroize/constant-time for secrets; serde hardening (untagged enums, size limits); service-edge defaults; **spawning external programs** (`std::process::Command` — argv vs shell, the Windows `.bat` CVE, a dropped `Child` that keeps running, deadlines that don't kill, §9) |
    | [rules/06-performance.md](rules/06-performance.md) | Performance work or claims: profiling (samply/perf/flamegraph, criterion/divan), allocation reduction (Cow/SmallVec/buffer reuse), accidental clones, iterator fusion, release profile (LTO, codegen-units, panic=abort), PGO |
    | [rules/07-tooling-ci.md](rules/07-tooling-ci.md) | Setting up or auditing repo scaffolding: clippy policy and pedantic triage, rustfmt, nextest, MSRV declaration+testing, additive feature flags, docs.rs discipline, edition 2024 migration, CI baseline. **Test *strategy* — suite shape, TDD, doubles, test data, flake policy — lives in `sota-testing`; load it for any build that writes logic. This file owns Rust runner mechanics only.** |
    
    ## Top-10 non-negotiables
    
    1. **No `unwrap()`/bare `expect()` on production paths.** Propagate with `?` +
       context; `expect("...")` only with a message proving the invariant. An
       attacker-reachable panic is a DoS. (rules/02)
    2. **Every `unsafe` block has a `// SAFETY:` comment** discharging the called
       API's documented preconditions, and lives behind a sound safe abstraction.
       Unsafe code without Miri in CI is unaudited code. (rules/03)
    3. **Libraries: thiserror enums with `#[source]` chains. Applications:
       anyhow with `.context()`.** Never `anyhow::Error` in a public lib API;
       never silently swallowed errors. (rules/02)
    4. **Never block the async runtime:** no sync I/O, `std::thread::sleep`, or
       sustained CPU inside `async fn`; `spawn_blocking` or a compute pool. No
       `std::sync::MutexGuard` held across `.await`. (rules/04)
    5. **Every `select!`/timeout/abort path is cancellation-reviewed:** futures
       dropped at any `.await`; cancel-unsafe ops don't go in `select!` arms;
       invariants spanning awaits get drop guards. Spawned tasks are owned
       (JoinSet/TaskTracker), never orphaned. (rules/04)
    6. **Untrusted input gets checked arithmetic, size limits, and depth limits:**
       `checked_*`/`try_into` on lengths (release mode wraps silently), body-size
       caps before parsing, no `#[serde(untagged)]` or uncapped
       `with_capacity` on hostile data. (rules/05)
    7. **Supply chain is CI-enforced:** `cargo deny`/`cargo audit` on PRs +
       scheduled, `Cargo.lock` committed, new deps vetted (cargo vet or
       documented review), git deps pinned by rev. (rules/05)
    8. **Secrets are typed (`SecretString`/`Zeroizing`), redacted from
       Debug/logs, and compared in constant time** (`ct_eq`). Key material only
       from OS randomness. (rules/05)
    9. **Don't clone to satisfy the borrow checker; don't take owned params you
       only read.** `&str`/`&[T]` in signatures, split borrows, `mem::take`;
       newtypes over primitive obsession; exhaustive matches (no lazy `_ =>` on
       owned enums). (rules/01)
    10. **CI gate: fmt + clippy `-D warnings` (triaged pedantic) + nextest +
        doctests + MSRV job + feature-matrix check.** Performance claims require
        benchmarks; release profile (LTO/codegen-units/panic strategy) is a
        deliberate, documented choice. (rules/06, 07)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related