Executable Images and Service Launch Authority
capOS needs to install and replace independently built services without turning
the kernel into a package manager or treating a string in the boot manifest as
program authority. This proposal separates four concerns that the current
ProcessSpawner path combines:
- immutable executable identity;
- permission to construct a process;
- authority and resource policy for one service role; and
- package-generation, deployment, and rollback policy.
The manifest-builtin binary registry is a bootstrap mechanism, not the target system package namespace. Normal services should ultimately come from authenticated installed generations. Only the minimum code needed to reach and repair those generations belongs in the recovery floor.
Status and Scope
This is future design. The following foundations exist:
ProcessSpawner.spawn(name, binaryName, grants)constructs isolated native processes from a boot-time binary set and returns aProcessHandleplus a child-scopedCapabilityManager.- Resident boot binaries are immutable
Arc<[u8]>snapshots. The ISO path reads a bounded extent and verifies its manifest-committed SHA-256 digest before ELF loading. MemoryObjectprovides mapped page-backed storage, but its mappings are mutable and therefore are not sealed executable identity.- the installable-system path persists content-addressed configuration generations and active/candidate/known-good rollback pointers; persistent service-binary generation roots are not implemented;
ProcessControl.retargetCapscan preserve endpoint identity across a stateless replacement when the old process is already quiesced, but readiness, quiesce, graceful drain, state migration, and complete successor handoff remain future work.
This proposal does not claim production package signing, executable-image capabilities, runtime deployment, or live service replacement already works. It specifies the authority boundary those implementations should converge on.
Current Misalignment
The current ProcessSpawner is a valid broad capability: possession of it is
explicit authority, and a capability may legitimately expose a parameterized
factory. The problem is delegation breadth, not the existence of parameters.
One invocation currently lets the holder choose:
- any
binaryNamein the spawner’s attached boot-wide registry; - an arbitrary list of supported
CapGrantrecords; - supported child-local
KernelCapSourceobjects; and - a child lifecycle handle plus post-spawn capability-table management.
That surface is appropriate only for a root constructor held by init or a
trusted supervisor. It is too broad to represent permission to launch one
service. Recursively granting ProcessSpawner also copies the same boot-wide
binary selection authority into another process.
The current string selector has a second problem: a human-readable name is not stable code identity. A later boot image may bind the same name to different bytes. That is insufficient for audit, rollback, update policy, or reasoning about which code a capability authorized.
The existing RestrictedLauncher direction demonstrates that launch authority
can be narrowed by binary allowlists, fixed sessions, and bounded grant shapes.
The durable design should generalize that attenuation model while replacing
the boot-wide name registry with exact executable capabilities.
Authority Decomposition
The durable model has four distinct objects. They may share implementation state, but their authority must not be collapsed.
ExecutableImage: exact code identity
An ExecutableImage capability authorizes nomination of one immutable native
image for process construction. It identifies exact bytes, not a package name,
service role, filesystem path, or update channel.
Each published object records at least:
- a cryptographic content digest;
- byte length;
- architecture and capOS ABI identity;
- validated ELF entry point, segment, TLS, and page-permission metadata;
- the resource charge for its immutable backing; and
- optional admission metadata, such as the release-root identifier that authorized the image.
Different content generations are different executable-image objects. Two objects may contain identical bytes while carrying different admission provenance; the content digest establishes integrity and deduplication identity, while the surrounding package and launch policy establishes authorization.
Possessing an ExecutableImage is not, by itself, permission to create a
process. Process construction requires a separate constructor or launch-plan
capability. Conversely, a constructor cannot choose code the caller has not
supplied through an executable-image capability.
ServiceRevision: exact role configuration
A ServiceRevision is an immutable userspace policy object that binds:
- one exact
ExecutableImage; - a stable service-role identifier;
- a fixed launch/grant template;
- an ABI/schema compatibility declaration;
- a resource profile and instance budget;
- readiness and health contracts;
- the result facets visible to the launch caller; and
- package, provenance, and update-channel metadata.
Those metadata fields are descriptive, not self-authenticating. A production
revision is valid only when an authenticated SystemGeneration names it and
the generation admission path derives the revision capability. A development
image sealed under a local unsigned policy cannot cross into a production
controller merely by copying production-looking provenance fields; it must be
re-admitted through the production generation policy.
The same executable image may participate in multiple revisions. A shell binary launched for an operator and the same bytes launched for a guest are different authority configurations. An interpreter image and the script or Wasm payload it executes are also separate authorities.
This is why “one binary equals one launch capability” is incomplete. Exact binary identity should be a capability, but the delegated permission to run a service must also constrain its role, grants, session, resources, multiplicity, and management surface.
LaunchPlan: delegated instance creation
A LaunchPlan (or ServiceLauncher) is an attenuated userspace-served
capability derived from one admitted ServiceRevision. It is the authority an
ordinary supervisor or runtime caller uses to create an instance. It fixes the
revision, grant-slot schema, session and resource ceilings, multiplicity,
restart-rate budget, and result facets. Callers may fill only the dynamic
binding slots declared by the plan.
The plan implementation retains whatever root ProcessConstructor, image,
and child-management facets it needs. A plan holder does not receive those
facets and cannot replace the image, author arbitrary grants, mint arbitrary
kernel sources, or obtain a general child CapabilityManager.
ServiceController: mutable lifecycle authority
A ServiceController (or service slot) is a userspace-served capability for
one stable service identity. It may stage an authorized ServiceRevision,
start a candidate, check readiness, activate it, retain the prior generation,
and roll back. It owns the process handles and any child capability manager
needed to implement that protocol.
The controller is not a kernel-global service registry and is not the stable
identity clients invoke. Stable client identity remains the endpoint object
preserved by ProcessControl.retargetCaps. A controller only owns lifecycle
policy for the processes serving those endpoints.
Kernel Mechanism
The kernel should retain final ELF validation, address-space construction, cap-table construction, W^X enforcement, resource charging, and transactional process publication. Moving those operations wholesale into userspace would grant a loader authority to assemble another process’s page tables, initial CPU state, ring, CapSet, and capability table; the kernel would still need to revalidate nearly every result.
The proposed low-level interfaces are conceptually:
interface ExecutableImageFactory {
# Snapshot bytes from a caller-held MemoryObject, verify the expected
# digest, validate the immutable snapshot as a capOS executable, and
# return a newly published ExecutableImage capability.
sealMemory @0 (
sourceMemoryCapId :UInt32,
length :UInt64,
expectedSha256 :Data
) -> (imageIndex :UInt16);
}
interface ExecutableImage {
# Report immutable code identity and validated load metadata.
info @0 () -> (
sha256 :Data,
size :UInt64,
architecture :Text,
abi :Text
);
}
interface ProcessConstructor {
# Construct a child from the exact caller-held ExecutableImage. This is a
# root mechanism for init and trusted launch-plan implementations, not an
# ordinary application capability.
spawnImage @0 (
name :Text,
imageCapId :UInt32,
grants :List(CapGrant)
) -> (
handleIndex :UInt16,
capabilityManagerIndex :UInt16
);
}
This sketch follows the current manual-dispatch convention: a method that
names another capability in the caller’s table passes a local CapId, and
result capabilities arrive through bounded CQE transfer records. The exact
schema is not frozen by this proposal.
The initial synchronous contract is hard-bounded to
MAX_EXECUTABLE_IMAGE_BYTES = 4 MiB, matching the current spawn ceiling,
MAX_EXECUTABLE_PROGRAM_HEADERS = 128, and
MAX_EXECUTABLE_LOAD_SEGMENTS = 64 with at most one TLS segment. length and
every ELF offset/count conversion must be checked before allocation or
iteration. These are admission limits, not promises that the eventual package
format can never carry larger programs.
ProcessConstructor deliberately remains broad. Ordinary callers should see
a narrower userspace LaunchPlan or ServiceLauncher interface instead. That
interface accepts only declared dynamic bindings and returns only the lifecycle
or service facets authorized by the plan.
Pipe allocation is independent authority and should eventually move from
ProcessSpawner.createPipe to a separate PipeFactory capability. POSIX
subprocess code can then receive pipe construction without receiving general
process construction.
Sealing and Immutability
The factory must validate and load the same immutable snapshot. Validation of
a mutable MemoryObject, File, or userspace-served Store followed by a
later read would create a validation-to-use race.
The initial implementation should:
- validate the caller’s source range and expected digest shape;
- reserve the complete image-page and metadata charge;
- copy the source range into fresh kernel-owned pages;
- hash and parse that snapshot;
- reject invalid architecture, ABI, ELF bounds, permissions, or digest;
- publish the
ExecutableImagecapability only after all checks succeed; and - roll back every reservation and page allocation on every failure.
The source session’s executable-ingress ledger owns the snapshot pages, validated segment metadata, and sealing work reservation until publication; the published image retains that charge or transfers it atomically to an explicit repository budget. The factory rejects a request before copying when the byte, page, header, segment, metadata, or work reservation cannot be made.
The 4 MiB snapshot copy, SHA-256 pass, and bounded parse may run synchronously
only after a worst-case timing proof shows they fit the kernel dispatch budget.
If that proof does not hold, the interface must become a staged seal job: it
first obtains an immutable snapshot through exclusive source consumption or
write-map revocation, then advances hashing and parsing under fixed per-quantum
page/header work limits, and publishes only from finish. It must not yield
while retaining a merely mutable source reference.
Hostile proofs must cover an oversized source, excessive program headers and load segments, every checked-conversion boundary, exhausted page/metadata/work budgets, source mutation attempts, and rollback after reservation, snapshot, hash, parse, image insertion, and result-cap transfer failures.
The backing must be page-accounted through the existing resource-ledger model.
A multi-megabyte Arc<[u8]> charged only to the kernel heap is not an acceptable
long-term cache. Immutable executable pages may later be shared read-only
between processes or deduplicated by digest, but sharing must not bypass the
ledger of record.
Releasing one image capability removes only that local reference; outstanding copies remain usable. Object-wide prevention of future launches requires an explicit generation/repository epoch or revoker facet checked by launch plans. Neither local release nor epoch revocation terminates processes already created from the image; termination and replacement require process-lifecycle authority. Audit and crash records should retain the image digest and service-revision identity after references are released or revoked.
Package Ingestion and Persistent Generations
Files, Store objects, boot media, network downloads, and development uploads are artifact sources, not executable authority. A userspace package importer is responsible for:
- bounded artifact acquisition;
- chunk assembly and content hashing;
- signature and publisher verification;
- provenance and release-channel policy;
- architecture and ABI admission;
- anti-rollback epoch checks; and
- requesting sealing only after policy succeeds.
The current Store blob ceiling is smaller than a normal statically linked ELF, so persistent packages require a chunked or Merkle-rooted artifact format. A package root should bind chunk hashes, total length, executable digest, ABI, and provenance metadata. Hashes prove integrity; accepted signatures and local policy establish authenticity.
Installed software should use one authenticated immutable SystemGeneration
umbrella root. It atomically binds:
- the
SystemConfigOverlayor successor configuration root; - stable service-role identifiers to exact service revisions and package roots;
- architecture and ABI admission policy;
- release/signature provenance; and
- one monotonic release epoch.
Configuration and package roots remain separate content-addressed objects, but activation pointers never select them independently. The existing configuration-only generation is a compatibility predecessor; it must not silently become an unversioned package database or a second independently movable half of system state.
Candidate, active, known-good, and retained rollback pointers each name a whole
umbrella root. Staging validates the root signature and its complete transitive
hash closure before recording a candidate. Before starting any candidate
process, boot records the attempt against that same root. Generation validation
then authorizes exact ServiceRevision objects and derives their launch plans;
an image digest or provenance string outside that admission chain is
insufficient.
On boot, init or a recovery-owned package service will:
- mount the persistent Store and configuration region;
- select and authenticate the active or recovery generation;
- resolve and assemble its executable artifacts;
- seal exact executable images;
- derive the generation’s launch plans;
- start services and reach their health checkpoints; and
- confirm the candidate generation only after the required system health floor is running, atomically promoting that root to active and known-good.
A failed candidate clears or marks only its candidate/attempt record and reactivates the prior whole known-good root. The anti-rollback acceptance floor does not advance merely because a candidate boots. An authorized promotion may advance it only after policy explicitly abandons every retained rollback root below the new floor; otherwise known-good fallback would reject itself. Garbage collection must retain every object reachable from active, candidate, known-good, configured rollback, unconfirmed boot-attempt, live executable-image, and running-process records.
Launch Plans and Attenuation
A launch plan is derived monotonically under ceilings held by init or a trusted
launch-authority service. It is the delegated instance-creation object defined
above; its interface must not accept arbitrary CapGrant or KernelCapSource
lists.
Each grant slot is one of:
- plan-owned: a fixed capability held by the plan and forwarded to every instance;
- caller binding: the caller supplies a capability, but the plan fixes the name, expected interface, transfer mode, transfer scope, and any attenuator;
- child-local: the plan authorizes construction of one specific primitive under the child’s resource budget; or
- service export: an endpoint or other result facet retained by the supervisor or exposed through a narrower interface.
The plan also fixes or bounds:
- the child session derivation rule;
- resource profile and image/process page budgets;
- maximum concurrent instances and restart rate;
- executable revision or permitted update channel;
- whether termination, observation, or child capability management is exposed;
- readiness timeout and failure policy; and
- structured audit identity.
Automatic return of a general CapabilityManager defeats a closed launch
profile. The manager may remain private to the plan/controller, while ordinary
callers receive distinct observer, termination, or exported-service facets.
Post-spawn mutation needed by an application should use named, slot-specific
grant ports rather than general authority over the child cap table.
Runtime Deployment and Replacement
Tests and production should use the same deployment mechanism. The policy at
artifact ingress differs: a local-development profile may admit an unsigned
locally built image, while a production profile requires its configured release
signature, provenance, and anti-rollback checks. Both produce the same sealed
ExecutableImage object and use the same launch and lifecycle path.
Stateless service replacement proceeds as follows:
- authenticate and seal the candidate image;
- derive or resolve its service revision under the same role policy;
- start the candidate while retaining the old process;
- require an explicit readiness result;
- stop admission to the old process and drain calls under a future graceful handoff protocol;
- satisfy the current
retargetCapsprecondition that the old process has no in-flight call or parked receive, then retarget its served endpoint objects to the candidate; - terminate the old process after retargeting; and
- promote the candidate generation only after the health checkpoint.
The currently implemented retargetCaps is only one mechanism in that
protocol. Its no-in-flight/no-parked-receive precondition is not graceful
drain. Until readiness, admission stop, graceful drain, and successor
endpoint-slot handoff exist, the system must not claim zero-disconnect or
state-preserving replacement. Stateful services additionally need an explicit
state snapshot or externalized-state handoff.
Bootstrap and Recovery Floor
The kernel-embedded init image remains part of the kernel/bootstrap ABI. The general manifest-builtin binary list should not remain the ordinary package source merely because it exists before storage services start.
The recovery floor should contain only enough code and authority to:
- start canonical init;
- discover and mount the installed generation substrate;
- verify and activate a known-good generation;
- repair or roll back broken package/configuration pointers; and
- expose a bounded recovery console when normal startup cannot reach health.
If hardware requires userspace components before the persistent generation can
be mounted, those components belong in a sealed, measured RecoveryBundle with
fixed launch plans. The bundle is not enumerable application policy and must not
grow into a second system package namespace.
Focused tests that currently boot a demo directly as PID 1 should migrate behind canonical init and the normal launch path. A test-specific artifact ingress policy is acceptable; a test-only process-construction architecture is not.
POSIX and Language-Runtime Compatibility
POSIX execve needs a capability-scoped program resolver, not access to the
boot-wide binaryName registry. A ProgramResolver or ExecDirectory
capability maps the caller’s permitted paths or command names to launch plans.
The POSIX adapter may supply declared fd/stdio binding slots, subject to
FD_CLOEXEC, transfer-scope, and interface checks, but cannot add arbitrary
kernel sources or child grants.
A broad operator command catalogue remains possible as an explicitly granted resolver. Guest or service sessions receive narrower catalogues. Human-readable paths remain resolver-local policy; the launch decision ultimately names an exact image and role.
For Wasm, Lua, Python, and other interpreter-based runtimes, the interpreter is one executable image. The module/script and its imported capability bundle are separate data and authority supplied through the launch plan. Updating a script does not require pretending the interpreter binary changed.
Audit, Revocation, and Resource Accounting
Every launch attempt should record:
- launch-plan and service-role identity;
- exact executable digest and admitted generation;
- caller session reference and supervisor identity;
- grant-template identity and dynamic binding summary;
- resource budget and instance number;
- resulting pid/process generation; and
- success or typed failure outcome.
Revoking a launch plan prevents future instances. Controller policy decides whether revocation also terminates existing instances. Revoking an update channel prevents admission of future revisions without invalidating an already active, known-good revision. Emergency termination and endpoint disconnection remain explicit lifecycle operations.
Image storage, sealed snapshots, loaded executable pages, candidate overlap, capability slots, and restart attempts need one auditable ledger per resource class. Deployment must reserve for the old and candidate processes simultaneously before it begins; rollback must release all candidate charges.
Alternatives
Keep the boot-wide name registry
Rejected as the long-term model. It provides useful whole-image recovery and simple current tests, but it couples ordinary service deployment to image rebuild/reboot and delegates broad caller-selected choice within the registry. It remains only as a compatibility adapter during migration.
Pass raw ELF bytes through ProcessSpawner
Rejected. Ring parameters are bounded well below current executable sizes, and copying multi-megabyte artifacts through a control-plane invocation is the wrong transport boundary.
Let the kernel load an arbitrary File or Store directly
Deferred. The kernel cannot synchronously invoke an arbitrary userspace storage server while already dispatching process construction without a larger re-entrant or asynchronous kernel-to-userspace RPC design. Supporting only kernel fixture files would create a second, test-shaped architecture.
Load directly from mutable MemoryObject
Rejected without sealing. Another holder or writable mapping could change the bytes between validation and use. Snapshot-and-seal preserves the current boot-path property that ELF parsing and loading observe one immutable byte sequence.
Move complete process construction into userspace
Deferred. It substantially widens authority over address spaces, initial CPU state, cap tables, and publication while leaving the kernel responsible for final validation and rollback. The split can be reconsidered after executable images and launch plans establish a concrete userspace builder use case.
Make each binary the complete launch permission
Rejected as incomplete. It constrains code identity but not grants, session, resources, multiplicity, lifecycle, or update policy. The chosen decomposition uses exact image capabilities under role-specific launch plans.
Migration
Migration does not require a flag day:
- Add page-backed
ExecutableImage, sealing, digest process identity, and a low-level image-based spawn method. - Represent current resident and ISO boot binaries internally as executable
images. Keep
spawn(binaryName, grants)as a compatibility adapter over the new constructor. - Prove production-shaped ingestion: assemble an ELF through ordinary data capabilities, seal it, mutate or drop the source, and spawn the unchanged digest. Prove callers lacking the factory, image, or launch authority fail closed.
- Generalize restricted launchers into immutable service revisions and role-specific launch plans. Stop returning broad child-management authority to ordinary launch callers.
- Add the authenticated umbrella generation root and make candidate, active, known-good, rollback, epoch, and garbage-collection operations atomic across configuration and service payloads.
- Migrate init, ordinary services, POSIX program resolution, and focused tests away from the manifest binary registry.
- Add controller-driven readiness and stateless replacement, followed by graceful drain and stateful migration.
- Shrink the boot package to canonical init and an explicit recovery bundle,
then retire general
binaryNamespawning.
Each implementation slice must keep the legacy and new paths behaviorally
distinct in evidence. A successful image-based spawn does not prove package
authenticity, a local QEMU upload does not prove production release authority,
and retargetCaps alone does not prove graceful live upgrade.
Open Decisions
- Which trusted principal owns executable-image sealing in production, and how secure/measured boot anchors its accepted signing roots.
- Whether sealing always copies source pages or can consume an exclusively owned memory object without violating immutability or accounting.
- The package chunk/Merkle format and garbage-collection reachability rules.
- The stable capOS userspace ABI identifier and compatibility rules enforced at image admission.
- Whether image-page sharing is charged to the repository, each process, or a donated shared budget.
- The precise launch-plan schema and which attenuation wrappers remain userspace-served versus kernel-backed.
- How a successor receives inherited endpoint slot identities during
retargetCapswithout an out-of-band integer protocol. - The minimum hardware-specific contents of the recovery bundle.
Design Grounding
This design builds on:
- Capability Model for typed-interface authority, attenuation, transfer scope, and resource-ledger invariants;
- Process Model for current kernel process construction and lifecycle authority;
- Manifest and Service Startup for the current boot-package executor and service graph;
- Service Architecture for supervisors, service roles, restart policy, and the pre-init boundary;
- Userspace Binaries for native ELF, runtime, POSIX, and language-adapter contracts;
- Installable System for persistent generations, candidate confirmation, and rollback;
- Live Upgrade for endpoint retargeting, quiesce, drain, and state migration; and
- Capability Systems Survey and EROS, CapROS, and Coyotos for explicit capability attenuation, constructor-based confinement, persistent object identity, and the choice to keep persistence and package policy explicit rather than transparently kernel-global.