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
debug(const char * fmt,...)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
change_tz(const char * tzn)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
test_tz(const char * expect)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);
ATF_TC_HEAD(tz_default,tc)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 }
ATF_TC_BODY(tz_default,tc)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);
ATF_TC_HEAD(tz_invalid_file,tc)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 }
ATF_TC_BODY(tz_invalid_file,tc)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);
ATF_TC_HEAD(thin_jail,tc)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 }
ATF_TC_BODY(thin_jail,tc)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);
ATF_TC_HEAD(detect_tz_changes,tc)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 }
ATF_TC_BODY(detect_tz_changes,tc)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
test_tz_env(const char * tzval,const char * expect)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
tz_env_common(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);
ATF_TC_HEAD(tz_env,tc)397 ATF_TC_HEAD(tz_env, tc)
398 {
399 atf_tc_set_md_var(tc, "descr", "Test TZ environment variable");
400 }
ATF_TC_BODY(tz_env,tc)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);
ATF_TC_HEAD(tz_invalid_env,tc)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 }
ATF_TC_BODY(tz_invalid_env,tc)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);
ATF_TC_HEAD(setugid,tc)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 }
ATF_TC_BODY(setugid,tc)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);
ATF_TC_HEAD(tz_env_setugid,tc)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 }
ATF_TC_BODY(tz_env_setugid,tc)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
ATF_TP_ADD_TCS(tp)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