freepdfeditor/spike/E_sandbox/main.cpp

106 lines
4.0 KiB
C++

// 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 <cerrno>
#include <chrono>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
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<double, std::micro>(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);
}