blob: 29429168fef0012e68d8a972a0a9793daa0e673f [file] [log] [blame]
/*
* tlsdated.c - invoke tlsdate when necessary.
* Copyright (c) 2012 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* We invoke tlsdate once at system startup, then we start trying to invoke
* tlsdate when a new network route appears. We try a few times after each route
* comes up. As soon as we get a successful tlsdate run, we save that timestamp
* to disk, then linger to wait for system shutdown. At system shutdown
* (indicated by us getting SIGTERM), we save our timestamp to disk.
*/
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/rtc.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "routeup.h"
#include "util.h"
const char *kCacheDir = "/var/cache/tlsdated";
const char *kTempSuffix = ".new";
#ifndef TLSDATED_MAX_DATE
#define TLSDATED_MAX_DATE 1999991337 /* this'll be a great bug some day */
#endif
#ifndef RECENT_COMPILE_DATE
#error Need a RECENT_COMPILE_DATE
#endif
#if RECENT_COMPILE_DATE > TLSDATED_MAX_DATE
#error RECENT_COMPILE_DATE > TLSDATED_MAX_DATE
#endif
int is_sane_time(time_t ts)
{
return ts > RECENT_COMPILE_DATE && ts < TLSDATED_MAX_DATE;
}
/*
* Run tlsdate in a child process. We fork it off, then wait a specified time
* for it to exit; if it hasn't exited by then, we kill it and log a baffled
* message. We return tlsdate's exit code if tlsdate exits normally, and a
* negative value if we can't launch it or it exits uncleanly, so this function
* returns 0 for success and nonzero for failure, with >0 being a tlsdate exit
* code and <0 being an exec/fork/wait error.
*/
int tlsdate(char *argv[], char *envp[], int tries, int wait_between_tries)
{
pid_t pid;
if ((pid = fork()) > 0) {
/*
* We launched tlsdate; wait for up to kMaxTries intervals of
* kWaitBetweenTries for it to exit, then kill it if it still
* hasn't.
*/
int status = -1;
int i = 0;
for (i = 0; i < tries; ++i) {
info("wait for child attempt %d", i);
if (waitpid(-1, &status, WNOHANG) > 0)
break;
sleep(wait_between_tries);
}
if (i == tries) {
error("child hung?");
kill(pid, SIGKILL);
/* still have to wait() so we don't leak the child. */
wait(&status);
return -1;
}
info("child exited with %d", status);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
} else if (pid < 0) {
pinfo("fork() failed");
return -1;
}
execve(argv[0], argv, envp);
pinfo("execve() failed");
exit(1);
}
/*
* Load a time value out of the file named by path. Returns 0 if successful,
* -1 if not. The file contains the time in seconds since epoch in host byte
* order.
*/
int load_disk_timestamp(const char *path, time_t *t)
{
int fd = open(path, O_RDONLY | O_NOFOLLOW);
time_t tmpt;
if (fd < 0) {
perror("Can't open %s for reading", path);
return -1;
}
if (read(fd, &tmpt, sizeof(tmpt)) != sizeof(tmpt)) {
perror("Can't read seconds from %s", path);
close(fd);
return -1;
}
close(fd);
if (!is_sane_time(tmpt)) {
perror("Timevalue not sane: %lu", tmpt);
return -1;
}
*t = tmpt;
return 0;
}
/* Save a time value to the file named by path. */
void save_disk_timestamp(const char *path, time_t t)
{
char tmp[PATH_MAX];
int fd;
if (snprintf(tmp, sizeof(tmp), "%s%s", path, kTempSuffix)
>= sizeof(tmp)) {
pinfo("Path %s too long to use", path);
exit(1);
}
if ((fd = open(tmp, O_WRONLY | O_CREAT | O_NOFOLLOW | O_TRUNC,
S_IRWXU)) < 0) {
pinfo("open failed");
return;
}
if (write(fd, &t, sizeof(t)) != sizeof(t)) {
pinfo("write failed");
close(fd);
return;
}
if (close(fd)) {
pinfo("close failed");
return;
}
if (rename(tmp, path))
pinfo("rename failed");
}
/*
* Set the hardware clock referred to by fd (which should be a descriptor to
* some device that implements the interface documented in rtc(4)) to the system
* time. See hwclock(8) for details of why this is important. If we fail, we
* just return - there's nothing the caller can really do about a failure of
* this function except try later.
*/
void sync_hwclock(int fd) {
struct timeval tv;
struct tm *tm;
struct rtc_time rtctm;
if (gettimeofday(&tv, NULL)) {
pinfo("gettimeofday() failed");
return;
}
tm = gmtime(&tv.tv_sec);
/* these structs are identical, but separately defined */
rtctm.tm_sec = tm->tm_sec;
rtctm.tm_min = tm->tm_min;
rtctm.tm_hour = tm->tm_hour;
rtctm.tm_mday = tm->tm_mday;
rtctm.tm_mon = tm->tm_mon;
rtctm.tm_year = tm->tm_year;
rtctm.tm_wday = tm->tm_wday;
rtctm.tm_yday = tm->tm_yday;
rtctm.tm_isdst = tm->tm_isdst;
if (ioctl(fd, RTC_SET_TIME, &rtctm)) {
pinfo("ioctl(%d, RTC_SET_TIME, ...) failed", fd);
return;
}
info("synced rtc to sysclock");
}
/*
* Wait for a single event to happen. If should_netlink is true, we ask the
* supplied routeup context to wait for an event; otherwise, we wait for a byte
* of input on stdin. Semantics (for return value) are the same as for
* routeup_once().
*/
int wait_for_event(struct routeup *rtc, int should_netlink, int timeout)
{
char buf[1];
fd_set fds;
struct timeval tv;
int r;
if (should_netlink)
return routeup_once(rtc, timeout);
tv.tv_sec = timeout;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
r = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
if (r > 0)
return read(STDIN_FILENO, buf, sizeof(buf)) != 1;
return r == 0 ? 1 : -1;
}
char timestamp_path[PATH_MAX];
void sync_and_save(int hwclock_fd, int should_sync, int should_save)
{
struct timeval tv;
if (should_sync)
sync_hwclock(hwclock_fd);
if (should_save) {
if (gettimeofday(&tv, NULL))
pfatal("gettimeofday() failed");
save_disk_timestamp(timestamp_path, tv.tv_sec);
}
}
#ifdef TLSDATED_MAIN
void sigterm_handler(int _unused)
{
struct timeval tv;
if (gettimeofday(&tv, NULL))
/* can't use stdio or syslog inside a sig handler */
exit(2);
save_disk_timestamp(timestamp_path, tv.tv_sec);
exit(0);
}
void usage(const char *progn)
{
printf("Usage: %s [flags...] [--] [tlsdate command...]\n", progn);
printf(" -w don't set hwclock\n");
printf(" -p dry run (don't really set time)\n");
printf(" -r use stdin instead of netlink for routes\n");
printf(" -t <n> try n times when a new route appears\n");
printf(" -d <n> delay n seconds between tries\n");
printf(" -T <n> give subprocess n chances to exit\n");
printf(" -D <n> delay n seconds between wait attempts\n");
printf(" -c <path> set the cache directory\n");
printf(" -a <n> run at most every n seconds in steady state\n");
printf(" -l don't load disk timestamps\n");
printf(" -s don't save disk timestamps\n");
printf(" -v be verbose\n");
printf(" -h this\n");
}
int API main(int argc, char *argv[], char *envp[])
{
struct routeup rtc;
int max_tries = 10;
int wait_between_tries = 10;
int subprocess_tries = 10;
int subprocess_wait_between_tries = 3;
int steady_state_interval = 86400;
const char *base_path = kCacheDir;
int hwclock_fd = -1;
static const char *kDefaultArgv[] = {
"/usr/sbin/tlsdate", "-H", "clients3.google.com", NULL
};
char **tlsdate_argv = kDefaultArgv;
int should_sync_hwclock = 1;
int should_load_disk = 1;
int should_save_disk = 1;
int should_netlink = 1;
int dry_run = 0;
/* Parse arguments */
int opt;
while ((opt = getopt(argc, argv, "hwrpt:d:T:D:c:a:lsv")) != -1) {
switch (opt) {
case 'w':
should_sync_hwclock = 0;
break;
case 'r':
should_netlink = 0;
break;
case 'p':
dry_run = 1;
break;
case 't':
max_tries = atoi(optarg);
break;
case 'd':
wait_between_tries = atoi(optarg);
break;
case 'T':
subprocess_tries = atoi(optarg);
break;
case 'D':
subprocess_wait_between_tries = atoi(optarg);
break;
case 'c':
base_path = optarg;
break;
case 'a':
steady_state_interval = atoi(optarg);
break;
case 'l':
should_load_disk = 0;
break;
case 's':
should_save_disk = 0;
break;
case 'v':
verbose = 1;
break;
case 'h':
default:
usage(argv[0]);
exit(1);
}
}
if (optind < argc)
tlsdate_argv = argv + optind;
/* Validate arguments */
if (!max_tries)
fatal("-t argument must be nonzero");
if (!wait_between_tries)
fatal("-d argument must be nonzero");
if (!subprocess_tries)
fatal("-T argument must be nonzero");
if (!subprocess_wait_between_tries)
fatal("-D argument must be nonzero");
if (!steady_state_interval)
fatal("-a argument must be nonzero");
if (snprintf(timestamp_path, sizeof(timestamp_path), "%s/timestamp",
base_path) >= sizeof(timestamp_path))
fatal("supplied base path is too long: '%s'", base_path);
if (strlen(timestamp_path) + strlen(kTempSuffix) >= PATH_MAX)
fatal("supplied base path is too long: '%s'", base_path);
/* grab a handle to /dev/rtc for sync_hwclock() */
if (should_sync_hwclock && (hwclock_fd = open("/dev/rtc", O_RDONLY)) < 0)
pfatal("can't open hwclock fd");
/* set up a netlink context if we need one */
if (should_netlink && routeup_setup(&rtc))
pfatal("Can't open netlink socket");
if (!is_sane_time(time(NULL))) {
struct timeval tv = { 0, 0 };
/*
* If the time is before the build timestamp, we're probably on
* a system with a broken rtc. Try loading the timestamp off
* disk.
*/
tv.tv_sec = RECENT_COMPILE_DATE;
if (should_load_disk &&
load_disk_timestamp(timestamp_path, &tv.tv_sec))
pinfo("can't load disk timestamp");
if (!dry_run && settimeofday(&tv, NULL))
pfatal("settimeofday() failed");
/*
* don't save here - we either just loaded this time or used the
* default time, and neither of those are good to save
*/
sync_and_save(hwclock_fd, should_sync_hwclock, 0);
}
/* register a signal handler to save time at shutdown */
if (should_save_disk)
signal(SIGTERM, sigterm_handler);
/*
* Try once right away. If we fail, wait for a route to appear, then try
* for a while; repeat whenever another route appears. Try until we
* succeed.
*/
if (!tlsdate(tlsdate_argv, envp, subprocess_tries,
subprocess_wait_between_tries))
sync_and_save(hwclock_fd, should_sync_hwclock,
should_save_disk);
/*
* Loop until we catch a fatal signal or routeup_once() fails. We run
* tlsdate at least once a day, but possibly as often as routes come up;
* this should handle cases like a VPN being brought up and down
* periodically.
*/
while (wait_for_event(&rtc, should_netlink,
steady_state_interval) >= 0) {
/*
* If a route just came up, run tlsdate; if it
* succeeded, then we're good and can keep time locally
* from now on.
*/
int i;
for (i = 0; i < max_tries &&
tlsdate(tlsdate_argv, envp, subprocess_tries,
subprocess_wait_between_tries); ++i)
sleep(wait_between_tries);
if (i != max_tries) {
info("tlsdate succeeded");
sync_and_save(hwclock_fd, should_sync_hwclock,
should_save_disk);
break;
}
}
return 1;
}
#endif /* !TLSDATED_MAIN */