lib/machine/src/checkpoint/owner/state.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const identity = @import("identity.zig");
2 const instance_receipt = @import("../../instance/receipt/root.zig");
3 const memory = @import("memory.zig");
4 const builtin = @import("builtin");
5 const core = @import("machine_instance_core");
6 const os = @import("os");
7 const std = @import("std");
8 const types = @import("types.zig");
9
10 const layout = core.layout;
11 const provenance = core.provenance;
12
13 const Status = enum(u8) {
14 empty,
15 capturing,
16 published,
17 };
18
19 const OwnerState = struct {
20 status: std.atomic.Value(u8) = std.atomic.Value(u8).init(@backingInt(Status.empty)),
21 root: types.Root = undefined,
22 memory_digest: types.MemoryDigest = undefined,
23 cpu: types.CpuState = undefined,
24 receipt: instance_receipt.SemanticReceipt = undefined,
25 immutable_image: provenance.ImmutableImage = undefined,
26 };
27
28 pub const storage_alignment: usize = @alignOf(OwnerState);
29 pub const storage_bytes: usize = @sizeOf(OwnerState);
30 pub const ram_alignment: usize = layout.page_bytes;
31 pub const ram_bytes: usize = layout.ram_bytes;
32
33 /// Caller-owned metadata for one durable checkpoint. The external layout is
34 /// exactly `storage_bytes` at `storage_alignment`. The caller initializes the
35 /// storage before use. The caller keeps its address and its paired memory image
36 /// alive with unchanged bytes for as long as a handle can be used.
37 pub const Storage = struct {
38 bytes: [storage_bytes]u8 align(storage_alignment),
39
40 /// Hands back checkpoint storage that holds nothing and is ready to be
41 /// claimed once.
42 pub fn init() Storage {
43 var result: Storage = .{ .bytes = @splat(0) };
44 ownerState(&result).status = std.atomic.Value(u8).init(
45 @backingInt(Status.empty),
46 );
47 return result;
48 }
49 };
50
51 /// The failures of durable publication, ownership, corruption, identity, and
52 /// memory.
53 pub const Error = error{
54 CheckpointClosed,
55 CheckpointCorrupt,
56 CheckpointRootMismatch,
57 CheckpointStorageInUse,
58 MemoryAliasesStorage,
59 SourceAliasesDestination,
60 StorageAliasesInstance,
61 StorageAliasesOwner,
62 StorageAliasesRam,
63 RamBytesMismatch,
64 } || identity.Error || instance_receipt.Error || memory.Error || provenance.Error;
65
66 /// The result of crash recovery. A published result carries a handle borrowing
67 /// the supplied storage and memory. By contrast, an empty result means the
68 /// storage holds nothing and can take a publication.
69 pub const Recovery = union(enum) {
70 empty,
71 published: Checkpoint,
72 };
73
74 /// An exclusive publication claim for caller-filled memory. The caller aborts
75 /// an active claim when the fill stops part way. Publication consumes the
76 /// active claim.
77 pub const Materialization = struct {
78 storage: *Storage,
79 ram: []align(layout.page_bytes) u8,
80 active: bool = true,
81 };
82
83 const PublicationCut = enum(u8) {
84 before_claim,
85 claimed,
86 copied,
87 provenance_validated,
88 memory_normalized,
89 memory_hashed,
90 state_hashed,
91 root_written,
92 memory_written,
93 cpu_written,
94 receipt_written,
95 provenance_written,
96 published,
97 };
98
99 const PublicationOutcome = union(enum) {
100 interrupted,
101 published: Checkpoint,
102 };
103
104 /// A borrowed handle to published checkpoint metadata and a normalized memory
105 /// image. Both regions stay at one address with their bytes untouched while the
106 /// handle lives.
107 pub const Checkpoint = struct {
108 storage: *const Storage,
109 ram: []align(layout.page_bytes) const u8,
110 root_digest: os.abi.Digest,
111
112 /// Reports whether the storage is still published under this handle's root.
113 /// A malformed status byte, or a stored root other than this handle's,
114 /// returns false.
115 pub fn active(self: *const @This()) bool {
116 const owner = ownerStateConst(self.storage);
117 const status = checkedStatus(owner) catch return false;
118 return status == .published and owns(self, owner);
119 }
120
121 /// Recomputes the memory, state, and root identities from the stored bytes.
122 /// Closed, changed, or malformed storage returns the specific checkpoint
123 /// error.
124 pub fn verify(self: *const @This()) Error!void {
125 const owner = ownerStateConst(self.storage);
126 if (try checkedStatus(owner) != .published) {
127 return error.CheckpointClosed;
128 }
129 try verifyOwner(owner, self.ram);
130 if (!owns(self, owner)) return error.CheckpointClosed;
131 }
132
133 /// Returns the verified checkpoint root.
134 pub fn root(self: *const @This()) Error!types.Root {
135 try self.verify();
136 return ownerStateConst(self.storage).root;
137 }
138
139 /// Returns the verified checkpoint root together with the digest of the
140 /// settled receipt.
141 pub fn identity(self: *const @This()) Error!types.Identity {
142 try self.verify();
143 const owner = ownerStateConst(self.storage);
144 return .{
145 .root = owner.root,
146 .semantic = try instance_receipt.semanticReceiptDigest(owner.receipt),
147 };
148 }
149
150 /// Returns the verified state digest the checkpoint root commits. The
151 /// caller uses this digest to compare two captures without the
152 /// configuration they ran under.
153 pub fn stateDigest(self: *const @This()) Error!types.StateDigest {
154 try self.verify();
155 return ownerStateConst(self.storage).root.state;
156 }
157 };
158
159 pub const PublicationAudit = if (builtin.is_test) struct {
160 pub const Cut = PublicationCut;
161 pub const Outcome = PublicationOutcome;
162
163 pub fn replay(
164 checkpoint: *const Checkpoint,
165 storage: *Storage,
166 destination: []align(layout.page_bytes) u8,
167 cut: Cut,
168 ) Error!Outcome {
169 return replayUntilCut(checkpoint, storage, destination, cut);
170 }
171 } else struct {};
172
173 /// Copies live memory into a separate caller-owned memory region, normalizes
174 /// it, authenticates the material, and publishes one handle with a single
175 /// atomic store. Both memory slices must have `ram_alignment` and `ram_bytes`.
176 /// The two slices must be disjoint from the storage and from each other. A
177 /// failure after the storage is claimed returns that storage to empty. Copied
178 /// or normalized bytes can sit in the destination after a failure.
179 pub fn publish(
180 material: types.Material,
181 storage: *Storage,
182 source: []align(layout.page_bytes) const u8,
183 destination: []align(layout.page_bytes) u8,
184 ) Error!Checkpoint {
185 return switch (try publishUntil(
186 material,
187 storage,
188 source,
189 destination,
190 null,
191 )) {
192 .interrupted => unreachable,
193 .published => |checkpoint| checkpoint,
194 };
195 }
196
197 /// Claims empty storage for memory that another source will fill. The storage
198 /// and the memory lie apart from each other and carry the sizes the call
199 /// requires. An existing claim or publication returns `CheckpointStorageInUse`.
200 pub fn beginMaterialization(
201 storage: *Storage,
202 ram: []align(layout.page_bytes) u8,
203 ) Error!Materialization {
204 try validateStoredInputs(storage, ram);
205 const owner = ownerState(storage);
206 if (owner.status.cmpxchgStrong(
207 @backingInt(Status.empty),
208 @backingInt(Status.capturing),
209 .acq_rel,
210 .acquire,
211 ) != null) return error.CheckpointStorageInUse;
212 return .{ .storage = storage, .ram = ram };
213 }
214
215 /// Gives back the storage an active materialization claim held and leaves the
216 /// claim value inactive. Aborting the same value again does nothing further.
217 pub fn abortMaterialization(candidate: *Materialization) void {
218 if (!candidate.active) return;
219 const owner = ownerState(candidate.storage);
220 const previous = owner.status.cmpxchgStrong(
221 @backingInt(Status.capturing),
222 @backingInt(Status.empty),
223 .acq_rel,
224 .acquire,
225 );
226 std.debug.assert(previous == null);
227 candidate.active = false;
228 }
229
230 /// Authenticates caller-filled memory against the expected memory digest and
231 /// the expected checkpoint root, then publishes it with a single atomic store.
232 /// An authentication failure after the claim is confirmed aborts the claim. On
233 /// success, the returned handle points at the candidate's storage and memory.
234 pub fn publishMaterialized(
235 candidate: *Materialization,
236 material: types.Material,
237 expected_root: types.Root,
238 expected_memory: types.MemoryDigest,
239 ) Error!Checkpoint {
240 if (!candidate.active) return error.CheckpointStorageInUse;
241 const owner = ownerState(candidate.storage);
242 if (try checkedStatus(owner) != .capturing) {
243 return error.CheckpointStorageInUse;
244 }
245 errdefer abortMaterialization(candidate);
246 try provenance.validateRam(material.immutable_image, candidate.ram);
247 const memory_digest = try memory.validatedDigest(
248 candidate.ram,
249 material.immutable_image,
250 );
251 if (!std.meta.eql(memory_digest, expected_memory)) {
252 return error.MemoryDigestMismatch;
253 }
254 const state_digest = try identity.state(
255 material.receipt,
256 material.cpu,
257 material.immutable_image,
258 memory_digest,
259 );
260 const root_value = identity.root(material.profile, state_digest);
261 if (!std.meta.eql(root_value, expected_root)) {
262 return error.CheckpointRootMismatch;
263 }
264 return switch (publishAuthenticated(
265 material,
266 owner,
267 candidate.storage,
268 candidate.ram,
269 memory_digest,
270 root_value,
271 null,
272 )) {
273 .interrupted => unreachable,
274 .published => |checkpoint| result: {
275 candidate.active = false;
276 break :result checkpoint;
277 },
278 };
279 }
280
281 fn replayUntilCut(
282 checkpoint: *const Checkpoint,
283 storage: *Storage,
284 destination: []align(layout.page_bytes) u8,
285 cut: PublicationCut,
286 ) Error!PublicationOutcome {
287 try checkpoint.verify();
288 const source = ownerStateConst(checkpoint.storage);
289 return publishUntil(.{
290 .profile = source.root.profile,
291 .receipt = source.receipt,
292 .cpu = source.cpu,
293 .immutable_image = source.immutable_image,
294 }, storage, checkpoint.ram, destination, cut);
295 }
296
297 fn publishUntil(
298 material: types.Material,
299 storage: *Storage,
300 source: []align(layout.page_bytes) const u8,
301 destination: []align(layout.page_bytes) u8,
302 cut: ?PublicationCut,
303 ) Error!PublicationOutcome {
304 try validateStoredInputs(storage, source);
305 try validateStoredInputs(storage, destination);
306 if (buffersOverlap(source, destination)) {
307 return error.SourceAliasesDestination;
308 }
309 if (cut == .before_claim) return .interrupted;
310 const owner = ownerState(storage);
311 if (owner.status.cmpxchgStrong(
312 @backingInt(Status.empty),
313 @backingInt(Status.capturing),
314 .acq_rel,
315 .acquire,
316 ) != null) return error.CheckpointStorageInUse;
317 errdefer reset(owner);
318 if (cut == .claimed) return .interrupted;
319 @memcpy(destination, source);
320 if (cut == .copied) return .interrupted;
321 try provenance.validateRam(material.immutable_image, destination);
322 if (cut == .provenance_validated) return .interrupted;
323 try memory.normalize(destination, material.immutable_image);
324 if (cut == .memory_normalized) return .interrupted;
325 const memory_digest = memory.digest(destination);
326 if (cut == .memory_hashed) return .interrupted;
327 const state_digest = try identity.state(
328 material.receipt,
329 material.cpu,
330 material.immutable_image,
331 memory_digest,
332 );
333 if (cut == .state_hashed) return .interrupted;
334 const root_value = identity.root(material.profile, state_digest);
335 return publishAuthenticated(
336 material,
337 owner,
338 storage,
339 destination,
340 memory_digest,
341 root_value,
342 cut,
343 );
344 }
345
346 fn publishAuthenticated(
347 material: types.Material,
348 owner: *OwnerState,
349 storage: *Storage,
350 ram: []align(layout.page_bytes) const u8,
351 memory_digest: types.MemoryDigest,
352 root_value: types.Root,
353 cut: ?PublicationCut,
354 ) PublicationOutcome {
355 owner.root = root_value;
356 if (cut == .root_written) return .interrupted;
357 owner.memory_digest = memory_digest;
358 if (cut == .memory_written) return .interrupted;
359 owner.cpu = material.cpu;
360 if (cut == .cpu_written) return .interrupted;
361 owner.receipt = material.receipt;
362 if (cut == .receipt_written) return .interrupted;
363 owner.immutable_image = material.immutable_image;
364 if (cut == .provenance_written) return .interrupted;
365 owner.status.store(@backingInt(Status.published), .release);
366 const checkpoint: Checkpoint = .{
367 .storage = storage,
368 .ram = ram,
369 .root_digest = root_value.digest,
370 };
371 if (cut == .published or cut == null) {
372 return .{ .published = checkpoint };
373 }
374 unreachable;
375 }
376
377 /// Requires initialized storage to be empty. Storage already in use returns
378 /// `CheckpointStorageInUse`, and a malformed status byte returns
379 /// `CheckpointCorrupt`. The caller checks storage before starting work that
380 /// would waste it.
381 pub fn ensureAvailable(storage: *const Storage) Error!void {
382 if (try checkedStatus(ownerStateConst(storage)) != .empty) {
383 return error.CheckpointStorageInUse;
384 }
385 }
386
387 /// Opens a published checkpoint whose root is `expected`, checking the stored
388 /// material and the memory image behind it. A stored root other than `expected`
389 /// returns `CheckpointRootMismatch`. Ownership of the two regions stays with
390 /// the caller, and the returned handle points at them.
391 pub fn open(
392 storage: *const Storage,
393 ram: []align(layout.page_bytes) const u8,
394 expected: types.Root,
395 ) Error!Checkpoint {
396 try validateStoredInputs(storage, ram);
397 const owner = ownerStateConst(storage);
398 if (try checkedStatus(owner) != .published) {
399 return error.CheckpointClosed;
400 }
401 if (!std.meta.eql(owner.root, expected)) {
402 return error.CheckpointRootMismatch;
403 }
404 const checkpoint: Checkpoint = .{
405 .storage = storage,
406 .ram = ram,
407 .root_digest = expected.digest,
408 };
409 try checkpoint.verify();
410 return checkpoint;
411 }
412
413 /// Checks a durable checkpoint against `expected`, writes its normalized memory
414 /// into an aligned destination that lies outside the checkpoint's own regions,
415 /// and returns the restore material. Bytes already written stay in the
416 /// destination when the closing check fails.
417 pub fn materializeForRestore(
418 checkpoint: *const Checkpoint,
419 expected: types.Root,
420 destination: []align(layout.page_bytes) u8,
421 ) Error!types.Material {
422 const storage = checkpoint.storage;
423 const source = checkpoint.ram;
424 try validateStoredInputs(storage, destination);
425 if (buffersOverlap(source, destination)) {
426 return error.SourceAliasesDestination;
427 }
428 const material = try inspectForRestore(checkpoint, expected);
429 const owner = ownerStateConst(storage);
430 const memory_digest = owner.memory_digest;
431 const root_value = owner.root;
432 @memcpy(destination, source);
433 try verifyMaterial(material, memory_digest, root_value, destination);
434 return material;
435 }
436
437 /// Returns verified restore material once the checkpoint root equals
438 /// `expected`.
439 pub fn inspectForRestore(
440 checkpoint: *const Checkpoint,
441 expected: types.Root,
442 ) Error!types.Material {
443 const contents = try inspect(checkpoint);
444 if (!std.meta.eql(contents.root, expected)) {
445 return error.CheckpointRootMismatch;
446 }
447 return contents.material;
448 }
449
450 /// Rebuilds each identity a checkpoint carries, then returns the metadata it
451 /// has authenticated.
452 pub fn inspect(checkpoint: *const Checkpoint) Error!types.Contents {
453 const storage = checkpoint.storage;
454 const source = checkpoint.ram;
455 try validateStoredInputs(storage, source);
456 const owner = ownerStateConst(storage);
457 if (try checkedStatus(owner) != .published) {
458 return error.CheckpointClosed;
459 }
460 if (!owns(checkpoint, owner)) return error.CheckpointClosed;
461 const material = storedMaterial(owner);
462 const memory_digest = owner.memory_digest;
463 const root_value = owner.root;
464 try verifyMaterial(material, memory_digest, root_value, source);
465 return .{
466 .root = root_value,
467 .memory = memory_digest,
468 .material = material,
469 };
470 }
471
472 /// Validates the CPU restart frame and the settled receipt, then computes the
473 /// restore identity from the material and a memory digest.
474 pub fn identify(
475 material: types.Material,
476 memory_digest: types.MemoryDigest,
477 ) Error!types.Identity {
478 const state_digest = try identity.state(
479 material.receipt,
480 material.cpu,
481 material.immutable_image,
482 memory_digest,
483 );
484 return .{
485 .root = identity.root(material.profile, state_digest),
486 .semantic = try instance_receipt.semanticReceiptDigest(material.receipt),
487 };
488 }
489
490 /// Recovers caller-owned storage after a crash. An interrupted capture is reset
491 /// to empty. Storage found published needs an expected root from the caller,
492 /// and the call returns a handle it has verified. An invalid status, root,
493 /// storage, or memory returns the matching error.
494 pub fn recoverAfterCrash(
495 storage: *Storage,
496 ram: []align(layout.page_bytes) const u8,
497 expected: ?types.Root,
498 ) Error!Recovery {
499 try validateStoredInputs(storage, ram);
500 const owner = ownerState(storage);
501 return switch (try checkedStatus(owner)) {
502 .empty => .empty,
503 .capturing => result: {
504 reset(owner);
505 break :result .empty;
506 },
507 .published => .{ .published = try open(
508 storage,
509 ram,
510 expected orelse return error.CheckpointRootMismatch,
511 ) },
512 };
513 }
514
515 /// Confirms that an allocation made elsewhere for the metadata measures
516 /// `storage_bytes` and nothing else.
517 pub fn validateStorageBytes(bytes: usize) error{StorageBytesMismatch}!void {
518 if (bytes != storage_bytes) return error.StorageBytesMismatch;
519 }
520
521 fn validateStoredInputs(
522 storage: *const Storage,
523 ram: []align(layout.page_bytes) const u8,
524 ) Error!void {
525 try layout.validateRamBytes(ram.len);
526 if (buffersOverlap(ram, &storage.bytes)) {
527 return error.MemoryAliasesStorage;
528 }
529 }
530
531 fn verifyOwner(
532 owner: *const OwnerState,
533 ram: []align(layout.page_bytes) const u8,
534 ) Error!void {
535 return verifyMaterial(
536 storedMaterial(owner),
537 owner.memory_digest,
538 owner.root,
539 ram,
540 );
541 }
542
543 fn verifyMaterial(
544 material: types.Material,
545 memory_digest: types.MemoryDigest,
546 root_value: types.Root,
547 ram: []align(layout.page_bytes) const u8,
548 ) Error!void {
549 try provenance.validateRam(material.immutable_image, ram);
550 const actual_memory = try memory.validatedDigest(ram, material.immutable_image);
551 if (!std.meta.eql(actual_memory, memory_digest)) {
552 return error.MemoryDigestMismatch;
553 }
554 const actual = try identify(material, memory_digest);
555 if (!std.meta.eql(actual.root, root_value)) return error.CheckpointCorrupt;
556 }
557
558 fn storedMaterial(owner: *const OwnerState) types.Material {
559 return .{
560 .profile = owner.root.profile,
561 .receipt = owner.receipt,
562 .cpu = owner.cpu,
563 .immutable_image = owner.immutable_image,
564 };
565 }
566
567 fn ownerState(storage: *Storage) *OwnerState {
568 return @ptrCast(@alignCast(&storage.bytes));
569 }
570
571 fn ownerStateConst(storage: *const Storage) *const OwnerState {
572 return @ptrCast(@alignCast(&storage.bytes));
573 }
574
575 fn owns(checkpoint: *const Checkpoint, owner: *const OwnerState) bool {
576 return checkpoint.ram.len == layout.ram_bytes and
577 std.mem.eql(u8, &checkpoint.root_digest, &owner.root.digest);
578 }
579
580 fn checkedStatus(owner: *const OwnerState) error{CheckpointCorrupt}!Status {
581 return switch (owner.status.load(.acquire)) {
582 @backingInt(Status.empty) => .empty,
583 @backingInt(Status.capturing) => .capturing,
584 @backingInt(Status.published) => .published,
585 else => error.CheckpointCorrupt,
586 };
587 }
588
589 fn reset(owner: *OwnerState) void {
590 owner.status.store(@backingInt(Status.empty), .release);
591 }
592
593 fn buffersOverlap(left: []const u8, right: []const u8) bool {
594 if (left.len == 0 or right.len == 0) return false;
595 const left_start = @intFromPtr(left.ptr);
596 const right_start = @intFromPtr(right.ptr);
597 const left_end = std.math.add(usize, left_start, left.len) catch return true;
598 const right_end = std.math.add(usize, right_start, right.len) catch return true;
599 return left_start < right_end and right_start < left_end;
600 }
601
602 comptime {
603 std.debug.assert(storage_alignment > 0);
604 std.debug.assert(@alignOf(Storage) == storage_alignment);
605 std.debug.assert(@sizeOf(Storage) == storage_bytes);
606 }
607
608 var recovery_ram: [ram_bytes]u8 align(ram_alignment) = undefined;
609
610 test "restore materialization rejects aliased memory" {
611 var storage = Storage.init();
612 const checkpoint: Checkpoint = .{
613 .storage = &storage,
614 .ram = &recovery_ram,
615 .root_digest = @splat(0),
616 };
617 const expected: types.Root = .{
618 .digest = @splat(1),
619 .profile = .{ .digest = @splat(2) },
620 .state = .{ .digest = @splat(3) },
621 };
622 try std.testing.expectError(
623 error.SourceAliasesDestination,
624 materializeForRestore(&checkpoint, expected, &recovery_ram),
625 );
626 }
627
628 test "checkpoint recovery discards unpublished candidates" {
629 var storage = Storage.init();
630 switch (try recoverAfterCrash(&storage, &recovery_ram, null)) {
631 .empty => {},
632 .published => return error.TestExpectedEmptyCheckpoint,
633 }
634
635 const owner = ownerState(&storage);
636 owner.status.store(@backingInt(Status.capturing), .release);
637 switch (try recoverAfterCrash(&storage, &recovery_ram, null)) {
638 .empty => {},
639 .published => return error.TestExpectedEmptyCheckpoint,
640 }
641 owner.status.store(0xff, .release);
642 try std.testing.expectError(
643 error.CheckpointCorrupt,
644 recoverAfterCrash(&storage, &recovery_ram, null),
645 );
646 }