changeset 244:b8aa08503378

[tools] Add sandboxed online LaTeX editor Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 16:56:25 -0700
parents 823f2a8b16c8
children 3843bb6253ac
files mrjunejune/BUILD mrjunejune/README.md mrjunejune/latex_renderer.c mrjunejune/latex_renderer.h mrjunejune/main.c mrjunejune/src/tools/index.html mrjunejune/src/tools/latex_editor/index.css mrjunejune/src/tools/latex_editor/index.html mrjunejune/src/tools/latex_editor/index.js mrjunejune/test/BUILD mrjunejune/test/auto_generated_test.c mrjunejune/test/integration_test.c mrjunejune/test/latex_editor_test.js mrjunejune/test/latex_renderer_test.c mrjunejune/test/snapshots/tools.snapshot mrjunejune/test/snapshots/tools_latex_editor.snapshot seobeo/os/s_linux_edge.c seobeo/s_network.c seobeo/s_web.c seobeo/seobeo.h
diffstat 20 files changed, 2133 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/mrjunejune/BUILD	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/BUILD	Mon Aug 03 16:56:25 2026 -0700
@@ -171,6 +171,14 @@
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
+cc_library(
+  name = "latex_renderer",
+  srcs = ["latex_renderer.c"],
+  hdrs = ["latex_renderer.h"],
+  copts = ["-D_GNU_SOURCE"],
+  visibility = ["//mrjunejune/test:__pkg__"],
+)
+
 # Server binary
 cc_binary(
   name = "mrjunejune_server",
@@ -180,6 +188,7 @@
     "//markdown_converter:markdown_to_html_c",
     "//s3:s3",
     "//deita:deita",
+    ":latex_renderer",
   ],
   copts = ["-D_GNU_SOURCE"],
   linkopts = ["-lpthread"],
@@ -200,6 +209,7 @@
     "//markdown_converter:markdown_to_html_c",
     "//s3:s3",
     "//deita:deita",
+    ":latex_renderer",
   ],
   copts = ["-D_GNU_SOURCE"],
   linkopts = ["-lpthread"],
--- a/mrjunejune/README.md	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/README.md	Mon Aug 03 16:56:25 2026 -0700
@@ -39,6 +39,25 @@
 bazel test //mrjunejune/test:theme_and_webp_test
 ```
 
+## LaTeX editor
+
+`/tools/latex_editor` sends source to the C server, which compiles it with the
+host's `/usr/bin/pdflatex` and returns the PDF directly to the browser. Install
+TeX Live on production hosts before deploying:
+
+```bash
+sudo apt-get install texlive-latex-base texlive-latex-recommended
+```
+
+The compiler child uses restricted TeX file access, Linux Landlock, a seccomp
+network filter, and CPU, memory, file-size, process, and wall-clock limits.
+Compilation fails closed when the Linux sandbox cannot be applied.
+
+```bash
+bazel test //mrjunejune/test:latex_renderer_test
+bazel test //mrjunejune/test:latex_editor_test
+```
+
 ## TODO 
 
 - Add caching layer
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/latex_renderer.c	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,718 @@
+#include "mrjunejune/latex_renderer.h"
+
+#ifdef __linux__
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/audit.h>
+#include <linux/filter.h>
+#include <linux/landlock.h>
+#include <linux/seccomp.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifndef LANDLOCK_ACCESS_FS_REFER
+#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
+#endif
+#ifndef LANDLOCK_ACCESS_FS_TRUNCATE
+#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
+#endif
+
+#if defined(__x86_64__)
+#define LATEX_AUDIT_ARCH AUDIT_ARCH_X86_64
+#elif defined(__aarch64__)
+#define LATEX_AUDIT_ARCH AUDIT_ARCH_AARCH64
+#else
+#define LATEX_AUDIT_ARCH 0
+#endif
+
+#define LATEX_DIAGNOSTICS_MAX_BYTES (16 * 1024)
+#define LATEX_CPU_TIMEOUT_SECONDS 5
+#define LATEX_WALL_TIMEOUT_SECONDS 8
+
+static char *duplicate_message(const char *message)
+{
+  size_t length = strlen(message);
+  char *copy = malloc(length + 1);
+  if (copy)
+    memcpy(copy, message, length + 1);
+  return copy;
+}
+
+static Latex_Render_Result result_with_message(
+    Latex_Render_Status status,
+    const char *message)
+{
+  return (Latex_Render_Result){
+    .status = status,
+    .diagnostics = duplicate_message(message),
+  };
+}
+
+static bool write_all(int fd, const uint8_t *data, size_t size)
+{
+  while (size > 0)
+  {
+    ssize_t written = write(fd, data, size);
+    if (written < 0)
+    {
+      if (errno == EINTR)
+        continue;
+      return false;
+    }
+    data += written;
+    size -= (size_t)written;
+  }
+  return true;
+}
+
+static int remove_tree(const char *path, unsigned depth)
+{
+  if (depth > 16)
+    return -1;
+
+  struct stat status;
+  if (lstat(path, &status) != 0)
+    return errno == ENOENT ? 0 : -1;
+  if (!S_ISDIR(status.st_mode))
+    return unlink(path);
+
+  DIR *directory = opendir(path);
+  if (!directory)
+    return -1;
+
+  int result = 0;
+  struct dirent *entry;
+  while ((entry = readdir(directory)) != NULL)
+  {
+    if (strcmp(entry->d_name, ".") == 0 ||
+        strcmp(entry->d_name, "..") == 0)
+      continue;
+
+    char child[1024];
+    int length = snprintf(
+        child,
+        sizeof(child),
+        "%s/%s",
+        path,
+        entry->d_name);
+    if (length < 0 ||
+        (size_t)length >= sizeof(child) ||
+        remove_tree(child, depth + 1) != 0)
+    {
+      result = -1;
+      break;
+    }
+  }
+  closedir(directory);
+  return result == 0 ? rmdir(path) : result;
+}
+
+static bool set_limit(int resource, rlim_t value)
+{
+  struct rlimit limit = {
+    .rlim_cur = value,
+    .rlim_max = value,
+  };
+  return setrlimit(resource, &limit) == 0;
+}
+
+static bool add_landlock_path_rule(
+    int ruleset_fd,
+    const char *path,
+    uint64_t access,
+    bool required)
+{
+  int path_fd = open(path, O_PATH | O_CLOEXEC);
+  if (path_fd < 0)
+    return !required && errno == ENOENT;
+
+  struct stat status;
+  bool ok = fstat(path_fd, &status) == 0;
+  if (ok && !S_ISDIR(status.st_mode))
+  {
+    access &= LANDLOCK_ACCESS_FS_EXECUTE |
+        LANDLOCK_ACCESS_FS_READ_FILE |
+        LANDLOCK_ACCESS_FS_WRITE_FILE;
+  }
+
+  struct landlock_path_beneath_attr rule = {
+    .allowed_access = access,
+    .parent_fd = path_fd,
+  };
+  if (ok)
+    ok = syscall(
+        SYS_landlock_add_rule,
+        ruleset_fd,
+        LANDLOCK_RULE_PATH_BENEATH,
+        &rule,
+        0) == 0;
+  close(path_fd);
+  return ok;
+}
+
+static bool apply_filesystem_sandbox(const char *job_directory)
+{
+  int abi = syscall(
+      SYS_landlock_create_ruleset,
+      NULL,
+      0,
+      LANDLOCK_CREATE_RULESET_VERSION);
+  if (abi < 1)
+    return false;
+
+  uint64_t read_access =
+      LANDLOCK_ACCESS_FS_EXECUTE |
+      LANDLOCK_ACCESS_FS_READ_FILE |
+      LANDLOCK_ACCESS_FS_READ_DIR;
+  uint64_t write_access =
+      LANDLOCK_ACCESS_FS_WRITE_FILE |
+      LANDLOCK_ACCESS_FS_REMOVE_DIR |
+      LANDLOCK_ACCESS_FS_REMOVE_FILE |
+      LANDLOCK_ACCESS_FS_MAKE_CHAR |
+      LANDLOCK_ACCESS_FS_MAKE_DIR |
+      LANDLOCK_ACCESS_FS_MAKE_REG |
+      LANDLOCK_ACCESS_FS_MAKE_SOCK |
+      LANDLOCK_ACCESS_FS_MAKE_FIFO |
+      LANDLOCK_ACCESS_FS_MAKE_BLOCK |
+      LANDLOCK_ACCESS_FS_MAKE_SYM;
+  if (abi >= 2)
+    write_access |= LANDLOCK_ACCESS_FS_REFER;
+  if (abi >= 3)
+    write_access |= LANDLOCK_ACCESS_FS_TRUNCATE;
+
+  struct landlock_ruleset_attr ruleset = {
+    .handled_access_fs = read_access | write_access,
+  };
+  int ruleset_fd = syscall(
+      SYS_landlock_create_ruleset,
+      &ruleset,
+      sizeof(ruleset),
+      0);
+  if (ruleset_fd < 0)
+    return false;
+
+  const char *read_only_directories[] = {
+    "/usr",
+    "/lib",
+    "/lib64",
+    "/bin",
+    "/var/lib/texmf",
+    "/etc/texmf",
+    "/etc/fonts",
+  };
+  bool ok = true;
+  for (size_t i = 0;
+       ok && i < sizeof(read_only_directories) / sizeof(read_only_directories[0]);
+       i++)
+  {
+    ok = add_landlock_path_rule(
+        ruleset_fd,
+        read_only_directories[i],
+        read_access,
+        false);
+  }
+  if (ok)
+    ok = add_landlock_path_rule(
+        ruleset_fd,
+        "/etc/ld.so.cache",
+        LANDLOCK_ACCESS_FS_READ_FILE,
+        false);
+  if (ok)
+    ok = add_landlock_path_rule(
+        ruleset_fd,
+        "/dev/null",
+        LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+        true);
+  if (ok)
+    ok = add_landlock_path_rule(
+        ruleset_fd,
+        "/dev/urandom",
+        LANDLOCK_ACCESS_FS_READ_FILE,
+        false);
+  if (ok)
+    ok = add_landlock_path_rule(
+        ruleset_fd,
+        job_directory,
+        read_access | write_access,
+        true);
+  if (ok)
+    ok = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0;
+  if (ok)
+    ok = syscall(
+        SYS_landlock_restrict_self,
+        ruleset_fd,
+        0) == 0;
+  close(ruleset_fd);
+  return ok;
+}
+
+static bool apply_network_sandbox(void)
+{
+  if (LATEX_AUDIT_ARCH == 0)
+    return false;
+
+  struct sock_filter filters[64];
+  size_t count = 0;
+#define ADD_FILTER(value) filters[count++] = (struct sock_filter)value
+#define DENY_SYSCALL(number) \
+  do { \
+    ADD_FILTER(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, number, 0, 1)); \
+    ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM)); \
+  } while (0)
+
+  ADD_FILTER(BPF_STMT(
+      BPF_LD | BPF_W | BPF_ABS,
+      offsetof(struct seccomp_data, arch)));
+  ADD_FILTER(BPF_JUMP(
+      BPF_JMP | BPF_JEQ | BPF_K,
+      LATEX_AUDIT_ARCH,
+      1,
+      0));
+  ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS));
+  ADD_FILTER(BPF_STMT(
+      BPF_LD | BPF_W | BPF_ABS,
+      offsetof(struct seccomp_data, nr)));
+  DENY_SYSCALL(__NR_socket);
+  DENY_SYSCALL(__NR_socketpair);
+  DENY_SYSCALL(__NR_connect);
+  DENY_SYSCALL(__NR_accept);
+  DENY_SYSCALL(__NR_accept4);
+  DENY_SYSCALL(__NR_bind);
+  DENY_SYSCALL(__NR_listen);
+  DENY_SYSCALL(__NR_sendto);
+  DENY_SYSCALL(__NR_recvfrom);
+  DENY_SYSCALL(__NR_sendmsg);
+  DENY_SYSCALL(__NR_recvmsg);
+  ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW));
+
+  struct sock_fprog program = {
+    .len = (unsigned short)count,
+    .filter = filters,
+  };
+#undef DENY_SYSCALL
+#undef ADD_FILTER
+  return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) == 0;
+}
+
+static void configure_child_environment(const char *job_directory)
+{
+  clearenv();
+  setenv("HOME", job_directory, 1);
+  setenv("TMPDIR", job_directory, 1);
+  setenv("TEXMFVAR", job_directory, 1);
+  setenv("TEXMFCONFIG", job_directory, 1);
+  setenv("openin_any", "p", 1);
+  setenv("openout_any", "p", 1);
+  setenv("shell_escape", "0", 1);
+  setenv("SOURCE_DATE_EPOCH", "0", 1);
+  setenv("LANG", "C.UTF-8", 1);
+}
+
+static void close_inherited_descriptors(void)
+{
+#ifdef SYS_close_range
+  if (syscall(SYS_close_range, 3U, UINT_MAX, 0) == 0)
+    return;
+#endif
+  struct rlimit limit;
+  rlim_t maximum = 65536;
+  if (getrlimit(RLIMIT_NOFILE, &limit) == 0 &&
+      limit.rlim_cur != RLIM_INFINITY &&
+      limit.rlim_cur < maximum)
+    maximum = limit.rlim_cur;
+  for (int fd = 3; fd < (int)maximum; fd++)
+    close(fd);
+}
+
+static void run_compiler_child(
+    const char *compiler,
+    const char *job_directory,
+    int output_fd)
+{
+  if (dup2(output_fd, STDOUT_FILENO) < 0 ||
+      dup2(output_fd, STDERR_FILENO) < 0)
+    _exit(120);
+  if (output_fd != STDOUT_FILENO && output_fd != STDERR_FILENO)
+    close(output_fd);
+  close_inherited_descriptors();
+  if (setpgid(0, 0) != 0 ||
+      prctl(PR_SET_PDEATHSIG, SIGKILL) != 0 ||
+      getppid() == 1)
+    _exit(119);
+  if (chdir(job_directory) != 0)
+    _exit(121);
+
+  configure_child_environment(job_directory);
+  if (!set_limit(RLIMIT_CPU, LATEX_CPU_TIMEOUT_SECONDS) ||
+      !set_limit(RLIMIT_AS, 768 * 1024 * 1024) ||
+      !set_limit(RLIMIT_FSIZE, LATEX_PDF_MAX_BYTES) ||
+      !set_limit(RLIMIT_NOFILE, 64) ||
+      !set_limit(RLIMIT_NPROC, 1))
+    _exit(122);
+  if (!apply_filesystem_sandbox(job_directory) ||
+      !apply_network_sandbox())
+    _exit(123);
+
+  char *const arguments[] = {
+    (char *)compiler,
+    "-interaction=nonstopmode",
+    "-halt-on-error",
+    "-no-shell-escape",
+    "-file-line-error",
+    "-output-directory=.",
+    "document.tex",
+    NULL,
+  };
+  execv(compiler, arguments);
+  _exit(errno == ENOENT ? 127 : 126);
+}
+
+static double elapsed_seconds(
+    const struct timespec *start,
+    const struct timespec *now)
+{
+  return (double)(now->tv_sec - start->tv_sec) +
+      (double)(now->tv_nsec - start->tv_nsec) / 1000000000.0;
+}
+
+static int wait_for_compiler(
+    pid_t child,
+    int output_fd,
+    char *diagnostics,
+    size_t diagnostics_size,
+    bool *timed_out)
+{
+  int flags = fcntl(output_fd, F_GETFL, 0);
+  if (flags >= 0)
+    fcntl(output_fd, F_SETFL, flags | O_NONBLOCK);
+
+  size_t used = 0;
+  int child_status = 0;
+  bool child_done = false;
+  struct timespec start;
+  clock_gettime(CLOCK_MONOTONIC, &start);
+
+  while (!child_done)
+  {
+    char buffer[2048];
+    ssize_t count;
+    while ((count = read(output_fd, buffer, sizeof(buffer))) > 0)
+    {
+      size_t available = diagnostics_size - used - 1;
+      size_t copy_size = (size_t)count < available ? (size_t)count : available;
+      if (copy_size > 0)
+      {
+        memcpy(diagnostics + used, buffer, copy_size);
+        used += copy_size;
+      }
+    }
+
+    pid_t waited = waitpid(child, &child_status, WNOHANG);
+    if (waited == child)
+    {
+      child_done = true;
+      break;
+    }
+    if (waited < 0 && errno != EINTR)
+      break;
+
+    struct timespec now;
+    clock_gettime(CLOCK_MONOTONIC, &now);
+    if (elapsed_seconds(&start, &now) >= LATEX_WALL_TIMEOUT_SECONDS)
+    {
+      *timed_out = true;
+      kill(-child, SIGKILL);
+      while (waitpid(child, &child_status, 0) < 0 && errno == EINTR)
+      {
+      }
+      child_done = true;
+      break;
+    }
+
+    struct pollfd poll_fd = {
+      .fd = output_fd,
+      .events = POLLIN,
+    };
+    poll(&poll_fd, 1, 50);
+  }
+
+  char buffer[2048];
+  ssize_t count;
+  while ((count = read(output_fd, buffer, sizeof(buffer))) > 0)
+  {
+    size_t available = diagnostics_size - used - 1;
+    size_t copy_size = (size_t)count < available ? (size_t)count : available;
+    if (copy_size > 0)
+    {
+      memcpy(diagnostics + used, buffer, copy_size);
+      used += copy_size;
+    }
+  }
+  diagnostics[used] = '\0';
+  kill(-child, SIGKILL);
+  return child_status;
+}
+
+static bool read_pdf(
+    const char *path,
+    uint8_t **data,
+    size_t *size)
+{
+  int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
+  if (fd < 0)
+    return false;
+
+  struct stat status;
+  bool ok = fstat(fd, &status) == 0 &&
+      S_ISREG(status.st_mode) &&
+      status.st_size > 4 &&
+      status.st_size <= LATEX_PDF_MAX_BYTES;
+  uint8_t *content = NULL;
+  if (ok)
+  {
+    content = malloc((size_t)status.st_size);
+    ok = content != NULL;
+  }
+  size_t offset = 0;
+  while (ok && offset < (size_t)status.st_size)
+  {
+    ssize_t count = read(
+        fd,
+        content + offset,
+        (size_t)status.st_size - offset);
+    if (count < 0 && errno == EINTR)
+      continue;
+    if (count <= 0)
+    {
+      ok = false;
+      break;
+    }
+    offset += (size_t)count;
+  }
+  close(fd);
+
+  if (!ok ||
+      memcmp(content, "%PDF-", 5) != 0)
+  {
+    free(content);
+    return false;
+  }
+  *data = content;
+  *size = offset;
+  return true;
+}
+
+Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size)
+{
+  if (!source || source_size == 0)
+    return result_with_message(
+        LATEX_RENDER_INVALID_INPUT,
+        "LaTeX source is required.");
+  if (source_size > LATEX_SOURCE_MAX_BYTES)
+    return result_with_message(
+        LATEX_RENDER_INVALID_INPUT,
+        "LaTeX source exceeds the 64 KiB limit.");
+  if (memchr(source, '\0', source_size))
+    return result_with_message(
+        LATEX_RENDER_INVALID_INPUT,
+        "LaTeX source cannot contain NUL bytes.");
+
+  const char *compiler = "/usr/bin/pdflatex";
+  if (access(compiler, X_OK) != 0)
+    return result_with_message(
+        LATEX_RENDER_COMPILER_UNAVAILABLE,
+        "The server LaTeX compiler is unavailable.");
+
+  char job_directory[] = "/tmp/mrjunejune-latex-XXXXXX";
+  if (!mkdtemp(job_directory))
+    return result_with_message(
+        LATEX_RENDER_INTERNAL_ERROR,
+        "Unable to create the LaTeX workspace.");
+
+  char source_path[512];
+  snprintf(
+      source_path,
+      sizeof(source_path),
+      "%s/document.tex",
+      job_directory);
+  int source_fd = open(
+      source_path,
+      O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
+      0600);
+  if (source_fd < 0 || !write_all(source_fd, source, source_size))
+  {
+    if (source_fd >= 0)
+      close(source_fd);
+    remove_tree(job_directory, 0);
+    return result_with_message(
+        LATEX_RENDER_INTERNAL_ERROR,
+        "Unable to write the LaTeX source.");
+  }
+  close(source_fd);
+
+  int output_pipe[2];
+  if (pipe2(output_pipe, O_CLOEXEC) != 0)
+  {
+    remove_tree(job_directory, 0);
+    return result_with_message(
+        LATEX_RENDER_INTERNAL_ERROR,
+        "Unable to capture compiler output.");
+  }
+
+  pid_t child = fork();
+  if (child < 0)
+  {
+    close(output_pipe[0]);
+    close(output_pipe[1]);
+    remove_tree(job_directory, 0);
+    return result_with_message(
+        LATEX_RENDER_INTERNAL_ERROR,
+        "Unable to start the LaTeX compiler.");
+  }
+  if (child == 0)
+  {
+    close(output_pipe[0]);
+    run_compiler_child(compiler, job_directory, output_pipe[1]);
+  }
+  setpgid(child, child);
+
+  close(output_pipe[1]);
+  char *diagnostics = calloc(1, LATEX_DIAGNOSTICS_MAX_BYTES + 1);
+  if (!diagnostics)
+  {
+    kill(-child, SIGKILL);
+    waitpid(child, NULL, 0);
+    close(output_pipe[0]);
+    remove_tree(job_directory, 0);
+    return result_with_message(
+        LATEX_RENDER_INTERNAL_ERROR,
+        "Unable to allocate compiler diagnostics.");
+  }
+
+  bool timed_out = false;
+  int child_status = wait_for_compiler(
+      child,
+      output_pipe[0],
+      diagnostics,
+      LATEX_DIAGNOSTICS_MAX_BYTES + 1,
+      &timed_out);
+  close(output_pipe[0]);
+
+  Latex_Render_Result result = {
+    .status = LATEX_RENDER_INTERNAL_ERROR,
+    .diagnostics = diagnostics,
+  };
+  if (timed_out)
+  {
+    result.status = LATEX_RENDER_TIMEOUT;
+    free(result.diagnostics);
+    result.diagnostics = duplicate_message(
+        "Compilation exceeded the 8 second limit.");
+  }
+  else if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 127)
+  {
+    result.status = LATEX_RENDER_COMPILER_UNAVAILABLE;
+  }
+  else if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 123)
+  {
+    result.status = LATEX_RENDER_SANDBOX_ERROR;
+    free(result.diagnostics);
+    result.diagnostics = duplicate_message(
+        "The server could not apply the LaTeX security sandbox.");
+  }
+  else if (WIFSIGNALED(child_status) &&
+           (WTERMSIG(child_status) == SIGXCPU ||
+            WTERMSIG(child_status) == SIGKILL))
+  {
+    result.status = LATEX_RENDER_TIMEOUT;
+    free(result.diagnostics);
+    result.diagnostics = duplicate_message(
+        "Compilation exceeded its CPU or memory limit.");
+  }
+  else if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0)
+  {
+    result.status = LATEX_RENDER_COMPILE_ERROR;
+    if (!result.diagnostics[0])
+    {
+      free(result.diagnostics);
+      result.diagnostics = duplicate_message("LaTeX compilation failed.");
+    }
+  }
+  else
+  {
+    char pdf_path[512];
+    snprintf(pdf_path, sizeof(pdf_path), "%s/document.pdf", job_directory);
+    if (read_pdf(pdf_path, &result.pdf_data, &result.pdf_size))
+    {
+      result.status = LATEX_RENDER_OK;
+      free(result.diagnostics);
+      result.diagnostics = NULL;
+    }
+    else
+    {
+      result.status = LATEX_RENDER_INTERNAL_ERROR;
+      free(result.diagnostics);
+      result.diagnostics = duplicate_message(
+          "The compiler did not produce a valid PDF.");
+    }
+  }
+
+  remove_tree(job_directory, 0);
+  return result;
+}
+
+void Latex_Render_Result_Destroy(Latex_Render_Result *result)
+{
+  if (!result)
+    return;
+  free(result->pdf_data);
+  free(result->diagnostics);
+  memset(result, 0, sizeof(*result));
+}
+
+#else
+
+#include <stdlib.h>
+#include <string.h>
+
+Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size)
+{
+  (void)source;
+  (void)source_size;
+  const char *message =
+      "The LaTeX security sandbox is only available on Linux.";
+  Latex_Render_Result result = {
+    .status = LATEX_RENDER_SANDBOX_ERROR,
+    .diagnostics = malloc(strlen(message) + 1),
+  };
+  if (result.diagnostics)
+    strcpy(result.diagnostics, message);
+  return result;
+}
+
+void Latex_Render_Result_Destroy(Latex_Render_Result *result)
+{
+  if (!result)
+    return;
+  free(result->pdf_data);
+  free(result->diagnostics);
+  memset(result, 0, sizeof(*result));
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/latex_renderer.h	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,30 @@
+#ifndef MRJUNEJUNE_LATEX_RENDERER_H
+#define MRJUNEJUNE_LATEX_RENDERER_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define LATEX_SOURCE_MAX_BYTES (64 * 1024)
+#define LATEX_PDF_MAX_BYTES (4 * 1024 * 1024)
+
+typedef enum {
+  LATEX_RENDER_OK = 0,
+  LATEX_RENDER_INVALID_INPUT,
+  LATEX_RENDER_COMPILE_ERROR,
+  LATEX_RENDER_TIMEOUT,
+  LATEX_RENDER_COMPILER_UNAVAILABLE,
+  LATEX_RENDER_SANDBOX_ERROR,
+  LATEX_RENDER_INTERNAL_ERROR,
+} Latex_Render_Status;
+
+typedef struct {
+  Latex_Render_Status status;
+  uint8_t *pdf_data;
+  size_t pdf_size;
+  char *diagnostics;
+} Latex_Render_Result;
+
+Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size);
+void Latex_Render_Result_Destroy(Latex_Render_Result *result);
+
+#endif
--- a/mrjunejune/main.c	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/main.c	Mon Aug 03 16:56:25 2026 -0700
@@ -2,9 +2,11 @@
 #include "markdown_converter/markdown_to_html.h"
 #include "s3/s3_uploader.h"
 #include "deita/deita.h"
+#include "mrjunejune/latex_renderer.h"
 #include <time.h>
 #include <sys/stat.h>
 #include <stdarg.h>
+#include <stdatomic.h>
 #include <pthread.h>
 
 // UUID + /tmp/ + format (max 4)
@@ -13,6 +15,7 @@
 
 volatile sig_atomic_t stop_server = 0;
 static _Atomic uint32_t counter = 0;
+static _Atomic boolean g_latex_rendering = FALSE;
 
 // Media Processing Context for background threads
 typedef struct {
@@ -304,6 +307,106 @@
   return resp;
 }
 
+Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
+  Seobeo_Render_Html_FilePath(final_body, "/tools/latex_editor/index.html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  return resp;
+}
+
+static Seobeo_Request_Entry *LatexErrorResponse(
+    Dowa_Arena *arena,
+    int status,
+    const char *message)
+{
+  Seobeo_Request_Entry *resp = NULL;
+  char *status_value = Dowa_Arena_Allocate(arena, 16);
+  snprintf(status_value, 16, "%d", status);
+  char *body = Dowa_Arena_Allocate(arena, strlen(message) + 1);
+  strcpy(body, message);
+  Dowa_HashMap_Push_Arena(resp, "status", status_value, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", arena);
+  Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", body, arena);
+  return resp;
+}
+
+Seobeo_Request_Entry *RenderLatexPdf(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+  void *length_kv = Dowa_HashMap_Get_Ptr(req, "Content-Length");
+  if (!body_kv || !length_kv)
+    return LatexErrorResponse(arena, 400, "LaTeX source and Content-Length are required.");
+
+  const char *length_value = ((Seobeo_Request_Entry *)length_kv)->value;
+  char *length_end = NULL;
+  errno = 0;
+  unsigned long long parsed_length = strtoull(length_value, &length_end, 10);
+  if (errno != 0 ||
+      length_end == length_value ||
+      *length_end != '\0' ||
+      parsed_length == 0)
+    return LatexErrorResponse(arena, 400, "Content-Length is invalid.");
+  if (parsed_length > LATEX_SOURCE_MAX_BYTES)
+    return LatexErrorResponse(arena, 413, "LaTeX source exceeds the 64 KiB limit.");
+
+  boolean expected = FALSE;
+  if (!atomic_compare_exchange_strong(
+          &g_latex_rendering,
+          &expected,
+          TRUE))
+    return LatexErrorResponse(arena, 429, "The LaTeX compiler is busy. Try again shortly.");
+
+  Latex_Render_Result render = Latex_Render(
+      (const uint8_t *)((Seobeo_Request_Entry *)body_kv)->value,
+      (size_t)parsed_length);
+  atomic_store(&g_latex_rendering, FALSE);
+  if (render.status != LATEX_RENDER_OK)
+  {
+    int status = 500;
+    if (render.status == LATEX_RENDER_INVALID_INPUT)
+      status = 400;
+    else if (render.status == LATEX_RENDER_COMPILE_ERROR)
+      status = 422;
+    else if (render.status == LATEX_RENDER_TIMEOUT)
+      status = 504;
+    else if (render.status == LATEX_RENDER_COMPILER_UNAVAILABLE)
+      status = 503;
+
+    const char *diagnostics = render.diagnostics
+        ? render.diagnostics
+        : "LaTeX rendering failed.";
+    char *body = Dowa_Arena_Allocate(arena, strlen(diagnostics) + 1);
+    strcpy(body, diagnostics);
+    Latex_Render_Result_Destroy(&render);
+    return LatexErrorResponse(arena, status, body);
+  }
+
+  char *pdf = Dowa_Arena_Allocate(arena, render.pdf_size + 1);
+  if (!pdf)
+  {
+    Latex_Render_Result_Destroy(&render);
+    return LatexErrorResponse(arena, 500, "The generated PDF is too large to return.");
+  }
+  memcpy(pdf, render.pdf_data, render.pdf_size);
+  pdf[render.pdf_size] = '\0';
+  char *content_length = Dowa_Arena_Allocate(arena, 32);
+  snprintf(content_length, 32, "%zu", render.pdf_size);
+  Latex_Render_Result_Destroy(&render);
+
+  Seobeo_Request_Entry *resp = NULL;
+  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "application/pdf", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-length", content_length, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-disposition", "inline; filename=\"document.pdf\"", arena);
+  Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena);
+  Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", pdf, arena);
+  return resp;
+}
+
 // Background thread function for media processing
 void *Simple_WebpConverter_Background(void *arg)
 {
@@ -746,6 +849,7 @@
 CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html")
 CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter")
 CREATE_REDIRECT_HANDLER(HlsPlayer, "/tools/hls_player")
+CREATE_REDIRECT_HANDLER(LatexEditor, "/tools/latex_editor")
 CREATE_REDIRECT_HANDLER(Talk, "/talk")
 CREATE_REDIRECT_HANDLER(Editor, "/editor")
 
@@ -1254,28 +1358,26 @@
   // Insert into database
   const char *insert_query =
     "INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) "
-    "VALUES (?, ?, ?, ?, ?, 'pending')";
+    "VALUES (?, ?, ?, ?, ?, 'pending') RETURNING id";
 
   const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed };
-  int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, insert_query, 5, params);
-
-  if (result < 0)
+  Deita_Result_Set *id_result = Deita_Query_Execute_Prepared(
+      g_db_connection,
+      insert_query,
+      5,
+      params,
+      arena);
+  if (!id_result || !Deita_Result_Set_Next(id_result))
   {
+    if (id_result) Deita_Result_Set_Free(id_result);
     Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
     Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
     Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena);
     return resp;
   }
 
-  // Get the inserted media_id using last_insert_rowid()
-  const char *last_id_query = "SELECT last_insert_rowid()";
-  Deita_Result_Set *id_result = Deita_Query_Execute(g_db_connection, last_id_query, arena);
-  int64 media_id = 0;
-  if (id_result && Deita_Result_Set_Next(id_result))
-  {
-    media_id = Deita_Result_Set_Get_Integer(id_result, 0);
-  }
-  if (id_result) Deita_Result_Set_Free(id_result);
+  int64 media_id = Deita_Result_Set_Get_Integer(id_result, 0);
+  Deita_Result_Set_Free(id_result);
 
   // Generate presigned PUT URL
   S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires);
@@ -1868,11 +1970,14 @@
   Seobeo_Router_Register("GET", "/tools/file_converter/index.html", GetRedirectFileConverter);
   Seobeo_Router_Register("GET", "/tools/hls_player", GetHlsPlayer);
   Seobeo_Router_Register("GET", "/tools/hls_player/index.html", GetRedirectHlsPlayer);
+  Seobeo_Router_Register("GET", "/tools/latex_editor", GetLatexEditor);
+  Seobeo_Router_Register("GET", "/tools/latex_editor/index.html", GetRedirectLatexEditor);
 
   // -- File converter --/
   Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP);
   Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4);
   Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile);
+  Seobeo_Router_Register("POST", "/api/latex/render", RenderLatexPdf);
 
   // -- S3 Upload --/
   Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl);
@@ -1909,5 +2014,5 @@
   const char *server_port = getenv("MRJUNEJUNE_PORT");
   if (!server_port || server_port[0] == '\0')
     server_port = "6969";
-  Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 1);
+  Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4);
 }
--- a/mrjunejune/src/tools/index.html	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/src/tools/index.html	Mon Aug 03 16:56:25 2026 -0700
@@ -12,12 +12,9 @@
          <li><a href="/tools/markdown_to_html">MarkDown to HTML</a></li>
          <li><a href="/tools/file_converter">Images to Webp / Video to Mp4</a></li>
          <li><a href="/tools/hls_player">Online HLS Player</a></li>
+         <li><a href="/tools/latex_editor">Online LaTeX Editor</a></li>
          <li><a href="/notes">Personal Notes</a></li>
        </ul>
-       <h3> TODOs </h3>
-       <ul class="nav-list">
-         <li>- Simple online LaTex editor.</li>
-       </ul>
      </main>
      {{/parts/footer.html}}
   </body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/tools/latex_editor/index.css	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,195 @@
+.latex-page {
+  width: min(1500px, calc(100% - 2rem));
+  max-width: none;
+  margin: 1.5rem auto 3rem;
+}
+
+.latex-heading {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  gap: 1.5rem;
+  margin-bottom: 1rem;
+}
+
+.latex-heading h1,
+.latex-heading p {
+  margin-bottom: 0.4rem;
+}
+
+.latex-actions {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  flex-wrap: wrap;
+  gap: 0.65rem;
+}
+
+.latex-actions button,
+.button-link {
+  min-height: 42px;
+  padding: 0.65rem 1rem;
+  border: 2px solid rgb(var(--black));
+  border-radius: 8px;
+  background: var(--awesome);
+  color: rgb(var(--black));
+  font: inherit;
+  font-weight: 700;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.latex-actions button.secondary {
+  background: transparent;
+}
+
+.latex-actions button:disabled,
+.button-link.disabled {
+  opacity: 0.45;
+  cursor: not-allowed;
+  pointer-events: none;
+}
+
+.auto-compile {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.4rem;
+  white-space: nowrap;
+}
+
+.latex-status {
+  min-height: 1.5em;
+  margin: 0.5rem 0 1rem;
+  font-weight: 700;
+}
+
+.latex-status[data-state="working"] {
+  color: var(--orange);
+}
+
+.latex-status[data-state="ready"] {
+  color: var(--blue);
+}
+
+.latex-status[data-state="error"] {
+  color: var(--red);
+}
+
+.latex-workspace {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+  gap: 1rem;
+  min-height: 72vh;
+}
+
+.latex-panel {
+  min-width: 0;
+  overflow: hidden;
+  border: 2px solid rgb(var(--black));
+  border-radius: 12px;
+  background: var(--white);
+  box-shadow: 6px 6px 0 rgba(var(--black), 0.12);
+}
+
+.panel-heading {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 1rem;
+  min-height: 48px;
+  padding: 0.55rem 0.8rem;
+  border-bottom: 2px solid rgb(var(--black));
+  background: var(--gray-gradient);
+}
+
+.panel-heading h2,
+.panel-heading span {
+  margin: 0;
+  font-size: 0.95rem;
+}
+
+#latexSource {
+  display: block;
+  width: 100%;
+  height: calc(72vh - 50px);
+  min-height: 540px;
+  resize: vertical;
+  box-sizing: border-box;
+  padding: 1rem;
+  border: 0;
+  outline: 0;
+  background: rgb(var(--black));
+  color: var(--white);
+  font: 15px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+  tab-size: 2;
+}
+
+#pdfPreview {
+  display: block;
+  width: 100%;
+  height: calc(72vh - 50px);
+  min-height: 540px;
+  border: 0;
+  background: #525659;
+}
+
+.latex-diagnostics {
+  width: 100%;
+  height: calc(72vh - 50px);
+  min-height: 540px;
+  box-sizing: border-box;
+  overflow: auto;
+  margin: 0;
+  padding: 1rem;
+  white-space: pre-wrap;
+  background: #1e1e1e;
+  color: #ffb4ab;
+  font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+.latex-notes {
+  margin-top: 1.5rem;
+}
+
+.latex-notes h2,
+.latex-notes p {
+  margin-bottom: 0.4rem;
+}
+
+@media (max-width: 900px) {
+  .latex-heading {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .latex-actions {
+    justify-content: flex-start;
+  }
+
+  .latex-workspace {
+    grid-template-columns: 1fr;
+  }
+
+  #latexSource,
+  #pdfPreview,
+  .latex-diagnostics {
+    height: 65vh;
+    min-height: 480px;
+  }
+}
+
+@media (max-width: 520px) {
+  .latex-page {
+    width: min(100% - 1rem, 1500px);
+  }
+
+  .latex-actions {
+    align-items: stretch;
+  }
+
+  .latex-actions button,
+  .button-link {
+    flex: 1 1 45%;
+    text-align: center;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/tools/latex_editor/index.html	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,83 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    {{/parts/base_head.html}}
+    <title>LaTeX Editor - MrJuneJune</title>
+    <link rel="stylesheet" href="/tools/latex_editor/index.css" />
+    <script src="/tools/latex_editor/index.js" defer></script>
+  </head>
+  <body>
+    {{/parts/header.html}}
+    <main class="latex-page">
+      <div class="latex-heading">
+        <div>
+          <h1>Online LaTeX Editor</h1>
+          <p>Write LaTeX here. My server compiles it inside a locked-down sandbox and sends the PDF back to this preview.</p>
+        </div>
+        <div class="latex-actions">
+          <label class="auto-compile">
+            <input id="autoCompile" type="checkbox" checked />
+            Auto compile
+          </label>
+          <button id="resetButton" type="button" class="secondary">Reset</button>
+          <button id="compileButton" type="button">Compile PDF</button>
+          <a id="downloadButton" class="button-link disabled" aria-disabled="true">Download</a>
+        </div>
+      </div>
+
+      <p id="latexStatus" class="latex-status" role="status" aria-live="polite">
+        Preparing your first PDF...
+      </p>
+
+      <div class="latex-workspace">
+        <section class="latex-panel editor-panel" aria-labelledby="sourceHeading">
+          <div class="panel-heading">
+            <h2 id="sourceHeading">document.tex</h2>
+            <span id="sourceSize">0 / 65,536 bytes</span>
+          </div>
+          <textarea id="latexSource" spellcheck="false" autocomplete="off" aria-label="LaTeX source">\documentclass[11pt]{article}
+\usepackage[margin=1in]{geometry}
+\usepackage{amsmath}
+\usepackage{xcolor}
+
+\title{Hello from my server}
+\author{MrJuneJune}
+\date{\today}
+
+\begin{document}
+\maketitle
+
+This PDF was compiled by the custom C server behind this website.
+
+\[
+  e^{i\pi} + 1 = 0
+\]
+
+\textcolor{blue}{Edit this document and watch the preview update.}
+
+\end{document}
+</textarea>
+        </section>
+
+        <section class="latex-panel preview-panel" aria-labelledby="previewHeading">
+          <div class="panel-heading">
+            <h2 id="previewHeading">PDF preview</h2>
+            <span>Server rendered</span>
+          </div>
+          <iframe
+            id="pdfPreview"
+            title="Compiled LaTeX PDF preview"
+            src="about:blank"
+          ></iframe>
+          <pre id="latexDiagnostics" class="latex-diagnostics" hidden></pre>
+        </section>
+      </div>
+
+      <section class="latex-notes">
+        <h2>Limits</h2>
+        <p>Source is capped at 64 KiB and compilation at 8 seconds. Shell commands, network access, and reads outside the isolated TeX workspace are blocked.</p>
+      </section>
+    </main>
+    {{/parts/footer.html}}
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/tools/latex_editor/index.js	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,200 @@
+(function latexEditor() {
+  "use strict";
+
+  const SOURCE_LIMIT = 64 * 1024;
+  const STORAGE_KEY = "mrjunejune-latex-source";
+  const AUTO_COMPILE_DELAY = 700;
+
+  window.addEventListener("DOMContentLoaded", () => {
+    const source = document.querySelector("#latexSource");
+    const compileButton = document.querySelector("#compileButton");
+    const resetButton = document.querySelector("#resetButton");
+    const autoCompile = document.querySelector("#autoCompile");
+    const status = document.querySelector("#latexStatus");
+    const sourceSize = document.querySelector("#sourceSize");
+    const preview = document.querySelector("#pdfPreview");
+    const diagnostics = document.querySelector("#latexDiagnostics");
+    const download = document.querySelector("#downloadButton");
+    if (!source ||
+        !compileButton ||
+        !resetButton ||
+        !autoCompile ||
+        !status ||
+        !sourceSize ||
+        !preview ||
+        !diagnostics ||
+        !download) return;
+
+    const defaultSource = source.value;
+    const savedSource = localStorage.getItem(STORAGE_KEY);
+    if (savedSource) source.value = savedSource;
+
+    let currentPdfUrl = null;
+    let compileTimer = null;
+    let compileGeneration = 0;
+    let controller = null;
+
+    const byteLength = value => new TextEncoder().encode(value).byteLength;
+
+    const setStatus = (message, state) => {
+      status.textContent = message;
+      status.dataset.state = state;
+    };
+
+    const updateSize = () => {
+      const size = byteLength(source.value);
+      sourceSize.textContent = `${size.toLocaleString()} / ${SOURCE_LIMIT.toLocaleString()} bytes`;
+      sourceSize.dataset.overLimit = size > SOURCE_LIMIT ? "true" : "false";
+      return size;
+    };
+
+    const clearPdf = () => {
+      if (currentPdfUrl) URL.revokeObjectURL(currentPdfUrl);
+      currentPdfUrl = null;
+      preview.src = "about:blank";
+      download.removeAttribute("href");
+      download.removeAttribute("download");
+      download.classList.add("disabled");
+      download.setAttribute("aria-disabled", "true");
+    };
+
+    const showDiagnostics = message => {
+      clearPdf();
+      diagnostics.textContent = message;
+      diagnostics.hidden = false;
+      preview.hidden = true;
+    };
+
+    const showPdf = blob => {
+      if (currentPdfUrl) URL.revokeObjectURL(currentPdfUrl);
+      currentPdfUrl = URL.createObjectURL(blob);
+      diagnostics.hidden = true;
+      preview.hidden = false;
+      preview.src = currentPdfUrl;
+      download.href = currentPdfUrl;
+      download.download = "document.pdf";
+      download.classList.remove("disabled");
+      download.setAttribute("aria-disabled", "false");
+    };
+
+    const compile = async () => {
+      clearTimeout(compileTimer);
+      const generation = ++compileGeneration;
+      if (controller) controller.abort();
+      controller = null;
+      compileButton.disabled = false;
+      const text = source.value;
+      const size = updateSize();
+      if (!text.trim()) {
+        showDiagnostics("Write some LaTeX before compiling.");
+        setStatus("Nothing to compile.", "error");
+        return;
+      }
+      if (size > SOURCE_LIMIT) {
+        showDiagnostics("The server accepts at most 64 KiB of LaTeX source.");
+        setStatus("Source is too large.", "error");
+        return;
+      }
+
+      controller = new AbortController();
+      compileButton.disabled = true;
+      setStatus("Compiling on the server...", "working");
+
+      try {
+        const response = await fetch("/api/latex/render", {
+          method: "POST",
+          headers: {
+            "Content-Type": "text/plain; charset=utf-8",
+          },
+          body: text,
+          cache: "no-store",
+          signal: controller.signal,
+        });
+        if (generation !== compileGeneration) return;
+
+        if (!response.ok) {
+          const message = await response.text();
+          showDiagnostics(message || `Compilation failed (${response.status}).`);
+          setStatus("Fix the LaTeX errors shown in the preview pane.", "error");
+          return;
+        }
+
+        const blob = await response.blob();
+        if (blob.type !== "application/pdf" || blob.size < 5) {
+          throw new Error("The server returned an invalid PDF.");
+        }
+        showPdf(blob);
+        setStatus(
+          `PDF ready (${Math.ceil(blob.size / 1024).toLocaleString()} KiB).`,
+          "ready",
+        );
+      } catch (error) {
+        if (error.name === "AbortError" || generation !== compileGeneration) return;
+        showDiagnostics(error.message || "Unable to compile this document.");
+        setStatus("The PDF request failed.", "error");
+      } finally {
+        if (generation === compileGeneration) {
+          compileButton.disabled = false;
+          controller = null;
+        }
+      }
+    };
+
+    const queueCompile = () => {
+      clearTimeout(compileTimer);
+      if (!autoCompile.checked) return;
+      compileTimer = setTimeout(compile, AUTO_COMPILE_DELAY);
+    };
+
+    const invalidateCompile = () => {
+      compileGeneration++;
+      if (controller) controller.abort();
+      controller = null;
+      compileButton.disabled = false;
+    };
+
+    source.addEventListener("input", () => {
+      localStorage.setItem(STORAGE_KEY, source.value);
+      invalidateCompile();
+      const size = updateSize();
+      if (!source.value.trim()) {
+        showDiagnostics("Write some LaTeX before compiling.");
+        setStatus("Nothing to compile.", "error");
+        return;
+      }
+      if (size > SOURCE_LIMIT) {
+        showDiagnostics("The server accepts at most 64 KiB of LaTeX source.");
+        setStatus("Source is too large.", "error");
+        return;
+      }
+      setStatus(
+        autoCompile.checked ? "Waiting to compile..." : "Changes ready to compile.",
+        "working",
+      );
+      queueCompile();
+    });
+    source.addEventListener("keydown", event => {
+      if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
+        event.preventDefault();
+        compile();
+      }
+    });
+    compileButton.addEventListener("click", compile);
+    resetButton.addEventListener("click", () => {
+      source.value = defaultSource;
+      localStorage.removeItem(STORAGE_KEY);
+      updateSize();
+      compile();
+    });
+    autoCompile.addEventListener("change", () => {
+      if (autoCompile.checked) queueCompile();
+    });
+    window.addEventListener("beforeunload", () => {
+      if (controller) controller.abort();
+      if (currentPdfUrl) URL.revokeObjectURL(currentPdfUrl);
+    });
+
+    updateSize();
+    compile();
+  });
+})();
--- a/mrjunejune/test/BUILD	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/test/BUILD	Mon Aug 03 16:56:25 2026 -0700
@@ -48,6 +48,15 @@
   args = ["$(location //mrjunejune:mrjunejune_server)"],
 )
 
+cc_test(
+  name = "latex_renderer_test",
+  srcs = ["latex_renderer_test.c"],
+  deps = ["//mrjunejune:latex_renderer"],
+  size = "medium",
+  timeout = "moderate",
+  target_compatible_with = ["@platforms//os:linux"],
+)
+
 js_test(
   name = "hls_player_test",
   entry_point = "hls_player_test.js",
@@ -57,6 +66,27 @@
 )
 
 js_test(
+  name = "latex_editor_test",
+  entry_point = "latex_editor_test.js",
+  data = [
+    "//hg-web/e2e:node_modules/playwright-core",
+    "//mrjunejune:mrjunejune_server",
+    "@playwright_chromium_linux//:chrome",
+    "@playwright_chromium_linux//:chromium",
+  ],
+  env = {
+    "CHROMIUM_PATH": "$(rootpath @playwright_chromium_linux//:chrome)",
+  },
+  no_copy_to_bin = ["@playwright_chromium_linux//:chromium"],
+  size = "large",
+  target_compatible_with = [
+    "@platforms//cpu:x86_64",
+    "@platforms//os:linux",
+  ],
+  timeout = "long",
+)
+
+js_test(
   name = "theme_and_webp_test",
   entry_point = "theme_and_webp_test.js",
   data = [
--- a/mrjunejune/test/auto_generated_test.c	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/test/auto_generated_test.c	Mon Aug 03 16:56:25 2026 -0700
@@ -30,6 +30,8 @@
     {"/tools/file_converter/index.html", 301, SNAPSHOT_DIR, TEST_HOST, TEST_PORT},
     {"/tools/hls_player", 200, SNAPSHOT_DIR, TEST_HOST, TEST_PORT},
     {"/tools/hls_player/index.html", 301, SNAPSHOT_DIR, TEST_HOST, TEST_PORT},
+    {"/tools/latex_editor", 200, SNAPSHOT_DIR, TEST_HOST, TEST_PORT},
+    {"/tools/latex_editor/index.html", 301, SNAPSHOT_DIR, TEST_HOST, TEST_PORT},
     // TODO: POST route - POST /api/convert/image-to-webp - requires request body
     // TODO: POST route - POST /api/convert/video-to-mp4 - requires request body
     // TODO: Dynamic route - GET /api/download/:filename - fill in actual path
@@ -40,7 +42,28 @@
   };
 
   int count = sizeof(configs) / sizeof(configs[0]);
-  int result = Seobeo_Snapshots_Create_Batch(configs, count);
+  int result = 0;
+  if (argc > 2)
+  {
+    int found = 0;
+    for (int i = 0; i < count; i++)
+    {
+      if (strcmp(configs[i].path, argv[2]) != 0)
+        continue;
+      result = Seobeo_Snapshot_Create(&configs[i]);
+      found = 1;
+      break;
+    }
+    if (!found)
+    {
+      fprintf(stderr, "No snapshot route configured for %s\n", argv[2]);
+      result = 1;
+    }
+  }
+  else
+  {
+    result = Seobeo_Snapshots_Create_Batch(configs, count);
+  }
 
   stop_test_server(server_pid);
   return result;
--- a/mrjunejune/test/integration_test.c	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/test/integration_test.c	Mon Aug 03 16:56:25 2026 -0700
@@ -392,6 +392,7 @@
     {"/tools/markdown_to_html", 200, NULL, NULL, NULL, 0},
     {"/tools/file_converter", 200, NULL, NULL, NULL, 0},
     {"/tools/hls_player", 200, NULL, NULL, NULL, 0},
+    {"/tools/latex_editor", 200, NULL, NULL, NULL, 0},
     {"/talk", 200, NULL, NULL, NULL, 0},
   };
   int num_success_tests = sizeof(success_tests) / sizeof(success_tests[0]);
@@ -403,6 +404,7 @@
     {"/tools/markdown_to_html/index.html", 301, NULL, NULL, NULL, 0},
     {"/tools/file_converter/index.html", 301, NULL, NULL, NULL, 0},
     {"/tools/hls_player/index.html", 301, NULL, NULL, NULL, 0},
+    {"/tools/latex_editor/index.html", 301, NULL, NULL, NULL, 0},
   };
   int num_redirect_tests = sizeof(redirect_tests) / sizeof(redirect_tests[0]);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/test/latex_editor_test.js	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,298 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const http = require('node:http');
+const net = require('node:net');
+const path = require('node:path');
+const { spawn } = require('node:child_process');
+
+const RUNFILES = process.env.JS_BINARY__RUNFILES;
+const WORKSPACE = process.env.JS_BINARY__WORKSPACE;
+const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
+const playwrightPath = path.join(
+  runfilesWorkspace,
+  'hg-web/e2e/node_modules/playwright-core',
+);
+const { chromium } = require(playwrightPath);
+
+function findFreePort() {
+  return new Promise((resolve, reject) => {
+    const socket = net.createServer();
+    socket.once('error', reject);
+    socket.listen(0, '127.0.0.1', () => {
+      const address = socket.address();
+      socket.close(error => {
+        if (error) reject(error);
+        else resolve(String(address.port));
+      });
+    });
+  });
+}
+
+async function waitForServer(server, baseUrl, logs) {
+  const deadline = Date.now() + 15000;
+  while (Date.now() < deadline) {
+    if (server.exitCode !== null) {
+      throw new Error(`Server exited with ${server.exitCode}\n${logs.join('')}`);
+    }
+    try {
+      const response = await fetch(baseUrl);
+      if (response.ok) return;
+    } catch {
+      // Keep waiting for startup.
+    }
+    await new Promise(resolve => setTimeout(resolve, 100));
+  }
+  throw new Error(`Server startup timed out\n${logs.join('')}`);
+}
+
+async function stopProcess(child) {
+  if (!child || child.exitCode !== null) return;
+  child.kill('SIGTERM');
+  await new Promise(resolve => {
+    const timer = setTimeout(() => {
+      if (child.exitCode === null) child.kill('SIGKILL');
+    }, 3000);
+    child.once('exit', () => {
+      clearTimeout(timer);
+      resolve();
+    });
+  });
+}
+
+async function render(baseUrl, source) {
+  return fetch(`${baseUrl}/api/latex/render`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'text/plain; charset=utf-8' },
+    body: source,
+  });
+}
+
+function requestWithoutReuse(baseUrl, method, requestPath, body = '') {
+  const url = new URL(requestPath, baseUrl);
+  return new Promise((resolve, reject) => {
+    const request = http.request({
+      host: url.hostname,
+      port: url.port,
+      path: url.pathname,
+      method,
+      agent: false,
+      headers: {
+        Connection: 'close',
+        'Content-Type': 'text/plain; charset=utf-8',
+        'Content-Length': Buffer.byteLength(body),
+      },
+    }, response => {
+      const chunks = [];
+      response.on('data', chunk => chunks.push(chunk));
+      response.on('end', () => resolve({
+        status: response.statusCode,
+        body: Buffer.concat(chunks),
+      }));
+    });
+    request.on('error', reject);
+    request.end(body);
+  });
+}
+
+(async () => {
+  assert.ok(RUNFILES);
+  assert.ok(WORKSPACE);
+  const serverBinary = path.join(
+    runfilesWorkspace,
+    'mrjunejune/mrjunejune_server',
+  );
+  const chromiumPath = path.resolve(process.env.CHROMIUM_PATH);
+  const port = await findFreePort();
+  const baseUrl = `http://127.0.0.1:${port}`;
+  const logs = [];
+  let server;
+  let browser;
+
+  try {
+    server = spawn(serverBinary, [], {
+      cwd: runfilesWorkspace,
+      env: {
+        ...process.env,
+        MRJUNEJUNE_PORT: port,
+      },
+      stdio: ['ignore', 'pipe', 'pipe'],
+    });
+    server.stdout.on('data', chunk => logs.push(chunk.toString()));
+    server.stderr.on('data', chunk => logs.push(chunk.toString()));
+    await waitForServer(server, baseUrl, logs);
+
+    const valid = String.raw`\documentclass{article}
+\begin{document}
+Hello from the API. $a^2 + b^2 = c^2$
+\end{document}
+`;
+    let response = await render(baseUrl, valid);
+assert.equal(response.status, 200, await response.clone().text());
+    assert.match(response.headers.get('content-type') || '', /^application\/pdf/);
+    assert.equal(response.headers.get('cache-control'), 'no-store');
+    const pdf = Buffer.from(await response.arrayBuffer());
+    assert.ok(pdf.length > 1000);
+    assert.equal(pdf.subarray(0, 5).toString(), '%PDF-');
+
+    response = await render(
+      baseUrl,
+      String.raw`\documentclass{article}\begin{document}\badcommand\end{document}`,
+    );
+    assert.equal(response.status, 422);
+    assert.match(await response.text(), /Undefined control sequence/);
+
+    response = await render(baseUrl, 'x'.repeat(64 * 1024 + 1));
+    assert.equal(response.status, 413);
+    assert.match(await response.text(), /64 KiB/);
+
+    response = await render(
+      baseUrl,
+      String.raw`\documentclass{article}\begin{document}\input{/etc/passwd}\end{document}`,
+    );
+    assert.equal(response.status, 422);
+    assert.doesNotMatch(await response.text(), /root:x:/);
+
+    const shellPath = `/tmp/mrjunejune-latex-browser-${process.pid}`;
+    response = await render(
+      baseUrl,
+      String.raw`\documentclass{article}
+\begin{document}
+\immediate\write18{touch ${shellPath}}
+Shell escape stays disabled.
+\end{document}`,
+    );
+    assert.equal(response.status, 200);
+    assert.equal(fs.existsSync(shellPath), false);
+
+    const infiniteRender = requestWithoutReuse(
+      baseUrl,
+      'POST',
+      '/api/latex/render',
+      String.raw`\documentclass{article}\begin{document}\loop\iftrue\repeat\end{document}`,
+    );
+    await new Promise(resolve => setTimeout(resolve, 200));
+    const healthStarted = Date.now();
+    let isolatedResponse = await requestWithoutReuse(baseUrl, 'GET', '/tools');
+    assert.equal(isolatedResponse.status, 200);
+    assert.ok(Date.now() - healthStarted < 1500);
+    isolatedResponse = await requestWithoutReuse(
+      baseUrl,
+      'POST',
+      '/api/latex/render',
+      valid,
+    );
+    assert.equal(isolatedResponse.status, 429);
+    assert.match(isolatedResponse.body.toString(), /compiler is busy/);
+    isolatedResponse = await infiniteRender;
+    assert.equal(isolatedResponse.status, 504);
+    assert.match(
+      isolatedResponse.body.toString(),
+      /CPU or memory limit|8 second limit/,
+    );
+
+    browser = await chromium.launch({
+      executablePath: chromiumPath,
+      headless: true,
+      args: ['--no-sandbox'],
+    });
+    const context = await browser.newContext();
+    await context.addInitScript(() => {
+      window.__pdfUrls = { created: [], revoked: [] };
+      const createObjectURL = URL.createObjectURL.bind(URL);
+      const revokeObjectURL = URL.revokeObjectURL.bind(URL);
+      URL.createObjectURL = value => {
+        const url = createObjectURL(value);
+        window.__pdfUrls.created.push(url);
+        return url;
+      };
+      URL.revokeObjectURL = url => {
+        window.__pdfUrls.revoked.push(url);
+        revokeObjectURL(url);
+      };
+    });
+    const page = await context.newPage();
+    const browserErrors = [];
+    let testingExpectedFailure = false;
+    page.on('pageerror', error => browserErrors.push(error.message));
+    page.on('console', message => {
+      if (message.type() !== 'error') return;
+      if (testingExpectedFailure &&
+          message.text().startsWith('Failed to load resource:')) return;
+      browserErrors.push(message.text());
+    });
+
+    await page.goto(`${baseUrl}/tools/latex_editor`, {
+      waitUntil: 'networkidle',
+    });
+    await page.locator('#latexStatus[data-state="ready"]').waitFor({
+      timeout: 15000,
+    });
+    assert.match(
+      await page.locator('#pdfPreview').getAttribute('src'),
+      /^blob:/,
+    );
+    assert.equal(
+      await page.locator('#downloadButton').getAttribute('download'),
+      'document.pdf',
+    );
+
+    await page.locator('#autoCompile').uncheck();
+    await page.locator('#latexSource').fill(
+      String.raw`\documentclass{article}\begin{document}\badcommand\end{document}`,
+    );
+    testingExpectedFailure = true;
+    await page.getByRole('button', { name: 'Compile PDF' }).click();
+    await page.locator('#latexStatus[data-state="error"]').waitFor();
+    testingExpectedFailure = false;
+    assert.match(
+      await page.locator('#latexDiagnostics').textContent(),
+      /Undefined control sequence/,
+    );
+
+    await page.locator('#latexSource').fill(valid);
+    await page.getByRole('button', { name: 'Compile PDF' }).click();
+    await page.locator('#latexStatus[data-state="ready"]').waitFor();
+    const objectUrls = await page.evaluate(() => window.__pdfUrls);
+    assert.ok(objectUrls.created.length >= 2, JSON.stringify(objectUrls));
+    assert.ok(objectUrls.revoked.length >= 1, JSON.stringify(objectUrls));
+
+    let delayNextRender = true;
+    await page.route('**/api/latex/render', async route => {
+      if (!delayNextRender) {
+        await route.continue();
+        return;
+      }
+      delayNextRender = false;
+      await new Promise(resolve => setTimeout(resolve, 300));
+      try {
+        await route.fulfill({
+          status: 200,
+          contentType: 'application/pdf',
+          body: pdf,
+        });
+      } catch {
+        // The corrected client aborts this stale request.
+      }
+    });
+    await page.locator('#latexSource').fill(valid);
+    await page.getByRole('button', { name: 'Compile PDF' }).click();
+    await page.locator('#latexStatus[data-state="working"]').waitFor();
+    await page.locator('#latexSource').fill('x'.repeat(64 * 1024 + 1));
+    await page.locator('#latexStatus[data-state="error"]').waitFor();
+    await page.waitForTimeout(500);
+    assert.equal(
+      await page.locator('#latexStatus').getAttribute('data-state'),
+      'error',
+    );
+    await page.unroute('**/api/latex/render');
+
+    assert.deepEqual(browserErrors, []);
+    await context.close();
+  } finally {
+    if (browser) await browser.close();
+    await stopProcess(server);
+  }
+})().catch(error => {
+  console.error(error.stack || error);
+  process.exitCode = 1;
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/test/latex_renderer_test.c	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,105 @@
+#include "mrjunejune/latex_renderer.h"
+
+#include <assert.h>
+#include <dirent.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+static size_t latex_workspace_count(void)
+{
+  DIR *directory = opendir("/tmp");
+  assert(directory);
+  size_t count = 0;
+  struct dirent *entry;
+  while ((entry = readdir(directory)) != NULL)
+  {
+    if (strncmp(entry->d_name, "mrjunejune-latex-", 19) == 0)
+      count++;
+  }
+  closedir(directory);
+  return count;
+}
+
+int main(void)
+{
+  size_t initial_workspaces = latex_workspace_count();
+
+  const char *valid =
+      "\\documentclass{article}\n"
+      "\\begin{document}\n"
+      "Hello from the sandbox. $x^2 + y^2 = z^2$\n"
+      "\\end{document}\n";
+  Latex_Render_Result result = Latex_Render(
+      (const uint8_t *)valid,
+      strlen(valid));
+  if (result.status != LATEX_RENDER_OK)
+    fprintf(stderr, "%s\n", result.diagnostics ? result.diagnostics : "unknown error");
+  assert(result.status == LATEX_RENDER_OK);
+  assert(result.pdf_size > 1000);
+  assert(memcmp(result.pdf_data, "%PDF-", 5) == 0);
+  Latex_Render_Result_Destroy(&result);
+
+  const char *invalid =
+      "\\documentclass{article}\n"
+      "\\begin{document}\n"
+      "\\notarealcommand\n"
+      "\\end{document}\n";
+  result = Latex_Render((const uint8_t *)invalid, strlen(invalid));
+  assert(result.status == LATEX_RENDER_COMPILE_ERROR);
+  assert(result.diagnostics);
+  assert(strstr(result.diagnostics, "Undefined control sequence"));
+  Latex_Render_Result_Destroy(&result);
+
+  const char *host_read =
+      "\\documentclass{article}\n"
+      "\\begin{document}\n"
+      "\\input{/etc/passwd}\n"
+      "\\end{document}\n";
+  result = Latex_Render((const uint8_t *)host_read, strlen(host_read));
+  assert(result.status == LATEX_RENDER_COMPILE_ERROR);
+  assert(result.diagnostics);
+  assert(!strstr(result.diagnostics, "root:x:"));
+  Latex_Render_Result_Destroy(&result);
+
+  char shell_path[128];
+  snprintf(
+      shell_path,
+      sizeof(shell_path),
+      "/tmp/mrjunejune-latex-shell-%ld",
+      (long)getpid());
+  char shell_escape[512];
+  snprintf(
+      shell_escape,
+      sizeof(shell_escape),
+      "\\documentclass{article}\n"
+      "\\begin{document}\n"
+      "\\immediate\\write18{touch %s}\n"
+      "Shell escape must stay disabled.\n"
+      "\\end{document}\n",
+      shell_path);
+  result = Latex_Render(
+      (const uint8_t *)shell_escape,
+      strlen(shell_escape));
+  assert(result.status == LATEX_RENDER_OK);
+  assert(access(shell_path, F_OK) != 0);
+  Latex_Render_Result_Destroy(&result);
+
+  uint8_t oversized[LATEX_SOURCE_MAX_BYTES + 1] = {0};
+  memset(oversized, 'x', sizeof(oversized));
+  result = Latex_Render(oversized, sizeof(oversized));
+  assert(result.status == LATEX_RENDER_INVALID_INPUT);
+  Latex_Render_Result_Destroy(&result);
+
+  const char *infinite =
+      "\\documentclass{article}\n"
+      "\\begin{document}\n"
+      "\\loop\\iftrue\\repeat\n"
+      "\\end{document}\n";
+  result = Latex_Render((const uint8_t *)infinite, strlen(infinite));
+  assert(result.status == LATEX_RENDER_TIMEOUT);
+  Latex_Render_Result_Destroy(&result);
+
+  assert(latex_workspace_count() == initial_workspaces);
+  return 0;
+}
--- a/mrjunejune/test/snapshots/tools.snapshot	Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/test/snapshots/tools.snapshot	Mon Aug 03 16:56:25 2026 -0700
@@ -172,12 +172,9 @@
          <li><a href="/tools/markdown_to_html">MarkDown to HTML</a></li>
          <li><a href="/tools/file_converter">Images to Webp / Video to Mp4</a></li>
          <li><a href="/tools/hls_player">Online HLS Player</a></li>
+         <li><a href="/tools/latex_editor">Online LaTeX Editor</a></li>
          <li><a href="/notes">Personal Notes</a></li>
        </ul>
-       <h3> TODOs </h3>
-       <ul class="nav-list">
-         <li>- Simple online LaTex editor.</li>
-       </ul>
      </main>
      <div style="display: flex; align-items: center; justify-content: center; margin: 30px 0px;">
   <small>&copy; 2026 June Park</small>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/test/snapshots/tools_latex_editor.snapshot	Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,246 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#544e43">
+<link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
+
+<link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
+<link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
+
+<!-- <link rel="preload" href="/public/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin> -->
+<!-- <link rel="preload" href="/public/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin> -->
+
+<!-- <link rel="preload" href="/public/fonts/more-sugar.extras.otf" as="font" type="font/otf" crossorigin> -->
+<link rel="preload" href="/public/fonts/more-sugar.regular.otf" as="font" type="font/otf" crossorigin>
+<link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
+<link rel="preload" href="/public/epi_all_colors.svg" as="image">
+
+<link rel="preload" href="/base.css" as="style" />
+<link rel="stylesheet" href="/base.css" />
+
+<script src="/public/pwa-register.js" defer></script>
+
+
+    <title>LaTeX Editor - MrJuneJune</title>
+    <link rel="stylesheet" href="/tools/latex_editor/index.css" />
+    <script src="/tools/latex_editor/index.js" defer></script>
+  </head>
+  <body>
+    <style>
+  :root {
+    --header-background: var(--white);
+    --header-color: rgb(var(--black));
+    --link-hover-accent: var(--awesome);
+  }
+
+  /* Fixed icon in top left corner */
+  #themeToggle {
+    position: fixed;
+    top: 20px;
+    left: 20px;
+    background: var(--header-background);
+    display: flex;
+    align-items: center;
+    border-radius: 50%;
+    cursor: pointer;
+    z-index: 1000;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    transition: transform 0.2s ease;
+  }
+
+  #themeToggle:hover {
+    transform: scale(1.05);
+  }
+
+  /* Professional header */
+  header {
+    margin: auto;
+    padding: 1.5em 1em;
+    font-family: "More", sans-serif;
+    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    width: 720px;
+    max-width: calc(100% - 2em);
+    text-align: center;
+  }
+
+  header h1 {
+    margin: 0;
+    font-size: 1.8em;
+    font-weight: 700;
+    letter-spacing: -0.5px;
+  }
+
+  header h1 a {
+    text-decoration: none;
+    color: var(--header-color);
+  }
+
+  header h1 a::before {
+    display: none;
+  }
+
+  /* Mobile responsiveness */
+  @media (max-width: 720px) {
+    #themeToggle {
+      top: 15px;
+      left: 15px;
+    }
+
+    header {
+      padding: 1em;
+    }
+
+    header h1 {
+      font-size: 1.5em;
+    }
+  }
+
+  @media (max-width: 480px) {
+    #themeToggle {
+      top: 10px;
+      left: 10px;
+    }
+
+    #themeToggle img {
+      height: 40px;
+      width: 40px;
+    }
+
+    header h1 {
+      font-size: 1.3em;
+    }
+  }
+
+  #logo {
+    width: 300px;
+  }
+
+  /* 1. DEFINE THE DEFAULTS (Light Mode) */
+  :root {
+    --logo-invert: invert(0);
+    --epi-grayscale: grayscale(0) brightness(1);
+  }
+  
+  /* 2. MANUAL DARK OVERRIDE */
+  html.dark {
+    --logo-invert: invert(1);
+    --epi-grayscale: grayscale(1);
+  }
+  
+  /* 3. MANUAL LIGHT OVERRIDE */
+  html.light-mode {
+    --logo-invert: invert(0);
+    --epi-grayscale: brightness(2.9) grayscale(1);
+  }
+  
+  /* 4. SYSTEM PREFERENCE */
+  @media (prefers-color-scheme: dark) {
+    :root:not(.light-mode) {
+      --logo-invert: invert(1);
+    }
+  }
+  
+  /* 5. APPLY TO ELEMENTS */
+  #logo {
+    -webkit-filter: var(--logo-invert);
+    filter: var(--logo-invert);
+    transition: filter 0.3s ease;
+  }
+  
+  .epi-logo {
+    -webkit-filter: var(--epi-grayscale);
+    filter: var(--epi-grayscale);
+    transition: filter 0.3s ease;
+  }
+</style>
+
+<div id="themeToggle">
+  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
+</div>
+
+<header>
+  <h1><a href="/">MrJuneJune</a></h1>
+</header>
+<script src="/index.js"></script>
+
+
+    <main class="latex-page">
+      <div class="latex-heading">
+        <div>
+          <h1>Online LaTeX Editor</h1>
+          <p>Write LaTeX here. My server compiles it inside a locked-down sandbox and sends the PDF back to this preview.</p>
+        </div>
+        <div class="latex-actions">
+          <label class="auto-compile">
+            <input id="autoCompile" type="checkbox" checked />
+            Auto compile
+          </label>
+          <button id="resetButton" type="button" class="secondary">Reset</button>
+          <button id="compileButton" type="button">Compile PDF</button>
+          <a id="downloadButton" class="button-link disabled" aria-disabled="true">Download</a>
+        </div>
+      </div>
+
+      <p id="latexStatus" class="latex-status" role="status" aria-live="polite">
+        Preparing your first PDF...
+      </p>
+
+      <div class="latex-workspace">
+        <section class="latex-panel editor-panel" aria-labelledby="sourceHeading">
+          <div class="panel-heading">
+            <h2 id="sourceHeading">document.tex</h2>
+            <span id="sourceSize">0 / 65,536 bytes</span>
+          </div>
+          <textarea id="latexSource" spellcheck="false" autocomplete="off" aria-label="LaTeX source">\documentclass[11pt]{article}
+\usepackage[margin=1in]{geometry}
+\usepackage{amsmath}
+\usepackage{xcolor}
+
+\title{Hello from my server}
+\author{MrJuneJune}
+\date{\today}
+
+\begin{document}
+\maketitle
+
+This PDF was compiled by the custom C server behind this website.
+
+\[
+  e^{i\pi} + 1 = 0
+\]
+
+\textcolor{blue}{Edit this document and watch the preview update.}
+
+\end{document}
+</textarea>
+        </section>
+
+        <section class="latex-panel preview-panel" aria-labelledby="previewHeading">
+          <div class="panel-heading">
+            <h2 id="previewHeading">PDF preview</h2>
+            <span>Server rendered</span>
+          </div>
+          <iframe
+            id="pdfPreview"
+            title="Compiled LaTeX PDF preview"
+            src="about:blank"
+          ></iframe>
+          <pre id="latexDiagnostics" class="latex-diagnostics" hidden></pre>
+        </section>
+      </div>
+
+      <section class="latex-notes">
+        <h2>Limits</h2>
+        <p>Source is capped at 64 KiB and compilation at 8 seconds. Shell commands, network access, and reads outside the isolated TeX workspace are blocked.</p>
+      </section>
+    </main>
+    <div style="display: flex; align-items: center; justify-content: center; margin: 30px 0px;">
+  <small>&copy; 2026 June Park</small>
+</div>
+
+  </body>
+</html>
--- a/seobeo/os/s_linux_edge.c	Mon Aug 03 15:26:44 2026 -0700
+++ b/seobeo/os/s_linux_edge.c	Mon Aug 03 16:56:25 2026 -0700
@@ -37,7 +37,7 @@
   }
 
   struct epoll_event ev = {
-    .events = EPOLLIN | EPOLLET,
+    .events = EPOLLIN | EPOLLET | EPOLLEXCLUSIVE,
     .data.ptr = args->srv
   };
   if (epoll_ctl(epfd, EPOLL_CTL_ADD, args->srv->socket, &ev) < 0)
--- a/seobeo/s_network.c	Mon Aug 03 15:26:44 2026 -0700
+++ b/seobeo/s_network.c	Mon Aug 03 16:56:25 2026 -0700
@@ -17,6 +17,13 @@
 #endif
 }
 
+static void Seobeo_Socket_Set_Close_On_Exec(int socket_fd)
+{
+  int flags = fcntl(socket_fd, F_GETFD, 0);
+  if (flags >= 0)
+    fcntl(socket_fd, F_SETFD, flags | FD_CLOEXEC);
+}
+
 static ssize_t Seobeo_Socket_Write(
     int socket_fd,
     const void *buffer,
@@ -52,9 +59,14 @@
     free_server_info = free_server_info->ai_next
   )
   {
+    int socket_type = free_server_info->ai_socktype;
+#ifdef SOCK_CLOEXEC
+    socket_type |= SOCK_CLOEXEC;
+#endif
     if((socket_fd = socket(free_server_info->ai_family,
-                        free_server_info->ai_socktype, free_server_info->ai_protocol)) == -1)
+                        socket_type, free_server_info->ai_protocol)) == -1)
     { perror("socket"); continue; }
+    Seobeo_Socket_Set_Close_On_Exec(socket_fd);
     Seobeo_Socket_Disable_Sigpipe(socket_fd);
 
      if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
@@ -126,9 +138,14 @@
   { perror("getaddrinfo"); return NULL; }
 
 
+  int socket_type = server_infos->ai_socktype;
+#ifdef SOCK_CLOEXEC
+  socket_type |= SOCK_CLOEXEC;
+#endif
   if((socket_fd = socket(server_infos->ai_family,
-                      server_infos->ai_socktype, server_infos->ai_protocol)) == -1)
+                      socket_type, server_infos->ai_protocol)) == -1)
   { perror("socket"); return NULL; }
+  Seobeo_Socket_Set_Close_On_Exec(socket_fd);
   Seobeo_Socket_Disable_Sigpipe(socket_fd);
 
   if (connect(socket_fd, server_infos->ai_addr, server_infos->ai_addrlen) != 0)
@@ -178,14 +195,16 @@
   socklen_t addrlen = sizeof addr;
   char client_inet_addr[INET6_ADDRSTRLEN];
 
-  int client_fd = accept(p_server_handle->socket,
-                         (struct sockaddr*)&addr,
-                         &addrlen);
+  int client_fd = accept(
+      p_server_handle->socket,
+      (struct sockaddr*)&addr,
+      &addrlen);
   inet_ntop(
       addr.ss_family,
       Seobeo_Get_IP4_Or_IP6((struct sockaddr *)&addr),
       client_inet_addr, sizeof client_inet_addr);
   if (client_fd == -1) return NULL;
+  Seobeo_Socket_Set_Close_On_Exec(client_fd);
   Seobeo_Socket_Disable_Sigpipe(client_fd);
 
   // Set non blocking...
--- a/seobeo/s_web.c	Mon Aug 03 15:26:44 2026 -0700
+++ b/seobeo/s_web.c	Mon Aug 03 16:56:25 2026 -0700
@@ -4,6 +4,20 @@
 
 static char g_folder_path[512] = ".";
 
+static char *canonical_request_header(char *header)
+{
+  if (strcasecmp(header, "content-length") == 0) return "Content-Length";
+  if (strcasecmp(header, "content-type") == 0) return "Content-Type";
+  if (strcasecmp(header, "connection") == 0) return "Connection";
+  if (strcasecmp(header, "authorization") == 0) return "Authorization";
+  if (strcasecmp(header, "host") == 0) return "Host";
+  if (strcasecmp(header, "upgrade") == 0) return "Upgrade";
+  if (strcasecmp(header, "x-real-ip") == 0) return "X-Real-IP";
+  if (strcasecmp(header, "sec-websocket-key") == 0) return "Sec-WebSocket-Key";
+  if (strcasecmp(header, "sec-websocket-version") == 0) return "Sec-WebSocket-Version";
+  return header;
+}
+
 char* Seobeo_Web_LoadFile(const char *file_path, size_t *p_file_size)
 {
   char full_path[1024];
@@ -58,7 +72,11 @@
     case HTTP_UNAUTHORIZED: status_text = "Unauthorized"; break;
     case HTTP_FORBIDDEN: status_text = "Forbidden"; break;
     case HTTP_NOT_FOUND: status_text = "Not Found"; break;
+    case HTTP_PAYLOAD_TOO_LARGE: status_text = "Content Too Large"; break;
+    case HTTP_UNPROCESSABLE_CONTENT: status_text = "Unprocessable Content"; break;
+    case HTTP_TOO_MANY_REQUESTS: status_text = "Too Many Requests"; break;
     case HTTP_INTERNAL_ERROR: status_text = "Internal Server Error"; break;
+    case HTTP_SERVICE_UNAVAILABLE: status_text = "Service Unavailable"; break;
     case 502: status_text = "Bad Gateway"; break;
     case 504: status_text = "Gateway Timeout"; break;
     default: status_text = "Unknown"; break;
@@ -494,7 +512,11 @@
       memcpy(val, val_start, value_len);
       val[value_len] = '\0';
 
-      Dowa_HashMap_Push_Arena(*pp_map, key, val, p_arena);
+      Dowa_HashMap_Push_Arena(
+          *pp_map,
+          canonical_request_header(key),
+          val,
+          p_arena);
     }
 
     line = next + 2;
--- a/seobeo/seobeo.h	Mon Aug 03 15:26:44 2026 -0700
+++ b/seobeo/seobeo.h	Mon Aug 03 16:56:25 2026 -0700
@@ -37,6 +37,10 @@
 #define HTTP_UNAUTHORIZED 401
 #define HTTP_FORBIDDEN 403
 #define HTTP_NOT_FOUND 404
+#define HTTP_PAYLOAD_TOO_LARGE 413
+#define HTTP_UNPROCESSABLE_CONTENT 422
+#define HTTP_TOO_MANY_REQUESTS 429
+#define HTTP_SERVICE_UNAVAILABLE 503
 #define HTTP_INTERNAL_ERROR 500
 
 #define CREATE_REDIRECT_HANDLER(name, target_url) \