Skip to content

Repository files navigation

repx

repx is a Linux/eBPF prototype for reproducible build-process attestations with DAG provenance. It traces a selected set of process and file events, captures file identities while events are arriving, canonicalizes the observations into a deterministic set of distinct operations, and commits that set with a Merkle root. In output-rooted mode it additionally builds a content-addressed dataflow DAG (artifact/process nodes with read/write/exec edges) and includes the DAG root in the attestation.

The attestation answers a narrow question: did this command reproduce the same set of covered operations and selected outputs as a trusted baseline? It is not a complete record of every kernel action.

Build

nix develop -c ./build.sh

The resulting binary is target/release/repx. Loading the eBPF programs requires root or equivalent BPF capabilities.

Use

Trace a build and produce an attestation:

sudo ./target/release/repx trace \
  --output-root dist \
  -o repx-attestation.json \
  -- make release

Verify that a re-run produces the identical attestation root:

sudo ./target/release/repx verify \
  --output-root dist \
  -a repx-attestation.json \
  -- make release

Verification requires the exact command and explicit output-selection flags used during tracing. This prevents command and artifact policy from being silently inherited from an untrusted JSON file.

When a trusted root is distributed separately, pin it with --expected-root sha256:... to detect replacement of the attestation file.

Use --artifact PATH for individual outputs, --dump-ops PATH to inspect canonical operations, explain for a summary, and diff for added/missing operation hashes and output changes.

Signing

Generate a signing key pair:

repx generate-key --private-key my.key --public-key my.pub

Sign an attestation into a DSSE envelope:

repx sign --key my.key --attestation repx-attestation.json --output signed-envelope.json

Verify a signed envelope:

repx verify-signature --key my.pub signed-envelope.json

Stability Analysis

Use repx stability --strict run-*.json to measure attestation-root stability, process-root stability, and leaf-set Jaccard similarity across repeated traces. The --strict flag exits unsuccessfully unless every attestation root matches.

The privileged nix run .#test-determinism harness applies this analysis to the project's integration workload matrix, running each workload under repx trace 20 times (configurable via DETERMINISM_RUNS) and reporting root stability and leaf-set Jaccard similarity.

sudo nix run .#test-determinism
sudo nix run .#test-determinism -- smoke        # single workload
sudo DETERMINISM_RUNS=50 nix run .#test-determinism

Determinism soak (local race amplifier)

nix run .#soak repeats targeted workloads many times and compares attestation roots. Attestations are written outside the traced workspace so trace artifacts do not perturb the snapshot. On mismatch, artifacts (--dump-ops, --index, repx diff, debug stderr) are saved to a timestamped directory.

sudo nix run .#soak -- forks 50
sudo nix run .#soak -- fd-churn 50
sudo nix run .#soak -- all 50

Workloads: forks, fd-churn, rename-storm, unlink-after-write, cbuild, watch. See docs/threat-model.md for the soundness boundary.

Security Semantics

  • File reads, tool binaries, mmap inputs, and close-time outputs are hashed when their events reach userspace. Only regular files are opened, using nonblocking handles; retained handles allow deleted temporary files to remain hashable.
  • O_RDWR contributes both a read and a write. Writable private mappings are reads; only writable MAP_SHARED mappings are classified as file writes.
  • Process attribution uses a PID-reuse-safe lifetime plus an exec epoch. Forked children inherit the active tool identity and open-descriptor state; a later exec creates a distinct process instance.
  • Relative paths use a per-process working-directory cache inherited across fork, allowing short-lived processes to retain stable final-path attribution after their /proc entries disappear. Unobserved chdir changes remain best-effort.
  • Go importcfg control files are parsed and their random go-build<digits> work-root prefixes are canonicalized. Dependency names, work-relative layout, stable paths, and all other control-file data remain committed.
  • Canonicalization produces a sorted set of distinct covered operations. Counts, ordering, timestamps, and process indices are diagnostic data, not part of the process-set root.
  • Output-rooted attestations commit selected outputs and content-resolved dependencies. Unavailable transient intermediates remain visible in full-trace mode but are omitted from the output dependency walk. Missing, unreadable, and non-regular identities bind stable paths while normalizing recognized session and compiler temporary names.
  • The top-level root binds the process-set root, command, output selection, output hashes, and (in v0.4.0) the DAG root.
  • Event loss fails closed by default. trace --allow-dropped-events is an explicit escape hatch for incomplete diagnostic attestations.
  • io_uring setup/enter syscalls set io_uring_observed in trace metadata. Per-op io_uring attribution is not implemented; use trace --strict-io-uring to refuse such traces. Full threat model: docs/threat-model.md.
  • The eBPF ring uses compact fixed-size events and a 32 MiB capacity to absorb bursty build-system workloads such as Bazel.
  • Attestations can be signed with Ed25519 via DSSE envelopes (repx sign / repx verify-signature). Unsigned attestations must be distributed through a trusted channel or checked with --expected-root.

Provenance

v0.4.0 output-rooted attestations include a dag_root: a Merkle commitment to a content-addressed dataflow DAG extracted from the canonical operation set. This transforms repx from a reproducibility checker into a provenance system that can answer "what inputs and tools produced this output?" without re-running the build.

Nodes:

  • Artifact { content_hash } -- a file identified by its content hash
  • Process { process_index, tool_hash } -- a process instance identified by its executable binary hash and its logical index in the trace

Edges:

  • Read { artifact_hash, process_index } -- process read this artifact
  • Write { artifact_hash, process_index } -- process wrote this artifact
  • Exec { artifact_hash, process_index } -- process was exec'd from this binary

The DAG root is computed via a deterministic length-prefixed binary encoding (SHA-256) that is independent of serialization library, field ordering, or JSON representation. The dag_root is bound into the top-level attestation root alongside the process-set root, command, and output selection.

A DAG smoke test is available to verify the provenance root is present and deterministic:

sudo nix run .#dag-smoke

CO-RE / BTF Portability

The eBPF program discovers tracepoint field offsets at load time by parsing the kernel's format files under /sys/kernel/debug/tracing/events/*/format. Raw tracepoint formats are not covered by BTF/CO-RE, and field offsets vary across architectures and kernel configurations.

When debugfs is unavailable, set REPX_FALLBACK_OFFSETS=1 to use built-in x86-64 offsets (x86-64 only):

REPX_FALLBACK_OFFSETS=1 sudo ./target/release/repx trace ...

Fallback offsets are not portable across architectures or kernel configurations. CI environments should mount debugfs instead of relying on fallback offsets.

Coverage Boundary

Behavior Status
openat, openat2, open, close, file-backed mmap Covered
rename, renameat (old/new path tracking) Covered (distinct operation types)
unlink, unlinkat Covered (distinct operation type)
exec, fork, exit Covered (process tracking with PID-reuse safety and exec epoch)
Temp-file replacement Final rename paths are attributed; rename and deletion are distinct operations
dup*, inherited descriptors Fork-inherited descriptors are tracked; dup* remains incomplete
sendfile, copy_file_range, io_uring Not covered
Network I/O and remote inputs Not covered
External relative-path reads in --watch mode May be missed by kernel prefix filtering
External writes in --watch mode Resolved and prefix-checked in userspace before file observation
Paths longer than 4096 bytes Truncated paths are flagged and fail-closed unless --allow-dropped-events
--watch mode on busy hosts System-wide relative-path writes pass through the ring buffer for userspace filtering; expect higher overhead than fork-tree tracing

An attacker who controls the build can deliberately choose an uncovered path. Treat the current prototype as a deterministic commitment over observed covered behavior, not proof that no malicious activity occurred.

Cross-machine verification

Attestations embed content hashes for regular files. Missing, unreadable, or non-regular files are identified by hashes of their absolute paths (or workspace:<relative> paths when a workspace root is known), so verification is reliable on the same machine but may differ across checkout locations.

Tests

cargo test --workspace

Privileged integration scenarios are documented in tests/README-testing.md.

Additional test harnesses (require root):

sudo nix run .#dag-smoke           # DAG provenance root presence and determinism
sudo nix run .#test-determinism    # Repeated-trace stability across workloads

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages