Where Rust's safety guarantees stop, and how to secure Rust code, dependencies, builds and services. CI compiles every Rust example below.
Last verified 2026-10-05 · Rust 1.99 · edition 2024
| Job | Go to | First action |
|---|---|---|
| Know what Rust prevents | Guarantees | Plan for panics, overflow, logic bugs (LIM-2–LIM-4) |
| Vet a dependency | Dependencies | Check its crates.io Security tab (DEP-1) |
| Secure builds, releases, incident response | Supply chain | Commit Cargo.lock (SUP-4); advisory: SUP-8 |
Write or audit unsafe, FFI (foreign-code calls) |
Unsafe | Check each // SAFETY: against the callee's # Safety docs (UNS-4) |
| Share data across threads or tasks | Concurrency | thread::scope or Arc<Mutex<T>> (CON-1) |
| Audit unfamiliar code | Auditing | Open it only where no credentials live (SUP-5, AUD-9) |
| Choose crypto, TLS, secret storage | Crypto | rustls ≥0.23.45 (CRY-1) |
| Harden a network service | Services | Time out slow clients (SVC-1) |
| Pick a bug-finding tool | Tools | Run tests reaching unsafe under Miri (undefined-behavior detector) (TOOL-1) |
| Harden or sandbox a binary | Hardening | Release overflow checks (HARD-1) |
| Embedded, kernel, Wasm, safety-critical | Domains | Guard firmware stacks (DOM-1) |
| Zero-knowledge (ZK) proofs, threshold signing, FHE (encrypted computation) | Privacy tech | Test that circuits reject tampered values (PRIV-1) |
| Learn from past incidents | Casebook | Update Rust promptly (CASE-1) |
| Fix code from this guide's 2024 version | Corrections | Apply each row's fix |
| Report or follow vulnerabilities | Stay current | Toolchain: security@rust-lang.org; crates: SUP-9 |
- Commit
Cargo.lockand build with--locked(SUP-4). - Fail CI on RustSec advisories (DEP-1) and update Rust promptly (CASE-1).
- Build or open untrusted code only where no credentials live (SUP-5).
- Never
unwraporexpectinput or config (LIM-2). - Give each
unsafeblock a// SAFETY:proof (UNS-4) and run its tests under Miri (TOOL-1). - Use rustls ≥0.23.45 and never disable certificate checks (CRY-1).
- Load secrets from a secret store (a file your orchestrator mounts, or a secrets manager) into
SecretString(CRY-9). - Bound every request's time and size (SVC-1, SVC-4).
- Fuzz every parser of untrusted input (TOOL-3).
- Use the lints and release profile of this repository's
snippetscrate (UNS-1, TOOL-4, HARD-1):
# Workspace: put all of this in the root Cargo.toml, the lints as [workspace.lints.rust] and
# [workspace.lints.clippy]; add [lints] workspace = true to every package (never a virtual root).
[lints.rust]
unsafe_code = "forbid" # UNS-1. Crates that need unsafe: "deny", then #[allow] per module
[lints.clippy] # Opt-in restriction lints (TOOL-4); justify each #[expect(lint, reason = "...")]
# Only Clippy checks these, so run `cargo clippy --all-targets --locked` in CI. For tests, set
# allow-unwrap-in-tests, allow-expect-in-tests, allow-indexing-slicing-in-tests in clippy.toml
# Existing code: start indexing_slicing, arithmetic_side_effects at "warn"; #![deny] per module
unwrap_used = "deny" # LIM-2
expect_used = "deny" # LIM-2
indexing_slicing = "deny" # LIM-2
arithmetic_side_effects = "deny" # LIM-3
as_conversions = "deny" # AUD-3
undocumented_unsafe_blocks = "deny" # UNS-4
[profile.release]
# HARD-1: overflow panics instead of wrapping, in dependencies too
# (`as` casts still truncate silently)
overflow-checks = true
# HARD-2: the default; with "abort", any panic ends the whole process
# and catch_unwind cannot stop it. A panic while holding a std Mutex
# poisons it: handle PoisonError instead of unwrapping lock().
panic = "unwind"
# HARD-3: strip removes symbol names but is no meaningful security measure (rustc docs), and
# "symbols" can garble backtraces: drop it if you need crash reports. Panic messages keep
# source paths, so remap them in CI release builds (the last matching prefix wins; add one
# for a CARGO_HOME outside HOME). RUSTFLAGS replaces every rustflags setting in
# .cargo/config.toml; if you have any, add the flag there with an absolute path instead:
# RUSTFLAGS="--remap-path-prefix=$HOME=~" cargo build --release --locked
strip = "symbols"| Old advice | Risk | Fix |
|---|---|---|
"Pin dependencies" with = |
Blocks semver-compatible fixes | Commit Cargo.lock, build --locked (SUP-4) |
Token from OsRng and % len, then printed |
Breaks on rand ≥0.9; biased; leaked | getrandom::fill (CRY-6) |
| Secrets in env vars; default database fallback; URL printed | Fails open; leaks credentials | Secret store; fail closed (CRY-9) |
rustls with_safe_defaults and webpki |
Pre-0.22 API; verifier idle since 2024-02 | rustls ≥0.23.45, platform verifier (CRY-1) |
| "Implement secure communication protocols" like TLS, SSH | Home-made protocol bugs | rustls, russh or snow (Crypto) |
| JWT with a hard-coded key, no backend, no iss/aud check, token printed | Forgery; panics; leaked tokens | CRY-2 |
"type safety extend to FFI boundaries"; bindgen run from main; to_str().unwrap() on C strings |
Unchecked signatures; panics; wrong-allocator frees | UNS-10, UNS-12 |
Raw pointers via &num, then &mut num |
&mut invalidates the first (Miri, Stacked Borrows) |
&raw const, &raw mut (UNS-5) |
| "Arc is both Send and Sync"; threads never joined | Unsound sharing; lost work | CON-1 |
| Redaction credited to "move semantics" | Secret never wiped | SecretString (CRY-9) |
thiserror {0} in client errors |
Leaks internals | SVC-14 |
| Rust's types "prevent common web vulnerabilities" like XSS | Unescaped output | Autoescaping templates, DOM-3 |
| rust-analyzer for "vulnerability detection" | Runs untrusted build code; finds no vulnerabilities | AUD-9 |
cargo audit for crypto misuse; RustCrypto "audits and formal verification" |
False assurance | Check audit scope (Crypto) |
| Groth16 with a single-party setup; Paillier as MPC; GG18/GG20 code | Forged proofs; key extraction | PRIV-2, PRIV-5 |
| PQClean for post-quantum | Archived | CRY-8 |
Fuzzing std parsing |
Tests std, not your code | TOOL-3 |
| Prusti as the leading verifier | Last release 2024-03 | TOOL-6, TOOL-7 |
| A libssh2 authentication bypass Rust "would have prevented" | Overstates Rust | That was libssh CVE-2018-10933, a logic bug (LIM-4) |
| Firecracker's and Tock's isolation credited to Rust | Ignores other layers | KVM, seccomp, jailer (Hardening); MPU (Domains) |
Safe Rust (no unsafe) prevents memory corruption and data races: Android estimates, from one near-miss, 0.2 vs ~1,000 memory-safety vulnerabilities per million lines, Rust vs C/C++. Go, Java and Python are memory-safe too, but racy Go can corrupt memory. Both guarantees need sound rustc, std and unsafe (no undefined behavior), so safe Rust is no sandbox.
| Property | Holds only if | Real failure | Control |
|---|---|---|---|
| Memory and data-race safety | rustc, std and all unsafe are sound |
cve-rs; Linux Binder | LIM-1 Run untrusted Rust (plugins, generated code) as Wasm (HARD-5) or in a separate OS-sandboxed process (HARD-4) |
| No crash, leak, deadlock | never: unwrap, expect, [i] panic; safe Rust allows leaks, deadlocks |
Cloudflare outage | LIM-2 Never unwrap or expect input or config; CON-3 |
| No integer overflow | never: release wraps | Tock kernel crash | LIM-3 Use checked_* arithmetic on input sizes (HARD-1) |
| Correct logic | never | sudo-rs authentication bypass | LIM-4 Test that authentication rejects bad cases |
| No TOCTOU (check-then-use race) | never | std, uutils, sudo-rs | CON-6 |
| Constant time | never: LLVM may add branches | curve25519-dalek | CRY-5, CRY-7 |
| Safe builds | every build.rs, proc-macro is benign | arrayref | SUP-5 |
Legend. Boundary diagrams: subgraph = trust zone; solid arrow = compiler-checked (types only); dashed = trusted, unchecked. Decision diagram: diamond = question; rectangle = action; arrow label = answer; ✗ AVOID = stop.
Unlabeled arrows mean "relies on": rustc can't verify unsafe promises; any crate's build code runs on your machines.
flowchart TD
accTitle: Where Rust's guarantees stop
accDescr: Safe code in any crate is type-checked by rustc and calls your unsafe blocks through a safe API. Below that, each layer is trusted without checks - dependencies' unsafe code, std's unsafe code, rustc and LLVM, then the FFI and C libraries your binary links, then the operating system. Any crate, even one with no unsafe code, can ship build scripts and proc-macros that run with your privileges on developer and CI machines.
subgraph z1["Checked by rustc"]
n1["Safe code, any crate"]
end
subgraph z2["Trusted: soundness unchecked"]
n2["Your unsafe blocks"]
n3["Dependencies' unsafe code"]
n4["std's unsafe code"]
n5["rustc and LLVM"]
end
subgraph z3["Outside Rust"]
n6["FFI and C libraries"]
n7["Operating system"]
end
subgraph z4["Build time"]
n8["build.rs and proc-macros"]
n9["Developer and CI machines"]
end
n1 -->|"calls via safe API"| n2
n2 -.-> n3
n3 -.-> n4
n4 -.-> n5
n5 -.->|"your binary links"| n6
n6 -.-> n7
n1 -.->|"any crate may ship"| n8
n8 -.->|"run with your privileges"| n9
Go deeper
- Behavior not considered unsafe: use for the full list.
A dependency's build script (build.rs) and procedural macros (compile-time functions) run arbitrary code when you build (Cargo, Reference), and the toolchain trusts them fully. Vet new crates before their first build:
flowchart TD
accTitle: Crate vetting decision flow
accDescr: Each question is answered by the check with the same ID below. An unpatched advisory, archived repository or deprecation (DEP-1) means avoid, or vendor a patched copy. A crate with build-time code (DEP-2), no activity in the past year (DEP-3), or many or unknown publishers (DEP-4) must be covered by imported cargo-vet audits (DEP-5) or by your own audit. Otherwise depend on it. A failed audit means avoid, or vendor a patched copy.
Q1{"DEP-1: unpatched advisory, archived, deprecated?"} -->|yes| X["✗ AVOID, or vendor patched"]
Q1 -->|no| Q2{"DEP-2: build.rs or proc-macro?"}
Q2 -->|no| Q3{"DEP-3: active in past year?"}
Q2 -->|yes| Q5{"DEP-5: cargo-vet audits cover it?"}
Q3 -->|no| Q5
Q3 -->|yes| Q4{"DEP-4: how many can publish?"}
Q4 -->|"many or unknown"| Q5
Q4 -->|"few, known to you"| D["Depend"]
Q5 -->|yes| D
Q5 -->|no| A["Audit, then depend"]
A -->|fails| X
- DEP-1 Before adding a crate, confirm its exact name (typosquats), then check its crates.io Security tab for unpatched advisories. After,
cargo deny check advisories(0.20.2, 2026-07-09) fails on vulnerable or unmaintained crates; addunsound = "all"for transitive unsoundness. RustSec misses some repository-only advisories (cap-std). Archived orx.y.z+deprecatedalso means avoid: RustSec has no unmaintained advisory for archived serde_yaml. - DEP-2 Audit uncovered build-time code in the crates.io Code tab, which shows what Cargo downloads, not the repository. Stop rust-analyzer first; it runs that code.
- DEP-3 Activity is the later of the last release and default-branch commit.
- DEP-4
cargo supply-chain publishers(0.3.7, 2026-02-05) lists who can publish your dependencies; any can ship malware. - DEP-5 Run cargo-vet (0.10.2, 2026-01-13) in CI so new crates and versions fail until audited. Imports are not transitive; Google calls its audits best-effort. Vendor a patched copy via
[patch].
cargo install --locked cargo-deny@0.20.2 cargo-supply-chain@0.3.7 cargo-vet@0.10.2
cargo vet init # once per project
cargo vet import google
cargo vet import mozilla
gh api repos/OWNER/REPO/security-advisories --jq '.[] | .ghsa_id + " " + .summary' # DEP-1
gh api repos/OWNER/REPO --jq .archived # DEP-1: true means avoid
cargo add CRATE # stop rust-analyzer first (DEP-2)
cargo deny check advisories # DEP-1
cargo metadata --locked --format-version 1 | jq -r '.packages[]
| select(any(.targets[].kind[]; . == "custom-build" or . == "proc-macro"))
| "\(.name) \(.version)"' # DEP-2
gh api 'repos/OWNER/REPO/commits?per_page=1' --jq '.[0].commit.committer.date' # DEP-3
cargo supply-chain publishers # DEP-4
cargo vet trust CRATE PUBLISHER # DEP-4: only for few, known publishers
cargo vet # DEP-5: lists what still needs an auditGo deeper
- cargo-vet book: use for criteria and
cargo vet trust. - Trail of Bits: use for cargo-crev and cackle.
Attacks enter at every stage. Commit Cargo.lock, isolate secrets, publish without stored tokens, update for advisories, report privately.
flowchart TD
accTitle: Supply-chain path
accDescr: Seven stages in three trust zones, each labeled with the control in the table below. Publishing, for your crates and your dependencies, holds the maintainer account (SUP-1), the publish step (SUP-2) and the registry (SUP-3). Your build holds Cargo.lock (SUP-4), build scripts and proc macros (SUP-5), and CI secrets and caches (SUP-6). Users receive the shipped binary (SUP-7). Every edge is dashed because nothing on this path is compiler-checked.
subgraph UP["Publishing"]
A["SUP-1: Maintainer account"] -.-> B["SUP-2: Publish"]
B -.-> C["SUP-3: Registry"]
end
subgraph YB["Your build"]
D["SUP-4: Cargo.lock"] -.-> E["SUP-5: build.rs, proc macros"]
E -.-> F["SUP-6: CI secrets, caches"]
end
subgraph US["Users"]
G["SUP-7: Shipped binary"]
end
UP -.-> YB
YB -.-> US
| Stage | Control | Tool |
|---|---|---|
| Maintainer account (phishing) | SUP-1 Sign in only via typed or bookmarked URLs (2025-09-12); schedule calls yourself; never run what callers send (2026-09-17). | Passkeys |
| Publish | SUP-2 Use Trusted Publishing (30-minute CI tokens), then disable token publishing (2026-01-21). | crates-io-auth-action v1.0.5 |
| Registry (tar) | SUP-3 Run Rust ≥1.96.1 (Cargo's SSH-library CVEs). Check advisories daily (malware too). | DEP-1, OSS Rebuild |
| Cargo.lock (arrayref) | SUP-4 Commit Cargo.lock; build, install --locked; keep caret requirements, not = pins (Cargo). |
Dependabot (3-day cooldown) or Renovate minimumReleaseAge |
| build.rs, proc macros (proc-macro1) | SUP-5 Building or opening a repo runs its code; hold no credentials there. | Container or VM |
| CI secrets, caches (Miri) | SUP-6 No secrets in jobs that write caches PRs can read (2026-09-21). | GitHub environments, zizmor 1.30.1 |
| Shipped binary | SUP-7 Use cargo auditable build; attach an SBOM (dependency list), provenance (signed build record). |
cargo-auditable 0.7.7, cargo-cyclonedx 0.5.9, actions/attest v4.2.2 |
Respond:
- SUP-8 Advisory in your tree:
cargo update <crate>; if a parent blocks it (cargo tree -i <crate>), upgrade the parent or[patch]a fixed fork. Malware: wipe build machines; rotate secrets from clean ones (2022-05-10). - SUP-9 Toolchain or crates.io bug: security@rust-lang.org (policy). Crate bug: owners privately, then GitHub and RustSec advisories. Malware: help@crates.io (policy).
- SUP-10 Your crate: revoke leaked credentials first; ship semver-compatible fixes, then advisories; yanks miss lockfiles (Cargo).
- SUP-11 Manufacturer selling in the EU: since 2026-09-11, report actively exploited vulnerabilities, severe incidents via CRA's Single Reporting Platform: 24 h early warning, 72 h notification.
SUP-2 on GitHub:
# .github/workflows/release.yml. Trusted Publishing needs the crate on crates.io already: publish
# the first version with an API token scoped to publish-new and this crate, with an expiry; then
# register this file name and the "release" environment on crates.io and disable token publishing.
on: { push: { tags: ["v*"] } }
permissions: {}
jobs:
verify: # builds the crate, so dependency build scripts run here, without credentials
runs-on: ubuntu-latest
permissions: { contents: read }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- run: cargo package --locked
publish: # any step here can mint an OIDC token, so build nothing (--no-verify) and restore no caches
needs: verify
runs-on: ubuntu-latest
environment: release # in repo settings: require a reviewer, allow only v* tags
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
id: auth
- run: cargo publish --locked --no-verify
env: { CARGO_REGISTRY_TOKEN: "${{ steps.auth.outputs.token }}" }Go deeper
- crates.io Trusted Publishing: use for setup, GitLab.
- GitHub Actions secure use: use for hardening workflows.
unsafe and FFI rest on promises the compiler can't check; breaking one is undefined behavior (UB) program-wide.
| Check | Enforce with |
|---|---|
UNS-1 Forbid unsafe where unneeded; dependencies and their macros' unsafe aren't covered. |
[lints.rust] unsafe_code = "forbid" (Cargo ≥1.74) |
UNS-2 Edition 2024 makes env::set_var/remove_var unsafe: call them in non-async main before spawning threads. |
edition = "2024" (Rust ≥1.85) |
UNS-3 Prefer safe APIs: derived casts over transmute. |
zerocopy 0.8.59, bytemuck 1.25.2 |
UNS-4 Give each block one unsafe operation and a // SAFETY: proof of every # Safety precondition, valid while other threads run. |
Clippy undocumented_unsafe_blocks, multiple_unsafe_ops_per_block |
UNS-5 Keep unsafe small, inside a safe wrapper sound for every input; use &raw const/&raw mut. |
Clippy borrow_as_ptr; deny unsafe_op_in_unsafe_fn |
UNS-6 Check length math, distrust safe traits, stay panic-safe; Drop may never run. |
Clippy arithmetic_side_effects; UNS-8 |
UNS-7 Bound unsafe impl Send/Sync like std: Box<T> on T: Send/Sync; Arc<T> on T: Send + Sync; Mutex<T> on T: Send. |
AUD-1 |
UNS-8 Run every test reaching unsafe under Miri (AddressSanitizer for FFI); fuzz safe wrappers. |
TOOL-1, TOOL-3 |
| UNS-9 Model-check loop-bounded unsafe functions; Kani misses aliasing. | TOOL-6 |
UNS-10 Generate unsafe extern signatures; Rust trusts them unchecked. Take C enums, bools as integers; validate. |
bindgen 0.73.2 in build.rs, allowlisted |
| UNS-11 Catch panics before they reach C (abort since 1.81). | catch_unwind; extern "C-unwind" (1.71) for intended unwinding |
| UNS-12 Free memory with its allocator. | A Drop wrapper |
| Allocated by | Freed by | Rust type or API | Failure if wrong |
|---|---|---|---|
Rust (CString::into_raw) |
Rust | CString::from_raw |
C free(): wrong allocator |
Rust (Box::into_raw) |
Rust, once | Box::from_raw |
Double free |
| C, lent to you | C | CStr::from_ptr, then copy |
Use after free, double free |
| C, given to you | Its library's free | Drop wrapper |
CString::from_raw/Box::from_raw: UB |
use core::ffi::{CStr, c_char, c_int, c_void};
// Rust trusts these signatures unchecked: generate them from the header (UNS-10).
unsafe extern "C" {
fn abs(x: c_int) -> c_int; // keep unsafe, never `safe fn`: abs(INT_MIN) is UB
fn strdup(s: *const c_char) -> *mut c_char; // caller frees with free()
fn free(p: *mut c_void);
}
fn main() {
// SAFETY: -42 != c_int::MIN, so abs is defined.
let y = unsafe { abs(-42) };
// SAFETY: a c"..." literal is NUL-terminated and 'static.
let p = unsafe { strdup(c"from C".as_ptr()) };
if p.is_null() {
return; // allocation failed
}
// SAFETY: p is non-null and NUL-terminated; we copy it out before free().
let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned(); // no unwrap
// C allocated p, so C frees it. In a library, free in a Drop impl (UNS-12).
// SAFETY: p came from strdup and is freed exactly once, here.
unsafe { free(p.cast()) };
println!("{y} {s}");
}Go deeper
- Rustonomicon: use for aliasing rules.
Safe Rust rejects data races (unsynchronized concurrent access, one writing) at compile time, not race conditions or deadlocks (Nomicon); unsafe races can corrupt memory. Async adds cancellation, unbounded queues, blocked executors.
| Need | Use | Avoid (failure) |
|---|---|---|
| CON-1 Cross-thread sharing | thread::scope, or Arc of a Mutex, RwLock or atomic (Arc<T>: Send needs T: Send + Sync) |
Unconstrained unsafe impl Send/Sync (UNS-7) |
| CON-2 Panic while locked | Fail closed, or re-check invariants, then PoisonError::into_inner |
.lock().unwrap() in servers (panics cascade); tokio::sync::Mutex (silently unlocks) |
| CON-3 Deadlocks | One lock order; drop std guards before .await (await_holding_lock) |
Relocking a held Mutex (hang/panic) |
CON-4 Cancellation: select!, timeout, client disconnects drop futures |
reserve(), then Permit::send (tokio ≥1.53.2; 1.51.x: ≥1.51.5) |
send, read_exact, write_all (lost data) |
| CON-5 Bound queues, blocking | mpsc::channel(n) (threads: sync_channel(n)); Semaphore-capped spawn_blocking |
unbounded_channel, std channel (unbounded memory); blocking in tasks (stall) |
| CON-6 File check-then-use (TOCTOU) | One opened handle (cap-std ≥4.0.3) | Re-resolving the path (symlink swap) |
CON-7 Lock-dependent unsafe |
// SAFETY: holds in all interleavings; loom, shuttle |
Single-thread reasoning (memory corruption) |
use tokio::{sync::mpsc::{self, Sender}, time::{Duration, timeout}};
#[derive(Debug, PartialEq)]
enum Fwd { Full(String), Closed(String) } // either way, the caller still owns the message
// CON-4: reserve a slot before handing over `msg`, so a timeout can't lose it.
async fn forward(tx: &Sender<String>, msg: String) -> Result<(), Fwd> {
match timeout(Duration::from_secs(1), tx.reserve()).await {
Ok(Ok(permit)) => { permit.send(msg); Ok(()) }
Ok(Err(_closed)) => Err(Fwd::Closed(msg)), // receiver gone for good: never retry
Err(_elapsed) => Err(Fwd::Full(msg)), // queue full: retry later or reject
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let (tx, mut rx) = mpsc::channel(1); // bounded: back-pressure, not memory growth
assert_eq!(forward(&tx, "a".into()).await, Ok(()));
assert_eq!(forward(&tx, "b".into()).await, Err(Fwd::Full("b".into()))); // "b" comes back
assert_eq!(rx.recv().await.as_deref(), Some("a"));
drop(rx); // consumer gone: fail at once instead of waiting out the timeout
assert_eq!(forward(&tx, "c".into()).await, Err(Fwd::Closed("c".into())));
}Go deeper
- Oxide RFD 400, tokio
select!: use for cancel safety. - loom 0.7.2 (exhaustive), shuttle 0.9.5 (randomized): use for CON-7.
Confirm grep hits in a SUP-5 sandbox: Clippy, Miri, CodeQL and rust-analyzer (which finds no vulnerabilities) run the repo's build scripts and proc macros. Rust ≥1.97.0 emits v0-mangled symbols (_R-prefixed names); check reversing tools support them.
| Grep for | Bug class | Fix or confirm |
|---|---|---|
AUD-1 unsafe impl Send/Sync |
Bounds looser than std's: data races | Match std's bounds (UNS-7); Clippy non_send_fields_in_send_ty (opt-in) |
AUD-2 set_len |
Uninitialized memory | Miri; Clippy uninit_vec |
AUD-3 as u8, as usize |
Silent truncation, NaN → 0 | try_from (floats: (lo..=hi).contains(&x)); Clippy as_conversions (opt-in) |
AUD-4 AssertSqlSafe, QueryBuilder, "SELECT…{x}" |
SQL injection: unchecked string, unsanitized push |
.bind(), .push_bind(); CodeQL rust/sql-injection |
AUD-5 danger (reqwest, rustls verifiers) |
TLS verification off (reqwest, rustls): interception | Delete |
AUD-6 permissive, AllowOrigin::mirror_request |
CORS: any origin, with credentials | List allowed origins |
AUD-7 derive(Debug) on secrets |
Secrets in logs | secrecy::SecretBox; CodeQL rust/cleartext-logging |
AUD-8 == on MAC tags |
Timing leak | CRY-5 |
AUD-9 build.rs, proc-macro |
Build-time code execution, dependencies' too | Read first; VS Code Restricted Mode (no rust-analyzer) or a VM |
AUD-10 .cargo/config*, rust-toolchain* |
Any program as rustc on most cargo commands, including rust-analyzer's; runner on run/test/bench |
Read first; run cargo audit --file <checkout>/Cargo.lock outside the checkout, so neither file loads |
# grep and find only read files; nothing here runs the repo's code
REPO=path/to/untrusted/checkout
grep -rniE --include='*.rs' \
-e 'unsafe impl.*(Send|Sync)' -e 'set_len' -e ' as [iu](8|16|32|64|128|size)' \
-e 'AssertSqlSafe|QueryBuilder' -e '" *(select|insert|update|delete|where|order by) [^"]*[{]' \
-e 'danger|permissive' -e 'AllowOrigin::mirror_request|allow_any_origin' \
-e 'derive\(.*debug|^ *debug,' -e '(mac|tag)[^;]*==|==[^;]*(mac|tag)' "$REPO"
grep -rnE --include=Cargo.toml '^ *(build|proc[-_]macro) *=' "$REPO"
find "$REPO" -name build.rs -o -name 'rust-toolchain*' -o -path '*/.cargo/config*' -o -path '*/.vscode/*'Go deeper
- CodeQL Rust queries: use for taint tracking.
- ANSSI guidelines: use for rule-by-rule review.
- Check Point's Akira teardown: use for reversing Rust malware.
Never hand-roll TLS, SSH or Noise: use rustls, russh ≥0.64.1 or snow ≥0.9.5.
| Job | Use (minimum version) | Avoid (why) | Status 2026-10-05 |
|---|---|---|---|
| CRY-1 TLS | rustls ≥0.23.45, default aws-lc-rs (X25519MLKEM768 first); rustls-platform-verifier (no revocation on Linux) | webpki (idle since 2024-02; use rustls-webpki ≥0.103.13); *danger_accept_invalid*; ring with aws_lc_rs, or neither, without install_default() (builder() panics) |
0.23.45, 2026-09-14 |
| CRY-2 JWT | jsonwebtoken ≥10.3.0, features = ["aws_lc_rs"], ≥256-bit key, require iss, aud; asymmetric signing if others verify (RFC 9068) |
No backend feature (panics); rust_crypto (pulls rsa) |
11.1.0, 2026-09-16 |
| CRY-3 Passwords | argon2 ≥0.6.0 Argon2::default() (Argon2id at OWASP's minimum) |
Fast hashes like SHA-256 | 0.6.0, 2026-08-27 |
| CRY-4 Encryption | aes-gcm ≥0.10.3 or chacha20poly1305; random 96-bit nonces ≤2^32 messages per key, else counter or XChaCha20Poly1305 | Reused nonce (catastrophic); sodiumoxide (deprecated) | NCC-audited v0.3.0 |
| CRY-5 MAC check | hmac verify_slice or subtle ct_eq |
== on tags (timing leak) |
hmac 0.13.0, 2026-03-29 |
| CRY-6 Tokens, keys | getrandom::fill (Wasm: DOM-4) |
rng % len (biased); OsRng, RngCore (renamed in rand 0.10) |
0.4.3, 2026-06-17 |
| CRY-7 Signatures | ed25519-dalek ≥2.0, curve25519-dalek ≥4.1.3; k256 ≥0.13.2; RSA: aws-lc-rs (constant-time) | rsa signing, decryption (Marvin timing attack, unpatched) |
k256 NCC-reviewed 2023-08 (pre-0.14); p256, ecdsa unaudited |
| CRY-8 Post-quantum | CRY-1 for TLS; aws-lc-rs (aws-lc-sys ≥0.39.0), libcrux-ml-kem ≥0.0.10 (RUSTSEC-2026-0212) or ml-kem | pqcrypto, PQClean (archived); pqc_kyber (unpatched) | ml-kem, ml-dsa unaudited; libcrux pre-0.1, partly verified |
| CRY-9 Secrets | secrecy 0.10.3 SecretString from a secret store (wiped on drop, redacted Debug); exit if missing or empty |
Debug redaction alone (no wipe); dotenv (dotenvy, dev-only); env vars (leak to children, /proc, core dumps) |
Best-effort wipe: moved or reallocated copies remain |
| CRY-10 Hashing, key derivation | sha2, blake3; hkdf | MD5, SHA-1 (collisions) | sha2 0.11.0, 2026-03-25 |
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize};
const ISS: &str = "https://auth.example.com";
const AUD: &str = "orders-api";
#[derive(Serialize, Deserialize)]
struct Claims { sub: String, iss: String, aud: String, exp: u64 }
fn validation() -> Validation { // CRY-2: require every claim you check
let mut v = Validation::new(Algorithm::HS256); // pinned; HS256 only if this service also signs
v.set_issuer(&[ISS]);
v.set_audience(&[AUD]);
v.set_required_spec_claims(&["exp", "iss", "aud"]); // else tokens without iss/aud pass
v
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut key = [0u8; 32]; // 256-bit demo key; in production, load it from a secret store
getrandom::fill(&mut key)?;
let exp = jsonwebtoken::get_current_timestamp().saturating_add(300);
let claims = Claims { sub: "user-1".into(), iss: ISS.into(), aud: AUD.into(), exp };
let token = encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(&key))?;
decode::<Claims>(&token, &DecodingKey::from_secret(&key), &validation())?; // never log `token`
Ok(())
}Go deeper
- rustls manual: use for providers, FIPS.
- OWASP Secrets Management: use for key storage, rotation.
- Testing Handbook: use for Wycheproof, constant-time tests.
Bound every input's time, size, nesting, file access.
| Need | Use (minimum version) | Avoid (why) | Status 2026-10-05 |
|---|---|---|---|
| SVC-1 Slow clients (slowloris) | HTTP/1 header_read_timeout, timer (below; axum); HTTP/2: cap max_concurrent_streams, proxy-enforced timeouts; TimeoutLayer::with_status_code; Semaphore-capped connections |
axum::serve, bare HTTP/2 (no header timeout) |
hyper 1.11.1 |
| SVC-2 HTTP/2 floods | h2 ≥0.4.16 | 0.3.x (unpatched); 0.4.0–0.4.3 (CONTINUATION flood) | 0.4.19 |
| SVC-3 Cross-site forgery (CSRF) | tower-http CsrfLayer; GET changes no state |
very_permissive() CORS (AUD-6) |
0.7.1 |
| SVC-4 Body, WebSocket size | RequestBodyLimitLayer; max_message_size <64 MB |
DefaultBodyLimit alone (skips streams), disable() |
axum 0.8.9 |
| SVC-5 Length fields, decompression | Cap before allocating; take(cap+1) on decoders, rejecting over cap bytes |
with_capacity(wire_len), unbounded decoders (OOM) |
std |
| SVC-6 Deserializing | serde(try_from) validation; depth limits (serde_json: 128) |
derive(Deserialize) alone (skips validation); disable_recursion_limit, recursive postcard types (stack overflow) |
serde_json 1.0.151 |
| SVC-7 Formats | JSON, TOML; postcard for binary | serde_yaml (archived); serde_yml (unsound); bincode (unmaintained, unlimited) | 1.1.3 |
| SVC-8 SQL | sqlx ≥0.8.1 query!, push_bind |
push, AssertSqlSafe on input (injection; AUD-4) |
0.9.0 |
| SVC-9 User-named paths | cap-std ≥4.0.3 (3.x: ≥3.4.6) Dir (CON-6) |
base.join(input) (.. or absolute input escapes) |
4.0.3 |
| SVC-10 Archives (tar/zip-slip) | Into private empty directories: tar ≥0.4.46 unpack_in; zip ≥2.3.0 extract |
Entry::unpack (escapes target); enclosed_name() alone (symlinks escape) |
tar 0.4.46, zip 8.6.0 |
| SVC-11 Child processes | Rust ≥1.81.0 (.bat files); allowlisted args; env_clear() |
Untrusted args to cmd.exe, sh -c, raw_arg; inherited env (leaks secrets) |
std |
| SVC-12 User URLs (SSRF) | timeout; Policy::none(); vet IP literals, dns_resolver results; no_proxy(); capped chunk() |
Defaults: no timeout, 10 redirects, system proxy | reqwest 0.13.5 |
| SVC-13 Logs (injection, leaks) | tracing-subscriber ≥0.3.20 (escapes ANSI, not newlines); input as fields; instrument(skip_all) |
% (unescaped), messages with input (forged lines); bare #[instrument] (logs arguments) |
0.3.23 |
| SVC-14 Error responses | Generic message; log source() |
thiserror {0}, transparent (leak internals) |
2.0.21 |
use std::{convert::Infallible, io, sync::Arc, time::Duration};
use hyper::{Request, Response, body::Incoming, server::conn::http1, service::service_fn};
use hyper_util::rt::{TokioIo, TokioTimer};
// axum: copy only the Router-to-service_fn wiring from serve-with-hyper; keep this loop,
// since that example unwraps accept and binds 0.0.0.0. WebSockets need .with_upgrades().
async fn handle(_req: Request<Incoming>) -> Result<Response<String>, Infallible> {
Ok(Response::new("ok".into()))
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> io::Result<()> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
let slots = Arc::new(tokio::sync::Semaphore::new(1024)); // SVC-1: cap below the fd limit
loop {
let permit = slots.clone().acquire_owned().await.map_err(io::Error::other)?;
let Ok((tcp, _)) = listener.accept().await else { // never unwrap (LIM-2): fds run out
tokio::time::sleep(Duration::from_millis(100)).await; // back off, don't spin
continue;
};
let conn = http1::Builder::new() // HTTP/1 only: HTTP/2 has no header timeout (SVC-1)
.timer(TokioTimer::new()) // required: a timeout without a timer panics
.header_read_timeout(Duration::from_secs(10)) // SVC-1: drop slowloris clients
.serve_connection(TokioIo::new(tcp), service_fn(handle));
tokio::spawn(async move { let _ = conn.await; drop(permit) }); // slot freed at close
}
}Go deeper
- OWASP SSRF: use for address checks.
Match tool to bug class: Miri, sanitizers and fuzzers see only code that runs; Kani, only inputs its harness (test entry point) allows.
| Bug class | Catches it | Misses it | Cost |
|---|---|---|---|
TOOL-1 Undefined behavior (UB) in unsafe |
Miri: cargo +nightly miri test; AddressSanitizer (ASan) (-Zsanitizer=address) for FFI |
Untested paths; ASan: aliasing, some bugs in uninstrumented C | Nightly |
TOOL-2 Data races in unsafe; atomic-ordering bugs |
Miri; ThreadSanitizer (TSan): RUSTFLAGS=-Zsanitizer=thread RUSTDOCFLAGS=-Zsanitizer=thread cargo +nightly test -Zbuild-std --target <triple>; loom (systematic) |
Miri: unexplored interleavings; TSan: atomic::fence; loom: some relaxed orderings |
TSan: nightly; loom: its types |
| TOOL-3 Panics, wrong results on untrusted input | cargo-fuzz plus an oracle (below) | Code the harness skips; unchecked output | Nightly; harness per parser |
TOOL-4 Panic and overflow sites; unsafe without // SAFETY: |
Clippy unwrap_used, indexing_slicing, arithmetic_side_effects, undocumented_unsafe_blocks |
Reachability; false SAFETY claims | Stable; opt-in |
| TOOL-5 Injection (SQL, path, command, log) | CodeQL, GA 2025-10-14 | Classes outside its queries | Private repos need Code Security |
TOOL-6 Panic, overflow, out-of-bounds access, failed assert! (all inputs within bounds) |
Kani 0.68.0 | Aliasing, invalid values, data races; inputs past harness bounds | Harness per property |
| TOOL-7 Spec violations | Verus, Creusot, Aeneas, hax, Flux, all active 2026-10 | Wrong specs | Specs and proofs |
| Vulnerable, malicious dependencies | cargo-deny (DEP-1), cargo-vet (DEP-5) | Unreported or non-RustSec advisories; audit gaps | Stable; audits |
Avoid Prusti (no default-branch commit since 2024-03-26; pins nightly-2023-09-15), Rudra and MIRAI (archived).
Fuzz your parser against an oracle (a correctness check):
// Setup: cargo install --locked cargo-fuzz@0.13.2 && cargo fuzz init
// In fuzz/fuzz_targets/fuzz_target_1.rs, make the target call the oracle:
// fuzz_target!(|data: &[u8]| your_crate::check(data));
// Run: cargo +nightly fuzz run fuzz_target_1
use arbitrary::{Arbitrary, Unstructured};
#[derive(Arbitrary, Debug, PartialEq)]
pub struct Header { kind: u8, len: u16 }
pub fn encode(h: &Header) -> Vec<u8> {
[&[h.kind][..], &h.len.to_be_bytes()].concat()
}
pub fn decode(bytes: &[u8]) -> Option<Header> {
let &[kind, hi, lo] = bytes.first_chunk::<3>()?;
Some(Header { kind, len: u16::from_be_bytes([hi, lo]) })
}
pub fn check(data: &[u8]) {
let mut u = Unstructured::new(data);
let Ok(header) = Header::arbitrary(&mut u) else { return };
let _ = decode(u.take_rest()); // arbitrary bytes: may fail, must not panic
assert_eq!(decode(&encode(&header)), Some(header)); // round-trip oracle
}
fn main() { check(&[1, 2, 3, 4]) } // smoke test; the fuzzer explores the restGo deeper
- Fuzz Book: use for
Arbitraryinputs. - Testing Handbook: use for fuzzing campaigns.
Linux x86-64 builds already get address randomization, non-executable data, read-only relocations and stack probes (rustc); stack protectors and control-flow integrity need nightly, except Windows' -C control-flow-guard. Set the release profile and confine the process: Firecracker pairs Rust with KVM, seccomp syscall filters and a jailer.
| Need | Use (minimum safe version) | Avoid (why) | Failure prevented | Status 2026-10-05 |
|---|---|---|---|---|
| HARD-1 Overflow checks | overflow-checks = true; cargo test --release |
ring <0.17.12: panics | Silent wraparound | Default off |
| HARD-2 Panic policy | Servers: unwind, caught per request; stack overflow, OOM still abort (SVC-6) |
abort there: kills every request |
Whole-service outage | Default unwind |
| HARD-3 Hide build paths | --remap-path-prefix (RUSTFLAGS overrides config) |
Relying on strip: panic messages keep paths |
Leaked build paths | --remap-path-scope stable 1.95; trim-paths unstable |
| HARD-4 Least privilege | landlock 0.4.7 (Linux ≥5.13), default-deny seccompiler 0.5.0, before any thread or async runtime starts | Ignoring RulesetStatus: may enforce nothing |
Post-exploit access | seccompiler moved to rust-vmm |
| HARD-5 Untrusted Wasm | wasmtime crates ≥49.0.2; fuel or epochs (CPU), StoreLimits (memory) |
Older: fuel bypassable; CVE-2026-34971 (aarch64), CVE-2026-34987 (Winch) escapes | Sandbox escape, resource exhaustion | Older lines: 48.0.4, 36.0.17 |
Go deeper
- Wasmtime security: use for defense layers.
- Landlock crate: use for fail-closed compatibility.
Plan for hazards Rust doesn't check: overflowing firmware stacks overwrite statics, Wasm linear memory lacks internal guard pages and ASLR, and set_inner_html allows XSS. Tock isolates apps with hardware, yet its kernel had six isolation bugs Rust didn't catch. Run untrusted Wasm per HARD-5.
| Need | Use (minimum safe version) | Avoid (why) | Failure prevented | Status 2026-10-05 |
|---|---|---|---|---|
| DOM-1 Cortex-M stack | flip-link, or cortex-m-rt ≥0.7.6 set-msplim (Armv8-M Mainline) |
Default layout (stack meets statics) | Silent corruption | flip-link 0.1.12, 2025-11-10 |
| DOM-2 Linux kernel | Recheck // SAFETY: claims wherever locks drop |
GCC-based builds (experimental) | Binder-style races | Non-experimental since 7.0 |
| DOM-3 Wasm DOM output | set_text_content; ammonia ≥4.2.1 (4.1.x: ≥4.1.5) for user HTML |
set_inner_html on input |
XSS | 4.2.1, 2026-10-03 |
DOM-4 wasm32-unknown-unknown randomness |
getrandom wasm_js in binaries |
wasm_js in libraries (breaks non-Web builds) |
Broken builds | 0.4.3, 2026-06-17 |
| DOM-5 Wasm allocator | Default | wee_alloc (unmaintained) | Leaks | Last release 2019-08-22 |
| DOM-6 Certified products | Ferrocene: "the first quality-managed, TÜV SÜD–qualified, open source Rust toolchain" (ASIL D, SIL 3, Class C); Safety-Critical Rust Consortium and MISRA Rust addendum rules | MISRA's safe-Rust ratings on unsafe code |
Unchecked unsafe/FFI |
FLS (language spec) adopted 2025-03-26; Consortium draft 0.1.25 |
// VULNERABLE (DOM-3): the browser parses `comment` as HTML, so `<img src=x onerror=…>` runs script
fn show(el: &web_sys::Element, comment: &str) {
el.set_inner_html(comment);
}
fn main() {
let doc = web_sys::window().and_then(|w| w.document());
if let Some(el) = doc.and_then(|d| d.get_element_by_id("comment")) {
show(&el, "<img src=x onerror=alert(1)>");
}
}// FIXED (DOM-3): the browser shows `comment` as plain text and never parses it as HTML
fn show(el: &web_sys::Element, comment: &str) {
el.set_text_content(Some(comment));
}
fn main() {
let doc = web_sys::window().and_then(|w| w.document());
if let Some(el) = doc.and_then(|d| d.get_element_by_id("comment")) {
show(&el, "<img src=x onerror=alert(1)>");
}
}Go deeper
- Wasmtime security: use for sandbox guarantees.
- Kernel Rust docs: use for coding rules.
ZK proofs, threshold signatures and homomorphic encryption mostly fail in maths Rust can't check. Paillier is encryption, not multi-party computation: protocols using it must prove keys well formed.
| Need | Use (minimum safe version) | Avoid (why) | Prevents | Status |
|---|---|---|---|---|
| PRIV-1 ZK circuit | halo2_proofs; MockProver rejecting tampered private cells | Under-constrained circuits (commonest); arkworks (prototype) | Forgery | 0.4.0, 2026-09-29 |
| PRIV-2 Groth16 setup | No setup: halo2; else per-circuit multi-party ceremony parameters | generate_random_parameters (caller keeps "toxic waste") |
Forgery | groth16 0.2.0, 2026-09-26 |
| PRIV-3 Fiat-Shamir hash | Every public input and commitment | Partial transcripts (Frozen Heart) | Forgery | 2022-04-13 |
| PRIV-4 zkVM (proven execution) | risc0-zkvm ≥3.0.3 (2.x: ≥2.3.2) | Old guest image IDs (CVE-2025-61588); 2.0.0–2.0.2 (CVE-2025-52484) | Forgery | 3.0.6, 2026-07-17 |
| PRIV-5 Threshold ECDSA (t-of-n signing) | cggmp24 = "0.7.0-alpha.3" |
Crate cggmp21 (unpatched); GG18/GG20 code like ZenGo multi-party-ecdsa (BitForge, CVE-2023-33241; unmaintained, RustSec silent) | Key theft | Pre-release, 2025-12-04 |
| PRIV-6 Threshold Schnorr | ZcashFoundation/frost, frost-<curve> ≥2.2.0 (RFC 9591) |
frost-secp256k1-tr, rerandomized FROST (unaudited) |
Weakened shares | 3.0.0, 2026-04-23 |
| PRIV-7 FHE | tfhe-rs ≥1.1.0 defaults (IND-CPA^D: share decryptions of honest results only) | IND-CPA parameters; Concrete (Python-first) | Key recovery | 1.8.1, 2026-09-14; no side-channel mitigation: isolate client-key hosts; commercial use: patent licence |
Go deeper
- Zebra, Zallet (beta): use for Zcash (zcashd halted 2026-07-18).
Rust code has had memory corruption (unsafe, std, compiler) and logic, panic, timing, TOCTOU, supply-chain failures. Newest first.
| Disclosed | Event | Root cause | Control | Link |
|---|---|---|---|---|
| 2026-09-21 | Cached Miri output exposed CI secrets | Env saved in target/ |
SUP-6 | blog |
| 2026-09-17 | Video-call lures target crate owners | Social engineering | SUP-1 | blog |
| 2026-08-31 | sudo-rs sudoedit privilege escalation |
Path resolved twice | CON-6 | GHSA-f42v-x7gq-phc8 |
| 2026-08-20 | arrayref hijacked; proc-macro1 build.rs fetched malware |
Social engineering | SUP-1, SUP-4, SUP-5 | RUSTSEC-2026-0260, blog, 2026-09-17 |
| 2026-05-25 | Third-party registries: credential leak, cache overwrite | URL normalization; symlinks | SUP-3 Rust ≥1.96.0 | CVE-2026-5222, CVE-2026-5223 |
| 2026-04-22 | uutils: 113 audit findings; Ubuntu kept GNU cp/mv/rm |
Open TOCTOU bugs | CON-6 | Canonical |
| 2026-03-19 | Cargo's tar let crates chmod directories |
Symlinks followed | SUP-3 Rust ≥1.94.1; SVC-10 | CVE-2026-33056, RUSTSEC-2026-0067 |
| 2026-03-10 | chrono_anchor tried to exfiltrate .env files |
Malicious crate | CRY-9 | RUSTSEC-2026-0039 |
| 2025-12-16 | Linux rust_binder corrupted kernel memory |
Racy SAFETY claim |
CON-7 | CVE-2025-68260 |
| 2025-11-18 | Cloudflare 5xx outage | unwrap() on oversized file |
LIM-2 | Cloudflare |
| 2025-11-11 | sudo-rs skipped authentication; replayed typed passwords | Logic bugs | LIM-4 | CVE-2025-64517, CVE-2025-64170 |
| 2025-10 | TickTock: six Tock isolation bugs; an underflow crashed the kernel | Logic errors, missed checks | TOOL-7, LIM-3 | TickTock |
| 2025-09-12 | rustfoundation.dev phishing for GitHub logins |
Look-alike domain | SUP-1 | blog |
| 2024-06-18 | curve25519-dalek leaked timing | LLVM inserted a branch | CRY-7 ≥4.1.3 | RUSTSEC-2024-0344 |
| 2024-04-09 | Windows batch-file injection in Command |
Incomplete escaping | SVC-11 Rust ≥1.81.0 | CVE-2024-24576, CVE-2024-43402 |
| 2024-02-16 | cve-rs: memory corruption without unsafe |
Open compiler bug #25860 | LIM-1 | RUSTSEC-2025-0028, releases |
| 2022-01-20 | std remove_dir_all symlink race |
TOCTOU | CASE-1 Rust ≥1.58.1 | CVE-2022-21658 |
| 2018–2021 | std buffer overflows, double frees | Bugs in std's unsafe |
CASE-1 Rust ≥1.52.0 | CVE-2018-1000810, CVE-2021-31162 |
Go deeper
- RustSec: use for crate, malware advisories.
- Report Rust toolchain, Cargo or crates.io vulnerabilities to security@rust-lang.org (policy); crate bugs per SUP-9.
- Follow rustlang-security-announcements and the Rust blog for Rust alerts.
- Watch RustSec for crate advisories.
- Discuss on Zulip in #wg-secure-code.
Found advice here that could make code less safe? Report it through private vulnerability reporting. Other errors: open an issue or PR.