115 lines
4.5 KiB
C++
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
|