<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bartosz Osiej</title>
    <description>The latest articles on DEV Community by Bartosz Osiej (@bartoszosiej).</description>
    <link>https://dev.to/bartoszosiej</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4088500%2F7c989efc-dcb0-43df-9909-e04efd402bdf.png</url>
      <title>DEV Community: Bartosz Osiej</title>
      <link>https://dev.to/bartoszosiej</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bartoszosiej"/>
    <language>en</language>
    <item>
      <title>700 US county domains, one DNS record apart from safe</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Fri, 02 Oct 2026 11:09:03 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/700-us-county-domains-one-dns-record-apart-from-safe-2ai2</link>
      <guid>https://dev.to/bartoszosiej/700-us-county-domains-one-dns-record-apart-from-safe-2ai2</guid>
      <description>&lt;p&gt;I audited DNS for &lt;strong&gt;700 US county and K-12 government domains&lt;/strong&gt; this month. Passive lookups only — MX, TXT, CAA. No scanning, no probing, no service enumeration. Every record parsed per RR record and confirmed against at least two independent resolvers.&lt;/p&gt;

&lt;p&gt;The numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;286 of 700 (41%)&lt;/strong&gt; publish &lt;strong&gt;no DMARC record at all&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;only &lt;strong&gt;97 (14%)&lt;/strong&gt; are at &lt;code&gt;p=reject&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;156&lt;/strong&gt; are at &lt;code&gt;p=none&lt;/code&gt; — detectable, still delivered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;227&lt;/strong&gt; have SPF ending in &lt;code&gt;~all&lt;/code&gt; — soft-fail, spoofed mail lands anyway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;56&lt;/strong&gt; out of 700 are actually enforcing (SPF &lt;code&gt;-all&lt;/code&gt; + DMARC &lt;code&gt;p=reject&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  p=none is not "we have DMARC"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;p=none&lt;/code&gt; asks receiving servers to watch and report. It does not ask them to refuse anything. Spoofed mail claiming your domain still reaches the inbox — it's just marked, and users are trained to click through the mark. &lt;code&gt;p=reject&lt;/code&gt; is what turns &lt;em&gt;detect&lt;/em&gt; into &lt;em&gt;do not deliver&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order that works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_dmarc.example.gov. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.gov"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start at &lt;code&gt;p=quarantine&lt;/code&gt; with a &lt;code&gt;rua=&lt;/code&gt; address. Read the aggregate reports until the only senders listed are ones you recognise. Then move to &lt;code&gt;p=reject&lt;/code&gt;. &lt;strong&gt;Only then&lt;/strong&gt; tighten SPF from &lt;code&gt;~all&lt;/code&gt; to &lt;code&gt;-all&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Deploying &lt;code&gt;-all&lt;/code&gt; before the reports are clean is the single most common way a DMARC rollout gets rolled back — one legitimate sender gets dropped, mail stops, and the record gets deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit was wrong the first time
&lt;/h2&gt;

&lt;p&gt;Worth publishing, because it's the part most write-ups skip.&lt;/p&gt;

&lt;p&gt;The first pass concatenated each TXT RR set into one string and tested for a &lt;code&gt;v=spf1&lt;/code&gt; prefix. Any leading verification record — &lt;code&gt;MS=&lt;/code&gt;, an Apple domain-verification TXT — broke the match, and the domain came back as "no SPF". A second pass used a 3-second resolver timeout under 48-way concurrency and recorded those timeouts as "record absent".&lt;/p&gt;

&lt;p&gt;Between them, &lt;strong&gt;22 of the first 82 contact emails carried a finding that wasn't true&lt;/strong&gt;. All 22 were corrected by email the same day.&lt;/p&gt;

&lt;p&gt;The corrected pass: parse per record, confirm with two or more resolvers, and if the answer isn't confirmed, publish nothing rather than guess. A number that gets forwarded to an auditor should survive &lt;code&gt;dig&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full report
&lt;/h2&gt;

&lt;p&gt;Aggregates, methodology, the list of domains with no DMARC record, and a mailto that hands you your own domain's records as plain text:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://hartwell-labs.pl/report/" rel="noopener noreferrer"&gt;https://hartwell-labs.pl/report/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run one of these domains: mail me and you get your exact SPF, DMARC, CAA and MX as they resolve today. Free, no call, no pitch — enough to hand to whoever holds your DNS.&lt;/p&gt;




&lt;p&gt;Building detection &lt;em&gt;and response&lt;/em&gt; for Linux in the meantime — eBPF, no kernel modules, one static binary. If any of your systems run Linux and the mail-gateway view isn't the whole picture: &lt;a href="https://hartwell-labs.pl/talus.html" rel="noopener noreferrer"&gt;https://hartwell-labs.pl/talus.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>dns</category>
      <category>emailsecurity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Killing ransomware from inside the kernel: writing the response engine for an eBPF monitor in Rust</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:14:23 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/killing-ransomware-from-inside-the-kernel-writing-the-response-engine-for-an-ebpf-monitor-in-rust-5gfi</link>
      <guid>https://dev.to/bartoszosiej/killing-ransomware-from-inside-the-kernel-writing-the-response-engine-for-an-ebpf-monitor-in-rust-5gfi</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/bartoszosiej/detecting-ransomware-with-ebpf-in-rust-4779"&gt;part 1&lt;/a&gt; I showed how an eBPF monitor catches ransomware behavior: hook &lt;code&gt;execve&lt;/code&gt;/&lt;code&gt;openat&lt;/code&gt;/&lt;code&gt;unlink&lt;/code&gt;/&lt;code&gt;mkdir&lt;/code&gt;, stream events to userspace, score a 1-second sliding window per PID.&lt;/p&gt;

&lt;p&gt;This is the other half: what happens &lt;strong&gt;after&lt;/strong&gt; the verdict fires. Not another alert. Not a dashboard row. The process dies, in the same second, by design.&lt;/p&gt;

&lt;p&gt;Three parts: why a plain &lt;code&gt;SIGKILL&lt;/code&gt; from userspace is enough (no LSM, no kernel module), how I handle the race conditions, and how the agent sandboxes &lt;strong&gt;itself&lt;/strong&gt; so a bug can't turn your EDR into the attack.&lt;/p&gt;

&lt;p&gt;All code below is from &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;Talus&lt;/a&gt;, MIT, written in Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision point: a sliding window, not a signature
&lt;/h2&gt;

&lt;p&gt;First, the verdict itself. Ransomware has one loud fingerprint: it opens files in bulk. So the monitor keeps a per-PID deque of timestamps and scores it every event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.windows&lt;/span&gt;&lt;span class="nf"&gt;.entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.or_default&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WINDOW_SECS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.front&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.is_some_and&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.pop_front&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// evict events older than the window&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.threshold&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="py"&gt;.window_opens&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.threshold&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="py"&gt;.alerts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// verdict: this PID is mass-opening files RIGHT NOW&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WINDOW_SECS&lt;/code&gt; is 1. If a process crosses the open-rate threshold inside that second, it gets a verdict. The same pattern the big EDR vendors describe in marketing blogs - except here you can read the whole engine in one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SIGKILL from userspace is enough
&lt;/h2&gt;

&lt;p&gt;The obvious objection: "if your detection runs in the kernel, why not kill in the kernel?" You can do that with an LSM or a &lt;code&gt;bpf_send_signal()&lt;/code&gt; helper. I deliberately don't.&lt;/p&gt;

&lt;p&gt;Reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kill(2)&lt;/code&gt; is one syscall.&lt;/strong&gt; The event already traveled kernel → userspace through a per-CPU perf buffer with zero-copy handoff. The round-trip adds microseconds, not seconds. The ransomware's own encryption loop is orders of magnitude slower than that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy lives in userspace.&lt;/strong&gt; Thresholds, allowlists, the neural sidecar (MeMLP), audit logging - all of it is ordinary, debuggable Rust. Kernel code is where correctness goes to die and &lt;code&gt;panic&lt;/code&gt; is not an option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The kernel side stays dumb.&lt;/strong&gt; A dumb kernel program is a safe kernel program. All the eBPF side does is watch and report.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the response path is exactly what you'd write by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Send SIGKILL to a process. Returns true on success.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;kill_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SIGKILL cannot be caught, so the target process will terminate.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it's wired straight into the scoring loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.auto_kill&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kill_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ResponseAction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.ts&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.comm&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SIGKILL sent to PID {} ({})"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.comm&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Listings abridged - error paths and logging trimmed.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The race conditions nobody talks about
&lt;/h2&gt;

&lt;p&gt;Auto-kill means your monitor is now allowed to end processes. Two races matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Race 1: kill on verdict, not on first event.&lt;/strong&gt; A naive design kills the process the moment it sees one suspicious open. That's how you eat a false positive and murder your database. The verdict only fires when the &lt;em&gt;rate&lt;/em&gt; crosses the threshold inside the window - so a process that legitimately touches many files (a build, &lt;code&gt;updatedb&lt;/code&gt;, a backup job) either stays under the threshold or gets killed because it genuinely looks like ransomware, which is a tuning conversation, not a coin flip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Race 2: PID reuse.&lt;/strong&gt; The PID comes from the kernel as &lt;code&gt;u32&lt;/code&gt; and goes back into &lt;code&gt;libc::kill&lt;/code&gt; as &lt;code&gt;i32&lt;/code&gt;. Between the event and the kill, that PID could theoretically die and be reused. The window is tiny (the verdict is computed on the same event that triggered it, microseconds later), and SIGKILL to a recycled PID is bad but bounded. A full fix would verify &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/comm&lt;/code&gt; matches the reported process name before pulling the trigger - it's on the roadmap, and I'd rather ship the honest version than claim a perfect one.&lt;/p&gt;

&lt;p&gt;What you do &lt;strong&gt;not&lt;/strong&gt; get: catching file #1. The first opens of a real ransomware run will land. The design goal is narrower and honest: the attacker gets seconds, not your whole disk, and definitely not your backup job that started at 2 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing auto-kill without wrecking your machine
&lt;/h2&gt;

&lt;p&gt;You cannot test a response engine by running real ransomware. You don't need to. Ransomware's fingerprint is "opens files in bulk", so the test is just that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fake ransomware: 500 file opens as fast as the shell can go&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 500&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; /tmp/v&lt;span class="nv"&gt;$i&lt;/span&gt;.enc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/v&lt;span class="nv"&gt;$i&lt;/span&gt;.enc &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the monitor in observe mode with a low threshold first, confirm the alert fires, then flip on &lt;code&gt;--auto-kill&lt;/code&gt; and run the same loop. The loop gets SIGKILLed mid-run - usually inside the first second, well before 500.&lt;/p&gt;

&lt;p&gt;To keep even that controlled, run the victim inside a throwaway scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-run &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'for i in $(seq 1 500); do touch /tmp/v$i.enc; cat /tmp/v$i.enc &amp;gt;/dev/null; done'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same syscall pattern, same verdict, and if anything goes wrong the blast radius is one scope you can &lt;code&gt;systemctl&lt;/code&gt; away. I run this against my own desktop daily - a week of normal usage in observe mode (browsers, builds, editors) produced zero false positives, which is the number that actually matters for an auto-killing agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent must be unable to become the attack
&lt;/h2&gt;

&lt;p&gt;Here's the part I care about most. Your response engine just got permission to kill arbitrary processes, loaded an eBPF program, and holds capabilities. If someone compromises the &lt;em&gt;agent&lt;/em&gt;, they don't need ransomware anymore - they have your EDR.&lt;/p&gt;

&lt;p&gt;So Talus strips itself down before it touches a single event. Step 1: drop every capability it doesn't strictly need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CAP_BPF&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// load/attach eBPF programs&lt;/span&gt;
&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CAP_PERFMON&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// perf buffers&lt;/span&gt;
&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CAP_NET_ADMIN&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// some eBPF program types&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;to_drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="nf"&gt;.difference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.copied&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;to_drop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// PR_CAPBSET_DROP = 24&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;prctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;, &lt;code&gt;CAP_DAC_OVERRIDE&lt;/code&gt;, everything else - gone, via the capability bounding set, so no child process can get them back either.&lt;/p&gt;

&lt;p&gt;Step 2: seccomp. The agent builds a syscall allowlist (it needs a surprisingly short one: perf setup, kill, logging, that's mostly it) and installs a BPF filter so anything outside the list returns &lt;code&gt;EPERM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Step 3: Landlock (kernel ≥ 5.13). The filesystem gets a read-only ruleset over exactly the paths eBPF needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;allowed_paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/sys/kernel/debug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_DIR&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/sys/fs/bpf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_DIR&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/proc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_DIR&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dev/null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dev/urandom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_FILE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tmp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="n"&gt;LANDLOCK_ACCESS_FS_READ_DIR&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A compromised agent on a Landlocked kernel can read BPF maps and process info. It cannot write your disks. That asymmetry - "can kill, can't touch data" - is the whole design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;From my desktop, sustained: &lt;strong&gt;~280k events/s at ~7.6% CPU&lt;/strong&gt;, per-CPU perf buffers, zero-copy handoff. Single static binary, ~2 MB, no runtime dependencies. Install path: &lt;code&gt;pip install talus-process-monitor &amp;amp;&amp;amp; talus-monitor install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo (MIT): &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;https://github.com/BartoszOsiej/talus-process-monitor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 1 (detection): &lt;a href="https://dev.to/bartoszosiej/detecting-ransomware-with-ebpf-in-rust-4779"&gt;Detecting ransomware with eBPF in Rust&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run Linux servers and you try it in observe mode, I genuinely want to hear what your open-rate distribution looks like - threshold defaults are the hardest part of this whole thing, and real fleets beat my desktop every time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(For teams that want the response layer managed - web dashboard, license management, support - that's the Enterprise edition. The detector and everything you just read is MIT.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>linux</category>
      <category>ebpf</category>
    </item>
    <item>
      <title>Externum launches today: one typed source Python, Bash or bytecode (with honest benchmarks)</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Mon, 21 Sep 2026 15:32:51 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/externum-launches-today-one-typed-source-python-bash-or-bytecode-with-honest-benchmarks-460n</link>
      <guid>https://dev.to/bartoszosiej/externum-launches-today-one-typed-source-python-bash-or-bytecode-with-honest-benchmarks-460n</guid>
      <description>&lt;p&gt;One statically typed source compiles to readable CPython, a standalone &lt;code&gt;set -euo pipefail&lt;/code&gt; bash script, or a source-less bytecode artifact. The compiler is written mostly in itself, bootstrapped from a 6.4 KB Python stub.&lt;/p&gt;

&lt;p&gt;Today Externum is launching (Show HN + DevHunt launch week). This post is the honest version of that story: what it does, what it's bad at, and the numbers I'd want to see before trusting any "multi-target compiler" pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three backends
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python target&lt;/strong&gt; — emits plain CPython source. Typed source in, debuggable Python out; you can review the emitted file in a normal PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash target&lt;/strong&gt; — emits a standalone &lt;code&gt;set -euo pipefail&lt;/code&gt; script. Functions become bash functions with &lt;code&gt;local&lt;/code&gt; params, recursion works via &lt;code&gt;$(...)&lt;/code&gt; capture. Where bash has no equivalent (lists/dicts/classes), the compiler warns loudly instead of silently emitting junk. The first version silently produced empty files — which is how I learned that "empty output, exit 0" is the worst failure mode a compiler can have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bytecode target&lt;/strong&gt; — a &lt;code&gt;.exbc&lt;/code&gt; artifact run by a small VM. For "distribute the tool, not the code".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pipeline operator &lt;code&gt;x |&amp;gt; f(a, b)&lt;/code&gt; desugars to &lt;code&gt;f(x, a, b)&lt;/code&gt; in every backend, including the VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest numbers
&lt;/h2&gt;

&lt;p&gt;My first benchmark claim was wrong: I measured the CLI's code-generation mode instead of execution and briefly believed the Python path was 4.5× faster than CPython. Real numbers after fixing the harness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transpiled-Python path: &lt;strong&gt;~1.6× slower&lt;/strong&gt; than hand-written CPython (the cost of the typed multi-target story)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~11× faster than bash&lt;/strong&gt; on the same arithmetic loop&lt;/li&gt;
&lt;li&gt;VM: &lt;strong&gt;~30× slower&lt;/strong&gt; — it exists for source-less distribution, not speed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a benchmark in a language launch looks too good, assume the harness is measuring the wrong thing until proven otherwise. Mine was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;~380 tests, differential harness in CI: every backend runs the same suite and the outputs must agree — that harness found 3 real VM bugs that unit tests missed&lt;/li&gt;
&lt;li&gt;self-hosted: the compiler is written in Externum, bootstrapped from a minimal Python stub&lt;/li&gt;
&lt;li&gt;MIT, self-hosted, browser playground, &lt;code&gt;pip install externum&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Launch week
&lt;/h2&gt;

&lt;p&gt;You can also &lt;strong&gt;vote for Externum on DevHunt this week&lt;/strong&gt; (launch week 22–29.09): &lt;a href="https://devhunt.org/tool/externum" rel="noopener noreferrer"&gt;https://devhunt.org/tool/externum&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is MIT and free forever. If you want the curated path: the &lt;strong&gt;Pro Pack&lt;/strong&gt; (70+ exercises with solutions + a 10-page production guide on choosing between targets) is &lt;strong&gt;$29 instead of $50 this week&lt;/strong&gt; with code &lt;code&gt;LAUNCHWEEK&lt;/code&gt; — link in the repo README.&lt;/p&gt;

&lt;p&gt;Ask me anything here or on the HN thread — especially about the parts you think are wrong.&lt;/p&gt;

</description>
      <category>python</category>
      <category>compilers</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Externum — a typed language that compiles to Python, Bash and bytecode (Show DEV)</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Mon, 21 Sep 2026 13:39:21 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/externum-a-typed-language-that-compiles-to-python-bash-and-bytecode-show-dev-8ei</link>
      <guid>https://dev.to/bartoszosiej/externum-a-typed-language-that-compiles-to-python-bash-and-bytecode-show-dev-8ei</guid>
      <description>&lt;p&gt;I've been building &lt;strong&gt;Externum&lt;/strong&gt; — a self-hosted, statically typed language whose backends are runtimes that already exist everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python target&lt;/strong&gt; — emits readable CPython source (code-review friendly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash target&lt;/strong&gt; — standalone &lt;code&gt;set -euo pipefail\&lt;/code&gt; scripts; unsupported constructs warn loudly instead of silently emitting junk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bytecode target&lt;/strong&gt; — .exbc artifacts run by a small VM, no source needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline operator &lt;code&gt;x |&amp;gt; f(a, b)\&lt;/code&gt; works on every backend. The compiler is self-hosted (bootstrapped from a 6.4 KB Python stub), every backend is tested against the others by a &lt;strong&gt;differential harness in CI&lt;/strong&gt;, and the benchmark suite found 3 real VM bugs that unit tests missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest numbers&lt;/strong&gt;: 1.6x slower than CPython, 11x faster than bash. ~380 tests. MIT. On PyPI (&lt;code&gt;pip install externum\&lt;/code&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/BartoszOsiej/externum" rel="noopener noreferrer"&gt;https://github.com/BartoszOsiej/externum&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Playground (runs 100% in browser): &lt;a href="https://hartwell-labs.pl/externum/play.html" rel="noopener noreferrer"&gt;https://hartwell-labs.pl/externum/play.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live on DevHunt this week: &lt;a href="https://devhunt.org/tool/externum" rel="noopener noreferrer"&gt;https://devhunt.org/tool/externum&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback welcome — especially on the bash backend's design tradeoffs (what to emit when the typed semantics can't map cleanly).&lt;/p&gt;

&lt;h1&gt;
  
  
  showdev #python #compilers #opensource
&lt;/h1&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>compilers</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I ship open-source security tools solo — here's the full stack, the pricing, and the (zero) revenue so far</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Mon, 21 Sep 2026 07:52:07 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/i-ship-open-source-security-tools-solo-heres-the-full-stack-the-pricing-and-the-zero-revenue-2fd4</link>
      <guid>https://dev.to/bartoszosiej/i-ship-open-source-security-tools-solo-heres-the-full-stack-the-pricing-and-the-zero-revenue-2fd4</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I'm a 19-year-old solo developer building security and devtools under the name &lt;strong&gt;Hartwell Labs&lt;/strong&gt;. Everything is MIT-licensed and free. Two weeks ago I wrapped paid products around two of them — and I'm documenting the whole experiment, including the part nobody likes to publish: &lt;strong&gt;revenue so far is exactly $0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is the full stack: what the tools do, what I sell, why the code stays free, and what I've learned about the difference between downloads and dollars.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free stack (what you can take today, no money involved)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;talus-process-monitor&lt;/a&gt;&lt;/strong&gt; — a ransomware detection &amp;amp; response agent for Linux. It hooks &lt;code&gt;execve&lt;/code&gt;/&lt;code&gt;openat&lt;/code&gt; tracepoints with eBPF (via &lt;a href="https://aya-rs.dev" rel="noopener noreferrer"&gt;aya&lt;/a&gt;), streams events through per-CPU perf buffers, counts &lt;code&gt;openat&lt;/code&gt; calls per PID in a 1-second sliding window, and SIGKILLs the process when it crosses the threshold. ~280k events/s at under 8% CPU, zero false positives in simulation, MIT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BartoszOsiej/externum" rel="noopener noreferrer"&gt;externum&lt;/a&gt;&lt;/strong&gt; — a self-hosted typed language that compiles to readable Python, standalone Bash (&lt;code&gt;set -euo pipefail&lt;/code&gt;, real functions, recursion), and its own &lt;code&gt;.exbc&lt;/code&gt; bytecode VM. ~380 tests, differential harness in CI that runs every case through all three backends, MIT, on PyPI (431 installs last month).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BartoszOsiej/pqbit" rel="noopener noreferrer"&gt;pqbit&lt;/a&gt;&lt;/strong&gt; — a post-quantum Bitcoin experiment: ML-DSA-44/SLH-DSA signatures from genesis, UTXO + PoW node in Rust, fair-launch constitution. Early, public, and open for design review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually sell
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://buy.polar.sh/polar_cl_E577BTilme4dnUFsfbG0aE4qo7QILaugmEjsA0oajKK" rel="noopener noreferrer"&gt;Talus Enterprise Pack — $50&lt;/a&gt;&lt;/strong&gt;: a deployment &amp;amp; operations guide for running the detector in production — tuning the threshold per workload, dealing with backup-tool false positives, systemd hardening, alert routing, and a support channel. The binary was always free; this is the path from "cool demo" to "running on my servers".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://buy.polar.sh/polar_cl_qR2GOfEhXRqlTlPICwyo8XVqKkXjv4mj15cq926Dh1F" rel="noopener noreferrer"&gt;Externum Pro Pack — $29&lt;/a&gt;&lt;/strong&gt;: a 10-page production guide for the language — choosing between the three targets, the ownership model, the real-world workflow I use, troubleshooting. Delivered by email within 24h (honest trade-off: Polar's file-delivery API has a checksum bug, so delivery is manual until they fix it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why charge for guides instead of the code
&lt;/h2&gt;

&lt;p&gt;Three reasons, in order of honesty:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The code benefits more from being free than from being paid.&lt;/strong&gt; MIT got talus into awesome-list PR queues, got externum 431 PyPI installs, and got strangers talking to me on Mastodon. A paywall on a 2-week-old repo would have bought none of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What people actually lack is not the binary — it's operational judgment.&lt;/strong&gt; The tuning thresholds, the false-positive triage, the "which target do I pick" decision. That's worth packaging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$29–50 is a tip with a deliverable attached.&lt;/strong&gt; I'm not pretending this is enterprise pricing. It's "if this saved you an hour, here's a way to say thanks that gets you something back".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The numbers so far (the part most posts skip)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Downloads: externum &lt;strong&gt;431/month&lt;/strong&gt; on PyPI — the only channel with real organic pull&lt;/li&gt;
&lt;li&gt;GitHub stars across 10+ repos: &lt;strong&gt;~1 total&lt;/strong&gt; (yes, really)&lt;/li&gt;
&lt;li&gt;Mastodon followers: 6. Bluesky: 6. Telegram channel: 2 people (hi, mom)&lt;/li&gt;
&lt;li&gt;Polar orders: &lt;strong&gt;0&lt;/strong&gt; across both products&lt;/li&gt;
&lt;li&gt;Cold outreach: ~110 emails to media/newsletters over 10 days, one article accepted (LinuxSecurity, publishing this week), one legend of the industry reacted with a single emoji — which honestly made my week&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: &lt;strong&gt;distribution is the bottleneck, not the product, and not the price.&lt;/strong&gt; The free code travels; the paid wrapper hasn't moved at all yet. I suspect the missing piece is trust surface — 0★ repos and a fresh brand asking for $50 is a hard sell no matter how good the README is. The experiment now is whether published writing (this post, the LinuxSecurity article, upcoming Show HN launches) converts better than cold email did.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Everything free: &lt;a href="https://github.com/BartoszOsiej" rel="noopener noreferrer"&gt;github.com/BartoszOsiej&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buy.polar.sh/polar_cl_E577BTilme4dnUFsfbG0aE4qo7QILaugmEjsA0oajKK" rel="noopener noreferrer"&gt;Talus Enterprise Pack ($50)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buy.polar.sh/polar_cl_qR2GOfEhXRqlTlPICwyo8XVqKkXjv4mj15cq926Dh1F" rel="noopener noreferrer"&gt;Externum Pro Pack ($29)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://polar.sh/checkout/polar_c_rGI15C7IzCs54qElT4NJ5aLj7o3p7Uxdj4P4T1OtUsU" rel="noopener noreferrer"&gt;Support Session — $150&lt;/a&gt;&lt;/strong&gt;: a 60-minute call about deploying talus in your environment — threshold tuning, false-positive triage, systemd hardening — plus a written config summary.&lt;/li&gt;
&lt;li&gt;Build log: &lt;a href="https://t.me/hartwell_info" rel="noopener noreferrer"&gt;t.me/hartwell_info&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next milestone I'll report on: first sale, or the first piece of evidence that this pricing model is wrong. Both are useful data.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎉 Launch Week Deal (Sep 22–29)
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Externum Pro Pack&lt;/strong&gt; (70+ exercises, full solutions, companion e-book) is &lt;strong&gt;$29 instead of $50&lt;/strong&gt; with code &lt;code&gt;LAUNCHWEEK&lt;/code&gt; — max 100 redemptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://polar.sh/checkout/polar_c_UwKijv41Odj1Cnrh56St9FICXc2NLmewJXSnF1meGNN?discount_code=LAUNCHWEEK" rel="noopener noreferrer"&gt;Get the Pro Pack with LAUNCHWEEK&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also: we're on &lt;a href="https://devhunt.org/tool/externum" rel="noopener noreferrer"&gt;DevHunt&lt;/a&gt; this week — if the article was useful, a vote helps other devs find the language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This week only (Sep 22–27):&lt;/strong&gt; code &lt;code&gt;WEEK30&lt;/code&gt; = 30% off the Support Session ($150 → $105) and the Enterprise license ($50 → $35). The checkouts above apply it on the page.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>opensource</category>
      <category>ebpf</category>
    </item>
    <item>
      <title>I'm Building a Post-Quantum Bitcoin in Rust — and a Test Caught a Real Consensus Bug Tonight</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Sun, 20 Sep 2026 20:38:42 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/im-building-a-post-quantum-bitcoin-in-rust-and-a-test-caught-a-real-consensus-bug-tonight-1182</link>
      <guid>https://dev.to/bartoszosiej/im-building-a-post-quantum-bitcoin-in-rust-and-a-test-caught-a-real-consensus-bug-tonight-1182</guid>
      <description>&lt;p&gt;Most ransomware tools stop at detection. Most Bitcoin forks stop at marketing. This post is about neither — it's about building a Bitcoin whose signatures survive Shor's algorithm, and what "fair launch" means when you actually mean it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pqbit is open source, phase 2 just shipped, and this is the honest state of it.&lt;/strong&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/BartoszOsiej/pqbit" rel="noopener noreferrer"&gt;https://github.com/BartoszOsiej/pqbit&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The thesis, in one paragraph
&lt;/h2&gt;

&lt;p&gt;~7M BTC sit in quantum-exposed addresses — ECDSA public keys revealed on-chain by old P2PK outputs and reused addresses. BIP-360 (P2MR) was merged into the Bitcoin BIPs repo in February 2026, so the ecosystem agrees the problem is real. But Bitcoin moves slowly by design, and a migration touching every wallet is a decade of debate. &lt;strong&gt;pqbit skips the debate: the chain is born post-quantum.&lt;/strong&gt; Not a faster Bitcoin, not a token sale — one thesis, executed cleanly.&lt;/p&gt;
&lt;h2&gt;
  
  
  What shipped today (phase 2)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pqbit-core&lt;/strong&gt; — UTXO transaction model with canonical sighash, signatures via &lt;a href="https://crates.io/crates/bitcoinpqc" rel="noopener noreferrer"&gt;bitcoinpqc&lt;/a&gt;: ML-DSA-44 (FIPS 204) as primary, SLH-DSA-SHA2-128s (FIPS 205) as the conservative alternative. No ECDSA fallback at the consensus layer — that's the entire point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pqbit-node&lt;/strong&gt; — testnet chain engine: blocks, SHA-256d PoW (leading-zero-bits difficulty), coinbase with a height commitment, UTXO set with ML-DSA spend authorization, and a CLI miner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9/9 tests&lt;/strong&gt; — including a double-spend test that caught a real bug during development.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The bug the test caught (the good part)
&lt;/h2&gt;

&lt;p&gt;The first coinbase design stored the block height in the signature field. Sounds fine — except the sighash preimage commits prevouts and outputs but &lt;strong&gt;not signatures&lt;/strong&gt;. Signatures are witnesses, not committed data. Result: every coinbase hashed to the same txid, block after block. The UTXO set happily re-created the same output every block, and a legitimate "spend" of it succeeded twice.&lt;/p&gt;

&lt;p&gt;If that survived to a real network, it would be a consensus failure — minted outputs reappearing like ghosts. The fix follows Bitcoin's own pattern: commit the height in the prevout reference (&lt;code&gt;prev_txid[0..8]&lt;/code&gt;, little-endian). Prevouts are committed, so the height now is too. One test, one real protocol bug, one clean fix — in an evening, not after mainnet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before: height in the signature field (NOT committed by sighash)&lt;/span&gt;
&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="nf"&gt;.to_le_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// after: height committed in the prevout (sighash commits prevouts)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.copy_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="nf"&gt;.to_le_bytes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the fair-launch constitution is written like a legal document
&lt;/h2&gt;

&lt;p&gt;Zero premine. Zero presale. Founder stash capped at &lt;strong&gt;100 coins&lt;/strong&gt;, single public address, published at genesis — and &lt;em&gt;never moved&lt;/em&gt;. Moving one coin ends the project's own narrative, and everyone can watch that address forever. That's not a marketing line; it's a reputational bond with an observable state.&lt;/p&gt;

&lt;p&gt;The kill criteria are equally public: fewer than 50 non-founder nodes six months after genesis means the coin experiment freezes and pqbit returns to pure research. Sunk cost kills more projects than competitors do — we pre-signed the exit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's deliberately undecided
&lt;/h2&gt;

&lt;p&gt;The PoW algorithm. CPU-friendly maximizes early distribution but botnets exist; "ASIC-proof" claims age badly. The honest position is memory-hard-leaning, with the decision made from testnet data, not vibes. Same for block interval (5 vs 10 minutes). GENESIS.md is a draft &lt;strong&gt;open for public review&lt;/strong&gt; — tear it apart before genesis, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;Phase 3 is p2p networking. Then an explorator, a whitepaper, and — only when the founder says go — a public genesis with a timestamped fair launch. Every step ships in the open.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/BartoszOsiej/pqbit" rel="noopener noreferrer"&gt;https://github.com/BartoszOsiej/pqbit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Landing: &lt;a href="https://bartoszosiej.github.io/pqbit/" rel="noopener noreferrer"&gt;https://bartoszosiej.github.io/pqbit/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GENESIS draft: &lt;a href="https://github.com/BartoszOsiej/pqbit/blob/master/GENESIS.md" rel="noopener noreferrer"&gt;https://github.com/BartoszOsiej/pqbit/blob/master/GENESIS.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you know post-quantum crypto or consensus internals and want to find holes in the design before it grows — the issues tab is open. That's exactly the review GENESIS.md is asking for.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>bitcoin</category>
      <category>security</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Your Antivirus Only Watches. Mine Kills: Building a Detect-and-Respond Agent in Rust + eBPF</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Sun, 20 Sep 2026 18:50:01 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/your-antivirus-only-watches-mine-kills-building-a-detect-and-respond-agent-in-rust-ebpf-20do</link>
      <guid>https://dev.to/bartoszosiej/your-antivirus-only-watches-mine-kills-building-a-detect-and-respond-agent-in-rust-ebpf-20do</guid>
      <description>&lt;p&gt;Most ransomware "solutions" stop at detection: an alert, a dashboard row, an email you'll read tomorrow. By then the attacker has already encrypted the shares.&lt;/p&gt;

&lt;p&gt;I wanted the other thing: &lt;strong&gt;the process dies the moment the verdict fires.&lt;/strong&gt; This post is about how Talus does that in Rust, with eBPF doing the watching and a response layer doing the killing — and what it took to keep the whole pipeline at &lt;strong&gt;~280,000 events/second on ~7.6% CPU&lt;/strong&gt; on a live desktop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Talus is MIT-licensed and open source: &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;github.com/BartoszOsiej/talus-process-monitor&lt;/a&gt; — demo GIF included, it really does SIGKILL the ransomware simulator in the terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Ransomware is boring to detect in theory: it opens &lt;em&gt;a lot&lt;/em&gt; of files, very fast. The hard parts are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Seeing the opens without slowing them down.&lt;/strong&gt; Every file open goes through the kernel — if your monitor adds latency there, you're taxing every process on the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not killing &lt;code&gt;rsync&lt;/code&gt; or your build system.&lt;/strong&gt; A naive "too many opens = ransomware" rule murders backup jobs and &lt;code&gt;cargo build&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acting faster than the encryptor.&lt;/strong&gt; Detection that arrives after the first directory is wiped is just forensics.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Architecture: kernel watches, userspace thinks, response acts
&lt;/h2&gt;

&lt;p&gt;Talus is a pipeline, and every stage exists to keep stage 1 cheap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KERNEL SPACE (eBPF tracepoints, Linux 5.8+)
   │  hooks syscalls at the source — no LD_PRELOAD, no FUSE
   ▼
per-CPU perf buffers  ──►  zero-copy ring  ──►  userspace detection engine
                                                     │  per-PID sliding window
                                                     ▼
                                          TUI / JSON / WebSocket / Prometheus
                                                     │  verdict
                                                     ▼
                                               response layer (SIGKILL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this split works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;eBPF tracepoints&lt;/strong&gt; mean zero drivers and zero kernel patches. The probe is verified by the kernel before it runs — it cannot crash the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-CPU perf buffers&lt;/strong&gt; mean no cross-CPU lock contention. Each core streams its own events; nothing serializes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-copy handoff&lt;/strong&gt; to userspace means the detection engine reads events without copying them again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Detection: a 1-second sliding window per PID
&lt;/h2&gt;

&lt;p&gt;The heuristic is deliberately simple: per-PID file-open rate inside a 1-second sliding window. Cross the configurable threshold and the verdict fires.&lt;/p&gt;

&lt;p&gt;Sliding (not fixed) windows matter here. With fixed buckets, a bursty process that opens 200 files in 50ms every second can hide between samples. A sliding window sees the burst &lt;em&gt;inside&lt;/em&gt; the second.&lt;/p&gt;

&lt;p&gt;To cut false positives, the file ranking layer scores most-opened files with &lt;strong&gt;Shannon entropy&lt;/strong&gt; — encrypted or randomized filenames stand out from normal workloads immediately.&lt;/p&gt;

&lt;p&gt;And the threshold is yours to set: watch-mode for auditing, low-threshold + auto-kill for EDR mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Response: yes, it kills
&lt;/h2&gt;

&lt;p&gt;The response layer sends &lt;code&gt;SIGKILL&lt;/code&gt; to the offending PID the moment the verdict fires. Not "quarantine eventually", not "notify admin". Kill, then log, then alert.&lt;/p&gt;

&lt;p&gt;Is SIGKILL aggressive? Yes. That's the point — ransomware doesn't pause for your opinion. If you want to watch first, run monitor-only mode. The demo in the repo shows exactly this: terminal 1 runs Talus with a low threshold and auto-kill, terminal 2 runs a mass-file-encryption simulator, and Talus ends it mid-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance work that actually mattered
&lt;/h2&gt;

&lt;p&gt;Getting to 280k events/s at ~7.6% CPU was mostly &lt;em&gt;subtraction&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't copy what you can reference.&lt;/strong&gt; The zero-copy ring between kernel and userspace removed an entire memcpy stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't aggregate what you can count.&lt;/strong&gt; The eBPF side does minimal work per event — increment a map entry, emit when needed. The sliding-window math lives in userspace, where a bug costs a panic, not a kernel oops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let each CPU mind its own business.&lt;/strong&gt; Per-CPU buffers beat one big shared buffer by not fighting over locks at the busiest moment (an attack is precisely when events spike).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full data-flow diagram and pipeline stages are documented in &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor/blob/master/ARCHITECTURE.md" rel="noopener noreferrer"&gt;ARCHITECTURE.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# monitor-only&lt;/span&gt;
talus monitor

&lt;span class="c"&gt;# full EDR mode: detect + auto-kill&lt;/span&gt;
talus monitor &lt;span class="nt"&gt;--kill&lt;/span&gt; &lt;span class="nt"&gt;--threshold&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second terminal, if you want to see a verdict fire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ransomware-like mass file-open burst&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 10000&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;x &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/victim_&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(On a throwaway VM or container, like any security demo.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Roadmap items I'm working through: per-process allowlists tuned for build/backup workloads, and richer response actions beyond SIGKILL. The enterprise maturity report (level 4/20 path, priority patches, license) is on the landing page for teams that need a paper trail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing + docs:&lt;/strong&gt; &lt;a href="https://hartwell-labs.pl/talus-process-monitor/" rel="noopener noreferrer"&gt;https://hartwell-labs.pl/talus-process-monitor/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full docs hub:&lt;/strong&gt; &lt;a href="https://bartoszosiej.github.io/Docs/" rel="noopener noreferrer"&gt;https://bartoszosiej.github.io/Docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;https://github.com/BartoszOsiej/talus-process-monitor&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something on top of it — or it saves you from a real incident — I want to hear that story in the comments.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>linux</category>
      <category>ebpf</category>
    </item>
    <item>
      <title>Externum: a typed language that transpiles to Python and Bash — and its own benchmark caught two real bugs</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Sat, 19 Sep 2026 16:35:22 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/externum-a-typed-language-that-transpiles-to-python-and-bash-and-its-own-benchmark-caught-two-2n25</link>
      <guid>https://dev.to/bartoszosiej/externum-a-typed-language-that-transpiles-to-python-and-bash-and-its-own-benchmark-caught-two-2n25</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Externum is a statically-typed language I've been building that compiles one program three ways: to &lt;strong&gt;plain readable Python&lt;/strong&gt;, to &lt;strong&gt;standalone Bash&lt;/strong&gt; (a real &lt;code&gt;set -euo pipefail&lt;/code&gt; script, no interpreter dependency), and to a &lt;strong&gt;&lt;code&gt;.exbc&lt;/code&gt; bytecode artifact&lt;/strong&gt; that runs on a small VM without shipping the source. The compiler is written mostly in Externum itself. This post is about the design trade-offs of multi-target transpilation, what the benchmark actually proved (and what my first, &lt;em&gt;wrong&lt;/em&gt; benchmark claimed), and the two real VM bugs the benchmarking process exposed — root-caused and fixed within a day.&lt;/p&gt;

&lt;p&gt;Try it in the browser first: &lt;a href="https://hartwell-labs.pl/externum/" rel="noopener noreferrer"&gt;playground (no install)&lt;/a&gt;. Repo: &lt;a href="https://github.com/BartoszOsiej/externum" rel="noopener noreferrer"&gt;github.com/BartoszOsiej/externum&lt;/a&gt; · &lt;code&gt;pip install externum&lt;/code&gt; · MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why transpile to &lt;em&gt;other&lt;/em&gt; languages at all
&lt;/h2&gt;

&lt;p&gt;Writing a new language is easy; the hard question is: &lt;strong&gt;what does your user run when you're not there?&lt;/strong&gt; Every compiled language answers with "a binary for each OS × architecture you support." That's a support burden a solo maintainer cannot carry — so Externum never answers it. Instead it leans on runtimes that already exist everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; is on every server, laptop, and CI image on earth. Transpiling to plain CPython source means the user ships text files, can read and debug the output, and inherits the entire ecosystem for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash&lt;/strong&gt; is &lt;em&gt;in&lt;/em&gt; every Unix. If your program's backend compiles to a portable script, "installation" means &lt;code&gt;scp&lt;/code&gt; + &lt;code&gt;chmod +x&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bytecode VM&lt;/strong&gt; answers the third question: what if the user wants to &lt;em&gt;distribute&lt;/em&gt; their tool without shipping source? Compile once to &lt;code&gt;.exbc&lt;/code&gt;, hand the artifact to the VM — the source never leaves your machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a new insight (Lua did the "embed everywhere" thing decades ago), but making all three targets &lt;strong&gt;first-class, same-source outputs&lt;/strong&gt; is the part I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the same program looks like on every target
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def fact(n: Int) -&amp;gt; Int:
    if n &amp;lt;= 1:
        return 1
    return n * fact(n - 1)

print("6! =", fact(6))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;externum demo.ext --target python&lt;/code&gt; produces ordinary Python you could ship as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6! =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--target bash&lt;/code&gt; produces a real script — functions become bash functions with &lt;code&gt;local&lt;/code&gt; params, recursion works, defaults become &lt;code&gt;${2:-default}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
fact&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
         &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; n &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;fact &lt;span class="k"&gt;$((&lt;/span&gt; n &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"6! = &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fact 6&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;--target bytecode&lt;/code&gt; writes a &lt;code&gt;.exbc&lt;/code&gt; artifact (magic &lt;code&gt;EXBC&lt;/code&gt; + version + payload) that &lt;code&gt;externum run demo.exbc&lt;/code&gt; executes without ever seeing the source. Constructs a target can't express (lists/dicts/classes in bash) produce &lt;strong&gt;warnings, never silent empty output&lt;/strong&gt; — the old bash target once "compiled" by emitting nothing, and an empty file with exit code 0 is the kind of lie that costs someone a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark: including my own false start
&lt;/h2&gt;

&lt;p&gt;I benchmarked the arithmetic loop (2M iterations, identical output verified across implementations) with hyperfine. My &lt;strong&gt;first measurement was wrong&lt;/strong&gt; — I'd invoked the CLI without a subcommand, which &lt;em&gt;transpiles and prints the generated code&lt;/em&gt; instead of running it. That produced a flattering 4.5×-faster-than-CPython number, which I very briefly believed before noticing the output wasn't the program's output. HN would have found that in minutes; putting a wrong number in a README is how a Show HN dies.&lt;/p&gt;

&lt;p&gt;The honest numbers (i7-4610M, hyperfine, &lt;a href="https://github.com/BartoszOsiej/externum/tree/main/benchmarks" rel="noopener noreferrer"&gt;methodology + repro&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;vs Externum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;externum run&lt;/code&gt; (full pipeline)&lt;/td&gt;
&lt;td&gt;551 ms ± 42&lt;/td&gt;
&lt;td&gt;1.00×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiled artifact (compile once, run many)&lt;/td&gt;
&lt;td&gt;458 ms&lt;/td&gt;
&lt;td&gt;1.20× faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPython (idiomatic &lt;code&gt;for range&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;340 ms ± 10&lt;/td&gt;
&lt;td&gt;1.62× faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bash (&lt;code&gt;$(( ))&lt;/code&gt;, no forks)&lt;/td&gt;
&lt;td&gt;6.16 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.2× slower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So: &lt;strong&gt;the Python target costs ~1.6× over hand-written CPython&lt;/strong&gt; — the price of a typed source and a portable multi-target story, not magic. Against Bash, same-source wins by an order of magnitude. The VM path is ~30× slower than the transpile paths and that's fine: it exists for run-without-source distribution, not for CPU-bound loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two bugs the benchmark caught
&lt;/h2&gt;

&lt;p&gt;Building the benchmark was the best decision of the release, because it immediately fell into two holes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Module-level &lt;code&gt;x += 1&lt;/code&gt; never stored the value.&lt;/strong&gt; The bytecode compiler's augmented-assignment path loaded the variable with &lt;code&gt;LOAD_GLOBAL&lt;/code&gt; but stored with &lt;code&gt;STORE_VAR&lt;/code&gt; — writing into a frame nobody ever read. The loop variable never incremented; the VM spun in a &lt;em&gt;silent infinite loop&lt;/em&gt;. Worse, the operator map keyed on &lt;code&gt;"+"&lt;/code&gt; while the parser delivers &lt;code&gt;"+="&lt;/code&gt;, so every augmented op except &lt;code&gt;+=&lt;/code&gt; would have silently executed as addition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parenthesized right-hand sides became variable names.&lt;/strong&gt; The compiler couldn't strip outer parens, so &lt;code&gt;(a + b) % m&lt;/code&gt; compiled to an attempt to load a global literally named &lt;code&gt;"(a + b) % m"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither was caught by CI, because the existing VM tests didn't use those constructs — a good reminder that &lt;strong&gt;coverage measures what you wrote, not what users will&lt;/strong&gt;. Both are now root-caused, fixed, regression-tested (v4.2.0), and the VM runs the benchmark identically to CPython. I filed them as issues before fixing — &lt;a href="https://github.com/BartoszOsiej/externum/issues/21" rel="noopener noreferrer"&gt;#21&lt;/a&gt; and &lt;a href="https://github.com/BartoszOsiej/externum/issues/22" rel="noopener noreferrer"&gt;#22&lt;/a&gt; — because "benchmark caught real bugs, here's the postmortem" is worth more than the appearance of never having bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benchmark before bragging.&lt;/strong&gt; The false 4.5× number almost shipped because I measured the wrong command. Every number in the README is now reproducible from the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty output is a bug, not a feature gap.&lt;/strong&gt; The old bash target's silence was worse than a crash. Anything a target can't express must warn loudly at compile time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting is a forcing function.&lt;/strong&gt; Writing the compiler in the language it compiles means every language wart is personally painful, immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The roadmap from here: a JS target (the natural next runtime that's already everywhere), more stdlib, and keeping the honest-limits section of the README as current as the features list.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/BartoszOsiej/externum" rel="noopener noreferrer"&gt;github.com/BartoszOsiej/externum&lt;/a&gt; · Playground: &lt;a href="https://hartwell-labs.pl/externum/" rel="noopener noreferrer"&gt;bartoszosiej.github.io/externum&lt;/a&gt; · &lt;code&gt;pip install externum&lt;/code&gt; · MIT, ~380 tests&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>compilers</category>
      <category>python</category>
      <category>bash</category>
      <category>programminglanguages</category>
    </item>
    <item>
      <title>Detecting ransomware with eBPF in Rust</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:14:51 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/detecting-ransomware-with-ebpf-in-rust-4779</link>
      <guid>https://dev.to/bartoszosiej/detecting-ransomware-with-ebpf-in-rust-4779</guid>
      <description>&lt;h1&gt;
  
  
  Detecting ransomware with eBPF in Rust
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Title: Detecting ransomware with eBPF in Rust&lt;br&gt;
Target: ~1000 words, B1-safe English, working-code-first&lt;br&gt;
Build: demo + article for DEV.to / Draft.dev share&lt;br&gt;
Source repo: github.com/BartoszOsiej/talus-process-monitor (MIT)&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The idea (hook)
&lt;/h2&gt;

&lt;p&gt;Ransomware works in a simple way: it opens your files, encrypts them, and writes them back. Fast. One process can open hundreds or thousands of files within seconds.&lt;/p&gt;

&lt;p&gt;You do not need a huge model to spot it. You need to watch one number: &lt;strong&gt;how many files a single process opens per second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article shows a small eBPF program in Rust that does exactly that. It hooks two syscalls (&lt;code&gt;execve&lt;/code&gt; and &lt;code&gt;openat&lt;/code&gt;), counts file-open rate per process, and — when the rate is too high — kills the process.&lt;/p&gt;

&lt;p&gt;All code comes from a real open-source project: &lt;code&gt;talus-process-monitor&lt;/code&gt; (MIT, github.com/BartoszOsiej/talus-process-monitor).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What is eBPF in one sentence
&lt;/h2&gt;

&lt;p&gt;eBPF lets you attach small programs to kernel events (syscalls, network packets, timers) without writing or loading a kernel module. The programs run in a sandboxed VM in the kernel. You get kernel-level visibility with low overhead.&lt;/p&gt;

&lt;p&gt;In Rust, the library is called &lt;strong&gt;aya&lt;/strong&gt;. &lt;code&gt;aya-ebpf&lt;/code&gt; is the runtime for the kernel side. &lt;code&gt;aya&lt;/code&gt; manages loading from userspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Kernel side: a tiny eBPF program
&lt;/h2&gt;

&lt;p&gt;We define one "event" struct. Both kernel and userspace must agree on the layout, so it uses &lt;code&gt;#[repr(C)]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[repr(C)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ProcessEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;     &lt;span class="c1"&gt;// process name&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// opened file path&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[map]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;EVENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PerfEventArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PerfEventArray&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PerfEventArray&lt;/code&gt; is the channel between kernel and userspace. Each CPU has its own buffer, so concurrent processes do not block each other.&lt;/p&gt;

&lt;p&gt;Next, two tracepoints. These run when a process starts (&lt;code&gt;execve&lt;/code&gt;) or opens a file (&lt;code&gt;openat&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tracepoint(name&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sys_enter_execve"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;category&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"syscalls"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;sys_enter_execve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TracePointContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;emit_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EVENT_EXECVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[tracepoint(name&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sys_enter_openat"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;category&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"syscalls"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;sys_enter_openat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TracePointContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;emit_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EVENT_OPENAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main work happens in &lt;code&gt;emit_event&lt;/code&gt;. First we read PID and UID of the current process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bpf_get_current_uid_gid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we read the filename argument from the tracepoint args. The tracepoint layout is &lt;code&gt;(common fields, filename, flags, mode)&lt;/code&gt;, so the filename pointer sits at a fixed offset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ptr_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;core&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;size_of&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;c_char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;filename_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;filename_arg&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.read_at&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;c_char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename_offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="nf"&gt;.is_null&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// bpf_probe_read_user safely copies userspace memory&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;bpf_probe_read_user_str_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="py"&gt;.cast&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* copy into event.filename */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we push the event into the buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;EVENTS&lt;/span&gt;&lt;span class="nf"&gt;.output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One note about eBPF: you cannot use normal Rust std function calls. No &lt;code&gt;memcpy&lt;/code&gt;, no &lt;code&gt;memset&lt;/code&gt;, no &lt;code&gt;format!&lt;/code&gt;. The code uses hand-written byte loops (&lt;code&gt;raw_copy&lt;/code&gt;) to avoid LLVM builtins. This is a common eBPF gotcha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;raw_copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="nf"&gt;.add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="nf"&gt;.add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Userspace: the detection engine
&lt;/h2&gt;

&lt;p&gt;On the userspace side we read events off the perf buffer and keep a &lt;strong&gt;1-second sliding window&lt;/strong&gt; of open events per PID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.windows&lt;/span&gt;&lt;span class="nf"&gt;.entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.or_default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// VecDeque&amp;lt;Instant&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WINDOW_SECS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1s&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.front&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.is_some_and&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.pop_front&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;opens_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;opens_now&lt;/code&gt; reaches the threshold (default 50 opens in 1 second), we fire an alert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.threshold&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="py"&gt;.window_opens&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.threshold&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="py"&gt;.alerts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Alert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* pid, comm, opens */&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.auto_kill&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kill_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="py"&gt;.pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ResponseAction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kill_process&lt;/code&gt; is a plain &lt;code&gt;kill(2)&lt;/code&gt; with SIGKILL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;kill_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;libc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SIGKILL cannot be caught. The offending process stops immediately. That is the full "detect and respond" loop: &lt;strong&gt;hook → count → alert → kill&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run it
&lt;/h2&gt;

&lt;p&gt;Requirements: Linux kernel 5.8+, root (or &lt;code&gt;CAP_BPF&lt;/code&gt; + &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;), Rust nightly and &lt;code&gt;clang&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build&lt;/span&gt;
./build.sh

&lt;span class="c"&gt;# Monitor only (no killing)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;target/release/process-monitor &lt;span class="nt"&gt;--alert-threshold&lt;/span&gt; 50

&lt;span class="c"&gt;# EDR mode: detect and kill&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;target/release/process-monitor &lt;span class="nt"&gt;--alert-threshold&lt;/span&gt; 50 &lt;span class="nt"&gt;--auto-kill&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it with a loop that opens many files fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;process-monitor &lt;span class="nt"&gt;--alert-threshold&lt;/span&gt; 3 &lt;span class="nt"&gt;--auto-kill&lt;/span&gt;
&lt;span class="c"&gt;# in another terminal:&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 100&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; /tmp/f&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process that runs the loop violates the threshold and gets SIGKILL.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Limits and next steps
&lt;/h2&gt;

&lt;p&gt;This heuristic has false positives (a backup tool also opens many files fast) — that is why a configurable threshold and a lower default matter. Real-world improvements from the same repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shannon entropy scoring&lt;/strong&gt; on filenames: encrypted/randomized names have high entropy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network egress tracing&lt;/strong&gt; (&lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;sendto&lt;/code&gt;): catch data exfiltration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File-extension tracking&lt;/strong&gt;: mass &lt;code&gt;.enc&lt;/code&gt; / &lt;code&gt;.locked&lt;/code&gt; writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project also ships a small online-trained MLP model (no external deps) that turns raw event features into score: &lt;code&gt;benign / suspicious / ransomware&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;eBPF gives you kernel-level tracing without kernel modules.&lt;/li&gt;
&lt;li&gt;Rust + aya makes the whole pipeline safe to build and maintain.&lt;/li&gt;
&lt;li&gt;A 1-second sliding window on &lt;code&gt;openat&lt;/code&gt; rate is a cheap, real ransomware signal.&lt;/li&gt;
&lt;li&gt;Responding with SIGKILL turns a monitor into an EDR-style agent — 30 lines of Rust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full source code: &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;github.com/BartoszOsiej/talus-process-monitor&lt;/a&gt;&lt;br&gt;
Follow me on DEV.to for the next part: network egress detection with eBPF.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it on your machine
&lt;/h2&gt;

&lt;p&gt;Talus is open source (MIT) — community edition includes the TUI, real-time eBPF tracing, and the ransomware heuristic.&lt;/p&gt;

&lt;p&gt;If you want the full version — &lt;strong&gt;web dashboard, auto-kill response, SIEM exports&lt;/strong&gt; (Kafka / ClickHouse / Memgraph) — Talus Enterprise is &lt;strong&gt;$50 one-time, perpetual&lt;/strong&gt;: &lt;a href="https://buy.polar.sh/polar_cl_28JLJCk73sd8ugcsFrruInxtZo3uoU4ZgOJ3o3NjTJF" rel="noopener noreferrer"&gt;Get Talus Enterprise&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Activation: &lt;code&gt;talus license activate &amp;lt;KEY&amp;gt;&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>rust</category>
      <category>security</category>
    </item>
    <item>
      <title>bip-calendar</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:14:15 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/bip-calendar-1oi6</link>
      <guid>https://dev.to/bartoszosiej/bip-calendar-1oi6</guid>
      <description>&lt;h1&gt;
  
  
  Build-in-public: 30-day prompt calendar
&lt;/h1&gt;

&lt;p&gt;Source for &lt;code&gt;bip-draft.yml&lt;/code&gt; — the workflow picks &lt;code&gt;Day N = (day-of-year mod 30)&lt;/code&gt;.&lt;br&gt;
One section per day; each draft gets auto-enriched with yesterday's commits.&lt;br&gt;
&lt;strong&gt;Voice rule:&lt;/strong&gt; drafts are scaffolding — rewrite at least one sentence in your own words before posting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 1 — Show the stack
&lt;/h2&gt;

&lt;p&gt;Screenshot/diagram of the project constellation (linux-aegis, talus, externum, fortis, quantum-shield, NV2). One line each: what problem it kills. End: "which one should I deep-dive first?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 2 — Number drop
&lt;/h2&gt;

&lt;p&gt;Post one real metric (192 externum tests, boot log timing, benchmark ops/sec). Explain why that number was hard to get. Never fake precision — say "measured on my machine, config in repo".&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 3 — The in-tree war story
&lt;/h2&gt;

&lt;p&gt;How linux-aegis ships as four patches against upstream instead of a DKMS out-of-tree blob. Why "compiles in-tree, boots in QEMU from CI" was the whole battle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 4 — Teaser frame
&lt;/h2&gt;

&lt;p&gt;"One of my projects compiles to Python, Bash AND a native binary from the same source. Guess why the Bash backend is the weird one." Answer in the next post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 5 — Dev-log snippet
&lt;/h2&gt;

&lt;p&gt;Paste a 5-line terminal capture (talus catching a simulated churn burst, NV2 frame times). Caption: what the viewer is looking at in one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 6 — Anti-hustle take
&lt;/h2&gt;

&lt;p&gt;Build in public ≠ posting daily dopamine. I automate drafts and syndication precisely so the writing time goes into the work. Short, honest, no threads-of-threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 7 — Weekend build
&lt;/h2&gt;

&lt;p&gt;What got shipped this week across all repos (the workflow's commit list makes this trivial). One paragraph max, links to entity home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 8 — Explain like I'm five
&lt;/h2&gt;

&lt;p&gt;eBPF in 3 sentences for non-kernel people: syscall syscall syscall — police inside the kernel watching every door. Then one line on why that's fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 9 — Tooling shoutout
&lt;/h2&gt;

&lt;p&gt;Name one OSS tool that saved you hours this week (crier, git-cliff, libbpf). Genuine recommendation, no affiliation — good-faith networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 10 — Design decision
&lt;/h2&gt;

&lt;p&gt;Why quantum-shield derives per-chunk keys instead of one global nonce. The bug class it prevents, in plain language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 11 — Fail post
&lt;/h2&gt;

&lt;p&gt;Something that broke this week and the fix. The more mundane the better — reliability porn is boring, debugging stories are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 12 — Writing corner
&lt;/h2&gt;

&lt;p&gt;Line from The Stitcher Trilogy + one sentence on stitching horror with systems-brain discipline. Books and code are the same skill: constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 13 — Screenshot Saturday
&lt;/h2&gt;

&lt;p&gt;Best visual of the week (NV2 voxel render, root page CRT aesthetic, terminal UI). Let the image do the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 14 — Question to builders
&lt;/h2&gt;

&lt;p&gt;Ask a real question you have (CO-RE portability across 6.x? ML-KEM key rotation UX?). Answer every reply — questions are the cheapest engagement there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 15 — Milestone marker
&lt;/h2&gt;

&lt;p&gt;Tag/release shipped? Post the changelog's best three lines. git-cliff makes this a copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 16 — Myth-busting
&lt;/h2&gt;

&lt;p&gt;"You need a CS degree / a budget / a team to build systems software." You need a kernel tree, QEMU and stubbornness. Show the CI boot log as proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 17 — Architecture candy
&lt;/h2&gt;

&lt;p&gt;One Mermaid diagram (fortis boot chain or NV2 pipeline). Caption: "every arrow is a link in a trust chain".&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 18 — Workflow flex
&lt;/h2&gt;

&lt;p&gt;The pipeline itself: article → &lt;code&gt;git push&lt;/code&gt; → 9 platforms. "I don't have a marketing team; I have GitHub Actions." Link the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 19 — Contrarian take (polite)
&lt;/h2&gt;

&lt;p&gt;"Most 'post-quantum ready' tools are marketing until ML-KEM is in the default path." One argument, one caveat, invite pushback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 20 — Reading list
&lt;/h2&gt;

&lt;p&gt;Three things you actually read this week (patch series, paper, blog). One sentence each on what changed in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 21 — Weekly wrap
&lt;/h2&gt;

&lt;p&gt;Commits + what's next week. Consistency beats intensity — this is the post that compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 22 — Author life
&lt;/h2&gt;

&lt;p&gt;KDP dashboard moment, a review, or the process of writing book 2's ending. Writers on dev Twitter are rare; use that lane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 23 — Mini-tutorial
&lt;/h2&gt;

&lt;p&gt;One actionable tip (e.g., "add llms.txt to your site in 10 minutes"). Pure value post; nothing asked back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 24 — Progress bar
&lt;/h2&gt;

&lt;p&gt;NV2_ENGINE frame time over the month, or externum test count trend. Small chart, big credibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 25 — Behind the name
&lt;/h2&gt;

&lt;p&gt;Why the projects are named what they're named (aegis, talus, fortis). Names are free branding; tell the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 26 — Security PSA
&lt;/h2&gt;

&lt;p&gt;One habit that actually helps devs (verify signatures, zeroize keys, pin CI base commits). No fear-mongering, one concrete command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 27 — Collab bait
&lt;/h2&gt;

&lt;p&gt;"linux-aegis needs SELinux-stacking test scenarios — if you've done LSM stacking, I'd love your take." Specific asks get specific help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 28 — Tech/genre crossover
&lt;/h2&gt;

&lt;p&gt;"The Stitcher is a crime-horror trilogy written like a distributed system: state, failure modes, irreversible operations." Post the good line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 29 — Open roadmap
&lt;/h2&gt;

&lt;p&gt;Public TODO for the quarter. Accountability post; people return to check if you did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 30 — Gratitude + links
&lt;/h2&gt;

&lt;p&gt;Shout out people who replied/reshared this month. End with the entity home link. The 30-day loop restarts tomorrow.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>buildinpublic</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a Solana-like validator in Rust: what PoH, Tower BFT and Sealevel actually force you to think about</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:52:48 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/building-a-solana-like-validator-in-rust-what-poh-tower-bft-and-sealevel-actually-force-you-to-14h9</link>
      <guid>https://dev.to/bartoszosiej/building-a-solana-like-validator-in-rust-what-poh-tower-bft-and-sealevel-actually-force-you-to-14h9</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;TrustNode is a from-scratch Solana-style cluster in Rust: a verified Proof-of-History clock, Tower BFT-style consensus, a Sealevel-like parallel execution engine, gossip, and erasure-coded block recovery. The point was never to clone Solana — it's that these three subsystems force concrete, painful design decisions that generic "blockchain in Rust" tutorials skip. This is what they actually force.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PoH clock is a scheduling problem, not a hash chain
&lt;/h2&gt;

&lt;p&gt;The naive description — "hash a counter, publish a chain of hashes" — is one line. The real problem is that the clock sources events (&lt;code&gt;TickHeight&lt;/code&gt;, sequential slot heights) and the &lt;em&gt;validators&lt;/em&gt; re-derive them locally to trust the chain. When it breaks, it breaks as ordering: two validators disagree on which tick a transaction belonged to, and the whole ledger forks at that point.&lt;/p&gt;

&lt;p&gt;What actually mattered in the implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batching ticks, not hashing every transaction.&lt;/strong&gt; Hash a counter on each tick, but let a batch of entries commute into one tick. Constant re-hashing per-transaction kills throughput and makes the clock the bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clock must be derivable from the ledger.&lt;/strong&gt; If "slot N belongs to slot N" isn't an independent statement any node can recompute from the block alone, you've built a chain that needs a trusted signer — the exact thing you were trying to cryptographically remove.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifier side must match generator side bit-for-bit.&lt;/strong&gt; Off-by-one in the tick batching turns up as a consensus failure weeks later, not a compile error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: in a PoH system the clock &lt;em&gt;is&lt;/em&gt; the consensus substrate. Get the pure function right first; consensus is downstream of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tower BFT is a voting game at fixed heights
&lt;/h2&gt;

&lt;p&gt;Tower BFT simplifies PBFT by anchoring votes to the PoH slot height. The trick (and the trap) is that votes happen at &lt;em&gt;heights&lt;/em&gt;, and each validator commits to "I have not voted against this fork above slot X for Y slots." The consequence that's easy to miss until you implement it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vote lifetime is a number you have to choose.&lt;/strong&gt; How many slots does a lock hold? Too short and liveness collapses (validators flip-flop, the fork battle never ends); too long and the network can be bricked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit vs. finalize are different states.&lt;/strong&gt; Reaching "commit" as a local statement is easy; broadcasting finalization so &lt;em&gt;other&lt;/em&gt; nodes can rely on it is where cloudblocks appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lock mechanism is per-validator bookkeeping.&lt;/strong&gt; Track highest lock height, refuse to vote below it, and explain that in tests — because half the bugs turn out to be "validator voted against its own lock."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: BFT flavors differ in &lt;em&gt;where they put the voting rules&lt;/em&gt;, not in whether they have them. Implementing one in your own repo makes the whitepaper read like a checklist instead of a mystery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sealevel parallelism lives or dies on account locks
&lt;/h2&gt;

&lt;p&gt;Parallel execution is the marketing line; the implementation is "which accounts does this instruction touch, and can I prove they don't overlap." The real work is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static account-readset/writeset extraction.&lt;/strong&gt; Every instruction declares accounts before execution. If it doesn't, you can't schedule safely — so the API forces it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlap detection decides throughput.&lt;/strong&gt; Two programs touching disjoint accounts run in parallel; any overlap serializes. Most of the performance cliff lives in a naive overlap check (collision on account keys, not program IDs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The price of conflict is determinism.&lt;/strong&gt; Any scheduler that reorders conflicting instructions must be &lt;em&gt;reproducible across all validators&lt;/em&gt;, or the same block executes differently on different machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: "parallel VM" is 20% scheduling and 80% proving it's deterministic while parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd build differently
&lt;/h2&gt;

&lt;p&gt;Real transaction processing over RPC is the current frontier in the repo (commits land almost daily). The honest retro:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boot the consensus before the clock.&lt;/strong&gt; I built PoH first because it looks like the foundation. In hindsight the vote/lock rules are the design core; the clock is just the substrate they sit on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzz the ledger reconstruction.&lt;/strong&gt; Erasure-coded recovery looks simple until a node survives with 2-of-4 shards and has to rebuild &lt;em&gt;without&lt;/em&gt; trusting what it already has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Determinism tests on the scheduler early.&lt;/strong&gt; Portable scheduling is the difference between a demo and a network.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/BartoszOsiej/TrustNode" rel="noopener noreferrer"&gt;TrustNode&lt;/a&gt; — PoH clock, Tower BFT, Sealevel-style execution, gossip + erasure coding, real transaction processing over RPC (Rust, MIT).&lt;/p&gt;

</description>
      <category>rust</category>
      <category>blockchain</category>
      <category>consensus</category>
      <category>systems</category>
    </item>
    <item>
      <title>eBPF verifier limits are a design constraint: what CO-RE field offsets and bounded loops taught me</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:52:16 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/ebpf-verifier-limits-are-a-design-constraint-what-co-re-field-offsets-and-bounded-loops-taught-me-1akc</link>
      <guid>https://dev.to/bartoszosiej/ebpf-verifier-limits-are-a-design-constraint-what-co-re-field-offsets-and-bounded-loops-taught-me-1akc</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Working on talus-process-monitor (Rust userspace + libbpf/C eBPF) I kept bumping into the same three walls: CO-RE field offsets shifting between kernel versions, the verifier refusing anything that looks like an unbounded loop, and map access patterns that get rejected at load time. All three are &lt;em&gt;design&lt;/em&gt; constraints, not compiler annoyances. This post is what changed in my approach once I stopped fighting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  CO-RE relocation is a promise you have to verify
&lt;/h2&gt;

&lt;p&gt;Portable BPF (Compile Once - Run Everywhere) means the kernel rewrites your field accesses based on its own BTF. In theory: compile once, run on kernel 6.x. In practice I chased two classes of bugs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;struct file&lt;/code&gt; layout differences&lt;/strong&gt; — an offset that's valid on kernel A and wrong on kernel B. The relocation did its job; &lt;em&gt;I&lt;/em&gt; had assumed a field meant what it meant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unavailable BTF&lt;/strong&gt; — if the target kernel doesn't ship BTF, relocation fails at load with a cryptic error. It's a deployment check, not a code bug.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What made this tractable: a &lt;strong&gt;verification matrix in CI&lt;/strong&gt; — the same source compiled and &lt;em&gt;loaded&lt;/em&gt; against a set of kernels, with the load result asserted. When a field offset drifts, the pipeline turns red before a cluster does.&lt;/p&gt;

&lt;p&gt;Body of the lesson: "portable" is a claim that must be tested per-kernel, or it's just hope. A build that passes on one kernel is a demo, not portability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verifier forbids unbounded loops. That's fine; it teaches you state machines.
&lt;/h2&gt;

&lt;p&gt;The verifier allows bounded loops (with a maximum iteration count) and rejects anything unverifiable. If you want to scan "all entries in this hash map" you get rejected at load time. The productive response is to stop thinking "iterate until done" and think "fixed horizon, amortized":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lease the map.&lt;/strong&gt; Instead of iterating the whole map per event, keep a fixed-size sliding window of state and, on overflow, process the oldest batch — a bounded, verifier-friendly step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Folding state into the map entry.&lt;/strong&gt; Store the running score &lt;em&gt;in&lt;/em&gt; the entry being updated, so decisions don't require iterating unrelated entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push the unbounded part userspace.&lt;/strong&gt; The kernel probe exports a summary; the Rust side does the arbitrary-loop reasoning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mirrors how real agents behave: kernel-side reaction is quick and bounded; the risky/flexible analysis happens outside the hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map access patterns decide load-time rejection
&lt;/h2&gt;

&lt;p&gt;Two patterns get rejected at unexpected places: holding an entry pointer across a &lt;code&gt;perf&lt;/code&gt; output call, and nested lookup while a lookup key is pinned. The verifier tracks &lt;em&gt;what you are allowed to do while a value pointer is live&lt;/em&gt;. Restructure: copy the fields you need into stack-local values, then emit events. It's the difference between "loads everywhere" and "loads only when the code is shaped right."&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification has to live in CI, not in your head
&lt;/h2&gt;

&lt;p&gt;The current talus trunk carries a &lt;strong&gt;verification document&lt;/strong&gt; (field-offset matrix, per-kernel load expectations) plus CI that compiles and asserts. The single highest-value commit this project got was not a feature — it was the CI job that turned "should work on this kernel" into a machine-checked statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/BartoszOsiej/talus-process-monitor" rel="noopener noreferrer"&gt;talus-process-monitor&lt;/a&gt; — Rust + libbpf/C eBPF endpoint security agent with a MeMLP neural detection engine; verification matrix lives in &lt;a href="https://github.com/BartoszOsiej/talus-process-monitor/blob/master/VERIFICATION-EBPF.md" rel="noopener noreferrer"&gt;VERIFICATION-EBPF.md&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>rust</category>
      <category>kernel</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
