Mercurial
comparison mrjunejune/latex_renderer.c @ 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 | |
| children | 70f2a3dafc1c |
comparison
equal
deleted
inserted
replaced
| 243:823f2a8b16c8 | 244:b8aa08503378 |
|---|---|
| 1 #include "mrjunejune/latex_renderer.h" | |
| 2 | |
| 3 #ifdef __linux__ | |
| 4 | |
| 5 #include <dirent.h> | |
| 6 #include <errno.h> | |
| 7 #include <fcntl.h> | |
| 8 #include <linux/audit.h> | |
| 9 #include <linux/filter.h> | |
| 10 #include <linux/landlock.h> | |
| 11 #include <linux/seccomp.h> | |
| 12 #include <limits.h> | |
| 13 #include <poll.h> | |
| 14 #include <signal.h> | |
| 15 #include <stdbool.h> | |
| 16 #include <stddef.h> | |
| 17 #include <stdio.h> | |
| 18 #include <stdlib.h> | |
| 19 #include <string.h> | |
| 20 #include <sys/prctl.h> | |
| 21 #include <sys/resource.h> | |
| 22 #include <sys/stat.h> | |
| 23 #include <sys/syscall.h> | |
| 24 #include <sys/types.h> | |
| 25 #include <sys/wait.h> | |
| 26 #include <time.h> | |
| 27 #include <unistd.h> | |
| 28 | |
| 29 #ifndef LANDLOCK_ACCESS_FS_REFER | |
| 30 #define LANDLOCK_ACCESS_FS_REFER (1ULL << 13) | |
| 31 #endif | |
| 32 #ifndef LANDLOCK_ACCESS_FS_TRUNCATE | |
| 33 #define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14) | |
| 34 #endif | |
| 35 | |
| 36 #if defined(__x86_64__) | |
| 37 #define LATEX_AUDIT_ARCH AUDIT_ARCH_X86_64 | |
| 38 #elif defined(__aarch64__) | |
| 39 #define LATEX_AUDIT_ARCH AUDIT_ARCH_AARCH64 | |
| 40 #else | |
| 41 #define LATEX_AUDIT_ARCH 0 | |
| 42 #endif | |
| 43 | |
| 44 #define LATEX_DIAGNOSTICS_MAX_BYTES (16 * 1024) | |
| 45 #define LATEX_CPU_TIMEOUT_SECONDS 5 | |
| 46 #define LATEX_WALL_TIMEOUT_SECONDS 8 | |
| 47 | |
| 48 static char *duplicate_message(const char *message) | |
| 49 { | |
| 50 size_t length = strlen(message); | |
| 51 char *copy = malloc(length + 1); | |
| 52 if (copy) | |
| 53 memcpy(copy, message, length + 1); | |
| 54 return copy; | |
| 55 } | |
| 56 | |
| 57 static Latex_Render_Result result_with_message( | |
| 58 Latex_Render_Status status, | |
| 59 const char *message) | |
| 60 { | |
| 61 return (Latex_Render_Result){ | |
| 62 .status = status, | |
| 63 .diagnostics = duplicate_message(message), | |
| 64 }; | |
| 65 } | |
| 66 | |
| 67 static bool write_all(int fd, const uint8_t *data, size_t size) | |
| 68 { | |
| 69 while (size > 0) | |
| 70 { | |
| 71 ssize_t written = write(fd, data, size); | |
| 72 if (written < 0) | |
| 73 { | |
| 74 if (errno == EINTR) | |
| 75 continue; | |
| 76 return false; | |
| 77 } | |
| 78 data += written; | |
| 79 size -= (size_t)written; | |
| 80 } | |
| 81 return true; | |
| 82 } | |
| 83 | |
| 84 static int remove_tree(const char *path, unsigned depth) | |
| 85 { | |
| 86 if (depth > 16) | |
| 87 return -1; | |
| 88 | |
| 89 struct stat status; | |
| 90 if (lstat(path, &status) != 0) | |
| 91 return errno == ENOENT ? 0 : -1; | |
| 92 if (!S_ISDIR(status.st_mode)) | |
| 93 return unlink(path); | |
| 94 | |
| 95 DIR *directory = opendir(path); | |
| 96 if (!directory) | |
| 97 return -1; | |
| 98 | |
| 99 int result = 0; | |
| 100 struct dirent *entry; | |
| 101 while ((entry = readdir(directory)) != NULL) | |
| 102 { | |
| 103 if (strcmp(entry->d_name, ".") == 0 || | |
| 104 strcmp(entry->d_name, "..") == 0) | |
| 105 continue; | |
| 106 | |
| 107 char child[1024]; | |
| 108 int length = snprintf( | |
| 109 child, | |
| 110 sizeof(child), | |
| 111 "%s/%s", | |
| 112 path, | |
| 113 entry->d_name); | |
| 114 if (length < 0 || | |
| 115 (size_t)length >= sizeof(child) || | |
| 116 remove_tree(child, depth + 1) != 0) | |
| 117 { | |
| 118 result = -1; | |
| 119 break; | |
| 120 } | |
| 121 } | |
| 122 closedir(directory); | |
| 123 return result == 0 ? rmdir(path) : result; | |
| 124 } | |
| 125 | |
| 126 static bool set_limit(int resource, rlim_t value) | |
| 127 { | |
| 128 struct rlimit limit = { | |
| 129 .rlim_cur = value, | |
| 130 .rlim_max = value, | |
| 131 }; | |
| 132 return setrlimit(resource, &limit) == 0; | |
| 133 } | |
| 134 | |
| 135 static bool add_landlock_path_rule( | |
| 136 int ruleset_fd, | |
| 137 const char *path, | |
| 138 uint64_t access, | |
| 139 bool required) | |
| 140 { | |
| 141 int path_fd = open(path, O_PATH | O_CLOEXEC); | |
| 142 if (path_fd < 0) | |
| 143 return !required && errno == ENOENT; | |
| 144 | |
| 145 struct stat status; | |
| 146 bool ok = fstat(path_fd, &status) == 0; | |
| 147 if (ok && !S_ISDIR(status.st_mode)) | |
| 148 { | |
| 149 access &= LANDLOCK_ACCESS_FS_EXECUTE | | |
| 150 LANDLOCK_ACCESS_FS_READ_FILE | | |
| 151 LANDLOCK_ACCESS_FS_WRITE_FILE; | |
| 152 } | |
| 153 | |
| 154 struct landlock_path_beneath_attr rule = { | |
| 155 .allowed_access = access, | |
| 156 .parent_fd = path_fd, | |
| 157 }; | |
| 158 if (ok) | |
| 159 ok = syscall( | |
| 160 SYS_landlock_add_rule, | |
| 161 ruleset_fd, | |
| 162 LANDLOCK_RULE_PATH_BENEATH, | |
| 163 &rule, | |
| 164 0) == 0; | |
| 165 close(path_fd); | |
| 166 return ok; | |
| 167 } | |
| 168 | |
| 169 static bool apply_filesystem_sandbox(const char *job_directory) | |
| 170 { | |
| 171 int abi = syscall( | |
| 172 SYS_landlock_create_ruleset, | |
| 173 NULL, | |
| 174 0, | |
| 175 LANDLOCK_CREATE_RULESET_VERSION); | |
| 176 if (abi < 1) | |
| 177 return false; | |
| 178 | |
| 179 uint64_t read_access = | |
| 180 LANDLOCK_ACCESS_FS_EXECUTE | | |
| 181 LANDLOCK_ACCESS_FS_READ_FILE | | |
| 182 LANDLOCK_ACCESS_FS_READ_DIR; | |
| 183 uint64_t write_access = | |
| 184 LANDLOCK_ACCESS_FS_WRITE_FILE | | |
| 185 LANDLOCK_ACCESS_FS_REMOVE_DIR | | |
| 186 LANDLOCK_ACCESS_FS_REMOVE_FILE | | |
| 187 LANDLOCK_ACCESS_FS_MAKE_CHAR | | |
| 188 LANDLOCK_ACCESS_FS_MAKE_DIR | | |
| 189 LANDLOCK_ACCESS_FS_MAKE_REG | | |
| 190 LANDLOCK_ACCESS_FS_MAKE_SOCK | | |
| 191 LANDLOCK_ACCESS_FS_MAKE_FIFO | | |
| 192 LANDLOCK_ACCESS_FS_MAKE_BLOCK | | |
| 193 LANDLOCK_ACCESS_FS_MAKE_SYM; | |
| 194 if (abi >= 2) | |
| 195 write_access |= LANDLOCK_ACCESS_FS_REFER; | |
| 196 if (abi >= 3) | |
| 197 write_access |= LANDLOCK_ACCESS_FS_TRUNCATE; | |
| 198 | |
| 199 struct landlock_ruleset_attr ruleset = { | |
| 200 .handled_access_fs = read_access | write_access, | |
| 201 }; | |
| 202 int ruleset_fd = syscall( | |
| 203 SYS_landlock_create_ruleset, | |
| 204 &ruleset, | |
| 205 sizeof(ruleset), | |
| 206 0); | |
| 207 if (ruleset_fd < 0) | |
| 208 return false; | |
| 209 | |
| 210 const char *read_only_directories[] = { | |
| 211 "/usr", | |
| 212 "/lib", | |
| 213 "/lib64", | |
| 214 "/bin", | |
| 215 "/var/lib/texmf", | |
| 216 "/etc/texmf", | |
| 217 "/etc/fonts", | |
| 218 }; | |
| 219 bool ok = true; | |
| 220 for (size_t i = 0; | |
| 221 ok && i < sizeof(read_only_directories) / sizeof(read_only_directories[0]); | |
| 222 i++) | |
| 223 { | |
| 224 ok = add_landlock_path_rule( | |
| 225 ruleset_fd, | |
| 226 read_only_directories[i], | |
| 227 read_access, | |
| 228 false); | |
| 229 } | |
| 230 if (ok) | |
| 231 ok = add_landlock_path_rule( | |
| 232 ruleset_fd, | |
| 233 "/etc/ld.so.cache", | |
| 234 LANDLOCK_ACCESS_FS_READ_FILE, | |
| 235 false); | |
| 236 if (ok) | |
| 237 ok = add_landlock_path_rule( | |
| 238 ruleset_fd, | |
| 239 "/dev/null", | |
| 240 LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE, | |
| 241 true); | |
| 242 if (ok) | |
| 243 ok = add_landlock_path_rule( | |
| 244 ruleset_fd, | |
| 245 "/dev/urandom", | |
| 246 LANDLOCK_ACCESS_FS_READ_FILE, | |
| 247 false); | |
| 248 if (ok) | |
| 249 ok = add_landlock_path_rule( | |
| 250 ruleset_fd, | |
| 251 job_directory, | |
| 252 read_access | write_access, | |
| 253 true); | |
| 254 if (ok) | |
| 255 ok = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0; | |
| 256 if (ok) | |
| 257 ok = syscall( | |
| 258 SYS_landlock_restrict_self, | |
| 259 ruleset_fd, | |
| 260 0) == 0; | |
| 261 close(ruleset_fd); | |
| 262 return ok; | |
| 263 } | |
| 264 | |
| 265 static bool apply_network_sandbox(void) | |
| 266 { | |
| 267 if (LATEX_AUDIT_ARCH == 0) | |
| 268 return false; | |
| 269 | |
| 270 struct sock_filter filters[64]; | |
| 271 size_t count = 0; | |
| 272 #define ADD_FILTER(value) filters[count++] = (struct sock_filter)value | |
| 273 #define DENY_SYSCALL(number) \ | |
| 274 do { \ | |
| 275 ADD_FILTER(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, number, 0, 1)); \ | |
| 276 ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM)); \ | |
| 277 } while (0) | |
| 278 | |
| 279 ADD_FILTER(BPF_STMT( | |
| 280 BPF_LD | BPF_W | BPF_ABS, | |
| 281 offsetof(struct seccomp_data, arch))); | |
| 282 ADD_FILTER(BPF_JUMP( | |
| 283 BPF_JMP | BPF_JEQ | BPF_K, | |
| 284 LATEX_AUDIT_ARCH, | |
| 285 1, | |
| 286 0)); | |
| 287 ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)); | |
| 288 ADD_FILTER(BPF_STMT( | |
| 289 BPF_LD | BPF_W | BPF_ABS, | |
| 290 offsetof(struct seccomp_data, nr))); | |
| 291 DENY_SYSCALL(__NR_socket); | |
| 292 DENY_SYSCALL(__NR_socketpair); | |
| 293 DENY_SYSCALL(__NR_connect); | |
| 294 DENY_SYSCALL(__NR_accept); | |
| 295 DENY_SYSCALL(__NR_accept4); | |
| 296 DENY_SYSCALL(__NR_bind); | |
| 297 DENY_SYSCALL(__NR_listen); | |
| 298 DENY_SYSCALL(__NR_sendto); | |
| 299 DENY_SYSCALL(__NR_recvfrom); | |
| 300 DENY_SYSCALL(__NR_sendmsg); | |
| 301 DENY_SYSCALL(__NR_recvmsg); | |
| 302 ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)); | |
| 303 | |
| 304 struct sock_fprog program = { | |
| 305 .len = (unsigned short)count, | |
| 306 .filter = filters, | |
| 307 }; | |
| 308 #undef DENY_SYSCALL | |
| 309 #undef ADD_FILTER | |
| 310 return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) == 0; | |
| 311 } | |
| 312 | |
| 313 static void configure_child_environment(const char *job_directory) | |
| 314 { | |
| 315 clearenv(); | |
| 316 setenv("HOME", job_directory, 1); | |
| 317 setenv("TMPDIR", job_directory, 1); | |
| 318 setenv("TEXMFVAR", job_directory, 1); | |
| 319 setenv("TEXMFCONFIG", job_directory, 1); | |
| 320 setenv("openin_any", "p", 1); | |
| 321 setenv("openout_any", "p", 1); | |
| 322 setenv("shell_escape", "0", 1); | |
| 323 setenv("SOURCE_DATE_EPOCH", "0", 1); | |
| 324 setenv("LANG", "C.UTF-8", 1); | |
| 325 } | |
| 326 | |
| 327 static void close_inherited_descriptors(void) | |
| 328 { | |
| 329 #ifdef SYS_close_range | |
| 330 if (syscall(SYS_close_range, 3U, UINT_MAX, 0) == 0) | |
| 331 return; | |
| 332 #endif | |
| 333 struct rlimit limit; | |
| 334 rlim_t maximum = 65536; | |
| 335 if (getrlimit(RLIMIT_NOFILE, &limit) == 0 && | |
| 336 limit.rlim_cur != RLIM_INFINITY && | |
| 337 limit.rlim_cur < maximum) | |
| 338 maximum = limit.rlim_cur; | |
| 339 for (int fd = 3; fd < (int)maximum; fd++) | |
| 340 close(fd); | |
| 341 } | |
| 342 | |
| 343 static void run_compiler_child( | |
| 344 const char *compiler, | |
| 345 const char *job_directory, | |
| 346 int output_fd) | |
| 347 { | |
| 348 if (dup2(output_fd, STDOUT_FILENO) < 0 || | |
| 349 dup2(output_fd, STDERR_FILENO) < 0) | |
| 350 _exit(120); | |
| 351 if (output_fd != STDOUT_FILENO && output_fd != STDERR_FILENO) | |
| 352 close(output_fd); | |
| 353 close_inherited_descriptors(); | |
| 354 if (setpgid(0, 0) != 0 || | |
| 355 prctl(PR_SET_PDEATHSIG, SIGKILL) != 0 || | |
| 356 getppid() == 1) | |
| 357 _exit(119); | |
| 358 if (chdir(job_directory) != 0) | |
| 359 _exit(121); | |
| 360 | |
| 361 configure_child_environment(job_directory); | |
| 362 if (!set_limit(RLIMIT_CPU, LATEX_CPU_TIMEOUT_SECONDS) || | |
| 363 !set_limit(RLIMIT_AS, 768 * 1024 * 1024) || | |
| 364 !set_limit(RLIMIT_FSIZE, LATEX_PDF_MAX_BYTES) || | |
| 365 !set_limit(RLIMIT_NOFILE, 64) || | |
| 366 !set_limit(RLIMIT_NPROC, 1)) | |
| 367 _exit(122); | |
| 368 if (!apply_filesystem_sandbox(job_directory) || | |
| 369 !apply_network_sandbox()) | |
| 370 _exit(123); | |
| 371 | |
| 372 char *const arguments[] = { | |
| 373 (char *)compiler, | |
| 374 "-interaction=nonstopmode", | |
| 375 "-halt-on-error", | |
| 376 "-no-shell-escape", | |
| 377 "-file-line-error", | |
| 378 "-output-directory=.", | |
| 379 "document.tex", | |
| 380 NULL, | |
| 381 }; | |
| 382 execv(compiler, arguments); | |
| 383 _exit(errno == ENOENT ? 127 : 126); | |
| 384 } | |
| 385 | |
| 386 static double elapsed_seconds( | |
| 387 const struct timespec *start, | |
| 388 const struct timespec *now) | |
| 389 { | |
| 390 return (double)(now->tv_sec - start->tv_sec) + | |
| 391 (double)(now->tv_nsec - start->tv_nsec) / 1000000000.0; | |
| 392 } | |
| 393 | |
| 394 static int wait_for_compiler( | |
| 395 pid_t child, | |
| 396 int output_fd, | |
| 397 char *diagnostics, | |
| 398 size_t diagnostics_size, | |
| 399 bool *timed_out) | |
| 400 { | |
| 401 int flags = fcntl(output_fd, F_GETFL, 0); | |
| 402 if (flags >= 0) | |
| 403 fcntl(output_fd, F_SETFL, flags | O_NONBLOCK); | |
| 404 | |
| 405 size_t used = 0; | |
| 406 int child_status = 0; | |
| 407 bool child_done = false; | |
| 408 struct timespec start; | |
| 409 clock_gettime(CLOCK_MONOTONIC, &start); | |
| 410 | |
| 411 while (!child_done) | |
| 412 { | |
| 413 char buffer[2048]; | |
| 414 ssize_t count; | |
| 415 while ((count = read(output_fd, buffer, sizeof(buffer))) > 0) | |
| 416 { | |
| 417 size_t available = diagnostics_size - used - 1; | |
| 418 size_t copy_size = (size_t)count < available ? (size_t)count : available; | |
| 419 if (copy_size > 0) | |
| 420 { | |
| 421 memcpy(diagnostics + used, buffer, copy_size); | |
| 422 used += copy_size; | |
| 423 } | |
| 424 } | |
| 425 | |
| 426 pid_t waited = waitpid(child, &child_status, WNOHANG); | |
| 427 if (waited == child) | |
| 428 { | |
| 429 child_done = true; | |
| 430 break; | |
| 431 } | |
| 432 if (waited < 0 && errno != EINTR) | |
| 433 break; | |
| 434 | |
| 435 struct timespec now; | |
| 436 clock_gettime(CLOCK_MONOTONIC, &now); | |
| 437 if (elapsed_seconds(&start, &now) >= LATEX_WALL_TIMEOUT_SECONDS) | |
| 438 { | |
| 439 *timed_out = true; | |
| 440 kill(-child, SIGKILL); | |
| 441 while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) | |
| 442 { | |
| 443 } | |
| 444 child_done = true; | |
| 445 break; | |
| 446 } | |
| 447 | |
| 448 struct pollfd poll_fd = { | |
| 449 .fd = output_fd, | |
| 450 .events = POLLIN, | |
| 451 }; | |
| 452 poll(&poll_fd, 1, 50); | |
| 453 } | |
| 454 | |
| 455 char buffer[2048]; | |
| 456 ssize_t count; | |
| 457 while ((count = read(output_fd, buffer, sizeof(buffer))) > 0) | |
| 458 { | |
| 459 size_t available = diagnostics_size - used - 1; | |
| 460 size_t copy_size = (size_t)count < available ? (size_t)count : available; | |
| 461 if (copy_size > 0) | |
| 462 { | |
| 463 memcpy(diagnostics + used, buffer, copy_size); | |
| 464 used += copy_size; | |
| 465 } | |
| 466 } | |
| 467 diagnostics[used] = '\0'; | |
| 468 kill(-child, SIGKILL); | |
| 469 return child_status; | |
| 470 } | |
| 471 | |
| 472 static bool read_pdf( | |
| 473 const char *path, | |
| 474 uint8_t **data, | |
| 475 size_t *size) | |
| 476 { | |
| 477 int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); | |
| 478 if (fd < 0) | |
| 479 return false; | |
| 480 | |
| 481 struct stat status; | |
| 482 bool ok = fstat(fd, &status) == 0 && | |
| 483 S_ISREG(status.st_mode) && | |
| 484 status.st_size > 4 && | |
| 485 status.st_size <= LATEX_PDF_MAX_BYTES; | |
| 486 uint8_t *content = NULL; | |
| 487 if (ok) | |
| 488 { | |
| 489 content = malloc((size_t)status.st_size); | |
| 490 ok = content != NULL; | |
| 491 } | |
| 492 size_t offset = 0; | |
| 493 while (ok && offset < (size_t)status.st_size) | |
| 494 { | |
| 495 ssize_t count = read( | |
| 496 fd, | |
| 497 content + offset, | |
| 498 (size_t)status.st_size - offset); | |
| 499 if (count < 0 && errno == EINTR) | |
| 500 continue; | |
| 501 if (count <= 0) | |
| 502 { | |
| 503 ok = false; | |
| 504 break; | |
| 505 } | |
| 506 offset += (size_t)count; | |
| 507 } | |
| 508 close(fd); | |
| 509 | |
| 510 if (!ok || | |
| 511 memcmp(content, "%PDF-", 5) != 0) | |
| 512 { | |
| 513 free(content); | |
| 514 return false; | |
| 515 } | |
| 516 *data = content; | |
| 517 *size = offset; | |
| 518 return true; | |
| 519 } | |
| 520 | |
| 521 Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size) | |
| 522 { | |
| 523 if (!source || source_size == 0) | |
| 524 return result_with_message( | |
| 525 LATEX_RENDER_INVALID_INPUT, | |
| 526 "LaTeX source is required."); | |
| 527 if (source_size > LATEX_SOURCE_MAX_BYTES) | |
| 528 return result_with_message( | |
| 529 LATEX_RENDER_INVALID_INPUT, | |
| 530 "LaTeX source exceeds the 64 KiB limit."); | |
| 531 if (memchr(source, '\0', source_size)) | |
| 532 return result_with_message( | |
| 533 LATEX_RENDER_INVALID_INPUT, | |
| 534 "LaTeX source cannot contain NUL bytes."); | |
| 535 | |
| 536 const char *compiler = "/usr/bin/pdflatex"; | |
| 537 if (access(compiler, X_OK) != 0) | |
| 538 return result_with_message( | |
| 539 LATEX_RENDER_COMPILER_UNAVAILABLE, | |
| 540 "The server LaTeX compiler is unavailable."); | |
| 541 | |
| 542 char job_directory[] = "/tmp/mrjunejune-latex-XXXXXX"; | |
| 543 if (!mkdtemp(job_directory)) | |
| 544 return result_with_message( | |
| 545 LATEX_RENDER_INTERNAL_ERROR, | |
| 546 "Unable to create the LaTeX workspace."); | |
| 547 | |
| 548 char source_path[512]; | |
| 549 snprintf( | |
| 550 source_path, | |
| 551 sizeof(source_path), | |
| 552 "%s/document.tex", | |
| 553 job_directory); | |
| 554 int source_fd = open( | |
| 555 source_path, | |
| 556 O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, | |
| 557 0600); | |
| 558 if (source_fd < 0 || !write_all(source_fd, source, source_size)) | |
| 559 { | |
| 560 if (source_fd >= 0) | |
| 561 close(source_fd); | |
| 562 remove_tree(job_directory, 0); | |
| 563 return result_with_message( | |
| 564 LATEX_RENDER_INTERNAL_ERROR, | |
| 565 "Unable to write the LaTeX source."); | |
| 566 } | |
| 567 close(source_fd); | |
| 568 | |
| 569 int output_pipe[2]; | |
| 570 if (pipe2(output_pipe, O_CLOEXEC) != 0) | |
| 571 { | |
| 572 remove_tree(job_directory, 0); | |
| 573 return result_with_message( | |
| 574 LATEX_RENDER_INTERNAL_ERROR, | |
| 575 "Unable to capture compiler output."); | |
| 576 } | |
| 577 | |
| 578 pid_t child = fork(); | |
| 579 if (child < 0) | |
| 580 { | |
| 581 close(output_pipe[0]); | |
| 582 close(output_pipe[1]); | |
| 583 remove_tree(job_directory, 0); | |
| 584 return result_with_message( | |
| 585 LATEX_RENDER_INTERNAL_ERROR, | |
| 586 "Unable to start the LaTeX compiler."); | |
| 587 } | |
| 588 if (child == 0) | |
| 589 { | |
| 590 close(output_pipe[0]); | |
| 591 run_compiler_child(compiler, job_directory, output_pipe[1]); | |
| 592 } | |
| 593 setpgid(child, child); | |
| 594 | |
| 595 close(output_pipe[1]); | |
| 596 char *diagnostics = calloc(1, LATEX_DIAGNOSTICS_MAX_BYTES + 1); | |
| 597 if (!diagnostics) | |
| 598 { | |
| 599 kill(-child, SIGKILL); | |
| 600 waitpid(child, NULL, 0); | |
| 601 close(output_pipe[0]); | |
| 602 remove_tree(job_directory, 0); | |
| 603 return result_with_message( | |
| 604 LATEX_RENDER_INTERNAL_ERROR, | |
| 605 "Unable to allocate compiler diagnostics."); | |
| 606 } | |
| 607 | |
| 608 bool timed_out = false; | |
| 609 int child_status = wait_for_compiler( | |
| 610 child, | |
| 611 output_pipe[0], | |
| 612 diagnostics, | |
| 613 LATEX_DIAGNOSTICS_MAX_BYTES + 1, | |
| 614 &timed_out); | |
| 615 close(output_pipe[0]); | |
| 616 | |
| 617 Latex_Render_Result result = { | |
| 618 .status = LATEX_RENDER_INTERNAL_ERROR, | |
| 619 .diagnostics = diagnostics, | |
| 620 }; | |
| 621 if (timed_out) | |
| 622 { | |
| 623 result.status = LATEX_RENDER_TIMEOUT; | |
| 624 free(result.diagnostics); | |
| 625 result.diagnostics = duplicate_message( | |
| 626 "Compilation exceeded the 8 second limit."); | |
| 627 } | |
| 628 else if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 127) | |
| 629 { | |
| 630 result.status = LATEX_RENDER_COMPILER_UNAVAILABLE; | |
| 631 } | |
| 632 else if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 123) | |
| 633 { | |
| 634 result.status = LATEX_RENDER_SANDBOX_ERROR; | |
| 635 free(result.diagnostics); | |
| 636 result.diagnostics = duplicate_message( | |
| 637 "The server could not apply the LaTeX security sandbox."); | |
| 638 } | |
| 639 else if (WIFSIGNALED(child_status) && | |
| 640 (WTERMSIG(child_status) == SIGXCPU || | |
| 641 WTERMSIG(child_status) == SIGKILL)) | |
| 642 { | |
| 643 result.status = LATEX_RENDER_TIMEOUT; | |
| 644 free(result.diagnostics); | |
| 645 result.diagnostics = duplicate_message( | |
| 646 "Compilation exceeded its CPU or memory limit."); | |
| 647 } | |
| 648 else if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0) | |
| 649 { | |
| 650 result.status = LATEX_RENDER_COMPILE_ERROR; | |
| 651 if (!result.diagnostics[0]) | |
| 652 { | |
| 653 free(result.diagnostics); | |
| 654 result.diagnostics = duplicate_message("LaTeX compilation failed."); | |
| 655 } | |
| 656 } | |
| 657 else | |
| 658 { | |
| 659 char pdf_path[512]; | |
| 660 snprintf(pdf_path, sizeof(pdf_path), "%s/document.pdf", job_directory); | |
| 661 if (read_pdf(pdf_path, &result.pdf_data, &result.pdf_size)) | |
| 662 { | |
| 663 result.status = LATEX_RENDER_OK; | |
| 664 free(result.diagnostics); | |
| 665 result.diagnostics = NULL; | |
| 666 } | |
| 667 else | |
| 668 { | |
| 669 result.status = LATEX_RENDER_INTERNAL_ERROR; | |
| 670 free(result.diagnostics); | |
| 671 result.diagnostics = duplicate_message( | |
| 672 "The compiler did not produce a valid PDF."); | |
| 673 } | |
| 674 } | |
| 675 | |
| 676 remove_tree(job_directory, 0); | |
| 677 return result; | |
| 678 } | |
| 679 | |
| 680 void Latex_Render_Result_Destroy(Latex_Render_Result *result) | |
| 681 { | |
| 682 if (!result) | |
| 683 return; | |
| 684 free(result->pdf_data); | |
| 685 free(result->diagnostics); | |
| 686 memset(result, 0, sizeof(*result)); | |
| 687 } | |
| 688 | |
| 689 #else | |
| 690 | |
| 691 #include <stdlib.h> | |
| 692 #include <string.h> | |
| 693 | |
| 694 Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size) | |
| 695 { | |
| 696 (void)source; | |
| 697 (void)source_size; | |
| 698 const char *message = | |
| 699 "The LaTeX security sandbox is only available on Linux."; | |
| 700 Latex_Render_Result result = { | |
| 701 .status = LATEX_RENDER_SANDBOX_ERROR, | |
| 702 .diagnostics = malloc(strlen(message) + 1), | |
| 703 }; | |
| 704 if (result.diagnostics) | |
| 705 strcpy(result.diagnostics, message); | |
| 706 return result; | |
| 707 } | |
| 708 | |
| 709 void Latex_Render_Result_Destroy(Latex_Render_Result *result) | |
| 710 { | |
| 711 if (!result) | |
| 712 return; | |
| 713 free(result->pdf_data); | |
| 714 free(result->diagnostics); | |
| 715 memset(result, 0, sizeof(*result)); | |
| 716 } | |
| 717 | |
| 718 #endif |