Skip to content

About

A static constant-time analysis tool for aarch64/x86-64/rv64.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

substratum

A Haskell tool for static non-constant-time scanning of AArch64, x86-64, and RV64 assembly.

substratum reads compiled assembly and flags instructions whose timing can vary with the data they operate on: conditional and indirect branches, register-indexed memory accesses, variable-time division, variable-amount shifts, vector gathers and scatters, and calls whose work depends on a length or on content. Runtime profiles for GHC, C, Go, Rust, Wasmtime, and V8 filter the benign patterns each runtime emits (stack checks, panic guards, trap stubs, etc.) and recover call edges that a purely structural reading would miss.

A finding is not a leak. It marks an instruction that is data-dependent in general; whether that data is secret is for the auditor to decide. Every finding carries a stable rule ID, and every suppression names the rule and evidence behind it. What substratum could not establish (undecoded instructions, unknown control edges, incomplete call reach) is enumerated rather than omitted.

Usage

Capture assembly from the compiler, or disassemble a built artefact:

$ ghc -S -O2 Foo.hs                            # GHC
$ cabal build --ghc-options=-keep-s-files      # GHC, cabal project
$ rustc --emit=asm -C opt-level=2 foo.rs       # rustc
$ RUSTFLAGS=--emit=asm cargo build --release   # cargo
$ go tool compile -S foo.go > foo.s            # Go
$ objdump -d libfoo.so > libfoo.dump           # linked binary

Then scan it, selecting the ISA (--isa, default aarch64) and runtime (--runtime, default haskell):

$ substratum scan -i test/fixtures/rust-arm64-ct-audit.s --runtime rust
  ppad substratum v0.3.13
  input    test/fixtures/rust-arm64-ct-audit.s · aarch64 att · rust
  profile  aarch64, csel available
  ...
  __ZN8ct_audit12naive_lookup17he4c144cd2e2f8c5dE · ct_audit::naive_lookup
     151  div          udiv x8, x2, x1
     153  reg-index    ldrb w0, [x0, x8]

  __ZN8ct_audit13window_modpow17h59c6beb0be56e612E · ct_audit::window_modpow
     288  div          udiv x8, x21, x19
     420  var-shift    lsr  x8, x20, x8
     423  reg-index    ldp  x8, x9, [x8]
     433  cond-branch  b.ne LBB6_2
  ...
  18 functions · 9 with findings · 37 nct sites · 37 nct facts
  verdict-format aarch64-v3

The exit status is non-zero iff any finding remains active. One instruction (a site) can carry several independent findings (facts), hence the two counts.

Disassembler output takes --from objdump (or otool for Mach-O, v8 for d8 --print-wasm-code dumps), and --syntax selects AT&T (default), Intel, or Plan 9 (default under --runtime go).

Beyond that:

  • -s SYMBOL scans one function and everything it reaches, and lists where the reach walk was incomplete. --diff-vs-whole shows what a whole-file scan finds outside it.
  • --why traces each flagged branch condition to its origin (a loop counter, an immediate, a load, a function argument); the why subcommand prints the full chain for one finding.
  • --cmm maps GHC findings to Haskell source spans via a cmm dump from the same compilation (-g3 -ddump-cmm -ddump-to-file). Go, Rust, and C inputs carry spans in-band when built with debug info.
  • --target-contract arm-dit|intel-doitm discharges operand-timing findings that the vendor documents as data-independent under that mode. Memory-address findings are never discharged.
  • --show-runtime-patterns shows what the runtime profile filtered, and why.
  • --format json|sarif emits machine output, --review applies a ledger of accepted findings, and diff compares findings across two builds.
  • symbols, body, and parse show the call graph, one function's annotated body, and parse coverage.
  • branch-reach and cache-lines partition a function's blocks and cache lines by which side of a branch reaches them, for picking FLUSH+RELOAD probe targets.

Caveats

substratum is a structural classifier: it does no symbolic execution, alias analysis, or secret tracking.

A clean scan covers only the code in the input. parse, or the coverage object in JSON output, reports what was not modelled. A symbol-rooted scan stops at dynamic dispatch, so cross-check it against a whole-file scan.

branch-reach and cache-lines refuse a function with any unknown control edge, since answering from a partial graph overstates exclusivity. GHC's NCG backend enters closures with an indirect jump, so much of an NCG dump refuses; the LLVM backend's indirect call does not. cache-lines also needs byte addresses, so it takes disassembler input only.

go tool objdump's amd64 decoder lacks BMI2, ADX, and EVEX, and garbles hand-written assembly that uses them. substratum flags the symptoms but cannot recover the instructions; use llvm-objdump -d -M intel with --from objdump --syntax intel instead.

V8 input needs a d8 built with v8_enable_disassembler=true; release builds print empty function bodies. Cross-arch wasmtime compile needs cargo install wasmtime-cli, as the nixpkgs build includes only the host backend.

Documentation

substratum <command> --help documents each subcommand.

Performance

Current benchmark figures on a 214k-line GHC dump (test/fixtures/secp256k1NCG.s), on an M4 Silicon MacBook Air, look like (use cabal bench to run the benchmark suite):

  benchmarking parse/aarch64-att large (secp256k1NCG)
  time                 727.8 ms   (719.7 ms .. 739.6 ms)

  benchmarking nct/aarch64 large (ghc)
  time                 116.3 ms   (113.3 ms .. 119.3 ms)

  benchmarking callgraph/aarch64 large
  time                 30.34 ms   (28.20 ms .. 33.56 ms)

Development

You'll require Nix with flake support enabled. Enter a development shell with:

$ nix develop

Then do e.g.:

$ cabal run substratum -- scan -i test/fixtures/good.s

to scan a fixture. Changes that can move classification should be checked against the fixture corpus with:

$ bash scripts/check-nct-baseline.sh

(--update rewrites the baseline.)

About

A static constant-time analysis tool for aarch64/x86-64/rv64.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages