xref: /freebsd/lib/libc/tests/stdtime/detect_tz_changes_test.c (revision cf76bfbb235116702ea1689bf984f200a72d2790)
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