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)) >= 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 ATF_TC(thin_jail); 87 ATF_TC_HEAD(thin_jail, tc) 88 { 89 atf_tc_set_md_var(tc, "descr", "Test typical thin jail scenario"); 90 atf_tc_set_md_var(tc, "require.user", "root"); 91 } 92 ATF_TC_BODY(thin_jail, tc) 93 { 94 const struct tzcase *tzcase = tzcases; 95 char buf[128]; 96 struct tm *tm; 97 size_t len; 98 99 /* prepare chroot */ 100 ATF_REQUIRE_EQ(0, mkdir("root", 0755)); 101 ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755)); 102 change_tz(tzcase->tzfn); 103 /* enter chroot */ 104 ATF_REQUIRE_EQ(0, chroot("root")); 105 ATF_REQUIRE_EQ(0, chdir("/")); 106 /* check timezone */ 107 unsetenv("TZ"); 108 ATF_REQUIRE((tm = localtime(&then)) != NULL); 109 len = strftime(buf, sizeof(buf), "%z (%Z)", tm); 110 ATF_REQUIRE(len > 0); 111 ATF_CHECK_STREQ(tzcase->expect, buf); 112 } 113 114 #ifdef DETECT_TZ_CHANGES 115 /* 116 * Test time zone change detection. 117 * 118 * The parent creates a chroot containing only /etc/localtime, initially 119 * set to UTC. It then forks a child which enters the chroot, repeatedly 120 * checks the current time zone, and prints it to stdout if it changes 121 * (including once on startup). Meanwhile, the parent waits for output 122 * from the child. Every time it receives a line of text from the child, 123 * it checks that it is as expected, then changes /etc/localtime within 124 * the chroot to the next case in the list. Once it reaches the end of 125 * the list, it closes a pipe to notify the child, which terminates. 126 * 127 * Note that ATF and / or Kyua may have set the timezone before the test 128 * case starts (even unintentionally). Therefore, we start the test only 129 * after we've received and discarded the first report from the child, 130 * which should come almost immediately on startup. 131 */ 132 static const char *tz_change_interval_sym = "__tz_change_interval"; 133 static int *tz_change_interval_p; 134 static const int tz_change_interval = 3; 135 static int tz_change_timeout = 90; 136 137 ATF_TC(detect_tz_changes); 138 ATF_TC_HEAD(detect_tz_changes, tc) 139 { 140 atf_tc_set_md_var(tc, "descr", "Test timezone change detection"); 141 atf_tc_set_md_var(tc, "require.user", "root"); 142 atf_tc_set_md_var(tc, "timeout", "600"); 143 } 144 ATF_TC_BODY(detect_tz_changes, tc) 145 { 146 char obuf[1024] = ""; 147 char ebuf[1024] = ""; 148 struct pollfd fds[3]; 149 int opd[2], epd[2], spd[2]; 150 time_t changed, now; 151 const struct tzcase *tzcase = NULL; 152 struct tm *tm; 153 size_t olen = 0, elen = 0; 154 ssize_t rlen; 155 long curoff = LONG_MIN; 156 pid_t pid; 157 int nfds, status; 158 159 /* speed up the test if possible */ 160 tz_change_interval_p = dlsym(RTLD_SELF, tz_change_interval_sym); 161 if (tz_change_interval_p != NULL && 162 *tz_change_interval_p > tz_change_interval) { 163 debug("reducing detection interval from %d to %d", 164 *tz_change_interval_p, tz_change_interval); 165 *tz_change_interval_p = tz_change_interval; 166 tz_change_timeout = tz_change_interval * 3; 167 } 168 /* prepare chroot */ 169 ATF_REQUIRE_EQ(0, mkdir("root", 0755)); 170 ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755)); 171 change_tz("UTC"); 172 time(&changed); 173 /* output, error, sync pipes */ 174 if (pipe(opd) != 0 || pipe(epd) != 0 || pipe(spd) != 0) 175 atf_tc_fail("failed to pipe"); 176 /* fork child */ 177 if ((pid = fork()) < 0) 178 atf_tc_fail("failed to fork"); 179 if (pid == 0) { 180 /* child */ 181 dup2(opd[1], STDOUT_FILENO); 182 close(opd[0]); 183 close(opd[1]); 184 dup2(epd[1], STDERR_FILENO); 185 close(epd[0]); 186 close(epd[1]); 187 close(spd[0]); 188 unsetenv("TZ"); 189 ATF_REQUIRE_EQ(0, chroot("root")); 190 ATF_REQUIRE_EQ(0, chdir("/")); 191 fds[0].fd = spd[1]; 192 fds[0].events = POLLIN; 193 for (;;) { 194 ATF_REQUIRE(poll(fds, 1, 100) >= 0); 195 if (fds[0].revents & POLLHUP) { 196 /* parent closed sync pipe */ 197 _exit(0); 198 } 199 ATF_REQUIRE((tm = localtime(&then)) != NULL); 200 if (tm->tm_gmtoff == curoff) 201 continue; 202 olen = strftime(obuf, sizeof(obuf), "%z (%Z)", tm); 203 ATF_REQUIRE(olen > 0); 204 fprintf(stdout, "%s\n", obuf); 205 fflush(stdout); 206 curoff = tm->tm_gmtoff; 207 } 208 _exit(2); 209 } 210 /* parent */ 211 close(opd[1]); 212 close(epd[1]); 213 close(spd[1]); 214 /* receive output until child terminates */ 215 fds[0].fd = opd[0]; 216 fds[0].events = POLLIN; 217 fds[1].fd = epd[0]; 218 fds[1].events = POLLIN; 219 fds[2].fd = spd[0]; 220 fds[2].events = POLLIN; 221 nfds = 3; 222 for (;;) { 223 ATF_REQUIRE(poll(fds, 3, 1000) >= 0); 224 time(&now); 225 if (fds[0].revents & POLLIN && olen < sizeof(obuf)) { 226 rlen = read(opd[0], obuf + olen, sizeof(obuf) - olen); 227 ATF_REQUIRE(rlen >= 0); 228 olen += rlen; 229 } 230 if (olen > 0) { 231 ATF_REQUIRE_EQ('\n', obuf[olen - 1]); 232 obuf[--olen] = '\0'; 233 /* tzcase will be NULL at first */ 234 if (tzcase != NULL) { 235 debug("%s", obuf); 236 ATF_REQUIRE_STREQ(tzcase->expect, obuf); 237 debug("change to %s detected after %d s", 238 tzcase->tzfn, (int)(now - changed)); 239 if (tz_change_interval_p != NULL) { 240 ATF_CHECK((int)(now - changed) >= 241 *tz_change_interval_p - 1); 242 ATF_CHECK((int)(now - changed) <= 243 *tz_change_interval_p + 1); 244 } 245 } 246 olen = 0; 247 /* first / next test case */ 248 if (tzcase == NULL) 249 tzcase = tzcases; 250 else 251 tzcase++; 252 if (tzcase->tzfn == NULL) { 253 /* test is over */ 254 break; 255 } 256 change_tz(tzcase->tzfn); 257 changed = now; 258 } 259 if (fds[1].revents & POLLIN && elen < sizeof(ebuf)) { 260 rlen = read(epd[0], ebuf + elen, sizeof(ebuf) - elen); 261 ATF_REQUIRE(rlen >= 0); 262 elen += rlen; 263 } 264 if (elen > 0) { 265 ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr)); 266 elen = 0; 267 } 268 if (nfds > 2 && fds[2].revents & POLLHUP) { 269 /* child closed sync pipe */ 270 break; 271 } 272 /* 273 * The timeout for this test case is set to 10 minutes, 274 * because it can take that long to run with the default 275 * 61-second interval. However, each individual tzcase 276 * entry should not take much longer than the detection 277 * interval to test, so we can detect a problem long 278 * before Kyua terminates us. 279 */ 280 if ((now - changed) > tz_change_timeout) { 281 close(spd[0]); 282 if (tz_change_interval_p == NULL && 283 tzcase == tzcases) { 284 /* 285 * The most likely explanation in this 286 * case is that libc was built without 287 * time zone change detection. 288 */ 289 atf_tc_skip("time zone change detection " 290 "does not appear to be enabled"); 291 } 292 atf_tc_fail("timed out waiting for change to %s " 293 "to be detected", tzcase->tzfn); 294 } 295 } 296 close(opd[0]); 297 close(epd[0]); 298 close(spd[0]); /* this will wake up and terminate the child */ 299 if (olen > 0) 300 ATF_REQUIRE_EQ(olen, fwrite(obuf, 1, olen, stdout)); 301 if (elen > 0) 302 ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr)); 303 ATF_REQUIRE_EQ(pid, waitpid(pid, &status, 0)); 304 ATF_REQUIRE(WIFEXITED(status)); 305 ATF_REQUIRE_EQ(0, WEXITSTATUS(status)); 306 } 307 #endif /* DETECT_TZ_CHANGES */ 308 309 static void 310 test_tz_env(const char *tzval, const char *expect) 311 { 312 char buf[128]; 313 struct tm *tm; 314 size_t len; 315 316 setenv("TZ", tzval, 1); 317 ATF_REQUIRE((tm = localtime(&then)) != NULL); 318 len = strftime(buf, sizeof(buf), "%z (%Z)", tm); 319 ATF_REQUIRE(len > 0); 320 ATF_CHECK_STREQ(expect, buf); 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(tz_env_setugid); 337 ATF_TC_HEAD(tz_env_setugid, tc) 338 { 339 atf_tc_set_md_var(tc, "descr", "Test TZ environment variable " 340 "in setugid process"); 341 atf_tc_set_md_var(tc, "require.user", "root"); 342 } 343 ATF_TC_BODY(tz_env_setugid, tc) 344 { 345 const struct tzcase *tzcase; 346 347 ATF_REQUIRE_EQ(0, seteuid(UID_NOBODY)); 348 ATF_REQUIRE(issetugid()); 349 for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++) 350 test_tz_env(tzcase->tzfn, tzcase->expect); 351 } 352 353 ATF_TP_ADD_TCS(tp) 354 { 355 debugging = !getenv("__RUNNING_INSIDE_ATF_RUN") && 356 isatty(STDERR_FILENO); 357 ATF_TP_ADD_TC(tp, thin_jail); 358 #ifdef DETECT_TZ_CHANGES 359 ATF_TP_ADD_TC(tp, detect_tz_changes); 360 #endif /* DETECT_TZ_CHANGES */ 361 ATF_TP_ADD_TC(tp, tz_env); 362 ATF_TP_ADD_TC(tp, tz_env_setugid); 363 return (atf_no_error()); 364 } 365