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