agents/skills/injectable/l1/rust-unsafe-audit/SKILL.md
L1 supplement - audits Rust-specific hazards: unsafe blocks, uninitialized memory, Send/Sync violations, panic safety in hot paths, drop order, FFI.
npx skillsauth add plamentsv/plamen rust-unsafe-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
L1 trigger:
L1_PATTERN=trueAND target language = Rust Inject Into: Every L1 depth agent working on Rust code, in addition to the main skill Finding prefix:[RS-N]Status: v0.1 draft, Round 4 exemplars pending
Supplement to the main L1 skills when the target is written in Rust. Rust's safe subset prevents most memory safety bugs by default, but node clients use unsafe for performance (crypto, serialization), panic! for unrecoverable states, and FFI (blst, rocksdb, librocksdb-sys). Each is a bug class.
Every unsafe block is a memory-safety assertion by the author: "I guarantee this is safe." The audit must verify that guarantee.
Detection:
cargo-geiger counts unsafe per crate (cross-platform)unsafe { $$ } and unsafe fn $NAME and unsafe impl// SAFETY: ... comment should justify the unsafe)Checklist per unsafe block:
#[repr(C)], #[repr(transparent)])Tag: [RS-UNSAFE:{loc}:{category}]
Rust allows uninitialized memory via MaybeUninit or mem::uninitialized (deprecated). Reading uninitialized memory is Undefined Behavior.
Detection:
MaybeUninit, mem::uninitializedassume_init() is calledMaybeUninit::zeroed() for non-zeroable types is UBTag: [RS-UNINIT:{loc}]
Send allows a type to be transferred between threads; Sync allows shared references across threads. Manually implementing them (unsafe impl Send for T) is an assertion.
Detection:
unsafe impl Send for X / unsafe impl Sync for XRc<T> in an async context: Rc is not Send, only Arc isTag: [RS-SEND-SYNC:{type}:{justification}]
Panics in Rust unwind the stack by default. In a consensus-critical hot path, a panic kills the node. Exception: panic = "abort" in Cargo.toml makes panics immediately terminate the process.
Detection:
.unwrap() on user-input-derived values: if the input is attacker-controlled, panic is reachable.expect("...") samearr[i] indexing on attacker-controlled iassert! / assert_eq! in hot paths with assertion on untrusted dataTag: [RS-PANIC:{loc}:{input-source}]
Rule of thumb: any function that parses peer/RPC input must not .unwrap() or panic-index. Use ? and Result throughout.
Rust's Drop trait runs deterministically when a value goes out of scope. Bugs:
Rc / Arc prevent dropDetection:
impl Drop for X: is the drop logic panic-safe?Arc<Mutex<T>> cycles: are there weak references breaking cycles?Tag: [RS-DROP:{issue}]
Most L1 Rust clients call C libraries: blst (BLS), rocksdb-sys (storage), libsecp256k1-sys (signatures).
Detection:
extern "C" call siteTag: [RS-FFI:{function}:{issue}]
When binding to C / C++ / CUDA / HIP via FFI (extern "C", bindgen, hand-written headers, *.cu / *.hip files), audit every C integer type use. C type sizes differ between platforms:
| C type | Linux/macOS (LP64) | Windows MSVC (LLP64) | Safe? |
|---|---|---|---|
| unsigned long / long | 64 bits | 32 bits | NO — silent truncation on Windows |
| unsigned long long / long long | 64 bits | 64 bits | YES |
| int / unsigned int | 32 bits | 32 bits | YES |
| size_t / uintptr_t | pointer-width | pointer-width | YES (verify) |
| uint64_t / int64_t (<stdint.h>) | 64 bits | 64 bits | YES — preferred |
Check:
.h / .c / .cpp / .cu / .hip file in scope for unsigned long and long declarationsuint32_t, uint64_t) AND verify the corresponding Rust binding uses c_uint / c_ulong consistently with the actual C type after replacementc_ulong in Rust IS the platform-native unsigned long, so it has the same problem on Windows — fixed-width on both sides is the only safe answerunsigned long follows the host platform's ABI on most compilers, so Windows MSVC builds will silently truncateFail mode: 64-bit values (chunk offsets, partition hashes, block heights) silently truncated to 32 bits on Windows builds, producing different consensus output from Linux builds → silent network split between operators on different platforms.
Tag: [RS-FFI:c-type-portability:{file}:{line}]
Many Rust DB wrappers (mdbx-rs, sled, rocksdb) provide an update-style helper that runs a closure inside a transaction and commits afterwards. A common bug pattern: the wrapper commits the transaction unconditionally before checking whether the closure returned Ok or Err.
Check:
update_* / transact_* / with_txn_mut helper, read the implementationtxn.commit() calllet result = closure(&mut txn); match result { Ok(v) => txn.commit()?, Err(e) => txn.abort()?; return Err(e); }let result = closure(&mut txn); txn.commit()?; return result;DbExt::update_eyre traits) where the same pattern appears in user codeFail mode: a failed business-logic closure result still commits its partial database mutations, violating transactional integrity. State and error logs disagree.
Tag: [RS-DB:commit-before-check:{file}:{line}]
Rust panics on debug overflow, wraps on release (unless checked_* / wrapping_* / overflow-checks=true in profile).
Detection:
checked_add, checked_mul, etc.wrapping_* usage: is wrapping actually the intended semantic?as casts across integer sizes: are they truncating?Tag: [RS-OVERFLOW:{loc}:{op}]
std::sync::Mutex) across an await point is a deadlock risk (the future can be re-polled on another thread)tokio::spawn without joining: fire-and-forget tasks can leak.await inside a synchronous lock: use tokio::sync::Mutex insteadtokio::select!tokio::select! drops the not-chosen branch's future. If that branch was mid-critical-section (partial write to peer-state, half-appended to mempool, mid-decrement of a semaphore), the drop leaves state torn. Flag every tokio::select! where a branch mutates shared state without holding the mutation inside an async block that owns its guards. Tokio docs' "Cancellation safety" note applies: tokio::io::AsyncReadExt::read is cancel-safe; read_exact is not. Any non-cancel-safe future inside a select! is a correctness bug.
Tag: [RS-ASYNC:cancel-unsafe:{file}:{line}].
Send bounds across .awaitA future is Send only if every value it holds across an .await is Send. Holding a Rc<T>, a raw pointer, or a MutexGuard<!Send> across an .await silently pins the future to one thread and defeats the work-stealing scheduler (or fails to compile if the executor requires Send). Grep !Send types in async functions and verify they are dropped before the first .await.
Tag: [RS-ASYNC:non-send-across-await:{file}:{line}].
tokio::time::timeout does NOT cancel inner worktimeout(d, f).await only stops awaiting f — the spawned task behind f keeps running unless explicitly cancelled via a CancellationToken or abort handle. Requests that "timed out" at the caller still consume server resources to completion. In L1 RPC / gossip handlers this is a DoS amplifier: attacker sends N slow requests, each caller times out at 5s, but the server keeps all N tasks alive for the full work.
Tag: [RS-ASYNC:timeout-without-cancel:{file}:{line}].
Manually implementing Future and projecting Pin<&mut Self> to &mut Field requires either #[pin_project] or hand-written unsafe that respects structural pinning. Bare &mut self.inner_fut after self: Pin<&mut Self> is UB if inner_fut is !Unpin.
Tag: [RS-ASYNC:pin-projection-ub:{file}:{line}].
Tag (catchall): [RS-ASYNC:{loc}:{issue}]
Primitive types have alignment requirements (u32: 4, u64: 8, u128: 16). Writing through a raw pointer cast from *mut u8 to *mut u64 is undefined behavior unless the source pointer is aligned to the target type. CUDA/SIMD/zero-copy deserialization paths hit this constantly.
Methodology:
*mut T / *const T / &mut [u8] → &mut [U] cast in FFI-adjacent or zero-copy code (CUDA kernels, C FFI, bytemuck, zerocopy, transmute, ptr::write, ptr::read)[u8; N] are NOT aligned to u64/u128 by defaultVec<u8> is 1-byte aligned; not sufficient for SIMD load#[repr(align(N))] or AlignedVec gives a static guaranteechar* → uint64_t* writes on per-thread stack memory are a common alignment landmineCommon failures:
char buf[32] cast to uint64_t* without alignment guaranteebytemuck::cast_slice_mut used without checking source alignment (panics in debug, silent UB in release if check_cast is disabled)transmute::<&[u8], &[u32]> with no alignment assertionptr::copy_nonoverlapping at byte offsets that are not multiples of the target type sizeTag: [RS-ALIGNMENT:{cast-site}:{src-type}→{dst-type}]. Severity: Medium by default, upgrade to High if the UB is reachable from untrusted input.
RS-NEAR Signature::verify pre-auth panic ($150,000 bounty, Zellic, December 2023) — Message::from_slice(data).expect("32 bytes") and RecoveryId::from_i32().unwrap() in a pre-authentication handshake message handler. A single crafted p2p packet kills any node. Fixed in PR #10385 (commit e0f0da5c3dde29122e956dfd905811890de9a570). Zellic writeup. Skill catch point: Section 4 (panic safety). This is the canonical "unwrap in pre-auth handler" bug.
Academic study of real-world Rust memory-safety bugs (Songlh et al.) — systematic study identifying three bug classes: (1) automatic memory reclaim, (2) unsound function, (3) unsound generic/trait. Understanding Memory and Thread Safety Practices and Issues in Real-World Rust Programs. Skill catch point: Section 1 — for every unsafe block, document: (a) invariant the block assumes, (b) caller-side obligations, (c) test that violates each invariant by input mutation.
cargo-audit blind spots — cargo-audit only reports RustSec-tracked advisories; crates like ring, mio, crossbeam with heavy unsafe need manual review regardless of audit score. Rust Vulnerability Scanning: What cargo audit Misses. Skill catch point: run cargo-audit AND produce an unsafe-density report (cargo-geiger). Any crate with >X% unsafe lines in the dep tree gets manual review regardless of audit score.
The NEAR pattern — .unwrap() / .expect() / panic!() in any code reachable from network input — is the single most common Rust L1 bug and deserves a dedicated automated sweep. Grep every module reachable from p2p message deserialization and flag every panic-prone call as a finding until proven bounded.
bls-aggregation-audit (FFI to blst), execution-client-hardening (revm unsafe blocks)development
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project documentation checks. Generates a scored Audit Readiness Report and optionally runs static analysis. Trigger on: "prepare for audit", "audit readiness", "pre-audit check", "audit prep", "NatSpec check", or any request to review a Solidity codebase before a security review.
development
Launch the Plamen deterministic Web3 security audit pipeline
development
Run the Plamen smart-contract audit wizard in Codex
testing
Launch the Plamen deterministic L1 infrastructure audit pipeline