From nobody Wed May 20 19:39:30 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4gLMLW0wCbz6fJkp for ; Wed, 20 May 2026 19:39:31 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R13" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4gLMLV5Z3Kz3XlV for ; Wed, 20 May 2026 19:39:30 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1779305970; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=GP8/OOf1eWXH/yabQVAxX4+8AzCIgAXUIh5W2hLIV8g=; b=ZYmWOQcHnWy4BSQlR+2cVq+0aaowCCxOnvAJjyPvNvTRTrHW2bZFGRzeKxztrl2YoCQQ+E ho7uhrnqR4NMp/zLgVHEsUZj6u+7MMkjCkKIQkCd5Ysogr9zE/1g0fTftGG+lkhmjO+7pc 4KXZrtx458nQ/KW1Mxur38S4qpzK1co/nXUuI3ccUAUtxp96b632C0jw9E+T2jPxScDn71 U7tXNpuaBRNVcqQpHbMnulRKRJzDllT5BNzUVZSyzzlq5Av+DDCV+eej0KKgHCn5brfl+H US26bTE5QUobb33LpZ6Kn1hqK6tnq8M3+/bX8t8XzljRUxbGV3aY5SZhtbi+mQ== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1779305970; a=rsa-sha256; cv=none; b=T+2JxRhOjRq0hDEiOrNdlvNc7+DvNSzjgWn8ex4janfWEAZqrshDcb5D5HaWTJ784pA3XO xKKgGKRvsGz2+CRwG4/xJ5Tk37rqH4gHWgxiXJhVUR8HCGz4JBCxlLDMq6PyQPQN6DevfG 4Ywjg5bEakzZwfWDylDI38CJxk0511mxgF5WOGIg+Qn/Iiybm1g9Go5SzhJHLgSkxQwOTi rwHTFaU/tcAlpHFF5c6/VHihfomq1tyR+V0iqhxzZls2k/Vbt7OdPrYHr8PbFmnUEYxnGp ZntbJcqr1u9xdApQEtMQ0h+YsWbt/U7CrR5xUxeHvJNa77x5mX7IG1+6A6vf6w== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1779305970; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=GP8/OOf1eWXH/yabQVAxX4+8AzCIgAXUIh5W2hLIV8g=; b=k+z/11E5TBS4+RD7FBskXNJxf1RJ3IUUwR9fFcd1TEsz7UZeLkfLUX51a4ivFHcRgULdl+ dZq9LMTVhfneygeMBkF1gcdfdQeTQjlIiuwGhER+BfE8zeZ55AVAhyDlIxmxGzsSDxDMWp S6IeM0FtcJqudG3QVRGMaKhCQtvffmjXncbYoSUbCFMy9sy47VT7Ijs5+rimGlTmDCNtgc SrPSHOnIJ1Jq5nCr34V2wm63gPp7Fu4XR7KAQ7kBe7AaWJLYkbWEs5Sr/r8RnpZFDRK2cy uu1n4sjO6d8omVlzq4J4Z0bt/iN1uCsZsR/RR0bfzoEdqS3WjVd2QwHI6fw3zg== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4gLMLV52GXz1DXr for ; Wed, 20 May 2026 19:39:30 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 36570 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Wed, 20 May 2026 19:39:30 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org From: Mark Johnston Subject: git: 7363574d68ad - releng/15.0 - jaildesc: Make sure to drain selinfo sleepers in jaildesc_close() List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: markj X-Git-Repository: src X-Git-Refname: refs/heads/releng/15.0 X-Git-Reftype: branch X-Git-Commit: 7363574d68ad17a7d56f6025f41597f28baca6f1 Auto-Submitted: auto-generated Date: Wed, 20 May 2026 19:39:30 +0000 Message-Id: <6a0e0df2.36570.56cb3708@gitrepo.freebsd.org> The branch releng/15.0 has been updated by markj: URL: https://cgit.FreeBSD.org/src/commit/?id=7363574d68ad17a7d56f6025f41597f28baca6f1 commit 7363574d68ad17a7d56f6025f41597f28baca6f1 Author: Mark Johnston AuthorDate: 2026-05-10 15:15:45 +0000 Commit: Mark Johnston CommitDate: 2026-05-19 23:51:24 +0000 jaildesc: Make sure to drain selinfo sleepers in jaildesc_close() Otherwise they may be left on a freed selinfo list after the corresponding jaildesc struct is freed. This can be exploited to elevate privileges. Remove the JDF_SELECTED micro-optimization. doselwakeup() is a no-op if no one ever called selrecord() on the file description, so I see no reason to complicate the code to avoid the call. Add some regression tests. Approved by: so Security: FreeBSD-SA-26:19.file Security: CVE-2026-45251 Fixes: 66d8ffe3046d ("jaildesc: add kevent support") Reviewed by: kib, jamie Differential Revision: https://reviews.freebsd.org/D56945 --- sys/kern/kern_jaildesc.c | 10 +-- sys/sys/jaildesc.h | 1 - tests/sys/kern/Makefile | 2 + tests/sys/kern/jaildesc.c | 201 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 8 deletions(-) diff --git a/sys/kern/kern_jaildesc.c b/sys/kern/kern_jaildesc.c index 3f322b271400..679416e8fe28 100644 --- a/sys/kern/kern_jaildesc.c +++ b/sys/kern/kern_jaildesc.c @@ -197,10 +197,7 @@ jaildesc_knote(struct prison *pr, long hint) JAILDESC_LOCK(jd); if (hint == NOTE_JAIL_REMOVE) { jd->jd_flags |= JDF_REMOVED; - if (jd->jd_flags & JDF_SELECTED) { - jd->jd_flags &= ~JDF_SELECTED; - selwakeup(&jd->jd_selinfo); - } + selwakeup(&jd->jd_selinfo); } KNOTE_LOCKED(&jd->jd_selinfo.si_note, hint); JAILDESC_UNLOCK(jd); @@ -257,6 +254,7 @@ jaildesc_close(struct file *fp, struct thread *td) } prison_free(pr); } + seldrain(&jd->jd_selinfo); knlist_destroy(&jd->jd_selinfo.si_note); JAILDESC_LOCK_DESTROY(jd); free(jd, M_JAILDESC); @@ -276,10 +274,8 @@ jaildesc_poll(struct file *fp, int events, struct ucred *active_cred, JAILDESC_LOCK(jd); if (jd->jd_flags & JDF_REMOVED) revents |= POLLHUP; - if (revents == 0) { + else selrecord(td, &jd->jd_selinfo); - jd->jd_flags |= JDF_SELECTED; - } JAILDESC_UNLOCK(jd); return (revents); } diff --git a/sys/sys/jaildesc.h b/sys/sys/jaildesc.h index fda270d62e70..77c017f7e14d 100644 --- a/sys/sys/jaildesc.h +++ b/sys/sys/jaildesc.h @@ -71,7 +71,6 @@ struct jaildesc { /* * Flags for the jd_flags field */ -#define JDF_SELECTED 0x00000001 /* issue selwakeup() */ #define JDF_REMOVED 0x00000002 /* jail was removed */ #define JDF_OWNING 0x00000004 /* closing descriptor removes jail */ diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile index 5c2290c67b8f..8a2788470a8d 100644 --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -22,6 +22,7 @@ ATF_TESTS_C+= exterr_test ATF_TESTS_C+= fdgrowtable_test ATF_TESTS_C+= getdirentries_test ATF_TESTS_C+= jail_lookup_root +ATF_TESTS_C+= jaildesc ATF_TESTS_C+= inotify_test ATF_TESTS_C+= kill_zombie .if ${MK_OPENSSL} != "no" @@ -85,6 +86,7 @@ PROGS+= sendfile_helper LIBADD.copy_file_range+= md LIBADD.jail_lookup_root+= jail util +LIBADD.jaildesc+= pthread CFLAGS.sys_getrandom+= -I${SRCTOP}/sys/contrib/zstd/lib LIBADD.sys_getrandom+= zstd LIBADD.sys_getrandom+= c diff --git a/tests/sys/kern/jaildesc.c b/tests/sys/kern/jaildesc.c new file mode 100644 index 000000000000..11d751554887 --- /dev/null +++ b/tests/sys/kern/jaildesc.c @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2026 Mark Johnston + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* + * Create a persistent jail and return an owning descriptor for it. + * The jail is removed when the returned descriptor is closed. + */ +static int +create_jail(const char *name) +{ + struct iovec iov[8]; + int desc, jid, n; + + desc = -1; + n = 0; + iov[n].iov_base = __DECONST(void *, "name"); + iov[n++].iov_len = strlen("name") + 1; + iov[n].iov_base = __DECONST(void *, name); + iov[n++].iov_len = strlen(name) + 1; + iov[n].iov_base = __DECONST(void *, "path"); + iov[n++].iov_len = strlen("path") + 1; + iov[n].iov_base = __DECONST(void *, "/"); + iov[n++].iov_len = strlen("/") + 1; + iov[n].iov_base = __DECONST(void *, "persist"); + iov[n++].iov_len = strlen("persist") + 1; + iov[n].iov_base = NULL; + iov[n++].iov_len = 0; + iov[n].iov_base = __DECONST(void *, "desc"); + iov[n++].iov_len = strlen("desc") + 1; + iov[n].iov_base = &desc; + iov[n++].iov_len = sizeof(desc); + jid = jail_set(iov, n, JAIL_CREATE | JAIL_OWN_DESC); + ATF_REQUIRE_MSG(jid >= 0, "jail_set: %s", strerror(errno)); + return (desc); +} + +static void * +poll_jaildesc(void *arg) +{ + struct pollfd pfd; + + pfd.fd = *(int *)arg; + pfd.events = POLLHUP; + (void)poll(&pfd, 1, 5000); + return ((void *)(uintptr_t)pfd.revents); +} + +/* + * Regression test for the case where a jail descriptor is closed while a + * thread is blocking in poll(2) on it. + */ +ATF_TC(poll_close_race); +ATF_TC_HEAD(poll_close_race, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(poll_close_race, tc) +{ + pthread_t thr; + uintptr_t revents; + int error, jd; + + jd = create_jail("jaildesc_poll_close_race"); + + error = pthread_create(&thr, NULL, poll_jaildesc, &jd); + ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error)); + + /* Wait for the thread to block in poll(2). */ + usleep(250000); + + ATF_REQUIRE_MSG(close(jd) == 0, "close: %s", strerror(errno)); + + error = pthread_join(thr, (void *)&revents); + ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error)); + ATF_REQUIRE_EQ(revents, POLLNVAL); +} + +/* + * Verify that poll(2) of a jail descriptor returns POLLHUP when the jail + * is removed. + */ +ATF_TC(poll_remove_wakeup); +ATF_TC_HEAD(poll_remove_wakeup, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(poll_remove_wakeup, tc) +{ + pthread_t thr; + uintptr_t revents; + int error, jd; + + jd = create_jail("jaildesc_poll_remove_wakeup"); + + error = pthread_create(&thr, NULL, poll_jaildesc, &jd); + ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error)); + + /* Wait for the thread to block in poll(2). */ + usleep(250000); + + ATF_REQUIRE_MSG(jail_remove_jd(jd) == 0, + "jail_remove_jd: %s", strerror(errno)); + + error = pthread_join(thr, (void *)&revents); + ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error)); + ATF_REQUIRE_EQ(revents, POLLHUP); + + ATF_REQUIRE_MSG(close(jd) == 0, "close: %s", strerror(errno)); +} + +static int +get_jaildesc(const char *name) +{ + struct iovec iov[4]; + char namebuf[MAXHOSTNAMELEN]; + int desc, jid, n; + + strlcpy(namebuf, name, sizeof(namebuf)); + desc = -1; + n = 0; + iov[n].iov_base = __DECONST(void *, "name"); + iov[n++].iov_len = strlen("name") + 1; + iov[n].iov_base = namebuf; + iov[n++].iov_len = sizeof(namebuf); + iov[n].iov_base = __DECONST(void *, "desc"); + iov[n++].iov_len = strlen("desc") + 1; + iov[n].iov_base = &desc; + iov[n++].iov_len = sizeof(desc); + jid = jail_get(iov, n, JAIL_GET_DESC); + ATF_REQUIRE_MSG(jid >= 0, "jail_get: %s", strerror(errno)); + return (desc); +} + +/* + * Regression test for the same use-after-free as poll_close_race, but with a + * non-owning JAIL_GET_DESC descriptor obtained without root privileges. + */ +ATF_TC(poll_close_race_get_desc); +ATF_TC_HEAD(poll_close_race_get_desc, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(poll_close_race_get_desc, tc) +{ + struct passwd *pw; + pthread_t thr; + uintptr_t revents; + int error, jd, owning_jd; + + /* Create the jail as root; keep the owning descriptor for cleanup. */ + owning_jd = create_jail("jaildesc_poll_close_get_desc"); + + /* + * Drop root privileges. jail_get(2) with JAIL_GET_DESC does not + * require PRIV_JAIL_REMOVE, so a non-root process in the host prison + * can obtain a read-only descriptor for any visible jail. + */ + pw = getpwnam("nobody"); + ATF_REQUIRE_MSG(pw != NULL, "getpwnam: %s", strerror(errno)); + ATF_REQUIRE_MSG(setuid(pw->pw_uid) == 0, "setuid: %s", strerror(errno)); + + jd = get_jaildesc("jaildesc_poll_close_get_desc"); + + error = pthread_create(&thr, NULL, poll_jaildesc, &jd); + ATF_REQUIRE_MSG(error == 0, "pthread_create: %s", strerror(error)); + + /* Wait for the thread to block in poll(2). */ + usleep(250000); + + ATF_REQUIRE_MSG(close(jd) == 0, "close: %s", strerror(errno)); + + error = pthread_join(thr, (void *)&revents); + ATF_REQUIRE_MSG(error == 0, "pthread_join: %s", strerror(error)); + ATF_REQUIRE_EQ(revents, POLLNVAL); + + ATF_REQUIRE_MSG(close(owning_jd) == 0, "close: %s", strerror(errno)); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, poll_close_race); + ATF_TP_ADD_TC(tp, poll_remove_wakeup); + ATF_TP_ADD_TC(tp, poll_close_race_get_desc); + + return (atf_no_error()); +}