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 }