Doctor sealed input v1
September 10, 2026 ยท View on GitHub
Status: implemented private input boundary; HOSTED GREEN under the v0.4.0 release baseline. This primitive alone supplies no profile format, CLI activation, executable isolation or WP-05 completion.
Audience: CLI/platform contributors and reviewers.
Purpose and authority
The future offline doctor backend needs to obtain bytes without discovering or
reading an arbitrary host filesystem first. The unpublished safe platform facade
exposes DoctorOfflineInput::acquire(&File, max_bytes). Its caller provisions
and retains the input file; the acquisition never interprets a pathname, reads
an environment variable, or takes ownership of that file.
The returned opaque carrier owns only an immutable byte vector. Its contents are
untrusted and are not a parsed profile, authenticated selector, trusted tool,
loader closure, filesystem root, or permission to execute. There is no descriptor
accessor, mutable byte accessor, ambient lookup, registry, format serializer, or
publication operation. This is an input primitive, not an admitted DoctorHost.
The real doctor CLI still reports unavailable profiles.
Anonymous carrier creation
The separate create_doctor_offline_input(bytes, max_bytes) function accepts
explicit borrowed bytes and returns (File, DoctorOfflineInput). It fills one
fresh anonymous memory file, seals it, then uses the existing acquire operation
and exact byte comparison before transferring either result. The snapshot is
not constructed directly from caller bytes. Returning both objects avoids
requiring the caller to reacquire the snapshot just to parse a bundle or derive
its request. The caller owns the returned file and its subsequent lifetime;
dropping that file does not invalidate the snapshot.
Creation validates a zero limit (Invalid), above-ceiling limit (Limit), empty
input (Invalid) and input above the selected limit (Limit), in that order,
before any OS operation. Only the same native64 Linux configurations as
acquisition are admitted; other hosts then return Unsupported. It creates
with MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL. There is no retry with
weaker flags or filesystem fallback. EINVAL or ENOSYS from creation returns
Unsupported; other operational failures return Io.
The explicit non-executable flag creates a file without execute permission and
sets F_SEAL_EXEC, preventing later addition of execute permission. It does not
promise to prevent every executable mapping or use of copied content. See the
Linux non-executable memory-file contract.
No sysctl or host policy is modified; kernels or policies that cannot supply the
mandatory property reject. This stronger creation prerequisite does not change
the existing borrowed acquisition's accepted files or kernel requirements.
Writes are positional, at most 8,192 bytes each, with exact counts required.
Short, zero, interrupted or failed writes reject without retry or partial
publication. The shared file offset stays zero. Before returning, creation
requires all four immutable seals from the acquisition contract, the executable
seal, a regular file with no execute bits, the exact size, and close-on-exec.
Disagreed properties return Invalid; failed property queries return Io.
Only after these checks does ordinary sealed acquisition authenticate storage
and copy the snapshot; its existing errors remain unchanged. An unequal snapshot
rejects without returning either object.
Only the newly created descriptor is owned during failure cleanup; no supplied file, pathname or arbitrary inherited descriptor is touched. Ownership is consumed before exactly one checked close. A negative close result terminates the process; it is never retried or followed by another operation on that descriptor number. This new factory deliberately treats uncertain closure as fail-stop rather than inferring completion from an error. It does not change the existing acquisition's no-close contract or the caller's ownership after a successful return. Normal cleanup cannot replace the primary selected error.
The byte ceiling bounds the carrier and snapshot individually, not total resident memory: caller bytes, memory-file storage and the acquired snapshot can coexist. The bounds constrain application allocation and syscall counts, not hard real-time latency, kernel/LSM/VM behavior or aggregate host resource use. Creation does not authenticate content provenance, parse a profile, discover dependencies, start a worker, configure namespaces, or activate the ordinary CLI. Its returned file is transport storage, not execution authority.
Executable image creation
The separate create_doctor_offline_executable(bytes, max_bytes) factory returns
the same (File, DoctorOfflineInput) shape for worker/collector image storage.
It does not change the non-executable factory or turn borrowed input acquisition
into an executable validator. The safe facade delegates to the existing sys
quarantine; both factories share one private creation, bounded-write, snapshot
comparison and checked failure-cleanup implementation.
Common zero-limit, above-ceiling, empty-input and input-length checks retain
their order. Unsupported platforms then return Unsupported. On supported
native64 Linux hosts, the existing minimum ELF validator checks the explicit
bytes against the current architecture before creating a descriptor. Scripts,
malformed framing and foreign images return Invalid without creation or
cleanup effects. This is the same structural validator used by the bundle and
launcher, not a second parser. A structurally admitted interpreter name is not
looked up and does not prove that the image can load.
The executable route requires MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_EXEC.
Immediately after acquiring ownership of the new descriptor it sets mode
0500, before writing image bytes. The already-open read/write description
is used for bounded positional writes; no path reopen or temporary disk file
is needed. It adds the four immutable seals and F_SEAL_EXEC together, then
requires their presence, a regular file, exact permission/special bits 0500,
exact length and close-on-exec. Additional kernel seals are permitted. Existing
sealed acquisition and exact byte comparison precede transfer of either result.
The returned shared file offset is zero, and dropping the file leaves the
snapshot valid. Creation/query/write/cleanup errors retain the ordinary input
factory's error and one-shot ownership rules, including fail-stop uncertain
closure. There is no retry with weaker flags, executable-mode downgrade, sysctl
change or filesystem fallback when the host prohibits executable memory files.
Linux can reject explicit executable memfds under its namespace policy.
F_SEAL_EXEC prevents changing execute bits, not every permission or metadata
field; executable sealing may add further write-related seals. See the
kernel API and
seal implementation.
Mode 0500 is checked at handoff, not promised immutable against later caller
metadata operations. No factory result attests a particular worker/collector
role, provenance, library/configuration closure, credential behavior or binfmt
policy. It does not execute the bytes or confine other code in the calling
process. The provisioner still owns those facts and aggregate resources;
the launcher independently validates its actual
inherited descriptors before use. Ordinary CLI admission remains unavailable.
Admission and ordering
- Reject a zero caller limit with
Invalid; reject a caller limit above the immutable 1,073,741,824-byte (1 GiB) ceiling withLimit, before any syscall. Callers may lower this ceiling, not widen it. The ceiling is a resource bound, not a promise that every Clang/Node/Rust distribution fits into one carrier. It is not an authority boundary: seals, digests, the release signature, the ELF contract and the closed inventory decide admission, and none of them depend on size. Its derivation records the measurements it comes from and the bounds it must stay coherent with. - Only native 64-bit little-endian Linux x86-64/AArch64 is admitted. Other hosts
return
Unsupportedwithout querying the supplied file. - Borrow the still-owned file descriptor for the entire acquisition. Perform
F_GET_SEALSbefore metadata or content access. Require all ofF_SEAL_WRITE,F_SEAL_GROW,F_SEAL_SHRINK, andF_SEAL_SEAL. Other seal bits may coexist;F_SEAL_FUTURE_WRITEdoes not replaceF_SEAL_WRITE. - Only after that query authenticates immutable memory-file storage, require
fstatfsto reportTMPFS_MAGIC(excluding hugetlb), thenfstatto report a regular file with a nonnegative, nonempty length within the caller limit. - Reserve the bounded output storage fallibly. Copy with positional
preadcalls of exactly 8,192 bytes, except for the final remaining bytes. Any incomplete, zero, or failed read, including interruption, returnsIowith no partial carrier and no retry. Successful acquisition needs exactlyceil(length / 8192)reads and never changes the shared file offset. - Return only the complete owned bytes. The caller's file is neither duplicated nor closed on success or failure. Dropping it afterward does not invalidate the returned bytes.
The seal query returning EINVAL, a missing required seal, or invalid
storage/type/empty length rejects as Invalid. Other seal-query errors,
including EINTR and EBADF, reject as Io. An admitted storage length above
the requested bound rejects as Limit; metadata/read/allocation failures
reject as Io. Unsupported hosts do
not try another path. No input bytes are allocated or read before the storage
and size checks. This bounds application allocations and syscall counts, not
hard real-time latency of the kernel or allocator.
Carrier ceiling derivation
The 1,073,741,824-byte ceiling is measured, not chosen. On a GitHub-hosted
ubuntu-24.04 runner, encoding the full loader closures the pivoted worker root
requires (scripts/doctor-provisioned-linux-bundle.py --closure) gives:
| carrier | encoded bytes |
|---|---|
| node v22.23.2 + rustc 1.88.0, no Clang | 462,424,370 |
| + clang 9.0.1, the smallest official LLVM that runs on 24.04 | 568,339,434 |
| + clang 17.0.6 | 652,142,493 |
| Ubuntu's own clang-18 closure | ~713,000,000 |
render_rows admits only Node major 22 or newer and Rust 1.88 or newer, so the
two non-Clang roles cannot shrink. Under the previous 536,870,912-byte ceiling
those two closures alone took 86% of it, leaving 74,446,542 bytes for a whole
Clang role that no official LLVM release fits, so no current real distribution
set could satisfy the two real-distribution lifecycle fixtures the
production provisioner requires.
1,073,741,824 is 1.65 times the measured clang-17 three-role carrier and 1.51 times Ubuntu's clang-18 closure, so a Clang role may grow by half again before the ceiling binds. It remains a hard bound: 6.25% of a hosted runner's 16 GB.
The ceiling is not independent of the confined scope it feeds. A carrier of N
bytes costs 2N of unswappable residency inside the delegated cgroup-v2 scope the
provisioner installs: the worker's whole-carrier heap snapshot, which the root
plan borrows and therefore cannot release before the tool children run, plus the
page-rounded tmpfs root written out of it. That scope sets memory.swap.max to
0 and memory.oom.group to 1, so an overshoot kills the whole scope instead of
refusing cleanly. Its memory.max is therefore held at four times this ceiling:
2N of carrier residency and 2N covering page rounding, the bounded reply and
output buffers, and the resident tool. Raising one without the other makes the
cgroup the real ceiling, and a scope-wide OOM is an unsettled failure rather
than a Limit.
Three further bounds cover the same bundle and request bytes on their own paths
and are held equal to this ceiling, because the smallest of them is always the
effective limit: the signed capsule's MAX_ARTIFACT_BYTES
(production provisioner),
the release directory's MAX_ARTIFACT_BYTES, and the signed store's per-member
cap (signed install). Capsule artifact lengths
are fixed-width u64 and the bundle's own record lengths are u64 over usize
offsets, so neither value is near an encoding limit at either size.
Why the ordering matters
Linux routes F_GET_SEALS directly to memory-file seal inspection, recognizing
shmem/hugetlb storage rather than calling an arbitrary filesystem's metadata or
read handlers. The subsequent tmpfs check excludes hugetlb storage. See the
fcntl dispatch
and memory-file seal implementation.
Duplicating an arbitrary input first is not harmless: closing the rejected duplicate can call a filesystem flush handler even while the original is open. The implementation therefore never duplicates or closes the borrowed input. See the kernel close path. The safe Rust borrow keeps the file owned throughout the call; foreign unsafe code closing or replacing its raw descriptor violates that I/O-safety contract.
This reasoning excludes arbitrary supplied-filesystem metadata/read/flush dispatch during acquisition. It does not confine provisioning before the call, the kernel, LSM hooks, auditing, swap/paging, or other process activity. It does not establish any property of executable bytes or of a future launched process. In particular, seals prove immutable contents, not trusted provenance.
Required evidence
The focused sys fixtures must use real sealed memory files and cover exact binary bytes, unchanged caller position/flags/seals and continued handle use, all missing mandatory seal combinations, future-write-only rejection, live writable-map rejection, ordinary files/directories/pipes/sockets and O_PATH, exact/plus-one limits, sparse oversize rejection before output allocation, and failed/short/interrupted read non-publication. Per-invocation test-only operation observations must be calibrated by successful physical acquisition and prove that seal rejection never reaches metadata, allocation or reads. Metadata and read-failure injections exercise the shared rejection paths; they are simulations, not evidence of physical kernel faults or signal delivery. The no-duplicate/no-close property additionally requires source review: leaving the original descriptor usable alone would not detect a duplicate-close flush.
The facade retains forbid(unsafe_code) and only delegates to the existing OS
quarantine. Earlier CLI/profile and version-probe lifecycle fixtures are
unchanged and remain required. Unsupported-host behavior must be exercised
separately from Linux success. The four creator common-precedence and
unsupported-host tests passed locally on macOS arm64 with Rust 1.98; this is
not native acquisition or creation evidence.
The creator group separately passed 18 tests on Linux arm64, kernel 6.12.72, with Rust 1.88 in an offline Docker container with all capabilities dropped. Its two ignored private fail-stop helpers were selected by passing parent tests and exited 126 as required. These results include physical sealed-file creation and mutation rejection; injected failures still are not physical kernel-fault evidence. The expanded sealed-input group passed 32 tests in that same Linux environment, including acquisition and acquired-snapshot bundle/request handoff. The two private helpers retain the same parent-driven execution. This is not the provisioned worker-to-collector handoff gate.
Creator fixtures separately cover exact binary content and chunk boundaries, zero initial offset, close-on-exec, immutable and executable seals, rejected write/resize/shared-writable-map/execute-permission changes, and retained snapshot lifetime. Private fault controls cover creation, writes, seals, property queries and acquisition failures without exporting runtime injection. They must prove one-shot cleanup and no later write or transfer after failure; injected syscall outcomes do not establish physical kernel faults. A dedicated subprocess exercises fail-stop closure without targeting foreign descriptors. Native success fixtures require actual non-executable sealing support and must not silently skip or downgrade when that prerequisite is missing.
cargo test --locked -p semaprax-native-rust-interop-platform-sys --lib doctor::offline_input::create
Executable-factory regressions retain the non-executable cases and add literal native ELF framing, pre-effect malformed/foreign/script and limit rejection, exact chunk boundaries, mode/offset/seal/content checks, blocked content/size/ execute-bit mutations, failure-prefix and one-shot cleanup observations. Private fault controls exercise the shared native flow; they are not physical syscall fault evidence. A subprocess-only forced-close case must terminate without a later factory return. Unsupported-host cases do not substitute for Linux success.
Launcher admission tests pass the actual returned executable files, not rebuilt copies, into both image roles. The ignored production-launcher fixtures use factory-created healthy images alongside independently constructed hostile images; malformed inputs must still reach launcher rejection tests rather than being filtered out by the factory first. These ignored fixtures require explicit selection and their provisioned Linux context; the portable creator tests do not exercise them. Storage and structural acceptance do not prove executable startup; the real launcher/worker/collector runs need the complete provisioned context described in the launcher contract.
The collector's ignored crates/semaprax-doctor-collector/tests/support/created_handoff.rs additionally passes
the actual production-created files to the existing trusted launch fixture,
without serializing or resealing their contents. Independent literal bundle
and request checks precede native/all worker-to-report observations and an
unrepaired request-digest rejection. Existing literal-sealing hostile fixtures
remain independent. This gate requires the full
provisioned collector context,
not merely permission to create a memory file. The ignored handoff cases retain that separate selection requirement; passing
creator tests does not supply the launch context.
The separate offline bundle parser now consumes this input through a closed, bounded inventory; it still grants no execution authority. Before any production profile can be admitted, separately implement and review provisioning/bootstrap, complete tool and loader/configuration input closure, OS filesystem/IPC/network restrictions, descendant settlement and the explicit selector-to-admission binding. Physical platform and real selected-tool compatibility evidence remains mandatory.