tiny.machine.Instance
Defined in tiny.machine.
Operates one K0 guest that is running now, borrowing its storage and whatever backs its guest memory, so every lifecycle call goes through this handle.
API (23)
Actions
Public operations.
acknowledge: Closes a turn whose events have all been taken, after the delivery digest matches and both ring cursors have settled, so the caller concludes the turn.active: Says whether this handle is the one driving the lifecycle its storage currently holds, so a caller can tell whether a copied handle remains the live one.admissionBasis: Reports the position an input is admitted against once the events the activation produced have been taken, so the caller obtains the guest position needed to admit an input.captureCheckpoint: Writes the settled guest state into checkpoint storage and destination RAM the caller owns, so the caller turns settled state into a retained checkpoint.captureHot: Captures the pages that differ from a parent checkpoint the caller owns, so the caller records changed pages against a durable parent.deinit: Gives up whatever the backend holds and closes this lifecycle, so the caller gets its buffers back.deliverAdmitted: Checks one admitted input against the position and the authority in force, then writes it where the guest reads, so an admitted input reaches the guest.init: Boots K0 from an ELF image and its execution manifest under the profile the input names, so a caller performs a cold start of a guest.phase: Names the stage the lifecycle has reached, so the caller knows which calls are legal right now.privatePageCount: Counts the writable guest pages this instance has resident, so the caller can see how many pages a shared restore has copied.quiescenceReceipt: Hands back the checked evidence for the turn that has settled, so the caller receives the receipt for the completed turn.reactivate: Sets a settled instance running again under a newer authority, so the caller installs the authority required for the next turn.readMemory: Copies bytes out of guest memory, starting at a byte offset counted from zero, into a buffer the caller owns, so the caller can read guest state from outside the guest.residentMemoryBytes: Measures what this instance's guest memory holds in bytes, so the caller can determine what one instance is holding.restore: Rebuilds the guest named by the expected checkpoint root inside caller-owned RAM, under the selected profile, so the caller brings a captured guest back into RAM it owns.restoreShared: Brings the portable interpreter up over pages read from a store that authenticates them, so a caller restores a captured guest through store pages.run: Lets the selected backend carry the guest until it reaches one boundary this namespace reports, so the caller advances the guest.takeEvents: Moves every event of the current doorbell's group into a buffer the caller owns, for the caller has to take them before the turn can be acknowledged.writeMemory: Copies caller-owned bytes into guest memory at a byte offset counted from zero, so the caller can change guest state from outside the guest.
Fields and members
Public fields and members.
Source
Source: lib/machine/src/instance/owner.zig:261
zig
/// Operates one K0 guest that is running now, borrowing its storage and/// whatever backs its guest memory, so every lifecycle call goes through this/// handle. Each borrowed region keeps its address until `deinit`. Starting a/// new lifecycle in the same storage makes copied handles stale, because the/// session identity moves on.pub const Instance = struct { storage: *Storage, ram: []align(layout.page_bytes) u8, memory: core.memory.Access, session_identity: u64, /// Boots K0 from an ELF image and its execution manifest under the profile /// the input names, so a caller performs a cold start of a guest. That /// profile decides whether Linux KVM or the reference interpreter carries /// the guest. A ready outcome keeps `storage` and exactly `ram_bytes` bytes /// of RAM until `deinit`. Anything the validator turns away, and any /// backend that fails to come up, comes back as rejected. A host lacking /// the backend comes back as unavailable. pub fn init( storage: *Storage, ram: []align(layout.page_bytes) u8, input: types.Input, ) StartResult { return switch (input.profile.backend) { .linux_kvm_single_vcpu_v1 => initSelected( storage, ram, input, .accelerator, null, accelerator.start, ), .portable_x86_64_interpreter_v1 => initSelected( storage, ram, input, .reference, null, reference.start, ), }; } /// Rebuilds the guest named by the expected checkpoint root inside /// caller-owned RAM, under the selected profile, so the caller brings a /// captured guest back into RAM it owns. A ready outcome keeps the storage /// and the RAM until `deinit`. A host lacking the backend comes back as /// unavailable, before anything reaches RAM. Every other failure comes back /// as rejected. pub fn restore( storage: *Storage, ram: []align(layout.page_bytes) u8, input: types.RestoreInput, ) RestoreResult { return switch (input.profile.backend) { .linux_kvm_single_vcpu_v1 => restoreSelected( storage, ram, input, .accelerator, null, accelerator.acquireRestore, ), .portable_x86_64_interpreter_v1 => restoreSelected( storage, ram, input, .reference, null, null, ), }; } /// Brings the portable interpreter up over pages read from a store that /// authenticates them, so a caller restores a captured guest through store /// pages. The root provider and every buffer of branch storage have to /// outlive the instance, up to `deinit`. Each write takes a branch page /// from a fixed supply, and running out comes back as /// `MemoryCapacityExceeded`. pub fn restoreShared( storage: *Storage, root_storage: checkpoint.roots.Storage, branch_storage: checkpoint.roots.branch.Storage, input: types.SharedRestoreInput, ) RestoreResult { return restoreSharedSelected( storage, root_storage, branch_storage, input, ); } /// Gives up whatever the backend holds and closes this lifecycle, so the /// caller gets its buffers back. Calling it twice, or calling it on a stale /// copied handle, does nothing. Afterward the storage and the memory /// backing are the caller's to use again. pub fn deinit(self: *@This()) void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return; if (owner.phase == .closed) return; switch (owner.active_backend) { .accelerator => accelerator.deinit(backendState(owner)), .reference => reference.deinit(backendState(owner)), .none => {}, } owner.phase = .closed; owner.active_backend = .none; owner.manifest_bytes = 0; owner.ram_address = 0; owner.memory_identity = 0; owner.memory_kind = .none; owner.quiescence_receipt = null; } /// Says whether this handle is the one driving the lifecycle its storage /// currently holds, so a caller can tell whether a copied handle remains /// the live one. pub fn active(self: *const @This()) bool { const owner = ownerState(self.storage); return ownsLifecycle(self, owner) and owner.phase != .closed; } /// Names the stage the lifecycle has reached, so the caller knows which /// calls are legal right now. A closed storage and a stale handle both /// answer `closed`. pub fn phase(self: *const @This()) types.RunPhase { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return .closed; return owner.phase; } /// Copies bytes out of guest memory, starting at a byte offset counted from /// zero, into a buffer the caller owns, so the caller can read guest state /// from outside the guest. A closed handle, and any failure reaching the /// memory, come back as `MemoryError`. pub fn readMemory( self: *const @This(), address: usize, output: []u8, ) MemoryError!void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; return self.memory.read(address, output); } /// Copies caller-owned bytes into guest memory at a byte offset counted /// from zero, so the caller can change guest state from outside the guest. /// A closed handle, a range outside the guest, a page that fails its digest /// check, and exhausted branch storage all come back as `MemoryError`. pub fn writeMemory( self: *@This(), address: usize, input: []const u8, ) MemoryError!void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; return self.memory.write(address, input); } /// Counts the writable guest pages this instance has resident, so the /// caller can see how many pages a shared restore has copied. A guest /// living in the caller's RAM counts every page of it. A guest restored /// from a store counts the pages copied into branch storage. pub fn privatePageCount(self: *const @This()) MemoryError!u16 { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; return switch (owner.memory_kind) { .linear => @intCast(checkpoint.page_count), .branch => owner.branch.privatePageCount(), .none => error.Closed, }; } /// Measures what this instance's guest memory holds in bytes, so the caller /// can determine what one instance is holding. A guest living in the /// caller's RAM measures `ram_bytes`. A guest restored from a store adds up /// its branch bookkeeping, the digests it has authenticated, and the pages /// it has copied. pub fn residentMemoryBytes(self: *const @This()) MemoryError!u64 { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; return switch (owner.memory_kind) { .linear => layout.ram_bytes, .branch => owner.branch.residentBytes(), .none => error.Closed, }; } /// Lets the selected backend carry the guest until it reaches one boundary /// this namespace reports, so the caller advances the guest. A doorbell /// taken while the guest is ready, and one taken while it is quiescent, /// both move the lifecycle along. Any other boundary that comes back puts /// the lifecycle into its failed stage. A boundary carrying no guest /// progress is passed over and the backend is asked again, up to a limit. /// The limit is 128 tries, and reaching it comes back as `WouldBlock`. pub fn run(self: *@This()) RunError!types.Exit { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; const running_phase = owner.phase; switch (running_phase) { .booting, .input_delivered => {}, .draining_activation => return error.EventDrainRequired, .awaiting_input => return error.InputRequired, .draining_input => return error.EventDrainRequired, .awaiting_acknowledgement => return error.AcknowledgementRequired, .awaiting_reactivation => return error.ReactivationRequired, .closed, .failed, .capturing_checkpoint, .completing_io => return error.Closed, } for (0..run_stutter_limit) |_| { const raw = runBackend(owner) catch |failure| { owner.phase = .failed; return mapRunFailure(failure); }; if (std.meta.activeTag(raw) == .stutter) continue; const normalized = normalize(raw); if (std.meta.activeTag(raw) == .io) { owner.phase = .completing_io; completePendingIo(owner) catch |failure| { owner.phase = .failed; return mapRunFailure(failure); }; owner.phase = running_phase; } return applyExit(owner, running_phase, normalized); } return error.WouldBlock; } /// Reports the position an input is admitted against once the events the /// activation produced have been taken, so the caller obtains the guest /// position needed to admit an input. The guest has to be waiting for /// input. Both rings have to be settled, the one carrying requests and the /// one carrying events. pub fn admissionBasis( self: *const @This(), ) BasisError!input_admission.Basis { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; if (owner.phase != .awaiting_input) return error.InputUnavailable; if (owner.activation_stage != .complete) { return error.ActivationEventsPending; } const requests = try os.abi.RequestRing.frontiers( try layout.requestRingAccess(self.memory), owner.fence, ); const events = try os.abi.EventRing.frontiers( try layout.eventRingAccess(self.memory), owner.fence, ); if (requests.consumed != 0 or requests.produced != 0 or events.consumed != os.k0.events_per_activation or events.produced != os.k0.events_per_activation) { return error.EventReceiptMismatch; } return basis(owner); } /// Checks one admitted input against the position and the authority in /// force, then writes it where the guest reads, so an admitted input /// reaches the guest. The delivery value is copied during the call. A phase /// that forbids the call, a ring that will not take the input, a memory /// failure, and an admission that does not check out all leave the turn /// unacknowledged. pub fn deliverAdmitted( self: *@This(), value: *const input_admission.Delivery, ) DeliveryError!void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; const owned = value.*; if (owner.phase == .input_delivered or owner.phase == .draining_input) { return error.InputAlreadyDelivered; } if (owner.phase == .awaiting_acknowledgement) { return error.AcknowledgementRequired; } if (owner.phase != .awaiting_input) return error.InputUnavailable; if (owner.activation_stage != .complete) { return error.ActivationEventsPending; } try input_admission.verifyDelivery(basis(owner), owner.fence, &owned); const requests = try layout.requestRingAccess(self.memory); const events = try layout.eventRingAccess(self.memory); const request_frontiers = try os.abi.RequestRing.frontiers( requests, owner.fence, ); const event_frontiers = try os.abi.EventRing.frontiers( events, owner.fence, ); if (request_frontiers.consumed != 0 or request_frontiers.produced != 0 or event_frontiers.consumed != os.k0.events_per_activation or event_frontiers.produced != os.k0.events_per_activation) { return error.EventReceiptMismatch; } const request_sequence = std.math.add( u64, request_frontiers.produced, 1, ) catch return error.SequenceExhausted; const event_count: u64 = switch (owned.admission.record) { .terminal => os.k0.events_per_terminal_input, else => os.k0.events_per_nonterminal_input, }; const final_event_sequence = std.math.add( u64, event_frontiers.produced, event_count, ) catch return error.SequenceExhausted; var wire: os.abi.MessageWire = undefined; try delivery.encode( owner.fence, request_sequence, &owned.admission, &wire, ); try os.abi.RequestRing.push(requests, owner.fence, &wire); owner.pending = .{ .delivery = owned, .request_frontiers = request_frontiers, .event_frontiers = event_frontiers, .request_sequence = request_sequence, .final_event_sequence = final_event_sequence, .stage = .semantic, .block_root = null, .quiescence = null, .event_transcript_digest = @splat(0), .semantic_transcript_digest = @splat(0), }; owner.phase = .input_delivered; } /// Moves every event of the current doorbell's group into a buffer the /// caller owns, for the caller has to take them before the turn can be /// acknowledged. That buffer may not overlap the handle, its storage, or /// guest memory. When validation fails, the ring keeps its entries. pub fn takeEvents( self: *@This(), output: *types.EventBatch, ) EventError!void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; const output_bytes = std.mem.asBytes(output); if (image_admission.buffersOverlap(output_bytes, std.mem.asBytes(self))) { return error.OutputAliasesInstance; } if (image_admission.buffersOverlap(output_bytes, &self.storage.bytes)) { return error.OutputAliasesOwner; } if (self.memory.aliases(output_bytes)) { return error.OutputAliasesRam; } if (owner.phase == .draining_activation) { return takeActivationEvents(owner, self.memory, output); } if (owner.phase != .draining_input) return error.InputUnavailable; return takeInputEvents(owner, self.memory, output); } /// Closes a turn whose events have all been taken, after the delivery /// digest matches and both ring cursors have settled, so the caller /// concludes the turn. Closing it builds the evidence for the quiescent /// turn and moves the lifecycle to `awaiting_reactivation`. pub fn acknowledge( self: *@This(), receipt: input_admission.DeliveryReceipt, ) AcknowledgeError!void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; if (owner.phase != .awaiting_acknowledgement) { return error.InputUnavailable; } const pending = owner.pending orelse return error.InputUnavailable; if (pending.stage != .complete) return error.EventDrainRequired; if (!std.meta.eql(receipt, pending.delivery.receipt)) { return error.DeliveryReceiptMismatch; } const settled = try settledRings(owner, self.memory, pending); const committed = try prepareCommit(owner, pending, settled); owner.source_root = committed.basis.source_root; owner.frontiers = committed.basis.frontiers; owner.outstanding_effect = committed.basis.outstanding_effect; owner.block_root = committed.block_root.digest; owner.terminal_offset = committed.terminal_offset; owner.semantic_frontier = committed.semantic_frontier; owner.root_generation = committed.root_generation; owner.terminal_count = committed.terminal_count; owner.pending = null; owner.quiescence_receipt = committed.receipt; owner.phase = .awaiting_reactivation; } /// Hands back the checked evidence for the turn that has settled, so the /// caller receives the receipt for the completed turn. The guest has to be /// waiting for reactivation. Both rings have to stand where the evidence /// says they stand. pub fn quiescenceReceipt( self: *const @This(), ) QuiescenceReceiptError!types.QuiescenceReceipt { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; if (owner.phase != .awaiting_reactivation) { return error.QuiescenceUnavailable; } const value = owner.quiescence_receipt orelse return error.QuiescenceUnavailable; try quiescence_receipt.verifyForFence(value, owner.fence); try validateReceiptOwner(owner, value); const requests = try os.abi.RequestRing.frontiers( try layout.requestRingAccess(self.memory), owner.fence, ); const events = try os.abi.EventRing.frontiers( try layout.eventRingAccess(self.memory), owner.fence, ); if (requests.consumed != value.settled.request_cursor or requests.produced != value.settled.request_cursor or events.consumed != value.settled.event_cursor or events.produced != value.settled.event_cursor) { return error.EventReceiptMismatch; } return value; } /// Writes the settled guest state into checkpoint storage and destination /// RAM the caller owns, so the caller turns settled state into a retained /// checkpoint. Both have to stay alive for as long as the checkpoint is /// used. Buffers that overlap, evidence that does not check out, memory /// that cannot be read, and a checkpoint that will not build all turn the /// capture away. pub fn captureCheckpoint( self: *@This(), storage: *checkpoint.Storage, ram: []align(layout.page_bytes) u8, ) CaptureCheckpointError!checkpoint.Checkpoint { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; try layout.validateRamBytes(ram.len); if (image_admission.buffersOverlap(ram, std.mem.asBytes(self))) { return error.OutputAliasesInstance; } if (image_admission.buffersOverlap(ram, &self.storage.bytes)) { return error.OutputAliasesOwner; } if (self.memory.aliases(ram)) { return error.OutputAliasesRam; } if (image_admission.buffersOverlap(&storage.bytes, std.mem.asBytes(self))) { return error.StorageAliasesInstance; } if (image_admission.buffersOverlap(&storage.bytes, &self.storage.bytes)) { return error.StorageAliasesOwner; } if (self.memory.aliases(&storage.bytes)) { return error.StorageAliasesRam; } if (image_admission.buffersOverlap(&storage.bytes, ram)) { return error.MemoryAliasesStorage; } try checkpoint.ensureAvailable(storage); const receipt_value = try self.quiescenceReceipt(); owner.phase = .capturing_checkpoint; defer owner.phase = .awaiting_reactivation; const material = try checkpointMaterial(owner, receipt_value); if (self.memory.linearRam()) |linear| { return checkpoint.publish(material, storage, linear, ram); } try self.memory.read(0, ram); const memory = try checkpoint.validatedMemoryDigest( ram, material.immutable_image, ); const identity = try checkpoint.identify(material, memory); var candidate = try checkpoint.beginMaterialization(storage, ram); return checkpoint.publishMaterialized( &candidate, material, identity.root, memory, ); } /// Captures the pages that differ from a parent checkpoint the caller owns, /// so the caller records changed pages against a durable parent. What comes /// back borrows that parent and the hot storage passed in. A guest whose /// memory comes from a store answers `MemoryReadFailed`. pub fn captureHot( self: *@This(), parent: *const checkpoint.Checkpoint, storage: checkpoint.hot.Storage, ) CaptureHotError!checkpoint.hot.Snapshot { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; const index_bytes = std.mem.sliceAsBytes(storage.indices); if (image_admission.buffersOverlap(index_bytes, std.mem.asBytes(self)) or image_admission.buffersOverlap(storage.pages, std.mem.asBytes(self))) { return error.OutputAliasesInstance; } if (image_admission.buffersOverlap(index_bytes, &self.storage.bytes) or image_admission.buffersOverlap(storage.pages, &self.storage.bytes)) { return error.OutputAliasesOwner; } const receipt_value = try self.quiescenceReceipt(); owner.phase = .capturing_checkpoint; defer owner.phase = .awaiting_reactivation; const linear = self.memory.linearRam() orelse return error.MemoryReadFailed; return checkpoint.hot.capture( parent, try checkpointMaterial(owner, receipt_value), linear, storage, ); } /// Sets a settled instance running again under a newer authority, so the /// caller installs the authority required for the next turn. The new /// authority names the same world, counts one generation higher, and /// carries a different token. When it succeeds the lifecycle stands at /// `booting` again. pub fn reactivate( self: *@This(), fence: os.abi.ActivationFence, ) ReactivateError!void { const owner = ownerState(self.storage); if (!ownsLifecycle(self, owner)) return error.Closed; if (owner.phase != .awaiting_reactivation) { return error.InputUnavailable; } try os.abi.wire.validateFence(fence); if (!std.mem.eql(u8, &owner.fence.world, &fence.world)) { return error.ActivationWorldMismatch; } if (fence.generation <= owner.fence.generation) { return error.ActivationGenerationStale; } if (std.mem.eql(u8, &owner.fence.token, &fence.token)) { return error.ActivationTokenStale; } _ = std.math.add(u64, owner.terminal_offset, os.k0.ready_prompt.len) catch return error.StateCapacityExceeded; const frame = continuationFrame(owner, fence); var wire: os.abi.BootWire = undefined; try os.abi.encodeBootFrame(frame, &wire); restartBackend(owner, self.memory) catch |failure| { owner.phase = .failed; return failure; }; try layout.reactivateAccess(self.memory, frame, &wire); owner.fence = fence; owner.activation_stage = .ready; owner.quiescence_receipt = null; owner.phase = .booting; }};Source: lib/machine/src/root.zig:92
zig
pub const Instance = instance.Instance;Also reachable as
Audit
| Definitions | 20 |
|---|---|
| Public names | 40 |
| Members | 4 |
| Version | 26.7.0 |
| Revision | daab053ee433 |