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 *
pick_target(const struct exec_target * tab)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
allocate_counting_pmc(void)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
spin(void)126 spin(void)
127 {
128 volatile unsigned long s = 0;
129 int i;
130
131 for (i = 0; i < 2000000; i++)
132 s += i;
133 }
134
135 static void
require_unprivileged_owner(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
run_target(const struct exec_target * t,int * still_attached)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);
ATF_TC_HEAD(exec_setgid_drops_pmc,tc)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 }
ATF_TC_BODY(exec_setgid_drops_pmc,tc)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);
ATF_TC_HEAD(exec_setuid_no_double_unlink,tc)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 }
ATF_TC_BODY(exec_setuid_no_double_unlink,tc)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
ATF_TP_ADD_TCS(tp)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