1 /*- 2 * Copyright (c) 2025 Klara, Inc. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7 #include <sys/param.h> 8 #include <sys/conf.h> 9 #include <sys/stat.h> 10 #include <sys/wait.h> 11 12 #include <dlfcn.h> 13 #include <fcntl.h> 14 #include <limits.h> 15 #include <poll.h> 16 #include <stdarg.h> 17 #include <stdbool.h> 18 #include <stdio.h> 19 #include <stdlib.h> 20 #include <time.h> 21 #include <unistd.h> 22 23 #include <atf-c.h> 24 25 static const struct tzcase { 26 const char *tzfn; 27 const char *expect; 28 } tzcases[] = { 29 /* 30 * A handful of time zones and the expected result of 31 * strftime("%z (%Z)", tm) when that time zone is active 32 * and tm represents a date in the summer of 2025. 33 */ 34 { "America/Vancouver", "-0700 (PDT)" }, 35 { "America/New_York", "-0400 (EDT)" }, 36 { "Europe/London", "+0100 (BST)" }, 37 { "Europe/Paris", "+0200 (CEST)" }, 38 { "Asia/Kolkata", "+0530 (IST)" }, 39 { "Asia/Tokyo", "+0900 (JST)" }, 40 { "Australia/Canberra", "+1000 (AEST)" }, 41 { "UTC", "+0000 (UTC)" }, 42 { 0 }, 43 }; 44 45 static const time_t then = 1751328000; /* 2025-07-01 00:00:00 UTC */ 46 47 static bool debugging; 48 49 static void 50 debug(const char *fmt, ...) 51 { 52 va_list ap; 53 54 if (debugging) { 55 va_start(ap, fmt); 56 vfprintf(stderr, fmt, ap); 57 va_end(ap); 58 fputc('\n', stderr); 59 } 60 } 61 62 static void 63 change_tz(const char *tzn) 64 { 65 static const char *zfn = "/usr/share/zoneinfo"; 66 static const char *tfn = "root/etc/.localtime"; 67 static const char *dfn = "root/etc/localtime"; 68 ssize_t clen; 69 int zfd, sfd, dfd; 70 71 ATF_REQUIRE((zfd = open(zfn, O_DIRECTORY | O_SEARCH)) >= 0); 72 ATF_REQUIRE((sfd = openat(zfd, tzn, O_RDONLY)) >= 0); 73 ATF_REQUIRE((dfd = open(tfn, O_CREAT | O_TRUNC | O_WRONLY, 0644)) >= 0); 74 do { 75 clen = copy_file_range(sfd, NULL, dfd, NULL, SSIZE_MAX, 0); 76 ATF_REQUIRE_MSG(clen != -1, "failed to copy %s/%s: %m", 77 zfn, tzn); 78 } while (clen > 0); 79 ATF_CHECK_EQ(0, close(dfd)); 80 ATF_CHECK_EQ(0, close(sfd)); 81 ATF_CHECK_EQ(0, close(zfd)); 82 ATF_REQUIRE_EQ(0, rename(tfn, dfn)); 83 debug("time zone %s installed", tzn); 84 } 85 86 static void 87 test_tz(const char *expect) 88 { 89 char buf[128]; 90 struct tm *tm; 91 size_t len; 92 93 ATF_REQUIRE((tm = localtime(&then)) != NULL); 94 len = strftime(buf, sizeof(buf), "%z (%Z)", tm); 95 ATF_REQUIRE(len > 0); 96 ATF_CHECK_STREQ(expect, buf); 97 } 98 99 ATF_TC(thin_jail); 100 ATF_TC_HEAD(thin_jail, tc) 101 { 102 atf_tc_set_md_var(tc, "descr", "Test typical thin jail scenario"); 103 atf_tc_set_md_var(tc, "require.user", "root"); 104 } 105 ATF_TC_BODY(thin_jail, tc) 106 { 107 const struct tzcase *tzcase = tzcases; 108 109 /* prepare chroot */ 110 ATF_REQUIRE_EQ(0, mkdir("root", 0755)); 111 ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755)); 112 change_tz(tzcase->tzfn); 113 /* enter chroot */ 114 ATF_REQUIRE_EQ(0, chroot("root")); 115 ATF_REQUIRE_EQ(0, chdir("/")); 116 /* check timezone */ 117 unsetenv("TZ"); 118 test_tz(tzcase->expect); 119 } 120 121 #ifdef DETECT_TZ_CHANGES 122 /* 123 * Test time zone change detection. 124 * 125 * The parent creates a chroot containing only /etc/localtime, initially 126 * set to UTC. It then forks a child which enters the chroot, repeatedly 127 * checks the current time zone, and prints it to stdout if it changes 128 * (including once on startup). Meanwhile, the parent waits for output 129 * from the child. Every time it receives a line of text from the child, 130 * it checks that it is as expected, then changes /etc/localtime within 131 * the chroot to the next case in the list. Once it reaches the end of 132 * the list, it closes a pipe to notify the child, which terminates. 133 * 134 * Note that ATF and / or Kyua may have set the timezone before the test 135 * case starts (even unintentionally). Therefore, we start the test only 136 * after we've received and discarded the first report from the child, 137 * which should come almost immediately on startup. 138 */ 139 static const char *tz_change_interval_sym = "__tz_change_interval"; 140 static int *tz_change_interval_p; 141 static const int tz_change_interval = 3; 142 static int tz_change_timeout = 90; 143 144 ATF_TC(detect_tz_changes); 145 ATF_TC_HEAD(detect_tz_changes, tc) 146 { 147 atf_tc_set_md_var(tc, "descr", "Test timezone change detection"); 148 atf_tc_set_md_var(tc, "require.user", "root"); 149 atf_tc_set_md_var(tc, "timeout", "600"); 150 } 151 ATF_TC_BODY(detect_tz_changes, tc) 152 { 153 char obuf[1024] = ""; 154 char ebuf[1024] = ""; 155 struct pollfd fds[3]; 156 int opd[2], epd[2], spd[2]; 157 time_t changed, now; 158 const struct tzcase *tzcase = NULL; 159 struct tm *tm; 160 size_t olen = 0, elen = 0; 161 ssize_t rlen; 162 long curoff = LONG_MIN; 163 pid_t pid; 164 int nfds, status; 165 166 /* speed up the test if possible */ 167 tz_change_interval_p = dlsym(RTLD_SELF, tz_change_interval_sym); 168 if (tz_change_interval_p != NULL && 169 *tz_change_interval_p > tz_change_interval) { 170 debug("reducing detection interval from %d to %d", 171 *tz_change_interval_p, tz_change_interval); 172 *tz_change_interval_p = tz_change_interval; 173 tz_change_timeout = tz_change_interval * 3; 174 } 175 /* prepare chroot */ 176 ATF_REQUIRE_EQ(0, mkdir("root", 0755)); 177 ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755)); 178 change_tz("UTC"); 179 time(&changed); 180 /* output, error, sync pipes */ 181 if (pipe(opd) != 0 || pipe(epd) != 0 || pipe(spd) != 0) 182 atf_tc_fail("failed to pipe"); 183 /* fork child */ 184 if ((pid = fork()) < 0) 185 atf_tc_fail("failed to fork"); 186 if (pid == 0) { 187 /* child */ 188 dup2(opd[1], STDOUT_FILENO); 189 close(opd[0]); 190 close(opd[1]); 191 dup2(epd[1], STDERR_FILENO); 192 close(epd[0]); 193 close(epd[1]); 194 close(spd[0]); 195 unsetenv("TZ"); 196 ATF_REQUIRE_EQ(0, chroot("root")); 197 ATF_REQUIRE_EQ(0, chdir("/")); 198 fds[0].fd = spd[1]; 199 fds[0].events = POLLIN; 200 for (;;) { 201 ATF_REQUIRE(poll(fds, 1, 100) >= 0); 202 if (fds[0].revents & POLLHUP) { 203 /* parent closed sync pipe */ 204 _exit(0); 205 } 206 ATF_REQUIRE((tm = localtime(&then)) != NULL); 207 if (tm->tm_gmtoff == curoff) 208 continue; 209 olen = strftime(obuf, sizeof(obuf), "%z (%Z)", tm); 210 ATF_REQUIRE(olen > 0); 211 fprintf(stdout, "%s\n", obuf); 212 fflush(stdout); 213 curoff = tm->tm_gmtoff; 214 } 215 _exit(2); 216 } 217 /* parent */ 218 close(opd[1]); 219 close(epd[1]); 220 close(spd[1]); 221 /* receive output until child terminates */ 222 fds[0].fd = opd[0]; 223 fds[0].events = POLLIN; 224 fds[1].fd = epd[0]; 225 fds[1].events = POLLIN; 226 fds[2].fd = spd[0]; 227 fds[2].events = POLLIN; 228 nfds = 3; 229 for (;;) { 230 ATF_REQUIRE(poll(fds, 3, 1000) >= 0); 231 time(&now); 232 if (fds[0].revents & POLLIN && olen < sizeof(obuf)) { 233 rlen = read(opd[0], obuf + olen, sizeof(obuf) - olen); 234 ATF_REQUIRE(rlen >= 0); 235 olen += rlen; 236 } 237 if (olen > 0) { 238 ATF_REQUIRE_EQ('\n', obuf[olen - 1]); 239 obuf[--olen] = '\0'; 240 /* tzcase will be NULL at first */ 241 if (tzcase != NULL) { 242 debug("%s", obuf); 243 ATF_REQUIRE_STREQ(tzcase->expect, obuf); 244 debug("change to %s detected after %d s", 245 tzcase->tzfn, (int)(now - changed)); 246 if (tz_change_interval_p != NULL) { 247 ATF_CHECK((int)(now - changed) >= 248 *tz_change_interval_p - 1); 249 ATF_CHECK((int)(now - changed) <= 250 *tz_change_interval_p + 1); 251 } 252 } 253 olen = 0; 254 /* first / next test case */ 255 if (tzcase == NULL) 256 tzcase = tzcases; 257 else 258 tzcase++; 259 if (tzcase->tzfn == NULL) { 260 /* test is over */ 261 break; 262 } 263 change_tz(tzcase->tzfn); 264 changed = now; 265 } 266 if (fds[1].revents & POLLIN && elen < sizeof(ebuf)) { 267 rlen = read(epd[0], ebuf + elen, sizeof(ebuf) - elen); 268 ATF_REQUIRE(rlen >= 0); 269 elen += rlen; 270 } 271 if (elen > 0) { 272 ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr)); 273 elen = 0; 274 } 275 if (nfds > 2 && fds[2].revents & POLLHUP) { 276 /* child closed sync pipe */ 277 break; 278 } 279 /* 280 * The timeout for this test case is set to 10 minutes, 281 * because it can take that long to run with the default 282 * 61-second interval. However, each individual tzcase 283 * entry should not take much longer than the detection 284 * interval to test, so we can detect a problem long 285 * before Kyua terminates us. 286 */ 287 if ((now - changed) > tz_change_timeout) { 288 close(spd[0]); 289 if (tz_change_interval_p == NULL && 290 tzcase == tzcases) { 291 /* 292 * The most likely explanation in this 293 * case is that libc was built without 294 * time zone change detection. 295 */ 296 atf_tc_skip("time zone change detection " 297 "does not appear to be enabled"); 298 } 299 atf_tc_fail("timed out waiting for change to %s " 300 "to be detected", tzcase->tzfn); 301 } 302 } 303 close(opd[0]); 304 close(epd[0]); 305 close(spd[0]); /* this will wake up and terminate the child */ 306 if (olen > 0) 307 ATF_REQUIRE_EQ(olen, fwrite(obuf, 1, olen, stdout)); 308 if (elen > 0) 309 ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr)); 310 ATF_REQUIRE_EQ(pid, waitpid(pid, &status, 0)); 311 ATF_REQUIRE(WIFEXITED(status)); 312 ATF_REQUIRE_EQ(0, WEXITSTATUS(status)); 313 } 314 #endif /* DETECT_TZ_CHANGES */ 315 316 static void 317 test_tz_env(const char *tzval, const char *expect) 318 { 319 setenv("TZ", tzval, 1); 320 test_tz(expect); 321 } 322 323 ATF_TC(tz_env); 324 ATF_TC_HEAD(tz_env, tc) 325 { 326 atf_tc_set_md_var(tc, "descr", "Test TZ environment variable"); 327 } 328 ATF_TC_BODY(tz_env, tc) 329 { 330 const struct tzcase *tzcase; 331 332 for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++) 333 test_tz_env(tzcase->tzfn, tzcase->expect); 334 } 335 336 ATF_TC(setugid); 337 ATF_TC_HEAD(setugid, tc) 338 { 339 atf_tc_set_md_var(tc, "descr", "Test setugid process"); 340 atf_tc_set_md_var(tc, "require.user", "root"); 341 } 342 ATF_TC_BODY(setugid, tc) 343 { 344 const struct tzcase *tzcase = tzcases; 345 346 /* prepare chroot */ 347 ATF_REQUIRE_EQ(0, mkdir("root", 0755)); 348 ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755)); 349 change_tz(tzcase->tzfn); 350 /* enter chroot */ 351 ATF_REQUIRE_EQ(0, chroot("root")); 352 ATF_REQUIRE_EQ(0, chdir("/")); 353 /* become setugid */ 354 ATF_REQUIRE_EQ(0, seteuid(UID_NOBODY)); 355 ATF_REQUIRE(issetugid()); 356 /* check timezone */ 357 unsetenv("TZ"); 358 test_tz(tzcases->expect); 359 } 360 361 ATF_TC(tz_env_setugid); 362 ATF_TC_HEAD(tz_env_setugid, tc) 363 { 364 atf_tc_set_md_var(tc, "descr", "Test TZ environment variable " 365 "in setugid process"); 366 atf_tc_set_md_var(tc, "require.user", "root"); 367 } 368 ATF_TC_BODY(tz_env_setugid, tc) 369 { 370 const struct tzcase *tzcase = tzcases; 371 372 ATF_REQUIRE_EQ(0, seteuid(UID_NOBODY)); 373 ATF_REQUIRE(issetugid()); 374 for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++) 375 test_tz_env(tzcase->tzfn, tzcase->expect); 376 } 377 378 ATF_TP_ADD_TCS(tp) 379 { 380 debugging = !getenv("__RUNNING_INSIDE_ATF_RUN") && 381 isatty(STDERR_FILENO); 382 ATF_TP_ADD_TC(tp, thin_jail); 383 #ifdef DETECT_TZ_CHANGES 384 ATF_TP_ADD_TC(tp, detect_tz_changes); 385 #endif /* DETECT_TZ_CHANGES */ 386 ATF_TP_ADD_TC(tp, tz_env); 387 ATF_TP_ADD_TC(tp, setugid); 388 ATF_TP_ADD_TC(tp, tz_env_setugid); 389 return (atf_no_error()); 390 } 391