Study material
concept 4 · Rust

Rust as a defense language

Nemesis was written in Rust on purpose. A security tool cannot itself be a source of bugs or denial of service. That is the thread of this group: every language choice exists so the defender never becomes the problem.

In 30 seconds

The project is a crate workspace with Cargo.lock committed (reproducible build). Errors do not become loose exceptions: they are handled with Result/Option and match. If something panics anyway, a catch_unwind converts it into exit 2 (fail-closed: when in doubt, block). The regex engine is linear-time, no backtracking, so there is no ReDoS. And there are hard limits (depth, size) so the scanner stays deterministic and never hangs.

A hostile file targets the scanner; Rust guarantees: error as value, panic into exit 2, linear-time regex without ReDoS, and depth plus size limits. It all converges to fail-closed.
Errors handled, panic becomes exit 2, linear regex, hard limits: the tool fails closed, never open.

From scratch: what Rust provides

Rust has no "throw and someone catches it upstream" exceptions. An error is a value: a function returns Result (succeeded with T or failed with E) or Option (has a value or not), and the compiler forces both cases to be handled. The ownership model guarantees memory safety without a garbage collector. A workspace groups several crates under a single dependency resolution, and Cargo.lock pins exact versions. A panic is the controlled abort of a thread; catch_unwind catches it at the boundary.

Why it matters

Consider the worst case: the scanner receives a file built to take it down. If it overflowed the stack, looped forever or crashed silently, the attacker would have found a way to disable the defense just by writing a file. Rust closes those doors: errors are handled, panic becomes a block, regex cannot explode, and recursion and size have ceilings. The tool fails closed, never open.

Fail-open swallows the error and the action passes unscanned; Nemesis fail-closed captures the panic and turns it into exit 2, so the error never opens the door.
Fail-open vs fail-closed: the same internal error, two opposite fates. Under Nemesis, crashing the scanner opens nothing.

How it correlates

Everything here serves the contract from group 1: fail-closed is what guarantees that even an internal error ends in exit 2, keeping the blocking contract honored. The depth limits speak to the recursive decoder (nested payloads cannot become DoS) and to the pipeline (deterministic order). And the committed Cargo.lock is part of the supply chain posture: no silent dependency bumps.

How Nemesis applies it

The workspace declares the four crates and pins resolution; Cargo.lock goes into git:

📄 .nemesis/Cargo.toml:1-9 · workspace de quatro membros

[workspace]
resolver = "2"
members = [ "ast-linters", "ebpf-kernel", "nemesis-defender", "nemesis-doctor" ]

Error as value, not exception: reading a file is a match over Result, and the failure has explicit handling.

📄 .nemesis/nemesis-defender/src/main.rs:149-156 · Result/Option no caminho de I/O

let content = match std::fs::read(&path) {
    Ok(c) => c,
    Err(e) => {
        eprintln!("[nemesis-defender] Cannot read {}: {}", path.display(), e);
        std::process::exit(1);
    }
};

And the boundary fail-closed: the pretool main runs inside a catch_unwind; any panic becomes exit 2.

📄 .nemesis/hooks/nemesis-pretool-check-unix.rs:1023-1041 · catch_unwind → exit 2

fn main() {
    match std::panic::catch_unwind(|| { run_pretool() }) {
        Ok(()) => {}
        Err(panic) => {
            eprintln!("[NEMESIS ERROR] Hook panic: {}", /* msg */);
            std::process::exit(2);   // fail-closed: pânico = bloqueio
        }
    }
}

Against DoS, limits are declared as constants, for example the decoder maximum depth and the size ranges worth scanning:

📄 .nemesis/nemesis-defender/src/scanner/decoder.rs:22-28 · limites contra DoS

const MAX_DECODE_DEPTH: u8 = 3;        // payload aninhado não vira recursão infinita
const MIN_DECODED_LEN: usize = 8;
const MAX_CANDIDATE_LEN: usize = 4096; // não escaneia blobs gigantes

Decisions and limits

On the path that processes untrusted input there is no unwrap() or expect(): unwrap_or_else, .ok()? or let _ = handle best-effort, because an unwrap there would be an attacker-controlled panic. Fail-closed is applied by construction: exit 2 on panic is right for the pretool, where "when in doubt, block" is the goal; in the daemon the rule differs, it never deletes when in doubt (see group 9).

And ReDoS cannot reach the scanner: the adopted regex crate is guaranteed linear-time, no backtracking, so a hostile pattern cannot hang it. The exfil_chain and denylist patterns run on that engine.

Self-check

Why must a panic become exit 2 instead of just crashing?

Because crashing without a blocking code could be read as "passed". Converting the panic into exit 2 keeps the contract: when in doubt, the action is denied. That is fail-closed.

How does Rust keep the scanner from being taken down by a malicious file?

Error handled as value (no loose exceptions), memory safety through ownership, linear regex (no ReDoS) and hard depth/size limits. The tool fails closed, not open.

Why commit the Cargo.lock?

Because Nemesis ships binaries: pinning exact versions gives reproducible builds and prevents a dependency changing underneath (a supply chain vector). CI compiles with --locked.

Further reading

Citações verificadas contra o repositório. Voltar ao índice.