xref: /freebsd/tests/sys/pmc/pmc_exec_test.c (revision 068957e040f40eb7f484b2530481f3eae02f9d44)
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