* [PATCH] bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks
@ 2026-11-03 17:32 shaos
2026-11-03 17:51 ` sashiko-bot
2026-11-03 18:18 ` Alexei Starovoitov
0 siblings, 2 replies; 3+ messages in thread
From: shaos @ 2026-11-03 17:32 UTC (permalink / raw)
To: Amery Hung
Cc: Jamal Hadi Salim, Jiri Pirko, Martin KaFai Lau,
Toke Høiland-Jørgensen, netdev, bpf, linux-kernel
Hello,
A BPF qdisc's generated reset/destroy epilogue cancels its watchdog
timer. bpf_throw() unwinds directly to the exception callback and
skips that epilogue. If the timer was armed, qdisc deletion frees the
qdisc while the embedded timer remains queued. Later timer-queue
activity accesses the freed qdisc, producing a KASAN slab-use-after-free
and a kernel crash.
The reproducer registers a BPF qdisc whose dequeue callback arms the
watchdog and whose reset/destroy callbacks throw. Two fresh KASAN boots
reported use-after-free in rb_insert_color on an object allocated by
qdisc_alloc() and later freed by qdisc teardown. An otherwise identical
no-throw control completed deletion cleanly. The required BPF and
network-administration capabilities make this a privileged local kernel
crash; no privilege escalation or code execution is demonstrated.
HEAD commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
Git tree: 30fd2ef82912c26cf3a58ad0ba6afc375fa851ac
Source version: Linux 7.3
Compiler: gcc 11.4.0, GNU ld 2.38, clang 20 (BPF PoC)
The console log, full KASAN kernel config, and BPF C reproducer are
available from one fixed-revision Gist:
Console output:
https://gist.githubusercontent.com/shaos26/9908a8ddac69e303cb5dcc709958537f/raw/3fa043e43683907bcccacdc80a71b150920049da/console-output.txt
Kernel config:
https://gist.githubusercontent.com/shaos26/9908a8ddac69e303cb5dcc709958537f/raw/3fa043e43683907bcccacdc80a71b150920049da/kernel-config.txt
PoC source:
https://gist.githubusercontent.com/shaos26/9908a8ddac69e303cb5dcc709958537f/raw/3fa043e43683907bcccacdc80a71b150920049da/poc.c
Run this as root in a disposable VM booted with the target kernel,
CONFIG_DEBUG_INFO_BTF=y, bpftool, iproute2, clang 20, and libbpf-dev.
Build each variant against that kernel's BTF; test them on separate
fresh boots. With libbpf headers installed under /usr/include:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
CLANG=/usr/lib/llvm-20/bin/clang
LIBBPF_INC=/usr/include
"$CLANG" -target bpf -D__TARGET_ARCH_x86 -O2 -g -Wall -Werror \
-Wno-missing-declarations -I. -I"$LIBBPF_INC" \
-DVARIANT_THROW=1 -c poc.c -o qdisc-throw.bpf.o
"$CLANG" -target bpf -D__TARGET_ARCH_x86 -O2 -g -Wall -Werror \
-Wno-missing-declarations -I. -I"$LIBBPF_INC" \
-DVARIANT_THROW=0 -c poc.c -o qdisc-control.bpf.o
Mount bpffs if it is not already mounted, register the matching object,
attach its qdisc to loopback, send one packet to arm the watchdog, and
delete the qdisc:
mkdir -p /sys/fs/bpf
mount -t bpf bpffs /sys/fs/bpf 2>/dev/null || true
bpftool struct_ops register qdisc-throw.bpf.o /sys/fs/bpf
ip link set lo up
tc qdisc add dev lo root handle 1: agent31_uaf
ping -c 1 -W 1 127.0.0.1
tc qdisc del dev lo root
sleep 10
For the separate control boot, register qdisc-control.bpf.o and use
qdisc kind agent31_ctl in the same sequence.
On the vulnerable kernel, the two throw runs reproduced the KASAN UAF;
the no-throw control survived. With the patch below, the verifier
rejects bpf_throw() from reset/destroy with -EACCES, before qdisc
registration. The patched control object registered, attached, armed
and deleted normally, then survived the watchdog interval.
Fixes: 7a2dafda950b ("bpf: net_sched: Add a qdisc watchdog timer")
Signed-off-by: shaos <shawnq1@163.com>
---
net/sched/bpf_qdisc.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/net/sched/bpf_qdisc.c b/net/sched/bpf_qdisc.c
index 098ca02aed89c..61f0dc93d3856 100644
--- a/net/sched/bpf_qdisc.c
+++ b/net/sched/bpf_qdisc.c
@@ -12,6 +12,7 @@
#define QDISC_MOFF_IDX(moff) (moff / sizeof(void (*)(void)))
static struct bpf_struct_ops bpf_Qdisc_ops;
+static const struct bpf_verifier_ops bpf_qdisc_verifier_ops;
struct bpf_sched_data {
struct qdisc_watchdog watchdog;
@@ -131,6 +132,7 @@ static int bpf_qdisc_btf_struct_access(struct bpf_verifier_log *log,
}
BTF_ID_LIST_SINGLE(bpf_qdisc_init_prologue_ids, func, bpf_qdisc_init_prologue)
+BTF_ID_LIST_SINGLE(bpf_qdisc_throw_ids, func, bpf_throw)
static int bpf_qdisc_gen_prologue(struct bpf_insn *insn_buf, bool direct_write,
const struct bpf_prog *prog)
@@ -307,20 +309,31 @@ enum qdisc_ops_kf_flags {
QDISC_OPS_KF_COMMON = 0,
QDISC_OPS_KF_ENQUEUE = 1 << 0,
QDISC_OPS_KF_DEQUEUE = 1 << 1,
+ QDISC_OPS_KF_TEARDOWN = 1 << 2,
};
static const u32 qdisc_ops_context_flags[] = {
[QDISC_OP_IDX(enqueue)] = QDISC_OPS_KF_ENQUEUE,
[QDISC_OP_IDX(dequeue)] = QDISC_OPS_KF_DEQUEUE,
[QDISC_OP_IDX(init)] = QDISC_OPS_KF_COMMON,
- [QDISC_OP_IDX(reset)] = QDISC_OPS_KF_COMMON,
- [QDISC_OP_IDX(destroy)] = QDISC_OPS_KF_COMMON,
+ [QDISC_OP_IDX(reset)] = QDISC_OPS_KF_TEARDOWN,
+ [QDISC_OP_IDX(destroy)] = QDISC_OPS_KF_TEARDOWN,
};
static int bpf_qdisc_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id)
{
u32 moff, flags;
+ /* bpf_throw() bypasses the generated watchdog-cancellation epilogue. */
+ if (kfunc_id == bpf_qdisc_throw_ids[0] &&
+ prog->aux->st_ops &&
+ prog->aux->st_ops->verifier_ops == &bpf_qdisc_verifier_ops) {
+ moff = prog->aux->attach_st_ops_member_off;
+ flags = qdisc_ops_context_flags[QDISC_MOFF_IDX(moff)];
+ if (flags & QDISC_OPS_KF_TEARDOWN)
+ return -EACCES;
+ }
+
if (!btf_id_set8_contains(&qdisc_kfunc_ids, kfunc_id))
return 0;
--
2.34.1
^ permalink raw reply related [flat|nested] 3+ messages in thread* Re: [PATCH] bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks
2026-11-03 17:32 [PATCH] bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks shaos
@ 2026-11-03 17:51 ` sashiko-bot
2026-11-03 18:18 ` Alexei Starovoitov
1 sibling, 0 replies; 3+ messages in thread
From: sashiko-bot @ 2026-11-03 17:51 UTC (permalink / raw)
To: shaos; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] bpf: net_sched: duplicated st_ops check and lookup in bpf_qdisc_kfunc_filter()
- [Low] bpf: net_sched: raw bug-report boilerplate in commit message
--
commit 4987df9cdbc82b0c8a5dd77111f15b5660ca8808
Author: shaos <shawnq1@163.com>
bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks
This commit updates bpf_qdisc_kfunc_filter() to reject calls to bpf_throw()
from BPF qdisc reset and destroy callbacks so that the generated watchdog
cancellation epilogue is not bypassed during qdisc teardown.
> bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks
>
> Hello,
>
> A BPF qdisc's generated reset/destroy epilogue cancels its watchdog
> timer. bpf_throw() unwinds directly to the exception callback and
> skips that epilogue. If the timer was armed, qdisc deletion frees the
> qdisc while the embedded timer remains queued. Later timer-queue
> activity accesses the freed qdisc, producing a KASAN slab-use-after-free
> and a kernel crash.
>
> The reproducer registers a BPF qdisc whose dequeue callback arms the
> watchdog and whose reset/destroy callbacks throw. Two fresh KASAN boots
> reported use-after-free in rb_insert_color on an object allocated by
> qdisc_alloc() and later freed by qdisc teardown. An otherwise identical
> no-throw control completed deletion cleanly. The required BPF and
> network-administration capabilities make this a privileged local kernel
> crash; no privilege escalation or code execution is demonstrated.
>
> HEAD commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
> Git tree: 30fd2ef82912c26cf3a58ad0ba6afc375fa851ac
> Source version: Linux 7.3
> Compiler: gcc 11.4.0, GNU ld 2.38, clang 20 (BPF PoC)
>
> The console log, full KASAN kernel config, and BPF C reproducer are
> available from one fixed-revision Gist:
>
> Console output:
> https://gist.githubusercontent.com/shaos26/9908a8ddac69e303cb5dcc709958537f/raw/3fa043e43683907bcccacdc80a71b150920049da/console-output.txt
> Kernel config:
> https://gist.githubusercontent.com/shaos26/9908a8ddac69e303cb5dcc709958537f/raw/3fa043e43683907bcccacdc80a71b150920049da/kernel-config.txt
> PoC source:
> https://gist.githubusercontent.com/shaos26/9908a8ddac69e303cb5dcc709958537f/raw/3fa043e43683907bcccacdc80a71b150920049da/poc.c
>
> Run this as root in a disposable VM booted with the target kernel,
> CONFIG_DEBUG_INFO_BTF=y, bpftool, iproute2, clang 20, and libbpf-dev.
> Build each variant against that kernel's BTF; test them on separate
> fresh boots. With libbpf headers installed under /usr/include:
>
> bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
> CLANG=/usr/lib/llvm-20/bin/clang
> LIBBPF_INC=/usr/include
> "$CLANG" -target bpf -D__TARGET_ARCH_x86 -O2 -g -Wall -Werror \
> -Wno-missing-declarations -I. -I"$LIBBPF_INC" \
> -DVARIANT_THROW=1 -c poc.c -o qdisc-throw.bpf.o
> "$CLANG" -target bpf -D__TARGET_ARCH_x86 -O2 -g -Wall -Werror \
> -Wno-missing-declarations -I. -I"$LIBBPF_INC" \
> -DVARIANT_THROW=0 -c poc.c -o qdisc-control.bpf.o
>
> Mount bpffs if it is not already mounted, register the matching object,
> attach its qdisc to loopback, send one packet to arm the watchdog, and
> delete the qdisc:
>
> mkdir -p /sys/fs/bpf
> mount -t bpf bpffs /sys/fs/bpf 2>/dev/null || true
> bpftool struct_ops register qdisc-throw.bpf.o /sys/fs/bpf
> ip link set lo up
> tc qdisc add dev lo root handle 1: agent31_uaf
> ping -c 1 -W 1 127.0.0.1
> tc qdisc del dev lo root
> sleep 10
>
> For the separate control boot, register qdisc-control.bpf.o and use
> qdisc kind agent31_ctl in the same sequence.
>
> On the vulnerable kernel, the two throw runs reproduced the KASAN UAF;
> the no-throw control survived. With the patch below, the verifier
> rejects bpf_throw() from reset/destroy with -EACCES, before qdisc
> registration. The patched control object registered, attached, armed
> and deleted normally, then survived the watchdog interval.
>
> Fixes: 7a2dafda950b ("bpf: net_sched: Add a qdisc watchdog timer")
> Signed-off-by: shaos <shawnq1@163.com>
[Severity: Low]
This isn't a bug, but the commit message includes a conversational greeting
("Hello,"), bug-report metadata headers (HEAD commit, Git tree, Source
version, Compiler), external Gist URLs, and VM build/run shell instructions.
Could the commit message be trimmed to a self-contained description of the
problem and the fix without the bug-report boilerplate and external links?
> diff --git a/net/sched/bpf_qdisc.c b/net/sched/bpf_qdisc.c
> index 5691c13781a8b..20c0b892d2cb1 100644
> --- a/net/sched/bpf_qdisc.c
> +++ b/net/sched/bpf_qdisc.c
> @@ -12,6 +12,7 @@
> #define QDISC_MOFF_IDX(moff) (moff / sizeof(void (*)(void)))
>
> static struct bpf_struct_ops bpf_Qdisc_ops;
> +static const struct bpf_verifier_ops bpf_qdisc_verifier_ops;
>
> struct bpf_sched_data {
> struct qdisc_watchdog watchdog;
[ ... ]
> @@ -305,20 +307,31 @@ enum qdisc_ops_kf_flags {
> QDISC_OPS_KF_COMMON = 0,
> QDISC_OPS_KF_ENQUEUE = 1 << 0,
> QDISC_OPS_KF_DEQUEUE = 1 << 1,
> + QDISC_OPS_KF_TEARDOWN = 1 << 2,
> };
>
> static const u32 qdisc_ops_context_flags[] = {
> [QDISC_OP_IDX(enqueue)] = QDISC_OPS_KF_ENQUEUE,
> [QDISC_OP_IDX(dequeue)] = QDISC_OPS_KF_DEQUEUE,
> [QDISC_OP_IDX(init)] = QDISC_OPS_KF_COMMON,
> - [QDISC_OP_IDX(reset)] = QDISC_OPS_KF_COMMON,
> - [QDISC_OP_IDX(destroy)] = QDISC_OPS_KF_COMMON,
> + [QDISC_OP_IDX(reset)] = QDISC_OPS_KF_TEARDOWN,
> + [QDISC_OP_IDX(destroy)] = QDISC_OPS_KF_TEARDOWN,
> };
>
> static int bpf_qdisc_kfunc_filter(const struct bpf_prog *prog, u32 kfunc_id)
> {
> u32 moff, flags;
>
> + /* bpf_throw() bypasses the generated watchdog-cancellation epilogue. */
> + if (kfunc_id == bpf_qdisc_throw_ids[0] &&
> + prog->aux->st_ops &&
> + prog->aux->st_ops->verifier_ops == &bpf_qdisc_verifier_ops) {
> + moff = prog->aux->attach_st_ops_member_off;
> + flags = qdisc_ops_context_flags[QDISC_MOFF_IDX(moff)];
> + if (flags & QDISC_OPS_KF_TEARDOWN)
> + return -EACCES;
> + }
[Severity: Low]
This isn't a bug, but bpf_Qdisc_ops is already forward-declared in
net/sched/bpf_qdisc.c, and the existing code right below in
bpf_qdisc_kfunc_filter() already checks prog->aux->st_ops against
&bpf_Qdisc_ops and computes moff and flags:
if (!btf_id_set8_contains(&qdisc_kfunc_ids, kfunc_id))
return 0;
if (prog->aux->st_ops != &bpf_Qdisc_ops)
return -EACCES;
moff = prog->aux->attach_st_ops_member_off;
flags = qdisc_ops_context_flags[QDISC_MOFF_IDX(moff)];
Could bpf_qdisc_kfunc_filter() check prog->aux->st_ops == &bpf_Qdisc_ops
instead of adding a forward declaration for bpf_qdisc_verifier_ops, and share
the moff and flags lookup rather than duplicating it?
--
Sashiko AI review · https://sashiko.dev/#/patchset/6AEA1AAD.2E7977.08739@m16.mail.163.com?part=1
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks
2026-11-03 17:32 [PATCH] bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks shaos
2026-11-03 17:51 ` sashiko-bot
@ 2026-11-03 18:18 ` Alexei Starovoitov
1 sibling, 0 replies; 3+ messages in thread
From: Alexei Starovoitov @ 2026-11-03 18:18 UTC (permalink / raw)
To: shaos, Amery Hung
Cc: Jamal Hadi Salim, Jiri Pirko, Martin KaFai Lau,
Toke Høiland-Jørgensen, netdev, bpf, linux-kernel
On Tue, Nov 03, 2026 at 09:32 AM shaos <shawnq1@163.com> wrote:
> A BPF qdisc's generated reset/destroy epilogue cancels its watchdog
> timer. bpf_throw() unwinds directly to the exception callback and
> skips that epilogue.
That's a bug in the verifier. bpf_qdisc.c is the wrong place to fix it.
gen_epilogue() is a contract: the epilogue runs before the prog returns.
bpf_convert_ctx_accesses() puts the epilogue in place of BPF_EXIT
in the main prog only. The exception callback is a subprog.
So bpf_throw() breaks the contract for every gen_epilogue() user,
bpf_testmod included. Fix it there.
Then bpf_qdisc.c doesn't need to know about bpf_throw: no BTF id,
no QDISC_OPS_KF_TEARDOWN, no forward declaration.
Like:
diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
index ead7871..5363e88 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -909,6 +909,11 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
verifier_bug(env, "epilogue is too long");
return -EFAULT;
} else if (epilogue_cnt) {
+ /* bpf_throw() leaves via the exception callback */
+ if (env->seen_exception) {
+ verbose(env, "bpf_throw would skip the epilogue of the program\n");
+ return -EINVAL;
+ }
/* Save the ARG_PTR_TO_CTX for the epilogue to use */
subprogs[0].stack_depth += 8;
insn_buf[0] = BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_1,
> Fixes: 7a2dafda950b ("bpf: net_sched: Add a qdisc watchdog timer")
With the fix in the verifier it's
Fixes: 169c31761c8d ("bpf: Add gen_epilogue to bpf_verifier_ops")
Pls add a selftest as patch 2. bpf_testmod_st_ops has gen_epilogue().
A test_epilogue prog that calls bpf_throw(), with __failure and __msg(),
is enough. See progs/epilogue_exit.c.
Pls target bpf-next and cc ameryhung@gmail.com on respin.
pw-bot: cr
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-03 18:18 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-03 17:32 [PATCH] bpf: net_sched: reject bpf_throw() in qdisc teardown callbacks shaos
2026-11-03 17:51 ` sashiko-bot
2026-11-03 18:18 ` 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.