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 }