Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/lib/libc/tests/stdtime/detect_tz_changes_test.c
39530 views
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 "tzdir.h"
24
25
#include <atf-c.h>
26
27
struct tzcase {
28
const char *tzfn;
29
const char *expect;
30
};
31
32
static const struct tzcase tzcases[] = {
33
/*
34
* A handful of time zones and the expected result of
35
* strftime("%z (%Z)", tm) when that time zone is active
36
* and tm represents a date in the summer of 2025.
37
*/
38
{ "America/Vancouver", "-0700 (PDT)" },
39
{ "America/New_York", "-0400 (EDT)" },
40
{ "Europe/London", "+0100 (BST)" },
41
{ "Europe/Paris", "+0200 (CEST)" },
42
{ "Asia/Kolkata", "+0530 (IST)" },
43
{ "Asia/Tokyo", "+0900 (JST)" },
44
{ "Australia/Canberra", "+1000 (AEST)" },
45
{ "UTC", "+0000 (UTC)" },
46
{ 0 },
47
};
48
static const struct tzcase utc = { "UTC", "+0000 (UTC)" };
49
static const struct tzcase invalid = { "invalid", "+0000 (-00)" };
50
static const time_t then = 1751328000; /* 2025-07-01 00:00:00 UTC */
51
52
static bool debugging;
53
54
static void
55
debug(const char *fmt, ...)
56
{
57
va_list ap;
58
59
if (debugging) {
60
va_start(ap, fmt);
61
vfprintf(stderr, fmt, ap);
62
va_end(ap);
63
fputc('\n', stderr);
64
}
65
}
66
67
static void
68
change_tz(const char *tzn)
69
{
70
static const char *zfn = TZDIR;
71
static const char *tfn = "root" TZDEFAULT ".tmp";
72
static const char *dfn = "root" TZDEFAULT;
73
ssize_t clen;
74
int zfd, sfd, dfd;
75
76
ATF_REQUIRE((zfd = open(zfn, O_DIRECTORY | O_SEARCH)) >= 0);
77
ATF_REQUIRE((sfd = openat(zfd, tzn, O_RDONLY)) >= 0);
78
ATF_REQUIRE((dfd = open(tfn, O_CREAT | O_TRUNC | O_WRONLY, 0644)) >= 0);
79
do {
80
clen = copy_file_range(sfd, NULL, dfd, NULL, SSIZE_MAX, 0);
81
ATF_REQUIRE_MSG(clen != -1, "failed to copy %s/%s: %m",
82
zfn, tzn);
83
} while (clen > 0);
84
ATF_CHECK_EQ(0, close(dfd));
85
ATF_CHECK_EQ(0, close(sfd));
86
ATF_CHECK_EQ(0, close(zfd));
87
ATF_REQUIRE_EQ(0, rename(tfn, dfn));
88
debug("time zone %s installed", tzn);
89
}
90
91
static void
92
test_tz(const char *expect)
93
{
94
char buf[128];
95
struct tm *tm;
96
size_t len;
97
98
ATF_REQUIRE((tm = localtime(&then)) != NULL);
99
len = strftime(buf, sizeof(buf), "%z (%Z)", tm);
100
ATF_REQUIRE(len > 0);
101
ATF_CHECK_STREQ(expect, buf);
102
}
103
104
ATF_TC(tz_default);
105
ATF_TC_HEAD(tz_default, tc)
106
{
107
atf_tc_set_md_var(tc, "descr", "Test default zone");
108
atf_tc_set_md_var(tc, "require.user", "root");
109
}
110
ATF_TC_BODY(tz_default, tc)
111
{
112
/* prepare chroot with no /etc/localtime */
113
ATF_REQUIRE_EQ(0, mkdir("root", 0755));
114
ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755));
115
/* enter chroot */
116
ATF_REQUIRE_EQ(0, chroot("root"));
117
ATF_REQUIRE_EQ(0, chdir("/"));
118
/* check timezone */
119
unsetenv("TZ");
120
test_tz("+0000 (UTC)");
121
}
122
123
ATF_TC(tz_invalid_file);
124
ATF_TC_HEAD(tz_invalid_file, tc)
125
{
126
atf_tc_set_md_var(tc, "descr", "Test invalid zone file");
127
atf_tc_set_md_var(tc, "require.user", "root");
128
}
129
ATF_TC_BODY(tz_invalid_file, tc)
130
{
131
static const char *dfn = "root/etc/localtime";
132
int fd;
133
134
/* prepare chroot with bogus /etc/localtime */
135
ATF_REQUIRE_EQ(0, mkdir("root", 0755));
136
ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755));
137
ATF_REQUIRE((fd = open(dfn, O_RDWR | O_CREAT, 0644)) >= 0);
138
ATF_REQUIRE_EQ(8, write(fd, "invalid\n", 8));
139
ATF_REQUIRE_EQ(0, close(fd));
140
/* enter chroot */
141
ATF_REQUIRE_EQ(0, chroot("root"));
142
ATF_REQUIRE_EQ(0, chdir("/"));
143
/* check timezone */
144
unsetenv("TZ");
145
test_tz(invalid.expect);
146
}
147
148
ATF_TC(thin_jail);
149
ATF_TC_HEAD(thin_jail, tc)
150
{
151
atf_tc_set_md_var(tc, "descr", "Test typical thin jail scenario");
152
atf_tc_set_md_var(tc, "require.user", "root");
153
}
154
ATF_TC_BODY(thin_jail, tc)
155
{
156
const struct tzcase *tzcase = tzcases;
157
158
/* prepare chroot */
159
ATF_REQUIRE_EQ(0, mkdir("root", 0755));
160
ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755));
161
change_tz(tzcase->tzfn);
162
/* enter chroot */
163
ATF_REQUIRE_EQ(0, chroot("root"));
164
ATF_REQUIRE_EQ(0, chdir("/"));
165
/* check timezone */
166
unsetenv("TZ");
167
test_tz(tzcase->expect);
168
}
169
170
#ifdef DETECT_TZ_CHANGES
171
/*
172
* Test time zone change detection.
173
*
174
* The parent creates a chroot containing only /etc/localtime, initially
175
* set to UTC. It then forks a child which enters the chroot, repeatedly
176
* checks the current time zone, and prints it to stdout if it changes
177
* (including once on startup). Meanwhile, the parent waits for output
178
* from the child. Every time it receives a line of text from the child,
179
* it checks that it is as expected, then changes /etc/localtime within
180
* the chroot to the next case in the list. Once it reaches the end of
181
* the list, it closes a pipe to notify the child, which terminates.
182
*
183
* Note that ATF and / or Kyua may have set the timezone before the test
184
* case starts (even unintentionally). Therefore, we start the test only
185
* after we've received and discarded the first report from the child,
186
* which should come almost immediately on startup.
187
*/
188
static const char *tz_change_interval_sym = "__tz_change_interval";
189
static int *tz_change_interval_p;
190
static const int tz_change_interval = 3;
191
static int tz_change_timeout = 90;
192
193
ATF_TC(detect_tz_changes);
194
ATF_TC_HEAD(detect_tz_changes, tc)
195
{
196
atf_tc_set_md_var(tc, "descr", "Test timezone change detection");
197
atf_tc_set_md_var(tc, "require.user", "root");
198
atf_tc_set_md_var(tc, "timeout", "600");
199
}
200
ATF_TC_BODY(detect_tz_changes, tc)
201
{
202
char obuf[1024] = "";
203
char ebuf[1024] = "";
204
struct pollfd fds[3];
205
int opd[2], epd[2], spd[2];
206
time_t changed, now;
207
const struct tzcase *tzcase = NULL;
208
struct tm *tm;
209
size_t olen = 0, elen = 0;
210
ssize_t rlen;
211
long curoff = LONG_MIN;
212
pid_t pid;
213
int nfds, status;
214
215
/* speed up the test if possible */
216
tz_change_interval_p = dlsym(RTLD_SELF, tz_change_interval_sym);
217
if (tz_change_interval_p != NULL &&
218
*tz_change_interval_p > tz_change_interval) {
219
debug("reducing detection interval from %d to %d",
220
*tz_change_interval_p, tz_change_interval);
221
*tz_change_interval_p = tz_change_interval;
222
tz_change_timeout = tz_change_interval * 3;
223
}
224
/* prepare chroot */
225
ATF_REQUIRE_EQ(0, mkdir("root", 0755));
226
ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755));
227
change_tz("UTC");
228
time(&changed);
229
/* output, error, sync pipes */
230
if (pipe(opd) != 0 || pipe(epd) != 0 || pipe(spd) != 0)
231
atf_tc_fail("failed to pipe");
232
/* fork child */
233
if ((pid = fork()) < 0)
234
atf_tc_fail("failed to fork");
235
if (pid == 0) {
236
/* child */
237
dup2(opd[1], STDOUT_FILENO);
238
close(opd[0]);
239
close(opd[1]);
240
dup2(epd[1], STDERR_FILENO);
241
close(epd[0]);
242
close(epd[1]);
243
close(spd[0]);
244
unsetenv("TZ");
245
ATF_REQUIRE_EQ(0, chroot("root"));
246
ATF_REQUIRE_EQ(0, chdir("/"));
247
fds[0].fd = spd[1];
248
fds[0].events = POLLIN;
249
for (;;) {
250
ATF_REQUIRE(poll(fds, 1, 100) >= 0);
251
if (fds[0].revents & POLLHUP) {
252
/* parent closed sync pipe */
253
_exit(0);
254
}
255
ATF_REQUIRE((tm = localtime(&then)) != NULL);
256
if (tm->tm_gmtoff == curoff)
257
continue;
258
olen = strftime(obuf, sizeof(obuf), "%z (%Z)", tm);
259
ATF_REQUIRE(olen > 0);
260
fprintf(stdout, "%s\n", obuf);
261
fflush(stdout);
262
curoff = tm->tm_gmtoff;
263
}
264
_exit(2);
265
}
266
/* parent */
267
close(opd[1]);
268
close(epd[1]);
269
close(spd[1]);
270
/* receive output until child terminates */
271
fds[0].fd = opd[0];
272
fds[0].events = POLLIN;
273
fds[1].fd = epd[0];
274
fds[1].events = POLLIN;
275
fds[2].fd = spd[0];
276
fds[2].events = POLLIN;
277
nfds = 3;
278
for (;;) {
279
ATF_REQUIRE(poll(fds, 3, 1000) >= 0);
280
time(&now);
281
if (fds[0].revents & POLLIN && olen < sizeof(obuf)) {
282
rlen = read(opd[0], obuf + olen, sizeof(obuf) - olen);
283
ATF_REQUIRE(rlen >= 0);
284
olen += rlen;
285
}
286
if (olen > 0) {
287
ATF_REQUIRE_EQ('\n', obuf[olen - 1]);
288
obuf[--olen] = '\0';
289
/* tzcase will be NULL at first */
290
if (tzcase != NULL) {
291
debug("%s", obuf);
292
ATF_REQUIRE_STREQ(tzcase->expect, obuf);
293
debug("change to %s detected after %d s",
294
tzcase->tzfn, (int)(now - changed));
295
if (tz_change_interval_p != NULL) {
296
ATF_CHECK((int)(now - changed) >=
297
*tz_change_interval_p - 1);
298
ATF_CHECK((int)(now - changed) <=
299
*tz_change_interval_p + 1);
300
}
301
}
302
olen = 0;
303
/* first / next test case */
304
if (tzcase == NULL)
305
tzcase = tzcases;
306
else
307
tzcase++;
308
if (tzcase->tzfn == NULL) {
309
/* test is over */
310
break;
311
}
312
change_tz(tzcase->tzfn);
313
changed = now;
314
}
315
if (fds[1].revents & POLLIN && elen < sizeof(ebuf)) {
316
rlen = read(epd[0], ebuf + elen, sizeof(ebuf) - elen);
317
ATF_REQUIRE(rlen >= 0);
318
elen += rlen;
319
}
320
if (elen > 0) {
321
ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr));
322
elen = 0;
323
}
324
if (nfds > 2 && fds[2].revents & POLLHUP) {
325
/* child closed sync pipe */
326
break;
327
}
328
/*
329
* The timeout for this test case is set to 10 minutes,
330
* because it can take that long to run with the default
331
* 61-second interval. However, each individual tzcase
332
* entry should not take much longer than the detection
333
* interval to test, so we can detect a problem long
334
* before Kyua terminates us.
335
*/
336
if ((now - changed) > tz_change_timeout) {
337
close(spd[0]);
338
if (tz_change_interval_p == NULL &&
339
tzcase == tzcases) {
340
/*
341
* The most likely explanation in this
342
* case is that libc was built without
343
* time zone change detection.
344
*/
345
atf_tc_skip("time zone change detection "
346
"does not appear to be enabled");
347
}
348
atf_tc_fail("timed out waiting for change to %s "
349
"to be detected", tzcase->tzfn);
350
}
351
}
352
close(opd[0]);
353
close(epd[0]);
354
close(spd[0]); /* this will wake up and terminate the child */
355
if (olen > 0)
356
ATF_REQUIRE_EQ(olen, fwrite(obuf, 1, olen, stdout));
357
if (elen > 0)
358
ATF_REQUIRE_EQ(elen, fwrite(ebuf, 1, elen, stderr));
359
ATF_REQUIRE_EQ(pid, waitpid(pid, &status, 0));
360
ATF_REQUIRE(WIFEXITED(status));
361
ATF_REQUIRE_EQ(0, WEXITSTATUS(status));
362
}
363
#endif /* DETECT_TZ_CHANGES */
364
365
static void
366
test_tz_env(const char *tzval, const char *expect)
367
{
368
setenv("TZ", tzval, 1);
369
test_tz(expect);
370
}
371
372
static void
373
tz_env_common(void)
374
{
375
char path[MAXPATHLEN];
376
const struct tzcase *tzcase = tzcases;
377
int len;
378
379
/* relative path */
380
for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++)
381
test_tz_env(tzcase->tzfn, tzcase->expect);
382
/* absolute path */
383
for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++) {
384
len = snprintf(path, sizeof(path), "%s/%s", TZDIR, tzcase->tzfn);
385
ATF_REQUIRE(len > 0 && (size_t)len < sizeof(path));
386
test_tz_env(path, tzcase->expect);
387
}
388
/* absolute path with additional slashes */
389
for (tzcase = tzcases; tzcase->tzfn != NULL; tzcase++) {
390
len = snprintf(path, sizeof(path), "%s/////%s", TZDIR, tzcase->tzfn);
391
ATF_REQUIRE(len > 0 && (size_t)len < sizeof(path));
392
test_tz_env(path, tzcase->expect);
393
}
394
}
395
396
ATF_TC(tz_env);
397
ATF_TC_HEAD(tz_env, tc)
398
{
399
atf_tc_set_md_var(tc, "descr", "Test TZ environment variable");
400
}
401
ATF_TC_BODY(tz_env, tc)
402
{
403
tz_env_common();
404
/* escape from TZDIR is permitted when not setugid */
405
test_tz_env("../zoneinfo/UTC", utc.expect);
406
}
407
408
409
ATF_TC(tz_invalid_env);
410
ATF_TC_HEAD(tz_invalid_env, tc)
411
{
412
atf_tc_set_md_var(tc, "descr", "Test invalid TZ value");
413
atf_tc_set_md_var(tc, "require.user", "root");
414
}
415
ATF_TC_BODY(tz_invalid_env, tc)
416
{
417
test_tz_env("invalid", invalid.expect);
418
test_tz_env(":invalid", invalid.expect);
419
}
420
421
ATF_TC(setugid);
422
ATF_TC_HEAD(setugid, tc)
423
{
424
atf_tc_set_md_var(tc, "descr", "Test setugid process");
425
atf_tc_set_md_var(tc, "require.user", "root");
426
}
427
ATF_TC_BODY(setugid, tc)
428
{
429
const struct tzcase *tzcase = tzcases;
430
431
/* prepare chroot */
432
ATF_REQUIRE_EQ(0, mkdir("root", 0755));
433
ATF_REQUIRE_EQ(0, mkdir("root/etc", 0755));
434
change_tz(tzcase->tzfn);
435
/* enter chroot */
436
ATF_REQUIRE_EQ(0, chroot("root"));
437
ATF_REQUIRE_EQ(0, chdir("/"));
438
/* become setugid */
439
ATF_REQUIRE_EQ(0, seteuid(UID_NOBODY));
440
ATF_REQUIRE(issetugid());
441
/* check timezone */
442
unsetenv("TZ");
443
test_tz(tzcases->expect);
444
}
445
446
ATF_TC(tz_env_setugid);
447
ATF_TC_HEAD(tz_env_setugid, tc)
448
{
449
atf_tc_set_md_var(tc, "descr", "Test TZ environment variable "
450
"in setugid process");
451
atf_tc_set_md_var(tc, "require.user", "root");
452
}
453
ATF_TC_BODY(tz_env_setugid, tc)
454
{
455
ATF_REQUIRE_EQ(0, seteuid(UID_NOBODY));
456
ATF_REQUIRE(issetugid());
457
tz_env_common();
458
/* escape from TZDIR is not permitted when setugid */
459
test_tz_env("../zoneinfo/UTC", invalid.expect);
460
}
461
462
ATF_TP_ADD_TCS(tp)
463
{
464
debugging = !getenv("__RUNNING_INSIDE_ATF_RUN") &&
465
isatty(STDERR_FILENO);
466
ATF_TP_ADD_TC(tp, tz_default);
467
ATF_TP_ADD_TC(tp, tz_invalid_file);
468
ATF_TP_ADD_TC(tp, thin_jail);
469
#ifdef DETECT_TZ_CHANGES
470
ATF_TP_ADD_TC(tp, detect_tz_changes);
471
#endif /* DETECT_TZ_CHANGES */
472
ATF_TP_ADD_TC(tp, tz_env);
473
ATF_TP_ADD_TC(tp, tz_invalid_env);
474
ATF_TP_ADD_TC(tp, setugid);
475
ATF_TP_ADD_TC(tp, tz_env_setugid);
476
return (atf_no_error());
477
}
478
479