<?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: 云微</title>
    <description>The latest articles on DEV Community by 云微 (@yunwei37).</description>
    <link>https://dev.to/yunwei37</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%2F1139584%2F27142ebf-c0a3-449b-9482-d63e79238a26.jpeg</url>
      <title>DEV Community: 云微</title>
      <link>https://dev.to/yunwei37</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yunwei37"/>
    <language>en</language>
    <item>
      <title>AgentCgroup: What Happens When AI Agents Meet OS Resources?</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Thu, 27 Aug 2026 23:23:50 +0000</pubDate>
      <link>https://dev.to/yunwei37/agentcgroup-what-happens-when-ai-agents-meet-os-resources-1h2f</link>
      <guid>https://dev.to/yunwei37/agentcgroup-what-happens-when-ai-agents-meet-os-resources-1h2f</guid>
      <description>&lt;p&gt;An AI agent spends several quiet minutes reading and editing files, then launches &lt;code&gt;pytest&lt;/code&gt;. Memory can rise by hundreds of megabytes in a second as the test process loads dependencies, only to fall again when the command exits. A container-level controller sees one workload cross a limit. It cannot tell that the burst belongs to a short-lived tool process while the long-lived agent runtime holds the conversation, partial diagnosis, and edits that make the task recoverable.&lt;/p&gt;

&lt;p&gt;We measured how often that pattern occurs by running 144 SWE-rebench tasks with two LLM backends. OS work such as container setup and tool execution consumes 55% to 60% of end-to-end latency, and memory reaches 15.4 times its average level with sub-second changes up to 3 GB/s. Even two Bash calls can differ by 13.7 times in memory demand because one runs &lt;code&gt;git status&lt;/code&gt; while another launches a test suite. Token count offers almost no warning of the next peak: its correlation with peak memory is −0.14 for Haiku and +0.02 for GLM.&lt;/p&gt;

&lt;p&gt;Static allocation reacts badly to this combination. Reserving the observed peak wastes up to 93% of provisioned capacity during quiet phases, while killing the container at the peak discards minutes of accumulated, non-reproducible agent state. The &lt;a href="https://arxiv.org/abs/2602.09345" rel="noopener noreferrer"&gt;AgentCgroup paper&lt;/a&gt; starts from these measurements and develops an eBPF-based controller that can respond at tool-call granularity.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Paper: &lt;a href="https://arxiv.org/abs/2602.09345" rel="noopener noreferrer"&gt;&lt;em&gt;AgentCgroup: Understanding and Controlling OS Resources of AI Agents&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/eunomia-bpf/agentcgroup" rel="noopener noreferrer"&gt;github.com/eunomia-bpf/agentcgroup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Follow the Agent from Edit to Test
&lt;/h2&gt;

&lt;p&gt;To follow the &lt;code&gt;pytest&lt;/code&gt; burst back to the process that caused it, we instrumented &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; while it ran 144 software engineering tasks from &lt;a href="https://github.com/swe-bench/SWE-ReB" rel="noopener noreferrer"&gt;SWE-rebench&lt;/a&gt;. The traces preserve tool-call boundaries alongside one-second CPU and memory samples, so a rise in container memory can be matched to the command running at that moment. We used two LLM backends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Haiku 4.5 (cloud API): LLM inference runs on Anthropic's cloud; the container only runs the agent framework and tool calls.&lt;/li&gt;
&lt;li&gt;GLM-4.7-Flash (local GPU): LLM inference runs on a local GPU; everything happens on the same machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both use the exact same agent framework (Claude Code, Node.js-based). The only difference is the underlying model and where inference happens. This lets us isolate the effect of model choice on container-level resource dynamics.&lt;/p&gt;

&lt;h3&gt;
  
  
  How We Captured the Burst
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Intel Core Ultra 9 285K (24 cores, 5.8 GHz), 128 GB DDR5, Ubuntu 24.04.3 LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;Linux 6.15.11 with cgroup v2 enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container Runtime&lt;/td&gt;
&lt;td&gt;Podman (rootless, isolated containers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent Framework&lt;/td&gt;
&lt;td&gt;Claude Code (Node.js)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Models&lt;/td&gt;
&lt;td&gt;Haiku 4.5 (cloud API, 33 tasks) + GLM-4.7-Flash (local GPU, 111 tasks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmark&lt;/td&gt;
&lt;td&gt;SWE-rebench (real GitHub issues from open-source projects)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;1-second interval CPU/memory sampling via &lt;code&gt;podman stats&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracing&lt;/td&gt;
&lt;td&gt;Tool call boundaries (type, start/end timestamps) from agent execution traces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tasks cover CLI tools, build systems, scientific and medical code, data processing, and web projects at three difficulty levels. We imposed no resource limit during characterization because the trace must show the unconstrained peak before a controller can decide how to handle it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool Call Owns the Spike
&lt;/h2&gt;

&lt;p&gt;The opening &lt;code&gt;pytest&lt;/code&gt; process is not an edge case hidden beneath model inference. Across both backends, most end-to-end time is spent starting the environment and running tools, which puts the operating system directly on the critical path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most Time Is Spent Outside the Model
&lt;/h3&gt;

&lt;p&gt;Contrary to the intuition that "the LLM is the bottleneck," our measurements show that LLM reasoning accounts for only 40–45% of end-to-end task latency. The remaining 55–60% is OS-level overhead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Latency Component&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;th&gt;GLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container + agent initialization&lt;/td&gt;
&lt;td&gt;47.7%&lt;/td&gt;
&lt;td&gt;31.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool execution&lt;/td&gt;
&lt;td&gt;10.4%&lt;/td&gt;
&lt;td&gt;24.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM reasoning&lt;/td&gt;
&lt;td&gt;41.9%&lt;/td&gt;
&lt;td&gt;44.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Container startup alone averages 26.5 seconds (median 23.0s, max 97s), driven by Podman's user-namespace ID remapping of overlay layers that scales with image size. Since SWE-rebench container images range from 2.9 GB to 17.3 GB (median 3.5 GB), roughly 7x larger than typical microservice images and 70x larger than serverless functions, this initialization overhead is substantial.&lt;/p&gt;

&lt;p&gt;Each task then remains alive for 5 to 11 minutes, keeping LLM context, code edits, and tool results in one stateful process. Bash and sub-agent calls consume more than 90% of tool time, although Haiku offloads more work to sub-agents while GLM performs almost everything through local Bash. A controller therefore has to preserve the long-lived runtime while accounting for the very different commands launched beneath the same Bash tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow Bash One Layer Deeper
&lt;/h3&gt;

&lt;p&gt;The tool name still does not reveal the resource demand. Breaking Bash calls down by command semantics shows where the time goes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bash Category&lt;/th&gt;
&lt;th&gt;% of Bash Time (Haiku)&lt;/th&gt;
&lt;th&gt;% of Bash Time (GLM)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test execution (pytest, unittest, etc.)&lt;/td&gt;
&lt;td&gt;72.9%&lt;/td&gt;
&lt;td&gt;43.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python snippets&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;26.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package installation&lt;/td&gt;
&lt;td&gt;10.8%&lt;/td&gt;
&lt;td&gt;10.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git operations&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File exploration&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Test execution overwhelmingly dominates, and as shown in the next section, it is also the most resource-intensive category.&lt;/p&gt;

&lt;p&gt;The calls also move through a recognizable sequence when execution is divided into ten equal phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand phase (0–30%): Read operations dominate (code exploration)&lt;/li&gt;
&lt;li&gt;Modify phase (30–70%): Edit operations are distributed throughout; Bash begins rising&lt;/li&gt;
&lt;li&gt;Verify phase (40–100%): Bash peaks (repeated test execution, debugging)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase signature mirrors the "understand, modify, verify" workflow of human software engineering, providing a basis for phase-aware resource control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Burst Ends Before a Container Controller Can Adapt
&lt;/h2&gt;

&lt;p&gt;On our 24-core platform, CPU remains below 36% even at the concurrency limit imposed by memory. Peak memory reaches 2 to 4 GB per task, so allocating every container at peak consumes the machine while much of its CPU capacity remains idle. The trace explains why: one stable layer needs protection, while a second layer appears only when tools run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protect the 185 MB Stateful Baseline
&lt;/h3&gt;

&lt;p&gt;Agent memory has a two-layer structure. The opening &lt;code&gt;pytest&lt;/code&gt; run belongs to the transient layer, while the conversation and framework live in the stable layer beneath it.&lt;/p&gt;

&lt;p&gt;Layer 1, the framework baseline (~185 MB): The Node.js runtime, V8 JIT cache, and agent framework state maintain a stable, incompressible memory floor throughout execution, even during LLM reasoning phases with zero tool activity. Across all 144 tasks, early-execution memory averages 183 MB (Haiku) and 188 MB (GLM).&lt;/p&gt;

&lt;p&gt;Layer 2, tool-call bursts (500 MB to 2+ GB): Test execution, dependency installation, and data processing operations create transient spikes that last only 1–2 seconds before collapsing back to the ~185 MB baseline.&lt;/p&gt;

&lt;p&gt;When we normalize and aggregate memory traces across all 144 tasks by execution progress, the pattern is clear: the first half of execution stays at a stable 185–200 MB baseline, while the second half shows increasing variance with large spikes, corresponding to the Bash-intensive verify phase.&lt;/p&gt;

&lt;p&gt;In a multi-tenant deployment, 64 concurrent instances require ~12 GB just for the framework baseline alone. The tool-call bursts layered on top are the real resource management challenge, and they require different treatment from the stable baseline.&lt;/p&gt;

&lt;p&gt;Annotating each sample with its active tool call shows that memory bursts concentrate around tools 1.9 times more often than their share of execution time would suggest. CPU bursts are more dispersed, especially when local inference adds background CPU load, so CPU and memory need separate control signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Static Limit Has No Good Setting
&lt;/h3&gt;

&lt;p&gt;The tool-driven memory layer changes faster than a container-level policy expects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maximum memory change rate: 3 GB/second&lt;/li&gt;
&lt;li&gt;Maximum CPU change rate: &amp;gt;50%/second&lt;/li&gt;
&lt;li&gt;Burst duration: typically 1–2 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The highest case we observed, a pydicom bioinformatics task (Medical_Bio_Hard), reached 4060 MB peak versus 264 MB average, a 15.4x peak-to-average ratio. This 4 GB spike lasted approximately 1–2 seconds before falling back to the 230 MB baseline.&lt;/p&gt;

&lt;p&gt;Allocating the pydicom container at its 4060 MB peak leaves 93% of that memory unused during the typical 264 MB phase. Setting the limit near the average lets a one-second tool burst trigger an OOM kill and discard the protected baseline. The policy needs to distinguish the command that created the temporary layer, so we compared memory spikes inside Bash:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bash Category&lt;/th&gt;
&lt;th&gt;P95 Memory Spike (Haiku)&lt;/th&gt;
&lt;th&gt;P95 Memory Spike (GLM)&lt;/th&gt;
&lt;th&gt;Avg CPU Spike&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test execution (pytest, etc.)&lt;/td&gt;
&lt;td&gt;518 MB&lt;/td&gt;
&lt;td&gt;234 MB&lt;/td&gt;
&lt;td&gt;+3.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package installation&lt;/td&gt;
&lt;td&gt;233 MB&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git operations&lt;/td&gt;
&lt;td&gt;13.5 MB&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File exploration&lt;/td&gt;
&lt;td&gt;4.5 MB&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Medical and bioinformatics commands average a 4 GB peak, while web and network commands average 291 MB. A Bash-level budget still groups &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;git status&lt;/code&gt;, and &lt;code&gt;pytest&lt;/code&gt; together, so the useful boundary lies at the actual command process. CPU cannot stand in for memory at that boundary either: their correlation ranges from -0.84 to +0.50 across tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next Pytest Run Does Not Look Like the Last One
&lt;/h2&gt;

&lt;p&gt;A static limit might still work if yesterday's trace predicted tomorrow's peak. Repeating the exact same task, &lt;code&gt;iterative/dvc#777&lt;/code&gt;, shows why the agent cannot rely on that history:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Execution Time&lt;/th&gt;
&lt;th&gt;Solution Strategy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;402 seconds&lt;/td&gt;
&lt;td&gt;Strategy A (different file modifications)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;222 seconds&lt;/td&gt;
&lt;td&gt;Strategy B (different approach)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;259 seconds&lt;/td&gt;
&lt;td&gt;Strategy C (different file count)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a 1.8x variance in execution time, with completely different solution strategies each time. This non-determinism stems from LLM reasoning randomness and decision-path diversity: the agent may choose entirely different code modifications, tool sequences, and debugging approaches on each run.&lt;/p&gt;

&lt;p&gt;The model's own activity offers little earlier warning. Correlations between LLM-observable proxies and resource consumption remain weak:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Proxy to Target&lt;/th&gt;
&lt;th&gt;Haiku (r)&lt;/th&gt;
&lt;th&gt;GLM (r)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens to peak memory&lt;/td&gt;
&lt;td&gt;−0.14&lt;/td&gt;
&lt;td&gt;+0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation rounds to execution time&lt;/td&gt;
&lt;td&gt;+0.57&lt;/td&gt;
&lt;td&gt;+0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation rounds to peak memory&lt;/td&gt;
&lt;td&gt;+0.02&lt;/td&gt;
&lt;td&gt;+0.11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Output token count shows essentially zero correlation with peak memory. Even conversation rounds, which moderately predict execution time, are useless for predicting memory. Resource consumption is driven by what tools execute (e.g., pytest vs. file read), not by the scale of LLM reasoning. This means that even if one can predict how much an agent will "think," one still cannot predict how much memory it will need.&lt;/p&gt;

&lt;p&gt;The familiar edit, test, fail, and retry loop adds another source of drift:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;th&gt;GLM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tasks with retry loops (3+ consecutive identical Bash calls)&lt;/td&gt;
&lt;td&gt;85% (28/33)&lt;/td&gt;
&lt;td&gt;97% (108/111)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average retry groups per task&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;3.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum consecutive retries&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time consumed by retries&lt;/td&gt;
&lt;td&gt;7.4%&lt;/td&gt;
&lt;td&gt;20.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "execute test, observe failure, modify code, re-test" iteration loop is the agent's behavioral signature. Each retry retains prior memory context without cleanup, leading to progressive memory accumulation, up to 502 MB of unreleased memory in the worst case we observed. This means memory limits that were adequate early in execution may trigger OOM kills later as retries accumulate.&lt;/p&gt;

&lt;p&gt;Across all tasks, peak memory spans 197 MB to 4 GB, a 20x range under the same agent framework. Model choice changes the profile again. The controller therefore needs the current command boundary and current pressure; a percentile from prior runs cannot substitute for either signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the Tool Call Its Own Resource Domain
&lt;/h2&gt;

&lt;p&gt;Return to the &lt;code&gt;pytest&lt;/code&gt; burst. The 185 MB agent runtime and its test child currently share one container limit, even though the runtime contains expensive state and the child owns the temporary allocation. AgentCgroup maps the agent workload to a cgroup v2 node and places each tool call in a child node. A &lt;code&gt;git status&lt;/code&gt; process and a &lt;code&gt;pytest&lt;/code&gt; process can then receive different constraints while both remain inside the workload's total budget.&lt;/p&gt;

&lt;p&gt;This hierarchy also changes recovery. Crossing a soft limit can freeze or throttle the tool subtree while the parent agent remains alive. If termination becomes necessary, cgroup v2 can kill that subtree atomically, preserving the conversation and edits held by the runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move the Response into the Kernel
&lt;/h2&gt;

&lt;p&gt;The command boundary solves granularity, but a one-second, 3 GB/s burst still demands a fast response. AgentCgroup executes control logic at kernel cgroup enforcement points through eBPF, removing the user-space signal, decision, and write-back loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On CPU, &lt;code&gt;sched_ext&lt;/code&gt; maintains per-workload and per-tool-call metadata in BPF maps, prioritizing latency-sensitive tool calls with automatic fail-safe reversion on errors.&lt;/li&gt;
&lt;li&gt;On memory, &lt;code&gt;memcg_bpf_ops&lt;/code&gt; hooks implement custom throttling delays when a cgroup breaches its soft limit (&lt;code&gt;memory.high&lt;/code&gt;), with &lt;code&gt;memory.max&lt;/code&gt; as the hard limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same in-kernel observations replace prediction with current evidence. AgentCgroup traces process creation and memory allocation, detects tool boundaries, and applies graduated responses as pressure changes: &lt;code&gt;memory.high&lt;/code&gt; delays throttle allocation, &lt;code&gt;cgroup.freeze&lt;/code&gt; pauses a subtree, and &lt;code&gt;memory.max&lt;/code&gt; remains the hard boundary. The parent runtime keeps the state needed to decide what to do after the tool returns or fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Trace Replay Shows
&lt;/h2&gt;

&lt;p&gt;We evaluated AgentCgroup by replaying real agent memory traces at 50x accelerated speed in a multi-tenant setting on a patched Linux 6.19.0-rc5 kernel (bpf-next + memcg struct_ops RFC patches). Three concurrent agent traces share constrained memory:&lt;/p&gt;

&lt;p&gt;Tight memory scenario (1100 MB total for ~1233 MB combined demand):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Baseline: OOM-kills one low-priority process (66% survival)&lt;/li&gt;
&lt;li&gt;AgentCgroup: all processes complete (100% survival), 239 throttle triggers, high-priority agent finishes with only +2.8% overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moderate memory scenario (1300 MB total):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AgentCgroup reduces high-priority P95 allocation latency by 29% (70.97 to 50.14 ms) through reduced memory contention&lt;/li&gt;
&lt;li&gt;Total completion time: −1.1% (net improvement)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measured P50 latency overhead is 0.3%, and BPF throttling precision stays within 2.3% relative error. These are trace-replay results from a proof-of-concept kernel path, so they demonstrate the control mechanism under the paper's setup rather than production-scale behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Results
&lt;/h2&gt;

&lt;p&gt;The fastest inspection path uses the repository's collected traces in user space, before moving on to experiments that load eBPF programs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--recurse-submodules&lt;/span&gt; https://github.com/eunomia-bpf/agentcgroup.git
&lt;span class="nb"&gt;cd &lt;/span&gt;agentcgroup
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
python analysis/characterization.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/eunomia-bpf/agentcgroup/blob/main/docs/REPRODUCING.md" rel="noopener noreferrer"&gt;reproduction guide&lt;/a&gt; then maps commands to the CPU scheduling, memory isolation, and overhead experiments. CPU control requires Linux 6.12 or newer with &lt;code&gt;sched_ext&lt;/code&gt; and cgroup v2. The memory experiments additionally use the &lt;code&gt;memcg_bpf_ops&lt;/code&gt; kernel path described by the artifact, so that part requires the matching kernel support and root access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Evidence Stops
&lt;/h2&gt;

&lt;p&gt;The characterization covers Claude Code on SWE-rebench, and the controller evaluation uses accelerated trace replay with a proof-of-concept implementation. Live concurrent agents, other frameworks such as OpenHands and SWE-agent, additional container runtimes, and the upstream status of &lt;code&gt;memcg_bpf_ops&lt;/code&gt; remain open work. The &lt;a href="https://arxiv.org/abs/2602.09345" rel="noopener noreferrer"&gt;paper&lt;/a&gt; and &lt;a href="https://github.com/eunomia-bpf/agentcgroup" rel="noopener noreferrer"&gt;eunomia-bpf/agentcgroup&lt;/a&gt; repository expose the raw experiments, analysis scripts, controller code, and reproduction instructions needed to test those boundaries.&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>linux</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>eBPF Tutorial: Inspecting the Executable Image After exec</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Tue, 25 Aug 2026 23:05:41 +0000</pubDate>
      <link>https://dev.to/yunwei37/ebpf-tutorial-inspecting-the-executable-image-after-exec-3el7</link>
      <guid>https://dev.to/yunwei37/ebpf-tutorial-inspecting-the-executable-image-after-exec-3el7</guid>
      <description>&lt;p&gt;When a process calls &lt;code&gt;execve&lt;/code&gt;, the kernel replaces its memory image with a new executable. But which executable, exactly? If the command line says &lt;code&gt;/usr/bin/wrapper.sh --config /etc/app.conf&lt;/code&gt;, the actual running code might be a Python interpreter or a compiled binary launched three layers deep through wrapper scripts. Security tools, container runtimes, and troubleshooting utilities all need to know what the kernel &lt;em&gt;actually&lt;/em&gt; installed, not just what the user typed.&lt;/p&gt;

&lt;p&gt;This tutorial builds a tool that captures that information at the kernel level. It hooks the moment credentials are committed after exec, schedules a deferred callback, then reads the installed executable's ELF header and reports the architecture, byte order, and file type. Along the way, it demonstrates two recent kernel features (BPF task work and file dynptr) that together solve a problem traditional eBPF approaches cannot.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Complete source: &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/54-exec-image-inspector" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/54-exec-image-inspector&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: reading file content from eBPF
&lt;/h2&gt;

&lt;p&gt;Why would a security tool need to read the executable's content rather than just its path? Because the path alone does not identify what code will run. A hash of the executable's content can verify it matches a known-good binary. Embedded signatures or certificates can prove provenance. Specific byte patterns at known offsets can identify packing, obfuscation, or tampering. None of this information is available from the path; you must read the file's bytes. Doing so from eBPF, at the exact moment of exec, eliminates the race window that plagues user-space approaches.&lt;/p&gt;

&lt;p&gt;The most direct approach to inspecting an executable is reading &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/exe&lt;/code&gt;, but this only works if the process is still alive. Short-lived processes exit before you can read them. Even if you catch them in time, the &lt;code&gt;/proc&lt;/code&gt; filesystem is accessed from user space, creating a race window: by the time you read the symlink, the process might have called &lt;code&gt;execve&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;Tracepoints and kprobes can hook &lt;code&gt;sched_process_exec&lt;/code&gt; to observe exec events synchronously, but these hooks run in what the kernel calls a &lt;strong&gt;non-sleepable context&lt;/strong&gt;. This matters because of how Linux manages file data in memory.&lt;/p&gt;

&lt;p&gt;When you read from a file, the kernel first checks whether the requested bytes are already in the &lt;strong&gt;page cache&lt;/strong&gt;, a memory region that caches recently-accessed file data. If they are, the read completes immediately. If they are not (a &lt;strong&gt;cold page&lt;/strong&gt;), the kernel must issue I/O to the storage device, and the calling context must &lt;strong&gt;sleep&lt;/strong&gt; while waiting for that I/O to complete.&lt;/p&gt;

&lt;p&gt;BPF programs attached to tracepoints and kprobes cannot sleep. They run with interrupts potentially disabled and locks held; sleeping would deadlock the system. If a BPF program tries to read file content and encounters a cold page, the read fails with &lt;code&gt;-EFAULT&lt;/code&gt; instead of waiting for I/O.&lt;/p&gt;

&lt;p&gt;This creates a fundamental limitation: you can observe exec events, but you cannot reliably read the executable's content to verify its ELF header or check embedded metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: BPF task work and file dynptr
&lt;/h2&gt;

&lt;p&gt;Linux 6.18 introduced &lt;strong&gt;BPF task work&lt;/strong&gt;, a mechanism that lets a BPF program schedule a callback to run later in a safe, sleepable context. The callback executes before the target task returns to user space, at a point where the kernel permits sleeping.&lt;/p&gt;

&lt;p&gt;Linux 6.19 introduced &lt;strong&gt;file dynptr&lt;/strong&gt;, which provides verifier-tracked access to file data. A dynptr (dynamic pointer) is a BPF abstraction that tracks a pointer's bounds at verification time; the file variant wraps file I/O operations so the verifier can ensure memory safety.&lt;/p&gt;

&lt;p&gt;Combining these features, the design becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach an LSM hook to &lt;code&gt;bprm_committed_creds&lt;/code&gt;, which fires after exec installs the new credentials&lt;/li&gt;
&lt;li&gt;In the hook (non-sleepable), create per-exec state and schedule a task work callback&lt;/li&gt;
&lt;li&gt;The callback runs in a sleepable context, where it can access the installed executable, read content from any offset (including cold pages), and send results to user space&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation (schedule work in a non-sleepable hook, read the file in a sleepable callback) is the key insight.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BPF task work operates
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;bpf_task_work_schedule_signal(task, work, map, callback)&lt;/code&gt;, the kernel associates your callback with the specified task. The callback does not run immediately; it runs later, at a safe point before that task returns to user space.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;struct bpf_task_work&lt;/code&gt; is an opaque structure that the kernel uses to track the scheduled callback. Your BPF program allocates storage for it but does not interpret its contents. This tool uses a HASH map keyed by &lt;code&gt;pid_tgid&lt;/code&gt;; each &lt;code&gt;struct exec_work&lt;/code&gt; value contains the &lt;code&gt;bpf_task_work&lt;/code&gt; storage plus fields for timestamps and intermediate results. Separate keys allow concurrent execs to remain independent.&lt;/p&gt;

&lt;p&gt;The callback signature is &lt;code&gt;int callback(struct bpf_map *map, void *key, void *value)&lt;/code&gt;. The &lt;code&gt;value&lt;/code&gt; parameter points to the map element containing your &lt;code&gt;bpf_task_work&lt;/code&gt;, so you can pass data from the scheduling hook to the callback through surrounding fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  How file dynptr operates
&lt;/h2&gt;

&lt;p&gt;A dynptr wraps a pointer with bounds information that the BPF verifier can track. For file dynptrs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bpf_dynptr_from_file(file, flags, dynptr)&lt;/code&gt; creates a dynptr from a file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bpf_dynptr_read(dst, len, dynptr, offset, flags)&lt;/code&gt; reads content at the specified offset&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bpf_dynptr_file_discard(dynptr)&lt;/code&gt; releases the dynptr's internal state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every path that creates a dynptr, including error paths, must call &lt;code&gt;bpf_dynptr_file_discard&lt;/code&gt; to release it. Failing to do so leaks internal resources.&lt;/p&gt;

&lt;p&gt;In a non-sleepable context, &lt;code&gt;bpf_dynptr_read&lt;/code&gt; only succeeds if the target bytes are already in the page cache. Accessing a cold page returns &lt;code&gt;-EFAULT&lt;/code&gt;. In a sleepable context, the same call can trigger page fault handling and wait for I/O, so it succeeds even for cold pages. This difference is why task work matters: the callback runs in a sleepable context where file reads work reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool architecture
&lt;/h2&gt;

&lt;p&gt;The user-space program loads and attaches the BPF program, creates a ring buffer reader, and prints &lt;code&gt;READY scope=system-wide&lt;/code&gt;. It then remains active until SIGINT or SIGTERM while workloads run normally.&lt;/p&gt;

&lt;p&gt;For every successful exec after &lt;code&gt;READY&lt;/code&gt;, the &lt;code&gt;lsm/bprm_committed_creds&lt;/code&gt; hook inserts one &lt;code&gt;exec_work&lt;/code&gt; value into the &lt;code&gt;pending&lt;/code&gt; HASH map under the current &lt;code&gt;pid_tgid&lt;/code&gt;, records a timestamp, and schedules a task work callback. Failed insertions or scheduling attempts are counted and clean up the map entry.&lt;/p&gt;

&lt;p&gt;The callback (&lt;code&gt;inspect_executable&lt;/code&gt;) runs later in the execing task's sleepable context. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calls &lt;code&gt;bpf_get_task_exe_file&lt;/code&gt; to get the installed executable (returning a referenced &lt;code&gt;struct file&lt;/code&gt; that must be released with &lt;code&gt;bpf_put_file&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Resolves the path with &lt;code&gt;bpf_path_d_path&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates a file dynptr and reads the 64-byte ELF header&lt;/li&gt;
&lt;li&gt;Parses ELF fields: magic number, class (32/64-bit), data (endianness), type (executable vs shared object), and machine (architecture)&lt;/li&gt;
&lt;li&gt;Sends an event through the ring buffer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;User space polls the ring buffer until a signal arrives. On shutdown it detaches the LSM program, waits until &lt;code&gt;completed &amp;gt;= scheduled&lt;/code&gt;, drains remaining events, prints the counters, and then destroys the skeleton.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ij0ymrddc7qe9crr5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ij0ymrddc7qe9crr5v.png" alt="Exec image inspector data flow" width="799" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code walkthrough
&lt;/h2&gt;

&lt;p&gt;The implementation spans four files: a shared header, a compatibility header for new kernel interfaces, the BPF program, and the user-space loader.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared header
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;exec_image_inspector.h&lt;/code&gt; defines structures shared between BPF and user space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef __EXEC_IMAGE_INSPECTOR_H
#define __EXEC_IMAGE_INSPECTOR_H
&lt;/span&gt;
&lt;span class="cp"&gt;#define EXEC_COMM_LEN 16
#define EXEC_PATH_LEN 256
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;is_elf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;elf_class&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;elf_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;elf_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;elf_machine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;header_error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;path_error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;latency_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&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;EXEC_COMM_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EXEC_PATH_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inspector_stats&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;scheduled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;schedule_errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;header_errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;path_errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;cleanup_errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cp"&gt;#endif &lt;/span&gt;&lt;span class="cm"&gt;/* __EXEC_IMAGE_INSPECTOR_H */&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exec_event&lt;/code&gt; carries everything needed to report one exec: process identifiers, the resolved path, ELF metadata, and error codes. &lt;code&gt;inspector_stats&lt;/code&gt; accumulates counters that user space reads from the BSS section at exit to report success and failure rates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compatibility header
&lt;/h3&gt;

&lt;p&gt;The repository's vendored vmlinux headers predate Linux 6.18/6.19, so &lt;code&gt;bpf_experimental.h&lt;/code&gt; declares the new interfaces locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef __EXEC_IMAGE_INSPECTOR_BPF_EXPERIMENTAL_H
#define __EXEC_IMAGE_INSPECTOR_BPF_EXPERIMENTAL_H
&lt;/span&gt;
&lt;span class="cm"&gt;/*
 * These Linux 6.18/6.19 declarations are not present in the repository's
 * older generated UAPI and vmlinux headers. Keep them local until those
 * vendored headers are regenerated.
 */&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_task_work&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;opaque&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;aligned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bpf_task_work_callback_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_map&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_task_work_schedule_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_task_work&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;map__map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;bpf_task_work_callback_t&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bpf_get_task_exe_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;bpf_put_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_path_d_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;buf__sz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr__uninit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr_file_discard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;dynptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#endif &lt;/span&gt;&lt;span class="cm"&gt;/* __EXEC_IMAGE_INSPECTOR_BPF_EXPERIMENTAL_H */&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These declarations use &lt;code&gt;__ksym&lt;/code&gt; to mark them as kernel symbols resolved at load time. Once the repository's vmlinux headers are regenerated from a 6.19+ kernel, this file can be removed.&lt;/p&gt;

&lt;h3&gt;
  
  
  BPF program
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;exec_image_inspector.bpf.c&lt;/code&gt; implements the LSM hook and the task work callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"vmlinux.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_tracing.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"bpf_experimental.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"exec_image_inspector.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;LICENSE&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#define ENOENT 2
&lt;/span&gt;
&lt;span class="cp"&gt;#define EI_CLASS 4
#define EI_DATA 5
#define ELFCLASS32 1
#define ELFCLASS64 2
#define ELFDATA2LSB 1
#define ELFDATA2MSB 2
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inspector_stats&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_RINGBUF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_work&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;scheduled_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_task_work&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_HASH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_F_NO_PREALLOC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_work&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="nf"&gt;read_elf_u16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__u8&lt;/span&gt; &lt;span class="n"&gt;data&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ELFDATA2MSB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;__u16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&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;span class="n"&gt;__u16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&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="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;inspect_executable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_map&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;header&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_work&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_event&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr&lt;/span&gt; &lt;span class="n"&gt;dynptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callbacks&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="n"&gt;pid_tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&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="n"&gt;__u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;pid_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;scheduled_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bpf_get_current_comm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_task_btf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_task_exe_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ENOENT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_errors&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="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_path_d_path&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;file&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;f_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_errors&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;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;dynptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bpf_dynptr_file_discard&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;dynptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_errors&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="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;put_file&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_dynptr_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&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;dynptr&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_errors&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;span class="n"&gt;bpf_dynptr_file_discard&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;dynptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header_error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;header&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="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;header&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="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'E'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'L'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_elf&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elf_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EI_CLASS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elf_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EI_DATA&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elf_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_elf_u16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&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="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elf_data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elf_machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read_elf_u16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elf_data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nl"&gt;put_file:&lt;/span&gt;
    &lt;span class="n"&gt;bpf_put_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nl"&gt;emit:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_ringbuf_output&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;events&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="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&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;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropped&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_map_delete_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanup_errors&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="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed&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="k"&gt;return&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="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lsm/bprm_committed_creds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schedule_exec_inspection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;linux_binprm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bprm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_work&lt;/span&gt; &lt;span class="n"&gt;empty_work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_work&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;bprm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;pid_tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matched&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_update_elem&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;pending&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;key&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;empty_work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_NOEXIST&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule_errors&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&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;pending&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;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule_errors&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_map_delete_elem&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;pending&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;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanup_errors&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;scheduled_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_task_btf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_task_work_schedule_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&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;work&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;work&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;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;inspect_executable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule_errors&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_map_delete_elem&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;pending&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;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cleanup_errors&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduled&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 entry point is &lt;code&gt;schedule_exec_inspection&lt;/code&gt;, declared with &lt;code&gt;SEC("lsm/bprm_committed_creds")&lt;/code&gt;. This LSM hook fires after the new executable's credentials have been installed. The hook itself is non-sleepable, so it creates the per-exec state and schedules the deferred work.&lt;/p&gt;

&lt;p&gt;For each exec, the program inserts a zeroed &lt;code&gt;struct exec_work&lt;/code&gt; into the &lt;code&gt;pending&lt;/code&gt; HASH map with &lt;code&gt;BPF_NOEXIST&lt;/code&gt;, keyed by &lt;code&gt;pid_tgid&lt;/code&gt;, then records the timestamp. The callback deletes that exact key. This supports concurrent execs without sharing one task-work slot.&lt;/p&gt;

&lt;p&gt;The hook calls &lt;code&gt;bpf_task_work_schedule_signal&lt;/code&gt; after saving the timestamp. The kernel holds the references needed to execute the callback later. Every insert, lookup, or scheduling failure is counted, and every path that created pending state deletes it.&lt;/p&gt;

&lt;p&gt;The callback &lt;code&gt;inspect_executable&lt;/code&gt; calculates latency for diagnostics, then acquires the executable file with &lt;code&gt;bpf_get_task_exe_file&lt;/code&gt;. This returns a referenced &lt;code&gt;struct file&lt;/code&gt; that must be released with &lt;code&gt;bpf_put_file&lt;/code&gt;. The callback resolves the path, creates a file dynptr, reads the 64-byte ELF header, and parses it. The &lt;code&gt;read_elf_u16&lt;/code&gt; helper handles endianness: ELF files declare their byte order in the header, and multi-byte fields must be read accordingly.&lt;/p&gt;

&lt;p&gt;Every path that creates a dynptr, success or failure, must call &lt;code&gt;bpf_dynptr_file_discard&lt;/code&gt;. Finally, &lt;code&gt;bpf_ringbuf_output&lt;/code&gt; sends the event to user space, the callback deletes its pending entry, and &lt;code&gt;completed&lt;/code&gt; is incremented so shutdown can wait for finished work rather than merely scheduled work.&lt;/p&gt;

&lt;h3&gt;
  
  
  User-space loader
&lt;/h3&gt;

&lt;p&gt;The complete loader is linked at the beginning of this tutorial. Its main lifecycle is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;exec_image_inspector_bpf&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;event_context&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ring_buffer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ring_buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argv&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;install_signal_handlers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to install signal handlers: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;strerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;error&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="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;libbpf_set_print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libbpf_print_fn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;setup_inspector&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;events&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;skel&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;ring_buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;monitor_execs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;exec_image_inspector_bpf__detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;drain_pending_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;report_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&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;events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&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="nl"&gt;cleanup:&lt;/span&gt;
    &lt;span class="n"&gt;ring_buffer__free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;exec_image_inspector_bpf__destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setup_inspector&lt;/code&gt; opens, loads, and attaches the skeleton before creating the ring buffer. &lt;code&gt;monitor_execs&lt;/code&gt; prints &lt;code&gt;READY&lt;/code&gt; and polls until SIGINT or SIGTERM.&lt;/p&gt;

&lt;p&gt;On shutdown, &lt;code&gt;main&lt;/code&gt; detaches first. &lt;code&gt;drain_pending_events&lt;/code&gt; then waits in bounded 100 ms polls for &lt;code&gt;completed&lt;/code&gt; to catch up with &lt;code&gt;scheduled&lt;/code&gt;, drains the ring buffer, and reports all counters before resources are destroyed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;handle_event&lt;/code&gt; formats the output, translating numeric ELF values to readable names while preserving the raw values for scripting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and running
&lt;/h2&gt;

&lt;p&gt;Build from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule update &lt;span class="nt"&gt;--init&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt;
make &lt;span class="nt"&gt;-C&lt;/span&gt; src/54-exec-image-inspector clean
make &lt;span class="nt"&gt;-C&lt;/span&gt; src/54-exec-image-inspector &lt;span class="nt"&gt;-j2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before running, check that &lt;code&gt;bpf&lt;/code&gt; appears in the active LSM list:&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;cat&lt;/span&gt; /sys/kernel/security/lsm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;bpf&lt;/code&gt; is missing, add it to the kernel command line: change &lt;code&gt;lsm=&amp;lt;existing-list&amp;gt;&lt;/code&gt; to &lt;code&gt;lsm=&amp;lt;existing-list&amp;gt;,bpf&lt;/code&gt; in your bootloader configuration.&lt;/p&gt;

&lt;p&gt;Start the monitor:&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; ./src/54-exec-image-inspector/exec_image_inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the program prints &lt;code&gt;READY scope=system-wide&lt;/code&gt;, it reports each successful exec with an &lt;code&gt;EXEC&lt;/code&gt; line containing the process IDs, command name, resolved executable path, ELF metadata, and callback latency. Press Ctrl-C to stop; the final &lt;code&gt;SUMMARY&lt;/code&gt; shows scheduling, callback, error, drop, and event counts.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;--verbose&lt;/code&gt; when libbpf diagnostics are needed:&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; ./src/54-exec-image-inspector/exec_image_inspector &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel version&lt;/td&gt;
&lt;td&gt;Linux 6.19+ (BPF task work introduced in 6.18, file dynptr in 6.19)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel configuration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CONFIG_BPF=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_SYSCALL=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_JIT=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_LSM=y&lt;/code&gt;, &lt;code&gt;CONFIG_SECURITY=y&lt;/code&gt;, &lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active LSM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/sys/kernel/security/lsm&lt;/code&gt; must contain &lt;code&gt;bpf&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Tested on x86_64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileges&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Limitations and extensions
&lt;/h2&gt;

&lt;p&gt;This tool observes successful execs system-wide after &lt;code&gt;READY&lt;/code&gt;. The pending HASH map supports up to 4096 concurrent &lt;code&gt;pid_tgid&lt;/code&gt; keys; insertion or scheduling pressure is visible in &lt;code&gt;schedule_errors&lt;/code&gt;. Shutdown waits for callbacks for about one second before returning an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This tutorial demonstrates how to combine BPF task work and file dynptr to inspect the executable image actually installed by exec. The LSM hook schedules work for each exec, and the task work callback reads the file in a sleepable context. This lets eBPF programs read file data reliably, even when the target bytes are not in the page cache.&lt;/p&gt;

&lt;p&gt;The persistent monitor exposes a natural &lt;code&gt;READY&lt;/code&gt; boundary for independent workloads. Detach-before-drain shutdown, per-exec pending state, and completed-work accounting keep concurrent callbacks safe while preserving the original task-work and file-dynptr lesson.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To learn more about eBPF, visit our tutorial repository at &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial&lt;/a&gt; or &lt;a href="https://eunomia.dev/tutorials/" rel="noopener noreferrer"&gt;https://eunomia.dev/tutorials/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/5c8fd7e2b5b0a527cf88740da122166695382a78" rel="noopener noreferrer"&gt;BPF task-work plumbing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/38aa7003e369802f81a078f6673d10d97013f04f" rel="noopener noreferrer"&gt;bpf_task_work_schedule_signal kfunc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/8d8771dc03e48300e80b43744dd3c320ccaf746a" rel="noopener noreferrer"&gt;File-backed dynptr plumbing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/e3e36edb1b8f0e6975c68acd2e1202ec0397fd75" rel="noopener noreferrer"&gt;File dynptr kfuncs and helpers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/2c52e8943a437af6093d8b0f0920f1764f0e5f64" rel="noopener noreferrer"&gt;Sleepable file-dynptr dispatch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/Documentation/bpf/kfuncs.rst" rel="noopener noreferrer"&gt;Kernel kfunc documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/file_reader.c" rel="noopener noreferrer"&gt;File-reader BPF selftest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/task_work.c" rel="noopener noreferrer"&gt;BPF task-work selftest&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>linux</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>eBPF Tutorial: Building an Egress Pacer with BPF Qdisc</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Mon, 24 Aug 2026 05:22:08 +0000</pubDate>
      <link>https://dev.to/yunwei37/ebpf-tutorial-building-an-egress-pacer-with-bpf-qdisc-4fna</link>
      <guid>https://dev.to/yunwei37/ebpf-tutorial-building-an-egress-pacer-with-bpf-qdisc-4fna</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Testing Under Bandwidth Constraints
&lt;/h2&gt;

&lt;p&gt;Suppose you need to test how your application behaves when bandwidth is limited. You create a veth pair to simulate a network link and want to cap egress at 64 Kbit/s while observing what happens when the queue fills up and packets start dropping. Traditional approaches (complex tc configurations or user-space proxies) work but are heavyweight for a quick validation.&lt;/p&gt;

&lt;p&gt;Linux 6.16 introduced a new option: BPF qdisc. Instead of configuring existing schedulers, you can implement a complete queuing discipline directly in eBPF. This tutorial builds a FIFO rate limiter that demonstrates the full qdisc lifecycle: registering as the root qdisc, managing packet ownership through enqueue and dequeue, computing transmission times, using the watchdog timer for scheduling, and cleaning up when removed.&lt;/p&gt;

&lt;p&gt;This example is designed for controlled interfaces like veth, TAP, or IFB, environments where you have full control and can safely experiment with packet scheduling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Complete source code: &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/53-egress-pacer" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/53-egress-pacer&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why BPF Qdisc Exists
&lt;/h2&gt;

&lt;p&gt;To see what BPF qdisc changes, compare it with three common approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built-in schedulers are fixed algorithms.&lt;/strong&gt; tc provides TBF (Token Bucket Filter), HTB (Hierarchical Token Bucket), and others, but these are predefined behaviors in the kernel. If you need something custom (prioritizing by application type, adjusting rates based on real-time metrics), you must either patch the kernel or chain multiple tc classifiers together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TC BPF programs cannot control timing.&lt;/strong&gt; Regular TC BPF programs can inspect packets and decide whether to pass or drop them, but the underlying qdisc still controls when packets actually transmit. &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/20-tc" rel="noopener noreferrer"&gt;Lesson 20&lt;/a&gt; shows this pattern: BPF decides pass/drop, but not "send this packet at time T."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User-space solutions add overhead.&lt;/strong&gt; Tools like tc-netem with external helpers or DPDK can shape traffic, but they introduce context switches and deployment complexity. For testing, you often want something lightweight.&lt;/p&gt;

&lt;p&gt;BPF qdisc addresses these gaps. Starting with Linux 6.16, you can implement &lt;code&gt;Qdisc_ops&lt;/code&gt; callbacks (&lt;code&gt;enqueue&lt;/code&gt;, &lt;code&gt;dequeue&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;reset&lt;/code&gt;, &lt;code&gt;destroy&lt;/code&gt;) in eBPF and register them as a qdisc type. Your BPF program fully controls packet queuing and timing while staying in kernel space. The &lt;code&gt;egress_pacer&lt;/code&gt; in this tutorial is a FIFO rate limiter built this way.&lt;/p&gt;

&lt;h2&gt;
  
  
  How BPF Qdisc Works
&lt;/h2&gt;

&lt;p&gt;Unlike regular TC BPF programs that run within an existing qdisc, a BPF qdisc &lt;em&gt;is&lt;/em&gt; the qdisc. When you attach it to an interface's root position, your BPF code handles every egress packet.&lt;/p&gt;

&lt;p&gt;Here is the lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;: User space sets the rate and queue limit, loads the BPF program, registers the &lt;code&gt;struct_ops&lt;/code&gt; implementation, and attaches it to &lt;code&gt;TC_H_ROOT&lt;/code&gt; on the target interface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enqueue&lt;/strong&gt;: When the kernel needs to queue a packet for transmission, it calls your &lt;code&gt;enqueue&lt;/code&gt; callback. You receive an &lt;code&gt;skb&lt;/code&gt;, create a node to hold it, compute when it should transmit based on packet length and rate, and add the node to your queue. If the queue is full or allocation fails, you drop the packet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dequeue&lt;/strong&gt;: When the kernel wants to transmit, it calls your &lt;code&gt;dequeue&lt;/code&gt; callback. You check if the front packet's transmission time has arrived. If yes, return the &lt;code&gt;skb&lt;/code&gt;; if not, schedule a watchdog timer for the correct time and return NULL. The kernel will call &lt;code&gt;dequeue&lt;/code&gt; again when the timer fires.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reset&lt;/strong&gt;: When the qdisc is removed, the kernel calls &lt;code&gt;reset&lt;/code&gt;. You iterate through any remaining packets, release their resources, and zero the counters.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Throughout this lifecycle, your BPF program owns the packets. From receiving an &lt;code&gt;skb&lt;/code&gt; in &lt;code&gt;enqueue&lt;/code&gt;, through holding it in your data structure, to returning it in &lt;code&gt;dequeue&lt;/code&gt; or freeing it in &lt;code&gt;reset&lt;/code&gt;, the BPF program is responsible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnsjr659zb9zoj9j2jy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqnsjr659zb9zoj9j2jy8.png" alt="egress_pacer data flow: from configuration, attachment, enqueue, BPF FIFO, dequeue to transmit path, including policy-drop branch, watchdog loop, and reset lifecycle" width="800" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The solid lines trace a packet's normal path: configuration and attachment, then enqueue into the FIFO, then dequeue to the transmit path. When the queue is full or allocation fails, packets take the policy-drop branch. If dequeue is called before a packet's transmission time, the node is pushed back to the front, a watchdog is scheduled, and NULL is returned; the watchdog later re-enters dequeue. The dashed lines show the lifecycle path: removing the qdisc triggers reset, which frees queued packets and zeros qlen and backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Implementation
&lt;/h2&gt;

&lt;p&gt;The implementation consists of four files: a shared header defining statistics, a compatibility header providing BPF graph-object declarations, the BPF program implementing qdisc callbacks, and a user-space loader that manages the lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared Header
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;egress_pacer.h&lt;/code&gt; defines statistics shared between BPF and user space. These six counters categorize every packet outcome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef __EGRESS_PACER_H
#define __EGRESS_PACER_H
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pacer_stats&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;enqueued&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;dequeued&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;policy_dropped&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;cleanup_dropped&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;bytes_dequeued&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;max_qlen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cp"&gt;#endif &lt;/span&gt;&lt;span class="cm"&gt;/* __EGRESS_PACER_H */&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enqueued&lt;/code&gt; / &lt;code&gt;dequeued&lt;/code&gt;: Packets that successfully entered and left the FIFO.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policy_dropped&lt;/code&gt;: Packets dropped at enqueue because the queue was full or allocation failed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanup_dropped&lt;/code&gt;: Packets still in the queue when reset runs. These were queued but never transmitted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bytes_dequeued&lt;/code&gt;: Total bytes actually transmitted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_qlen&lt;/code&gt;: Peak queue depth observed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Compatibility Header
&lt;/h3&gt;

&lt;p&gt;BPF qdisc uses "graph-object" kfuncs, kernel functions for managing BPF-owned linked lists and objects. This local header provides the necessary declarations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* SPDX-License-Identifier: GPL-2.0 */&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef __EGRESS_PACER_BPF_EXPERIMENTAL_H
#define __EGRESS_PACER_BPF_EXPERIMENTAL_H
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_core_read.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#define __contains(name, node) \
    __attribute__((btf_decl_tag("contains:" #name ":" #node)))
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bpf_obj_new_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;local_type_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#define bpf_obj_new(type) \
    ((type *)bpf_obj_new_impl(bpf_core_type_id_local(type), NULL))
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;bpf_obj_drop_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;kptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#define bpf_obj_drop(kptr) bpf_obj_drop_impl(kptr, NULL)
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_list_push_front_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_head&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#define bpf_list_push_front(head, node) \
    bpf_list_push_front_impl(head, node, NULL, 0)
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_list_push_back_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_head&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#define bpf_list_push_back(head, node) \
    bpf_list_push_back_impl(head, node, NULL, 0)
&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="n"&gt;bpf_list_pop_front&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_head&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#endif &lt;/span&gt;&lt;span class="cm"&gt;/* __EGRESS_PACER_BPF_EXPERIMENTAL_H */&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These helpers let BPF code manage kernel objects safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bpf_obj_new&lt;/code&gt; allocates a new object of the given type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bpf_obj_drop&lt;/code&gt; frees it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bpf_list_push_back&lt;/code&gt; / &lt;code&gt;bpf_list_push_front&lt;/code&gt; / &lt;code&gt;bpf_list_pop_front&lt;/code&gt; implement a doubly-linked list.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;__contains&lt;/code&gt; macro generates a BTF tag telling the verifier what node type the list head contains, enabling safe ownership tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  BPF Program
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;egress_pacer.bpf.c&lt;/code&gt; implements the qdisc. The &lt;code&gt;SEC(".struct_ops")&lt;/code&gt; declaration registers this as a &lt;code&gt;Qdisc_ops&lt;/code&gt; implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"vmlinux.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_tracing.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"bpf_experimental.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"egress_pacer.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;rate_kbps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;queue_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;packet_node&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;eligible_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;packet_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sk_buff&lt;/span&gt; &lt;span class="n"&gt;__kptr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_node&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_spin_lock&lt;/span&gt; &lt;span class="n"&gt;queue_lock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_head&lt;/span&gt; &lt;span class="n"&gt;packet_queue&lt;/span&gt; &lt;span class="nf"&gt;__contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;next_departure_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"struct_ops/egress_pacer_enqueue"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_enqueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sk_buff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Qdisc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_sk_buff_ptr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;to_free&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;packet_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;eligible_ns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval_ns&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="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;packet_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qdisc_packet_len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qlen&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_obj_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;interval_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;packet_len&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000000ULL&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;rate_kbps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;packet_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;packet_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_kptr_xchg&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;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;bpf_spin_lock&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;queue_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;eligible_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_departure_ns&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;next_departure_ns&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;next_departure_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eligible_ns&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;eligible_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eligible_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bpf_list_push_back&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;packet_queue&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;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qlen&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;qstats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backlog&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;packet_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bpf_spin_unlock&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;queue_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NET_XMIT_SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nl"&gt;drop:&lt;/span&gt;
    &lt;span class="n"&gt;bpf_qdisc_skb_drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_free&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NET_XMIT_DROP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"struct_ops/egress_pacer_dequeue"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sk_buff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_dequeue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Qdisc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_list_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;packet_node&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sk_buff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;bpf_spin_lock&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;queue_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_list_pop_front&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;packet_queue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bpf_spin_unlock&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;queue_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;packet_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;eligible_ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bpf_spin_lock&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;queue_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;bpf_list_push_front&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;packet_queue&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;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;bpf_spin_unlock&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;queue_lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;bpf_qdisc_watchdog_schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;eligible_ns&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;skb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_kptr_xchg&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;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qlen&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;qstats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backlog&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;packet_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bpf_obj_drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/* reset drains packet_queue and frees every remaining skb. */&lt;/span&gt;
&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".struct_ops"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Qdisc_ops&lt;/span&gt; &lt;span class="n"&gt;pacer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enqueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_enqueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dequeue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_dequeue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_reset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destroy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;egress_pacer_destroy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"bpf_pacer"&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;h4&gt;
  
  
  Key data structures
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;const volatile&lt;/code&gt; variables &lt;code&gt;rate_kbps&lt;/code&gt; and &lt;code&gt;queue_limit&lt;/code&gt; live in &lt;code&gt;.rodata&lt;/code&gt;. User space writes them after &lt;code&gt;open()&lt;/code&gt; but before &lt;code&gt;load()&lt;/code&gt;, and the verifier treats them as compile-time constants thereafter.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;packet_node&lt;/code&gt; structure holds each queued packet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eligible_ns&lt;/code&gt;: The earliest time this packet may transmit (in nanoseconds).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packet_len&lt;/code&gt;: The packet's length for backlog accounting.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skb&lt;/code&gt;: A &lt;code&gt;__kptr&lt;/code&gt; field that owns the kernel socket buffer. The &lt;code&gt;__kptr&lt;/code&gt; annotation tells the verifier this field holds an owned kernel pointer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node&lt;/code&gt;: A &lt;code&gt;bpf_list_node&lt;/code&gt; for linking into the FIFO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;private(A)&lt;/code&gt; macro places the lock, list head, and departure timestamp in a private data section with proper alignment.&lt;/p&gt;

&lt;h4&gt;
  
  
  The enqueue callback
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;egress_pacer_enqueue&lt;/code&gt; receives a packet from the kernel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the packet length from &lt;code&gt;qdisc_skb_cb(skb)-&amp;gt;pkt_len&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the queue is at capacity, drop the packet.&lt;/li&gt;
&lt;li&gt;Allocate a &lt;code&gt;packet_node&lt;/code&gt;. If allocation fails, drop.&lt;/li&gt;
&lt;li&gt;Compute when this packet may transmit: &lt;code&gt;interval_ns = packet_len * 8 * 1000000 / rate_kbps&lt;/code&gt; (bits divided by kilobits/sec gives nanoseconds).&lt;/li&gt;
&lt;li&gt;Transfer ownership of the &lt;code&gt;skb&lt;/code&gt; to the node using &lt;code&gt;bpf_kptr_xchg&lt;/code&gt;. After this call, the node owns the &lt;code&gt;skb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Under the spin lock, set the packet's eligible time. If the queue was idle (&lt;code&gt;next_departure_ns&lt;/code&gt; is in the past), this packet can leave immediately; otherwise it waits behind the previous departure.&lt;/li&gt;
&lt;li&gt;Push the node onto the FIFO, update counters, unlock, and return success.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;bpf_qdisc_skb_drop&lt;/code&gt; call handles dropped packets: it adds the &lt;code&gt;skb&lt;/code&gt; to the kernel's free list and updates qdisc statistics.&lt;/p&gt;

&lt;h4&gt;
  
  
  The dequeue callback
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;egress_pacer_dequeue&lt;/code&gt; is called when the kernel wants a packet to transmit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pop the front node from the FIFO.&lt;/li&gt;
&lt;li&gt;If the queue is empty, return NULL.&lt;/li&gt;
&lt;li&gt;Check if the current time has reached &lt;code&gt;eligible_ns&lt;/code&gt;. If not, push the node back to the front, schedule a watchdog timer for &lt;code&gt;eligible_ns&lt;/code&gt;, and return NULL. The kernel will call dequeue again when the timer fires.&lt;/li&gt;
&lt;li&gt;If ready, extract the &lt;code&gt;skb&lt;/code&gt; from the node using &lt;code&gt;bpf_kptr_xchg&lt;/code&gt;, decrement counters, free the node, update statistics, and return the &lt;code&gt;skb&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The watchdog mechanism (&lt;code&gt;bpf_qdisc_watchdog_schedule&lt;/code&gt;) is how BPF qdisc implements transmission timing: you tell the kernel when to wake you, and it calls dequeue at that time.&lt;/p&gt;

&lt;h4&gt;
  
  
  The reset callback
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;egress_pacer_reset&lt;/code&gt; runs when the qdisc is removed. Any packets still in the queue must be freed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Loop through all nodes using &lt;code&gt;bpf_for&lt;/code&gt; (a BPF helper for bounded iteration).&lt;/li&gt;
&lt;li&gt;Pop each node, extract its &lt;code&gt;skb&lt;/code&gt;, and free it with &lt;code&gt;bpf_kfree_skb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Count freed packets in &lt;code&gt;cleanup_dropped&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Zero the departure timestamp and qdisc counters.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Registration
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;SEC(".struct_ops")&lt;/code&gt; block at the end registers the callbacks as a &lt;code&gt;Qdisc_ops&lt;/code&gt; structure. The &lt;code&gt;id&lt;/code&gt; field (&lt;code&gt;"bpf_pacer"&lt;/code&gt;) is the name user space uses to instantiate this qdisc.&lt;/p&gt;

&lt;h3&gt;
  
  
  User-Space Loader
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;egress_pacer.c&lt;/code&gt; handles command-line arguments, configures the BPF program, attaches the qdisc, waits for the specified duration, and cleans up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;net/if.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/libbpf.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"egress_pacer.skel.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;rate_kbps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;queue_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_kbps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/* Argument parsing, signal handling, and diagnostics are omitted here. */&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;wait_for_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;egress_pacer_bpf&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_tc_hook&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_TC_QDISC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TC_H_ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TC_H_MAKE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;16&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="n"&gt;qdisc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"bpf_pacer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;qdisc_created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ifindex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;if_nametoindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;skel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;egress_pacer_bpf__open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rate_kbps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_kbps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;queue_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;egress_pacer_bpf__load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;egress_pacer_bpf__attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bpf_tc_hook_create&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;hook&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;qdisc_created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"READY interface=%s rate_kbps=%llu queue_limit=%u duration=%u&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_kbps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;wait_for_duration&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qdisc_created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;bpf_tc_hook_destroy&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;hook&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SUMMARY enqueued=%llu dequeued=%llu policy_dropped=%llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enqueued&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dequeued&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy_dropped&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;egress_pacer_bpf__destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&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 loader follows a standard libbpf pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse arguments and verify the interface exists with &lt;code&gt;if_nametoindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open the skeleton and write rate/queue limit to &lt;code&gt;.rodata&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Load the BPF program (this verifies and JIT-compiles it).&lt;/li&gt;
&lt;li&gt;Attach the &lt;code&gt;struct_ops&lt;/code&gt; implementation, making &lt;code&gt;bpf_pacer&lt;/code&gt; available as a qdisc type.&lt;/li&gt;
&lt;li&gt;Create the root qdisc on the target interface with &lt;code&gt;bpf_tc_hook_create&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;bpf_tc_hook_create&lt;/code&gt; call has exclusive semantics: if the interface already has a root qdisc, the kernel returns &lt;code&gt;-EEXIST&lt;/code&gt; and the program exits without changing anything. For a root-qdisc conflict on the requested interface, the loader prints &lt;code&gt;tc qdisc show dev IFACE&lt;/code&gt; and the recovery command &lt;code&gt;sudo tc qdisc del dev IFACE root&lt;/code&gt;. A &lt;code&gt;struct_ops&lt;/code&gt; name conflict is global rather than interface-local, so the loader instead prints &lt;code&gt;tc qdisc show&lt;/code&gt; for every interface and does not guess which interface to modify. It never runs a destructive command automatically; remove a root qdisc only after confirming that the displayed &lt;code&gt;bpf_pacer&lt;/code&gt; entry is stale. This safety measure means the tool is best suited for dedicated interfaces where you control the configuration.&lt;/p&gt;

&lt;p&gt;After successful attachment, the program prints &lt;code&gt;READY&lt;/code&gt;, then polls every 100ms until duration expires or a signal arrives. Cleanup order matters: first &lt;code&gt;bpf_tc_hook_destroy&lt;/code&gt; (which triggers reset and frees queued packets), then read final statistics, then destroy the skeleton.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compilation and Execution
&lt;/h2&gt;

&lt;p&gt;Build from source:&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;cd &lt;/span&gt;src/53-egress-pacer
make clean
make &lt;span class="nt"&gt;-j2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick an interface you control and inspect its current root qdisc before attaching the pacer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tc qdisc show dev veth-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the interface is available for this experiment, start the pacer and let normal application traffic pass through it:&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; ./egress_pacer &lt;span class="nt"&gt;--interface&lt;/span&gt; veth-service &lt;span class="nt"&gt;--rate-kbps&lt;/span&gt; 64000 &lt;span class="nt"&gt;--queue-limit&lt;/span&gt; 256 &lt;span class="nt"&gt;--duration&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool prints &lt;code&gt;READY&lt;/code&gt; only after the qdisc is active. When the duration expires, its output has this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;READY interface=veth-service rate_kbps=64000 queue_limit=256 duration=30
SUMMARY enqueued=... dequeued=... policy_dropped=... cleanup_dropped=0 bytes_dequeued=... max_qlen=...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enqueued&lt;/code&gt; counts packets accepted by the qdisc, &lt;code&gt;dequeued&lt;/code&gt; counts packets released to the device, and &lt;code&gt;policy_dropped&lt;/code&gt; counts packets rejected when the queue was full. If the process is killed before normal cleanup and leaves &lt;code&gt;bpf_pacer&lt;/code&gt; behind, the next run prints qdisc state across all interfaces. Find the interface whose line contains &lt;code&gt;bpf_pacer&lt;/code&gt;, confirm that it is the stale instance, and then remove that actual root qdisc with &lt;code&gt;sudo tc qdisc del dev ACTUAL_IFACE root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Command-line options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usage: ./egress_pacer --interface IFACE [--rate-kbps KBPS] [--queue-limit PACKETS] [--duration SEC] [--verbose]

Options:
  -i, --interface IFACE       target interface (required)
  -r, --rate-kbps KBPS        egress rate in Kbit/s, 8-100000000 (default: 1024)
  -q, --queue-limit PACKETS   queue capacity in packets, 1-65535 (default: 256)
  -d, --duration SEC          how long to run, 1-86400 seconds (default: 10)
  -v, --verbose               print libbpf debug output
  -h, --help                  show help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you see &lt;code&gt;READY&lt;/code&gt;, the qdisc is installed and pacing traffic. When the duration ends (or you press Ctrl+C), the program removes the qdisc and prints &lt;code&gt;SUMMARY&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Requirements
&lt;/h3&gt;

&lt;p&gt;BPF qdisc requires Linux 6.16 or later. Your kernel must have BTF and BPF JIT enabled:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Minimum Version / Config&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux kernel&lt;/td&gt;
&lt;td&gt;6.16+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFIG_BPF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFIG_BPF_SYSCALL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFIG_BPF_JIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFIG_DEBUG_INFO_BTF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFIG_NET_SCHED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFIG_NET_SCH_BPF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libbpf&lt;/td&gt;
&lt;td&gt;1.6.0+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileges&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The repository includes libbpf and bpftool in &lt;code&gt;src/third_party&lt;/code&gt;. Use &lt;code&gt;tc&lt;/code&gt; from iproute2 to inspect the target interface and recover a stale root qdisc if necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This tutorial demonstrated BPF qdisc through a complete FIFO rate limiter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;enqueue&lt;/strong&gt; takes ownership of packets and schedules transmission times based on packet length and rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dequeue&lt;/strong&gt; uses the watchdog timer to wake at the right moment and returns packets to the kernel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reset&lt;/strong&gt; frees any packets still queued when the qdisc is removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These three phases (receiving packets, timing their release, and cleaning up) form a reusable pattern for custom scheduling disciplines.&lt;/p&gt;

&lt;p&gt;This particular scheduler implements a single aggregate FIFO with fixed rate and queue limit set at load time. It is intentionally simple, suited for validating correctness on controlled interfaces. A production scheduler could build on this foundation to add fairness, dynamic policies, or state persistence across restarts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To learn more about eBPF, check out our tutorial repository at &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial&lt;/a&gt; or visit our website at &lt;a href="https://eunomia.dev/tutorials/" rel="noopener noreferrer"&gt;https://eunomia.dev/tutorials/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/c8240344956e3f0b4e8f1d40ec3435e47040cacb" rel="noopener noreferrer"&gt;Linux BPF qdisc merge commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/7a2dafda950b78611dc441c83d105dfdc7082681" rel="noopener noreferrer"&gt;Qdisc watchdog follow-up commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libbpf/libbpf/commit/f580871b429c550edf910a1b0d700510245351df" rel="noopener noreferrer"&gt;libbpf qdisc TC hook support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libbpf/libbpf/releases/tag/v1.6.0" rel="noopener noreferrer"&gt;libbpf 1.6.0 release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/bpf_qdisc_fifo.c" rel="noopener noreferrer"&gt;Upstream BPF FIFO qdisc selftest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/prog_tests/bpf_qdisc.c" rel="noopener noreferrer"&gt;Upstream BPF qdisc test runner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libbpf/libbpf/releases/tag/v1.7.0" rel="noopener noreferrer"&gt;libbpf 1.7.0 release&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>eBPF Tutorial: Tracing Slow vfs_read Calls with fsession</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:57:27 +0000</pubDate>
      <link>https://dev.to/yunwei37/ebpf-tutorial-tracing-slow-vfsread-calls-with-fsession-48p5</link>
      <guid>https://dev.to/yunwei37/ebpf-tutorial-tracing-slow-vfsread-calls-with-fsession-48p5</guid>
      <description>&lt;p&gt;When a file-backed service shows read-latency spikes, application-level timing tells you that requests slowed down, but it cannot distinguish whether the kernel blocked on I/O or user-space logic took too long. The useful questions are: which thread issued the read, how many bytes did it request, what did the call return, and how long did that single &lt;code&gt;vfs_read&lt;/code&gt; invocation take?&lt;/p&gt;

&lt;p&gt;This tutorial demonstrates how to measure &lt;code&gt;vfs_read&lt;/code&gt; call latency using the &lt;strong&gt;fsession&lt;/strong&gt; mechanism introduced in Linux 7.0. fsession is a new eBPF program type that runs once at function entry and once at return, with built-in per-invocation storage for correlating the two phases. The tool we build timestamps function entry, computes latency at return, filters by process and threshold, and reports slow-read events through a ring buffer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Complete source: &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/52-fsession-latency" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/52-fsession-latency&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem: Correlating Function Entry and Return
&lt;/h2&gt;

&lt;p&gt;To measure how long a kernel function takes, you need to record a timestamp when it starts and compute the difference when it returns. This sounds simple, but the traditional approaches in eBPF all have drawbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traditional Approach: Two Programs with a Hash Map
&lt;/h3&gt;

&lt;p&gt;The most common pattern uses two separate BPF programs - one attached to function entry (fentry), one to function return (fexit). The entry program records the timestamp in a hash map keyed by thread ID; the return program looks up the timestamp, computes latency, and deletes the entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Entry program:                          Return program:
1. Get thread ID                        1. Get thread ID
2. Get timestamp                        2. Look up timestamp from map
3. Store timestamp in hash map          3. Compute latency
                                        4. Delete entry from map
                                        5. Report event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but has several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map overhead&lt;/strong&gt;: Every function call requires a hash map insertion at entry and a lookup plus deletion at return. For high-frequency functions like &lt;code&gt;vfs_read&lt;/code&gt;, this overhead adds up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State leaks&lt;/strong&gt;: If a thread is killed between entry and return (e.g., &lt;code&gt;kill -9&lt;/code&gt;), the hash map entry is never deleted and leaks memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No intrinsic relationship&lt;/strong&gt;: The two programs are completely independent. The only thing connecting them is the external hash map - there's no guarantee they're actually tracking the same function invocation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  kprobe/kretprobe: Same Pattern, Higher Overhead
&lt;/h3&gt;

&lt;p&gt;The kprobe mechanism has the same two-program structure and requires the same external hash map for correlation. Additionally, kprobes work through a software breakpoint mechanism (replacing the first instruction with an interrupt), which has higher overhead than fentry hooks that use the kernel's ftrace infrastructure and JIT-optimized call sequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  User-Space Sampling: Statistical, Not Precise
&lt;/h3&gt;

&lt;p&gt;Tools like &lt;code&gt;perf record&lt;/code&gt; sample stack traces periodically and can build statistical profiles of where time is spent. However, sampling cannot measure the latency of individual function calls. If you need to capture tail-latency events - the occasional 100ms read that causes a timeout - statistical sampling may miss them entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fsession Solution
&lt;/h2&gt;

&lt;p&gt;Linux 7.0 introduced &lt;strong&gt;fsession&lt;/strong&gt;, which solves the correlation problem at the kernel level. When you declare a BPF program with &lt;code&gt;SEC("fsession/vfs_read")&lt;/code&gt;, the kernel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calls your program once when &lt;code&gt;vfs_read&lt;/code&gt; enters&lt;/li&gt;
&lt;li&gt;Allocates an 8-byte scratch area (the "session cookie") tied to this specific invocation&lt;/li&gt;
&lt;li&gt;Calls your program again when &lt;code&gt;vfs_read&lt;/code&gt; returns&lt;/li&gt;
&lt;li&gt;Deallocates the session cookie&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight is that the session cookie is automatically managed and scoped to exactly one function invocation. Your program distinguishes entry from return using &lt;code&gt;bpf_session_is_return(ctx)&lt;/code&gt;, and reads or writes the cookie using &lt;code&gt;bpf_session_cookie(ctx)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Session Cookie Replaces
&lt;/h3&gt;

&lt;p&gt;In the traditional approach, you'd use a hash map like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Traditional: hash map keyed by thread ID&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_HASH&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10240&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// pid_tgid&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// timestamp&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With fsession, this entire map disappears. The timestamp lives in the session cookie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fsession: per-invocation cookie, no map needed&lt;/span&gt;
&lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_session_cookie&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;*&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cookie persists from entry to return and is automatically cleaned up afterward - no leaks possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Return-Phase Context Includes Function Arguments
&lt;/h3&gt;

&lt;p&gt;Another fsession advantage: the return-phase context includes both the original function arguments and the return value. In the traditional approach, if you need access to function arguments at return time (e.g., to include the requested byte count in your event), you must store them in the hash map at entry. With fsession, arguments are directly available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fsession/vfs_read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measure_vfs_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;loff_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// At return, 'count' and 'ret' are both available&lt;/span&gt;
    &lt;span class="c1"&gt;// No need to store 'count' anywhere at entry&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code Implementation
&lt;/h2&gt;

&lt;p&gt;This tool consists of three files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fsession_latency.h&lt;/code&gt;: Shared data structures for BPF and user space&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsession_latency.bpf.c&lt;/code&gt;: The BPF program that measures latency&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsession_latency.c&lt;/code&gt;: User-space loader that manages the BPF lifecycle and prints results&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Shared Header
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fsession_latency.h&lt;/code&gt; defines the event structure sent through the ring buffer and the aggregate statistics structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef __FSESSION_LATENCY_H
#define __FSESSION_LATENCY_H
&lt;/span&gt;
&lt;span class="cp"&gt;#define FSESSION_COMM_LEN 16
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;latency_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;device_major&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;device_minor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&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;FSESSION_COMM_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_stats&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cp"&gt;#endif &lt;/span&gt;&lt;span class="cm"&gt;/* __FSESSION_LATENCY_H */&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The four counters track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;calls&lt;/code&gt;: Total &lt;code&gt;vfs_read&lt;/code&gt; invocations observed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slow&lt;/code&gt;: Calls that met or exceeded the latency threshold&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;errors&lt;/code&gt;: Calls where &lt;code&gt;vfs_read&lt;/code&gt; returned a negative error code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dropped&lt;/code&gt;: Events that couldn't be submitted because the ring buffer was full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;FSESSION_COMM_LEN&lt;/code&gt; is 16 to match the kernel's &lt;code&gt;TASK_COMM_LEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each event also carries &lt;code&gt;device_major&lt;/code&gt;, &lt;code&gt;device_minor&lt;/code&gt;, &lt;code&gt;inode&lt;/code&gt;, and &lt;code&gt;mode&lt;/code&gt;. The BPF program splits the kernel's raw &lt;code&gt;s_dev&lt;/code&gt; encoding into its 12-bit major and 20-bit minor fields. Combined with &lt;code&gt;i_ino&lt;/code&gt;, this gives a stable VFS object identity; &lt;code&gt;i_mode&lt;/code&gt; lets user space print &lt;code&gt;regular&lt;/code&gt;, &lt;code&gt;fifo&lt;/code&gt;, &lt;code&gt;character&lt;/code&gt;, and other object types.&lt;/p&gt;

&lt;h3&gt;
  
  
  BPF Program
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fsession_latency.bpf.c&lt;/code&gt; is the core of the tool. Here is the complete kernel-side program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#define bpf_session_is_return bpf_session_is_return_vmlinux_snapshot
#define bpf_session_cookie bpf_session_cookie_vmlinux_snapshot
#include&lt;/span&gt; &lt;span class="cpf"&gt;"vmlinux.h"&lt;/span&gt;&lt;span class="cp"&gt;
#undef bpf_session_is_return
#undef bpf_session_cookie
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_core_read.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_tracing.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"fsession_latency.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#define KERNEL_MINOR_BITS 20
#define KERNEL_MINOR_MASK ((1U &amp;lt;&amp;lt; KERNEL_MINOR_BITS) - 1)
&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;LICENSE&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;threshold_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;target_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_stats&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_RINGBUF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cm"&gt;/*
 * The repository vmlinux.h snapshot predates the ctx argument on these
 * kfunc prototypes. Rename those stale declarations while including the
 * snapshot, then provide the Linux 7.0 signatures below.
 */&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bpf_session_is_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&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;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bpf_session_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&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;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fsession/vfs_read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measure_vfs_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;loff_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_session_cookie&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="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bpf_session_is_return&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_tgid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&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="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_tgid&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="n"&gt;started&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;return&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="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&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;0&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="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!*&lt;/span&gt;&lt;span class="n"&gt;started&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&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;started&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold_ns&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow&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="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ringbuf_reserve&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;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropped&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="k"&gt;return&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="n"&gt;__builtin_memset&lt;/span&gt;&lt;span class="p"&gt;(&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;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&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="n"&gt;__u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;pid_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&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="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;latency_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_inode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_sb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s_dev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_major&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;KERNEL_MINOR_BITS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_minor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;KERNEL_MINOR_MASK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_ino&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_mode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;bpf_get_current_comm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;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;bpf_ringbuf_submit&lt;/span&gt;&lt;span class="p"&gt;(&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;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The walkthrough below explains the same program section by section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#define bpf_session_is_return bpf_session_is_return_vmlinux_snapshot
#define bpf_session_cookie bpf_session_cookie_vmlinux_snapshot
#include&lt;/span&gt; &lt;span class="cpf"&gt;"vmlinux.h"&lt;/span&gt;&lt;span class="cp"&gt;
#undef bpf_session_is_return
#undef bpf_session_cookie
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_core_read.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_tracing.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"fsession_latency.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#define KERNEL_MINOR_BITS 20
#define KERNEL_MINOR_MASK ((1U &amp;lt;&amp;lt; KERNEL_MINOR_BITS) - 1)
&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;LICENSE&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The macro dance at the top is a compatibility workaround. The repository's &lt;code&gt;vmlinux.h&lt;/code&gt; snapshot was generated before Linux 7.0 added a &lt;code&gt;ctx&lt;/code&gt; argument to &lt;code&gt;bpf_session_is_return&lt;/code&gt; and &lt;code&gt;bpf_session_cookie&lt;/code&gt;. The macros rename the old declarations during the include, then we provide the correct signatures below. A &lt;code&gt;vmlinux.h&lt;/code&gt; regenerated from kernel 7.0+ wouldn't need this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;threshold_ns&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;target_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_stats&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_RINGBUF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&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;const volatile&lt;/code&gt; variables in BPF programs have special semantics. They're placed in the &lt;code&gt;.rodata&lt;/code&gt; section and can be set by user space after opening the skeleton but before loading. Once loaded, the verifier treats them as compile-time constants, enabling optimizations like dead-code elimination when &lt;code&gt;target_tgid&lt;/code&gt; is 0.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stats&lt;/code&gt; is a global variable in the &lt;code&gt;.bss&lt;/code&gt; section, directly readable from user space after the program runs.&lt;/p&gt;

&lt;p&gt;The ring buffer (&lt;code&gt;events&lt;/code&gt;) is sized at 256 KB - enough for thousands of events before overflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/*
 * The repository vmlinux.h snapshot predates the ctx argument on these
 * kfunc prototypes. Rename those stale declarations while including the
 * snapshot, then provide the Linux 7.0 signatures below.
 */&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bpf_session_is_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&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;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bpf_session_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&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;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;strong&gt;kfunc&lt;/strong&gt; declarations - kernel functions exported for BPF programs to call. The &lt;code&gt;__ksym&lt;/code&gt; attribute tells the verifier to resolve these symbols from the running kernel at load time, rather than expecting them to be defined in the BPF object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fsession/vfs_read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measure_vfs_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;loff_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_get_current_pid_tgid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_session_cookie&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="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SEC("fsession/vfs_read")&lt;/code&gt; tells the kernel this is an fsession program attached to &lt;code&gt;vfs_read&lt;/code&gt;. The &lt;code&gt;BPF_PROG&lt;/code&gt; macro expands to set up the standard tracing context; &lt;code&gt;ctx&lt;/code&gt; is implicitly available for passing to kfuncs.&lt;/p&gt;

&lt;p&gt;The function signature lists &lt;code&gt;vfs_read&lt;/code&gt;'s parameters followed by its return value. At entry, &lt;code&gt;ret&lt;/code&gt; is undefined; at return, all parameters and the return value are valid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bpf_session_is_return&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_tgid&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&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="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_tgid&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="n"&gt;started&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;return&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="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&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;0&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;strong&gt;Entry phase&lt;/strong&gt;: First, check if we should filter this call. If &lt;code&gt;target_tgid&lt;/code&gt; is set (non-zero) and the current process's TGID doesn't match, write 0 to the cookie to mark this invocation as "skip" and return. Otherwise, write the current monotonic timestamp to the cookie.&lt;/p&gt;

&lt;p&gt;The TGID is in the upper 32 bits of &lt;code&gt;bpf_get_current_pid_tgid()&lt;/code&gt;'s return value; the lower 32 bits are the thread ID (PID in kernel terms).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!*&lt;/span&gt;&lt;span class="n"&gt;started&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ktime_get_ns&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;started&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold_ns&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;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;strong&gt;Return phase&lt;/strong&gt;: If the cookie is 0, the entry phase filtered this call - return immediately. Otherwise, compute latency and update the aggregate counters. The &lt;code&gt;__sync_fetch_and_add&lt;/code&gt; provides atomic updates since multiple CPUs may execute this program concurrently.&lt;/p&gt;

&lt;p&gt;If the latency is below the threshold, we're done - the call is counted but no event is emitted. This keeps the ring buffer focused on slow calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;    &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow&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="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ringbuf_reserve&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;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;__sync_fetch_and_add&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropped&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="k"&gt;return&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="n"&gt;__builtin_memset&lt;/span&gt;&lt;span class="p"&gt;(&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;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&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="n"&gt;__u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;pid_tgid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pid_tgid&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="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;latency_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_inode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_sb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s_dev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_major&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;KERNEL_MINOR_BITS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_minor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;KERNEL_MINOR_MASK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_ino&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_mode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;bpf_get_current_comm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;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;bpf_ringbuf_submit&lt;/span&gt;&lt;span class="p"&gt;(&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;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For slow calls, increment the &lt;code&gt;slow&lt;/code&gt; counter and try to reserve space in the ring buffer. If the reservation fails (buffer full), increment &lt;code&gt;dropped&lt;/code&gt; so the user knows events were lost. On success, zero the event, copy call fields, and read object identity from &lt;code&gt;file-&amp;gt;f_inode&lt;/code&gt; before submitting it.&lt;/p&gt;

&lt;p&gt;Note that both &lt;code&gt;count&lt;/code&gt; and &lt;code&gt;file&lt;/code&gt; are available directly - no need to store either at entry. This is the fsession advantage: function arguments persist to the return phase. The tool intentionally does not resolve a path; paths can be renamed or have multiple aliases. For a regular file, use the device to identify the mount and search it by inode, for example &lt;code&gt;find /mount -xdev -inum INODE -print&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  User-Space Loader
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fsession_latency.c&lt;/code&gt; handles command-line parsing, BPF lifecycle management, and event consumption. The key sections:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration via read-only data&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;threshold_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold_us&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_tgid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After opening the skeleton but before loading, user space writes the threshold (converted from microseconds to nanoseconds) and target TGID to the &lt;code&gt;.rodata&lt;/code&gt; section. These become constants in the BPF program.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ring buffer consumption&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;latency_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EVENT comm=%-16s tgid=%u pid=%u object=%u:%u:%llu type=%s "&lt;/span&gt;
           &lt;span class="s"&gt;"requested=%llu result=%lld latency_us=%llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;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;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tgid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;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;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_major&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;device_minor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;file_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;latency_ns&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;events_printed&lt;/span&gt;&lt;span class="o"&gt;++&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;0&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;Each event is printed with the process name, IDs, VFS identity and type, requested bytes, return value, and latency in microseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean shutdown sequence&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;fsession_latency_bpf__detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ring_buffer__consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... error handling ...&lt;/span&gt;

&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SUMMARY calls=%llu slow=%llu errors=%llu dropped=%llu events=%llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events_printed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shutdown order matters for correctness:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detach the BPF program (stops new events from being generated)&lt;/li&gt;
&lt;li&gt;Drain remaining events from the ring buffer&lt;/li&gt;
&lt;li&gt;Read the final counter values (now stable since the program is detached)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures &lt;code&gt;events_printed&lt;/code&gt; matches the events actually delivered through the ring buffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compilation and Execution
&lt;/h2&gt;

&lt;p&gt;Build from source (the repository vendors libbpf 1.7.0 and bpftool v7.7.0):&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;cd &lt;/span&gt;src/52-fsession-latency
make clean
make &lt;span class="nt"&gt;-j2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trace a specific service process for 30 seconds, reporting reads of 10 ms or longer:&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="nv"&gt;SERVICE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-n&lt;/span&gt; my-service&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./fsession_latency &lt;span class="nt"&gt;--pid&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--threshold-us&lt;/span&gt; 10000 &lt;span class="nt"&gt;--duration&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note about PID namespaces&lt;/strong&gt;: The &lt;code&gt;--pid&lt;/code&gt; option compares against the TGID in the host (initial) PID namespace. If your target runs in a container with its own PID namespace, you need to find its host-visible TGID. Inside the container the process might be PID 1, but from the host it could be PID 12345. Use &lt;code&gt;pgrep&lt;/code&gt; on the host or inspect &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt; for the &lt;code&gt;NSpid&lt;/code&gt; line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Command-Line Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usage: ./fsession_latency [--threshold-us USEC] [--duration SEC] [--pid TGID] [--verbose]

Options:
  -t, --threshold-us USEC  slow-read threshold (default: 1000 microseconds)
  -d, --duration SEC       trace duration, 1-86400 (default: 10 seconds)
  -p, --pid TGID           trace a specific process ID (default: all processes)
  -v, --verbose            print libbpf diagnostics
  -h, --help               show this help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example Output
&lt;/h3&gt;

&lt;p&gt;For example, a Python service waiting on a FIFO can produce this output when the writer responds after 50 ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;Tracing vfs_read for 1 seconds;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000 us&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;selected
&lt;span class="go"&gt;EVENT comm=python3          tgid=1245 pid=1245 object=0:16:784 type=fifo requested=1 result=1 latency_us=50246
SUMMARY calls=66 slow=1 errors=0 dropped=0 events=1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SUMMARY&lt;/code&gt; line shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;66 total &lt;code&gt;vfs_read&lt;/code&gt; calls were observed&lt;/li&gt;
&lt;li&gt;1 was slow (met the threshold)&lt;/li&gt;
&lt;li&gt;0 returned errors&lt;/li&gt;
&lt;li&gt;0 events were dropped&lt;/li&gt;
&lt;li&gt;1 event was printed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Environment Requirements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel version&lt;/td&gt;
&lt;td&gt;Linux 7.0+ (fsession first introduced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTF&lt;/td&gt;
&lt;td&gt;Must be enabled (&lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CONFIG_BPF=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_SYSCALL=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_JIT=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_EVENTS=y&lt;/code&gt;, &lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt;, &lt;code&gt;CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS=y&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BPF JIT&lt;/td&gt;
&lt;td&gt;Must be enabled at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Tested on x86_64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege&lt;/td&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The upstream merge commit is &lt;code&gt;f17b474e36647c23801ef8fdaf2255ab66dd2973&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending This Tool
&lt;/h2&gt;

&lt;p&gt;The fsession pattern demonstrated here applies to any kernel function where you need to correlate entry and return. Some directions for extension:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Additional functions&lt;/strong&gt;: Attach to &lt;code&gt;vfs_write&lt;/code&gt;, &lt;code&gt;vfs_fsync&lt;/code&gt;, or other VFS operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File path filtering&lt;/strong&gt;: Use &lt;code&gt;file-&amp;gt;f_path&lt;/code&gt; to filter by specific files or mount points&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack traces&lt;/strong&gt;: Add &lt;code&gt;bpf_get_stackid()&lt;/code&gt; to capture kernel and/or user stack traces for slow calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Histograms&lt;/strong&gt;: Replace per-event reporting with latency histograms using &lt;code&gt;BPF_MAP_TYPE_ARRAY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cgroup filtering&lt;/strong&gt;: Add cgroup ID checks for container-aware tracing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;dropped&lt;/code&gt; counter remains the signal for ring buffer pressure - if it's growing, either increase the buffer size or raise the threshold to reduce event volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This tutorial demonstrated how to measure kernel function latency using the fsession mechanism in Linux 7.0. The key advantages over the traditional fentry/fexit with hash map approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single program&lt;/strong&gt;: One BPF program handles both entry and return, with &lt;code&gt;bpf_session_is_return()&lt;/code&gt; distinguishing the phases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in correlation&lt;/strong&gt;: The 8-byte session cookie replaces the external hash map for passing data between phases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No state leaks&lt;/strong&gt;: The kernel manages cookie lifecycle - no cleanup required, no leaks possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arguments available at return&lt;/strong&gt;: Function parameters persist to the return phase without explicit storage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For this &lt;code&gt;vfs_read&lt;/code&gt; example, that last property also lets the return phase report the VFS object's device, inode, and type. Treat the result as generic VFS-call latency; use the object identity for follow-up rather than assuming every slow event is a disk problem.&lt;/p&gt;

&lt;p&gt;The pattern is simple: check &lt;code&gt;bpf_session_is_return()&lt;/code&gt;, use &lt;code&gt;bpf_session_cookie()&lt;/code&gt; for per-invocation state, and access arguments/return value directly. This applies wherever you need to correlate function entry and exit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To learn more about eBPF, check out our tutorial repository at &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial&lt;/a&gt; or visit &lt;a href="https://eunomia.dev/tutorials/" rel="noopener noreferrer"&gt;https://eunomia.dev/tutorials/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/f17b474e36647c23801ef8fdaf2255ab66dd2973" rel="noopener noreferrer"&gt;Linux fsession merge commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/progs/fsession_test.c" rel="noopener noreferrer"&gt;Upstream fsession program selftest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/v7.1/tools/testing/selftests/bpf/prog_tests/fsession_test.c" rel="noopener noreferrer"&gt;Upstream fsession runner selftest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libbpf/libbpf/releases/tag/v1.7.0" rel="noopener noreferrer"&gt;libbpf 1.7.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libbpf/bpftool/releases/tag/v7.7.0" rel="noopener noreferrer"&gt;bpftool v7.7.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>linux</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>eBPF Tutorial: Precisely Isolating Established TCP Connections</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:48:55 +0000</pubDate>
      <link>https://dev.to/yunwei37/ebpf-tutorial-precisely-isolating-established-tcp-connections-56ld</link>
      <guid>https://dev.to/yunwei37/ebpf-tutorial-precisely-isolating-established-tcp-connections-56ld</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;An outbound IP address has just been added to a threat-intelligence blocklist. Your firewall now rejects new connections to that address. But when you check &lt;code&gt;netstat&lt;/code&gt;, you find a server already has an active TCP session to the same destination. You need to close that exact connection immediately, without killing the process, disrupting unrelated traffic, or waiting for the session to time out.&lt;/p&gt;

&lt;p&gt;This seemingly simple requirement is surprisingly difficult to achieve with traditional tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Killing the process&lt;/strong&gt; disrupts all its connections, not just the problematic one. Worse, the process may immediately reconnect to the same destination after restarting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Firewall rules&lt;/strong&gt; block new connections but have no effect on sessions that are already established. Even &lt;code&gt;iptables -m state --state ESTABLISHED&lt;/code&gt; can only match existing connections for filtering future packets; it cannot tear down an active session.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User-space tools&lt;/strong&gt; like &lt;code&gt;tcpkill&lt;/code&gt; work by observing network packets and then injecting TCP RST packets. This approach suffers from a race condition: the socket state can change between observing the connection and sending the RST. RST injection also requires guessing the correct sequence number and may fail against encrypted tunnels or certain network configurations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we need is a kernel-level mechanism that can identify and destroy specific sockets atomically, with no race window. This is exactly what Linux 6.5 introduced with the &lt;code&gt;bpf_sock_destroy&lt;/code&gt; kernel function (kfunc).&lt;/p&gt;

&lt;p&gt;This tutorial builds a command-line tool that uses a BPF iterator to walk the kernel's TCP socket table, find established connections matching a specific IPv4 address and port, and destroy them on demand. You will learn how BPF iterators provide safe, locked traversal of kernel data structures, and how kfuncs like &lt;code&gt;bpf_sock_destroy&lt;/code&gt; expose kernel operations to BPF programs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Complete source code: &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/51-tcp-quarantine" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/51-tcp-quarantine&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Background: BPF Iterators and Kfuncs
&lt;/h2&gt;

&lt;p&gt;Before diving into the code, let's understand the two kernel features that make this tool possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  BPF Iterators
&lt;/h3&gt;

&lt;p&gt;A BPF iterator is a special type of BPF program that the kernel invokes repeatedly, once for each element in some kernel data structure. Unlike tracepoints or kprobes that fire when specific events occur, iterators let you actively scan kernel state on demand.&lt;/p&gt;

&lt;p&gt;The TCP iterator (&lt;code&gt;SEC("iter/tcp")&lt;/code&gt;) iterates over all TCP sockets in the network namespace of the process that triggers it. User space initiates a scan by reading from an iterator file descriptor. Each &lt;code&gt;read()&lt;/code&gt; call causes the kernel to invoke your BPF callback for a batch of sockets. When &lt;code&gt;read()&lt;/code&gt; returns zero (EOF), the traversal is complete.&lt;/p&gt;

&lt;p&gt;Because the kernel controls the iteration and holds appropriate locks while invoking your callback, there's no race condition between reading socket state and acting on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kfuncs
&lt;/h3&gt;

&lt;p&gt;Kfuncs (kernel functions) are a mechanism for BPF programs to call specific kernel functions directly. Unlike the older BPF helper functions which have a fixed ABI, kfuncs are regular kernel functions that are explicitly marked as callable from BPF. They can do things that helpers cannot, including operations that modify kernel state in complex ways.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bpf_sock_destroy&lt;/code&gt; kfunc, introduced in Linux 6.5 (commit &lt;code&gt;4ddbcb886268af8d12a23e6640b39d1d9c652b1b&lt;/code&gt;), allows a BPF program running in an iterator context to forcibly close a socket. The kernel executes the full protocol-specific teardown: removing the socket from hash tables, sending FIN/RST as appropriate, and releasing resources. This happens synchronously: by the time the call returns, the socket is destroyed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Tool Works
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;tcp_quarantine&lt;/code&gt; tool combines these two features into a practical workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User space specifies a remote IPv4 address and port. By default this is a dry run; apply mode also requires one local IPv4 address and port copied from the dry-run output.&lt;/li&gt;
&lt;li&gt;The BPF program is loaded with these parameters baked into its read-only data section.&lt;/li&gt;
&lt;li&gt;User space opens an iterator file descriptor and reads from it, triggering the kernel to invoke the BPF callback for each TCP socket.&lt;/li&gt;
&lt;li&gt;For each socket, the BPF callback checks: Is this an IPv4 socket? Is it in the ESTABLISHED state? Does its remote endpoint match? In apply mode, does its local endpoint also match?&lt;/li&gt;
&lt;li&gt;Matching sockets are printed and counted. Only an apply scan with all four endpoint fields equal calls &lt;code&gt;bpf_sock_destroy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;After traversal completes, user space reads statistics from the BPF program's BSS section and reports results.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final match and destroy happen in one iterator callback on the same socket. Dry-run and apply are separate scans, so a connection can disappear between them; in that case apply safely reports &lt;code&gt;matched=0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Walkthrough
&lt;/h2&gt;

&lt;p&gt;The implementation has three source files: a shared header defining the statistics structure, the BPF program that performs the actual iteration and destruction, and a user-space loader that orchestrates everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Statistics Structure
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tcp_quarantine.h&lt;/code&gt; defines counters that the BPF program updates during iteration and user space reads afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef __TCP_QUARANTINE_H
#define __TCP_QUARANTINE_H
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;quarantine_stats&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;scanned&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;established&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;destroyed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cp"&gt;#endif &lt;/span&gt;&lt;span class="cm"&gt;/* __TCP_QUARANTINE_H */&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These five counters track: total sockets examined, how many were IPv4 and ESTABLISHED, how many matched the active selector, how many were successfully destroyed (apply mode only), and how many &lt;code&gt;bpf_sock_destroy&lt;/code&gt; calls failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  BPF Iterator Program
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tcp_quarantine.bpf.c&lt;/code&gt; is the kernel-side code. The &lt;code&gt;SEC("iter/tcp")&lt;/code&gt; annotation tells the loader this is a TCP iterator program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"vmlinux.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_core_read.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_endian.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"tcp_quarantine.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#define AF_INET 2
#define TCP_ESTABLISHED 1
&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;LICENSE&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;target_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="n"&gt;target_port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;target_local_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="n"&gt;target_local_port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;quarantine_stats&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpf_sock_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sock_common&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;__ksym&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"iter/tcp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;quarantine_tcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_iter__tcp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sock_common&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sk_common&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;seq_file&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;dst_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;dst_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="n"&gt;dst_port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="n"&gt;local_port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u8&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;sk&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scanned&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skc_family&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skc_state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;family&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;AF_INET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;TCP_ESTABLISHED&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;established&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;dst_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skc_daddr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;dst_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skc_dport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst_addr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_addr&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;dst_port&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;bpf_htons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_port&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;local_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skc_rcv_saddr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;local_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_CORE_READ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skc_num&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_addr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_local_addr&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
              &lt;span class="n"&gt;local_port&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_local_port&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;local_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ntohl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_addr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;dst_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ntohl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst_addr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;BPF_SEQ_PRINTF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="s"&gt;"MATCH local=%u.%u.%u.%u:%u remote=%u.%u.%u.%u:%u&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;local_host&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_host&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_host&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_host&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;dst_host&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst_host&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst_host&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst_host&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;apply&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_sock_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destroyed&lt;/span&gt;&lt;span class="o"&gt;++&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;0&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;Let's break down the key elements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration variables&lt;/strong&gt;: The five &lt;code&gt;const volatile&lt;/code&gt; variables hold the remote endpoint, optional local endpoint, and apply mode in the BPF program's read-only data section (&lt;code&gt;.rodata&lt;/code&gt;). User space writes them after opening the skeleton but before loading the program. The verifier can eliminate the local-match and destruction path when &lt;code&gt;apply&lt;/code&gt; is false.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kfunc declaration&lt;/strong&gt;: The line &lt;code&gt;extern int bpf_sock_destroy(struct sock_common *sock) __ksym;&lt;/code&gt; declares &lt;code&gt;bpf_sock_destroy&lt;/code&gt; as an external kernel symbol. The &lt;code&gt;__ksym&lt;/code&gt; annotation tells the loader to resolve this at load time by looking up the kernel function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CO-RE field access&lt;/strong&gt;: &lt;code&gt;BPF_CORE_READ(sk, skc_family)&lt;/code&gt; reads the &lt;code&gt;skc_family&lt;/code&gt; field from the socket structure using BTF (BPF Type Format) information. This is part of CO-RE (Compile Once, Run Everywhere): the compiled BPF program contains relocation records that the loader patches based on the running kernel's BTF data. A program compiled on one kernel version will work on others even if structure field offsets differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Byte order handling&lt;/strong&gt;: Remote and local addresses match the network byte order produced by &lt;code&gt;inet_pton&lt;/code&gt;. The remote &lt;code&gt;skc_dport&lt;/code&gt; is in network byte order and is compared with &lt;code&gt;bpf_htons(target_port)&lt;/code&gt;; local &lt;code&gt;skc_num&lt;/code&gt; is already in host byte order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive filtering&lt;/strong&gt;: The callback first counts all sockets (&lt;code&gt;scanned&lt;/code&gt;), then IPv4 ESTABLISHED sockets (&lt;code&gt;established&lt;/code&gt;), then remote matches. Dry-run prints every candidate as &lt;code&gt;MATCH local=... remote=...&lt;/code&gt;. Apply mode rechecks the selected local endpoint before incrementing &lt;code&gt;matched&lt;/code&gt; or destroying anything. There is deliberately no option that destroys every remote match.&lt;/p&gt;

&lt;h3&gt;
  
  
  User-Space Loader
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tcp_quarantine.c&lt;/code&gt; handles argument parsing, BPF lifecycle management, and result reporting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;arpa/inet.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/libbpf.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"tcp_quarantine.skel.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;remote_argument&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;local_argument&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;in_addr&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INET_ADDRSTRLEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/* Argument validation and endpoint parsing are omitted here. */&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;parse_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;run_iterator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_program&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_link&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;iter_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_program__attach_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;iter_fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_iter_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_link__fd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)))&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="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&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="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter_fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bpf_link__destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;errno&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="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;tcp_quarantine_bpf&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

    &lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;parse_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remote_argument&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;remote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;parse_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_argument&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;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;skel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tcp_quarantine_bpf__open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_local_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_local_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rodata&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;tcp_quarantine_bpf__load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;run_iterator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;progs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quarantine_tcp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SUMMARY mode=%s matched=%llu destroyed=%llu failed=%llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"apply"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"dry-run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destroyed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;tcp_quarantine_bpf__destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&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 key workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open the skeleton&lt;/strong&gt;: &lt;code&gt;tcp_quarantine_bpf__open()&lt;/code&gt; parses the embedded BPF object but doesn't load it yet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configure parameters&lt;/strong&gt;: We write the parsed remote endpoint, optional local endpoint, and apply flag to &lt;code&gt;skel-&amp;gt;rodata-&amp;gt;*&lt;/code&gt; before &lt;code&gt;load()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load the program&lt;/strong&gt;: &lt;code&gt;tcp_quarantine_bpf__load()&lt;/code&gt; loads the BPF program into the kernel. The verifier runs, BTF relocations are applied, and kfuncs are resolved.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the iterator&lt;/strong&gt;: &lt;code&gt;run_iterator()&lt;/code&gt; attaches the program, creates an iterator file descriptor, and reads from it until EOF. The read loop drives the kernel to invoke our BPF callback for every TCP socket.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read results&lt;/strong&gt;: After iteration, we read statistics from &lt;code&gt;skel-&amp;gt;bss-&amp;gt;stats&lt;/code&gt;. The BSS section is automatically memory-mapped, so these reads just access shared memory.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The iterator callback writes each candidate with &lt;code&gt;BPF_SEQ_PRINTF&lt;/code&gt;. The &lt;code&gt;read()&lt;/code&gt; loop copies that iterator output to stdout, making the dry run actionable; aggregate results still come from BSS statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Namespace Boundary
&lt;/h2&gt;

&lt;p&gt;The TCP BPF iterator scans sockets in the network namespace of the process that triggers it. This is a critical boundary to understand: the tool can only see and operate on connections within its own network namespace.&lt;/p&gt;

&lt;p&gt;If you need to close a connection inside a container or another namespace, first enter that namespace using &lt;code&gt;nsenter&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="nb"&gt;sudo &lt;/span&gt;nsenter &lt;span class="nt"&gt;--net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/&amp;lt;target-pid&amp;gt;/ns/net &lt;span class="nt"&gt;--&lt;/span&gt; ./tcp_quarantine &lt;span class="se"&gt;\&lt;/span&gt;
  203.0.113.99:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;&amp;lt;target-pid&amp;gt;&lt;/code&gt; is the PID of any process in the target network namespace. The &lt;code&gt;--net&lt;/code&gt; flag changes only the network namespace while keeping the mount namespace intact, so the tool binary remains accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Applications See
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;bpf_sock_destroy&lt;/code&gt; tears down a socket, the kernel executes the protocol's shutdown path. The specific error an application sees depends on which end of the connection is destroyed and what operation it next attempts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The upstream TCP selftest expects &lt;code&gt;ECONNABORTED&lt;/code&gt; for the client and &lt;code&gt;ECONNRESET&lt;/code&gt; for the server.&lt;/li&gt;
&lt;li&gt;Writes may produce &lt;code&gt;EPIPE&lt;/code&gt; with a &lt;code&gt;SIGPIPE&lt;/code&gt; signal.&lt;/li&gt;
&lt;li&gt;The exact behavior varies by kernel version and the timing of operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat any of these outcomes as application-visible evidence that the connection was torn down; confirm the exact behavior expected by your application and kernel version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and Running
&lt;/h2&gt;

&lt;p&gt;Build the tool:&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;cd &lt;/span&gt;src/51-tcp-quarantine
make clean
make &lt;span class="nt"&gt;-j2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dry-run mode&lt;/strong&gt; (the default) scans for matches but doesn't destroy anything:&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; ./tcp_quarantine 127.0.0.1:42063
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy one &lt;code&gt;MATCH&lt;/code&gt; line's local endpoint into apply mode. This destroys only that complete 4-tuple:&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; ./tcp_quarantine &lt;span class="nt"&gt;--apply&lt;/span&gt; 127.0.0.1:55490 127.0.0.1:42063
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Command-line reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usage: ./tcp_quarantine [--apply LOCAL_IPV4:PORT] REMOTE_IPV4:PORT [--verbose]

Options:
  -a, --apply IPv4:PORT   local endpoint selected from dry-run output
  -v, --verbose           print libbpf diagnostics
  -h, --help              show this help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example Session
&lt;/h3&gt;

&lt;p&gt;Suppose a service has two established connections to the same remote listener. A normal dry-run lists both candidates; copying one local endpoint into the apply command selects only that connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;MATCH local=127.0.0.1:55490 remote=127.0.0.1:42063
MATCH local=127.0.0.1:55494 remote=127.0.0.1:42063
SUMMARY mode=dry-run remote=127.0.0.1:42063 scanned=8 established=6 matched=2 destroyed=0 failed=0
MATCH local=127.0.0.1:55490 remote=127.0.0.1:42063
SUMMARY mode=apply remote=127.0.0.1:42063 local=127.0.0.1:55490 scanned=7 established=5 matched=1 destroyed=1 failed=0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Socket counts and ports vary. What matters is that dry-run lists both remote matches without changing them, while apply reports &lt;code&gt;matched=1 destroyed=1&lt;/code&gt; for the copied 4-tuple and leaves the other socket intact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;Linux 6.5+ (when &lt;code&gt;bpf_sock_destroy&lt;/code&gt; was introduced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTF&lt;/td&gt;
&lt;td&gt;Required (&lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel configs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CONFIG_BPF=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_SYSCALL=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_JIT=y&lt;/code&gt;, &lt;code&gt;CONFIG_BPF_EVENTS=y&lt;/code&gt;, &lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt;, &lt;code&gt;CONFIG_INET=y&lt;/code&gt;, &lt;code&gt;CONFIG_PROC_FS=y&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BPF JIT&lt;/td&gt;
&lt;td&gt;Must be enabled. Kfuncs require the JIT. Kernels built with &lt;code&gt;CONFIG_BPF_JIT_ALWAYS_ON&lt;/code&gt; require no runtime configuration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Tested on x86_64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileges&lt;/td&gt;
&lt;td&gt;Root required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If loading fails, the tool prints both the kernel error and a checklist of prerequisites to help you diagnose the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This tutorial demonstrated how to use BPF iterators and the &lt;code&gt;bpf_sock_destroy&lt;/code&gt; kfunc to surgically terminate one selected TCP connection. Unlike process killing, firewall rules, or user-space RST injection, this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works atomically with no race window between inspection and action&lt;/li&gt;
&lt;li&gt;Does not require guessing TCP sequence numbers&lt;/li&gt;
&lt;li&gt;Does not disrupt unrelated connections on the same process&lt;/li&gt;
&lt;li&gt;Is not affected by encryption or unusual network configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool uses the complete IPv4 4-tuple as its smallest safe destructive selector. Possible extensions include IPv6 support, process/cgroup attribution, multi-namespace orchestration, and integration with threat-intelligence feeds; broad wildcard destruction is intentionally outside this tutorial.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To learn more about eBPF, visit our tutorial repository at &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial&lt;/a&gt; or our website at &lt;a href="https://eunomia.dev/tutorials/" rel="noopener noreferrer"&gt;https://eunomia.dev/tutorials/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b" rel="noopener noreferrer"&gt;bpf_sock_destroy kernel commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/progs/sock_destroy_prog.c" rel="noopener noreferrer"&gt;Upstream sock_destroy selftest (BPF program)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/prog_tests/sock_destroy.c" rel="noopener noreferrer"&gt;Upstream sock_destroy selftest (userspace test)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>linux</category>
      <category>security</category>
      <category>networking</category>
    </item>
    <item>
      <title>Why eBPF Verifier Errors Are Hard to Fix: The Diagnostic Gap</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:25:56 +0000</pubDate>
      <link>https://dev.to/yunwei37/why-ebpf-verifier-errors-are-hard-to-fix-the-diagnostic-gap-443o</link>
      <guid>https://dev.to/yunwei37/why-ebpf-verifier-errors-are-hard-to-fix-the-diagnostic-gap-443o</guid>
      <description>&lt;p&gt;When developers load an eBPF program into the Linux kernel, the verifier must prove the program safe before any bytecode runs. The verifier walks every possible execution path through the program, tracking what it knows about each register and stack slot at each instruction. If it finds an instruction it cannot prove safe (a memory access through an unvalidated pointer, a read past the end of a packet, or an unbounded loop), it rejects the program and prints an error.&lt;/p&gt;

&lt;p&gt;The problem is that the verifier's error message names the instruction where it got stuck, not the instruction where the program went wrong. The two can be far apart. A bounds check might be missing twenty instructions earlier; a pointer might have lost its type information after passing through a branch; a helper function might have returned a value the verifier can no longer track. The developer sees the final symptom, not the root cause.&lt;/p&gt;

&lt;p&gt;The paper &lt;a href="https://arxiv.org/abs/2607.02748" rel="noopener noreferrer"&gt;Characterizing and Bridging the Diagnostic Gap in eBPF Verifier Rejections&lt;/a&gt; studies this gap systematically. We reproduced 235 real verifier rejections under a fixed kernel and compiler, then asked: how much does the terminal error message actually tell you about what to fix? The answer is surprisingly little. &lt;code&gt;EINVAL&lt;/code&gt; covers 47% of the cases; a single normalized error string can map to as many as nine completely different root causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Verifier Thinks
&lt;/h2&gt;

&lt;p&gt;To understand why verifier errors are so hard to debug, it helps to understand what the verifier is actually doing. The verifier performs abstract interpretation: it simulates executing the program without running it, tracking a simplified model of what each register and memory location might contain at each instruction.&lt;/p&gt;

&lt;p&gt;This model is called the abstract state. For each register, the verifier might know: this is a pointer to the packet data, valid from offset 0 to 42. Or: this is a scalar whose value is between 0 and 255. Or: this is a pointer returned by &lt;code&gt;bpf_map_lookup_elem&lt;/code&gt;, and the program has not yet checked whether it is null. These facts constrain what operations the program can safely perform.&lt;/p&gt;

&lt;p&gt;The verifier builds these facts as it walks the program. When the program performs a bounds check (&lt;code&gt;if (ptr + 8 &amp;gt; data_end) return&lt;/code&gt;), the verifier records that &lt;code&gt;ptr&lt;/code&gt; is now known to be at least 8 bytes from the end. When the program calls a helper that returns a map value, the verifier records the pointer type and the requirement for a null check. When the program branches, the verifier explores both paths and tracks which facts hold on each.&lt;/p&gt;

&lt;p&gt;These accumulated facts are the &lt;em&gt;proofs&lt;/em&gt; that later instructions depend on. A packet read is safe only if the verifier can still see a proof that the access is within bounds. A map-value write is safe only if the pointer came from a lookup helper and passed a null check. The site's &lt;a href="https://eunomia.dev/blog/2024/02/11/the-secure-path-forward-for-ebpf-runtime-challenges-and-innovations/" rel="noopener noreferrer"&gt;eBPF security overview&lt;/a&gt; covers the verifier's safety role more broadly; this post focuses on the diagnostic problem.&lt;/p&gt;

&lt;p&gt;The critical point: proofs can be lost. A register that held a bounded packet pointer might get overwritten. A branch might merge two paths where one has a proof and one does not. The compiler might optimize away the operation that established the proof, or reorder instructions so the verifier no longer sees the connection. When this happens, the verifier rejects the program at the instruction that &lt;em&gt;needed&lt;/em&gt; the proof, not at the instruction that &lt;em&gt;lost&lt;/em&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Verification Stops vs. Where the Proof Was Lost
&lt;/h2&gt;

&lt;p&gt;Consider a packet-parsing example from the paper. The program computes a UDP header pointer, checks it against &lt;code&gt;data_end&lt;/code&gt;, then reads &lt;code&gt;dest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;udph&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;udphdr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data_end&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="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;dst_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;__constant_ntohs&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;udphdr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;udph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajfx84z3ao44ou13yj67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajfx84z3ao44ou13yj67.png" alt="A real eBPF verifier rejection shown as source, verifier log, and proof-oriented diagnostic" width="800" height="1292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Figure 1 from the paper puts three views side by side: the source reads from a UDP header, the raw verifier log stops at &lt;code&gt;R5 invalid mem access 'scalar'&lt;/code&gt;, and the proof-oriented diagnostic identifies what the load required: a packet pointer that the verifier could still recognize at the dereference.&lt;/p&gt;

&lt;p&gt;The snippet looks guarded, but the bytecode no longer preserves the packet-pointer proof at the load. The terminal line &lt;code&gt;R5 invalid mem access 'scalar'&lt;/code&gt; says the verifier sees a scalar where it expected a packet pointer. It does not say when the packet pointer became a scalar, whether the source forgot a bounds check, whether compiler lowering merged provenance away, or whether the developer should rederive a pointer.&lt;/p&gt;

&lt;p&gt;This is the diagnostic gap: the error names the symptom, not the cause. The source code might be entirely correct; the problem might be in how the compiler lowered it, or in how the verifier tracks types across branches. Alternatively, the source might have a real bug, but twenty lines earlier than the error points to. Either way, the terminal message alone does not tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 235 Reproduced Rejections Reveal
&lt;/h2&gt;

&lt;p&gt;To study this problem with real data, we assembled a corpus of verifier rejections that developers actually encountered. We started with 936 candidate reports from Stack Overflow questions, GitHub issues, GitHub fix commits, and Linux kernel selftests. Each candidate was rebuilt and loaded with Linux 6.15.11, clang 18, and verifier log level 2. Only 235 still produced a verifier rejection under that fixed setup; the rest depended on a different environment, no longer failed with the selected toolchain, or lacked the source material to rebuild.&lt;/p&gt;

&lt;p&gt;This filtering gives the corpus a clear scope: a reproducible sample rather than an estimate of every verifier failure developers encounter. Each retained case includes the faulty source and the developer's own fix from the original report. Those paired artifacts allowed us to label both the root cause and the layer where the accepted repair landed.&lt;/p&gt;

&lt;p&gt;Source changes repaired 191 cases, or 81% of the corpus. The remaining 44 cases involved source that was correct for the intended operation: 18 were repaired in the compiler, 14 through the environment, and 12 in the verifier. A context-field read compiled with &lt;code&gt;-O0&lt;/code&gt;, for example, could lose its verifier-visible pointer type during lowering; changing the compilation setting repaired the program while leaving its C logic intact. The rejected instruction alone gives no way to choose among these layers.&lt;/p&gt;

&lt;p&gt;The 191 source bugs fall into 12 root-cause categories. Ten are eBPF-specific, arising from the verifier's requirements around bounds, pointer provenance, object lifetimes, and helper protocols, concepts that do not exist in normal C programming.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Root-cause category&lt;/th&gt;
&lt;th&gt;Cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unclamped scalar used as an offset or length&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corrupted or stale dynptr object&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packet access without a bound on every path&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing null check&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pointer type or provenance mismatch&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unverified address dereferenced&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index exceeds object capacity&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context or contract misuse&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unpaired resource reference&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interrupt flag restored out of order&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe signature mismatched with the ABI&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oversized or uninitialized stack buffer&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These categories require different repairs. Tightening a scalar range, preserving a packet bound across every path, checking a map lookup for null, and releasing a reference in the right order all satisfy the verifier, but they restore different facts. A developer who sees &lt;code&gt;invalid mem access 'scalar'&lt;/code&gt; cannot tell from that message alone whether the fix is a bounds check, a null check, a type cast, or a compiler flag.&lt;/p&gt;

&lt;p&gt;The paper measures this ambiguity by normalizing register numbers and offsets in the final verifier line. The 235 rejections produced 167 distinct strings, which collapsed to 82 message templates; 15 templates covered more than one root cause. The four most common templates show how quickly a familiar message spreads across unrelated mistakes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Terminal message template&lt;/th&gt;
&lt;th&gt;Cases&lt;/th&gt;
&lt;th&gt;Root-cause categories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;R# invalid mem access 'scalar'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invalid access to packet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invalid access to map value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;R# !read_ok&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;EINVAL&lt;/code&gt; is broader still, appearing in 47% of all reproduced rejections.&lt;/p&gt;

&lt;p&gt;The verifier log actually contains far more information than the terminal error: at log level 2, it prints the abstract state after every instruction. The problem is that developers must manually trace through this state to figure out where the proof was lost. The terminal line discards the history needed to connect the rejected operation to a root cause and a repair layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Rejection Location to Repair Information
&lt;/h2&gt;

&lt;p&gt;A useful diagnostic would answer the questions the terminal error leaves open: what proof did the verifier need at the rejected instruction, and where did the program lose it?&lt;/p&gt;

&lt;p&gt;The paper introduces a research prototype called &lt;a href="https://github.com/eunomia-bpf/bpfix" rel="noopener noreferrer"&gt;bpfix&lt;/a&gt; that attempts this reconstruction. It reads the verifier's log-level-2 output (the per-instruction abstract states) and traces backward from the rejected operation. It identifies what proof was required (packet bounds, pointer provenance, null check, etc.), when that proof first appeared in the state, and when it disappeared. If debug metadata is available, it maps these transitions back to source lines.&lt;/p&gt;

&lt;p&gt;A map-value case from the paper illustrates the difference. A developer cast the address of a BPF map object directly to a pointer and tried to write through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&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;globals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier rejected the write with &lt;code&gt;only read from bpf_array is supported&lt;/code&gt;. The error names the rejected operation, but not the underlying problem: you cannot write through a map object pointer directly. The verifier expects a map-value pointer, which must come from a helper like &lt;code&gt;bpf_map_lookup_elem&lt;/code&gt;. The correct fix follows that protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;key&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="n"&gt;__u64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&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;globals&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;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;v&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repair establishes the missing proof in three steps: look up the map element, check the returned pointer for null, then write through it. A diagnostic that names the required proof (a map-value pointer from a helper) and the loss point (the direct cast) gives the developer more to work with than the terminal error alone.&lt;/p&gt;

&lt;p&gt;This approach also distinguishes cases that share an error message but need different fixes. One program constructs a packet address from an integer offset; it never establishes packet-pointer provenance and needs a source change. Another program derives and bounds-checks its pointer correctly, but compiler optimization merges the value into a scalar before the load; the fix is a compiler flag, not a source change. Both produce &lt;code&gt;invalid mem access 'scalar'&lt;/code&gt;, but they belong to different repair layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can LLMs Fix Verifier Errors?
&lt;/h2&gt;

&lt;p&gt;The paper tests whether better diagnostic context improves automated repair. If the diagnostic gap matters, models should perform better when given the missing proof information than when given only the raw verifier log.&lt;/p&gt;

&lt;p&gt;We built bpfix-bench, a benchmark of 75 source-level repair tasks. Forty are constructed around specific verifier proofs that the repaired program must re-establish; 35 are minimized from open-source projects like Cilium, xdp-tools, and &lt;a href="https://eunomia.dev/blog/2023/11/11/bpftime-extending-ebpf-from-kernel-to-user-space/" rel="noopener noreferrer"&gt;bpftime&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each task has an executable test suite independent of the diagnostic tool. A candidate fix must compile, load through the kernel verifier, pass a functional test, and pass a source-semantics check. The last requirement matters: it catches patches that make the error disappear by deleting the offending code path or changing the program's behavior. Success means restoring a verifier-acceptable program that still does what it was supposed to do.&lt;/p&gt;

&lt;p&gt;The experiment compared two prompt conditions: one where the model received the raw verifier log, and one where it received a shorter diagnostic that named the required proof and relevant source span. Three models were tested at temperature zero: Qwen3.6 27B, GLM 5.2, and Qwen2.5 3B (as a lower-capacity comparison). One-shot mode judged the first candidate; retry mode returned failure information once and allowed a second attempt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Raw log, one shot&lt;/th&gt;
&lt;th&gt;Localized diagnostic, one shot&lt;/th&gt;
&lt;th&gt;Raw log, one retry&lt;/th&gt;
&lt;th&gt;Localized diagnostic, one retry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.6 27B&lt;/td&gt;
&lt;td&gt;22/75&lt;/td&gt;
&lt;td&gt;38/75&lt;/td&gt;
&lt;td&gt;30/75&lt;/td&gt;
&lt;td&gt;44/75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM 5.2&lt;/td&gt;
&lt;td&gt;28/75&lt;/td&gt;
&lt;td&gt;38/75&lt;/td&gt;
&lt;td&gt;47/75&lt;/td&gt;
&lt;td&gt;52/75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5 3B&lt;/td&gt;
&lt;td&gt;0/75&lt;/td&gt;
&lt;td&gt;8/75&lt;/td&gt;
&lt;td&gt;0/75&lt;/td&gt;
&lt;td&gt;10/75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5vu0p9hxk22pc6ah319o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5vu0p9hxk22pc6ah319o.png" alt="Repair success across three models given a raw verifier log or a localized proof diagnostic" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results show a consistent improvement when models receive proof-localized context instead of raw logs. Qwen3.6 27B improved from 29% to 51% one-shot success; GLM 5.2 from 37% to 51%; Qwen2.5 3B from 0% to 11%. The gains persist with retry: Qwen3.6 goes from 40% to 59%, GLM from 63% to 69%.&lt;/p&gt;

&lt;p&gt;The benchmark records the first stage where each one-shot candidate fails, which makes the aggregate result easier to interpret.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Compile&lt;/th&gt;
&lt;th&gt;Verifier load&lt;/th&gt;
&lt;th&gt;Functional test&lt;/th&gt;
&lt;th&gt;Source semantics&lt;/th&gt;
&lt;th&gt;No program returned&lt;/th&gt;
&lt;th&gt;Accepted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.6 27B&lt;/td&gt;
&lt;td&gt;Raw log&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.6 27B&lt;/td&gt;
&lt;td&gt;Localized&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM 5.2&lt;/td&gt;
&lt;td&gt;Raw log&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM 5.2&lt;/td&gt;
&lt;td&gt;Localized&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5 3B&lt;/td&gt;
&lt;td&gt;Raw log&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen2.5 3B&lt;/td&gt;
&lt;td&gt;Localized&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The stage-by-stage breakdown shows where the improvement comes from. For Qwen3.6, verifier-load failures dropped from 19 to 10 and source-semantics failures from 22 to 16. These are exactly the stages tied to restoring proofs while preserving behavior, which is the information the diagnostic adds.&lt;/p&gt;

&lt;p&gt;The 3B model shows a different pattern. Verifier-load failures dropped from 62 to 39, and prompts that previously exceeded its context window now fit. But compile failures rose from 7 to 14; the model reached more repair attempts but still struggled with basic code generation.&lt;/p&gt;

&lt;p&gt;These results are narrow: three models, 75 tasks, temperature zero, at most one retry. Even the best result (52/75 with retry) leaves many tasks unsolved. But the consistent improvement across models supports a specific conclusion: repair is easier when you know which proof was lost, not just where verification stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Verifier Errors in Practice
&lt;/h2&gt;

&lt;p&gt;The paper's findings suggest a different approach to debugging verifier rejections. Instead of asking "what is wrong with this line?", start by asking what the rejected operation &lt;em&gt;required&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Many verifier rejections happen because some instruction needed a proof the verifier did not have. A packet load needs packet-pointer provenance and a valid bound. A map-value write needs a pointer from a lookup helper with a null check. A dynptr slice needs a live dynptr object. Identifying the required proof is the first step.&lt;/p&gt;

&lt;p&gt;The next step is tracing backward through the abstract states to see when that proof appeared, disappeared, or never existed. The verifier log at level 2 contains this information (it prints the state after every instruction), but you have to read it manually. Look for where a register changes from a typed pointer to a scalar, where a bound disappears after a branch merge, or where a required check never appears.&lt;/p&gt;

&lt;p&gt;A practical reading order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the rejected operation:&lt;/strong&gt; what kind of access is it? Packet read, map write, helper call?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name the required proof:&lt;/strong&gt; packet bounds, pointer provenance, null check, scalar range?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace backward:&lt;/strong&gt; when did the register have that proof? When did it lose it? Did it ever have it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Determine the repair layer:&lt;/strong&gt; is the problem in source (missing check), compiler (optimization hid the proof), environment (wrong kernel version), or verifier (precision limit)?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes this trace leads outside the source. If the abstract states show a proof established and then discarded during compilation, the fix might be a compiler flag. If the source never established the proof, you need a code change. The terminal error is where you start; the proof history tells you where to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2607.02748" rel="noopener noreferrer"&gt;Characterizing and Bridging the Diagnostic Gap in eBPF Verifier Rejections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eunomia-bpf/bpfix" rel="noopener noreferrer"&gt;bpfix GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/bpf/verifier.html" rel="noopener noreferrer"&gt;Linux kernel documentation on the eBPF verifier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libbpf/bpfvv" rel="noopener noreferrer"&gt;BPF Verifier Visualizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doi.org/10.1145/3672197.3673429" rel="noopener noreferrer"&gt;An Empirical Study on the Challenges of eBPF Application Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eunomia-bpf/bpftime" rel="noopener noreferrer"&gt;bpftime userspace eBPF runtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>linux</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>An Empirical Study: AI Agent Rules Need Context and Layered Enforcement</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:57:55 +0000</pubDate>
      <link>https://dev.to/yunwei37/an-empirical-study-ai-agent-rules-need-context-and-layered-enforcement-43on</link>
      <guid>https://dev.to/yunwei37/an-empirical-study-ai-agent-rules-need-context-and-layered-enforcement-43on</guid>
      <description>&lt;p&gt;A rule like "run the full test suite before committing" looks simple until an AI coding agent edits a source file after the last test run and then calls &lt;code&gt;git commit&lt;/code&gt;. The kernel sees an ordinary process writing a commit object, while the harness sees one more tool call, yet the decision depends on which test result is still fresh, which edit invalidated it, and whether this commit is allowed now.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2606.25189" rel="noopener noreferrer"&gt;ActPlane paper&lt;/a&gt; measures the gap between the behavioral rules developers write and the subset a system can actually check. Its statement-level analysis of 2,116 instructions shows that developers are not short of rules; the difficulty lies in turning natural-language requirements into state that a system can observe and evaluate over time. Many rules concern files, processes, or network activity but still depend on repository structure, task progress, or prior events, so a single OS hook can cover only part of the policy set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developers Have Already Written the Policies
&lt;/h2&gt;

&lt;p&gt;Most discussions of AI agent safety start from threat models or attack surfaces. ActPlane starts from a different question: what do developers already tell their agents to do and not do, and what would it take to enforce those instructions?&lt;/p&gt;

&lt;p&gt;The study examines 64 popular repositories containing CLAUDE.md and AGENTS.md files (median 20K GitHub stars, snapshot from 2026-05-23), covering 84 instruction files and 2,116 individual statements. Unlike prior work that analyzed instruction files at the file or section-heading level, ActPlane classifies every statement independently. The study asks three questions: are instruction files primarily behavioral policies or descriptive context? Which policies require OS-level enforcement, and what kinds of OS-level checks do they need? What context is needed to instantiate these policies into concrete, enforceable rules?&lt;/p&gt;

&lt;p&gt;Statements were extracted through a two-pass LLM agent-assisted pipeline that recorded source line ranges and four labels per statement: content type, topic, enforcement level, and context requirement. A validation script verified full source coverage and verbatim span matching, then two independent agents (Claude and Codex) cross-checked the results. A stratified sample of 100 statements went through independent human review, which confirmed the labels were correct.&lt;/p&gt;

&lt;p&gt;Across those 2,116 statements, 64% are policies that require, forbid, or condition a specific agent action. The remaining 36% are descriptive context, such as architecture notes or project background. Policy density varies widely across repositories, from 0% to 97%, with 70.1% of repositories containing more policy statements than descriptive ones. File- or heading-level studies do not report this statement-level distribution, which is why the finer classification matters.&lt;/p&gt;

&lt;p&gt;To understand how policies distribute across concerns, the study assigns each statement to one of 12 topic categories adapted from prior instruction-file research, applied at statement granularity rather than file granularity. Development Process and Implementation Details dominate the policy landscape at 87% and 85% respectively. Architecture is mostly descriptive at 23% because directory layouts and design summaries make up the bulk of those sections. The imported source figures call policy statements directives and call the system-observable policy subset system-level directives. The prose follows the paper's policy and system-observable terminology.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmyzvgnswc37awox1e9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmyzvgnswc37awox1e9e.png" alt="Policy fraction per repository across 64 repos with CLAUDE.md or AGENTS.md" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five real statements from the dataset illustrate the range of enforcement requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statement&lt;/th&gt;
&lt;th&gt;Enforcement level&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S4: "Never push to main directly."&lt;/td&gt;
&lt;td&gt;per-event&lt;/td&gt;
&lt;td&gt;self-contained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S5: "Never modify upstream source code."&lt;/td&gt;
&lt;td&gt;per-event&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S6: "Run the full test suite before committing."&lt;/td&gt;
&lt;td&gt;cross-event&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S7: "Data read from .env must not reach the network."&lt;/td&gt;
&lt;td&gt;cross-event&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S8: "Do not update dependencies without approval."&lt;/td&gt;
&lt;td&gt;per-event&lt;/td&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Enforcement Gap Begins with Context
&lt;/h2&gt;

&lt;p&gt;Each policy exits at the first matching tier of an enforcement waterfall. Semantic-only covers reasoning, communication, or output style; content covers predicates over file contents; per-event covers a single command, file access, or network connection; and cross-event covers policies that depend on temporal ordering or data lineage across operations. The union of content, per-event, and cross-event tiers is called system-observable.&lt;/p&gt;

&lt;p&gt;Of the 1,361 policies in the dataset, only 17% are semantic-only. The remaining 83% are system-observable, comprising 38% that require content inspection, 29% that match one OS event, and 16% that require cross-event state. Only the per-event and cross-event classes, 45% together, form the OS-enforceable subset. Cross-event policies concentrate in Development Process, which accounts for 39.5% of all cross-event policies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feuskop4ooz84cbouqjyc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feuskop4ooz84cbouqjyc.png" alt="Enforcement waterfall showing semantic-only, content, per-event, and cross-event distribution across 1,361 policies" width="610" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These cross-event policies follow four recurring patterns. Temporal ordering constrains sequencing: "run tests before committing" requires that one event happened after another, not merely at some earlier point. Cross-file consistency links changes across artifacts: "update docs when behavior changes" couples a source edit to a documentation update. Multi-step workflows enforce release checklists with verification gates, where each step must complete before the next begins. Conditional triggers couple operations: "if you change specs, also update the SDK" fires only when a precondition is met.&lt;/p&gt;

&lt;p&gt;None of these can be decided from a single event, so enforcement must record what ran, in what order, and what has changed since. Such policies are widespread, with 81% of repositories containing at least one cross-event policy and 43% spanning all four enforcement tiers.&lt;/p&gt;

&lt;p&gt;Context dependence compounds the enforcement challenge. Of the 1,127 system-observable policies, only 26.4% are self-contained. The majority, 64.2%, require project context: "the test suite" or "upstream source" must be resolved against a specific repository before the policy becomes a concrete rule. Even a per-event policy like S5, "Never modify upstream source code," requires resolving which paths constitute "upstream source" before a file-write check can fire. Another 9.4% require task context, such as "unless explicitly requested" or "without approval."&lt;/p&gt;

&lt;p&gt;The two difficulties compound, because the policies that require tracking state across events are also the ones that rarely specify the concrete commands and paths needed to write the rule. Cross-event policies are 95% context-dependent (77% project, 19% task), compared to 58% for content policies. A policy that says "run tests before commit" sounds simple until the enforcement engine needs to know which test command to watch for, which source directories count as "relevant edits," and whether the test passed or merely ran.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9rnuzxslqo2ms4xhp661.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9rnuzxslqo2ms4xhp661.png" alt="Context waterfall showing self-contained, project context, and task context distribution across 1,127 system-observable policies" width="602" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A fixed set of static rules can cover only the self-contained fraction. Instantiating the rest requires reading the repository and interpreting the current task before any check can run.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent policy enforcement begins by compiling repository and task context into concrete state that deterministic checks can evaluate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One Rule Crosses Several Enforcement Layers
&lt;/h2&gt;

&lt;p&gt;Prompt instructions rely on the model's own compliance, but they are vulnerable to prompt injection and compete with the user's task prompt for attention in a long context window. Separate agents or LLM guards can check prompts, responses, or action trajectories at runtime, but these checks are inherently probabilistic.&lt;/p&gt;

&lt;p&gt;Tool-call guardrails and application-level information-flow control (IFC) systems intercept at the harness boundary deterministically, but they observe only harness-mediated requests, not system-level effects once a tool starts executing. An indirect subprocess, shell-out, or compiled binary can bypass the tool boundary. Consider an agent that writes a Python script containing &lt;code&gt;subprocess.run(["git", "push"])&lt;/code&gt; and then executes it: the tool-call layer sees "run python script.py," not the &lt;code&gt;git push&lt;/code&gt; inside it.&lt;/p&gt;

&lt;p&gt;OS-level mechanisms like seccomp, AppArmor, Landlock, and Tetragon control resource access, not actions in the sense developers write about. They expect statically pre-written policies and return opaque errors that confuse the agent: a bare EPERM with no explanation of what rule was violated or how to recover.&lt;/p&gt;

&lt;p&gt;Those layers still leave a structural split between who holds policy context and who can see every execution path. Most rules need project or task context that resides with the agent, so the agent itself must turn policies into concrete rules, yet many policies define event ordering or data flow that tool-call guardrails never see, so the rules must still be concrete enough for deterministic OS-level enforcement. Bridging that gap is what ActPlane addresses.&lt;/p&gt;

&lt;p&gt;Two design requirements follow. The policy specification must be agent-writable yet OS-enforceable, so the agent can produce concrete rules from natural-language policies with minimal expertise and receive semantic feedback to understand violations and recover. Enforcement must also stay safe, isolated, and efficient, meaning agent-authored policy must not weaken constraints set by higher authority, must not affect other agents' policies, and must not slow the agent's normal workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling Intent into Enforceable State
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwprdfpypdgjq35trdy6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwprdfpypdgjq35trdy6p.png" alt="ActPlane overview: the agent resolves policy context, ActPlane compiles the DSL, and the kernel enforces it" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each ActPlane rule has five components: a source that identifies what is being governed, a target operation (such as exec, write, or connect), an effect, an optional temporal gate, and a reason string for semantic feedback. The paper's running example makes this concrete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kill exec "git" "commit" unless after exec "go" "test" exits 0 since write "**/*.go"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule kills any &lt;code&gt;git commit&lt;/code&gt; unless &lt;code&gt;go test&lt;/code&gt; has exited successfully since the most recent relevant source edit. The reason field, omitted here for brevity, provides the agent with a structured explanation when the rule fires.&lt;/p&gt;

&lt;p&gt;Effects form a gradient matching the distinction between instructions and constraints. Block is a pre-operation synchronous denial with no TOCTOU gap: the kernel intercepts the system call before it executes, and the agent can reroute. Kill terminates the process after the operation has begun, preventing the agent from switching to an alternate channel. Notify delivers guidance without stopping the action. Constraints use block or kill; instructions use notify.&lt;/p&gt;

&lt;p&gt;Temporal gates let rules express ordering rather than point-in-time predicates. The &lt;code&gt;after ... since ...&lt;/code&gt; construct encodes that one event must have occurred after another: tests must have run after the most recent edit, not merely at some earlier point. The &lt;code&gt;exits N&lt;/code&gt; qualifier distinguishes successful from failed exits. A lineage gate checks process ancestry, allowing rules to restrict operations to specific process trees.&lt;/p&gt;

&lt;p&gt;Information-flow labels propagate along fork, exec, read, write, and connect and are monotonic: once a process reads a labeled object, the label cannot be removed. When a process reads &lt;code&gt;.env&lt;/code&gt;, it acquires that file's source label. If it later attempts to connect to an external endpoint, the rule matching that label fires and blocks the connection. This is how S7 from the study ("Data read from .env must not reach the network") becomes an enforceable cross-event rule.&lt;/p&gt;

&lt;p&gt;Policy authority relies on a temporal trust boundary. Rules loaded before the agent starts are higher-authority and immutable to the agent. The agent and its sub-agents can add new rules or narrow existing ones within child domains, but they cannot weaken, remove, or disable inherited constraints. Runtime deltas arrive through a ring buffer and pass through an in-kernel authority checker that validates each change against the domain hierarchy before activation. The trusted computing base consists of the kernel enforcement engine and the higher-authority policy, and everything below this boundary is untrusted execution. A compromised userspace agent therefore cannot modify the active rule set beyond what its domain hierarchy permits.&lt;/p&gt;

&lt;p&gt;Because labels are monotonic, long-running sessions risk over-tainting: after many reads, a process can accumulate so many labels that every subsequent operation triggers a rule. In a typical coding session, a process might read dozens of configuration and source files, and without mitigation each read adds a label, so after enough reads every subsequent write or connect would match some rule. ActPlane mitigates this by clearing inherited labels when a fresh subprocess is spawned, bounding taint accumulation to the lifetime of each process rather than the entire session.&lt;/p&gt;

&lt;p&gt;The 607-policy dataset exercises most DSL features and validates the language's expressiveness. Effects skew toward observation: 66% of clauses are notify, 29% are block, and only 5% are kill, reflecting that most policies monitor rather than prevent. Hooks concentrate on code execution (60% exec) and file mutation (37% write), with network and cleanup operations under 1% each. Cross-event features see substantial use, with 28% of policies using an &lt;code&gt;after/since&lt;/code&gt; temporal gate and 214 using &lt;code&gt;unless&lt;/code&gt; to encode exceptions.&lt;/p&gt;

&lt;p&gt;The implementation stays compact enough to reason about. The userspace compiler and runner are roughly 3.2K lines of Rust, and the eBPF enforcement engine is roughly 1.8K lines of BPF C. BPF-LSM hooks handle pre-operation decisions (block), while tracepoints handle observation and post-operation termination (kill). Labels live as 64-bit bitmasks in per-object BPF maps, so propagation reduces to a single bitwise OR, and the engine can support up to 128 concurrent rules, comfortably above the largest observed repository's 66 policies. For deeper coverage of the deployment architecture and mechanism details, see &lt;a href="https://eunomia.dev/blog/2026/05/31/actplane-pushing-agent-harness-enforcement-down-to-kernel-ebpf/" rel="noopener noreferrer"&gt;ActPlane: Pushing Agent Harness Enforcement Down to Kernel eBPF&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery Reveals What Enforcement Alone Misses
&lt;/h2&gt;

&lt;p&gt;Before recovery can matter, the DSL has to accept the policies developers already wrote. A Codex agent compiled all 607 OS-enforceable policies into ActPlane rules on the first or second attempt, with only 2 of 607 needing a syntax-error retry. The translation completed in 34 minutes using 7 subagents with 4 running in parallel, expanding 607 policies into 1,283 rule lines. The cost was roughly $0.028 per policy using 1.7M input tokens and 177K output, compared to approximately $11 per rule at typical software-engineer rates. Most policies are structurally simple, with 74% having at most two enforcement clauses and even the 95th percentile staying under 152 tokens.&lt;/p&gt;

&lt;p&gt;Contextual enforcement resolves far more violations than any baseline. On the decision-compliance benchmark (190 traces, 38 rules drawn from the empirical study), ActPlane achieves a 75.8% Decision Compliance Rate under the primary Qwen3.6-27B setting. In this setting, the tested agent, prompt-filter classifier, and trajectory judge all use Qwen3.6-27B; the benchmark traces were generated earlier by Codex CLI under GPT-5.5, and policy artifacts for the non-prompt systems were translated separately with Claude Opus 4.6:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;DCR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ActPlane&lt;/td&gt;
&lt;td&gt;75.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ActPlane-opaque (no feedback)&lt;/td&gt;
&lt;td&gt;53.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIDES (tool-level IFC)&lt;/td&gt;
&lt;td&gt;48.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-filter&lt;/td&gt;
&lt;td&gt;48.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-regex&lt;/td&gt;
&lt;td&gt;45.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The benchmark generates five traces per rule: two compliant traces that stress false-positive detection (one exercising permitted actions near the boundary, one performing technically compliant operations that resemble violations) and three violation traces reflecting execution-path categories observed in deployed coding agents. Direct violations trigger the prohibited operation via a tool call, script violations split authoring and execution into a subprocess, and hidden violations embed the side effect in an auxiliary artifact behind a neutral entrypoint.&lt;/p&gt;

&lt;p&gt;The gap concentrates on violation traces, where ActPlane correctly resolves 86 of 114, compared to 27 to 44 for baselines, a 2.0 to 3.2 times improvement. All systems perform comparably on compliant traces and direct violations, so ActPlane's advantage opens on script and hidden trace families, where the prohibited effect occurs inside a subprocess or behind a neutral entrypoint and tool-call interception cannot observe it.&lt;/p&gt;

&lt;p&gt;Semantic feedback is the dividing line between compliance and retry loops. Full ActPlane produces three times more correct violation-trace outcomes than the same engine without feedback, 86 versus 27. Of the violations ActPlane detects, recovery rate, the fraction where the agent then complies, is 97.7% with semantic feedback versus 31.4% without. When the agent learns why an action was blocked and what state it needs to repair, it reroutes and completes the task. When it receives only a bare denial, it retries the same forbidden action through alternative paths.&lt;/p&gt;

&lt;p&gt;That recovery loop only helps if enforcement stays cheap enough for everyday coding. End-to-end overhead was measured on two workloads under no-hit configurations where policies are loaded but no rule fires. The first workload is an agent trace suite that replays 68 tool actions with 20 Bash subprocesses. The second is a Linux kernel build (&lt;code&gt;defconfig&lt;/code&gt; + &lt;code&gt;vmlinux&lt;/code&gt;, &lt;code&gt;make -j24&lt;/code&gt;). At 32 active rules, ActPlane adds 1.9% on the agent trace and 6.5% on the kernel build. Even at 100 rules, overhead stays below 8.4%.&lt;/p&gt;

&lt;p&gt;Microbenchmarks isolate where per-syscall cost concentrates. Across the one- through 100-rule configurations, the absolute additions on &lt;code&gt;fork&lt;/code&gt; and &lt;code&gt;exec&lt;/code&gt; range from 3.12 to 68.73 microseconds. At 100 rules specifically, &lt;code&gt;fork&lt;/code&gt; adds 20.39 microseconds and &lt;code&gt;exec&lt;/code&gt; adds 68.73 microseconds over native latencies of 48.94 and 248.30 microseconds.&lt;/p&gt;

&lt;p&gt;Under the same 100-rule load, absolute latencies reach 13.4 microseconds for &lt;code&gt;open&lt;/code&gt;, 0.84 microseconds for &lt;code&gt;write&lt;/code&gt;, and 3.17 microseconds for &lt;code&gt;connect&lt;/code&gt;, so path lookups and rule scans dominate these otherwise sub-microsecond file and network calls. The cumulative ActPlane overhead of an entire tool-call's syscall sequence is five to six orders of magnitude smaller than a single LLM inference turn of 2 to 10 seconds. Policy updates propagate quickly: a one-rule hot reload submitted through the userspace ring buffer reaches the kernel drain path in 26.3 microseconds on average, and an immediate exec violation is detected at p50 176.4 microseconds including process launch and event delivery.&lt;/p&gt;

&lt;p&gt;ActPlane's advantage replicates under a second model. A DeepSeek-Pro V4 end-to-end replication preserves the system ranking with ActPlane highest at 77.4% DCR, and per-cell agreement between the two model settings yields a Cohen's kappa of 0.822.&lt;/p&gt;

&lt;p&gt;Translation quality drives both detection and recovery rates, because rules that are too narrow miss violations while rules that are too broad match compliant actions. To measure improvability, the paper feeds each false-negative trace's evidence and corrective feedback to the translation agent and lets it revise the rule once. Rerunning the 28 false-negative traces with revised rules recovers 26 (93%), showing that the DSL supports iterative refinement.&lt;/p&gt;

&lt;p&gt;Results on a selected set of real-world coding tasks suggest the pattern may extend beyond synthetic traces. On a 21-task subset of OctoBench with 61 OS-enforceable rules spanning seven repositories, ActPlane improves user-query reward by 9.9 points and implementation/test reward by 9.7 points over the no-enforcement baseline. The gains extend beyond compliance-typed checks, suggesting that OS-level enforcement with semantic feedback can help agents follow rules and complete tasks more effectively on this subset.&lt;/p&gt;

&lt;p&gt;A separate safety benchmark extends the evidence beyond the paper's own dataset. On 361 OpenAgentSafety personal-assistant tasks, ActPlane loads agent-generated safety policies as higher-authority rules before the agent begins, preventing 74% of baseline-unsafe behaviors (78 of 106 unsafe outcomes blocked). Those policies were generated only from task descriptions, without human tuning. That deployment-like constraint also reveals a cost: ActPlane activated on 16% of tasks whose baseline was already safe when a description-only policy matched a benign operation near the prohibited boundary.&lt;/p&gt;

&lt;p&gt;The 28 unblocked cases fall into three categories: chat or semantic harm where the unsafe behavior is a message with no OS-observable artifact, unsafe file content that falls outside ActPlane's primary scope, and service-side artifacts where the effect is a WebDAV upload or database mutation inside a service container that the current hook set does not observe.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane source code&lt;/a&gt; is available on GitHub. The &lt;code&gt;policies/&lt;/code&gt; directory contains the full set of 607 translated rules across all 64 repositories, ready to serve as starting points for your own instruction files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Layered Enforcement Stops
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is eBPF enough for AI agent safety?
&lt;/h3&gt;

&lt;p&gt;eBPF provides deterministic enforcement over OS events such as file writes, process launches, and network connections. The per-event and cross-event classes form the directly OS-enforceable 45% of policies. The broader 83% system-observable set also includes 38% content policies, which need linters or static analyzers, while the remaining 17% cover reasoning, communication style, or output quality and require harness-level controls. Task intent, policy authority, content semantics, and isolation remain responsibilities of the layers around kernel enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can behavioral baselines replace policy?
&lt;/h3&gt;

&lt;p&gt;Behavioral baselines answer "is this unusual?" by flagging deviations from historical patterns. Policy answers "is this permitted under the current task?" A routine &lt;code&gt;git commit&lt;/code&gt; can be perfectly normal by baseline standards and still violate a project rule. An unfamiliar deployment endpoint can trigger an anomaly alert and still be legitimate because the user explicitly requested it. Detection and authorization are two different problems, and a mature system uses both, letting anomaly signals propose candidate rules that go through authority-aware review before becoming enforced policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does AgentSight enforce ActPlane policies?
&lt;/h3&gt;

&lt;p&gt;No. &lt;a href="https://eunomia.dev/blog/2025/08/26/agentsight-keeping-your-ai-agents-under-control-with-ebpf-powered-system-observability/" rel="noopener noreferrer"&gt;AgentSight&lt;/a&gt; is a system-level profiler and monitor that captures what agents do at runtime. ActPlane is the enforcement engine that blocks or redirects actions violating loaded policies. The two serve complementary roles: runtime evidence from AgentSight can feed into policy review and refinement, but observation and enforcement remain separate responsibilities. An organization might use AgentSight to discover that agents frequently write to production config files, then encode a corresponding ActPlane rule to block that pattern with an explanation of why it is forbidden.&lt;/p&gt;

&lt;h3&gt;
  
  
  What remains outside ActPlane's coverage?
&lt;/h3&gt;

&lt;p&gt;The paper's threat model is explicit about boundaries. Semantically equivalent operations can bypass string-based matching: an agent could use a custom Git client instead of calling &lt;code&gt;exec git&lt;/code&gt;, though the underlying &lt;code&gt;connect&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt; system calls remain visible. Service-side effects behind protocol boundaries, such as WebDAV uploads or database mutations inside service containers, also escape the current hook set. File-content semantics, kernel compromise, CAP_BPF compromise, and side channels are all out of scope. The 17% of policies that are semantic-only, covering reasoning quality, communication tone, or output formatting, require harness-layer handling rather than kernel enforcement.&lt;/p&gt;

&lt;p&gt;For a team governing coding agents, the practical decision is narrower than "add more eBPF." Start from the statements already sitting in CLAUDE.md and AGENTS.md, resolve the project and task context they omit, and only then compile the OS-enforceable subset into kernel checks with feedback the agent can act on. Prior instruction-file studies worked at file or section granularity, while the ActPlane dataset measures individual policy statements and maps them to enforcement and context requirements. The &lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane repository&lt;/a&gt; contains the implementation, and a broader three-layer security model placing kernel enforcement alongside isolation, identity, and content controls appears in &lt;a href="https://eunomia.dev/blog/2026/05/25/runtime-security-for-ai-agents/" rel="noopener noreferrer"&gt;Runtime Observability and Enforcement for Opaque AI Agents with eBPF&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2606.25189" rel="noopener noreferrer"&gt;ActPlane: Programmable OS-Level Policy Enforcement for Agent Harnesses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane source code and policy artifacts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2511.12884" rel="noopener noreferrer"&gt;Agent READMEs: An Empirical Study of Context Files for Agentic Coding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2505.23643" rel="noopener noreferrer"&gt;Securing AI Agents with Information-Flow Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/userspace-api/landlock.html" rel="noopener noreferrer"&gt;Landlock: Unprivileged Access Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tetragon.io/" rel="noopener noreferrer"&gt;Tetragon: eBPF-based Security Observability and Runtime Enforcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2601.10343" rel="noopener noreferrer"&gt;OctoBench: Benchmarking Scaffold-Aware Instruction Following in Repository-Grounded Agentic Coding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openreview.net/forum?id=xggSxCFQbA" rel="noopener noreferrer"&gt;OpenAgentSafety: A Comprehensive Framework for Evaluating Real-World AI Agent Safety&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doi.org/10.1145/3766882.3767169" rel="noopener noreferrer"&gt;AgentSight: System-Level Observability for AI Agents Using eBPF&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>security</category>
      <category>ebpf</category>
    </item>
    <item>
      <title>ACRFence: Preventing Semantic Rollback Attacks in Agent Checkpoint-Restore</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Tue, 02 Jun 2026 11:11:23 +0000</pubDate>
      <link>https://dev.to/yunwei37/acrfence-preventing-semantic-rollback-attacks-in-agent-checkpoint-restore-5eja</link>
      <guid>https://dev.to/yunwei37/acrfence-preventing-semantic-rollback-attacks-in-agent-checkpoint-restore-5eja</guid>
      <description>&lt;p&gt;AI agent frameworks are bringing checkpoint/restore, time travel, and rewind into everyday developer workflows. If an agent makes a mistake, it can go back to a checkpoint. If a user wants to explore another path, the agent can branch from an earlier state. This is useful for debugging and human-in-the-loop control, but it becomes dangerous once the agent has already called external tools.&lt;/p&gt;

&lt;p&gt;Traditional checkpoint/restore rolls back local state. It cannot undo side effects that have already happened in the external world. For ordinary programs, the usual answer is idempotency: retry the external call with the same request id, and the server returns the previous result instead of executing the action again. But an LLM agent is not an ordinary deterministic program. After restore, it may synthesize a semantically equivalent tool call with slightly different fields, such as a new UUID, timestamp, nonce, or reference number. The server cannot see that this is a retry of the same intent. It only sees a new valid request.&lt;/p&gt;

&lt;p&gt;This post is based on our arXiv paper &lt;a href="https://arxiv.org/abs/2603.20625" rel="noopener noreferrer"&gt;&lt;strong&gt;ACRFence: Preventing Semantic Rollback Attacks in Agent Checkpoint-Restore&lt;/strong&gt;&lt;/a&gt;. We introduce &lt;strong&gt;semantic rollback attacks&lt;/strong&gt;: attacks that exploit the gap between rolled-back agent state and non-rolled-back external state to trigger duplicate irreversible actions or revive consumed authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Transfer Example
&lt;/h2&gt;

&lt;p&gt;Suppose a user asks an agent to transfer $500 to Bob. The agent calls a bank API, generates a unique reference id &lt;code&gt;a1b2c3d4&lt;/code&gt;, and the transfer succeeds. The agent then calls Bob's MCP service to confirm the receipt. Bob's service returns a malformed response that crashes the agent. The framework restores the agent to a checkpoint before the transfer.&lt;/p&gt;

&lt;p&gt;After restore, the agent again executes the intent "transfer $500 to Bob." This time, however, it generates a different reference id, &lt;code&gt;f9a8b7c6&lt;/code&gt;. The bank's duplicate detection logic only sees two different references, so it accepts the second transfer. Bob receives $1000, while the agent's local view remains "I transferred once."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb49uvxu6e68vznmry8p3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb49uvxu6e68vznmry8p3.png" alt="Action Replay attack flow" width="800" height="558"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: Action Replay. A malicious MCP service triggers a crash after a successful transfer. After restore, the agent reissues the transfer with a new reference id, so the bank treats it as a new transaction.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The key point is not that the transfer API lacks idempotency. The problem is that the precondition for idempotency is broken. Systems such as Stripe and AWS ECS rely on the caller retrying with the same idempotency key or the same critical parameters. An LLM agent rethinks after restore and may produce a different token sequence. Even at temperature 0, byte-identical tool calls are not guaranteed. As a result, traditional server-side deduplication cannot recognize a "semantically same" retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: Local Rollback, External Progress
&lt;/h2&gt;

&lt;p&gt;Checkpoint/restore systems can save local process state, conversation context, variables, file descriptors, and related runtime state. They cannot automatically undo committed external effects. Transfers, emails, cloud resource creation, data deletion, and one-time token consumption are all irreversible side effects from the framework's point of view.&lt;/p&gt;

&lt;p&gt;In the agent setting, three facts combine badly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The agent state is rolled back.&lt;/strong&gt; The agent returns to an old checkpoint and no longer remembers that the transfer succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The external state is not rolled back.&lt;/strong&gt; The bank ledger, approval system, or cloud control plane still records the previous successful action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The post-restore tool call may differ.&lt;/strong&gt; The LLM may regenerate UUIDs, nonces, timestamps, or even change the target object under user guidance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxiv5f4q9aif0yojtxtif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxiv5f4q9aif0yojtxtif.png" alt="Divergence between agent state and external state" width="800" height="310"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: Restore only affects local agent state. External state keeps moving forward. This divergence is the core of semantic rollback attacks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This resembles the classic output commit problem in distributed systems: once output has been committed to the outside world, rolling back the local process alone cannot take the whole system back in time. The new twist is that an LLM agent may synthesize a different request after restore, blurring the boundary between "retry" and "new request."&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack 1: Action Replay
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Action Replay&lt;/strong&gt; targets irreversible tool calls that have already succeeded. The attacker does not need to control the bank or compromise the agent. It is enough to control a later service in the agent's tool chain, such as Bob's invoice-confirmation MCP service or a seemingly harmless callback endpoint.&lt;/p&gt;

&lt;p&gt;The attack path is direct:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent executes an irreversible action after a checkpoint, such as a transfer or cloud resource creation.&lt;/li&gt;
&lt;li&gt;The external service returns success, and the side effect is committed.&lt;/li&gt;
&lt;li&gt;An attacker-controlled later tool returns a malformed response, triggering crash or restore.&lt;/li&gt;
&lt;li&gt;The agent returns to the old checkpoint and repeats the same task.&lt;/li&gt;
&lt;li&gt;The LLM generates a fresh request id, so the target service cannot recognize the repeated intent and commits again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcls3mys0lmtpn12nceie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcls3mys0lmtpn12nceie.png" alt="Normal execution compared with attack execution" width="800" height="424"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: Normal execution transfers once. In the attack path, crash-induced restore causes the same semantic action to execute twice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In our experiments, we used Claude Code CLI backed by Qwen3-32B. External services were simulated as MCP tool servers: a bank service with UUID-based duplicate detection and a malicious payee service that crashes the agent after a successful transfer. Across 10 checkpoint/restore trials, all 10 produced duplicate commits. A no-checkpoint baseline produced none. This confirms that the vulnerability comes from the interaction between restore and external side effects, not from ordinary model randomness alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack 2: Authority Resurrection
&lt;/h2&gt;

&lt;p&gt;The second attack class is &lt;strong&gt;Authority Resurrection&lt;/strong&gt;, which targets one-time authorization tokens or short-lived credentials.&lt;/p&gt;

&lt;p&gt;Consider an enterprise data deletion workflow. The agent first obtains manager approval, and the approval service returns a one-time token. The agent uses that token to delete Alice's data, and the server marks the token as consumed. A user or malicious insider then rewinds the agent to the checkpoint immediately after approval was granted. In the agent's local state, the token appears again. In the external approval system, the token should already be consumed.&lt;/p&gt;

&lt;p&gt;If the target service validates tokens statelessly, for example by checking only a signature and expiration time, the agent may reuse the same token on another target, such as Bob's data. The audit log may show that the manager approved deletion for Alice, while Bob's data was also deleted. The discrepancy is visible only by correlating approval and execution logs.&lt;/p&gt;

&lt;p&gt;Our experiment simulated two approval services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Validation mode&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateless validation, checking only token signature&lt;/td&gt;
&lt;td&gt;2/2 reuse attempts succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateful validation, recording token consumption server-side&lt;/td&gt;
&lt;td&gt;All reuse attempts were rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This shows that checkpoint/restore can do more than duplicate financial side effects. It can also break authorization semantics by reviving authority that should have been consumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Not One Framework's Bug
&lt;/h2&gt;

&lt;p&gt;The paper surveys reports across multiple frameworks and communities. The concrete symptoms differ, but they point to the same boundary: restore, retry, approval, preemption, and human-in-the-loop flows can cause tool calls to execute more than once, while frameworks generally do not enforce exactly-once semantics at the tool boundary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework or system&lt;/th&gt;
&lt;th&gt;Observed issue type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;Tool nodes may re-execute after resume or interrupt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CrewAI&lt;/td&gt;
&lt;td&gt;Workflows run twice, causing repeated emails or actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google ADK&lt;/td&gt;
&lt;td&gt;Rewind documentation warns that external side effects are not undone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutoGen / OpenAI Agents&lt;/td&gt;
&lt;td&gt;Graph nodes or function calls are triggered repeatedly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code / Cursor&lt;/td&gt;
&lt;td&gt;Duplicate tool behavior around approval, checkpoint, or undo flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenHands / Vercel AI / LiveKit / n8n&lt;/td&gt;
&lt;td&gt;Duplicate messages, repeated tool calls, doubled token cost, or repeated charges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These cases do not mean every framework has the same bug. They show that "restoring agent state to the past" while "the external world remains in the present" is a systemic issue. Relying on developers to make tools idempotent is not enough, because the post-restore agent request may not be the same request.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACRFence: Replay-or-Fork at the Tool Boundary
&lt;/h2&gt;

&lt;p&gt;ACRFence does not try to make every LLM agent deterministic. Instead, it records irreversible effects at the tool boundary and enforces &lt;strong&gt;replay-or-fork&lt;/strong&gt; semantics after restore.&lt;/p&gt;

&lt;p&gt;ACRFence can be deployed as an MCP proxy or a similar tool-call proxy between the agent and external services. For each irreversible tool call, ACRFence records an effect log that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;thread and branch identifiers, to distinguish execution branches in the same session;&lt;/li&gt;
&lt;li&gt;tool name and arguments;&lt;/li&gt;
&lt;li&gt;return value or error;&lt;/li&gt;
&lt;li&gt;runtime context, such as process, network, and file-access context, which can be enriched by eBPF-based system-level monitors such as &lt;a href="https://github.com/eunomia-bpf/agentsight/" rel="noopener noreferrer"&gt;AgentSight&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;consumed credentials or authorization objects, when applicable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the agent restores from a checkpoint and issues another tool call, ACRFence does not immediately forward it. It first compares the new call with the historical effect log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantically equivalent: replay.&lt;/strong&gt; If the new call only changes non-intent fields such as request id or timestamp, while recipient, amount, resource target, and other intent fields are the same, ACRFence returns the previously recorded response without re-executing the external operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantically divergent: fork.&lt;/strong&gt; If the new call changes intent-critical fields, such as a different recipient or a different customer deletion target, ACRFence blocks the call, shows the prior effect log, and requires an explicit fork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credential reuse: reject or inform.&lt;/strong&gt; If the call tries to reuse a consumed token, ACRFence informs the agent before the request reaches the target service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use an analyzer LLM for semantic comparison instead of requiring every tool to provide a hand-written schema and idempotency rule. For example, two &lt;code&gt;transfer&lt;/code&gt; calls with different UUIDs but the same amount and recipient should be treated as the same intent. Two &lt;code&gt;delete_customer_data&lt;/code&gt; calls with the same approval token but different customer ids should be treated as dangerous divergence. The analyzer runs only on the restore path, not on every normal tool call.&lt;/p&gt;

&lt;p&gt;ACRFence aims to provide two guarantees:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Replay safety:&lt;/strong&gt; semantically equivalent irreversible calls after restore do not execute again; ACRFence returns the cached result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Divergence detection:&lt;/strong&gt; semantically different calls after restore must explicitly fork; they cannot silently inherit external effects or authority from an old branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How This Differs from Idempotency and Durable Execution
&lt;/h2&gt;

&lt;p&gt;Idempotency is still important, but it solves the problem of "the same request is retried." ACRFence works one level higher, at agent intent: request fields may change while intent stays the same, or the fields may look valid while intent has drifted to a new target.&lt;/p&gt;

&lt;p&gt;Durable execution systems usually require deterministic orchestrator logic, with nondeterministic values recorded as side effects and replayed on recovery. That works well for traditional workflows. LLM agents, however, generate their next action from context. Rather than assuming post-restore calls will be byte-identical, ACRFence treats divergence as expected and makes replay versus fork explicit at the tool boundary.&lt;/p&gt;

&lt;p&gt;In this division of labor, checkpoint/restore lets the agent return to an earlier state. ACRFence ensures that reconnecting that old state to the external world does not duplicate irreversible side effects or revive consumed authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Next Steps
&lt;/h2&gt;

&lt;p&gt;The work validates the two attack classes, while ACRFence itself remains a design that still needs a full implementation and system evaluation. Several challenges remain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The analyzer LLM may misclassify calls, so false replay and false fork risks need careful evaluation.&lt;/li&gt;
&lt;li&gt;An adaptive attacker who knows the comparison logic may craft ambiguous parameters to evade semantic detection.&lt;/li&gt;
&lt;li&gt;The boundary between "intent fields" and "non-intent fields" is not always obvious for every tool.&lt;/li&gt;
&lt;li&gt;The current experiments cover one model and one framework; more agent frameworks, models, and real tool ecosystems should be evaluated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core conclusion is clear: once agent frameworks introduce checkpoint, rewind, time travel, and branch exploration, external tool calls cannot rely only on traditional idempotency keys. The restore path is a new security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Checkpoint/restore makes AI agents easier to debug, recover, and steer across multiple execution paths. But once agents can call external tools, local rollback and external non-rollback create a semantic gap. Action Replay can turn one payment, one resource creation, or one email into many. Authority Resurrection can make consumed authorization reappear in local agent state.&lt;/p&gt;

&lt;p&gt;ACRFence records irreversible effects at the tool boundary and enforces replay-or-fork after restore: same intent replays the result without re-execution, different intent must explicitly fork, and consumed credentials cannot be silently reused. As more agent frameworks support checkpoint and time travel, this kind of tool-boundary semantics will become part of the reliability and security foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2603.20625" rel="noopener noreferrer"&gt;ACRFence: Preventing Semantic Rollback Attacks in Agent Checkpoint-Restore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://criu.org/" rel="noopener noreferrer"&gt;CRIU: Checkpoint/Restore In Userspace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.langchain.com/oss/python/langgraph/persistence" rel="noopener noreferrer"&gt;LangGraph Persistence and Time Travel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.langchain.com/oss/python/langgraph/durable-execution" rel="noopener noreferrer"&gt;LangGraph Durable Execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/2025-03-26" rel="noopener noreferrer"&gt;Model Context Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe API: Idempotent Requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/go/side-effects" rel="noopener noreferrer"&gt;Temporal Side Effects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/adk-docs/sessions/session/rewind/" rel="noopener noreferrer"&gt;Google ADK Session Rewind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hashicorp/vault/issues/28378" rel="noopener noreferrer"&gt;Vault issue #28378: single-use token reappears after snapshot restore&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Runtime Observability and Enforcement for Opaque AI Agents with eBPF: Beyond Sandboxes and Approvals</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Tue, 02 Jun 2026 11:11:21 +0000</pubDate>
      <link>https://dev.to/yunwei37/runtime-observability-and-enforcement-for-opaque-ai-agents-with-ebpf-beyond-sandboxes-and-approvals-8n6</link>
      <guid>https://dev.to/yunwei37/runtime-observability-and-enforcement-for-opaque-ai-agents-with-ebpf-beyond-sandboxes-and-approvals-8n6</guid>
      <description>&lt;p&gt;AI coding agents now run for hours, complete entire features end-to-end,&lt;br&gt;
optimize production GPU kernels, and merge thousands of pull requests&lt;br&gt;
autonomously. Meanwhile, most agent security still relies on human-in-the-loop&lt;br&gt;
approval, and Anthropic's own data shows users approve 93% of prompts without&lt;br&gt;
meaningful review. The result is predictable: products add bypass modes, users&lt;br&gt;
disable permission gates, and 65% of firms report agent security incidents.&lt;/p&gt;

&lt;p&gt;But the deeper problem is not approval fatigue. It is that the agent harness&lt;br&gt;
(the prompt loop, tool routing, permission logic, and sandbox defaults) is&lt;br&gt;
increasingly a third-party product the platform team did not write, running in a&lt;br&gt;
sandbox the platform team may not own. The harness is not a trusted security&lt;br&gt;
boundary. This post argues for separating agent security into three layers with&lt;br&gt;
three different owners: intent authorization (harness-owned), execution&lt;br&gt;
isolation (ownership contested), and side-effect verification (must be&lt;br&gt;
platform-owned). When the layers agree, you have confidence. When they&lt;br&gt;
disagree, you need independent observability and enforcement at the OS level to&lt;br&gt;
detect it, and that is exactly the layer most agent platforms are missing. We&lt;br&gt;
are building projects towards this direction:&lt;br&gt;
&lt;a href="https://github.com/eunomia-bpf/agentsight/" rel="noopener noreferrer"&gt;AgentSight&lt;/a&gt; for runtime observation and&lt;br&gt;
&lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane&lt;/a&gt; for runtime harness enforcement, both using eBPF to provide an&lt;br&gt;
independent runtime observability and enforcement below the agent harness.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Now: Complexity Up, Guardrails Behind
&lt;/h2&gt;

&lt;p&gt;The important change in 2026 is not that agents exist. It is the scale and&lt;br&gt;
duration of what they do.&lt;/p&gt;

&lt;p&gt;A year ago, the typical agent task was "fix this bug" or "write this function."&lt;br&gt;
In 2026, agents routinely run for hours on complex, multi-step work. OpenAI&lt;br&gt;
documented a Codex session that &lt;a href="https://developers.openai.com/blog/run-long-horizon-tasks-with-codex" rel="noopener noreferrer"&gt;ran for 25 hours uninterrupted&lt;/a&gt;,&lt;br&gt;
consuming 13 million tokens and producing 30,000 lines of code from a blank&lt;br&gt;
repository. Anthropic's agentic coding report cites a &lt;a href="https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf" rel="noopener noreferrer"&gt;12.5-million-line&lt;br&gt;
codebase change completed in a single 7-hour run&lt;/a&gt;. Meta's&lt;br&gt;
&lt;a href="https://engineering.fb.com/2026/04/02/developer-tools/kernelevolve-how-metas-ranking-engineer-agent-optimizes-ai-infrastructure/" rel="noopener noreferrer"&gt;KernelEvolve&lt;/a&gt; uses multi-agent coordination to write and optimize&lt;br&gt;
production GPU kernels, compressing work that previously required weeks of&lt;br&gt;
expert systems engineering into hours. On SWE-bench Verified, &lt;a href="https://www.vals.ai/benchmarks/swebench" rel="noopener noreferrer"&gt;top agents now&lt;br&gt;
resolve 60–70%&lt;/a&gt; of real GitHub issues, up from under 30% in early&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Devin has &lt;a href="https://cognition.ai/blog/devin-annual-performance-review-2025" rel="noopener noreferrer"&gt;merged hundreds of thousands of pull requests&lt;/a&gt;
across enterprise customers with a 67% merge rate. Goldman Sachs &lt;a href="https://www.cnbc.com/2025/07/11/goldman-sachs-autonomous-coder-pilot-marks-major-ai-milestone.html" rel="noopener noreferrer"&gt;deployed
hundreds of Devin instances&lt;/a&gt; across a 12,000-person engineering team.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Beyond coding, general-purpose autonomous agents have gone mainstream.&lt;br&gt;
&lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;, an open-source agent with&lt;br&gt;
over 300,000 GitHub stars, connects to LLMs and executes shell commands,&lt;br&gt;
browser automation, email, calendar, and file operations on the user's machine.&lt;br&gt;
CrowdStrike called it &lt;a href="https://www.crowdstrike.com/en-us/blog/what-security-teams-need-to-know-about-openclaw-ai-super-agent/" rel="noopener noreferrer"&gt;"the AI Super Agent" security teams need to worry&lt;br&gt;
about&lt;/a&gt;:&lt;br&gt;
between January and April 2026, &lt;a href="https://www.reco.ai/blog/openclaw-the-ai-agent-security-crisis-unfolding-right-now" rel="noopener noreferrer"&gt;470 security advisories&lt;/a&gt;&lt;br&gt;
were filed against it across three disclosure waves.&lt;/p&gt;

&lt;p&gt;These are not research demos. They are production workflows: background tasks,&lt;br&gt;
parallel execution, multi-hour sessions, end-to-end feature development, kernel&lt;br&gt;
optimization, and enterprise-scale code changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meanwhile, the guardrails designed to keep agents safe have not kept pace.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most agent security still relies on human-in-the-loop approval: a prompt asks&lt;br&gt;
the user to approve or deny each action before it executes. This works for short&lt;br&gt;
sessions with a few tool calls. It does not work when an agent makes hundreds of&lt;br&gt;
decisions over hours of autonomous operation.&lt;/p&gt;

&lt;p&gt;The evidence suggests that approval-based control is already failing in&lt;br&gt;
practice. Anthropic's own data shows that &lt;a href="https://www.anthropic.com/engineering/claude-code-auto-mode" rel="noopener noreferrer"&gt;Claude Code users approve 93% of&lt;br&gt;
permission prompts&lt;/a&gt;, a rate consistent with rubber-stamping&lt;br&gt;
rather than meaningful review. An independent stress test of Claude Code's auto&lt;br&gt;
mode found an &lt;a href="https://arxiv.org/abs/2604.04978" rel="noopener noreferrer"&gt;81% false negative rate&lt;/a&gt; on ambiguous&lt;br&gt;
state-changing actions, meaning the classifier allowed 4 out of 5 actions that&lt;br&gt;
should have required human review. Real incidents have followed: in documented&lt;br&gt;
cases, users running agents without permission gates had their &lt;a href="https://gist.github.com/hartphoenix/698eb8ef8b08ad2ce6a99cf7346cd7cc" rel="noopener noreferrer"&gt;home directories&lt;br&gt;
deleted&lt;/a&gt; by &lt;code&gt;rm -rf&lt;/code&gt; commands the agent generated. A 2026&lt;br&gt;
industry survey found that &lt;a href="https://www.kiteworks.com/cybersecurity-risk-management/ai-agent-security-incidents-2026/" rel="noopener noreferrer"&gt;65% of firms reported AI agent security&lt;br&gt;
incidents&lt;/a&gt;, primarily&lt;br&gt;
unauthorized data access, credential exposure, and exfiltration to external&lt;br&gt;
endpoints, with most involving organizations lacking proper agent access&lt;br&gt;
controls.&lt;/p&gt;

&lt;p&gt;Products have responded by adding bypass mechanisms. Claude Code offers&lt;br&gt;
&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;. Windsurf's Cascade agent &lt;a href="https://stackbuilt.co/blog/windsurf-vs-cursor-2026" rel="noopener noreferrer"&gt;proceeds&lt;br&gt;
autonomously&lt;/a&gt; where Cursor stops to ask. Community guides now&lt;br&gt;
focus on "how to safely use YOLO mode." Anthropic researcher Nicholas Carlini&lt;br&gt;
ran &lt;a href="https://x.com/nicholas_carlini" rel="noopener noreferrer"&gt;16 parallel Claude agents with permissions bypassed&lt;/a&gt;, with the&lt;br&gt;
caveat: "Run this in a container, not your actual machine."&lt;/p&gt;

&lt;p&gt;This is the tension: &lt;strong&gt;the more capable agents become, the more users want to&lt;br&gt;
let them run uninterrupted, and the less effective human-in-the-loop becomes as&lt;br&gt;
the primary security boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That tension is what creates the need for a different security model.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Accountability Gap
&lt;/h2&gt;

&lt;p&gt;The deeper issue is not just that agents are more capable. It is that the agent&lt;br&gt;
harness, the component that decides what the agent does, is increasingly a&lt;br&gt;
third-party product the platform team did not write.&lt;/p&gt;

&lt;p&gt;A modern agent harness is not a thin wrapper around a model. It includes a&lt;br&gt;
prompt loop, planning and retry logic, tool routing, MCP clients, permission&lt;br&gt;
modes, approval gates, hooks, memory, logs, credential handling, and sometimes&lt;br&gt;
sandbox defaults. In many deployments, that harness comes from a hosted&lt;br&gt;
coding-agent service or an open-source framework the platform team does not&lt;br&gt;
control.&lt;/p&gt;

&lt;p&gt;This is already visible across the ecosystem. GitHub Copilot's &lt;a href="https://docs.github.com/en/copilot/concepts/about-copilot-coding-agent" rel="noopener noreferrer"&gt;coding&lt;br&gt;
agent&lt;/a&gt; runs autonomously in GitHub Actions, researching&lt;br&gt;
repositories, creating plans, making changes, and opening pull requests. OpenAI&lt;br&gt;
&lt;a href="https://developers.openai.com/codex/cloud" rel="noopener noreferrer"&gt;Codex&lt;/a&gt; runs background tasks in sandboxed cloud environments with&lt;br&gt;
controlled network access. Claude Code runs cloud sessions in Anthropic-managed&lt;br&gt;
VMs with scoped credentials. Kubernetes SIG is defining &lt;a href="https://agent-sandbox.sigs.k8s.io/" rel="noopener noreferrer"&gt;Agent&lt;br&gt;
Sandbox&lt;/a&gt; for isolated, stateful agent workloads. Recent research&lt;br&gt;
datasets show &lt;a href="https://arxiv.org/abs/2602.09185" rel="noopener noreferrer"&gt;agent-authored pull requests at scale&lt;/a&gt; across real&lt;br&gt;
repositories.&lt;/p&gt;

&lt;p&gt;The ownership split is now explicit in major platforms. Anthropic's shared&lt;br&gt;
responsibility framework &lt;a href="https://www.anthropic.com/research/trustworthy-agents" rel="noopener noreferrer"&gt;divides agent security into four&lt;br&gt;
layers&lt;/a&gt; (Model, Harness, Tools, Environment) and&lt;br&gt;
stresses that an agent's behavior depends on all four working together, so the&lt;br&gt;
harness, tools, and environment, the layers shaped by the deploying party, are&lt;br&gt;
as decisive as the model itself. Anthropic itself notes that even together,&lt;br&gt;
these layered safeguards are not a guarantee. The question the framework&lt;br&gt;
leaves open is what happens when a failure crosses these layers, and whether&lt;br&gt;
the deployer has independent observability to detect it. In cloud infrastructure,&lt;br&gt;
the analogous gap in shared responsibility led to independent observability&lt;br&gt;
and audit services (CloudTrail, Config, GuardDuty) controlled by the&lt;br&gt;
customer, not the provider. Agent infrastructure has no equivalent yet: the&lt;br&gt;
deployer is told it owns harness, tools, and environment, but often has no&lt;br&gt;
independent way to verify what those layers actually did at runtime.&lt;/p&gt;

&lt;p&gt;GitHub's agentic&lt;br&gt;
workflow architecture starts from the premise that &lt;a href="https://github.blog/ai-and-ml/generative-ai/under-the-hood-security-architecture-of-github-agentic-workflows/" rel="noopener noreferrer"&gt;"agents cannot be trusted by&lt;br&gt;
default, especially in the presence of untrusted inputs"&lt;/a&gt;,&lt;br&gt;
using kernel-enforced communication boundaries that hold even if the agent&lt;br&gt;
container is compromised. OpenAI's Codex documentation &lt;a href="https://developers.openai.com/codex/agent-approvals-security" rel="noopener noreferrer"&gt;acknowledges&lt;/a&gt;&lt;br&gt;
that "devcontainers provide substantial protection, but they do not prevent&lt;br&gt;
every attack."&lt;/p&gt;

&lt;p&gt;The platform team still owns the repository, the CI runner, the Kubernetes&lt;br&gt;
cluster, the service accounts, the secrets, and the internal network. But the&lt;br&gt;
runtime acting on those assets may be opaque.&lt;/p&gt;

&lt;p&gt;There is also a second split that matters even more for platform teams: &lt;strong&gt;the&lt;br&gt;
sandbox may not be controlled by the environment owner either.&lt;/strong&gt; If the agent&lt;br&gt;
runs in a provider-managed cloud (Claude Code on the web runs in&lt;br&gt;
&lt;a href="https://docs.anthropic.com/en/docs/claude-code/security" rel="noopener noreferrer"&gt;Anthropic-managed isolated VMs&lt;/a&gt; with scoped credential&lt;br&gt;
proxies; Codex runs in &lt;a href="https://developers.openai.com/codex/concepts/sandboxing" rel="noopener noreferrer"&gt;OpenAI-managed containers&lt;/a&gt;), the&lt;br&gt;
platform team cannot attach its own monitoring, modify isolation policy, or&lt;br&gt;
inspect the sandbox internals. Even Anthropic's own managed agent architecture&lt;br&gt;
explicitly &lt;a href="https://www.anthropic.com/engineering/managed-agents" rel="noopener noreferrer"&gt;decouples the "brain" (Claude + harness) from the&lt;br&gt;
"hands" (sandboxes)&lt;/a&gt;, treating containers as disposable and ensuring tokens are never reachable&lt;br&gt;
from the sandbox where generated code runs. This is good architecture, but it is the provider's architecture,&lt;br&gt;
not the platform team's.&lt;/p&gt;

&lt;p&gt;When agents run locally or on self-hosted infrastructure (GitHub now &lt;a href="https://github.blog/changelog/2025-10-28-copilot-coding-agent-now-supports-self-hosted-runners/" rel="noopener noreferrer"&gt;supports&lt;br&gt;
self-hosted runners&lt;/a&gt; for its coding agent, and Kubernetes&lt;br&gt;
Agent Sandbox provides &lt;a href="https://agent-sandbox.sigs.k8s.io/" rel="noopener noreferrer"&gt;gVisor/Kata-backed isolation&lt;/a&gt; under the&lt;br&gt;
platform operator's control), the environment owner can wrap the agent in its&lt;br&gt;
own sandbox and observability. When agents run in provider-managed&lt;br&gt;
environments, independent observability and enforcement must move to the&lt;br&gt;
boundaries the platform team does control.&lt;/p&gt;

&lt;p&gt;This creates the accountability gap: &lt;strong&gt;the platform team is responsible for&lt;br&gt;
production impact from a workload it cannot fully inspect, running in a sandbox&lt;br&gt;
it may not own.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The old mental model was simple: the agent is risky, so put it in a sandbox.&lt;br&gt;
The new reality has a different trust boundary: the agent and its harness are&lt;br&gt;
part of the workload, and the environment owner needs independent runtime observability.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three Layers, Three Questions
&lt;/h2&gt;

&lt;p&gt;MCP, sandboxes, and OS-level observability are all necessary for agent security.&lt;br&gt;
They are not interchangeable. Each answers a fundamentally different question,&lt;br&gt;
and each has a different owner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intent authorization&lt;/strong&gt; (MCP, tool gateways, approval prompts) answers: what&lt;br&gt;
is the agent &lt;em&gt;supposed&lt;/em&gt; to do? Which tools may it call, under which identity,&lt;br&gt;
with which scopes? This is the right place to enforce access control before a&lt;br&gt;
dangerous action happens. But a tool approval is not proof of side effects. A&lt;br&gt;
framework log saying "run tests" does not prove that the process tree only ran&lt;br&gt;
tests. An MCP server can be well-authenticated and still be part of a workflow&lt;br&gt;
that causes unexpected local effects. This layer is typically owned or mediated&lt;br&gt;
by the agent harness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution isolation&lt;/strong&gt; (containers, VMs, network policy, namespaces) answers:&lt;br&gt;
what &lt;em&gt;can&lt;/em&gt; the agent reach? Which files, network endpoints, credentials, and&lt;br&gt;
syscalls are available? This is the right place to limit blast radius. But a&lt;br&gt;
sandbox does not automatically record what the agent attempted within its&lt;br&gt;
constraints: which process read a secret, which subprocess opened a network&lt;br&gt;
connection, whether the sandbox policy matched the approved intent. This layer's&lt;br&gt;
ownership is contested: it may belong to the agent provider, the platform team,&lt;br&gt;
or both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side-effect verification&lt;/strong&gt; (OS/runtime observability) answers: what &lt;em&gt;actually&lt;br&gt;
happened&lt;/em&gt;? Which processes ran, which files were read, which network connections&lt;br&gt;
were opened, which credentials were accessed? This layer provides facts about&lt;br&gt;
execution, independent of what the framework reported or the sandbox intended.&lt;br&gt;
This layer must be owned by the environment operator. Otherwise there is no&lt;br&gt;
independent source of truth.&lt;/p&gt;

&lt;p&gt;The security model is the combination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authorize intent  →  isolate execution  →  verify side effects
(harness-owned)      (ownership contested)  (must be platform-owned)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When all three layers agree, you have confidence. When they disagree, you need&lt;br&gt;
OS-level observability and controls, independent of the harness, to detect the&lt;br&gt;
mismatch, contain the damage, and reconstruct what happened.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Independence Matters
&lt;/h2&gt;

&lt;p&gt;The reason to keep these layers independent follows from the trends above, but&lt;br&gt;
also from a deeper structural argument about ownership and trust.&lt;/p&gt;
&lt;h3&gt;
  
  
  Approval fatigue
&lt;/h3&gt;

&lt;p&gt;When approvals are relaxed (as the evidence above shows they routinely are),&lt;br&gt;
the other two layers must compensate. If you auto-approve routine actions, you&lt;br&gt;
need an independent way to verify what those actions actually did. If you&lt;br&gt;
bypass permissions for speed, you need stronger containment and stronger observability.&lt;/p&gt;
&lt;h3&gt;
  
  
  Harness opacity
&lt;/h3&gt;

&lt;p&gt;When the harness is opaque, application-level telemetry cannot be the sole&lt;br&gt;
source of truth. OpenTelemetry GenAI conventions and framework-level tracing are&lt;br&gt;
valuable when you own the framework. But opaque agent apps, closed-source&lt;br&gt;
runtimes, hosted execution, stripped binaries, and arbitrary subprocess trees&lt;br&gt;
can all break the assumption that the framework trace is complete. OpenClaw&lt;br&gt;
illustrates this directly: its behavior is &lt;a href="https://arxiv.org/html/2603.27517v2" rel="noopener noreferrer"&gt;non-deterministic across&lt;br&gt;
runs&lt;/a&gt;, producing different tool-calling&lt;br&gt;
sequences for the same input, which makes static code review inadequate and&lt;br&gt;
drove multiple teams to build dedicated runtime observability tools for it&lt;br&gt;
(&lt;a href="https://www.sentinelone.com/blog/oneclaw-discovery-and-observability-for-the-agentic-era/" rel="noopener noreferrer"&gt;OneClaw&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://www.epsilla.com/blogs/clawtrace-launch-openclaw-agent-observability" rel="noopener noreferrer"&gt;ClawTrace&lt;/a&gt;).&lt;br&gt;
Security researchers have already found &lt;a href="https://thehackernews.com/2025/12/researchers-uncover-30-flaws-in-ai.html" rel="noopener noreferrer"&gt;30+ vulnerabilities across all major AI&lt;br&gt;
IDEs&lt;/a&gt; (Cursor, Copilot, Windsurf, Claude Code), enabling data theft&lt;br&gt;
and remote code execution through prompt injection into agent tool chains.&lt;/p&gt;

&lt;p&gt;The MCP layer records intended tool calls. The OS layer records actual side&lt;br&gt;
effects. When the harness is opaque, the gap between these two is exactly where&lt;br&gt;
security incidents live.&lt;/p&gt;
&lt;h3&gt;
  
  
  The trust boundary is an ownership boundary
&lt;/h3&gt;

&lt;p&gt;The deepest reason for independence is that the three layers serve different&lt;br&gt;
owners with different incentives.&lt;/p&gt;

&lt;p&gt;The harness provider's goal is to complete the user's task: maximize&lt;br&gt;
autonomous coding productivity, reduce permission friction, deliver results.&lt;br&gt;
The platform team's goal is to protect the repository, secrets, cluster,&lt;br&gt;
CI runner, internal network, and production APIs. These goals are not opposed,&lt;br&gt;
but they are not identical. When they conflict, when the fastest path to task&lt;br&gt;
completion involves reading credentials, opening network connections, or&lt;br&gt;
modifying files outside the workspace, the harness will optimize for&lt;br&gt;
completion unless an independent boundary stops it.&lt;/p&gt;

&lt;p&gt;This is why &lt;a href="https://arxiv.org/abs/2602.09947" rel="noopener noreferrer"&gt;Bhattarai and Vu argue&lt;/a&gt; that&lt;br&gt;
"probabilistic compliance is not compliance": training-based and&lt;br&gt;
classifier-based defenses may reduce empirical attack rates, but cannot provide&lt;br&gt;
deterministic guarantees under adversarial conditions. Only architectural&lt;br&gt;
enforcement can. Red Hat's experience deploying multi-agent systems on Kagenti&lt;br&gt;
frames the same insight differently: this is &lt;a href="https://next.redhat.com/2026/03/05/zero-trust-ai-agents-on-kubernetes-what-i-learned-deploying-multi-agent-systems-on-kagenti/" rel="noopener noreferrer"&gt;"a multi-tenancy problem disguised&lt;br&gt;
as an AI problem"&lt;/a&gt;. The agent is an untrusted tenant. The&lt;br&gt;
platform needs the same kind of isolation, identity, and audit controls it would&lt;br&gt;
apply to any untrusted workload.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/" rel="noopener noreferrer"&gt;OWASP Top 10 for Agentic Applications&lt;/a&gt; reinforces this&lt;br&gt;
framing. Its top risk (ASI01, Agent Goal Hijacking) is that "agents cannot&lt;br&gt;
reliably distinguish instructions from data," and a single malicious input from a&lt;br&gt;
repository, issue, MCP response, or web page can redirect the agent to perform&lt;br&gt;
harmful actions using its legitimate tools. This is not a hypothetical:&lt;br&gt;
&lt;a href="https://bishopfox.com/blog/otto-support-confused-deputy" rel="noopener noreferrer"&gt;Bishop Fox demonstrated&lt;/a&gt; confused deputy attacks where&lt;br&gt;
instructions embedded in support tickets caused agents to exfiltrate data using&lt;br&gt;
authorized tools, with "the user's name on every audit log entry." &lt;a href="https://www.docker.com/blog/mcp-horror-stories-github-prompt-injection/" rel="noopener noreferrer"&gt;Docker&lt;br&gt;
documented&lt;/a&gt; a GitHub prompt injection chain where a&lt;br&gt;
malicious issue hijacked an MCP-connected agent to steal confidential data from&lt;br&gt;
private repositories.&lt;/p&gt;

&lt;p&gt;The threat model for platform teams therefore has three adversary categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Which layer fails&lt;/th&gt;
&lt;th&gt;Runtime observability detects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Compromised agent&lt;/strong&gt; (prompt injection, malicious repo/issue/MCP response)&lt;/td&gt;
&lt;td&gt;Intent layer: agent is tricked into unintended actions&lt;/td&gt;
&lt;td&gt;Actual side effects diverge from stated intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Untrusted harness&lt;/strong&gt; (opaque permission logic, incomplete logs, unauditable internal state)&lt;/td&gt;
&lt;td&gt;Cannot verify harness completeness&lt;/td&gt;
&lt;td&gt;OS-level facts independent of harness reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Sandbox escape or policy gap&lt;/strong&gt; (container breakout, mounted credentials, network bypass)&lt;/td&gt;
&lt;td&gt;Isolation layer fails or is misconfigured&lt;/td&gt;
&lt;td&gt;Detects behavior outside expected sandbox boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AISI's &lt;a href="https://arxiv.org/abs/2603.02277" rel="noopener noreferrer"&gt;SandboxEscapeBench&lt;/a&gt; makes the third category concrete:&lt;br&gt;
frontier models can reliably escape container sandboxes under&lt;br&gt;
misconfigurations that plausibly occur in real systems, and the researchers&lt;br&gt;
discovered four unintended escape paths the benchmark designers had missed.&lt;br&gt;
Their recommendation: &lt;a href="https://arxiv.org/abs/2603.02277" rel="noopener noreferrer"&gt;"treat plain Docker isolation as insufficient by&lt;br&gt;
default."&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In all three cases, OS/runtime observability is the independent control&lt;br&gt;
that lets the platform team detect the problem, regardless of which other layer&lt;br&gt;
failed.&lt;/p&gt;
&lt;h2&gt;
  
  
  What OS-Level Monitoring Captures
&lt;/h2&gt;

&lt;p&gt;At the OS/runtime layer, observability captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Process lineage&lt;/strong&gt;: the full tree from agent to subprocess to network call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File access&lt;/strong&gt;: which paths were read or written, including credential paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network behavior&lt;/strong&gt;: connections, destinations, timing, data volume&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container metadata&lt;/strong&gt;: namespace, cgroup, pod identity, service account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subprocess behavior&lt;/strong&gt;: commands that bypass framework instrumentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This data is collected below the application layer, typically via eBPF,&lt;br&gt;
audit subsystems, or kernel instrumentation. It does not require modifying the&lt;br&gt;
agent app. Its key property is independence: the observability is owned and&lt;br&gt;
operated by the environment operator, not by the agent provider.&lt;/p&gt;

&lt;p&gt;This makes cross-layer comparison possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Framework report:    run tests
Sandbox policy:      workspace mounted, registry allowed, SA token mounted
OS observability:       agent → shell → python → curl
                     read: /var/run/secrets/.../token
                     connect: unknown external host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer saw a different part of the event. Without the OS layer, this is an&lt;br&gt;
undetected credential theft: a service account token read and exfiltrated while&lt;br&gt;
the framework logged only "running tests." The platform team discovers the&lt;br&gt;
breach days later, if at all. OS-level observability is what turns an invisible data leak into a real-time&lt;br&gt;
detection.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deployment Reality
&lt;/h2&gt;

&lt;p&gt;OS-level observability is strongest when you control the host, node, or VM where the&lt;br&gt;
agent executes. If the agent runs entirely in a provider-managed environment,&lt;br&gt;
you may not be able to attach eBPF inside it.&lt;/p&gt;

&lt;p&gt;In that case, the same model applies, but observability shifts to the boundaries you do control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository permissions and branch protection&lt;/li&gt;
&lt;li&gt;Scoped credentials with minimal lifetime&lt;/li&gt;
&lt;li&gt;CI/CD and GitHub audit logs&lt;/li&gt;
&lt;li&gt;Network proxies and webhook events&lt;/li&gt;
&lt;li&gt;Artifact access logs&lt;/li&gt;
&lt;li&gt;Provider-supplied session logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This observability is weaker than owning the runtime boundary, but it is still better&lt;br&gt;
than treating the agent transcript as the only source of truth.&lt;/p&gt;

&lt;p&gt;The design question for platform teams is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where is the lowest layer I actually control?&lt;br&gt;
That is where independent observability should live.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  AgentSight and ActPlane: Observe, Then Enforce
&lt;/h2&gt;

&lt;p&gt;We are building open-source tools that implement the verification layer&lt;br&gt;
described above, each addressing a different half of the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/eunomia-bpf/agentsight/" rel="noopener noreferrer"&gt;AgentSight&lt;/a&gt;&lt;/strong&gt; is a zero-instrumentation observability tool for&lt;br&gt;
AI agents. It uses eBPF to intercept SSL/TLS traffic and monitor process&lt;br&gt;
behavior at the system boundary, with no code changes, no SDKs, and no&lt;br&gt;
framework integration required. Point it at any agent process (Claude Code,&lt;br&gt;
Codex, a custom Python agent) and it captures the full picture: process&lt;br&gt;
lineage, LLM API calls (prompts and completions), file access, network&lt;br&gt;
connections, and tool invocations, all correlated into a live timeline. This is&lt;br&gt;
the "see what actually happened" layer. Because it operates below the&lt;br&gt;
application, it works even when the agent runtime is opaque, closed-source, or&lt;br&gt;
running arbitrary subprocesses that bypass framework-level tracing. In&lt;br&gt;
practice, this means detecting credential access, data exfiltration attempts,&lt;br&gt;
and unauthorized network connections as they happen, not days later when an&lt;br&gt;
external party reports the breach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane&lt;/a&gt;&lt;/strong&gt; is an OS-level harness for AI agents. Where AgentSight&lt;br&gt;
observes, ActPlane enforces. You write behavioral contracts in a YAML-based&lt;br&gt;
rule language (labeled information-flow control, not static allow-lists), and&lt;br&gt;
ActPlane compiles them into an eBPF program that enforces constraints at the&lt;br&gt;
kernel level: every &lt;code&gt;exec&lt;/code&gt;, file open, and network connect in the agent's&lt;br&gt;
entire process tree is checked against the policy. When a rule is violated,&lt;br&gt;
ActPlane blocks the action and feeds a human-readable reason back to the agent&lt;br&gt;
through its hook system, so the agent self-corrects rather than failing&lt;br&gt;
silently. The rule language supports data-flow tracking across fork/exec&lt;br&gt;
chains, causal ordering ("run tests before committing"), and staleness&lt;br&gt;
invalidation, going well beyond what sandboxes or tool-layer guards can&lt;br&gt;
express.&lt;/p&gt;

&lt;p&gt;The two tools are complementary. AgentSight provides runtime observability:&lt;br&gt;
independent, below-the-application visibility into what the agent did. ActPlane&lt;br&gt;
provides the enforcement plane: deterministic, kernel-level guarantees about&lt;br&gt;
what the agent cannot do. Together they implement the "verify side effects"&lt;br&gt;
layer of the three-layer model, independent of the harness provider and&lt;br&gt;
independent of who owns the sandbox.&lt;/p&gt;

&lt;p&gt;Both are possible implementations of this architecture, not the only ones.&lt;br&gt;
The important point is the separation: observe and enforce at a layer the&lt;br&gt;
environment operator controls, regardless of which agent runtime sits above.&lt;/p&gt;

&lt;p&gt;This also addresses ecosystem gaps Anthropic identifies: the need for&lt;br&gt;
cross-deployment security telemetry sharing and open standards for agent&lt;br&gt;
security. Independent runtime observability that travels with the workload,&lt;br&gt;
rather than being locked to a specific harness or provider, is the foundation&lt;br&gt;
for both.&lt;/p&gt;
&lt;h2&gt;
  
  
  Practical Checklist
&lt;/h2&gt;

&lt;p&gt;If you are building or evaluating an agent platform, ask these questions at&lt;br&gt;
each layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intent authorization (MCP / tool access):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are MCP servers allowlisted?&lt;/li&gt;
&lt;li&gt;Are OAuth scopes minimal and audience-bound?&lt;/li&gt;
&lt;li&gt;Are local MCP servers treated as code execution risk?&lt;/li&gt;
&lt;li&gt;Are high-risk tools gated by human approval?&lt;/li&gt;
&lt;li&gt;Are tool calls logged with enough context for audit?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Execution isolation (sandboxing):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is filesystem access default-deny or broad workspace mount?&lt;/li&gt;
&lt;li&gt;Can the agent reach cloud metadata endpoints?&lt;/li&gt;
&lt;li&gt;Is network egress restricted by domain, IP, or proxy?&lt;/li&gt;
&lt;li&gt;Are service account tokens mounted into the environment?&lt;/li&gt;
&lt;li&gt;Are process, memory, CPU, and runtime duration bounded?&lt;/li&gt;
&lt;li&gt;Who owns the sandbox policy: the platform team or the agent provider?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Side-effect verification (runtime observability):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you reconstruct process lineage for an agent session?&lt;/li&gt;
&lt;li&gt;Can you see file and credential access below the framework?&lt;/li&gt;
&lt;li&gt;Can you correlate network egress with pod, service account, and command?&lt;/li&gt;
&lt;li&gt;Can you detect mismatch between tool intent and OS side effects?&lt;/li&gt;
&lt;li&gt;Can you replay an incident without trusting only framework logs?&lt;/li&gt;
&lt;li&gt;Can you demonstrate to auditors (SOC 2, ISO 27001) how automated agent
access to production data and credentials is monitored and logged?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Guardrail integration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which side effects should be blocked immediately?&lt;/li&gt;
&lt;li&gt;Which should trigger alert or human review?&lt;/li&gt;
&lt;li&gt;Which policies belong in MCP config, sandbox config, Kubernetes policy,
eBPF/LSM, or network controls?&lt;/li&gt;
&lt;li&gt;What happens when framework logs and OS-level observability disagree?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Agent runtimes are becoming more capable, more managed, and more opaque. The&lt;br&gt;
security model cannot depend on any single layer, especially when the layers&lt;br&gt;
have different owners.&lt;/p&gt;

&lt;p&gt;The harness is not a trusted boundary. The sandbox ownership depends on the&lt;br&gt;
deployment model. The only layer the environment operator can guarantee it&lt;br&gt;
owns is OS/runtime observability.&lt;/p&gt;

&lt;p&gt;MCP authorizes intent. Sandboxes constrain execution. OS-level observability verifies side&lt;br&gt;
effects. Each is necessary; none is sufficient. The practical model is their&lt;br&gt;
separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authorize intent  →  isolate execution  →  verify side effects
(harness-owned)      (ownership contested)  (must be platform-owned)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation details vary by deployment, but the separation, and the&lt;br&gt;
ownership question, is the part that should remain stable.&lt;/p&gt;

&lt;p&gt;If you are exploring this space, &lt;a href="https://github.com/eunomia-bpf/agentsight/" rel="noopener noreferrer"&gt;AgentSight&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane&lt;/a&gt; are our open-source starting points for the observation&lt;br&gt;
and enforcement layers respectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/copilot/concepts/about-copilot-coding-agent" rel="noopener noreferrer"&gt;GitHub Docs: About Copilot coding agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/ai-and-ml/generative-ai/under-the-hood-security-architecture-of-github-agentic-workflows/" rel="noopener noreferrer"&gt;GitHub: Security Architecture of Agentic Workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2025-10-28-copilot-coding-agent-now-supports-self-hosted-runners/" rel="noopener noreferrer"&gt;GitHub: Copilot coding agent supports self-hosted runners&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/cloud" rel="noopener noreferrer"&gt;OpenAI Codex cloud documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/blog/run-long-horizon-tasks-with-codex" rel="noopener noreferrer"&gt;OpenAI: Run long horizon tasks with Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/agent-approvals-security" rel="noopener noreferrer"&gt;OpenAI: Codex Agent Approvals &amp;amp; Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf" rel="noopener noreferrer"&gt;Anthropic 2026 Agentic Coding Trends Report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/research/trustworthy-agents" rel="noopener noreferrer"&gt;Anthropic: Trustworthy Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/claude-code-auto-mode" rel="noopener noreferrer"&gt;Anthropic Engineering: Claude Code auto mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/claude-code-sandboxing" rel="noopener noreferrer"&gt;Anthropic Engineering: Making Claude Code More Secure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/engineering/managed-agents" rel="noopener noreferrer"&gt;Anthropic Engineering: Scaling Managed Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www-cdn.anthropic.com/43ec7e770925deabc3f0bc1dbf0133769fd03812.pdf" rel="noopener noreferrer"&gt;Anthropic NIST RFI on Agentic Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/security" rel="noopener noreferrer"&gt;Claude Code security documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/permission-modes" rel="noopener noreferrer"&gt;Claude Code permission modes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;MCP Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/authorization" rel="noopener noreferrer"&gt;MCP Authorization documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agent-sandbox.sigs.k8s.io/" rel="noopener noreferrer"&gt;Kubernetes SIGs Agent Sandbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/how-to/agent-sandbox" rel="noopener noreferrer"&gt;Google Cloud: Agent Sandbox on GKE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://engineering.fb.com/2026/04/02/developer-tools/kernelevolve-how-metas-ranking-engineer-agent-optimizes-ai-infrastructure/" rel="noopener noreferrer"&gt;Meta KernelEvolve&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.vals.ai/benchmarks/swebench" rel="noopener noreferrer"&gt;SWE-Bench Verified Leaderboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cognition.ai/blog/devin-annual-performance-review-2025" rel="noopener noreferrer"&gt;Devin's 2025 Performance Review&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cnbc.com/2025/07/11/goldman-sachs-autonomous-coder-pilot-marks-major-ai-milestone.html" rel="noopener noreferrer"&gt;Goldman Sachs autonomous coder pilot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://next.redhat.com/2026/03/05/zero-trust-ai-agents-on-kubernetes-what-i-learned-deploying-multi-agent-systems-on-kagenti/" rel="noopener noreferrer"&gt;Red Hat: Zero trust AI agents on Kubernetes with Kagenti&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2602.09185" rel="noopener noreferrer"&gt;AIDev: Studying AI Coding Agents on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2605.07135" rel="noopener noreferrer"&gt;Agentic Workflow Injection in GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2604.04978" rel="noopener noreferrer"&gt;Measuring the Permission Gate: Claude Code Auto Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2602.09947" rel="noopener noreferrer"&gt;Trustworthy Agentic AI Requires Deterministic Architectural Boundaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2604.13630" rel="noopener noreferrer"&gt;SafeHarness: Security Architecture for LLM-based Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2603.02277" rel="noopener noreferrer"&gt;SandboxEscapeBench: Can AI Agents Escape Their Sandboxes?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/" rel="noopener noreferrer"&gt;OWASP Top 10 for Agentic Applications 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bishopfox.com/blog/otto-support-confused-deputy" rel="noopener noreferrer"&gt;Bishop Fox: The Confused Deputy, MCP Attack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/mcp-horror-stories-github-prompt-injection/" rel="noopener noreferrer"&gt;Docker: MCP Horror Stories, GitHub Prompt Injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thehackernews.com/2025/12/researchers-uncover-30-flaws-in-ai.html" rel="noopener noreferrer"&gt;30+ Vulnerabilities in AI Coding Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kiteworks.com/cybersecurity-risk-management/ai-agent-security-incidents-2026/" rel="noopener noreferrer"&gt;AI Agent Security Incidents Hit 65% of Firms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bvp.com/atlas/securing-ai-agents-the-defining-cybersecurity-challenge-of-2026" rel="noopener noreferrer"&gt;Bessemer: Securing AI agents in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infoq.com/articles/securing-autonomous-ai-agents-kubernetes/" rel="noopener noreferrer"&gt;InfoQ: Securing Autonomous AI Agents on Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.crowdstrike.com/en-us/blog/what-security-teams-need-to-know-about-openclaw-ai-super-agent/" rel="noopener noreferrer"&gt;CrowdStrike: What Security Teams Need to Know About OpenClaw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reco.ai/blog/openclaw-the-ai-agent-security-crisis-unfolding-right-now" rel="noopener noreferrer"&gt;Reco.ai: The OpenClaw Agent Security Crisis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2603.27517v2" rel="noopener noreferrer"&gt;OpenClaw Security Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sentinelone.com/blog/oneclaw-discovery-and-observability-for-the-agentic-era/" rel="noopener noreferrer"&gt;SentinelOne: OneClaw Discovery and Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.epsilla.com/blogs/clawtrace-launch-openclaw-agent-observability" rel="noopener noreferrer"&gt;Epsilla: ClawTrace Agent Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//agentsight_paper.md"&gt;AgentSight blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eunomia-bpf/agentsight/" rel="noopener noreferrer"&gt;AgentSight repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eunomia-bpf/ActPlane" rel="noopener noreferrer"&gt;ActPlane repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>ai</category>
      <category>security</category>
      <category>observability</category>
    </item>
    <item>
      <title>When CPU Noise Slows Down GPU Inference: Measuring Scheduler and IRQ Impact with eBPF</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Sun, 31 May 2026 23:35:33 +0000</pubDate>
      <link>https://dev.to/yunwei37/when-cpu-noise-slows-down-gpu-inference-measuring-scheduler-and-irq-impact-with-ebpf-egg</link>
      <guid>https://dev.to/yunwei37/when-cpu-noise-slows-down-gpu-inference-measuring-scheduler-and-irq-impact-with-ebpf-egg</guid>
      <description>&lt;p&gt;GPU inference often looks like a GPU problem, but the CPU still sits on the critical path. It prepares inputs, launches CUDA kernels, manages synchronization, handles runtime calls, and shares cores with system work, interrupts, and other tenants. If that CPU-side launch path is delayed, the GPU can be left waiting even when the GPU kernels themselves are fast.&lt;/p&gt;

&lt;p&gt;This post asks a concrete question: when an LLM inference workload is running on a GPU, how much do Linux CPU scheduling decisions and IRQ handling actually matter?&lt;/p&gt;

&lt;p&gt;To answer it, we built an eBPF tracing tool, &lt;code&gt;cuda_sched_trace&lt;/code&gt;, that records CUDA kernel launches, scheduler context switches, and hard/soft IRQ events with nanosecond timestamps. We then ran Qwen3 0.6B inference under clean and noisy-neighbor conditions: CPU load from &lt;code&gt;stress-ng&lt;/code&gt;, network load from &lt;code&gt;iperf3&lt;/code&gt;, disk load from &lt;code&gt;fio&lt;/code&gt;, a combined heavy-load case, and a mitigation case using CPU pinning and priority adjustment.&lt;/p&gt;

&lt;p&gt;The short version: in a clean environment, scheduler and IRQ overhead are small. Under production-like noisy-neighbor conditions, they can become very real. Combined CPU, network, and disk interference reduced throughput by &lt;strong&gt;20.5%&lt;/strong&gt;, while simple CPU pinning reduced context switches by &lt;strong&gt;96.3%&lt;/strong&gt; and recovered most of the lost throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CPU Scheduling Shows Up in GPU Inference
&lt;/h2&gt;

&lt;p&gt;Modern GPU workloads, particularly LLM inference and training, require tight coordination between CPU and GPU execution. The CPU is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preparing input data and kernel parameters&lt;/li&gt;
&lt;li&gt;launching GPU kernels through CUDA APIs&lt;/li&gt;
&lt;li&gt;managing memory transfers and synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An interruption to that CPU-side workflow can delay GPU kernel submission. In the worst case, the GPU has available compute capacity but no new work to execute.&lt;/p&gt;

&lt;p&gt;The motivation comes partly from Meta's work on &lt;code&gt;sched_ext&lt;/code&gt; for AI training optimization, where production issues include "IRQs preempting our important tasks." Network interrupts (&lt;code&gt;NET_RX&lt;/code&gt;/&lt;code&gt;NET_TX&lt;/code&gt;) and block device interrupts can matter for large distributed training jobs, and custom scheduling policies can improve AI workload performance by 5-20%.&lt;/p&gt;

&lt;p&gt;But the impact is workload-dependent. A single-node LLM inference loop is not the same as distributed training with all-reduce traffic. Before investing in custom scheduling, we wanted measurements that separate scheduler problems from normal application behavior.&lt;/p&gt;

&lt;p&gt;The study has four goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure the baseline impact of CPU scheduling on GPU kernel launches.&lt;/li&gt;
&lt;li&gt;Characterize IRQ interference patterns and their performance cost.&lt;/li&gt;
&lt;li&gt;Quantify noisy-neighbor impact under CPU, network, disk, and combined load.&lt;/li&gt;
&lt;li&gt;Evaluate how much CPU pinning and priority adjustment help.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tracing the Launch Path
&lt;/h2&gt;

&lt;p&gt;We developed &lt;code&gt;cuda_sched_trace&lt;/code&gt;, an eBPF-based tracing tool that combines CUDA API uprobes, Linux scheduler tracepoints, and IRQ tracepoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  CUDA API Tracing
&lt;/h3&gt;

&lt;p&gt;The tool attaches uprobes to CUDA Driver and Runtime APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Attach to CUDA Driver API&lt;/span&gt;
&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uprobe/cuLaunchKernel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;trace_cuLaunchKernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Capture: timestamp, pid, tid, grid/block dimensions, shared memory, stream&lt;/span&gt;
    &lt;span class="c1"&gt;// Mark process as GPU process for scheduler tracking&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Attach to CUDA Runtime API&lt;/span&gt;
&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uprobe/cudaLaunchKernel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;trace_cudaLaunchKernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uprobe/cudaDeviceSynchronize"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;trace_cudaDeviceSynchronize_enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uretprobe/cudaDeviceSynchronize"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;trace_cudaDeviceSynchronize_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pt_regs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scheduler Event Tracing
&lt;/h3&gt;

&lt;p&gt;Scheduler activity is captured through &lt;code&gt;sched_switch&lt;/code&gt;, filtered to GPU-related processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tp_btf/sched_switch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sched_switch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;preempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;task_struct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only track if prev or next is a GPU process&lt;/span&gt;
    &lt;span class="c1"&gt;// Record: timestamp, prev/next pid, off-cpu/on-cpu duration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  IRQ Tracing
&lt;/h3&gt;

&lt;p&gt;Hard and soft IRQs are tracked through kernel tracepoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tp_btf/irq_handler_entry"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;irq_handler_entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;irq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;irqaction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Track hard IRQ entry, record IRQ number and handler name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tp_btf/irq_handler_exit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;irq_handler_exit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;irq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;irqaction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Calculate IRQ duration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tp_btf/softirq_entry"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;softirq_entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;vec_nr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Track soft IRQ: TIMER, NET_RX, NET_TX, BLOCK, SCHED, RCU, etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tp_btf/softirq_exit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;BPF_PROG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;softirq_exit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;vec_nr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Calculate soft IRQ duration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data path is straightforward: the GPU application issues CUDA calls; eBPF programs observe CUDA, scheduler, and IRQ events in kernel space; events are sent through a BPF ring buffer; analysis scripts parse the resulting CSV.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                         User Space                               │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐  │
│  │ GPU App     │    │ cuda_sched  │    │ Analysis Scripts    │  │
│  │ (qwen3.cu)  │    │ _trace      │    │ (Python)            │  │
│  └──────┬──────┘    └──────┬──────┘    └──────────┬──────────┘  │
│         │                  │                       │             │
│         │ CUDA calls       │ perf_event            │ CSV parsing │
│         ▼                  ▼                       ▼             │
├─────────────────────────────────────────────────────────────────┤
│                         Kernel Space                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐  │
│  │ uprobes     │    │ tracepoints │    │ BPF Ring Buffer     │  │
│  │ (CUDA API)  │    │ (sched,irq) │    │ (Event Queue)       │  │
│  └─────────────┘    └─────────────┘    └─────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benchmark and Environment
&lt;/h2&gt;

&lt;p&gt;The benchmark is Qwen3 0.6B LLM inference using &lt;code&gt;qwen3.cu&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Qwen3-0.6B-FP32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;td&gt;Single-turn Q&amp;amp;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;"What is eBPF?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;~30-50 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel Pattern&lt;/td&gt;
&lt;td&gt;Burst submission (~950 launches per token)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU Memory&lt;/td&gt;
&lt;td&gt;~3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This benchmark is useful because it resembles modern LLM inference, mixes compute-bound and memory-bound kernels, shows a clear burst submission pattern, and produces a measurable throughput metric in tokens per second.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Specification&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;24 cores (specific model TBD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;NVIDIA GPU with CUDA support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Sufficient for model + system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Linux 6.15.11-061511-generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;BTF-enabled for CO-RE eBPF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUDA&lt;/td&gt;
&lt;td&gt;Driver API + Runtime API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We used three interference tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;stress-ng&lt;/td&gt;
&lt;td&gt;CPU load&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--cpu 0 --cpu-method fft&lt;/code&gt; (all cores)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iperf3&lt;/td&gt;
&lt;td&gt;Network I/O&lt;/td&gt;
&lt;td&gt;Server + Client, 10 parallel streams, 60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fio&lt;/td&gt;
&lt;td&gt;Disk I/O&lt;/td&gt;
&lt;td&gt;&lt;code&gt;randwrite, bs=4k, iodepth=32, 4 jobs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full experiment has six scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Interference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;Clean environment&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy CPU&lt;/td&gt;
&lt;td&gt;CPU-intensive&lt;/td&gt;
&lt;td&gt;stress-ng on all cores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Network&lt;/td&gt;
&lt;td&gt;Network I/O&lt;/td&gt;
&lt;td&gt;iperf3 localhost loopback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Disk&lt;/td&gt;
&lt;td&gt;Disk I/O&lt;/td&gt;
&lt;td&gt;fio random write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy Load&lt;/td&gt;
&lt;td&gt;Combined&lt;/td&gt;
&lt;td&gt;CPU + Network + Disk simultaneously&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimized&lt;/td&gt;
&lt;td&gt;CPU pinning&lt;/td&gt;
&lt;td&gt;stress-ng + taskset -c 0-3 + nice -n -10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Data collection follows the same pattern in every run:&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;# Start tracing&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./cuda_sched_trace &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; trace.csv 2&amp;gt; trace.log &amp;amp;
&lt;span class="nv"&gt;TRACE_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="c"&gt;# Run benchmark&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;qwen3.cu
/usr/bin/time &lt;span class="nt"&gt;-v&lt;/span&gt; ./runcu Qwen3-0.6B-FP32.gguf &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"What is eBPF?"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; 1

&lt;span class="c"&gt;# Stop tracing&lt;/span&gt;
&lt;span class="nb"&gt;sudo kill&lt;/span&gt; &lt;span class="nt"&gt;-SIGINT&lt;/span&gt; &lt;span class="nv"&gt;$TRACE_PID&lt;/span&gt;

&lt;span class="c"&gt;# Analyze results&lt;/span&gt;
python3 analyze_scheduler_impact.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Analysis Method
&lt;/h2&gt;

&lt;p&gt;The central analysis compares consecutive CUDA kernel launches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launch_i -&amp;gt; [interval] -&amp;gt; Launch_i+1

Group A: Launches with NO context switch in interval (normal flow)
Group B: Launches with context switch in interval (preempted)

Preemption Penalty = median(Group B interval) - median(Group A interval)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To compare runs of different lengths, scheduler and IRQ counts are normalized per 1,000 kernel launches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sched/1K = (Total Context Switches / Total Kernel Launches) x 1000
IRQ/1K = (Total IRQs / Total Kernel Launches) x 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Performance impact is reported as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Slowdown % = (Baseline tok/s - Scenario tok/s) / Baseline tok/s x 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  RQ1: Does CPU Scheduler Significantly Impact GPU Performance in Clean Environments?
&lt;/h2&gt;

&lt;p&gt;The first question is whether scheduler preemption matters when the machine is otherwise clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment design&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Condition: clean system, no artificial interference&lt;/li&gt;
&lt;li&gt;Metrics: context switch frequency, preemption penalty, total runtime impact&lt;/li&gt;
&lt;li&gt;Analysis: launch-pair comparison with and without context switches&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Runtime&lt;/td&gt;
&lt;td&gt;79.5 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel Launches&lt;/td&gt;
&lt;td&gt;51,464&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context Switches&lt;/td&gt;
&lt;td&gt;592 (7.44 Hz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OFF-CPU Time&lt;/td&gt;
&lt;td&gt;7.88 ms (0.01%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Launch-pair analysis shows that almost every consecutive launch pair is unaffected by context switches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Percentage&lt;/th&gt;
&lt;th&gt;P50 Interval&lt;/th&gt;
&lt;th&gt;P90 Interval&lt;/th&gt;
&lt;th&gt;P99 Interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No Context Switch&lt;/td&gt;
&lt;td&gt;51,401&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;2 us&lt;/td&gt;
&lt;td&gt;4 us&lt;/td&gt;
&lt;td&gt;4 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With Context Switch&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;15.3 ms&lt;/td&gt;
&lt;td&gt;15.5 ms&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The median preemption penalty is &lt;strong&gt;15.3 ms&lt;/strong&gt;. That is large for the affected pairs, but only 62 pairs were affected.&lt;/p&gt;

&lt;p&gt;Tail-latency attribution confirms that most outliers are not caused by scheduler preemption:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Percentile&lt;/th&gt;
&lt;th&gt;Total Outliers&lt;/th&gt;
&lt;th&gt;With Context Switch&lt;/th&gt;
&lt;th&gt;Attribution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P95+&lt;/td&gt;
&lt;td&gt;2,580&lt;/td&gt;
&lt;td&gt;62 (2.4%)&lt;/td&gt;
&lt;td&gt;97.6% application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P99+&lt;/td&gt;
&lt;td&gt;515&lt;/td&gt;
&lt;td&gt;62 (12.0%)&lt;/td&gt;
&lt;td&gt;88.0% application&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The total scheduler impact is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Impact = Affected Pairs x Penalty = 62 x 15ms = 0.93 seconds
Percentage = 0.93 / 79.5 = 1.2%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Finding:&lt;/strong&gt; in clean environments, CPU scheduler impact is minimal at &lt;strong&gt;1.2%&lt;/strong&gt;. The vast majority of kernel launch pairs, &lt;strong&gt;99.9%&lt;/strong&gt;, are unaffected by context switches. Tail latency mostly comes from application behavior such as token-generation boundaries, not scheduler preemption.&lt;/p&gt;

&lt;h2&gt;
  
  
  RQ2: What Is the Impact of IRQ Interrupts on GPU Performance?
&lt;/h2&gt;

&lt;p&gt;The second question is whether IRQs directly interfere with the CPU-side launch path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment design&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Condition: clean system with IRQ tracing enabled&lt;/li&gt;
&lt;li&gt;Metrics: IRQ frequency, duration, type distribution&lt;/li&gt;
&lt;li&gt;Analysis: IRQ time as percentage of total runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Runtime&lt;/td&gt;
&lt;td&gt;4.99 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel Launches&lt;/td&gt;
&lt;td&gt;125,236&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft IRQs&lt;/td&gt;
&lt;td&gt;653 events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard IRQs&lt;/td&gt;
&lt;td&gt;0 events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Soft IRQ type distribution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Total Time&lt;/th&gt;
&lt;th&gt;Avg Time&lt;/th&gt;
&lt;th&gt;Max Time&lt;/th&gt;
&lt;th&gt;Percentage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TIMER&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;td&gt;0.77 ms&lt;/td&gt;
&lt;td&gt;2.4 us&lt;/td&gt;
&lt;td&gt;30.1 us&lt;/td&gt;
&lt;td&gt;49%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RCU&lt;/td&gt;
&lt;td&gt;291&lt;/td&gt;
&lt;td&gt;0.40 ms&lt;/td&gt;
&lt;td&gt;1.4 us&lt;/td&gt;
&lt;td&gt;17.2 us&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NET_RX&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;0.13 ms&lt;/td&gt;
&lt;td&gt;4.5 us&lt;/td&gt;
&lt;td&gt;14.0 us&lt;/td&gt;
&lt;td&gt;4.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCHED&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;0.07 ms&lt;/td&gt;
&lt;td&gt;4.9 us&lt;/td&gt;
&lt;td&gt;18.9 us&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total IRQ impact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total IRQ Time: 1.38 ms
Percentage of Runtime: 0.0276%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are real reasons to worry about IRQs: direct handler time, cache pollution, CPU pipeline disruption, and delay accumulation on critical paths. But for this local inference workload, actual IRQ impact is small.&lt;/p&gt;

&lt;p&gt;The reason is the workload shape. Qwen3 submits about 950 launches in a burst lasting less than 100 us, so IRQs rarely land inside the burst. Most IRQs happen between bursts during CPU compute. TIMER interrupts dominate and have a small cache footprint. There is little network I/O, so &lt;code&gt;NET_RX&lt;/code&gt; appears only 30 times, and there are no hard IRQs from NVMe or SSD block-device interrupts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding:&lt;/strong&gt; IRQ impact is negligible for local LLM inference at &lt;strong&gt;0.0276%&lt;/strong&gt;. This does not mean IRQs never matter. Distributed training with network communication or on-the-fly data loading can see much higher IRQ impact, estimated around &lt;strong&gt;5-20%&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  RQ3: How Do Noisy Neighbors Affect GPU Performance?
&lt;/h2&gt;

&lt;p&gt;The third question is the most production-relevant one: what happens when the GPU workload shares a machine with other CPU, network, and disk activity?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment design&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Interference&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Reference point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy CPU&lt;/td&gt;
&lt;td&gt;stress-ng (all cores)&lt;/td&gt;
&lt;td&gt;CPU contention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Network&lt;/td&gt;
&lt;td&gt;iperf3 (10 streams)&lt;/td&gt;
&lt;td&gt;Network IRQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Disk&lt;/td&gt;
&lt;td&gt;fio (4 jobs, randwrite)&lt;/td&gt;
&lt;td&gt;Block IRQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy Load&lt;/td&gt;
&lt;td&gt;All three combined&lt;/td&gt;
&lt;td&gt;Production simulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimized&lt;/td&gt;
&lt;td&gt;CPU stress + taskset + nice&lt;/td&gt;
&lt;td&gt;Mitigation test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;Normalized metrics per 1,000 kernel launches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Launches&lt;/th&gt;
&lt;th&gt;Sched/1K&lt;/th&gt;
&lt;th&gt;Soft IRQ/1K&lt;/th&gt;
&lt;th&gt;Hard IRQ/1K&lt;/th&gt;
&lt;th&gt;IRQ Time (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;56,882&lt;/td&gt;
&lt;td&gt;22.8&lt;/td&gt;
&lt;td&gt;5.8&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy CPU&lt;/td&gt;
&lt;td&gt;61,184&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11,932.8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Network&lt;/td&gt;
&lt;td&gt;154,394&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;2.7&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Disk&lt;/td&gt;
&lt;td&gt;126,670&lt;/td&gt;
&lt;td&gt;29.3&lt;/td&gt;
&lt;td&gt;3.9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heavy Load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99,424&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6,044.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimized&lt;/td&gt;
&lt;td&gt;108,984&lt;/td&gt;
&lt;td&gt;445.2&lt;/td&gt;
&lt;td&gt;2.8&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Performance impact:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;tok/s&lt;/th&gt;
&lt;th&gt;Runtime (s)&lt;/th&gt;
&lt;th&gt;Slowdown&lt;/th&gt;
&lt;th&gt;Context Switch Increase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;54.77&lt;/td&gt;
&lt;td&gt;3.00&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy CPU&lt;/td&gt;
&lt;td&gt;49.93&lt;/td&gt;
&lt;td&gt;4.15&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;524x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Network&lt;/td&gt;
&lt;td&gt;53.23&lt;/td&gt;
&lt;td&gt;7.22&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;0.26x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noisy Disk&lt;/td&gt;
&lt;td&gt;54.95&lt;/td&gt;
&lt;td&gt;5.60&lt;/td&gt;
&lt;td&gt;-0.3%&lt;/td&gt;
&lt;td&gt;1.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heavy Load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43.56&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.97&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;265x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimized&lt;/td&gt;
&lt;td&gt;53.75&lt;/td&gt;
&lt;td&gt;5.10&lt;/td&gt;
&lt;td&gt;1.9%&lt;/td&gt;
&lt;td&gt;19.5x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Scenario Analysis
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Noisy CPU (&lt;code&gt;stress-ng&lt;/code&gt;)&lt;/strong&gt; causes the most direct scheduling pressure. Context switches increase &lt;strong&gt;524x&lt;/strong&gt;, from 22.8 to 11,932.8 per 1,000 launches, and throughput drops by &lt;strong&gt;8.8%&lt;/strong&gt;. The mechanism is simple: the CFS scheduler time-slices between the GPU process and &lt;code&gt;stress-ng&lt;/code&gt; workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Noisy Network (&lt;code&gt;iperf3&lt;/code&gt;)&lt;/strong&gt; behaves differently. Context switches actually decrease, because the network load changes CPU competition patterns, while soft IRQs rise slightly. Throughput drops only &lt;strong&gt;2.8%&lt;/strong&gt;. In this local setup, network I/O primarily shows up as IRQ overhead rather than scheduler pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Noisy Disk (&lt;code&gt;fio&lt;/code&gt;)&lt;/strong&gt; introduces the first hard IRQs, corresponding to block-device interrupts, but context switches remain low and throughput is effectively unchanged at &lt;strong&gt;-0.3%&lt;/strong&gt; slowdown. Disk I/O has little impact on this workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy Load (CPU + Network + Disk)&lt;/strong&gt; is the worst case. Throughput drops by &lt;strong&gt;20.5%&lt;/strong&gt;, and scheduler events rise to 6,044.6 per 1,000 launches, a &lt;strong&gt;265x&lt;/strong&gt; increase over baseline. Interestingly, that is only &lt;strong&gt;50.7%&lt;/strong&gt; of the context-switch rate in the Noisy CPU case. The interference sources compete with each other, but their combined effect is still worst overall.&lt;/p&gt;

&lt;p&gt;Heavy-load soft IRQ breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Total Time&lt;/th&gt;
&lt;th&gt;Avg Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RCU&lt;/td&gt;
&lt;td&gt;213&lt;/td&gt;
&lt;td&gt;217.4 us&lt;/td&gt;
&lt;td&gt;1.0 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIMER&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;122.9 us&lt;/td&gt;
&lt;td&gt;7.2 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCHED&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;33.3 us&lt;/td&gt;
&lt;td&gt;6.7 us&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Finding:&lt;/strong&gt; noisy neighbors significantly affect GPU performance. Combined CPU, network, and disk interference causes &lt;strong&gt;20.5%&lt;/strong&gt; degradation. The signatures differ by source: CPU contention increases context switches, network I/O affects IRQ overhead, disk I/O introduces block interrupts with little throughput impact here, and combined load is worst due to cumulative effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  RQ4: Can CPU Pinning Effectively Mitigate Scheduler Impact?
&lt;/h2&gt;

&lt;p&gt;The fourth question is whether a simple deployment-level mitigation helps before reaching for a custom scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment design&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Baseline: Noisy CPU scenario with &lt;code&gt;stress-ng&lt;/code&gt; on all cores&lt;/li&gt;
&lt;li&gt;Optimized: same &lt;code&gt;stress-ng&lt;/code&gt; load, but the GPU process runs with:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;taskset -c 0-3&lt;/code&gt; to pin it to cores 0-3&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nice -n -10&lt;/code&gt; to give it higher priority&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Noisy CPU&lt;/th&gt;
&lt;th&gt;Optimized&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sched/1K&lt;/td&gt;
&lt;td&gt;11,932.8&lt;/td&gt;
&lt;td&gt;445.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.3% reduction&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tok/s&lt;/td&gt;
&lt;td&gt;49.93&lt;/td&gt;
&lt;td&gt;53.75&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.6% improvement&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vs. Baseline&lt;/td&gt;
&lt;td&gt;8.8% slower&lt;/td&gt;
&lt;td&gt;1.9% slower&lt;/td&gt;
&lt;td&gt;Significant recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CPU pinning and priority adjustment recover most of the lost throughput. But the optimized case still has 445.2 scheduler events per 1,000 launches, compared with 22.8 in the clean baseline. That is still &lt;strong&gt;19.5x&lt;/strong&gt; higher than baseline.&lt;/p&gt;

&lt;p&gt;Complete elimination is hard because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;stress-ng&lt;/code&gt; workers may still be scheduled on cores 0-3.&lt;/li&gt;
&lt;li&gt;System daemons and kernel threads cannot be fully excluded by &lt;code&gt;taskset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;IRQ affinity may still route interrupts to pinned cores.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For stronger isolation, the next steps are kernel-level isolation and IRQ placement:&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;# 1. Use isolcpus kernel parameter (boot time)&lt;/span&gt;
&lt;span class="nv"&gt;isolcpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4-7 &lt;span class="nv"&gt;nohz_full&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4-7

&lt;span class="c"&gt;# 2. Bind GPU process to isolated cores&lt;/span&gt;
taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 4-7 ./gpu_app

&lt;span class="c"&gt;# 3. Bind IRQs away from GPU cores&lt;/span&gt;
&lt;span class="nb"&gt;echo &lt;/span&gt;0-3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /proc/irq/&lt;span class="k"&gt;*&lt;/span&gt;/smp_affinity_list

&lt;span class="c"&gt;# 4. Use cgroups for CPU isolation&lt;/span&gt;
cgcreate &lt;span class="nt"&gt;-g&lt;/span&gt; cpu:gpu_workload
cgset &lt;span class="nt"&gt;-r&lt;/span&gt; cpuset.cpus&lt;span class="o"&gt;=&lt;/span&gt;4-7 gpu_workload
cgexec &lt;span class="nt"&gt;-g&lt;/span&gt; cpu:gpu_workload ./gpu_app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Finding:&lt;/strong&gt; CPU pinning is highly effective. It reduces context switches by &lt;strong&gt;96.3%&lt;/strong&gt; and recovers &lt;strong&gt;7.6%&lt;/strong&gt; throughput. But full recovery under heavy load requires deeper isolation such as &lt;code&gt;isolcpus&lt;/code&gt;, &lt;code&gt;nohz_full&lt;/code&gt;, cpusets, and IRQ affinity management.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Results Mean
&lt;/h2&gt;

&lt;p&gt;The results point to four practical insights.&lt;/p&gt;

&lt;p&gt;First, environment matters. Scheduler impact ranges from &lt;strong&gt;1.2%&lt;/strong&gt; in a clean environment to &lt;strong&gt;20.5%&lt;/strong&gt; under combined heavy load. Optimizing the scheduler on a quiet dedicated server may not be worth the complexity. On a shared host, it can be the difference between stable and degraded inference.&lt;/p&gt;

&lt;p&gt;Second, workload shape matters. Qwen3 has bursty kernel submission, roughly 950 launches in less than 100 us per token burst. That shape makes it resilient to many IRQs because interrupts usually occur between bursts. A different workload with continuous network communication, streaming input, or tighter CPU-GPU handoff might behave differently.&lt;/p&gt;

&lt;p&gt;Third, interference sources have distinct signatures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interference&lt;/th&gt;
&lt;th&gt;Primary Impact&lt;/th&gt;
&lt;th&gt;Secondary Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;Context switches&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;IRQ overhead&lt;/td&gt;
&lt;td&gt;Slight scheduling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk&lt;/td&gt;
&lt;td&gt;Hard IRQs&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combined&lt;/td&gt;
&lt;td&gt;All of above&lt;/td&gt;
&lt;td&gt;Worst overall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fourth, simple mitigations work, but only up to a point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU pinning: very effective, &lt;strong&gt;96%&lt;/strong&gt; context-switch reduction&lt;/li&gt;
&lt;li&gt;Priority adjustment: helpful but limited&lt;/li&gt;
&lt;li&gt;Full isolation: requires kernel configuration and IRQ affinity management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison with Meta's sched_ext Findings
&lt;/h2&gt;

&lt;p&gt;Our results differ from Meta's AI training observations because the workload is different.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Meta (AI Training)&lt;/th&gt;
&lt;th&gt;Our Study (LLM Inference)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary Issue&lt;/td&gt;
&lt;td&gt;Network IRQ (NET_RX)&lt;/td&gt;
&lt;td&gt;CPU scheduling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IRQ Impact&lt;/td&gt;
&lt;td&gt;5-20%&lt;/td&gt;
&lt;td&gt;0.03% (local inference)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimization&lt;/td&gt;
&lt;td&gt;sched_ext layer&lt;/td&gt;
&lt;td&gt;taskset + nice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload&lt;/td&gt;
&lt;td&gt;Distributed training&lt;/td&gt;
&lt;td&gt;Single-node inference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key difference is communication. Distributed training constantly exchanges data through all-reduce, making &lt;code&gt;NET_RX&lt;/code&gt; a major bottleneck. Local inference has minimal network I/O, so the dominant issue under noise is CPU scheduling rather than network interrupts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;There are several limits to this study:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;eBPF tracing itself adds &lt;strong&gt;1-5%&lt;/strong&gt; overhead.&lt;/li&gt;
&lt;li&gt;The tool only supports CUDA, not OpenCL or HIP.&lt;/li&gt;
&lt;li&gt;The trace does not include GPU-side execution timing, so it cannot directly measure actual kernel runtime.&lt;/li&gt;
&lt;li&gt;IRQ attribution is limited: the trace cannot always identify which process caused a given IRQ.&lt;/li&gt;
&lt;li&gt;The experiments use a single GPU and do not cover multi-GPU behavior.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Practical Recommendations
&lt;/h2&gt;

&lt;p&gt;For production deployments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;th&gt;Expected Benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated Server&lt;/td&gt;
&lt;td&gt;No optimization needed&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared Server (light)&lt;/td&gt;
&lt;td&gt;taskset + nice&lt;/td&gt;
&lt;td&gt;5-10% improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared Server (heavy)&lt;/td&gt;
&lt;td&gt;isolcpus + IRQ affinity&lt;/td&gt;
&lt;td&gt;15-20% improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;CPU limits + nodeSelector&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The decision tree is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is GPU workload latency-sensitive?
├── No -&amp;gt; No optimization needed
└── Yes -&amp;gt; Is server shared?
    ├── No -&amp;gt; Monitor only, optimize if needed
    └── Yes -&amp;gt; How heavy is colocated load?
        ├── Light -&amp;gt; taskset + nice
        └── Heavy -&amp;gt; isolcpus + dedicated cores
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CPU scheduling and IRQ handling do not always matter for GPU inference, but they matter under the conditions where production systems often run: shared hosts, background load, and noisy neighbors.&lt;/p&gt;

&lt;p&gt;The clean baseline shows minimal overhead: &lt;strong&gt;1.2%&lt;/strong&gt; scheduler impact and &lt;strong&gt;0.03%&lt;/strong&gt; IRQ impact. But combined CPU, network, and disk interference causes &lt;strong&gt;20.5%&lt;/strong&gt; throughput degradation. CPU pinning cuts context switches by &lt;strong&gt;96.3%&lt;/strong&gt; and recovers most of the lost performance, but not all of it.&lt;/p&gt;

&lt;p&gt;The practical lesson is to measure first. Use tracing to identify whether your workload is scheduler-bound, IRQ-sensitive, or mostly application-limited. Then choose the mitigation that matches the signature: CPU pinning for CPU contention, IRQ affinity for interrupt interference, I/O tuning for block-device pressure, and full CPU isolation when the workload is latency-sensitive and colocated load is heavy.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Meta Platforms, Inc. "Accelerating AI Training with sched_ext." Linux Plumbers Conference 2025. &lt;a href="https://lpc.events/event/19/contributions/2039/" rel="noopener noreferrer"&gt;https://lpc.events/event/19/contributions/2039/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NVIDIA Corporation. "CUDA Driver API Reference." &lt;a href="https://docs.nvidia.com/cuda/cuda-driver-api/" rel="noopener noreferrer"&gt;https://docs.nvidia.com/cuda/cuda-driver-api/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux Kernel Documentation. "BPF Documentation." &lt;a href="https://www.kernel.org/doc/html/latest/bpf/" rel="noopener noreferrer"&gt;https://www.kernel.org/doc/html/latest/bpf/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;stress-ng. "A tool to load and stress a computer system." &lt;a href="https://github.com/ColinIanKing/stress-ng" rel="noopener noreferrer"&gt;https://github.com/ColinIanKing/stress-ng&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;iperf3. "A TCP, UDP, and SCTP network bandwidth measurement tool." &lt;a href="https://github.com/esnet/iperf" rel="noopener noreferrer"&gt;https://github.com/esnet/iperf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;fio. "Flexible I/O Tester." &lt;a href="https://github.com/axboe/fio" rel="noopener noreferrer"&gt;https://github.com/axboe/fio&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ebpf</category>
      <category>gpu</category>
      <category>llm</category>
      <category>performance</category>
    </item>
    <item>
      <title>eBPF Tutorial by Example 50: Composable Traffic Control with TCX Links</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Sun, 31 May 2026 23:34:31 +0000</pubDate>
      <link>https://dev.to/yunwei37/ebpf-tutorial-by-example-50-composable-traffic-control-with-tcx-links-5hmo</link>
      <guid>https://dev.to/yunwei37/ebpf-tutorial-by-example-50-composable-traffic-control-with-tcx-links-5hmo</guid>
      <description>&lt;p&gt;Ever tried attaching multiple BPF programs to the TC ingress path and got frustrated managing qdisc handles, filter priorities, and the &lt;code&gt;tc&lt;/code&gt; CLI? Or needed one application's TC program to coexist safely with another's without accidentally overwriting it? Traditional &lt;code&gt;cls_bpf&lt;/code&gt; attachment through &lt;code&gt;tc&lt;/code&gt; works, but it inherits decades of queueing discipline plumbing that was never designed for the BPF-centric world. What if you could attach, order, and manage TC programs using the same link-based API that XDP and cgroup programs already enjoy?&lt;/p&gt;

&lt;p&gt;This is what &lt;strong&gt;TCX&lt;/strong&gt; (Traffic Control eXtension) solves. Introduced by Daniel Borkmann and merged in Linux 6.6, TCX provides a lightweight, fd-based multi-program attach infrastructure for the TC ingress and egress data path. Programs get BPF link semantics (safe ownership, auto-detachment on close, and explicit ordering through &lt;code&gt;BPF_F_BEFORE&lt;/code&gt; / &lt;code&gt;BPF_F_AFTER&lt;/code&gt; flags) without touching a single qdisc or filter priority.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll attach two TCX ingress programs to the loopback interface, place one before the other, query the kernel's live chain state, and generate traffic to verify execution order.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The complete source code: &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/50-tcx" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/50-tcx&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction to TCX: Why Classic TC Attachment Needed a Rethink
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem: Qdisc Plumbing and Unsafe Ownership
&lt;/h3&gt;

&lt;p&gt;Classic &lt;code&gt;tc&lt;/code&gt; BPF attachment (&lt;code&gt;cls_bpf&lt;/code&gt;) was bolted onto the existing Traffic Control framework. To attach a BPF program, you first needed a &lt;code&gt;clsact&lt;/code&gt; qdisc on the interface, then added a filter with a handle and priority. This worked fine for a single operator, but created real problems in cloud-native environments where multiple applications need to attach TC programs to the same interface:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No ownership model&lt;/strong&gt;: A &lt;code&gt;tc filter del&lt;/code&gt; from one application can accidentally remove another application's program. There's no protection against this because classic tc filters are identified by handle/priority, not by the process that created them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Priority conflicts&lt;/strong&gt;: Two applications might pick the same priority number. The second attachment silently replaces the first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Permanent attachment by default&lt;/strong&gt;: Classic tc filters persist until explicitly removed. If the application that attached a filter crashes without cleanup, the filter remains, potentially with stale program logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI dependency&lt;/strong&gt;: Even with libbpf, the attachment model was tied to netlink, the same mechanism the &lt;code&gt;tc&lt;/code&gt; CLI uses. This meant your BPF application was sharing a control plane with every other tc user on the system.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These issues became acute in projects like Cilium, where the BPF dataplane needs to coexist with third-party CNI plugins, observability agents, and security tools that all want to hook into TC.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Link-Based Multi-Program Management
&lt;/h3&gt;

&lt;p&gt;TCX takes a fundamentally different approach. Instead of piggybacking on qdisc infrastructure, it provides a dedicated, qdisc-less extension point for BPF programs at the TC ingress and egress hooks. The key design principles:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BPF Link Semantics&lt;/strong&gt;: &lt;code&gt;bpf_program__attach_tcx()&lt;/code&gt; creates a &lt;code&gt;BPF_LINK_TYPE_TCX&lt;/code&gt; link. Like XDP links and cgroup links, TCX links give you safe ownership: the link is pinned to the file descriptor, auto-detaches when the fd is closed, and cannot be accidentally overridden by another application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit Ordering&lt;/strong&gt;: Instead of implicit priority numbers, you place programs relative to each other using &lt;code&gt;BPF_F_BEFORE&lt;/code&gt; and &lt;code&gt;BPF_F_AFTER&lt;/code&gt;. You can also use &lt;code&gt;BPF_F_REPLACE&lt;/code&gt; to atomically swap a specific program. All operations support an &lt;code&gt;expected_revision&lt;/code&gt; field that prevents race conditions during concurrent modifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chain Return Codes&lt;/strong&gt;: TCX defines simplified return codes that make multi-program composition explicit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Return Code&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCX_NEXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;Non-terminating; pass the packet to the next program in the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCX_PASS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Accept the packet and terminate the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCX_DROP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Drop the packet and terminate the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCX_REDIRECT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Redirect the packet and terminate the chain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unknown return codes are mapped to &lt;code&gt;TCX_NEXT&lt;/code&gt; for forward compatibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coexistence with Classic TC&lt;/strong&gt;: TCX links can coexist with traditional &lt;code&gt;cls_bpf&lt;/code&gt; filters on the same interface. The kernel runs TCX programs first, then falls through to classic &lt;code&gt;tcf_classify()&lt;/code&gt; if present. This allows gradual migration from classic tc to TCX without a disruptive cutover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the eBPF Program
&lt;/h2&gt;

&lt;p&gt;Our BPF object contains two programs that demonstrate chain composition. Here is the complete source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;linux/bpf.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_endian.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef TCX_NEXT
#define TCX_NEXT -1
#endif
&lt;/span&gt;
&lt;span class="cp"&gt;#ifndef TCX_PASS
#define TCX_PASS 0
#endif
&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;LICENSE&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;stats_hits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;classifier_hits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;last_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;__u16&lt;/span&gt; &lt;span class="n"&gt;last_protocol&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;last_ifindex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tcx/ingress"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;tcx_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;__sk_buff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stats_hits&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;last_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;last_protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_ntohs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;last_ifindex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ifindex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TCX_NEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tcx/ingress"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;tcx_classifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;__sk_buff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;skb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;classifier_hits&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TCX_PASS&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;Let's walk through this step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Section Names: &lt;code&gt;SEC("tcx/ingress")&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;SEC("tcx/ingress")&lt;/code&gt; annotation tells libbpf that this program should be attached to the TCX ingress hook rather than the classic TC classifier. This is not just a naming convention; libbpf maps this section name to &lt;code&gt;BPF_PROG_TYPE_SCHED_CLS&lt;/code&gt; with the appropriate attach type for TCX. The corresponding egress variant is &lt;code&gt;SEC("tcx/egress")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that &lt;code&gt;SEC("tc")&lt;/code&gt;, &lt;code&gt;SEC("classifier")&lt;/code&gt;, and &lt;code&gt;SEC("action")&lt;/code&gt; are now considered deprecated by libbpf in favor of the &lt;code&gt;tcx/*&lt;/code&gt; section names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global Variables as Counters
&lt;/h3&gt;

&lt;p&gt;Instead of using a BPF map for counters, we use global variables (&lt;code&gt;stats_hits&lt;/code&gt;, &lt;code&gt;classifier_hits&lt;/code&gt;, &lt;code&gt;last_len&lt;/code&gt;, etc.). The libbpf skeleton exposes these through &lt;code&gt;skel-&amp;gt;bss-&amp;gt;stats_hits&lt;/code&gt;, which makes the user-space code simpler. This is fine for a single-CPU demo; for production use, you would want per-CPU maps to avoid data races.&lt;/p&gt;

&lt;h3&gt;
  
  
  Return Codes: &lt;code&gt;TCX_NEXT&lt;/code&gt; vs &lt;code&gt;TCX_PASS&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the heart of TCX composition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tcx_stats&lt;/code&gt; returns &lt;code&gt;TCX_NEXT&lt;/code&gt;, which means "I've done my work, now pass the packet to the next program in the chain." The chain continues executing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tcx_classifier&lt;/code&gt; returns &lt;code&gt;TCX_PASS&lt;/code&gt;, which is a terminal verdict: the packet is accepted and no further programs in the chain run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we had placed &lt;code&gt;tcx_classifier&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; &lt;code&gt;tcx_stats&lt;/code&gt; in the chain, &lt;code&gt;tcx_stats&lt;/code&gt; would never execute because &lt;code&gt;TCX_PASS&lt;/code&gt; terminates the chain. Ordering matters, and TCX makes it explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  User-Space Loader: Attaching and Querying the Chain
&lt;/h2&gt;

&lt;p&gt;The user-space code demonstrates three key TCX operations: attaching programs, ordering them relative to each other, and querying the live chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Attach the First Program
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;classifier_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_program__attach_tcx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;progs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcx_classifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;ifindex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This attaches &lt;code&gt;tcx_classifier&lt;/code&gt; to the TCX ingress hook on the specified interface. Passing &lt;code&gt;NULL&lt;/code&gt; for options means "use defaults", so the program gets appended to the chain. At this point, the chain has one program.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Insert the Second Program &lt;em&gt;Before&lt;/em&gt; the First
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;LIBBPF_OPTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_tcx_opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;before_opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BPF_F_BEFORE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relative_fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_program__fd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;progs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcx_classifier&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;stats_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_program__attach_tcx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;progs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcx_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;ifindex&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;before_opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;bpf_tcx_opts&lt;/code&gt; structure tells the kernel to insert &lt;code&gt;tcx_stats&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; &lt;code&gt;tcx_classifier&lt;/code&gt; in the chain. The &lt;code&gt;.relative_fd&lt;/code&gt; field identifies the reference point, which is the fd of the already-attached classifier program. After this, the chain is: &lt;code&gt;tcx_stats&lt;/code&gt; → &lt;code&gt;tcx_classifier&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You could equivalently use &lt;code&gt;BPF_F_AFTER&lt;/code&gt; with a different reference to achieve the same ordering. The important point is that you express the desired order directly, rather than hoping that two numeric priorities sort correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Query the Chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;LIBBPF_OPTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bpf_prog_query_opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&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="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prog_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prog_ids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;link_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_ids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_prog_query_opts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ifindex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_TCX_INGRESS&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;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After attachment, the loader queries the kernel for the live chain state. The returned data includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;revision&lt;/code&gt;&lt;/strong&gt;: A monotonically increasing counter that changes on every chain modification. This is the value you would pass as &lt;code&gt;expected_revision&lt;/code&gt; if you wanted to perform atomic updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prog_ids[]&lt;/code&gt;&lt;/strong&gt;: The BPF program IDs in chain order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;link_ids[]&lt;/code&gt;&lt;/strong&gt;: The corresponding BPF link IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows any observer to determine exactly which programs are attached and in what order, which is invaluable for debugging multi-program pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Generate Traffic and Read Counters
&lt;/h3&gt;

&lt;p&gt;The loader sends a UDP packet to &lt;code&gt;127.0.0.1&lt;/code&gt; (port 9, discard) to trigger the chain, waits briefly, then reads the global variables to verify both programs executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  tcx_stats hits      : %llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stats_hits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  tcx_classifier hits : %llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bss&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;classifier_hits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both counters are 1, the chain worked as expected: &lt;code&gt;tcx_stats&lt;/code&gt; ran first (recording metadata and returning &lt;code&gt;TCX_NEXT&lt;/code&gt;), then &lt;code&gt;tcx_classifier&lt;/code&gt; ran second (counting the packet and returning &lt;code&gt;TCX_PASS&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Compilation and Execution
&lt;/h2&gt;

&lt;p&gt;This example requires Linux 6.6+ with TCX support and a recent libbpf.&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;cd &lt;/span&gt;bpf-developer-tutorial/src/50-tcx
make
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./tcx_demo &lt;span class="nt"&gt;-i&lt;/span&gt; lo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attached TCX programs to lo (ifindex=1)
TCX ingress chain revision: 3
  slot 0: prog_id=812 link_id=901
  slot 1: prog_id=811 link_id=900

Counters:
  tcx_stats hits      : 1
  tcx_classifier hits : 1
  last ifindex        : 1
  last protocol       : 0x0800
  last length         : 46
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The revision is 3 because the chain was modified twice: once when &lt;code&gt;tcx_classifier&lt;/code&gt; was attached (revision went from 0 to 1), and once when &lt;code&gt;tcx_stats&lt;/code&gt; was inserted before it (revision went to 2). The query itself increments the revision to 3.&lt;/p&gt;

&lt;p&gt;If you want to inspect the attach behavior without traffic, add &lt;code&gt;-n&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="nb"&gt;sudo&lt;/span&gt; ./tcx_demo &lt;span class="nt"&gt;-i&lt;/span&gt; lo &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;-v&lt;/code&gt; to enable libbpf debug output, which is helpful for seeing the low-level BPF syscall sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Differs from Lesson 20 (Classic TC)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="//../20-tc/README.md"&gt;Lesson 20-tc&lt;/a&gt; teaches the classic TC data path: creating a &lt;code&gt;clsact&lt;/code&gt; qdisc, attaching a &lt;code&gt;SEC("tc")&lt;/code&gt; program as a filter, and using &lt;code&gt;__sk_buff&lt;/code&gt; for packet inspection. That lesson is still valuable because the &lt;strong&gt;packet processing model&lt;/strong&gt; is identical: TCX programs receive the same &lt;code&gt;__sk_buff&lt;/code&gt; context and use the same helpers for packet parsing.&lt;/p&gt;

&lt;p&gt;What TCX replaces is the &lt;strong&gt;control plane&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Classic TC (Lesson 20)&lt;/th&gt;
&lt;th&gt;TCX (Lesson 50)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attach mechanism&lt;/td&gt;
&lt;td&gt;Netlink / &lt;code&gt;tc&lt;/code&gt; CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bpf_program__attach_tcx()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;None; anyone can &lt;code&gt;tc filter del&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;BPF link; auto-detaches on fd close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering&lt;/td&gt;
&lt;td&gt;Implicit priority numbers&lt;/td&gt;
&lt;td&gt;Explicit &lt;code&gt;BPF_F_BEFORE&lt;/code&gt; / &lt;code&gt;BPF_F_AFTER&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-program&lt;/td&gt;
&lt;td&gt;Manual priority management&lt;/td&gt;
&lt;td&gt;Built-in chain with revision tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Section name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SEC("tc")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SEC("tcx/ingress")&lt;/code&gt; / &lt;code&gt;SEC("tcx/egress")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel requirement&lt;/td&gt;
&lt;td&gt;Any modern kernel&lt;/td&gt;
&lt;td&gt;Linux 6.6+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are building new libbpf-based networking tools, TCX is the recommended interface. Cilium has already migrated from classic tc to TCX for its dataplane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we learned how TCX modernizes TC program attachment by replacing qdisc-based plumbing with BPF link semantics. We attached two ingress programs, controlled their execution order with &lt;code&gt;BPF_F_BEFORE&lt;/code&gt;, queried the live chain with &lt;code&gt;bpf_prog_query_opts()&lt;/code&gt;, and verified that both programs executed in the correct order. TCX provides safe ownership, explicit ordering, revision-aware updates, and coexistence with classic TC, making it the foundation for composable, multi-program traffic control in modern eBPF applications.&lt;/p&gt;

&lt;p&gt;If you'd like to learn more about eBPF, visit our tutorial code repository at &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial&lt;/a&gt; or website &lt;a href="https://eunomia.dev/tutorials/" rel="noopener noreferrer"&gt;https://eunomia.dev/tutorials/&lt;/a&gt; for more examples and complete tutorials.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/bpf/20230707172455.7634-3-daniel@iogearbox.net/" rel="noopener noreferrer"&gt;TCX kernel commit: fd-based tcx multi-prog infra with link support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SCHED_CLS/" rel="noopener noreferrer"&gt;BPF_PROG_TYPE_SCHED_CLS documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ebpf.io/ebpf-library/libbpf/userspace/bpf_program__attach_tcx/" rel="noopener noreferrer"&gt;bpf_program__attach_tcx libbpf API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bpfconf.ebpf.io/bpfconf2024/bpfconf2024_material/tcx_netkit_update_and_global_sk_iter.pdf" rel="noopener noreferrer"&gt;Cilium TCX &amp;amp; Netkit update (BPFConf 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://oldvger.kernel.org/bpfconf2023_material/tcx_meta_netdev_borkmann.pdf" rel="noopener noreferrer"&gt;Generic multi-prog API, tcx links and meta device (BPFConf 2023)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/bpf/" rel="noopener noreferrer"&gt;https://docs.kernel.org/bpf/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>tutorial</category>
      <category>network</category>
    </item>
    <item>
      <title>eBPF Tutorial by Example: BPF Token for Delegated Privilege and Secure Program Loading</title>
      <dc:creator>云微</dc:creator>
      <pubDate>Tue, 17 Mar 2026 07:48:37 +0000</pubDate>
      <link>https://dev.to/yunwei37/ebpf-tutorial-by-example-bpf-token-for-delegated-privilege-and-secure-program-loading-3b5i</link>
      <guid>https://dev.to/yunwei37/ebpf-tutorial-by-example-bpf-token-for-delegated-privilege-and-secure-program-loading-3b5i</guid>
      <description>&lt;p&gt;Ever needed to let a container or CI job load an eBPF program without giving it full &lt;code&gt;CAP_BPF&lt;/code&gt; or &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;? Or wanted to expose XDP packet processing to a tenant workload while ensuring it can only create the specific map types and program types you've approved? Before BPF token, the answer was binary: either you had the capabilities to do &lt;em&gt;everything&lt;/em&gt; in BPF, or you could do &lt;em&gt;nothing&lt;/em&gt;. There was no middle ground.&lt;/p&gt;

&lt;p&gt;This is what &lt;strong&gt;BPF Token&lt;/strong&gt; solves. Introduced by Andrii Nakryiko and merged in Linux 6.9, BPF token is a delegation mechanism that lets a privileged process (like a container runtime or systemd) create a precisely scoped permission set for BPF operations, then hand it to an unprivileged process through a bpffs mount. The unprivileged process can load programs, create maps, and attach hooks, but only the types that were explicitly allowed. No broad capabilities required.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll set up a delegated bpffs mount in a user namespace, derive a BPF token from it, and use libbpf to load and attach a minimal XDP program, all from a process that has zero BPF capabilities of its own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The complete source code: &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/features/bpf_token" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/features/bpf_token&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction to BPF Token: Solving the Privilege Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem: All-or-Nothing BPF Capabilities
&lt;/h3&gt;

&lt;p&gt;Traditional eBPF requires &lt;code&gt;CAP_BPF&lt;/code&gt; for program loading and map creation, plus additional capabilities like &lt;code&gt;CAP_PERFMON&lt;/code&gt; for tracing, &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; for networking hooks, and &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; for certain advanced operations. These capabilities are inherently &lt;strong&gt;system-wide&lt;/strong&gt;: you cannot namespace or sandbox &lt;code&gt;CAP_BPF&lt;/code&gt;. As the kernel documentation explains, this is by design: BPF tracing helpers like &lt;code&gt;bpf_probe_read_kernel()&lt;/code&gt; can access arbitrary kernel memory, which fundamentally cannot be scoped to a single namespace.&lt;/p&gt;

&lt;p&gt;This creates a real problem in multi-tenant environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Container isolation&lt;/strong&gt;: A Kubernetes pod that needs to run a simple XDP program must be given &lt;code&gt;CAP_BPF&lt;/code&gt; + &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt;, which also grants it the ability to load &lt;em&gt;any&lt;/em&gt; BPF program type and create &lt;em&gt;any&lt;/em&gt; map type. There's no way to say "you can load XDP programs but not kprobes."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI/CD pipelines&lt;/strong&gt;: A build job that tests an eBPF-based observability tool needs root-equivalent capabilities to load programs, even though the test only exercises a specific, well-known program type.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Third-party integrations&lt;/strong&gt;: A service mesh sidecar that attaches sockops programs needs capabilities that also grant it the ability to trace every process on the host.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is that organizations either give broad BPF capabilities (weakening their security posture) or prohibit BPF entirely in unprivileged contexts (limiting the technology's adoption).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Scoped Delegation Through bpffs
&lt;/h3&gt;

&lt;p&gt;BPF token takes a different approach. Instead of trying to namespace capabilities (which is fundamentally unsafe for BPF), it introduces an explicit delegation model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;privileged process&lt;/strong&gt; (container runtime, init system, platform daemon) creates a bpffs instance with specific delegation options that define exactly which BPF operations are allowed.&lt;/li&gt;
&lt;li&gt;The privileged process passes this bpffs mount to an &lt;strong&gt;unprivileged process&lt;/strong&gt; (container, CI job, tenant workload).&lt;/li&gt;
&lt;li&gt;The unprivileged process derives a &lt;strong&gt;BPF token&lt;/strong&gt; from the bpffs mount. The token is a file descriptor that carries the delegated permission set.&lt;/li&gt;
&lt;li&gt;When the unprivileged process makes &lt;code&gt;bpf()&lt;/code&gt; syscalls (through libbpf or directly), it passes the token fd. The kernel checks permissions against the token instead of against the process's capabilities.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The token is scoped along four independent axes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Delegation Option&lt;/th&gt;
&lt;th&gt;What It Controls&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delegate_cmds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which &lt;code&gt;bpf()&lt;/code&gt; commands are allowed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prog_load:map_create:btf_load:link_create&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delegate_maps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which map types can be created&lt;/td&gt;
&lt;td&gt;&lt;code&gt;array:hash:ringbuf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delegate_progs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which program types can be loaded&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xdp:socket_filter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delegate_attachs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which attach types are allowed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xdp:cgroup_inet_ingress&lt;/code&gt; or &lt;code&gt;any&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each axis is a bitmask. If a bit isn't set, the corresponding operation is denied even if the token is present. This gives platform engineers fine-grained control: you can allow a container to load XDP programs with array maps but deny it access to kprobes, perf events, or hash-of-maps.&lt;/p&gt;

&lt;h3&gt;
  
  
  The User Namespace Constraint
&lt;/h3&gt;

&lt;p&gt;One critical design decision: &lt;strong&gt;a BPF token must be created inside the same user namespace as the bpffs instance, and that user namespace must not be &lt;code&gt;init_user_ns&lt;/code&gt;&lt;/strong&gt;. This is intentional. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A host-namespace bpffs (the one at &lt;code&gt;/sys/fs/bpf&lt;/code&gt;) does &lt;strong&gt;not&lt;/strong&gt; produce usable tokens. Tokens only work when the bpffs is associated with a non-init user namespace.&lt;/li&gt;
&lt;li&gt;The privileged parent configures the bpffs before passing it to the child, but the child (in its own user namespace) is the one that creates and uses the token.&lt;/li&gt;
&lt;li&gt;This design prevents a process with an existing token from using it to escalate privileges outside its namespace boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How libbpf Makes It Transparent
&lt;/h3&gt;

&lt;p&gt;For applications built with libbpf (which is most of them), token usage is nearly transparent. You have three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit path&lt;/strong&gt;: Set &lt;code&gt;bpf_object_open_opts.bpf_token_path&lt;/code&gt; when opening the BPF object. libbpf will derive the token from the specified bpffs mount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variable&lt;/strong&gt;: Set &lt;code&gt;LIBBPF_BPF_TOKEN_PATH&lt;/code&gt; to point to the bpffs mount. libbpf picks it up automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default path&lt;/strong&gt;: If the default &lt;code&gt;/sys/fs/bpf&lt;/code&gt; is a delegated bpffs in the current user namespace, libbpf uses it implicitly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the token is derived, libbpf passes it to every relevant syscall (&lt;code&gt;BPF_MAP_CREATE&lt;/code&gt;, &lt;code&gt;BPF_BTF_LOAD&lt;/code&gt;, &lt;code&gt;BPF_PROG_LOAD&lt;/code&gt;, and &lt;code&gt;BPF_LINK_CREATE&lt;/code&gt;) without any source-code changes in the BPF application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the eBPF Program
&lt;/h2&gt;

&lt;p&gt;The BPF side of this demo is intentionally minimal: a tiny XDP program on loopback. This keeps the focus on the token workflow. Here's the complete source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SPDX-License-Identifier: GPL-2.0&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;vmlinux.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;bpf/bpf_helpers.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;LICENSE&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GPL"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;token_stats&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__u64&lt;/span&gt; &lt;span class="n"&gt;packets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;last_ifindex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BPF_MAP_TYPE_ARRAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__uint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_entries&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="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__u32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;token_stats&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;stats_map&lt;/span&gt; &lt;span class="nf"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".maps"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;SEC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"xdp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;handle_packet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;xdp_md&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;token_stats&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;__u32&lt;/span&gt; &lt;span class="n"&gt;key&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="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&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;stats_map&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;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;stats&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;packets&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;last_ifindex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ingress_ifindex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;XDP_PASS&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 few design choices to note:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;BPF_MAP_TYPE_ARRAY&lt;/code&gt;&lt;/strong&gt; was chosen because the delegation policy explicitly allows &lt;code&gt;array&lt;/code&gt; maps. If we had used a hash map instead, loading would fail because the token doesn't grant &lt;code&gt;hash&lt;/code&gt; map creation permission. This is the token model in action; even trivial program changes can be caught by the delegation policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SEC("xdp")&lt;/code&gt;&lt;/strong&gt; matches the &lt;code&gt;delegate_progs=xdp&lt;/code&gt; policy. If you changed this to &lt;code&gt;SEC("kprobe/...")&lt;/code&gt;, the kernel would reject it at load time with an &lt;code&gt;EPERM&lt;/code&gt; because kprobe isn't in the allowed program types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;XDP_PASS&lt;/code&gt;&lt;/strong&gt; simply lets every packet through. The program's only purpose is to prove that a token-backed load and attach succeeded. In production, you'd replace this with real packet-processing logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  User-Space Loader: Token-Backed Loading
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;token_trace.c&lt;/code&gt; loader is a standard libbpf skeleton program with one key addition: it passes a &lt;code&gt;bpf_token_path&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;bpf_object_open_opts&lt;/span&gt; &lt;span class="n"&gt;open_opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="n"&gt;open_opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;open_opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bpf_token_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;skel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_trace_bpf__open_opts&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;open_opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point on, libbpf takes over. When it calls &lt;code&gt;bpf(BPF_MAP_CREATE)&lt;/code&gt; to create &lt;code&gt;stats_map&lt;/code&gt;, it includes the token fd. When it calls &lt;code&gt;bpf(BPF_PROG_LOAD)&lt;/code&gt; for the XDP program, it includes the token fd. When it calls &lt;code&gt;bpf(BPF_LINK_CREATE)&lt;/code&gt; to attach to the interface, it includes the token fd.&lt;/p&gt;

&lt;p&gt;The rest of the loader is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_trace_bpf__load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// token used for map_create + prog_load&lt;/span&gt;
&lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_program__attach_xdp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skel&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;progs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_packet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ifindex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// token used for link_create&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After attaching, the loader reads the map before and after generating a test packet to verify the program executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map_fd&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;key&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;before&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... generate UDP packet to 127.0.0.1 ...&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map_fd&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;key&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;after&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"delta          : %llu&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packets&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the delta is 1, the XDP program was successfully loaded and attached using only delegated capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Namespace Orchestrator: &lt;code&gt;token_userns_demo&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Because BPF token requires a non-init user namespace, running a bare &lt;code&gt;token_trace -t /sys/fs/bpf&lt;/code&gt; on the host won't work. The &lt;code&gt;token_userns_demo.c&lt;/code&gt; wrapper automates the complex namespace choreography. Here's the full sequence:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Fork and Create Namespaces
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parent (root, init_user_ns)          child (unprivileged, new userns)
         │                                        │
         │   fork()                               │
         ├────────────────────────────────────────&amp;gt;│
         │                                        │
         │                            unshare(CLONE_NEWUSER)
         │                            unshare(CLONE_NEWNS | CLONE_NEWNET)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child creates a new user namespace (where it maps itself to uid/gid 0), a new mount namespace (so bpffs mounts are private), and a new network namespace (so &lt;code&gt;lo&lt;/code&gt; is a fresh interface it can attach to).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create bpffs and Configure Delegation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parent (root, init_user_ns)          child (new userns)
         │                                        │
         │                            fs_fd = fsopen("bpf", 0)
         │   &amp;lt;───── send fs_fd via SCM_RIGHTS ────│
         │                                        │
    fsconfig(fs_fd, "delegate_cmds", ...)         │  (waiting for ack)
    fsconfig(fs_fd, "delegate_maps", "array")     │
    fsconfig(fs_fd, "delegate_progs", "xdp:...")  │
    fsconfig(fs_fd, "delegate_attachs", "any")    │
    fsconfig(fs_fd, FSCONFIG_CMD_CREATE)          │
         │                                        │
         │   ───────── send ack ─────────────────&amp;gt;│
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child calls &lt;code&gt;fsopen("bpf", 0)&lt;/code&gt; to create a bpffs filesystem context in its user namespace, then sends the file descriptor to the parent via a Unix socket (&lt;code&gt;SCM_RIGHTS&lt;/code&gt;). The parent, running as root in the init namespace, configures the delegation policy with &lt;code&gt;fsconfig()&lt;/code&gt;, then materializes the filesystem with &lt;code&gt;FSCONFIG_CMD_CREATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This two-step dance is necessary because: (a) the bpffs must be created in the child's user namespace (for the token to be valid there), but (b) only the privileged parent can set delegation options (because those options grant BPF capabilities).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Mount and Load
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;child &lt;span class="o"&gt;(&lt;/span&gt;new userns&lt;span class="o"&gt;)&lt;/span&gt;
         │
    mnt_fd &lt;span class="o"&gt;=&lt;/span&gt; fsmount&lt;span class="o"&gt;(&lt;/span&gt;fs_fd, 0, 0&lt;span class="o"&gt;)&lt;/span&gt;
    token_path &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/proc/self/fd/&amp;lt;mnt_fd&amp;gt;"&lt;/span&gt;
    set_loopback_up&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./token_trace"&lt;/span&gt;, &lt;span class="s2"&gt;"-t"&lt;/span&gt;, token_path, &lt;span class="s2"&gt;"-i"&lt;/span&gt;, &lt;span class="s2"&gt;"lo"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child materializes the bpffs as a detached mount (no mount point needed, since &lt;code&gt;/proc/self/fd/&amp;lt;mnt_fd&amp;gt;&lt;/code&gt; gives a path), brings the loopback interface up in its network namespace, and &lt;code&gt;exec&lt;/code&gt;s &lt;code&gt;token_trace&lt;/code&gt; with the bpffs path. From &lt;code&gt;token_trace&lt;/code&gt;'s perspective, it's just opening a BPF object with a token path. It doesn't know or care about the namespace setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing a bpffs Mount Manually
&lt;/h2&gt;

&lt;p&gt;If you want to experiment with the mount syntax outside the demo wrapper, the repository includes a helper script:&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;cd &lt;/span&gt;bpf-developer-tutorial/src/features/bpf_token
bash setup_token_bpffs.sh /tmp/bpf-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mounts bpffs at &lt;code&gt;/tmp/bpf-token&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delegate_cmds=prog_load:map_create:btf_load:link_create
delegate_maps=array
delegate_progs=xdp:socket_filter
delegate_attachs=any
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;socket_filter&lt;/code&gt;?&lt;/strong&gt; libbpf performs a trivial program-load probe before loading the real BPF object. This probe uses a generic &lt;code&gt;BPF_PROG_TYPE_SOCKET_FILTER&lt;/code&gt; program to detect kernel feature support. Without &lt;code&gt;socket_filter&lt;/code&gt; in the delegation policy, the probe fails and libbpf refuses to proceed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;delegate_attachs=any&lt;/code&gt;?&lt;/strong&gt; The same libbpf probe path also triggers attach-type validation in the kernel's token checking code. Using &lt;code&gt;any&lt;/code&gt; avoids having to enumerate every possible attach type for probe compatibility.&lt;/p&gt;

&lt;p&gt;Note that a host-namespace mount like this is useful for inspecting the delegation policy (e.g., with &lt;code&gt;bpftool token list&lt;/code&gt;), but won't produce working tokens unless the &lt;code&gt;bpf(BPF_TOKEN_CREATE)&lt;/code&gt; syscall comes from a matching non-init user namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compilation and Execution
&lt;/h2&gt;

&lt;p&gt;Build all binaries:&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;cd &lt;/span&gt;bpf-developer-tutorial/src/features/bpf_token
make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the end-to-end demo:&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; ./token_userns_demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token path     : /proc/self/fd/5
interface      : lo (ifindex=1)
packets before : 0
packets after  : 1
delta          : 1
last ifindex   : 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;delta: 1&lt;/code&gt; confirms that the XDP program was successfully loaded and attached using a BPF token, with no &lt;code&gt;CAP_BPF&lt;/code&gt; or &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; in the child process.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;-v&lt;/code&gt; for verbose libbpf output to see the token being created and used:&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; ./token_userns_demo &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already manage your own delegated bpffs in a user namespace, you can run the loader directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./token_trace &lt;span class="nt"&gt;-t&lt;/span&gt; /proc/self/fd/&amp;lt;mnt-fd&amp;gt; &lt;span class="nt"&gt;-i&lt;/span&gt; lo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Applications
&lt;/h2&gt;

&lt;p&gt;While this tutorial uses a minimal XDP program, the BPF token pattern scales to production scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Container runtimes&lt;/strong&gt; (LXD, Docker, Kubernetes): Mount a delegated bpffs into a container with only the program and map types the workload needs. LXD already supports this through its &lt;code&gt;security.delegate_bpf&lt;/code&gt; option.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI/CD testing&lt;/strong&gt;: Give build jobs the ability to load and test specific eBPF programs without granting them host-level capabilities. The delegation policy acts as an allowlist for BPF operations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-tenant BPF platforms&lt;/strong&gt;: A platform daemon creates per-tenant bpffs mounts with different delegation policies. One tenant might be allowed XDP + array maps, while another might get tracepoint + ringbuf access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LSM integration&lt;/strong&gt;: Because BPF tokens integrate with Linux Security Modules, you can combine token delegation with SELinux or AppArmor policies for defense-in-depth. Each token gets its own security context that LSM hooks can inspect.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we learned how BPF token provides a delegation model for eBPF privilege that goes beyond the binary "all or nothing" of Linux capabilities. We walked through the complete flow: a privileged parent configures a bpffs instance with specific delegation options, an unprivileged child in a user namespace derives a token from that bpffs, and libbpf transparently uses the token for map creation, program loading, and attachment. The result is a minimal XDP program running in an unprivileged context, something that was impossible before Linux 6.9.&lt;/p&gt;

&lt;p&gt;BPF token is not a niche feature. It represents the kernel's answer to a fundamental question in the eBPF ecosystem: how do you safely share BPF capabilities in a multi-tenant world without granting unconstrained access to the BPF subsystem?&lt;/p&gt;

&lt;p&gt;If you'd like to learn more about eBPF, visit our tutorial code repository at &lt;a href="https://github.com/eunomia-bpf/bpf-developer-tutorial" rel="noopener noreferrer"&gt;https://github.com/eunomia-bpf/bpf-developer-tutorial&lt;/a&gt; or website &lt;a href="https://eunomia.dev/tutorials/" rel="noopener noreferrer"&gt;https://eunomia.dev/tutorials/&lt;/a&gt; for more examples and complete tutorials.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.ebpf.io/linux/concepts/token/" rel="noopener noreferrer"&gt;BPF Token concept documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lore.kernel.org/bpf/20240103222034.2582628-1-andrii@kernel.org/T/" rel="noopener noreferrer"&gt;BPF token kernel patch series (Andrii Nakryiko)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lwn.net/Articles/959350/" rel="noopener noreferrer"&gt;BPF token LWN article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lwn.net/Articles/947173/" rel="noopener noreferrer"&gt;Finer-grained BPF tokens LWN discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://documentation.ubuntu.com/lxd/latest/explanation/bpf/" rel="noopener noreferrer"&gt;Privilege delegation using BPF Token (LXD documentation)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ebpf.io/ebpf-library/libbpf/userspace/bpf_token_create/" rel="noopener noreferrer"&gt;bpf_token_create() libbpf API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/bpf/" rel="noopener noreferrer"&gt;https://docs.kernel.org/bpf/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ebpf</category>
      <category>tutorial</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
