// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors // // Spike E main (§14 step 8): fork into a UI process and a sandboxed document // process, connect them over a socketpair, send N Parse requests, and measure // the round-trip latency. This is the M0 spike that measures the cost of the // §2.1 process split — the one thing the plan says is "genuinely painful to // retrofit", so it must be de-risked at M0. // // The sandbox is installed in the child AFTER fork, before any untrusted data // is processed. The parent (UI) does no untrusted parsing. // // Usage: // spike_e_sandbox [n_requests=1000] [payload_bytes=1024] // // Exit 0 if all requests round-trip correctly (the channel works under the // sandbox); 1 otherwise. The latency numbers go in the JSON report's notes. #include "DocumentProcess.h" #include "UIProcess.h" #include "../common/SpikeRunner.h" #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char** argv) { const std::size_t n_requests = (argc >= 2) ? std::size_t(std::atoll(argv[1])) : 1000; const std::size_t payload_bytes = (argc >= 3) ? std::size_t(std::atoll(argv[2])) : 1024; // Create a socketpair for the two processes to talk over. Set it // non-blocking up front — Cap'n Proto's event loop needs non-blocking fds. // We use a socketpair (not TCP) so the document process has no network // address to reach and the sandbox can deny socket() outright; a TCP // connection would let the sandboxed process talk to the network stack. int fds[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { std::fprintf(stderr, "socketpair failed: %s\n", std::strerror(errno)); return 2; } // Fork: child becomes the sandboxed document process; parent is the UI. // The child inherits the socketpair fd; the sandbox (installed in the // child after fork) denies socket/connect/open/fork/exec but allows the // already-open fd the UI handed it. pid_t pid = fork(); if (pid < 0) { std::fprintf(stderr, "fork failed: %s\n", std::strerror(errno)); return 2; } if (pid == 0) { // Child — document process. Close the parent's end, run on our end. close(fds[0]); int rc = freepdfeditor::spike::e::run_document_process(fds[1]); close(fds[1]); _exit(rc); } // Parent — UI process. Close the child's end, drive the channel, measure. close(fds[1]); auto t0 = std::chrono::steady_clock::now(); auto r = freepdfeditor::spike::e::run_ui_process( fds[0], fds[1], n_requests, payload_bytes); auto t1 = std::chrono::steady_clock::now(); close(fds[0]); // Reap the child. int status = 0; waitpid(pid, &status, 0); double wall_us = std::chrono::duration(t1 - t0).count(); freepdfeditor::spike::SpikeResult sr{}; sr.spike = "E"; sr.name = "sandbox + IPC bring-up (seccomp-bpf, length-prefixed socketpair)"; sr.total = n_requests; sr.passed = r.requests_acked; sr.failed = n_requests - r.requests_acked; sr.errored = r.ok ? 0 : 1; sr.metric_name = "round_trip_success_rate"; sr.metric_value = n_requests ? double(r.requests_acked) / double(n_requests) : 0.0; sr.target = 1.0; sr.gate_met = r.ok; char buf[512]; std::snprintf(buf, sizeof(buf), "requests=%zu acked=%zu payload=%zuB " "avg_latency=%.1fus min=%.1fus max=%.1fus wall=%.1fus " "child_exit=%d; seccomp-bpf deny=open/socket/connect/fork/exec, " "allow=memory/read/write/poll/epoll/exit", n_requests, r.requests_acked, payload_bytes, r.avg_latency_us, r.min_latency_us, r.max_latency_us, wall_us, WIFEXITED(status) ? WEXITSTATUS(status) : -1); sr.notes = buf; if (!r.ok) sr.notes += "; " + r.error; return freepdfeditor::spike::emit_json_report(sr); }