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