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