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