freepdfeditor/spike/E_sandbox/DocumentProcess.cpp

98 lines
3.6 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
//
// DocumentProcess.cpp — the sandboxed document process. Installs the seccomp
// filter, then serves Parse requests over the socketpair fd using a trivial
// length-prefixed binary protocol.
//
// Protocol (the "parse" operation counts the bytes it was handed):
// Request: [u64 request_id][u64 payload_len][payload_len bytes]
// Response: [u64 request_id][u64 byte_count][u8 ok]
//
// The spike's goal (§14 step 8) is to measure the cost of the process split
// and prove the sandbox works, not to exercise Cap'n Proto. An earlier
// version used Cap'n Proto two-party RPC over the socketpair; the server
// received and processed requests (the parse handler ran) but the responses
// never reached the client — a real integration issue recorded in the spike
// result doc for M2 to debug with the full event-loop integration. The raw
// protocol measures the channel cost without that blocker.
#include "DocumentProcess.h"
#include "Sandbox.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <unistd.h>
namespace freepdfeditor::spike::e {
namespace {
// Read exactly n bytes from fd (handles partial reads). Returns false on EOF/error.
bool read_exact(int fd, void* buf, std::size_t n)
{
auto* p = static_cast<char*>(buf);
while (n > 0) {
ssize_t r = read(fd, p, n);
if (r <= 0) return false;
p += r; n -= std::size_t(r);
}
return true;
}
// Write exactly n bytes to fd (handles partial writes). Returns false on error.
bool write_exact(int fd, const void* buf, std::size_t n)
{
const auto* p = static_cast<const char*>(buf);
while (n > 0) {
ssize_t w = write(fd, p, n);
if (w <= 0) return false;
p += w; n -= std::size_t(w);
}
return true;
}
} // namespace
int run_document_process(int fd)
{
// Allow disabling the sandbox via an env var for diagnosing RPC issues
// (the spike's gate requires the sandbox ON; this is a debug aid only).
const char* skip = std::getenv("FPE_SPIKE_E_NO_SANDBOX");
if (skip == nullptr || skip[0] == '\0') {
// Install the sandbox BEFORE any untrusted data arrives. After this,
// the process cannot open files, create sockets, fork, or exec.
if (install_sandbox() != 0) {
std::fprintf(stderr, "[doc] sandbox install failed\n");
return 1;
}
} else {
std::fprintf(stderr, "[doc] sandbox DISABLED (FPE_SPIKE_E_NO_SANDBOX set)\n");
}
// Serve requests until the UI process closes the channel (EOF on read).
for (;;) {
std::uint64_t request_id = 0, payload_len = 0;
if (!read_exact(fd, &request_id, sizeof(request_id))) break; // EOF
if (!read_exact(fd, &payload_len, sizeof(payload_len))) break;
// Per ADR-0004 the document process is untrusted: bound-check the
// length it sent. The UI also checks, but defence in depth.
if (payload_len > 16 * 1024 * 1024) break; // 16 MiB cap
std::vector<unsigned char> payload(payload_len);
if (!read_exact(fd, payload.data(), payload_len)) break;
// The "parse": count the bytes.
std::uint64_t byte_count = payload_len;
std::uint8_t ok = 1;
if (!write_exact(fd, &request_id, sizeof(request_id))) break;
if (!write_exact(fd, &byte_count, sizeof(byte_count))) break;
if (!write_exact(fd, &ok, sizeof(ok))) break;
}
return 0;
}
} // namespace freepdfeditor::spike::e