From nobody Thu Aug 27 14:03:16 2026 X-Original-To: dev-commits-src-main@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 4hW3Bs2jYXz6q1ZC for ; Thu, 27 Aug 2026 14:03:17 +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 "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hW3Bs0vX1z3fB5 for ; Thu, 27 Aug 2026 14:03:17 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1787839397; 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=LG3v2zjwvmxdM7TNbusWQb/JSNZtuHIy0p2rCaz+3qc=; b=rAqWyJx5Lp+ytw3Kz9B0bG45xKOo7ku4esUpafl6/YSC6qdRXQ97qraYE1tymkQkNC+V5h QDY8Z6SlvNaPl/hA5ORRbVHLRjyLkUFeVdMSmCQpHBeSpoJOK4PcMepxVtJUVdoWiCTmgV IlVyotWsK8Zbk/Jju+9x0ZrBFQeOdcayHzj1I2JGWUmc/uyxy73pphzToBTts8mveW9tYd FIw/O+WqLV3/9f0UAeqRazzMPn0s/1XcQlX3RhO+b7ftEM32e5Ji1s1/XoER1Tkm/iz+NB 5hsnsmpRNl9eps/L1opCgFWjLbLIBMfNQ9h0csXyvxXk8evFsjlUalOEGGhEhg== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1787839397; a=rsa-sha256; cv=none; b=ha6pSPgGG889YQplpoco/NUy2sH6KfwdjE/eOUexRlKxU/Vq5954AXE/al44OMC1XsVu+Z Qj9kx+tVraYpHqiql9rFFcBNe5l8OVAlVplb0zA40KBBbATHBkduyLK1J/XT6UvT4TIBbs +5vyims/qD5VbcFAZhjHpNzcc6f6OOlD1qMrL73kQqKlLG1NCLWvccZexXgdtolf4m5pzi HHL1VqfAxComsXQOuoPzY12cKjQkHKmksrxo1I1iytvPLKynr2T/b5KBb2wciUTr+I7Ked p/rz4TCWBiAx7h5hdVETUXRt8qz9vM6BrEbMh3m7Pq9HOcFkchg7uuplX2Nxsg== 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=1787839397; 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=LG3v2zjwvmxdM7TNbusWQb/JSNZtuHIy0p2rCaz+3qc=; b=HfnK9yilwEt0da4bsuf21GjH3CIDNBQM2oY7Ml5VjjCH9hTBbi6MScuX8LIx+AwJeOHhxW qQRrm6GLH6G5p8i2H1EeIpzzDogd0K2D0MEZrD52cR1a5TLew/5pa2f0WxPYRDK9biGDMF W0F7pFpx7YgvJlvrT9FzqZqaWDeXOMu+xel0Y48oQZuGfXFKvj9aHVBqN8O2ByYwI/Nmbp DmQ3SQHajarxx1/M1osKw85iEu5UmVjooOfSWFef3Mh4g0pC/A3tIuTVOtLfLQUsHceWDn gGa8AkLhtEGn+GfivYll3Xvo308h8q3hQTZ6LgSp9OEIP9Vkp4z+lL2amVp+vg== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hW3Br6Tq1zBSm for ; Thu, 27 Aug 2026 14:03:16 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 25951 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Thu, 27 Aug 2026 14:03:16 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Alexander Leidinger Subject: git: 068957e040f4 - main - hwpmc: add regression tests for a credential-changing exec List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@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: netchild X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 068957e040f40eb7f484b2530481f3eae02f9d44 Auto-Submitted: auto-generated Date: Thu, 27 Aug 2026 14:03:16 +0000 Message-Id: <6a9043a4.25951.47d021ee@gitrepo.freebsd.org> The branch main has been updated by netchild: URL: https://cgit.FreeBSD.org/src/commit/?id=068957e040f40eb7f484b2530481f3eae02f9d44 commit 068957e040f40eb7f484b2530481f3eae02f9d44 Author: Alexander Leidinger AuthorDate: 2026-08-25 18:55:22 +0000 Commit: Alexander Leidinger CommitDate: 2026-08-27 14:02:36 +0000 hwpmc: add regression tests for a credential-changing exec This tests what FreeBSD-SA-26:56.hwpmc fixed. exec_setgid_drops_pmc asserts the kernel takes a process-mode PMC away when its target execs a set-gid program its owner is not entitled to trace. exec_setuid_no_double_unlink lets the target exec a set-uid program; the teardown must unlink the process descriptor exactly once, and completing at all is the assertion. Both need an unprivileged owner and must not drop privileges themselves, since p_candebug() would then refuse the target to its own owner; they ask for require.user instead. MFC after: 1 month MFC to: stable/15 MFC to: stable/14 Assisted-by: Claude Code (Opus 5) --- tests/sys/pmc/Makefile | 1 + tests/sys/pmc/pmc_exec_test.c | 264 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) diff --git a/tests/sys/pmc/Makefile b/tests/sys/pmc/Makefile index abd049a8c50a..8b99a4c21bff 100644 --- a/tests/sys/pmc/Makefile +++ b/tests/sys/pmc/Makefile @@ -1,6 +1,7 @@ TESTSDIR= ${TESTSBASE}/sys/pmc ATF_TESTS_C+= pmc_detach_test +ATF_TESTS_C+= pmc_exec_test ATF_TESTS_C+= pmc_wrap_test LIBADD+= pmc diff --git a/tests/sys/pmc/pmc_exec_test.c b/tests/sys/pmc/pmc_exec_test.c new file mode 100644 index 000000000000..d4d476cd20de --- /dev/null +++ b/tests/sys/pmc/pmc_exec_test.c @@ -0,0 +1,264 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 Alexander Leidinger + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other advertising materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/** + * @file + * Regression tests for a process-mode PMC whose target exec()s a program + * that changes its credentials: what FreeBSD-SA-26:56.hwpmc fixed. + * + * pmc_process_exec() must drop such a PMC unless its owner may still trace + * the new credentials, and the detach must unlink the process descriptor + * exactly once. + * + * The owner must be unprivileged: root may trace anything, so as root + * neither case reaches the branch under test - hence require.user. The + * test must not drop privileges itself either: setuid(2) sets P_SUGID, + * fork(2) passes it to the child, and p_candebug() then refuses the target + * to its unprivileged owner, so pmc_attach() would fail with EPERM first. + * + * The privileged exec target is picked at run time from base binaries, and + * a case skips if none of them carries a set-id bit any more. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +static const char *counting_events[] = { + "instructions", + "cycles", + "branches", + "unhalted-core-cycles", + "inst_retired.any", + "cpu_clk_unhalted.thread", + "ls_not_halted_cyc", + NULL +}; + +/** + * @internal + * Exec targets. 'setid' is the bit the entry needs; the argument vectors + * are chosen so the program exits at once and touches nothing. wall(1) + * reads its message from stdin, which the caller holds open on a pipe, so + * that target stays alive long enough to be inspected. + */ +struct exec_target { + const char *path; + const char *const argv[5]; + mode_t setid; + int blocks_on_stdin; +}; + +static const struct exec_target setgid_targets[] = { + { "/usr/bin/wall", { "wall", NULL }, S_ISGID, 1 }, + { NULL, { NULL }, 0, 0 } +}; + +static const struct exec_target setuid_targets[] = { + { "/sbin/ping", { "ping", "-c", "1", "127.0.0.1", NULL }, S_ISUID, 0 }, + { NULL, { NULL }, 0, 0 } +}; + +static const struct exec_target * +pick_target(const struct exec_target *tab) +{ + struct stat sb; + int i; + + for (i = 0; tab[i].path != NULL; i++) { + if (stat(tab[i].path, &sb) != 0) + continue; + if ((sb.st_mode & tab[i].setid) != 0) + return (&tab[i]); + } + return (NULL); +} + +static pmc_id_t +allocate_counting_pmc(void) +{ + pmc_id_t id = PMC_ID_INVALID; + int i; + + for (i = 0; counting_events[i] != NULL; i++) { + if (pmc_allocate(counting_events[i], PMC_MODE_TC, 0, + PMC_CPU_ANY, &id, 0) == 0) + return (id); + } + return (PMC_ID_INVALID); +} + +static void +spin(void) +{ + volatile unsigned long s = 0; + int i; + + for (i = 0; i < 2000000; i++) + s += i; +} + +static void +require_unprivileged_owner(void) +{ + + if (geteuid() == 0) + atf_tc_skip("the PMC owner must be unprivileged: root may " + "trace any credentials, so the check under test is never " + "reached"); +} + +/** + * @internal + * Fork a target, attach a running counting PMC to it, and let it exec the + * privileged program. Returns with the PMC released; *still_attached is + * only meaningful for a target that blocks after the exec. + */ +static void +run_target(const struct exec_target *t, int *still_attached) +{ + pmc_id_t id; + pid_t target; + char token; + int gopipe[2], inpipe[2], status; + + ATF_REQUIRE(pipe(gopipe) == 0); + ATF_REQUIRE(pipe(inpipe) == 0); + ATF_REQUIRE((target = fork()) >= 0); + + if (target == 0) { + (void)close(gopipe[1]); + if (t->blocks_on_stdin) + (void)dup2(inpipe[0], STDIN_FILENO); + (void)close(inpipe[1]); + if (read(gopipe[0], &token, 1) != 1) + _exit(1); + spin(); + (void)execv(t->path, __DECONST(char **, t->argv)); + _exit(1); + } + (void)close(gopipe[0]); + (void)close(inpipe[0]); + + if (pmc_init() != 0) { + (void)kill(target, SIGKILL); + (void)waitpid(target, &status, 0); + atf_tc_skip("hwpmc(4) is not available"); + } + if ((id = allocate_counting_pmc()) == PMC_ID_INVALID) { + (void)kill(target, SIGKILL); + (void)waitpid(target, &status, 0); + atf_tc_skip("no process-mode counting event is allocatable"); + } + + ATF_REQUIRE_MSG(pmc_attach(id, target) == 0, "pmc_attach: %s", + strerror(errno)); + ATF_REQUIRE(pmc_start(id) == 0); + + /* Into execve(2). */ + ATF_REQUIRE(write(gopipe[1], "g", 1) == 1); + (void)close(gopipe[1]); + + if (t->blocks_on_stdin) { + (void)usleep(400000); + errno = 0; + *still_attached = pmc_detach(id, target) == 0; + if (!*still_attached) + ATF_REQUIRE_MSG(errno == ESRCH, "pmc_detach: %s", + strerror(errno)); + (void)kill(target, SIGKILL); + } else { + *still_attached = 0; + } + (void)waitpid(target, &status, 0); + (void)pmc_release(id); +} + +ATF_TC(exec_setgid_drops_pmc); +ATF_TC_HEAD(exec_setgid_drops_pmc, tc) +{ + + atf_tc_set_md_var(tc, "descr", + "a process-mode PMC is detached when its target execs into " + "credentials its owner may not trace"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(exec_setgid_drops_pmc, tc) +{ + const struct exec_target *t; + int still_attached; + + require_unprivileged_owner(); + if ((t = pick_target(setgid_targets)) == NULL) + atf_tc_skip("no set-gid exec target available"); + + run_target(t, &still_attached); + ATF_REQUIRE_MSG(!still_attached, + "the PMC survived an exec into credentials its owner may not " + "trace"); +} + +ATF_TC(exec_setuid_no_double_unlink); +ATF_TC_HEAD(exec_setuid_no_double_unlink, tc) +{ + + atf_tc_set_md_var(tc, "descr", + "detaching a process-mode PMC at a credential-changing exec " + "unlinks the process descriptor exactly once"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(exec_setuid_no_double_unlink, tc) +{ + const struct exec_target *t; + int still_attached; + + require_unprivileged_owner(); + if ((t = pick_target(setuid_targets)) == NULL) + atf_tc_skip("no set-uid exec target available"); + + /* Completing at all is the assertion. */ + run_target(t, &still_attached); +} + +ATF_TP_ADD_TCS(tp) +{ + + ATF_TP_ADD_TC(tp, exec_setgid_drops_pmc); + ATF_TP_ADD_TC(tp, exec_setuid_no_double_unlink); + + return (atf_no_error()); +}