openconvert
the problem

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.

build order

Five stages, each doing one thing.

stagewhat it does
detectIdentifies the file by its bytes, never its extension. Polyglots are refused outright.
routeBuilds the plan from classed routes. Refusal is an outcome like any other, with its reason named.
confineEach engine gets its own OS sandbox: a filesystem slice ending at the job directory, no network, open descriptors instead of filenames.
executeThe engine tries what it wants. The kernel answers. Both are logged.
receiptWhat happened is written down beside the output — engines, classes, limits, hashes.
recorded · oc-images under Landlock + seccomp 4 attempts · 4 denied
  1. connect() → 203.0.113.7:443 EPERM no network namespace
  2. open("~/.ssh/id_rsa") EACCES outside the job directory
  3. fork() → /bin/sh EPERM seccomp denies execve
  4. 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.

non-destructive

An archive member named .bashrc is not an escape. It is a filename.

a typical extractor 4 files replaced
  1. .bashrc truncated on open · previous contents gone
  2. id_rsa truncated on open · previous contents gone
  3. resume.docx truncated on open · previous contents gone
  4. Thumbs.db truncated on open · previous contents gone
openconvert 4 files added
  1. .bashrc untouched · written as .bashrc (1)
  2. id_rsa untouched · written as id_rsa (1)
  3. resume.docx untouched · written as resume.docx (1)
  4. Thumbs.db untouched · written as Thumbs.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.

supply chain

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.

limits

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
disclosure

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.