Files
docview/docs/windows-sandbox-port.md
2026-09-21 13:41:40 +09:00

9.7 KiB

Windows worker sandbox port

Status: the Windows broker, bounded handle transport, and PDF/archive worker bootstrap are connected in source. Native Windows execution and an MSVC/Windows Qt build have not been performed. Successful Linux regression tests and a compile-only check against Wine declarations are not Windows security or compatibility evidence. R12, G-SANDBOX on Windows, and distribution acceptance remain open.

Launch and verification

WorkerProcess uses WindowsRuntimeStaging, createWindowsPipePair, and launchWindowsWorker on Windows. There is no unprotected QProcess fallback. The broker opens the original document with read access, checks the file type and reparse attributes, and supplies only duplicated handles to the worker. The shared installation tree and original document ACL are never changed.

The launcher creates a unique temporary AppContainer profile and requires the LPAC process attribute, zero capability SIDs, low integrity, a one-process Job, a 1.5 GiB per-process committed-memory limit, kill-on-close, no Job breakaway, and desktop/clipboard restrictions. The child-process policy attribute also blocks child creation. The process starts suspended; token identity and Job limits are checked before it resumes. Environment variables are reduced to the private runtime/System32 search path and SystemRoot. Arguments use Windows C runtime quoting without a shell.

Handle inheritance is limited to a read-only document handle, two directional overlapped pipes, and one inert NUL handle for standard streams. The child receives --document-handle, --ipc-read-handle, --ipc-write-handle, and --sandbox-sid; it rejects --source and --socket on Windows. startWorkerRuntime verifies the actual kernel token, LPAC state, SID, capability count, integrity level, immediate Job membership and limits, and private-profile access restrictions. A flag or SID argument alone cannot establish protection. A failed bootstrap exits with code 5 before parsing.

The document handle is duplicated again with only read rights and the inherited original is closed. _open_osfhandle transfers the restricted handle to a CRT descriptor; QFile owns that descriptor. PDFium's file-access callback and libzip's seekable source callback use this already-open QFile. They never reopen the original path. Source callbacks retain bounded index and entry checks; archive output is streamed as provisional chunks to the Main broker, which alone verifies and commits files.

WindowsPipeDevice uses overlapped I/O, QWinEventNotifier, a 256 KiB transport read buffer and a 16 MiB write queue. CBOR framing retains the 1 MiB control / 8 MiB render limits. Worker extraction drains pending transport writes above 128 KiB, with a five-second timeout; synchronous parser callbacks reject reentrant commands during this drain. WorkerProcess waits for a protected bootstrap ping before reporting the Windows worker ready.

Private profile and runtime storage

Creating an AppContainer also creates writable private storage. That default is insufficient for a read-only parser. The launcher replaces DACLs only on its newly generated profile and broker-created runtime copies: the owner retains full access, the package receives read/execute access, and explicit package deny entries block file creation, writes, deletion, ACL changes, and ownership changes. Existing descendant entries are treated similarly. Reparse points and files with multiple hard links are rejected. The profile filesystem is checked again while the child is suspended.

The broker briefly impersonates the suspended child's token to obtain its own profile registry HKEY with GetAppContainerRegistryLocation, then returns to the broker identity before applying protected DACLs recursively. Package registry access is limited to read; creating keys/links, setting values, deleting keys and changing ACLs/ownership are explicitly denied. Unexpected registry links, oversized trees, and API failures stop launch. The child independently attempts to acquire each write permission on its profile directory and registry root and requires access-denied results. These checks do not create files or values.

The launcher exposes no writable worker output directory. The profile is deleted on normal NativeProcess destruction after Job termination and handle closure; recovery after interrupted broker shutdown still needs native validation. Runtime staging uses a finite DLL/executable manifest with hashes and private owner permissions. It does not copy user font directories or grant access to document parents, user configuration/state, or shared installation directories.

Font behavior and remaining validation

The PDF worker now installs the public FPDF_SYSFONTINFO version 2 adapter and requests selected font bytes through font.map, font.read, and font.close on its existing protected IPC transport. It does not reactivate PDFium's default filesystem font provider. Main does not link PDFium or parse SFNT/TTC tables. See the font broker contract.

The source Windows backend builds an OS font index with EnumFontFamiliesExW, selects an indexed family using CreateFontIndirectW and a thread-owned HDC, verifies GetTextFaceW/GetTextMetricsW, and takes a bounded snapshot through GetFontData. A TTC includes its collection bytes and the selected face offset derived from GDI's collection and table-zero sizes. Only an opaque session ID, selected metadata, and 64 KiB byte chunks reach the worker; HDC/HFONT and paths stay in Main. GDI calls run on dedicated background threads, limited to two across active and revoked services, with 256 MiB of total snapshot charges. A shared LRU can reclaim unused snapshots from either service while protecting issued handles. Revocation drops late replies without joining an in-progress OS call. Variable-font instances are skipped and subsequent static candidates are tried. No extra LPAC font capability or font-directory ACL grant was added.

This Windows backend passed only a supplementary object-compilation check against Wine declarations and Linux Qt headers. An MSVC/Windows Qt build, actual GDI selection and TTC behavior, font RPC through LPAC, Japanese fallback, glyph metrics, and non-ASCII family names remain unverified on native Windows. Linux broker and drawing results are not Windows fallback-font evidence. The implementation follows the pinned PDFium Windows adapter and Microsoft's GetFontData contract; their semantics still need native execution tests.

Native acceptance must first prove successful PDF/ZIP/EPUB operation, then reject source writes and path reopen, sibling/configuration/state reads, external and loopback connections, child creation, unrelated inherited handles, runtime mutation, and writes to own profile, its Temp descendants, own registry storage, and arbitrary temporary directories. Tests must check actual API error codes and distinguish denied access from malformed test setup. Also verify memory enforcement, broker termination, watchdog/restart, profile cleanup, non-ASCII paths, resource reparse protection, renderer restrictions, and clean packaged installs. Each protection setup failure must leave the worker stopped.

Evidence recorded on the Linux development host

After the shared bootstrap and libzip handle callback changes: Archive 57 passed, including reading an unlinked-but-open source, rejecting writable/null sources, source-lifetime failure, and unchanged source hashes; PDF 15 passed through the production WorkerProcess route; Canvas 11 passed with real sandboxed PDF worker IPC. These exercise the Linux path and platform-independent callback behavior only.

windows_sandbox.cpp also passed a C++ syntax-only check using Linux Qt declarations and Wine Win32 headers. The local Wine headers lack current AppContainer declarations/constants, so documented Microsoft declarations were supplied in a temporary compile-only prefix. This is not a link test, ABI check, Windows Qt build, or native execution, and is not an acceptance gate.

API references