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