Sandbox protocol¶
This page describes the interface between Dangerzone (the host) and the sandbox that converts a document to pixels. It is the "API" of the container image. The image and the conversion code it runs are maintained in the dangerzone-image repository. The host side lives in dangerzone/isolation_provider/.
Invocation¶
For each document, Dangerzone starts one container from the sandbox image and runs:
On Qubes OS with the native integration, the equivalent is the dz.Convert RPC call to a disposable qube (and dz.ConvertDev during development).
The container is started with these flags, in addition to the image digest pinned after signature verification:
| Flag | Purpose |
|---|---|
--network=none |
No network access |
--cap-drop all --cap-add SYS_CHROOT |
Drop every capability except the one gVisor needs to set up its root |
--security-opt no-new-privileges |
No privilege escalation |
--security-opt seccomp=<profile> |
Bundled seccomp profile that allows gVisor to run (share/seccomp.gvisor.json), since default profiles may block ptrace(2) |
--userns nomap |
Do not map the host user into the container (Podman 4.1 and later) |
--security-opt label=type:container_engine_t |
SELinux label that lets a container engine run nested, on hosts that use SELinux |
-u dangerzone |
Run as the unprivileged dangerzone user inside the container |
--log-driver none |
Do not keep container logs |
--rm |
Remove the container when it exits |
-i |
Keep standard input open |
-e RUNSC_DEBUG=1 |
Only with --debug: make gVisor log verbosely |
Inside the container, the conversion runs under gVisor (runsc), which in turn spawns an inner container, with no capabilities and privileges. You can read more about it in the gVisor integration docs.
Input¶
The host writes the raw bytes of the untrusted document to the container's standard input, then closes it. No filename, format, or options are sent: the sandbox detects the format itself.
Output¶
The container writes a binary stream to standard output. All integers are 2-byte unsigned big-endian values (INT_BYTES = 2).
n_pages uint16
repeat n_pages times:
width uint16
height uint16
pixels width * height * 3 bytes, RGB, row-major
Pages are rendered with a DPI (dots per inch) of 150, which strikes the best balance between OCR accuracy, document size, and readability.
The host validates every value before using it:
| Value | Accepted range | Error otherwise |
|---|---|---|
n_pages |
1 to MAX_PAGES (10000) |
MaxPagesException |
width |
1 to MAX_PAGE_WIDTH (10000) |
MaxPageWidthException |
height |
1 to MAX_PAGE_HEIGHT (10000) |
MaxPageHeightException |
A short read (fewer bytes than announced) raises ConverterProcException. After the last page, the host closes standard output and ignores anything else. Each page's pixels are compressed and turned into a PDF page on the host (with OCR if requested) as soon as they arrive, so memory use grows slowly with the document size.
Standard error¶
Standard error carries the sandbox's own diagnostics. It can be enriched with debug information from gVisor and Podman by passing the --debug flag. Since it's untrusted, it is sanitized as well and any control characters are stripped before writing to the terminal.
Exit codes¶
The container communicates failures through its exit code. Conversion errors start at ERROR_SHIFT = 128:
| Exit code | Meaning |
|---|---|
| 128 | Unspecified conversion error |
| 138 | The document format is not supported |
| 144 | HWP / HWPX formats are not supported in Qubes |
| 148 | Conversion to PDF with LibreOffice failed |
| 158 | The document appears to be corrupted and could not be opened |
| 168 | Page-related error (generic) |
| 169 | Number of pages could not be extracted from PDF |
| 170 | Number of pages exceeds maximum |
| 172 | A page exceeded the maximum width |
| 173 | A page exceeded the maximum height |
| 174 | Page count mismatch between the document and the rendered pages |
| 228 | Some unexpected error occurred while converting the document |
| 126 | Qubes OS only: the qrexec call failed |
Any other code maps to "Unknown error code". The host-side definitions are in dangerzone/conversion_errors.py.
Versioning¶
The image is published as ghcr.io/freedomofpress/dangerzone/v1. The v1 suffix is the protocol version: a change that breaks this interface will be published under v2, and a Dangerzone release will switch to it. See Independent sandbox updates.