aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Johnston <markj@FreeBSD.org>2026-06-24 19:57:00 +0000
committerMark Johnston <markj@FreeBSD.org>2026-06-30 17:00:23 +0000
commit1bad1d8c14cdff127d97accd49b0e6da22501b99 (patch)
tree206a151d9683271229762ca95885e8f7e4bc6627
parent1491dec424e72a2f429d84d11b0d248abc6c6cda (diff)
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
-rw-r--r--sys/kern/uipc_shm.c22
-rw-r--r--sys/vm/vm_page.c23
-rw-r--r--tests/sys/posixshm/posixshm_test.c89
3 files changed, 121 insertions, 13 deletions
diff --git a/sys/kern/uipc_shm.c b/sys/kern/uipc_shm.c
index b1b6a8657d69..8341364f4d16 100644
--- a/sys/kern/uipc_shm.c
+++ b/sys/kern/uipc_shm.c
@@ -326,6 +326,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
@@ -333,11 +334,27 @@ 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) {
+ struct pctrie_iter pages;
+ vm_page_t m;
+ bool removed __diagused;
+
+ vm_page_iter_init(&pages, object);
+restart:
+ VM_RADIX_FOREACH(m, &pages) {
+ if (!vm_page_busy_acquire(m, VM_ALLOC_WAITFAIL)) {
+ pctrie_iter_reset(&pages);
+ goto restart;
+ }
+ removed = vm_page_iter_remove(&pages, 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",
@@ -818,7 +835,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;
@@ -874,7 +891,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 8697d9361d84..bb44cd9de486 100644
--- a/sys/vm/vm_page.c
+++ b/sys/vm/vm_page.c
@@ -4287,6 +4287,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));
@@ -4345,17 +4348,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);
}
/*
@@ -4529,13 +4530,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 e816e2d1efa7..e4b57dec6311 100644
--- a/tests/sys/posixshm/posixshm_test.c
+++ b/tests/sys/posixshm/posixshm_test.c
@@ -33,6 +33,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>
@@ -2148,6 +2149,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, true);
+
+ 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)
{
@@ -2236,6 +2324,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());