A converter's job is running a hostile parser.
Every conversion means executing a memory-unsafe parser over a file a stranger wrote. Running locally moves that parser onto your machine. Local without confinement is a downgrade, not an upgrade — so confinement is the floor, and nothing in the config can lower it.
Five stages, each doing one thing.
| stage | what it does |
|---|---|
| detect | Identifies the file by its bytes, never its extension. Polyglots are refused outright. |
| route | Builds the plan from classed routes. Refusal is an outcome like any other, with its reason named. |
| confine | Each engine gets its own OS sandbox: a filesystem slice ending at the job directory, no network, open descriptors instead of filenames. |
| execute | The engine tries what it wants. The kernel answers. Both are logged. |
| receipt | What happened is written down beside the output — engines, classes, limits, hashes. |
-
connect() → 203.0.113.7:443EPERM no network namespace -
open("~/.ssh/id_rsa")EACCES outside the job directory -
fork() → /bin/shEPERM seccomp denies execve -
write("../../Startup/x.lnk")EACCES not a single path component
invoice.pdf → invoice.png completed · nothing left the job directory
A machine that cannot deny an engine the network does not convert. The mechanism per platform is on features → platforms.
An archive member named .bashrc is not an escape. It is a filename.
-
.bashrctruncated on open · previous contents gone -
id_rsatruncated on open · previous contents gone -
resume.docxtruncated on open · previous contents gone -
Thumbs.dbtruncated on open · previous contents gone
-
.bashrcuntouched · written as.bashrc (1) -
id_rsauntouched · written asid_rsa (1) -
resume.docxuntouched · written asresume.docx (1) -
Thumbs.dbuntouched · written asThumbs.db (1)
Every create is O_EXCL; no truncating open exists anywhere in the codebase,
and a source scan fails the build if one appears. Structural, not a setting.
Model weights are untrusted input too.
Pickle is refused at the file-type layer, before anything opens it. Safetensors, GGUF and ONNX only; hash-pinned on every load; ONNX custom operators rejected against an allowlist; models fetched from our signed index. A malicious checkpoint is treated exactly like a malicious document.
What we do not protect against.
Published because it is rare, and rare because it costs vendors cover stories. This is the threat model's own table, word for word.
open the table ▾
| A full exploit chain through the sandbox stack | A memory-corruption bug plus a working kernel or AppContainer escape defeats us. We reduce the odds — four workers, memory-safe fast paths, no Ghostscript, per-adapter fuzzing, a kill switch — but we do not claim immunity |
| A compromised operating system | If the OS or a kernel driver is already hostile, nothing in user space helps |
| Hardware side channels | Spectre-class attacks across the sandbox boundary are out of scope |
| Internet-provenance detection | Mark-of-the-web and the macOS quarantine xattr fail open. A file that reached the disk via sync, a non-NTFS volume, a USB stick, an archive extracted by another tool, or a copy operation that drops the attribute looks local — because the OS has told us nothing else. The automatic floor raise is a useful signal, not a guarantee, and a user who knows a file is untrusted should set the floor themselves |
| Data carried between files in one batch by a compromised worker | Under the default `worker_reuse: Balanced` ([03 §8.4](03-ARCHITECTURE.md#84-the-worker-session-and-how-many-files-one-worker-sees)) one worker process serves several files that share a trusted provenance. A capability is revoked between files — verified, including inside an AppContainer — but revocation removes the handle, not the memory. An engine compromised by file A can retain A's bytes and write them into the output of file B in the same batch. Files of *untrusted* provenance never share a worker — asserted by SR-20, because a mitigation named in this table and tested nowhere is not a mitigation — and `worker_reuse: Isolated` removes the residual entirely at ~38 ms per file on Windows. The receipt records which files shared a worker, so this is disclosed per conversion and not only here |
| Our own copy not preserving mark-of-the-web | Untrusted-provenance inputs are copied before routing, and the mandated `create_new` path strips `Zone.Identifier` (S26). `Provenance` is therefore captured at `detect()` before any copy exists and never re-derived from the copy ([03 §11.3](03-ARCHITECTURE.md#113-input-identity)). Any future code path that re-reads provenance from a job-dir copy would silently see a local file |
| One archive extracting to different files on different platforms | NTFS does not fold NFC/NFD; APFS does; ext4 does not (S13, S24). The same archive containing both forms of one name yields two files on Windows and Linux and one on macOS. We preserve names byte-for-byte and treat NFC-equal names as a collision, which is the best available behaviour — it does not make the outcome identical across platforms, and for a product whose second pillar is telling you what happened, that is worth stating |
| In-place modification of an input we are reading | We hold one read handle per input, which defeats replace-by-rename. A process that already has write access to the same inode can still modify it underneath us. Untrusted-provenance and volatile-volume inputs are copied first; local trusted inputs are not, because copying every byte of every conversion is a real cost |
| Semantically harmful but structurally valid content | A perfectly well-formed PDF containing a fraudulent invoice converts cleanly. We check structure, not truth |
| The user's own destination choices | Converting to a network share or a synced folder sends the output there. We refuse to overwrite; we do not refuse to write |
| Malicious models the user fetches deliberately | A hostile ONNX graph from an untrusted source is a risk the user has chosen. The persistent indicator says so |
| Correctness of third-party engines | libvips can produce a wrong image without being compromised. The receipt records what ran; it does not certify the result |
| A user who has already lost their account | Every file in [03 §12](03-ARCHITECTURE.md#12-state-on-disk) is writable by the user, so an attacker who is the user can edit them. A16's controls stop those files being an *escalation* path, not an access path |
| Physical access and shoulder-surfing | Out of scope |
Found something? Tell us.
Reports go to the contact in /.well-known/security.txt, coordinated disclosure
respected, response inside a week. The weekly revocation manifest exists so that a fix to
an engine reaches every install within seven days of it being known.
Formal notices: contact.