freepdfeditor/spike/E_sandbox/DocumentProcess.cpp

115 lines
4.5 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 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 <capnp/rpc-twoparty.h>
#include <capnp/rpc.h>
#include <kj/async-io.h>
#include <kj/memory.h>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <unistd.h>
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<void> 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<kj::AsyncIoStream> 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<DocumentProcessImpl>());
// 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