All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH RFC bpf-next 0/2] bpf: Do not let preemption cause false recursion drops in trampoline progs
@ 2026-10-25 11:02 ` Mike Bazov
  0 siblings, 0 replies; 8+ messages in thread
From: Mike Bazov via B4 Relay @ 2026-10-25 11:02 UTC (permalink / raw)
  To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Martin KaFai Lau, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Song Liu, Yonghong Song, Jiri Olsa, John Fastabend, Shuah Khan,
	bpf, linux-kselftest, linux-kernel, Mike Bazov

Problem
=======

Non-sleepable fentry and fexit BPF programs silently skip invocations
from unrelated tasks whenever the task currently executing the program
on that CPU is preempted. Every skipped invocation is counted in
prog->stats->recursion_misses, even though no recursion occurred.

In __bpf_prog_enter_recur() (kernel/bpf/trampoline.c), the per-CPU
recursion counter prog->active is acquired under
rcu_read_lock_dont_migrate(), which disables CPU migration but leaves
preemption enabled:

  CPU N
  -----
  Task A enters fentry/foo -> prog->active on CPU N becomes 1
  Task A is preempted mid-program on CPU N (prog->active stays 1)
  Task B runs on CPU N, calls foo() -> __bpf_prog_enter_recur()
    bpf_prog_get_recursion_context() sees prog->active == 2 != 1
    increments recursion_misses and returns 0 (skips BPF program)
  Task A resumes on CPU N, finishes, drops prog->active to 0

Before Linux 7.0 this was hidden on most distribution kernels because
they defaulted to PREEMPT_VOLUNTARY, and non-sleepable BPF programs
contain no voluntary preemption points. Commit 7dadeaa6e851 ("sched:
Further restrict the preemption modes") removed PREEMPT_NONE and
PREEMPT_VOLUNTARY on x86 and arm64, leaving only lazy and full
preemption. As a result, every distribution kernel on 7.0+ now preempts
tasks inside non-sleepable fentry/fexit programs during normal operation
and silently drops unrelated tasks' events on the same CPU.

BPF program authors cannot work around this from inside the program:
prog->active is tested by the trampoline before the first BPF
instruction executes, and bpf_preempt_disable() cannot span tail calls.
By contrast, kprobes and classic perf tracepoints run BPF programs with
preemption disabled (trace_call_bpf() asserts cant_sleep(), and commit
16718274ee75 ("tracing: perf: Have perf tracepoint callbacks always
disable preemption") explicitly kept preemption disabled for perf
tracepoint callbacks), so the same BPF logic has different completeness
guarantees depending solely on whether it attaches via fentry/fexit or
kprobe/tracepoint.

Why This Matters in Practice
============================

We hit this in a production security agent that attaches paired
fentry/fexit probes to inet_sendmsg(), inet6_sendmsg(), inet_recvmsg(),
and inet6_recvmsg() to inspect stateful application-layer protocols:

1. Broken entry/exit pairing: the fentry program snapshots the msghdr /
   iov_iter state into per-socket storage and the fexit program consumes
   that snapshot once the call completes with the byte count. Because
   each program has its own prog->active counter, preemption skips one
   half of the pair independently -- either dropping the payload and
   leaving stale per-socket state (when fexit is skipped) or losing the
   capture entirely (when fentry is skipped).
2. Broken stateful protocol parsing: for stateful protocols (HTTP/1.1
   keep-alive/pipelining/chunked framing, HTTP/2 header state, database
   wire protocols, or per-session first-message classification), a
   single skipped sendmsg/recvmsg call desynchronizes the state machine
   for the remainder of the connection.
3. Kprobes are not a drop-in replacement: on hot socket paths fentry is
   ~2x faster than kprobe (40M/s vs 20M/s in trig-fentry vs trig-kprobe),
   and kretprobe relies on a fixed maxactive instance pool that can be
   exhausted on blocking calls like inet_recvmsg().

Minimal Standalone Reproducer (47 lines)
========================================

repro.bpf.c:

  #include <linux/bpf.h>
  #include <bpf/bpf_helpers.h>

  char LICENSE[] SEC("license") = "GPL";
  static int cb(__u32 i, void *ctx) { return 0; }

  SEC("fentry/__set_task_comm")
  int on_fentry(void *ctx) { bpf_loop(1000000, cb, NULL, 0); return 0; }

  SEC("fexit/__set_task_comm")
  int on_fexit(void *ctx) { bpf_loop(1000000, cb, NULL, 0); return 0; }

repro.c:

  #define _GNU_SOURCE
  #include <sched.h>
  #include <stdio.h>
  #include <unistd.h>
  #include <sys/prctl.h>
  #include <sys/wait.h>
  #include <bpf/bpf.h>
  #include <bpf/libbpf.h>
  #include "repro.skel.h"

  int main(void)
  {
      int stats = bpf_enable_stats(BPF_STATS_RUN_TIME);
      struct repro *skel = repro__open_and_load();
      struct bpf_program *p;
      cpu_set_t cpu;

      repro__attach(skel);
      CPU_ZERO(&cpu);
      CPU_SET(sched_getcpu(), &cpu);
      sched_setaffinity(0, sizeof(cpu), &cpu);

      pid_t pid = fork();
      for (int i = 0; i < 100; i++) {
          prctl(PR_SET_NAME, "repro");
          if (!pid) usleep(50);
      }
      if (!pid)
          return 0;
      wait(NULL);

      bpf_object__for_each_program(p, skel->obj) {
          struct bpf_prog_info info = {};
          __u32 len = sizeof(info);

          bpf_prog_get_info_by_fd(bpf_program__fd(p), &info, &len);
          printf("%-9s expected=200 run_cnt=%-4llu recursion_misses=%llu\n",
                 bpf_program__name(p), info.run_cnt, info.recursion_misses);
      }
      repro__destroy(skel);
      close(stats);
  }

Results across 10 runs (2,000 total calls per probe) on the same x86_64
VM, comparing unmodified mainline (a804c5742bff) with this series:

  Kernel & Preemption Mode        fentry skipped       fexit skipped
  -----------------------------   ------------------   ------------------
  6.8.0-55 stock (voluntary)         0 / 1000 ( 0%)       0 / 1000 ( 0%)
  6.8.0-55 stock (full)            254 / 1000 (25%)     263 / 1000 (26%)
  7.3.0 mainline (lazy)            553 / 2000 (28%)     691 / 2000 (35%)
  7.3.0 mainline (full)            694 / 2000 (35%)     713 / 2000 (36%)
  7.3.0 + patch  (lazy)              0 / 2000 ( 0%)       0 / 2000 ( 0%)
  7.3.0 + patch  (full)              0 / 2000 ( 0%)       0 / 2000 ( 0%)

Why This Fix (and Why Not Per-Task Recursion Tracking)
======================================================

Relaxing prog->active to check whether current is the same task would
not be safe on its own:

1. Per-CPU private stack: since commit a76ab5731e32 ("bpf: Find eligible
   subprogs for private stack support"), recursion-checked trampoline
   programs allocate a single per-CPU private stack
   (prog->aux->priv_stack_ptr) and rely on prog->active to guarantee
   that at most one task on a CPU uses that stack at a time. Allowing a
   second task on the same CPU to enter the program while the first task
   is preempted would corrupt the first task's private stack frames.
2. Per-CPU scratch maps: existing fentry/fexit programs rely on the
   same per-CPU non-reentrancy guarantee when building events in
   BPF_MAP_TYPE_PERCPU_ARRAY scratch buffers.

Therefore, Patch 1 disables preemption (preempt_disable_notrace() +
rcu_read_lock()) in __bpf_prog_enter_recur() / __bpf_prog_exit_recur()
when !IS_ENABLED(CONFIG_PREEMPT_RT), matching classic perf tracepoint
and kprobe BPF execution while leaving PREEMPT_RT and sleepable
trampoline programs (__bpf_prog_enter_sleepable_recur) unchanged.

Testing
=======

- A/B reproducer above on mainline 7.3 (lazy and full preemption):
  drops go from 28-36% to 0%.
- True same-task recursion (fentry/htab_map_delete_elem deleting from
  its own map, as in prog_tests/recursion.c): still blocked 10/10 times.
- Debug kernel with CONFIG_DEBUG_ATOMIC_SLEEP=y, CONFIG_DEBUG_PREEMPT=y,
  CONFIG_PROVE_LOCKING=y, and CONFIG_PROVE_RCU=y: zero lockdep, RCU, or
  atomic-sleep warnings across both the reproducer and the full
  test_progs run (debug_locks remains 1).
- Full test_progs A/B comparison on mainline 7.3: zero new failures
  introduced; all fentry/fexit/trampoline/recursion test suites pass.
- Selftests bench trigger throughput (3 x 5s runs, pinned producer):
    trig-fentry:  39.93M/s (base) -> 42.05M/s (patched)
    trig-fexit:   31.98M/s (base) -> 33.54M/s (patched)
- CONFIG_PREEMPT_RT=y kernel (7.3.0-rt): builds cleanly with W=1; booted
  and verified that trampoline_preempt skips via __kconfig while the
  recursion selftest passes.

Question for Discussion
=======================

Is disabling preemption in __bpf_prog_enter_recur() on !PREEMPT_RT the
preferred fix for fentry/fexit, or would you prefer a different
mechanism for reconciling preemptible execution with prog->active and
the per-CPU private stack?

Signed-off-by: Mike Bazov <mikeba@sentinelone.com>
---
Mike Bazov (2):
      bpf: Keep recursion-checked trampoline programs non-preemptible on !PREEMPT_RT
      selftests/bpf: Add trampoline preemption false-recursion test

 kernel/bpf/trampoline.c                            |  17 +-
 .../selftests/bpf/prog_tests/trampoline_preempt.c  | 222 +++++++++++++++++++++
 .../selftests/bpf/progs/trampoline_preempt.c       |  56 ++++++
 3 files changed, 293 insertions(+), 2 deletions(-)
---
base-commit: a804c5742bff224d4cb4d25dd383554d598be32b
change-id: 20261025-b4-bpf-trampoline-preempt-a1417898a8ed

Best regards,
-- 
Mike Bazov <mikeba@sentinelone.com>



^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-10-30 22:22 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-25 11:02 [PATCH RFC bpf-next 0/2] bpf: Do not let preemption cause false recursion drops in trampoline progs Mike Bazov via B4 Relay
2026-10-25 11:02 ` Mike Bazov
2026-10-25 11:02 ` [PATCH RFC bpf-next 1/2] bpf: Keep recursion-checked trampoline programs non-preemptible on !PREEMPT_RT Mike Bazov via B4 Relay
2026-10-25 11:02   ` Mike Bazov
2026-10-25 11:38   ` sashiko-bot
2026-10-25 11:02 ` [PATCH RFC bpf-next 2/2] selftests/bpf: Add trampoline preemption false-recursion test Mike Bazov via B4 Relay
2026-10-25 11:02   ` Mike Bazov
2026-10-30 22:22 ` [PATCH RFC bpf-next 0/2] bpf: Do not let preemption cause false recursion drops in trampoline progs Alexei Starovoitov

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.