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