Mercurial
view mrjunejune/latex_renderer.c @ 247:70f2a3dafc1c
[build] Bundle offline Tectonic runtime
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 03 Aug 2026 20:20:06 -0700 |
| parents | b8aa08503378 |
| children | b8b6e726964a |
line wrap: on
line source
#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 #define TECTONIC_BUNDLE_URL "https://data1.fullyjustified.net/tlextras-2022.0r0.tar" #define TECTONIC_CACHE_PATH "third_party/tectonic/runtime/cache" #define TECTONIC_COMPILER_PATH "third_party/tectonic/runtime/bin/tectonic" #define TECTONIC_FONTCONFIG_SYSROOT_PATH "third_party/tectonic/runtime/sysroot" 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 add_landlock_tree_rules( int ruleset_fd, const char *path, uint64_t access, unsigned depth) { if (depth > 32 || !add_landlock_path_rule(ruleset_fd, path, access, true)) return false; struct stat status; if (lstat(path, &status) != 0) return false; if (!S_ISDIR(status.st_mode)) return true; DIR *directory = opendir(path); if (!directory) return false; bool ok = true; struct dirent *entry; while (ok && (entry = readdir(directory)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; char child[PATH_MAX]; int length = snprintf( child, sizeof(child), "%s/%s", path, entry->d_name); ok = length >= 0 && (size_t)length < sizeof(child) && add_landlock_tree_rules( ruleset_fd, child, access, depth + 1); } closedir(directory); return ok; } static bool apply_filesystem_sandbox( const char *job_directory, const char *compiler, const char *cache_directory, const char *fontconfig_sysroot) { 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; bool ok = true; if (ok) ok = add_landlock_path_rule( ruleset_fd, compiler, LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE, true); if (ok) ok = add_landlock_tree_rules( ruleset_fd, cache_directory, read_access | LANDLOCK_ACCESS_FS_MAKE_DIR, 0); if (ok) ok = add_landlock_tree_rules( ruleset_fd, fontconfig_sysroot, read_access, 0); 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, const char *cache_directory, const char *fontconfig_sysroot) { clearenv(); setenv("HOME", job_directory, 1); setenv("TMPDIR", job_directory, 1); setenv("XDG_CACHE_HOME", job_directory, 1); char tectonic_cache[PATH_MAX]; snprintf( tectonic_cache, sizeof(tectonic_cache), "%s/tectonic", cache_directory); setenv("TECTONIC_CACHE_DIR", tectonic_cache, 1); setenv("FONTCONFIG_FILE", "fonts.conf", 1); setenv("FONTCONFIG_PATH", "/etc/fonts", 1); setenv("FONTCONFIG_SYSROOT", fontconfig_sysroot, 1); setenv("TECTONIC_UNTRUSTED_MODE", "1", 1); setenv("SOURCE_DATE_EPOCH", "0", 1); setenv("TZ", "UTC", 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 *cache_directory, const char *fontconfig_sysroot, 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, cache_directory, fontconfig_sysroot); 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, compiler, cache_directory, fontconfig_sysroot) || !apply_network_sandbox()) _exit(123); char *const arguments[] = { (char *)compiler, "--untrusted", "--only-cached", "--chatter", "minimal", "--color", "never", "--reruns", "1", "--outdir", ".", "--bundle", TECTONIC_BUNDLE_URL, "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; } static bool resolve_runtime_path( const char *relative_path, char resolved[PATH_MAX]) { if (realpath(relative_path, resolved)) return true; const char *runfiles = getenv("RUNFILES_DIR"); const char *workspace = getenv("TEST_WORKSPACE"); char candidate[PATH_MAX]; if (runfiles && workspace) { int length = snprintf( candidate, sizeof(candidate), "%s/%s/%s", runfiles, workspace, relative_path); if (length >= 0 && (size_t)length < sizeof(candidate) && realpath(candidate, resolved)) return true; } char executable[PATH_MAX]; ssize_t executable_length = readlink( "/proc/self/exe", executable, sizeof(executable) - 1); if (executable_length <= 0) return false; executable[executable_length] = '\0'; char *last_slash = strrchr(executable, '/'); if (last_slash) { *last_slash = '\0'; int length = snprintf( candidate, sizeof(candidate), "%s/%s", executable, relative_path); if (length >= 0 && (size_t)length < sizeof(candidate) && realpath(candidate, resolved)) return true; } char *package = strstr(executable, "/mrjunejune/"); if (!package) return false; *package = '\0'; int length = snprintf( candidate, sizeof(candidate), "%s/%s", executable, relative_path); return length >= 0 && (size_t)length < sizeof(candidate) && realpath(candidate, resolved) != NULL; } 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."); char compiler[PATH_MAX]; char cache_directory[PATH_MAX]; char fontconfig_sysroot[PATH_MAX]; if (!resolve_runtime_path(TECTONIC_COMPILER_PATH, compiler) || !resolve_runtime_path(TECTONIC_CACHE_PATH, cache_directory) || !resolve_runtime_path( TECTONIC_FONTCONFIG_SYSROOT_PATH, fontconfig_sysroot) || access(compiler, X_OK) != 0) return result_with_message( LATEX_RENDER_COMPILER_UNAVAILABLE, "The Bazel Tectonic runtime 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, cache_directory, fontconfig_sysroot, 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