lib/machine/src/checkpoint/hot/owner.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const checkpoint = @import("../root.zig");
  2 const std = @import("std");
  3 
  4 pub const page_bytes: usize = checkpoint.ram_alignment;
  5 pub const page_count: usize = checkpoint.page_count;
  6 
  7 const HotError = error{
  8     SnapshotAliasesDestination,
  9 };
 10 
 11 /// The error set combines the failures of a durable checkpoint with destination
 12 /// aliasing for a snapshot.
 13 pub const Error = checkpoint.DurableError || HotError;
 14 
 15 /// The capacity fixes how much storage a caller supplies for a capped number of
 16 /// changed pages. The byte counts cover one two-byte index and one aligned
 17 /// 4096-byte page for each slot.
 18 pub const Capacity = struct {
 19     pages: u16,
 20     index_storage_bytes: u32,
 21     page_storage_bytes: u32,
 22 
 23     /// The function derives the exact byte counts for `pages` slots. A count
 24     /// above 16,384 returns `DeltaCapacityExceeded`.
 25     pub fn derive(pages: usize) error{DeltaCapacityExceeded}!Capacity {
 26         if (pages > page_count) return error.DeltaCapacityExceeded;
 27         const index_storage_bytes = std.math.mul(
 28             usize,
 29             pages,
 30             @sizeOf(u16),
 31         ) catch return error.DeltaCapacityExceeded;
 32         const page_storage_bytes = std.math.mul(
 33             usize,
 34             pages,
 35             page_bytes,
 36         ) catch return error.DeltaCapacityExceeded;
 37         return .{
 38             .pages = @intCast(pages),
 39             .index_storage_bytes = @intCast(index_storage_bytes),
 40             .page_storage_bytes = @intCast(page_storage_bytes),
 41         };
 42     }
 43 };
 44 
 45 pub const maximum_capacity = Capacity.derive(page_count) catch unreachable;
 46 
 47 /// The storage holds the caller-owned output arrays for one changed-page
 48 /// capture. The value borrows both slices. The caller keeps their addresses and
 49 /// captured prefixes unchanged for as long as a snapshot can be used.
 50 pub const Storage = struct {
 51     capacity: Capacity,
 52     indices: []u16,
 53     pages: []align(page_bytes) u8,
 54 
 55     /// The function joins an index slice and a page slice that carry the same
 56     /// number of slots. Every index slot has one aligned page behind it in the
 57     /// page storage. More than 16,384 indices returns `DeltaCapacityExceeded`.
 58     /// Any other page-storage length returns `DeltaStorageMismatch`.
 59     pub fn init(
 60         indices: []u16,
 61         pages: []align(page_bytes) u8,
 62     ) Error!Storage {
 63         const capacity = try Capacity.derive(indices.len);
 64         if (pages.len != capacity.page_storage_bytes) {
 65             return error.DeltaStorageMismatch;
 66         }
 67         return .{
 68             .capacity = capacity,
 69             .indices = indices,
 70             .pages = pages,
 71         };
 72     }
 73 };
 74 
 75 /// The record holds the restore result from a snapshot. The `copied_pages`
 76 /// field counts the changed pages written into the caller-owned destination
 77 /// memory.
 78 pub const Materialized = struct {
 79     material: checkpoint.Material,
 80     copied_pages: u16,
 81 };
 82 
 83 const Inspection = struct {
 84     parent: checkpoint.Contents,
 85     identity: checkpoint.Identity,
 86 };
 87 
 88 /// The snapshot holds a changed-page checkpoint borrowing a durable parent and
 89 /// the captured storage. The caller preserves the parent, the indices, and the
 90 /// page bytes for the snapshot's lifetime.
 91 pub const Snapshot = struct {
 92     parent: *const checkpoint.Checkpoint,
 93     identity_value: checkpoint.Identity,
 94     memory: checkpoint.MemoryDigest,
 95     material: checkpoint.Material,
 96     indices: []const u16,
 97     pages: []align(page_bytes) const u8,
 98 
 99     /// The function reports how many ordered changed pages the snapshot holds.
100     /// A caller uses this count to size a report or a later publication.
101     pub fn dirtyPageCount(self: *const @This()) u16 {
102         std.debug.assert(self.indices.len <= page_count);
103         return @intCast(self.indices.len);
104     }
105 
106     /// The function revalidates the parent, the changed pages, the memory
107     /// digest, and the stored identity. A recomputed value differing from the
108     /// stored one returns `CheckpointCorrupt`.
109     pub fn identity(self: *const @This()) checkpoint.DurableError!checkpoint.Identity {
110         return (try inspect(self)).identity;
111     }
112 
113     fn inspect(self: *const @This()) checkpoint.DurableError!Inspection {
114         const parent_contents = try checkpoint.inspect(self.parent);
115         try checkpoint.validateParent(self.material, parent_contents.material);
116         try checkpoint.validateDeltaImage(
117             self.indices,
118             self.material.immutable_image,
119         );
120         const memory = try checkpoint.deltaDigest(
121             self.parent.ram,
122             self.indices,
123             self.pages,
124         );
125         if (!std.meta.eql(memory, self.memory)) {
126             return error.CheckpointCorrupt;
127         }
128         const actual = try checkpoint.identify(self.material, memory);
129         if (!std.meta.eql(actual, self.identity_value)) {
130             return error.CheckpointCorrupt;
131         }
132         return .{ .parent = parent_contents, .identity = actual };
133     }
134 
135     /// The function returns the fully verified child checkpoint root.
136     pub fn root(self: *const @This()) checkpoint.DurableError!checkpoint.Root {
137         return (try self.identity()).root;
138     }
139 
140     /// The function verifies `expected` and requires the destination to already
141     /// hold the parent memory image. The snapshot storage and the destination
142     /// memory must be disjoint. The call returns the verified identity.
143     pub fn prepareForRestore(
144         self: *const @This(),
145         expected: checkpoint.Root,
146         destination: []align(page_bytes) const u8,
147     ) Error!checkpoint.Identity {
148         const inspection = try inspect(self);
149         if (!std.meta.eql(inspection.identity.root, expected)) {
150             return error.CheckpointRootMismatch;
151         }
152         try validateDestinationAgainst(self, destination, inspection.parent);
153         return inspection.identity;
154     }
155 
156     /// The function overlays the changed pages on authenticated parent memory,
157     /// then verifies the child memory digest. The call returns the material and
158     /// the number of copied pages. Pages already copied stay in the destination
159     /// when the closing digest check fails.
160     pub fn materializeForRestore(
161         self: *const @This(),
162         expected: checkpoint.Root,
163         destination: []align(page_bytes) u8,
164     ) Error!Materialized {
165         const inspection = try inspect(self);
166         if (!std.meta.eql(inspection.identity.root, expected)) {
167             return error.CheckpointRootMismatch;
168         }
169         try validateDestinationAgainst(self, destination, inspection.parent);
170         for (self.indices, 0..) |page_index, index| {
171             const start = @as(usize, page_index) * page_bytes;
172             const source = self.pages[index * page_bytes ..][0..page_bytes];
173             @memcpy(destination[start..][0..page_bytes], source);
174         }
175         const restored = try checkpoint.validatedMemoryDigest(
176             destination,
177             self.material.immutable_image,
178         );
179         if (!std.meta.eql(restored, self.memory)) {
180             return error.MemoryDigestMismatch;
181         }
182         return .{
183             .material = self.material,
184             .copied_pages = self.dirtyPageCount(),
185         };
186     }
187 
188     /// The function returns the durable parent's complete normalized memory.
189     pub fn evidenceRam(
190         self: *const @This(),
191     ) []align(page_bytes) const u8 {
192         return self.parent.ram;
193     }
194 
195     /// The function answers whether a byte range touches the snapshot value,
196     /// the parent handle, the storage and memory behind that parent, or the
197     /// captured index and page arrays.
198     pub fn aliases(self: *const @This(), bytes: []const u8) bool {
199         return buffersOverlap(bytes, std.mem.asBytes(self)) or
200             buffersOverlap(bytes, std.mem.asBytes(self.parent)) or
201             buffersOverlap(bytes, &self.parent.storage.bytes) or
202             buffersOverlap(bytes, self.parent.ram) or
203             buffersOverlap(bytes, std.mem.sliceAsBytes(self.indices)) or
204             buffersOverlap(bytes, self.pages);
205     }
206 };
207 
208 fn validateDestinationAgainst(
209     snapshot: *const Snapshot,
210     destination: []align(page_bytes) const u8,
211     parent: checkpoint.Contents,
212 ) Error!void {
213     if (snapshot.aliases(destination)) {
214         return error.SnapshotAliasesDestination;
215     }
216     const actual = try checkpoint.validatedMemoryDigest(
217         destination,
218         parent.material.immutable_image,
219     );
220     if (!std.meta.eql(actual, parent.memory)) {
221         return error.MemoryDigestMismatch;
222     }
223 }
224 
225 /// The function captures ordered changed pages from live memory against a
226 /// durable parent. The material must share the parent's profile fingerprint,
227 /// immutable image descriptor, and execution fingerprint. The supplied storage
228 /// bounds the captured page count, and exceeding it returns
229 /// `DeltaCapacityExceeded`. Failures before the identity is built leave both
230 /// arrays unchanged, and rejection of the identity can leave both arrays
231 /// changed. On success the returned snapshot holds borrowed references to the
232 /// parent and to both output arrays.
233 pub fn capture(
234     parent: *const checkpoint.Checkpoint,
235     material: checkpoint.Material,
236     current: []align(page_bytes) const u8,
237     storage: Storage,
238 ) Error!Snapshot {
239     const parent_contents = try checkpoint.inspect(parent);
240     try checkpoint.validateParent(material, parent_contents.material);
241     try validateStorage(storage);
242     try validateStorageAliases(parent, storage);
243     const delta = try checkpoint.captureDelta(
244         parent.ram,
245         current,
246         material.immutable_image,
247         storage.indices,
248         storage.pages,
249     );
250     const count: usize = delta.page_count;
251     const identity_value = try checkpoint.identify(material, delta.memory);
252     return .{
253         .parent = parent,
254         .identity_value = identity_value,
255         .memory = delta.memory,
256         .material = material,
257         .indices = storage.indices[0..count],
258         .pages = storage.pages[0 .. count * page_bytes],
259     };
260 }
261 
262 fn validateStorage(storage: Storage) Error!void {
263     const actual = try Capacity.derive(storage.indices.len);
264     if (!std.meta.eql(actual, storage.capacity) or
265         storage.pages.len != storage.capacity.page_storage_bytes)
266     {
267         return error.DeltaStorageMismatch;
268     }
269 }
270 
271 fn validateStorageAliases(
272     parent: *const checkpoint.Checkpoint,
273     storage: Storage,
274 ) Error!void {
275     const index_bytes = std.mem.sliceAsBytes(storage.indices);
276     if (buffersOverlap(index_bytes, std.mem.asBytes(parent)) or
277         buffersOverlap(index_bytes, &parent.storage.bytes) or
278         buffersOverlap(storage.pages, std.mem.asBytes(parent)) or
279         buffersOverlap(storage.pages, &parent.storage.bytes))
280     {
281         return error.DeltaAliasesInput;
282     }
283 }
284 
285 fn buffersOverlap(left: []const u8, right: []const u8) bool {
286     if (left.len == 0 or right.len == 0) return false;
287     const left_start = @intFromPtr(left.ptr);
288     const right_start = @intFromPtr(right.ptr);
289     const left_end = std.math.add(usize, left_start, left.len) catch return true;
290     const right_end = std.math.add(usize, right_start, right.len) catch return true;
291     return left_start < right_end and right_start < left_end;
292 }
293 
294 test "dirty tracking capacity rejects max plus one" {
295     try std.testing.expectEqual(
296         @as(u16, page_count),
297         maximum_capacity.pages,
298     );
299     try std.testing.expectError(
300         error.DeltaCapacityExceeded,
301         Capacity.derive(page_count + 1),
302     );
303 }