SuperAPI AI Directory

rust-strict

Security & auditing

View repository

Updated 10-15 07:42

About this skill

Rust Strict Standard

Security and strictness rules complementing the existing rust-skills (179 rules). These rules are derived from 5 production Rust projects.

CRITICAL: Workspace Lint Configuration

Every Rust project must configure workspace lints. Baseline:

# Cargo.toml (workspace root)
[workspace.lints.rust]
unsafe_code = "deny"              # No unsafe in production code
unused_qualifications = "deny"

[workspace.lints.clippy]
unwrap_used = "deny"              # Force proper error handling
expect_used = "deny"              # Same: use ? or ok_or_else()

Per-crate opt-in:

# crates/my-crate/Cargo.toml
[lints]
workspace = true

Exceptions (feature-gated, never blanket)

// FFI crate only: deny everywhere else
#![cfg_attr(feature = "local-llm", allow(unsafe_code))]

// Static initialization (regex, lazy): this is the ONLY acceptable expect()
static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"...").expect("regex must compile") // Bug if this fails
});

CRITICAL: Error Handling Hierarchy

Rule: Library crates use thiserror, app crates use anyhow

// Library crate: structured errors
#[derive(Debug, thiserror::Error)]
pub enum GatewayError {
    #[error("authentication required")]
    AuthRequired,

    #[error("rate limited: retry after {retry_after_ms}ms")]
    RateLimited { retry_after_ms: u64 },

    #[error("payload too large: {size} exceeds {max} bytes")]
    PayloadTooLarge { size: usize, max: usize },
}

impl GatewayError {
    pub fn status_code(&self) -> StatusCode { /* match self */ }
    pub fn is_retryable(&self) -> bool { matches!(self, Self::RateLimited { .. }) }
}

// App crate: flexible propagation
fn main() -> anyhow::Result<()> {
    let config = load_config().context("failed to load configuration")?;
    Ok(())
}

Rule: Tauri commands return Result<T, String>

#[tauri::command]
pub async fn my_command(
    state: tauri::State<'_, AppState>,
) -> Result<ResponseData, String> {
    let data = do_work().map_err(|e| format!("operation failed: {e}"))?;
    Ok(data)
}

Tauri serializes errors as strings: this is the framework constraint, not a hack.

Rule: Never Box<dyn Error>: use domain-specific enums

// BAD
fn process() -> Result<(), Box<dyn std::error::Error>> { ... }

// GOOD
fn process() -> Result<(), GatewayError> { ... }

CRITICAL: Unsafe Audit Rules

When unsafe is acceptable (with mandatory SAFETY comment)

// 1. SIMD intrinsics with runtime feature detection
#[target_feature(enable = "avx2,fma")]
unsafe fn cosine_similarity_avx2(a: &[f32], b: &[f32]) -> f64 {
    // SAFETY: AVX2 feature verified by caller via is_x86_feature_detected!
    let va = unsafe { _mm256_loadu_ps(a_ptr.add(offset)) };
    // offset < chunks*8 <= n: bounds checked by loop structure
}

// 2. Test-only env manipulation (with guard lock)
#[cfg(test)]
fn test_env_var() {
    let _guard = env_lock(); // Prevents test race conditions
    // SAFETY: env_lock serializes all env var access in tests
    unsafe { std::env::set_var("KEY", "value") };
}

When unsafe is NEVER acceptable

  • String/buffer manipulation (use safe APIs)
  • Pointer arithmetic without bounds proof
  • transmute (almost always wrong: use From/Into)
  • Implementing Send/Sync manually (unless wrapping C FFI)
  • unsafe impl without audited invariants

HIGH: Concurrency Safety

Rule: Never hold RwLock/Mutex across .await

// BAD: deadlock risk
let mut data = state.data.write().await;
data.insert(key, value);
let json = serde_json::to_string(&*data)?;
tokio::fs::write(path, json).await; // STILL LOCKED

// GOOD: clone, drop, then write
let json = {
    let mut data = state.data.write().await;
    data.insert(key, value);
    serde_json::to_string(&*data)?
}; // Lock dropped
tokio::fs::write(path, json).await;

Rule: Never hold RwLock across disk I/O

// BAD
let mut cookies = state.cookies.write().await;
cookies.insert(name, result);
std::fs::write(&path, serde_json::to_string(&*cookies)?); // SYNC I/O WHILE LOCKED

// GOOD
let json = {
    let mut cookies = state.cookies.write().await;
    cookies.insert(name, result);
    serde_json::to_string(&*cookies)?
};
tokio::fs::write(&path, json).await;

Rule: Use atomics for flags/counters, RwLock for data

pub struct AppState {
    // Atomics: lock-free for simple state
    pub is_running: AtomicBool,
    pub connection_count: AtomicUsize,

    // RwLock: for collections and complex data
    pub sessions: RwLock<HashMap<String, Session>>,

    // Watch channel: for shutdown signaling
    pub shutdown: watch::Sender<bool>,
}

Rule: BoundedMap for all caches (prevent memory leaks)

// BAD: unbounded HashMap cache
let cache: HashMap<String, CachedValue> = HashMap::new();

// GOOD: bounded with capacity + TTL
let cache = BoundedMap::new(1000, Duration::from_secs(300));

HIGH: Secret Handling

Rule: Secrets in Keychain, never plain files

// macOS: system `security` CLI (subprocess, not FFI)
#[cfg(target_os = "macos")]
fn keychain_get(account: &str) -> Result<Option<String>, String> {
    let output = Command::new("security")
        .args(["find-generic-password", "-s", "AppName", "-a", account, "-w"])
        .output()
        .map_err(|e| format!("keychain read failed: {e}"))?;
    // ...
}

// Fallback: Tauri plugin-store (encrypted)
#[cfg(not(target_os = "macos"))]
fn save_fallback(app: &AppHandle, name: &str, value: &str) -> Result<(), String> { ... }

Rule: Use obfstr!() for hardcoded strings in binary

// BAD: visible in binary strings
let api_url = "https://api.internal.example.com/v2";

// GOOD: obfuscated at compile time
let api_url = obfstr::obfstr!("https://api.internal.example.com/v2");

Rule: Validate empty strings on secret storage

pub fn save_secret(name: &str, value: &str) -> Result<(), String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Err("Secret value cannot be empty".to_string());
    }
    // Proceed with trimmed value
}

HIGH: Input Validation (Tauri Commands)

Rule: Enum-constrain all command inputs

// BAD: arbitrary string input
#[tauri::command]
fn spawn_agent(agent: String) -> Result<(), String> {
    Command::new(&agent).spawn(); // COMMAND INJECTION
}

// GOOD: enum-constrained
#[derive(Debug, Clone, Deserialize)]
pub enum AgentType { Claude, Codex, Opencode }

#[tauri::command]
fn spawn_agent(agent: AgentType) -> Result<(), String> {
    let binary = match agent {
        AgentType::Claude => "claude",
        AgentType::Codex => "codex",
        AgentType::Opencode => "opencode",
    };
    // Safe: only known values
}

Rule: Use tempfile crate for temp files

// BAD: predictable path, symlink attacks
std::fs::write("/tmp/prompt.txt", prompt)?;

// GOOD: unpredictable, auto-cleanup
let mut tmp = tempfile::NamedTempFile::new()
    .map_err(|e| format!("temp file failed: {e}"))?;
writeln!(tmp, "{}", prompt)?;
let path = tmp.path().to_string_lossy().to_string();

MEDIUM: Dependency Rules

Rule: Explicit feature control

# BAD: pulls in everything
reqwest = "0.12"

# GOOD: only what you need
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }

Rule: rustls-tls over native-tls

Pure Rust TLS stack: no OpenSSL dependency, auditable, consistent behavior.

Rule: Workspace dependency inheritance

# Workspace Cargo.toml
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }

# Crate Cargo.toml
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }

HIGH: Type Safety Patterns

Rule: Newtype for IDs and units

// BAD: any UUID can be passed where any ID is expected
pub fn ban_user(uid: Uuid, by: Uuid) { ... }

// GOOD: type system rejects swapped arguments
pub struct UserId(Uuid);
pub struct AdminId(Uuid);

pub fn ban_user(uid: UserId, by: AdminId) { ... }

Same pattern for units: Bytes(u64), Millis(u64), RetryCount(u32).

Rule: #[must_use] on results that should not be silently dropped

#[must_use = "builder is incomplete until .build() is called"]
pub struct RequestBuilder { ... }

#[must_use]
pub fn try_acquire(&self) -> Option<Lease> { ... }

Result<T, E> already has #[must_use]. Add it to builders, locks, and validation outcomes.

Rule: Explicit overflow handling on arithmetic

// BAD: wraps silently in release mode
let next = current + delta;

// GOOD: pick the semantic you mean
let next = current.checked_add(delta).ok_or(Error::Overflow)?;  // fail
let next = current.saturating_add(delta);                        // clamp
let next = current.wrapping_add(delta);                          // wrap (rare, document why)

Default to checked_* for anything user-influenced (sizes, counts, indices).

HIGH: Edition 2024

Edition 2024 ships with Rust 1.85 (Feb 2025). Current stable is 1.95.0. New crates start at edition 2024 with current stable. Migration changes that affect strict-mode rules:

Rule: Wrap external symbols in unsafe extern "C" blocks

// BAD: edition 2024 hard error
extern "C" { fn external_fn(); }

// GOOD
unsafe extern "C" {
    fn external_fn();
}

Forces the FFI declaration site to acknowledge unsafety.

Rule: unsafe_op_in_unsafe_fn is warn-by-default

Inside unsafe fn, every individual unsafe operation now needs its own unsafe { ... } block. This makes the audit surface explicit, do not bypass with #[allow].

Rule: Capture lifetimes explicitly with use<...> on RPIT

// Edition 2024: opaque return types capture all generic params by default
fn parse(s: &str) -> impl Iterator<Item = &str> + use<'_> { ... }

The use<> syntax is the escape hatch for the new capture rules. Reach for it when the inferred capture set is wrong.

Rule: gen is a reserved keyword

If you have variables, functions, or modules named gen, rename them before migrating.

Rule: tracing for structured logs, not log or env_logger

use tracing::{info, instrument, warn};

#[instrument(skip(state), fields(user_id = %req.user_id))]
async fn handler(state: AppState, req: Request) -> Result<Response, Error> {
    info!("processing request");
    ...
}

tracing is the de-facto standard. Pair with tracing-subscriber for output and tracing-opentelemetry for distributed tracing.

MEDIUM: Release Profile

Desktop apps (smallest binary)

[profile.release]
strip = true
lto = true            # Full LTO
codegen-units = 1
opt-level = 3
panic = "abort"        # No unwinding

Server/gateway (balanced)

[profile.release]
strip = true
lto = "thin"          # Faster compile than full LTO
codegen-units = 1

Dev profile (fast compile)

[profile.dev.package."*"]
opt-level = 3         # Optimize deps in dev (faster runtime)

Vulnerability Checklist

  • No unsafe blocks without SAFETY comments and justification
  • No .unwrap() or .expect() in production (deny via lint)
  • No string-constructed commands (enum-constrained)
  • No predictable temp file paths (use tempfile crate)
  • No secrets in source code (use env vars, Keychain, obfstr)
  • No unbounded caches (use BoundedMap with capacity + TTL)
  • No locks held across .await or disk I/O
  • CSP configured in tauri.conf.json (for Tauri apps)
  • cargo audit passes with no advisories
  • cargo clippy -- -D warnings clean