Build1 publisher2 min readPublished
Fifty openat calls in one second is the whole ransomware rule in talus-process-monitor
The eBPF side of the MIT-licensed talus-process-monitor just reports file opens; the counting, the per-PID rate window and the SIGKILL all live in a userspace loop, a perf-buffer hop after the syscall.
The Engineer · Build desk

What happened
- The MIT-licensed talus-process-monitor attaches aya tracepoints to sys_enter_execve and sys_enter_openat and pushes a fixed-layout event for each call into a per-CPU PerfEventArray.
- A userspace loop keeps a one-second sliding window of open timestamps per PID and raises an alert when the count reaches the default threshold of 50.
- In auto-kill mode the same loop calls libc::kill with SIGKILL on the offending PID, a signal the process cannot catch.
- Running it takes Linux 5.8 or newer, root or CAP_BPF plus CAP_SYS_ADMIN, Rust nightly and clang.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Coverage is exactly the hook list: encryption that reaches files by some route other than openat leaves the per-process rate flat, and the alert stays quiet.
- cost Every file-opening process on the host pays for the instrumentation, in a path copy and a perf-buffer slot, not only the process under suspicion.
- exposure A false positive ends in SIGKILL, so a mis-tuned threshold takes down a legitimate process with no chance for it to flush state or clean up.
- decision Whoever switches on auto-kill turns a tuning number into a termination policy, and that number has to come from measured behaviour on the host it will run on.
The eBPF half of talus-process-monitor only reports. It reads the PID from `bpf_get_current_pid_tgid() >> 32`, pulls the path out of userspace memory with `bpf_probe_read_user_str_bytes`, and calls `EVENTS.output` [8][9]. The rate math sits in userspace: one `VecDeque<Instant>` per PID, trimmed to a one-second cutoff, with `window.len()` checked against the threshold [11]. The response is `libc::kill` with SIGKILL [13]. Both of those happen after the event has crossed a per-CPU perf buffer [7].
The alert branch tests equality, not greater-or-equal: `stats.window_opens == self.threshold` [12]. The window grows one event at a time, so in the ordinary case the count lands on the threshold value and fires [11]. Lose one event between the kernel and the ring and the count can step from 49 to 51, straight past the branch [22]. I would write it as a `>=` with a per-PID latch.
The event is fixed size. Under `repr(C)`, `event_type` sits at byte 0, three bytes of padding follow, `pid` at 4, `uid` at 8, `comm` at 12 through 27 and `filename` at 28 through 91, so each open costs 92 bytes [6][17]. At the threshold rate that is 4,600 bytes a second from one process [18]. The post says a ransomware process can open thousands of files within seconds, and at 1,000 opens a second the same struct moves about 92 KB a second [1][19]. Each CPU has its own buffer, so concurrent processes do not block each other [7].
The counter moves only on `sys_enter_openat` [4][11]. Only opens that pass through that syscall move the per-process rate [20]. The path field is 64 bytes, so longer paths reach userspace truncated [21]. The decision reads only the count, so that truncation is a forensics limit only [12].
The kernel-side constraints are handled straight. There is no `memcpy`, no `memset` and no `format!` available, so the copy is a hand-written `while` loop over bytes to keep LLVM from emitting a builtin, which the post calls a common eBPF gotcha [10]. The published article also still carries its own production brief at the top, down to "Target: ~1000 words, B1-safe English, working-code-first" [16].
For the default to transfer, nothing legitimate on your host can open 50 files in one second, and that assumption has to be checked against whatever the machine actually runs. The repo ships the check: `sudo target/release/process-monitor --alert-threshold 50` alerts without killing anything [15].
What to watch
- Whether the alert condition moves from == to >= with a per-PID latch.
- Whether coverage grows past openat, and what that does to per-process open rate as a signal.
- Whether anyone publishes false-positive counts at the default 50-in-one-second threshold on ordinary developer workloads.