1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2026 Alexander Leidinger <netchild@FreeBSD.org> 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions 8 * are met: 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other advertising materials provided with the 14 * distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 20 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 * SUCH DAMAGE. 27 */ 28 29 /** 30 * @file 31 * Regression tests for a process-mode PMC whose target exec()s a program 32 * that changes its credentials: what FreeBSD-SA-26:56.hwpmc fixed. 33 * 34 * pmc_process_exec() must drop such a PMC unless its owner may still trace 35 * the new credentials, and the detach must unlink the process descriptor 36 * exactly once. 37 * 38 * The owner must be unprivileged: root may trace anything, so as root 39 * neither case reaches the branch under test - hence require.user. The 40 * test must not drop privileges itself either: setuid(2) sets P_SUGID, 41 * fork(2) passes it to the child, and p_candebug() then refuses the target 42 * to its unprivileged owner, so pmc_attach() would fail with EPERM first. 43 * 44 * The privileged exec target is picked at run time from base binaries, and 45 * a case skips if none of them carries a set-id bit any more. 46 */ 47 48 #include <sys/types.h> 49 #include <sys/stat.h> 50 #include <sys/wait.h> 51 52 #include <errno.h> 53 #include <pmc.h> 54 #include <signal.h> 55 #include <stdlib.h> 56 #include <string.h> 57 #include <unistd.h> 58 59 #include <atf-c.h> 60 61 static const char *counting_events[] = { 62 "instructions", 63 "cycles", 64 "branches", 65 "unhalted-core-cycles", 66 "inst_retired.any", 67 "cpu_clk_unhalted.thread", 68 "ls_not_halted_cyc", 69 NULL 70 }; 71 72 /** 73 * @internal 74 * Exec targets. 'setid' is the bit the entry needs; the argument vectors 75 * are chosen so the program exits at once and touches nothing. wall(1) 76 * reads its message from stdin, which the caller holds open on a pipe, so 77 * that target stays alive long enough to be inspected. 78 */ 79 struct exec_target { 80 const char *path; 81 const char *const argv[5]; 82 mode_t setid; 83 int blocks_on_stdin; 84 }; 85 86 static const struct exec_target setgid_targets[] = { 87 { "/usr/bin/wall", { "wall", NULL }, S_ISGID, 1 }, 88 { NULL, { NULL }, 0, 0 } 89 }; 90 91 static const struct exec_target setuid_targets[] = { 92 { "/sbin/ping", { "ping", "-c", "1", "127.0.0.1", NULL }, S_ISUID, 0 }, 93 { NULL, { NULL }, 0, 0 } 94 }; 95 96 static const struct exec_target * 97 pick_target(const struct exec_target *tab) 98 { 99 struct stat sb; 100 int i; 101 102 for (i = 0; tab[i].path != NULL; i++) { 103 if (stat(tab[i].path, &sb) != 0) 104 continue; 105 if ((sb.st_mode & tab[i].setid) != 0) 106 return (&tab[i]); 107 } 108 return (NULL); 109 } 110 111 static pmc_id_t 112 allocate_counting_pmc(void) 113 { 114 pmc_id_t id = PMC_ID_INVALID; 115 int i; 116 117 for (i = 0; counting_events[i] != NULL; i++) { 118 if (pmc_allocate(counting_events[i], PMC_MODE_TC, 0, 119 PMC_CPU_ANY, &id, 0) == 0) 120 return (id); 121 } 122 return (PMC_ID_INVALID); 123 } 124 125 static void 126 spin(void) 127 { 128 volatile unsigned long s __unused = 0; 129 int i; 130 131 for (i = 0; i < 2000000; i++) 132 s += i; 133 } 134 135 static void 136 require_unprivileged_owner(void) 137 { 138 139 if (geteuid() == 0) 140 atf_tc_skip("the PMC owner must be unprivileged: root may " 141 "trace any credentials, so the check under test is never " 142 "reached"); 143 } 144 145 /** 146 * @internal 147 * Fork a target, attach a running counting PMC to it, and let it exec the 148 * privileged program. Returns with the PMC released; *still_attached is 149 * only meaningful for a target that blocks after the exec. 150 */ 151 static void 152 run_target(const struct exec_target *t, int *still_attached) 153 { 154 pmc_id_t id; 155 pid_t target; 156 char token; 157 int gopipe[2], inpipe[2], status; 158 159 ATF_REQUIRE(pipe(gopipe) == 0); 160 ATF_REQUIRE(pipe(inpipe) == 0); 161 ATF_REQUIRE((target = fork()) >= 0); 162 163 if (target == 0) { 164 (void)close(gopipe[1]); 165 if (t->blocks_on_stdin) 166 (void)dup2(inpipe[0], STDIN_FILENO); 167 (void)close(inpipe[1]); 168 if (read(gopipe[0], &token, 1) != 1) 169 _exit(1); 170 spin(); 171 (void)execv(t->path, __DECONST(char **, t->argv)); 172 _exit(1); 173 } 174 (void)close(gopipe[0]); 175 (void)close(inpipe[0]); 176 177 if (pmc_init() != 0) { 178 (void)kill(target, SIGKILL); 179 (void)waitpid(target, &status, 0); 180 atf_tc_skip("hwpmc(4) is not available"); 181 } 182 if ((id = allocate_counting_pmc()) == PMC_ID_INVALID) { 183 (void)kill(target, SIGKILL); 184 (void)waitpid(target, &status, 0); 185 atf_tc_skip("no process-mode counting event is allocatable"); 186 } 187 188 ATF_REQUIRE_MSG(pmc_attach(id, target) == 0, "pmc_attach: %s", 189 strerror(errno)); 190 ATF_REQUIRE(pmc_start(id) == 0); 191 192 /* Into execve(2). */ 193 ATF_REQUIRE(write(gopipe[1], "g", 1) == 1); 194 (void)close(gopipe[1]); 195 196 if (t->blocks_on_stdin) { 197 (void)usleep(400000); 198 errno = 0; 199 *still_attached = pmc_detach(id, target) == 0; 200 if (!*still_attached) 201 ATF_REQUIRE_MSG(errno == ESRCH, "pmc_detach: %s", 202 strerror(errno)); 203 (void)kill(target, SIGKILL); 204 } else { 205 *still_attached = 0; 206 } 207 (void)waitpid(target, &status, 0); 208 (void)pmc_release(id); 209 } 210 211 ATF_TC(exec_setgid_drops_pmc); 212 ATF_TC_HEAD(exec_setgid_drops_pmc, tc) 213 { 214 215 atf_tc_set_md_var(tc, "descr", 216 "a process-mode PMC is detached when its target execs into " 217 "credentials its owner may not trace"); 218 atf_tc_set_md_var(tc, "require.user", "unprivileged"); 219 } 220 ATF_TC_BODY(exec_setgid_drops_pmc, tc) 221 { 222 const struct exec_target *t; 223 int still_attached; 224 225 require_unprivileged_owner(); 226 if ((t = pick_target(setgid_targets)) == NULL) 227 atf_tc_skip("no set-gid exec target available"); 228 229 run_target(t, &still_attached); 230 ATF_REQUIRE_MSG(!still_attached, 231 "the PMC survived an exec into credentials its owner may not " 232 "trace"); 233 } 234 235 ATF_TC(exec_setuid_no_double_unlink); 236 ATF_TC_HEAD(exec_setuid_no_double_unlink, tc) 237 { 238 239 atf_tc_set_md_var(tc, "descr", 240 "detaching a process-mode PMC at a credential-changing exec " 241 "unlinks the process descriptor exactly once"); 242 atf_tc_set_md_var(tc, "require.user", "unprivileged"); 243 } 244 ATF_TC_BODY(exec_setuid_no_double_unlink, tc) 245 { 246 const struct exec_target *t; 247 int still_attached; 248 249 require_unprivileged_owner(); 250 if ((t = pick_target(setuid_targets)) == NULL) 251 atf_tc_skip("no set-uid exec target available"); 252 253 /* Completing at all is the assertion. */ 254 run_target(t, &still_attached); 255 } 256 257 ATF_TP_ADD_TCS(tp) 258 { 259 260 ATF_TP_ADD_TC(tp, exec_setgid_drops_pmc); 261 ATF_TP_ADD_TC(tp, exec_setuid_no_double_unlink); 262 263 return (atf_no_error()); 264 } 265