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