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