Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 a ProcessHandle plus a child-scoped CapabilityManager.
  • 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.
  • MemoryObject provides 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.retargetCaps can 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:

  1. any binaryName in the spawner’s attached boot-wide registry;
  2. an arbitrary list of supported CapGrant records;
  3. supported child-local KernelCapSource objects; and
  4. 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:

  1. validate the caller’s source range and expected digest shape;
  2. reserve the complete image-page and metadata charge;
  3. copy the source range into fresh kernel-owned pages;
  4. hash and parse that snapshot;
  5. reject invalid architecture, ABI, ELF bounds, permissions, or digest;
  6. publish the ExecutableImage capability only after all checks succeed; and
  7. 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 SystemConfigOverlay or 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:

  1. mount the persistent Store and configuration region;
  2. select and authenticate the active or recovery generation;
  3. resolve and assemble its executable artifacts;
  4. seal exact executable images;
  5. derive the generation’s launch plans;
  6. start services and reach their health checkpoints; and
  7. 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:

  1. authenticate and seal the candidate image;
  2. derive or resolve its service revision under the same role policy;
  3. start the candidate while retaining the old process;
  4. require an explicit readiness result;
  5. stop admission to the old process and drain calls under a future graceful handoff protocol;
  6. satisfy the current retargetCaps precondition that the old process has no in-flight call or parked receive, then retarget its served endpoint objects to the candidate;
  7. terminate the old process after retargeting; and
  8. 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:

  1. Add page-backed ExecutableImage, sealing, digest process identity, and a low-level image-based spawn method.
  2. Represent current resident and ISO boot binaries internally as executable images. Keep spawn(binaryName, grants) as a compatibility adapter over the new constructor.
  3. 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.
  4. Generalize restricted launchers into immutable service revisions and role-specific launch plans. Stop returning broad child-management authority to ordinary launch callers.
  5. Add the authenticated umbrella generation root and make candidate, active, known-good, rollback, epoch, and garbage-collection operations atomic across configuration and service payloads.
  6. Migrate init, ordinary services, POSIX program resolution, and focused tests away from the manifest binary registry.
  7. Add controller-driven readiness and stateless replacement, followed by graceful drain and stateful migration.
  8. Shrink the boot package to canonical init and an explicit recovery bundle, then retire general binaryName spawning.

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 retargetCaps without 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.