Skip to main content

Talus — Architecture

Internal architecture of Talus: the kernel-side eBPF programs, the userspace event pipeline, the sliding-window alerting heuristic, and the output layer.


1. System overview

┌─────────────────────────────────────────────────────────────────────────────┐
│ KERNEL SPACE │
│ syscall entry eBPF tracepoint programs map │
│ ┌───────────┐ ┌──────────────────────────────────┐ ┌──────────────┐ │
│ │ execve │──►│ process-monitor-ebpf │──►│ EVENTS │ │
│ │ openat │ │ #[tracepoint] sys_enter_execve │ │ PerfEventArray│ │
│ └───────────┘ │ #[tracepoint] sys_enter_openat │ └──────┬───────┘ │
│ └──────────────────────────────────┘ │ per-CPU │
└─────────────────────────────────────────────────────────────────┼───────────┘
┌─────────────────────────────────────────────────────────────────▼───────────┐
│ USERSPACE │
│ reader thread ──► MPSC channel ──► Monitor (sliding window + alerting) │
│ │ │
│ TUI │ JSON │ Plain │ Diagnose │
└──────────────────────────────────────────────────────────────────────────────┘

Two crates form the workspace:

CrateRoleToolchain
process-monitor-ebpfKernel-side tracepoint programs, #![no_std], aya-ebpfRust nightly (-Z build-std)
process-monitorUserspace: loader, reader thread, monitor core, output modesRust stable

2. Kernel side — process-monitor-ebpf

Both programs run in tracepoint context on syscall entry, before the kernel copies arguments, so all userspace pointers are read with bpf_probe_read_user — never dereferenced. This keeps the code verifier-safe.

The kernel and userspace agree on a fixed #[repr(C)] layout so records are memcpy'd across the perf buffer without serialization:

pub struct ProcessEvent {
pub event_type: u8, // 0 = EXEC, 1 = OPEN
pub pid: u32,
pub uid: u32,
pub comm: [u8; 16], // process comm (truncated)
pub filename: [u8; 64], // target path (truncated)
}

An 85-byte payload that occupies 92 bytes on the wire — small and fixed-size, which makes per-CPU perf buffering cheap (no allocation, no variable-length encoding in kernel context).

3. Userspace — process-monitor

Startup sequence (Monitor::start)

  1. Privilege check — bails unless geteuid() == 0.
  2. Object loadaya::Ebpf::load_file parses the compiled eBPF object.
  3. Program load + attach — each TracePoint program attaches to syscalls/sys_enter_execve / sys_enter_openat.
  4. Map hand-off — the EVENTS PerfEventArray moves into the reader thread.
  5. Channel — MPSC connects reader thread → monitor.

Reader thread (talus-reader)

  • Enumerates online CPUs, opens one PerfEventArrayBuffer per CPU.
  • Decodes batches into pre-allocated BytesMut pools (zero per-event allocation in the hot loop).
  • Counts events.lost (perf-buffer overruns) and forwards Msg::Lost.
  • Idles 1 ms when no buffer has data — ~1 ms latency, near-zero idle CPU.

Monitor core

stats: HashMap<u32, ProcStats> // pid → cumulative stats
windows: HashMap<u32, VecDeque<Instant>> // pid → open timestamps (1 s window)

handle_event records stats, pushes Open timestamps onto the PID's sliding window, evicts entries older than 1 s, and emits an Alert exactly when the window crosses the configured threshold (--alert-threshold, 0 disables).

Output layer

Monitor::poll returns a Vec&lt;Output&gt; per tick (Event | Alert) routed by mode: TUI (frankentui: 7-panel cyberpunk interface — events, process tree, network, top files, extensions, alerts, heatmap) · JSON (NDJSON) · Plain · Diagnose (verifies tracepoint IDs under /sys/kernel/tracing/events, loads + attaches, listens 5 s, prints counters).

4. Data flow summary

kernel userspace reader monitor core output
────────── ───────────────── ───────────── ──────
openat entry ──► EVENTS map ──► perf buffer ──► Msg::Event ──► sliding window ──► TUI / JSON / plain
(per CPU) │ │
└─ Msg::Lost ────► lost counter ──► status bar
└─ Alert (threshold) ──► alerts panel

5. Performance characteristics

AspectDesign
Kernel overheadTwo tracepoint programs; fixed-size record; no allocation
Userspace decodePre-allocated BytesMut pools; zero per-event allocation
LatencyEvents typically visible in < 1 ms
Idle CPUReader sleeps 1 ms when no buffers have data
MemorySliding window evicts every poll; maps bounded by live PIDs
BinaryFull LTO + strip = "symbols" + panic = "abort" release profile