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