xref: /illumos-gate/usr/src/test/os-tests/tests/secure_getenv/secure_getenv.c (revision 35e0963c135e57db23f111dfe9dbf1dae97a9fc3)
1 /*
2  * This file and its contents are supplied under the terms of the
3  * Common Development and Distribution License ("CDDL"), version 1.0.
4  * You may only use this file in accordance with the terms of version
5  * 1.0 of the CDDL.
6  *
7  * A full copy of the text of the CDDL should have accompanied this
8  * source.  A copy of the CDDL is also available via the Internet at
9  * http://www.illumos.org/license/CDDL.
10  */
11 
12 /*
13  * Copyright 2025 Oxide Computer Company
14  */
15 
16 /*
17  * Test whether or not secure_getenv(3C) correctly ratchets off certain
18  * privileges. In general, this happens if our uid/euid or gid/egid mismatch or
19  * whether or not there was some kind of privilege escalation. We have a checker
20  * program that we'll use to actually look at this.
21  */
22 
23 #include <stdlib.h>
24 #include <err.h>
25 #include <stdbool.h>
26 #include <sys/sysmacros.h>
27 #include <unistd.h>
28 #include <sys/fork.h>
29 #include <wait.h>
30 #include <pwd.h>
31 #include <limits.h>
32 #include <libgen.h>
33 #include <sys/debug.h>
34 #include <priv.h>
35 
36 static const char *getenv_checker = "checker";
37 static char getenv_path[PATH_MAX];
38 static struct passwd *getenv_nobody;
39 
40 typedef struct {
41 	const char *gf_desc;
42 	bool gf_secure;
43 	void (*gf_forker)(const char *);
44 } getenv_fork_t;
45 
46 /*
47  * Set all of our effective IDs to nobody.
48  */
49 static void
50 getenv_fork_nobody(const char *desc)
51 {
52 	if (setgid(getenv_nobody->pw_gid) != 0) {
53 		errx(EXIT_FAILURE, "TEST FAILED: %s: failed to setgid to "
54 		    "nobody (%u)", desc, getenv_nobody->pw_gid);
55 	}
56 
57 	if (setuid(getenv_nobody->pw_uid) != 0) {
58 		errx(EXIT_FAILURE, "TEST FAILED: %s: failed to setuid to "
59 		    "nobody (%u)", desc, getenv_nobody->pw_uid);
60 	}
61 }
62 
63 static void
64 getenv_fork_seteuid(const char *desc)
65 {
66 	if (seteuid(getenv_nobody->pw_uid) != 0) {
67 		errx(EXIT_FAILURE, "TEST FAILED: %s: failed to seteuid to "
68 		    "nobody (%u)", desc, getenv_nobody->pw_uid);
69 	}
70 }
71 
72 static void
73 getenv_fork_setegid(const char *desc)
74 {
75 	if (setegid(getenv_nobody->pw_gid) != 0) {
76 		errx(EXIT_FAILURE, "TEST FAILED: %s: failed to setegid to "
77 		    "nobody (%u)", desc, getenv_nobody->pw_gid);
78 	}
79 }
80 
81 static void
82 getenv_fork_seteugid(const char *desc)
83 {
84 	getenv_fork_setegid(desc);
85 	getenv_fork_seteuid(desc);
86 }
87 
88 /*
89  * An executing process is considered to have a privilege increase if the
90  * inheritable set is larger than the permitted set. Because this is launched as
91  * a privileged process we generally have a default inheritable set of 'basic',
92  * but our permitted is 'all'. So we first increase our inheritable set and then
93  * drop our permitted set.
94  */
95 static void
96 getenv_fork_privs(const char *desc)
97 {
98 	priv_set_t *priv = priv_allocset();
99 
100 	if (priv == NULL) {
101 		err(EXIT_FAILURE, "TEST FAILED: %s: failed to allocate a "
102 		    "priv_set_t", desc);
103 	}
104 
105 	VERIFY0(priv_addset(priv, PRIV_PROC_CLOCK_HIGHRES));
106 	if (setppriv(PRIV_ON, PRIV_INHERITABLE, priv) != 0) {
107 		err(EXIT_FAILURE, "TEST FAILED: %s: failed to add privs to "
108 		    "the inheritable set", desc);
109 	}
110 
111 	priv_basicset(priv);
112 	if (setppriv(PRIV_SET, PRIV_PERMITTED, priv) != 0) {
113 		err(EXIT_FAILURE, "TEST FAILED: %s: failed to set permitted "
114 		    "set to the basic set", desc);
115 	}
116 
117 	priv_freeset(priv);
118 }
119 
120 static const getenv_fork_t getenv_tests[] = { {
121 	.gf_desc = "change all to nobody",
122 	.gf_secure = false,
123 	.gf_forker = getenv_fork_nobody
124 }, {
125 	.gf_desc = "seteuid to nobody",
126 	.gf_secure = true,
127 	.gf_forker = getenv_fork_seteuid
128 }, {
129 	.gf_desc = "setegid to nobody",
130 	.gf_secure = true,
131 	.gf_forker = getenv_fork_setegid
132 }, {
133 	.gf_desc = "sete[ug]id to nobody",
134 	.gf_secure = true,
135 	.gf_forker = getenv_fork_seteugid
136 }, {
137 	.gf_desc = "privilege increase",
138 	.gf_secure = true,
139 	.gf_forker = getenv_fork_privs
140 } };
141 
142 static bool
143 getenv_fork(const getenv_fork_t *test)
144 {
145 	pid_t child;
146 	siginfo_t cret;
147 
148 	child = forkx(FORK_NOSIGCHLD | FORK_WAITPID);
149 	if (child == 0) {
150 		char *argv[4] = { (char *)getenv_checker, (char *)test->gf_desc,
151 		    NULL, NULL };
152 		if (test->gf_secure) {
153 			argv[2] = "secure";
154 		}
155 		test->gf_forker(test->gf_desc);
156 		(void) execv(getenv_path, argv);
157 		warn("TEST FAILED: %s: failed to exec verifier %s",
158 		    test->gf_desc, getenv_path);
159 		_exit(EXIT_FAILURE);
160 	}
161 
162 	if (waitid(P_PID, child, &cret, WEXITED) < 0) {
163 		err(EXIT_FAILURE, "TEST FAILED: internal test failure waiting "
164 		    "for forked child to report");
165 	}
166 
167 	if (cret.si_code != CLD_EXITED) {
168 		warnx("TEST FAILED: %s: child process did not successfully "
169 		    "exit: found si_code: %d", test->gf_desc, cret.si_code);
170 		return (false);
171 	} else if (cret.si_status != 0) {
172 		warnx("TEST FAILED: %s: child process did not exit with code "
173 		    "0: found %d", test->gf_desc, cret.si_status);
174 		return (false);
175 	}
176 
177 	return (true);
178 }
179 
180 static void
181 getenv_getpath(void)
182 {
183 	ssize_t ret;
184 	char dir[PATH_MAX];
185 
186 	ret = readlink("/proc/self/path/a.out", dir, sizeof (dir));
187 	if (ret < 0) {
188 		err(EXIT_FAILURE, "INTERNAL TEST FAILURE: failed to read our "
189 		    "a.out path from /proc");
190 	} else if (ret == 0) {
191 		errx(EXIT_FAILURE, "INTERNAL TEST FAILURE: reading "
192 		    "/proc/self/path/a.out returned 0 bytes");
193 	} else if (ret == sizeof (dir)) {
194 		errx(EXIT_FAILURE, "INTERNAL TEST FAILURE: Using "
195 		    "/proc/self/path/a.out requires truncation");
196 	}
197 
198 	dir[ret] = '\0';
199 	if (snprintf(getenv_path, sizeof (getenv_path), "%s/%s", dirname(dir),
200 	    getenv_checker) >= sizeof (getenv_path)) {
201 		errx(EXIT_FAILURE, "INTERNAL TEST FAILURE: constructing path "
202 		    "for child process would overflow internal buffer");
203 	}
204 }
205 
206 int
207 main(void)
208 {
209 	int ret = EXIT_SUCCESS;
210 
211 	(void) clearenv();
212 	if (putenv("SECRET=keep it safe") != 0) {
213 		errx(EXIT_FAILURE, "INTERNAL TEST FAILURE: failed to put "
214 		    "environment variable");
215 	}
216 	VERIFY3P(getenv("SECRET"), !=, NULL);
217 
218 	getenv_getpath();
219 	if ((getenv_nobody = getpwnam("nobody")) == NULL) {
220 		err(EXIT_FAILURE, "failed to get passwd entry for nobody");
221 	}
222 
223 	for (size_t i = 0; i < ARRAY_SIZE(getenv_tests); i++) {
224 		if (!getenv_fork(&getenv_tests[i]))
225 			ret = EXIT_FAILURE;
226 	}
227 
228 	if (ret == EXIT_SUCCESS) {
229 		(void) printf("All tests passed successfully\n");
230 	}
231 
232 	return (ret);
233 }
234