/*
 * tlsdate-setter.c - privileged time setter for tlsdated
 * Copyright (c) 2013 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.
 */

#include "config.h"

#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <event2/event.h>

#include "src/conf.h"
#include "src/dbus.h"
#include "src/seccomp.h"
#include "src/tlsdate.h"
#include "src/util.h"

/* Atomically writes the timestamp to the specified fd. */
int
save_timestamp_to_fd (int fd, time_t t)
{
  return platform->file_write(fd, &t, sizeof (t));
}

void
report_setter_error (siginfo_t *info)
{
  const char *code;
  int killit = 0;
  switch (info->si_code)
    {
    case CLD_EXITED:
      code = "EXITED";
      break;
    case CLD_KILLED:
      code = "KILLED";
      break;
    case CLD_DUMPED:
      code = "DUMPED";
      break;
    case CLD_STOPPED:
      code = "STOPPED";
      killit = 1;
      break;
    case CLD_TRAPPED:
      code = "TRAPPED";
      killit = 1;
      break;
    case CLD_CONTINUED:
      code = "CONTINUED";
      killit = 1;
      break;
    default:
      code = "???";
      killit = 1;
    }
  info ("tlsdate-setter exitting: code:%s status:%d pid:%d uid:%d",
        code, info->si_status, info->si_pid, info->si_uid);
  if (killit)
    kill (info->si_pid, SIGKILL);
}

void
time_setter_coprocess (int time_fd, int notify_fd, struct state *state)
{
  int save_fd = -1;
  int status;
  prctl (PR_SET_NAME, "tlsdated-setter");
  if (state->opts.should_save_disk && !state->opts.dry_run)
    {
      const mode_t perms = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
      /* TODO(wad) platform->file_open */
      if ( (save_fd = open (state->timestamp_path,
                            O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC,
                            perms)) < 0 ||
           fchmod (save_fd, perms) != 0)
        {
          /* Attempt to unlink the path on the way out. */
          unlink (state->timestamp_path);
          status = SETTER_NO_SAVE;
          goto notify_and_die;
        }
    }
  /* XXX: Drop all privs but CAP_SYS_TIME */
#ifdef HAVE_SECCOMP_FILTER
  if (enable_setter_seccomp())
    {
      status = SETTER_NO_SBOX;
      goto notify_and_die;
    }
#endif
  while (1)
    {
      struct timeval tv = { 0, 0 };
      /* The wire protocol is a time_t, but the caller should
       * always be the unprivileged tlsdated process which spawned this
       * helper.
       * There are two special messages:
       * (time_t)   0: requests a clean shutdown
       * (time_t) < 0: indicates not to write to disk
       * On Linux, time_t is a signed long.  Expanding the protocol
       * is easy, but writing one long only is ideal.
       */
      ssize_t bytes = read (time_fd, &tv.tv_sec, sizeof (tv.tv_sec));
      int save = 1;
      if (bytes == -1)
        {
          if (errno == EINTR)
            continue;
          status = SETTER_READ_ERR;
          goto notify_and_die;
        }
      if (bytes == 0)
        {
          /* End of pipe */
          status = SETTER_READ_ERR;
          goto notify_and_die;
        }
      if (bytes != sizeof (tv.tv_sec))
        continue;
      if (tv.tv_sec < 0)
        {
          /* Don't write to disk */
          tv.tv_sec = -tv.tv_sec;
          save = 0;
        }
      if (tv.tv_sec == 0)
        {
          status = SETTER_EXIT;
          goto notify_and_die;
        }
      if (is_sane_time (tv.tv_sec))
        {
          /* It would be nice if time was only allowed to move forward, but
           * if a single time source is wrong, then it could make it impossible
           * to recover from once the time is written to disk.
           */
          status = SETTER_BAD_TIME;
          if (!state->opts.dry_run)
            {
              if (settimeofday (&tv, NULL) < 0)
                {
                  status = SETTER_SET_ERR;
                  goto notify_and_die;
                }
              if (state->opts.should_sync_hwclock &&
                  platform->rtc_write(&state->hwclock, &tv))
                {
                  status = SETTER_NO_RTC;
                  goto notify_and_die;
                }
              if (save && save_fd != -1 &&
                  save_timestamp_to_fd (save_fd, tv.tv_sec))
                {
                  status = SETTER_NO_SAVE;
                  goto notify_and_die;
                }
            }
          status = SETTER_TIME_SET;
        }
      /* TODO(wad) platform->file_write */
      IGNORE_EINTR (write (notify_fd, &status, sizeof(status)));
    }
notify_and_die:
  IGNORE_EINTR (write (notify_fd, &status, sizeof(status)));
  close (notify_fd);
  close (save_fd);
  _exit (status);
}