xref: /freebsd/lib/libc/tests/stdtime/detect_tz_changes_test.c (revision 8a847947153e60d812852e5c02de93997b2d3038)
1 /*-
2  * Copyright (c) 2025 Klara, Inc.
3  *
4  * SPDX-License-Identifier: BSD-2-Clause
5  */
6 
7 #include <sys/stat.h>
8 #include <sys/wait.h>
9 
10 #include <dlfcn.h>
11 #include <fcntl.h>
12 #include <limits.h>
13 #include <poll.h>
14 #include <stdarg.h>
15 #include <stdbool.h>
16 #include <stdio.h>
17 #include <stdlib.h>
18 #include <time.h>
19 #include <unistd.h>
20 
21 #include <atf-c.h>
22 
23 static const struct tzcase {
24 	const char *tzfn;
25 	const char *expect;
26 } tzcases[] = {
27 	/*
28 	 * A handful of time zones and the expected result of
29 	 * strftime("%z (%Z)", tm) when that time zone is active
30 	 * and tm represents a date in the summer of 2025.
31 	 */
32 	{ "America/Vancouver",	"-0700 (PDT)"	},
33 	{ "America/New_York",	"-0400 (EDT)"	},
34 	{ "Europe/London",	"+0100 (BST)"	},
35 	{ "Europe/Paris",	"+0200 (CEST)"	},
36 	{ "Asia/Kolkata",	"+0530 (IST)"	},
37 	{ "Asia/Tokyo",		"+0900 (JST)"	},
38 	{ "Australia/Canberra",	"+1000 (AEST)"	},
39 	{ "UTC",		"+0000 (UTC)"	},
40 	{ 0 },
41 };
42 
43 static const time_t then = 1751328000; /* 2025-07-01 00:00:00 UTC */
44 static const char *tz_change_interval_sym = "__tz_change_interval";
45 static int *tz_change_interval_p;
46 static const int tz_change_interval = 3;
47 static int tz_change_timeout = 90;
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 = "/usr/share/zoneinfo";
68 	static const char *tfn = "root/etc/.localtime";
69 	static const char *dfn = "root/etc/localtime";
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)) >= 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 /*
89  * Test time zone change detection.
90  *
91  * The parent creates a chroot containing only /etc/localtime, initially
92  * set to UTC.  It then forks a child which enters the chroot, repeatedly
93  * checks the current time zone, and prints it to stdout if it changes
94  * (including once on startup).  Meanwhile, the parent waits for output
95  * from the child.  Every time it receives a line of text from the child,
96  * it checks that it is as expected, then changes /etc/localtime within
97  * the chroot to the next case in the list.  Once it reaches the end of
98  * the list, it closes a pipe to notify the child, which terminates.
99  *
100  * Note that ATF and / or Kyua may have set the timezone before the test
101  * case starts (even unintentionally).  Therefore, we start the test only
102  * after we've received and discarded the first report from the child,
103  * which should come almost immediately on startup.
104  */
105 ATF_TC(detect_tz_changes);
106 ATF_TC_HEAD(detect_tz_changes, tc)
107 {
108 	atf_tc_set_md_var(tc, "descr", "Test timezone change detection");
109 	atf_tc_set_md_var(tc, "require.user", "root");
110 	atf_tc_set_md_var(tc, "timeout", "600");
111 }
112 ATF_TC_BODY(detect_tz_changes, tc)
113 {
114 	char obuf[1024] = "";
115 	char ebuf[1024] = "";
116 	struct pollfd fds[3];
117 	int opd[2], epd[2], spd[2];
118 	time_t changed, now;
119 	const struct tzcase *tzcase = NULL;
120 	struct tm *tm;
121 	size_t olen = 0, elen = 0;
122 	ssize_t rlen;
123 	long curoff = LONG_MIN;
124 	pid_t pid;
125 	int nfds, status;
126 
127 	/* speed up the test if possible */
128 	tz_change_interval_p = dlsym(RTLD_SELF, tz_change_interval_sym);
129 	if (tz_change_interval_p != NULL &&
130 	    *tz_change_interval_p > tz_change_interval) {
131 		debug("reducing detection interval from %d to %d",
132 		    *tz_change_interval_p, tz_change_interval);
133 		*tz_change_interval_p = tz_change_interval;
134 		tz_change_timeout = tz_change_interval * 3;
135 	}
136 	/* prepare chroot */
137 	ATF_REQUIRE_EQ(0, mkdir("root", 0755));
138 	ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755));
139 	change_tz("UTC");
140 	time(&changed);
141 	/* output, error, sync pipes */
142 	if (pipe(opd) != 0 || pipe(epd) != 0 || pipe(spd) != 0)
143 		atf_tc_fail("failed to pipe");
144 	/* fork child */
145 	if ((pid = fork()) < 0)
146 		atf_tc_fail("failed to fork");
147 	if (pid == 0) {
148 		/* child */
149 		dup2(opd[1], STDOUT_FILENO);
150 		close(opd[0]);
151 		close(opd[1]);
152 		dup2(epd[1], STDERR_FILENO);
153 		close(epd[0]);
154 		close(epd[1]);
155 		close(spd[0]);
156 		unsetenv("TZ");
157 		ATF_REQUIRE_EQ(0, chroot("root"));
158 		ATF_REQUIRE_EQ(0, chdir("/"));
159 		fds[0].fd = spd[1];
160 		fds[0].events = POLLIN;
161 		for (;;) {
162 			ATF_REQUIRE(poll(fds, 1, 100) >= 0);
163 			if (fds[0].revents & POLLHUP) {
164 				/* parent closed sync pipe */
165 				_exit(0);
166 			}
167 			ATF_REQUIRE((tm = localtime(&then)) != NULL);
168 			if (tm->tm_gmtoff == curoff)
169 				continue;
170 			olen = strftime(obuf, sizeof(obuf), "%z (%Z)", tm);
171 			ATF_REQUIRE(olen > 0);
172 			fprintf(stdout, "%s\n", obuf);
173 			fflush(stdout);
174 			curoff = tm->tm_gmtoff;
175 		}
176 		_exit(2);
177 	}
178 	/* parent */
179 	close(opd[1]);
180 	close(epd[1]);
181 	close(spd[1]);
182 	/* receive output until child terminates */
183 	fds[0].fd = opd[0];
184 	fds[0].events = POLLIN;
185 	fds[1].fd = epd[0];
186 	fds[1].events = POLLIN;
187 	fds[2].fd = spd[0];
188 	fds[2].events = POLLIN;
189 	nfds = 3;
190 	for (;;) {
191 		ATF_REQUIRE(poll(fds, 3, 1000) >= 0);
192 		time(&now);
193 		if (fds[0].revents & POLLIN && olen < sizeof(obuf)) {
194 			rlen = read(opd[0], obuf + olen, sizeof(obuf) - olen);
195 			ATF_REQUIRE(rlen >= 0);
196 			olen += rlen;
197 		}
198 		if (olen > 0) {
199 			ATF_REQUIRE_EQ('\n', obuf[olen - 1]);
200 			obuf[--olen] = '\0';
201 			/* tzcase will be NULL at first */
202 			if (tzcase != NULL) {
203 				debug("%s", obuf);
204 				ATF_REQUIRE_STREQ(tzcase->expect, obuf);
205 				debug("change to %s detected after %d s",
206 				    tzcase->tzfn, (int)(now - changed));
207 				if (tz_change_interval_p != NULL) {
208 					ATF_CHECK((int)(now - changed) >=
209 					    *tz_change_interval_p - 1);
210 					ATF_CHECK((int)(now - changed) <=
211 					    *tz_change_interval_p + 1);
212 				}
213 			}
214 			olen = 0;
215 			/* first / next test case */
216 			if (tzcase == NULL)
217 				tzcase = tzcases;
218 			else
219 				tzcase++;
220 			if (tzcase->tzfn == NULL) {
221 				/* test is over */
222 				break;
223 			}
224 			change_tz(tzcase->tzfn);
225 			changed = now;
226 		}
227 		if (fds[1].revents & POLLIN && elen < sizeof(ebuf)) {
228 			rlen = read(epd[0], ebuf + elen, sizeof(ebuf) - elen);
229 			ATF_REQUIRE(rlen >= 0);
230 			elen += rlen;
231 		}
232 		if (elen > 0) {
233 			ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr));
234 			elen = 0;
235 		}
236 		if (nfds > 2 && fds[2].revents & POLLHUP) {
237 			/* child closed sync pipe */
238 			break;
239 		}
240 		/*
241 		 * The timeout for this test case is set to 10 minutes,
242 		 * because it can take that long to run with the default
243 		 * 61-second interval.  However, each individual tzcase
244 		 * entry should not take much longer than the detection
245 		 * interval to test, so we can detect a problem long
246 		 * before Kyua terminates us.
247 		 */
248 		if ((now - changed) > tz_change_timeout) {
249 			close(spd[0]);
250 			if (tz_change_interval_p == NULL &&
251 			    tzcase == tzcases) {
252 				/*
253 				 * The most likely explanation in this
254 				 * case is that libc was built without
255 				 * time zone change detection.
256 				 */
257 				atf_tc_skip("time zone change detection "
258 				    "does not appear to be enabled");
259 			}
260 			atf_tc_fail("timed out waiting for change to %s "
261 			    "to be detected", tzcase->tzfn);
262 		}
263 	}
264 	close(opd[0]);
265 	close(epd[0]);
266 	close(spd[0]); /* this will wake up and terminate the child */
267 	if (olen > 0)
268 		ATF_REQUIRE_EQ(olen, fwrite(obuf, 1, olen, stdout));
269 	if (elen > 0)
270 		ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr));
271 	ATF_REQUIRE_EQ(pid, waitpid(pid, &status, 0));
272 	ATF_REQUIRE(WIFEXITED(status));
273 	ATF_REQUIRE_EQ(0, WEXITSTATUS(status));
274 }
275 
276 ATF_TC(tz_env);
277 ATF_TC_HEAD(tz_env, tc)
278 {
279 	atf_tc_set_md_var(tc, "descr", "Test TZ environment variable");
280 }
281 ATF_TC_BODY(tz_env, tc)
282 {
283 	char buf[128];
284 	const struct tzcase *tzcase = NULL;
285 	struct tm *tm;
286 	size_t len;
287 
288 	for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++) {
289 		setenv("TZ", tzcase->tzfn, 1);
290 		ATF_REQUIRE((tm = localtime(&then)) != NULL);
291 		len = strftime(buf, sizeof(buf), "%z (%Z)", tm);
292 		ATF_REQUIRE(len > 0);
293 		ATF_REQUIRE_STREQ(tzcase->expect, buf);
294 	}
295 }
296 
297 ATF_TP_ADD_TCS(tp)
298 {
299 	debugging = !getenv("__RUNNING_INSIDE_ATF_RUN") &&
300 	    isatty(STDERR_FILENO);
301 	ATF_TP_ADD_TC(tp, detect_tz_changes);
302 	ATF_TP_ADD_TC(tp, tz_env);
303 	return (atf_no_error());
304 }
305