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 * Tests for hwpmc(4)'s handling of the PMC handle and for the privilege
32 * boundaries around the operations that do not take one.
33 *
34 * A pmc_id_t packs CPU, mode, class and row index into one 32-bit word,
35 * which the kernel hands to userland and accepts back on eleven
36 * operations. The cases pass ids that were never allocated, ids that were
37 * released, and ids belonging to a different process, and require each
38 * operation to refuse cleanly rather than act on a wrong PMC.
39 *
40 * Two properties are asserted because a refactor could break either with
41 * every existing test still passing: the CPU field is 12 bits, so a caller
42 * may name CPU 4095 on a machine that has four; and ids are unique per
43 * owner rather than globally, so two processes are given the same numeric
44 * id and only the per-owner lookup keeps them apart.
45 *
46 * The PMCs are SOFT-class, so nothing here needs a hardware counter. The
47 * privilege cases need an unprivileged subject - as root the checks under
48 * test are never reached - hence require.user.
49 */
50
51 #include <sys/param.h>
52 #include <sys/stat.h>
53 #include <sys/sysctl.h>
54 #include <sys/wait.h>
55
56 #include <errno.h>
57 #include <pmc.h>
58 #include <signal.h>
59 #include <stdint.h>
60 #include <stdio.h>
61 #include <stdlib.h>
62 #include <string.h>
63 #include <unistd.h>
64
65 #include <atf-c.h>
66
67 /**
68 * @internal
69 * The operations that take a pmc_id_t and reach the kernel with it.
70 * pmc_width() is deliberately absent: it answers from a userland copy of
71 * the CPU info and never enters the driver.
72 */
op_attach(pmc_id_t id)73 static int op_attach(pmc_id_t id) { return (pmc_attach(id, getpid())); }
op_detach(pmc_id_t id)74 static int op_detach(pmc_id_t id) { return (pmc_detach(id, getpid())); }
op_start(pmc_id_t id)75 static int op_start(pmc_id_t id) { return (pmc_start(id)); }
op_stop(pmc_id_t id)76 static int op_stop(pmc_id_t id) { return (pmc_stop(id)); }
op_write(pmc_id_t id)77 static int op_write(pmc_id_t id) { return (pmc_write(id, 0)); }
op_set(pmc_id_t id)78 static int op_set(pmc_id_t id) { return (pmc_set(id, 0)); }
op_release(pmc_id_t id)79 static int op_release(pmc_id_t id) { return (pmc_release(id)); }
80
81 static int
op_read(pmc_id_t id)82 op_read(pmc_id_t id)
83 {
84 pmc_value_t v;
85
86 return (pmc_read(id, &v));
87 }
88
89 static int
op_rw(pmc_id_t id)90 op_rw(pmc_id_t id)
91 {
92 pmc_value_t v;
93
94 return (pmc_rw(id, 0, &v));
95 }
96
97 static int
op_caps(pmc_id_t id)98 op_caps(pmc_id_t id)
99 {
100 uint32_t c;
101
102 return (pmc_capabilities(id, &c));
103 }
104
105 static int
op_getmsr(pmc_id_t id)106 op_getmsr(pmc_id_t id)
107 {
108 uint32_t msr;
109
110 return (pmc_get_msr(id, &msr));
111 }
112
113 static const struct {
114 const char *name;
115 int (*fn)(pmc_id_t);
116 } id_ops[] = {
117 { "attach", op_attach },
118 { "detach", op_detach },
119 { "start", op_start },
120 { "stop", op_stop },
121 { "read", op_read },
122 { "write", op_write },
123 { "rw", op_rw },
124 { "set", op_set },
125 { "caps", op_caps },
126 { "getmsr", op_getmsr },
127 { "release", op_release },
128 };
129
130 static void
require_hwpmc(void)131 require_hwpmc(void)
132 {
133
134 if (pmc_init() != 0)
135 atf_tc_skip("hwpmc(4) is not available");
136 }
137
138 static void
require_unprivileged(void)139 require_unprivileged(void)
140 {
141
142 if (geteuid() == 0)
143 atf_tc_skip("the subject must be unprivileged: root passes "
144 "the check under test");
145 }
146
147 /**
148 * @internal
149 * Allocate the first SOFT event this mode accepts. Every SOFT event takes
150 * the same allocate/attach/detach/release path, so which one it is does not
151 * matter; that one exists at all is what the case needs.
152 */
153 static pmc_id_t
allocate_soft_pmc(enum pmc_mode mode)154 allocate_soft_pmc(enum pmc_mode mode)
155 {
156 const char **names;
157 char spec[128];
158 pmc_id_t id;
159 int nnames, i;
160
161 if (pmc_event_names_of_class(PMC_CLASS_SOFT, &names, &nnames) != 0)
162 return (PMC_ID_INVALID);
163 for (i = 0; i < nnames; i++) {
164 (void)snprintf(spec, sizeof(spec), "SOFT-%s", names[i]);
165 if (pmc_allocate(spec, mode, 0, PMC_CPU_ANY, &id, 0) == 0)
166 return (id);
167 }
168 return (PMC_ID_INVALID);
169 }
170
171 static pmc_id_t
require_soft_pmc(enum pmc_mode mode)172 require_soft_pmc(enum pmc_mode mode)
173 {
174 pmc_id_t id;
175
176 if ((id = allocate_soft_pmc(mode)) == PMC_ID_INVALID)
177 atf_tc_skip("no SOFT-class PMC is allocatable");
178 return (id);
179 }
180
181 /**
182 * @internal
183 * Every id-taking operation must refuse this id. Checks rather than
184 * requires, so one operation that wrongly accepts does not hide the rest.
185 */
186 static void
check_all_ops_refuse(pmc_id_t id,const char * what)187 check_all_ops_refuse(pmc_id_t id, const char *what)
188 {
189 size_t i;
190
191 for (i = 0; i < nitems(id_ops); i++) {
192 errno = 0;
193 ATF_CHECK_MSG(id_ops[i].fn(id) != 0,
194 "pmc_%s() accepted %s id 0x%08x", id_ops[i].name, what,
195 id);
196 }
197 }
198
199 ATF_TC_WITHOUT_HEAD(never_allocated_id);
ATF_TC_BODY(never_allocated_id,tc)200 ATF_TC_BODY(never_allocated_id, tc)
201 {
202 pmc_id_t forged[6];
203 size_t i;
204 int ncpu;
205
206 require_hwpmc();
207 ncpu = pmc_ncpu();
208 ATF_REQUIRE(ncpu > 0);
209
210 /*
211 * Nothing has been allocated in this process, so every one of these
212 * has to be refused - including the two that are well-formed apart
213 * from naming a CPU that does not exist.
214 */
215 forged[0] = PMC_ID_INVALID;
216 forged[1] = PMC_ID_MAKE_ID(0xFFF, PMC_MODE_TC, PMC_CLASS_SOFT, 0);
217 forged[2] = PMC_ID_MAKE_ID(PMC_CPU_ANY, PMC_MODE_TC, PMC_CLASS_SOFT,
218 0xFF);
219 forged[3] = PMC_ID_MAKE_ID(ncpu, PMC_MODE_TC, PMC_CLASS_SOFT, 0);
220 forged[4] = 0;
221 forged[5] = PMC_ID_MAKE_ID(PMC_CPU_ANY, 0xF, 0xFF, 0);
222
223 for (i = 0; i < nitems(forged); i++)
224 check_all_ops_refuse(forged[i], "never-allocated");
225 }
226
227 ATF_TC_WITHOUT_HEAD(released_id);
ATF_TC_BODY(released_id,tc)228 ATF_TC_BODY(released_id, tc)
229 {
230 pmc_id_t id;
231
232 require_hwpmc();
233 id = require_soft_pmc(PMC_MODE_TC);
234
235 ATF_REQUIRE_MSG(pmc_attach(id, getpid()) == 0, "pmc_attach: %s",
236 strerror(errno));
237 ATF_REQUIRE_MSG(pmc_start(id) == 0, "pmc_start: %s", strerror(errno));
238 ATF_REQUIRE_MSG(pmc_stop(id) == 0, "pmc_stop: %s", strerror(errno));
239 ATF_REQUIRE_MSG(pmc_detach(id, getpid()) == 0, "pmc_detach: %s",
240 strerror(errno));
241 ATF_REQUIRE_MSG(pmc_release(id) == 0, "pmc_release: %s",
242 strerror(errno));
243
244 /* The same value is now stale. Reusing it is the double-free shape. */
245 check_all_ops_refuse(id, "released");
246 }
247
248 ATF_TC_WITHOUT_HEAD(another_owners_id);
ATF_TC_BODY(another_owners_id,tc)249 ATF_TC_BODY(another_owners_id, tc)
250 {
251 pmc_id_t id, mine;
252 pmc_value_t v;
253 pid_t child;
254 ssize_t n;
255 int down[2], up[2], status;
256 char token;
257
258 require_hwpmc();
259 ATF_REQUIRE(pipe(down) == 0);
260 ATF_REQUIRE(pipe(up) == 0);
261 ATF_REQUIRE((child = fork()) >= 0);
262
263 if (child == 0) {
264 pmc_value_t cv;
265 pmc_id_t cid;
266
267 (void)close(down[1]);
268 (void)close(up[0]);
269 if (pmc_init() != 0)
270 _exit(2);
271 if ((cid = allocate_soft_pmc(PMC_MODE_TC)) == PMC_ID_INVALID)
272 _exit(3);
273 if (pmc_attach(cid, getpid()) != 0 || pmc_start(cid) != 0)
274 _exit(4);
275 if (write(up[1], &cid, sizeof(cid)) != (ssize_t)sizeof(cid))
276 _exit(5);
277 if (read(down[0], &token, 1) != 1)
278 _exit(6);
279 /* Untouched by anything the parent did? */
280 _exit(pmc_read(cid, &cv) == 0 ? 0 : 7);
281 }
282 (void)close(down[0]);
283 (void)close(up[1]);
284
285 n = read(up[0], &id, sizeof(id));
286 if (n != (ssize_t)sizeof(id)) {
287 (void)kill(child, SIGKILL);
288 (void)waitpid(child, &status, 0);
289 atf_tc_skip("the child could not allocate a SOFT-class PMC");
290 }
291
292 /* We own nothing, so the child's id must not resolve for us. */
293 check_all_ops_refuse(id, "another owner's");
294
295 /*
296 * Now hold one of our own. Ids are per owner, so ours is very
297 * likely the same number; operating on it must still reach only
298 * ours, which the child's exit status confirms.
299 */
300 mine = allocate_soft_pmc(PMC_MODE_TC);
301 if (mine != PMC_ID_INVALID) {
302 ATF_CHECK_MSG(pmc_attach(mine, getpid()) == 0, "pmc_attach: %s",
303 strerror(errno));
304 ATF_CHECK(pmc_read(mine, &v) == 0);
305 ATF_CHECK(pmc_detach(mine, getpid()) == 0);
306 ATF_CHECK(pmc_release(mine) == 0);
307 }
308
309 ATF_REQUIRE(write(down[1], "g", 1) == 1);
310 (void)close(down[1]);
311 ATF_REQUIRE(waitpid(child, &status, 0) == child);
312 ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
313 "the child's own PMC did not survive our use of its id "
314 "(child exit %d)", WIFEXITED(status) ? WEXITSTATUS(status) : -1);
315 }
316
317 ATF_TC_WITHOUT_HEAD(read_write_before_start);
ATF_TC_BODY(read_write_before_start,tc)318 ATF_TC_BODY(read_write_before_start, tc)
319 {
320 pmc_value_t old, v;
321 pmc_id_t id;
322
323 require_hwpmc();
324 id = require_soft_pmc(PMC_MODE_TC);
325 ATF_REQUIRE_MSG(pmc_attach(id, getpid()) == 0, "pmc_attach: %s",
326 strerror(errno));
327
328 /* Allocated and attached, never started: a read must still work. */
329 ATF_CHECK_MSG(pmc_read(id, &v) == 0, "pmc_read before start: %s",
330 strerror(errno));
331
332 ATF_CHECK_MSG(pmc_write(id, 42) == 0, "pmc_write before start: %s",
333 strerror(errno));
334 ATF_CHECK_MSG(pmc_read(id, &v) == 0, "pmc_read: %s", strerror(errno));
335 ATF_CHECK_MSG(v == 42, "wrote 42, read back %ju", (uintmax_t)v);
336
337 ATF_CHECK(pmc_rw(id, 7, &old) == 0);
338 ATF_CHECK_MSG(old == 42, "pmc_rw returned %ju, expected the 42 "
339 "written before it", (uintmax_t)old);
340
341 ATF_REQUIRE(pmc_start(id) == 0);
342 ATF_REQUIRE(pmc_stop(id) == 0);
343 ATF_CHECK(pmc_detach(id, getpid()) == 0);
344 ATF_CHECK(pmc_release(id) == 0);
345 }
346
347 ATF_TC(pmcadmin_requires_privilege);
ATF_TC_HEAD(pmcadmin_requires_privilege,tc)348 ATF_TC_HEAD(pmcadmin_requires_privilege, tc)
349 {
350
351 atf_tc_set_md_var(tc, "descr",
352 "PMC_OP_PMCADMIN is refused to an unprivileged caller");
353 atf_tc_set_md_var(tc, "require.user", "unprivileged");
354 }
ATF_TC_BODY(pmcadmin_requires_privilege,tc)355 ATF_TC_BODY(pmcadmin_requires_privilege, tc)
356 {
357
358 require_hwpmc();
359 require_unprivileged();
360
361 errno = 0;
362 ATF_CHECK_MSG(pmc_disable(0, 0) != 0,
363 "an unprivileged process disabled a PMC row");
364 ATF_CHECK_MSG(errno == EPERM, "pmc_disable: expected EPERM, got %s",
365 strerror(errno));
366
367 errno = 0;
368 ATF_CHECK_MSG(pmc_enable(0, 0) != 0,
369 "an unprivileged process enabled a PMC row");
370 ATF_CHECK_MSG(errno == EPERM, "pmc_enable: expected EPERM, got %s",
371 strerror(errno));
372 }
373
374 ATF_TC(system_mode_requires_privilege);
ATF_TC_HEAD(system_mode_requires_privilege,tc)375 ATF_TC_HEAD(system_mode_requires_privilege, tc)
376 {
377
378 atf_tc_set_md_var(tc, "descr",
379 "a system-wide PMC is refused to an unprivileged caller");
380 atf_tc_set_md_var(tc, "require.user", "unprivileged");
381 }
ATF_TC_BODY(system_mode_requires_privilege,tc)382 ATF_TC_BODY(system_mode_requires_privilege, tc)
383 {
384 const char **names;
385 char spec[128];
386 pmc_id_t id;
387 size_t len;
388 int nnames, i, unpriv;
389
390 require_hwpmc();
391 require_unprivileged();
392
393 len = sizeof(unpriv);
394 if (sysctlbyname("security.bsd.unprivileged_syspmcs", &unpriv, &len,
395 NULL, 0) != 0)
396 atf_tc_skip("security.bsd.unprivileged_syspmcs is unreadable");
397 if (unpriv != 0)
398 atf_tc_skip("security.bsd.unprivileged_syspmcs is set: "
399 "unprivileged system-wide PMCs are permitted here");
400
401 if (pmc_event_names_of_class(PMC_CLASS_SOFT, &names, &nnames) != 0 ||
402 nnames == 0)
403 atf_tc_skip("no SOFT-class events");
404
405 /*
406 * A system-mode PMC must name a real CPU: PMC_CPU_ANY is rejected
407 * before the privilege check is reached, which would make this case
408 * pass for the wrong reason.
409 */
410 for (i = 0; i < nnames; i++) {
411 (void)snprintf(spec, sizeof(spec), "SOFT-%s", names[i]);
412 errno = 0;
413 if (pmc_allocate(spec, PMC_MODE_SC, 0, 0, &id, 0) == 0) {
414 (void)pmc_release(id);
415 atf_tc_fail("an unprivileged process allocated a "
416 "system-wide PMC (%s)", spec);
417 }
418 ATF_CHECK_MSG(errno == EPERM,
419 "%s: expected EPERM, got %s", spec, strerror(errno));
420 }
421 }
422
423 /**
424 * @internal
425 * A target that has exec'ed a set-id binary carries P_SUGID, which
426 * p_candebug() refuses to an unprivileged subject - the same rule
427 * PMC_OP_PMCATTACH inherits. wall(1) is set-gid and reads its message from
428 * stdin, so it stays alive on a pipe long enough to be attached to.
429 */
430 ATF_TC(attach_to_sugid_target);
ATF_TC_HEAD(attach_to_sugid_target,tc)431 ATF_TC_HEAD(attach_to_sugid_target, tc)
432 {
433
434 atf_tc_set_md_var(tc, "descr",
435 "attaching a PMC to a process that has exec'ed a set-id binary "
436 "is refused to an unprivileged owner");
437 atf_tc_set_md_var(tc, "require.user", "unprivileged");
438 }
ATF_TC_BODY(attach_to_sugid_target,tc)439 ATF_TC_BODY(attach_to_sugid_target, tc)
440 {
441 static const char *const argv[] = { "wall", NULL };
442 static const char path[] = "/usr/bin/wall";
443 struct stat sb;
444 pmc_id_t id;
445 pid_t target;
446 int gopipe[2], inpipe[2], status, rc;
447 char token;
448
449 require_hwpmc();
450 require_unprivileged();
451
452 if (stat(path, &sb) != 0 || (sb.st_mode & S_ISGID) == 0)
453 atf_tc_skip("%s is not set-gid here", path);
454 if (getegid() == sb.st_gid)
455 atf_tc_skip("the subject is already in %s's group, so it may "
456 "trace the target", path);
457
458 ATF_REQUIRE(pipe(gopipe) == 0);
459 ATF_REQUIRE(pipe(inpipe) == 0);
460 ATF_REQUIRE((target = fork()) >= 0);
461 if (target == 0) {
462 (void)close(gopipe[1]);
463 (void)dup2(inpipe[0], STDIN_FILENO);
464 (void)close(inpipe[1]);
465 if (read(gopipe[0], &token, 1) != 1)
466 _exit(1);
467 (void)execv(path, __DECONST(char **, argv));
468 _exit(1);
469 }
470 (void)close(gopipe[0]);
471 (void)close(inpipe[0]);
472
473 id = require_soft_pmc(PMC_MODE_TC);
474
475 /* Let it exec, then give it time to get there. */
476 ATF_REQUIRE(write(gopipe[1], "g", 1) == 1);
477 (void)close(gopipe[1]);
478 (void)usleep(400000);
479
480 errno = 0;
481 rc = pmc_attach(id, target);
482 if (rc == 0)
483 (void)pmc_detach(id, target);
484 (void)kill(target, SIGKILL);
485 (void)close(inpipe[1]);
486 (void)waitpid(target, &status, 0);
487 (void)pmc_release(id);
488
489 ATF_CHECK_MSG(rc != 0,
490 "an unprivileged owner attached a PMC to a set-id target");
491 ATF_CHECK_MSG(errno == EPERM, "pmc_attach: expected EPERM, got %s",
492 strerror(errno));
493 }
494
ATF_TP_ADD_TCS(tp)495 ATF_TP_ADD_TCS(tp)
496 {
497
498 ATF_TP_ADD_TC(tp, never_allocated_id);
499 ATF_TP_ADD_TC(tp, released_id);
500 ATF_TP_ADD_TC(tp, another_owners_id);
501 ATF_TP_ADD_TC(tp, read_write_before_start);
502 ATF_TP_ADD_TC(tp, pmcadmin_requires_privilege);
503 ATF_TP_ADD_TC(tp, system_mode_requires_privilege);
504 ATF_TP_ADD_TC(tp, attach_to_sugid_target);
505
506 return (atf_no_error());
507 }
508