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