git: 0848cdea83fd - releng/14.4 - posixshm: Fix handling of sendfile() with largepage objects

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Tue, 30 Jun 2026 17:21:04 UTC
The branch releng/14.4 has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=0848cdea83fd46a5f23f813e3069b11df31a7233

commit 0848cdea83fd46a5f23f813e3069b11df31a7233
Author:     Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2026-06-24 19:57:00 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-06-29 19:17:30 +0000

    posixshm: Fix handling of sendfile() with largepage objects
    
    sendfile(2) can transmit POSIX shared memory objects.  Typically it will
    look up and wire each page before sending it to a socket; once
    transmission is complete, the page is unwired and typically released
    back into the page queues.  sendfile() has an advisory flag, SF_NOCACHE,
    which means, "try to free the page once transmission is complete."  This
    is implemented in vm_page_release(), which expects to operate on managed
    pages.
    
    Pages belonging a largepage object are de-facto wired not explicitly so.
    Thus, vm_page_release() will unwire and, having found no additional
    references, free the page.  Because mappings of largepage objects are
    unmanaged, userspace can still access the now freed page.
    
    Fix the problem by explicitly wiring largepage pages.  Make the VM
    object destructor responsible for unwiring and freeing them.
    
    Add a regression test.
    
    Approved by:    so
    Security:       FreeBSD-SA-26:44.posixshm
    Security:       CVE-2026-49427
    Reported by:    Chris Jarrett-Davies <chrisjd@openai.com>
    Reviewed by:    kib
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D57832
---
 sys/kern/uipc_shm.c                | 19 ++++++--
 sys/vm/vm_page.c                   | 23 +++++-----
 tests/sys/posixshm/posixshm_test.c | 89 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 118 insertions(+), 13 deletions(-)

diff --git a/sys/kern/uipc_shm.c b/sys/kern/uipc_shm.c
index 8542510299a9..937784e5e860 100644
--- a/sys/kern/uipc_shm.c
+++ b/sys/kern/uipc_shm.c
@@ -323,6 +323,7 @@ static void
 shm_largepage_phys_ctor(vm_object_t object, vm_prot_t prot,
     vm_ooffset_t foff, struct ucred *cred)
 {
+	object->flags |= OBJ_PG_DTOR;
 }
 
 static void
@@ -330,11 +331,24 @@ shm_largepage_phys_dtor(vm_object_t object)
 {
 	int psind;
 
+	VM_OBJECT_ASSERT_WLOCKED(object);
+
 	psind = object->un_pager.phys.data_val;
 	if (psind != 0) {
+		vm_page_t m, mtmp;
+		bool removed __diagused;
+
+restart:
+		TAILQ_FOREACH_SAFE(m, &object->memq, listq, mtmp) {
+			if (!vm_page_busy_acquire(m, VM_ALLOC_WAITFAIL)) {
+				goto restart;
+			}
+			removed = vm_page_remove(m);
+			KASSERT(!removed, ("%s: page %p not wired", __func__, m));
+			vm_page_unwire(m, PQ_NONE);
+		}
 		atomic_subtract_long(&count_largepages[psind],
 		    object->size / (pagesizes[psind] / PAGE_SIZE));
-		vm_wire_sub(object->size);
 	} else {
 		KASSERT(object->size == 0,
 		    ("largepage phys obj %p not initialized bit size %#jx > 0",
@@ -856,7 +870,7 @@ shm_dotruncate_largepage(struct shmfd *shmfd, off_t length, void *rl_cookie)
 	if ((shmfd->shm_seals & F_SEAL_GROW) != 0)
 		return (EPERM);
 
-	aflags = VM_ALLOC_NORMAL | VM_ALLOC_ZERO;
+	aflags = VM_ALLOC_NORMAL | VM_ALLOC_ZERO | VM_ALLOC_WIRED;
 	if (shmfd->shm_lp_alloc_policy == SHM_LARGEPAGE_ALLOC_NOWAIT)
 		aflags |= VM_ALLOC_WAITFAIL;
 	try = 0;
@@ -904,7 +918,6 @@ shm_dotruncate_largepage(struct shmfd *shmfd, off_t length, void *rl_cookie)
 		object->size += OFF_TO_IDX(pagesizes[psind]);
 		shmfd->shm_size += pagesizes[psind];
 		atomic_add_long(&count_largepages[psind], 1);
-		vm_wire_add(atop(pagesizes[psind]));
 	}
 	return (0);
 }
diff --git a/sys/vm/vm_page.c b/sys/vm/vm_page.c
index 178b104ab95f..284895658651 100644
--- a/sys/vm/vm_page.c
+++ b/sys/vm/vm_page.c
@@ -4121,6 +4121,9 @@ vm_page_unwire_managed(vm_page_t m, uint8_t nqueue, bool noreuse)
 {
 	u_int old;
 
+	KASSERT(nqueue < PQ_COUNT,
+	    ("vm_page_unwire: invalid queue %u request for page %p",
+	    nqueue, m));
 	KASSERT((m->oflags & VPO_UNMANAGED) == 0,
 	    ("%s: page %p is unmanaged", __func__, m));
 
@@ -4179,17 +4182,15 @@ vm_page_unwire_managed(vm_page_t m, uint8_t nqueue, bool noreuse)
 void
 vm_page_unwire(vm_page_t m, uint8_t nqueue)
 {
-
-	KASSERT(nqueue < PQ_COUNT,
-	    ("vm_page_unwire: invalid queue %u request for page %p",
-	    nqueue, m));
+        KASSERT(nqueue < PQ_COUNT || nqueue == PQ_NONE,
+            ("%s: invalid queue %u request for page %p", __func__, nqueue, m));
 
 	if ((m->oflags & VPO_UNMANAGED) != 0) {
 		if (vm_page_unwire_noq(m) && m->ref_count == 0)
 			vm_page_free(m);
-		return;
+	} else {
+		vm_page_unwire_managed(m, nqueue, false);
 	}
-	vm_page_unwire_managed(m, nqueue, false);
 }
 
 /*
@@ -4363,13 +4364,15 @@ vm_page_release_toq(vm_page_t m, uint8_t nqueue, const bool noreuse)
 void
 vm_page_release(vm_page_t m, int flags)
 {
-	vm_object_t object;
-
-	KASSERT((m->oflags & VPO_UNMANAGED) == 0,
-	    ("vm_page_release: page %p is unmanaged", m));
+	if ((m->oflags & VPO_UNMANAGED) != 0) {
+		vm_page_unwire(m, PQ_NONE);
+		return;
+	}
 
 	if ((flags & VPR_TRYFREE) != 0) {
 		for (;;) {
+			vm_object_t object;
+
 			object = atomic_load_ptr(&m->object);
 			if (object == NULL)
 				break;
diff --git a/tests/sys/posixshm/posixshm_test.c b/tests/sys/posixshm/posixshm_test.c
index c792a85de9e3..dccc5add12a9 100644
--- a/tests/sys/posixshm/posixshm_test.c
+++ b/tests/sys/posixshm/posixshm_test.c
@@ -34,6 +34,7 @@
 #include <sys/ioctl.h>
 #include <sys/mman.h>
 #include <sys/resource.h>
+#include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/syscall.h>
 #include <sys/sysctl.h>
@@ -2151,6 +2152,93 @@ ATF_TC_BODY(largepage_reopen, tc)
 	    "close failed; errno=%d", errno);
 }
 
+static unsigned char
+largepage_sendfile_expected(size_t off)
+{
+
+	return ((unsigned char)(off * 131 + (off >> 8)));
+}
+
+ATF_TC_WITHOUT_HEAD(largepage_sendfile);
+ATF_TC_BODY(largepage_sendfile, tc)
+{
+	static const int flags[] = { 0, SF_NOCACHE };
+	char *addr;
+	off_t sbytes;
+	size_t ps[MAXPAGESIZES];
+	int error, fd, pscnt, sd[2], status;
+	pid_t child;
+
+	pscnt = pagesizes(ps);
+
+	for (int i = 1; i < pscnt; i++) {
+		for (int fi = 0; fi < (int)nitems(flags); fi++) {
+			fd = shm_open_large(i, SHM_LARGEPAGE_ALLOC_DEFAULT,
+			    ps[i]);
+			addr = mmap(NULL, ps[i], PROT_READ | PROT_WRITE,
+			    MAP_SHARED, fd, 0);
+			ATF_REQUIRE_MSG(addr != MAP_FAILED,
+			    "mmap(%zu bytes) failed; error=%d", ps[i], errno);
+
+			/* Fill with a verifiable pattern. */
+			for (size_t j = 0; j < ps[i]; j++)
+				addr[j] = largepage_sendfile_expected(j);
+
+			ATF_REQUIRE(socketpair(PF_LOCAL, SOCK_STREAM, 0,
+			    sd) == 0);
+
+			child = fork();
+			ATF_REQUIRE_MSG(child != -1,
+			    "fork() failed; error=%d", errno);
+			if (child == 0) {
+				char buf[BUFSIZ];
+				ssize_t len;
+				size_t off, resid;
+
+				(void)close(sd[0]);
+				off = 0;
+				for (resid = ps[i]; resid > 0; resid -= len) {
+					len = read(sd[1], buf, sizeof(buf));
+					if (len <= 0)
+						_exit(1);
+					for (ssize_t k = 0; k < len; k++) {
+						if ((unsigned char)buf[k] !=
+						    largepage_sendfile_expected(
+						    off + k))
+							_exit(2);
+					}
+					off += len;
+				}
+				_exit(0);
+			}
+			ATF_REQUIRE(close(sd[1]) == 0);
+
+			sbytes = 0;
+			error = sendfile(fd, sd[0], 0, ps[i], NULL, &sbytes,
+			    flags[fi]);
+			ATF_REQUIRE_MSG(error == 0,
+			    "sendfile() failed; error=%d flags=%#x",
+			    errno, flags[fi]);
+			ATF_REQUIRE_MSG(sbytes == (off_t)ps[i],
+			    "sendfile() short; sbytes=%jd expected=%zu flags=%#x",
+			    (intmax_t)sbytes, ps[i], flags[fi]);
+
+			ATF_REQUIRE(close(sd[0]) == 0);
+
+			ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+			    "waitpid() failed; error=%d", errno);
+			ATF_REQUIRE_MSG(WIFEXITED(status),
+			    "child killed by signal %d", WTERMSIG(status));
+			ATF_REQUIRE_MSG(WEXITSTATUS(status) == 0,
+			    "child exited with status %d (flags=%#x)",
+			    WEXITSTATUS(status), flags[fi]);
+
+			ATF_REQUIRE(munmap(addr, ps[i]) == 0);
+			ATF_REQUIRE(close(fd) == 0);
+		}
+	}
+}
+
 ATF_TC_WITHOUT_HEAD(largepage_truncate);
 ATF_TC_BODY(largepage_truncate, tc)
 {
@@ -2239,6 +2327,7 @@ ATF_TP_ADD_TCS(tp)
 	ATF_TP_ADD_TC(tp, largepage_pkru);
 #endif
 	ATF_TP_ADD_TC(tp, largepage_reopen);
+	ATF_TP_ADD_TC(tp, largepage_sendfile);
 	ATF_TP_ADD_TC(tp, largepage_truncate);
 
 	return (atf_no_error());