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