[LTP] [PATCH v4] io_uring/pintheft: Add CVE-2026-43494 regression test

Petr Vorel pvorel@suse.cz
Sun May 24 20:16:39 CEST 2026


Hi Sebastian,

> Test for PinTheft (CVE-2026-43494), fixed by:
> e17492979319 ("net/rds: reset op_nents when zerocopy page pin fails")

Thanks you!

> The bug is in the RDS zerocopy send error path: when pinning user pages
> for zerocopy send fails partway through, the error cleanup drops a page
> reference that the RDS message cleanup will drop again. Combined with
> io_uring fixed buffer registrations, this double-drop drains the
> FOLL_PIN counter and causes a page-cache overwrite exploitable for local
> privilege escalation (PinTheft).

...
> +static void setup(void)
> +{
> +	struct io_uring_params params = {};
> +	struct iovec fixed_iov;
> +	int val;
> +
> +	page_size = SAFE_SYSCONF(_SC_PAGESIZE);
> +	io_uring_setup_supported_by_kernel();
> +
> +	/*
> +	 * The exploit primitive keeps one fixed-buffer registration alive and
> +	 * clones it to another ring.
> +	 */
> +	ring_fd1 = io_uring_setup(1, &params);
> +	if (ring_fd1 < 0)
> +		tst_brk(TBROK | TERRNO, "io_uring_setup() failed for first ring");
> +
> +	memset(&params, 0, sizeof(params));
> +
> +	ring_fd2 = io_uring_setup(1, &params);
> +	if (ring_fd2 < 0)
> +		tst_brk(TBROK | TERRNO, "io_uring_setup() failed for second ring");
> +
> +	rds_fd = socket(AF_RDS, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
> +	if (rds_fd < 0) {
> +		if (errno == EAFNOSUPPORT || errno == ESOCKTNOSUPPORT ||
> +		    errno == EPROTONOSUPPORT || errno == ENOPROTOOPT)
> +			tst_brk(TCONF | TERRNO, "RDS is not available");
> +
> +		tst_brk(TBROK | TERRNO, "socket(AF_RDS) failed");

Just a quick Sunday evening comment (not yet looking into the reproducer itself).
I wonder if we need this complicated check when we already have kconfig based
checks at the end. Could we just simply use SAFE_SOCKET() here? Or have you
encountered problems with older kernels?

And I haven't found any sysctl check (it's just a kernel module), which would be
then part of lib/tst_kconfig.c.

> +	}
> +
> +	/* PinTheft uses the RDS TCP transport, so base RDS is not enough. */
> +	val = RDS_TRANS_TCP;
> +	TEST(setsockopt(rds_fd, SOL_RDS, SO_RDS_TRANSPORT, &val, sizeof(val)));
> +
> +	if (TST_RET) {
> +		if (TST_ERR == ENOPROTOOPT || TST_ERR == EINVAL)
> +			tst_brk(TCONF | TERRNO, "RDS TCP transport is not available");
> +
> +		tst_brk(TBROK | TERRNO, "setsockopt(SO_RDS_TRANSPORT) failed");
And the same here just SAFE_SETSOCKOPT() ?
> +	}
...
> +	/*
> +	 * Register only the first page as an io_uring fixed buffer. This creates
> +	 * the long-term page pin whose reference accounting the RDS bug damages.
> +	 */
> +	if (io_uring_register(ring_fd1, IORING_REGISTER_BUFFERS, &fixed_iov, 1))
> +		tst_brk(TBROK | TERRNO, "IORING_REGISTER_BUFFERS failed");
> +
> +	buffer_registered = 1;
> +
> +	/*
> +	 * Clone the fixed buffer registration into the second ring, matching the
> +	 * public reproducer's lifetime pattern without performing the later
> +	 * page-cache overwrite stage.
> +	 */
> +	if (clone_buffers(ring_fd2, ring_fd1)) {
> +		if (errno == EINVAL || errno == EOPNOTSUPP)
> +			tst_brk(TCONF | TERRNO, "IORING_REGISTER_CLONE_BUFFERS is not supported");
Also here do we need it? IMHO CONFIG_IO_URING should be enough.
And if errno is really needed, it'd IMHO be better to be in handled in
clone_buffers(), not separately.

> +
> +		tst_brk(TBROK | TERRNO, "IORING_REGISTER_CLONE_BUFFERS failed");
> +	}
...
> +	/* Mirror the public PoC trigger: RDS zerocopy over TCP. */
> +	val = 1;
> +	if (setsockopt(rds_fd, SOL_SOCKET, SO_ZEROCOPY, &val, sizeof(val))) {
> +		if (errno == ENOPROTOOPT || errno == EINVAL)
> +			tst_brk(TCONF | TERRNO, "SO_ZEROCOPY not supported on RDS sockets");
And here I'd also simplify with SAFE_SETSOCKOPT().
> +		tst_brk(TBROK | TERRNO, "setsockopt(SO_ZEROCOPY) failed");
> +	}
...
> +static struct tst_test test = {
> +	.test_all = run,
> +	.setup = setup,
> +	.cleanup = cleanup,
> +	.timeout = 60,
> +	.forks_child = 1,
> +	.taint_check = TST_TAINT_W | TST_TAINT_D,
> +	.needs_kconfigs = (const char *[]) {
> +		"CONFIG_RDS",
> +		"CONFIG_RDS_TCP",
CONFIG_RDS_TCP implies CONFIG_RDS.

Kind regards,
Petr

> +		"CONFIG_IO_URING",
> +		NULL
> +	},
> +	.save_restore = (const struct tst_path_val[]) {
> +		{"/proc/sys/kernel/io_uring_disabled", "0",
> +			TST_SR_SKIP_MISSING | TST_SR_TCONF_RO},
> +		{}
> +	},
> +	.tags = (const struct tst_tag[]) {
> +		{"linux-git", "e17492979319"},
> +		{"CVE", "2026-43494"},
> +		{}
> +	}
> +};


More information about the ltp mailing list