// 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 Cap'n Proto // two-party RPC (the M2 transport, ADR-0004). // // Integration note (the Spike E stall, now resolved): the server side CANNOT // use `EzRpcServer(int fd, ...)` — that constructor expects a LISTENING // socket and calls accept() on it. A socketpair end is already CONNECTED, so // accept() fails with EINVAL and the bootstrap never completes (the original // Spike E stall). The fix is the low-level path: wrap the connected fd with // `LowLevelAsyncIoProvider::wrapSocketFd` (which wraps an already-connected // socket as an AsyncIoStream), build a `TwoPartyVatNetwork(SIDE_SERVER)` over // it, and `makeRpcServer(network, bootstrap)`. This is the shape the // production M2 transport uses. // // The sandbox is installed BEFORE the KJ event loop runs. The seccomp // allow-list therefore includes the epoll/eventfd/timerfd syscalls the KJ // async I/O layer needs (Sandbox.cpp); the raw-protocol variant only needed // read/write. #include "DocumentProcess.h" #include "Sandbox.h" #include "ipc.capnp.h" #include #include #include #include #include #include #include #include namespace proto = freepdfeditor::spike::e; namespace freepdfeditor::spike::e { namespace { // The document-process capability: a trivial "parser" that counts the bytes // it was handed and returns the count. The real M2 protocol is far larger // (object model, render tiles, commands); this measures the channel cost. class DocumentProcessImpl final : public proto::DocumentProcess::Server { public: kj::Promise parse(ParseContext ctx) override { auto req = ctx.getParams().getRequest(); // Per ADR-0004 the document process is untrusted: bound-check the // payload length it was handed (defence in depth; the UI also checks). auto data = req.getData(); if (data.size() > 16 * 1024 * 1024) { // 16 MiB cap auto r = ctx.getResults().initResult(); r.setRequestId(req.getRequestId()); r.setOk(false); r.setError("payload exceeds 16 MiB cap"); return kj::READY_NOW; } auto r = ctx.getResults().initResult(); r.setRequestId(req.getRequestId()); r.setByteCount(data.size()); r.setOk(true); return kj::READY_NOW; } }; } // 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"); } // Set up the KJ async I/O context. This creates the epoll/eventfd // machinery the event loop needs (all allowed by the seccomp filter). kj::AsyncIoContext io = kj::setupAsyncIo(); // Wrap the already-connected socketpair end as an AsyncIoStream. This is // the key difference from the stalled EzRpcServer path: wrapSocketFd does // NOT call accept() — it uses the fd as-is for read/write. kj::Own stream = io.lowLevelProvider->wrapSocketFd(fd); // Two-party RPC: the document process is the SERVER side (it serves the // bootstrap capability the UI imports via getMain()). capnp::TwoPartyVatNetwork network(*stream, capnp::rpc::twoparty::Side::SERVER); auto server = capnp::makeRpcServer(network, kj::heap()); // Run the event loop until the UI process disconnects (onDisconnect // resolves), then return cleanly. auto disconnect = network.onDisconnect().then([]() { std::fprintf(stderr, "[doc] UI disconnected\n"); }); try { disconnect.wait(io.waitScope); } catch (kj::Exception& e) { std::fprintf(stderr, "[doc] event loop error: %s\n", e.getDescription().cStr()); return 1; } return 0; } } // namespace freepdfeditor::spike::e