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

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const canon = @import("../canon/root.zig");
  2 const core = @import("machine_instance_core");
  3 const os = @import("os");
  4 const std = @import("std");
  5 const types = @import("types.zig");
  6 
  7 const layout = core.layout;
  8 const protection = core.protection;
  9 const provenance = core.provenance;
 10 pub const page_count: usize = canon.page_count;
 11 const tree_levels: usize = canon.tree_levels;
 12 const DirtyPageSet = std.bit_set.Static(page_count);
 13 
 14 pub const Error = error{
 15     DeltaAliasesInput,
 16     DeltaCapacityExceeded,
 17     DeltaIndicesInvalid,
 18     DeltaStorageMismatch,
 19     MemoryDigestMismatch,
 20     MemoryNotNormalized,
 21     RamBytesMismatch,
 22 } || provenance.Error;
 23 
 24 /// Result of one changed-page capture. The `memory` field names the normalized
 25 /// child memory image, and the `page_count` field gives how many leading slots
 26 /// of the caller's index and page arrays hold captured data.
 27 pub const Delta = struct {
 28     memory: types.MemoryDigest,
 29     page_count: u16,
 30 };
 31 
 32 /// Checks that a memory image has the right length and normalized shape before
 33 /// hashing it into a memory digest. A length other than 67,108,864 bytes
 34 /// returns `RamBytesMismatch`, and page tables outside their canonical form, or
 35 /// a nonzero byte in the boot frame, either transport ring, or the kernel
 36 /// stack, returns `MemoryNotNormalized`.
 37 pub fn validatedDigest(
 38     ram: []align(layout.page_bytes) const u8,
 39     image: provenance.ImmutableImage,
 40 ) Error!types.MemoryDigest {
 41     try validateNormalizedMemory(ram, image);
 42     return digest(ram);
 43 }
 44 
 45 /// Computes the memory digest of one complete memory image. The call folds
 46 /// every page into the page tree in index order. The exact length is an
 47 /// assertion contract.
 48 pub fn digest(
 49     ram: []align(layout.page_bytes) const u8,
 50 ) types.MemoryDigest {
 51     std.debug.assert(ram.len == layout.ram_bytes);
 52     var frontier: [tree_levels + 1]os.abi.Digest = undefined;
 53     for (0..page_count) |index| {
 54         foldPage(&frontier, index, pageAt(ram, index));
 55     }
 56     return finishDigest(frontier[tree_levels]);
 57 }
 58 
 59 /// Captures the pages of live memory that differ from a normalized parent
 60 /// image. The call writes strictly ascending page indices into the caller's
 61 /// index array and one matching 4096-byte page record into the caller's page
 62 /// array. Page tables, the boot frame, the transport rings, and the stack keep
 63 /// their parent bytes, and no captured index names one of those pages. The two
 64 /// output slices must be disjoint from each other and from both memory images,
 65 /// and each index slot has exactly one aligned page waiting for it in the page
 66 /// array. Every failure, including too few index slots, happens before any
 67 /// output byte is written. The call returns the child memory digest and the
 68 /// number of changed pages.
 69 pub fn captureDelta(
 70     parent: []align(layout.page_bytes) const u8,
 71     current: []align(layout.page_bytes) const u8,
 72     image: provenance.ImmutableImage,
 73     indices: []u16,
 74     pages: []align(layout.page_bytes) u8,
 75 ) Error!Delta {
 76     try validateDeltaStorage(indices, pages);
 77     try validateCaptureAliases(parent, current, indices, pages);
 78     try validateLive(current, image);
 79     try validateNormalizedMemory(parent, image);
 80     var dirty = DirtyPageSet.empty;
 81     const facts = deltaFacts(parent, current, &dirty);
 82     if (facts.page_count > indices.len) return error.DeltaCapacityExceeded;
 83     fillDelta(current, indices, pages, &dirty);
 84     return facts;
 85 }
 86 
 87 /// Computes the memory digest that one ordered set of changed pages produces
 88 /// over a parent memory image. Indices must be strictly ascending, so each page
 89 /// appears once, and each supplied page must differ from the parent page at its
 90 /// index. An index naming a page table, the boot frame, a transport ring, or
 91 /// the stack is rejected. The page array measures 4096 bytes for each index
 92 /// supplied. A rejected index returns `DeltaIndicesInvalid`, and a wrong
 93 /// page-array length returns `DeltaStorageMismatch`.
 94 pub fn deltaDigest(
 95     parent: []align(layout.page_bytes) const u8,
 96     indices: []const u16,
 97     pages: []align(layout.page_bytes) const u8,
 98 ) Error!types.MemoryDigest {
 99     try layout.validateRamBytes(parent.len);
100     try validateDeltaStorage(indices, pages);
101     try validateDelta(parent, indices, pages);
102     return overlayDigest(parent, indices, pages);
103 }
104 
105 /// Rejects any changed page overlapping a load range of the immutable image.
106 /// The call validates the image descriptor before reading its load ranges, and
107 /// an overlapping page returns `DeltaIndicesInvalid`. A caller uses this
108 /// validation to keep a changed page from rewriting the kernel image a capture
109 /// claims to have run.
110 pub fn validateDeltaImage(
111     indices: []const u16,
112     image: provenance.ImmutableImage,
113 ) Error!void {
114     try provenance.validate(image);
115     for (indices) |page_index| {
116         const page_start = @as(u64, page_index) * layout.page_bytes;
117         const page_end = page_start + layout.page_bytes;
118         for (image.loads[0..image.load_count]) |load| {
119             const load_start = std.math.add(
120                 u64,
121                 os.boot.kernel.physical_base,
122                 load.physical_offset,
123             ) catch return error.ImmutableLoadOutOfBounds;
124             const load_end = std.math.add(
125                 u64,
126                 load_start,
127                 load.memory_bytes,
128             ) catch return error.ImmutableLoadOutOfBounds;
129             if (page_start < load_end and load_start < page_end) {
130                 return error.DeltaIndicesInvalid;
131             }
132         }
133     }
134 }
135 
136 /// Rewrites a memory image in place into its normalized form. The call
137 /// canonicalizes the page tables and zeroes the boot frame, the request ring,
138 /// the event ring, and the kernel stack. A protection plan the immutable image
139 /// descriptor rejects returns `MemoryNotNormalized`. A caller uses this call to
140 /// strip the per-run state that would otherwise give two captures of the same
141 /// logical state two different names.
142 pub fn normalize(
143     ram: []align(layout.page_bytes) u8,
144     image: provenance.ImmutableImage,
145 ) error{MemoryNotNormalized}!void {
146     const plan = try protectionPlan(image);
147     layout.canonicalizePageTables(ram, &plan) catch return error.MemoryNotNormalized;
148     zeroPage(ram, layout.boot_frame_address);
149     zeroPage(ram, layout.request_ring_address);
150     zeroPage(ram, layout.event_ring_address);
151     zeroRange(ram, layout.stack_base, layout.stack_end - layout.stack_base);
152 }
153 
154 fn validateNormalized(
155     ram: []align(layout.page_bytes) const u8,
156     plan: *const protection.Plan,
157 ) error{MemoryNotNormalized}!void {
158     layout.validateCanonicalPageTables(ram, plan) catch return error.MemoryNotNormalized;
159     try validateZeroPage(ram, layout.boot_frame_address);
160     try validateZeroPage(ram, layout.request_ring_address);
161     try validateZeroPage(ram, layout.event_ring_address);
162     try validateZeroRange(ram, layout.stack_base, layout.stack_end - layout.stack_base);
163 }
164 
165 fn protectionPlan(
166     image: provenance.ImmutableImage,
167 ) error{MemoryNotNormalized}!protection.Plan {
168     return protection.Plan.fromImmutable(image) catch error.MemoryNotNormalized;
169 }
170 
171 fn validateNormalizedMemory(
172     ram: []align(layout.page_bytes) const u8,
173     image: provenance.ImmutableImage,
174 ) Error!void {
175     try layout.validateRamBytes(ram.len);
176     const plan = try protectionPlan(image);
177     try validateNormalized(ram, &plan);
178 }
179 
180 fn validateLive(
181     ram: []align(layout.page_bytes) const u8,
182     image: provenance.ImmutableImage,
183 ) Error!void {
184     try layout.validateRamBytes(ram.len);
185     try provenance.validateRam(image, ram);
186     const plan = try protectionPlan(image);
187     layout.validateRestorablePageTables(ram, &plan) catch
188         return error.MemoryNotNormalized;
189 }
190 
191 fn deltaFacts(
192     parent: []align(layout.page_bytes) const u8,
193     current: []align(layout.page_bytes) const u8,
194     dirty: *DirtyPageSet,
195 ) Delta {
196     var frontier: [tree_levels + 1]os.abi.Digest = undefined;
197     var dirty_count: u16 = 0;
198     for (0..page_count) |index| {
199         const parent_page = pageAt(parent, index);
200         const current_page = normalizedPage(parent_page, current, index);
201         if (!std.mem.eql(u8, parent_page, current_page)) {
202             dirty.set(index);
203             dirty_count += 1;
204         }
205         foldPage(&frontier, index, current_page);
206     }
207     std.debug.assert(dirty.count() == @as(usize, dirty_count));
208     return .{
209         .memory = finishDigest(frontier[tree_levels]),
210         .page_count = dirty_count,
211     };
212 }
213 
214 fn fillDelta(
215     current: []align(layout.page_bytes) const u8,
216     indices: []u16,
217     pages: []align(layout.page_bytes) u8,
218     dirty: *const DirtyPageSet,
219 ) void {
220     std.debug.assert(dirty.count() <= indices.len);
221     var output_index: usize = 0;
222     var iterator = dirty.iterator(.{});
223     while (iterator.next()) |index| {
224         std.debug.assert(output_index < indices.len);
225         indices[output_index] = @intCast(index);
226         const start = output_index * layout.page_bytes;
227         @memcpy(pages[start..][0..layout.page_bytes], pageAt(current, index));
228         output_index += 1;
229     }
230     std.debug.assert(output_index == dirty.count());
231 }
232 
233 fn overlayDigest(
234     parent: []align(layout.page_bytes) const u8,
235     indices: []const u16,
236     pages: []align(layout.page_bytes) const u8,
237 ) types.MemoryDigest {
238     var frontier: [tree_levels + 1]os.abi.Digest = undefined;
239     var delta_index: usize = 0;
240     for (0..page_count) |index| {
241         const page = if (delta_index < indices.len and indices[delta_index] == index)
242             pageAt(pages, delta_index)
243         else
244             pageAt(parent, index);
245         if (delta_index < indices.len and indices[delta_index] == index) {
246             delta_index += 1;
247         }
248         foldPage(&frontier, index, page);
249     }
250     std.debug.assert(delta_index == indices.len);
251     return finishDigest(frontier[tree_levels]);
252 }
253 
254 fn validateDeltaStorage(indices: []const u16, pages: []const u8) Error!void {
255     if (indices.len > page_count) return error.DeltaCapacityExceeded;
256     const expected = std.math.mul(
257         usize,
258         indices.len,
259         layout.page_bytes,
260     ) catch return error.DeltaStorageMismatch;
261     if (pages.len != expected) return error.DeltaStorageMismatch;
262     if (buffersOverlap(std.mem.sliceAsBytes(indices), pages)) {
263         return error.DeltaAliasesInput;
264     }
265 }
266 
267 fn validateCaptureAliases(
268     parent: []const u8,
269     current: []const u8,
270     indices: []const u16,
271     pages: []const u8,
272 ) Error!void {
273     const index_bytes = std.mem.sliceAsBytes(indices);
274     if (buffersOverlap(index_bytes, parent) or
275         buffersOverlap(index_bytes, current) or
276         buffersOverlap(pages, parent) or
277         buffersOverlap(pages, current))
278     {
279         return error.DeltaAliasesInput;
280     }
281 }
282 
283 fn validateDelta(
284     parent: []align(layout.page_bytes) const u8,
285     indices: []const u16,
286     pages: []align(layout.page_bytes) const u8,
287 ) Error!void {
288     var previous: ?u16 = null;
289     for (indices, 0..) |page_index, index| {
290         if (page_index >= page_count or retainedFromParent(page_index)) {
291             return error.DeltaIndicesInvalid;
292         }
293         if (previous) |value| {
294             if (page_index <= value) return error.DeltaIndicesInvalid;
295         }
296         const page = pageAt(pages, index);
297         if (std.mem.eql(u8, pageAt(parent, page_index), page)) {
298             return error.DeltaIndicesInvalid;
299         }
300         previous = page_index;
301     }
302 }
303 
304 fn normalizedPage(
305     parent: *const [layout.page_bytes]u8,
306     current: []align(layout.page_bytes) const u8,
307     index: usize,
308 ) *const [layout.page_bytes]u8 {
309     return if (retainedFromParent(index)) parent else pageAt(current, index);
310 }
311 
312 fn retainedFromParent(index: usize) bool {
313     std.debug.assert(index < page_count);
314     const address = @as(u64, index) * layout.page_bytes;
315     for (layout.page_table_addresses) |table| {
316         if (address == table) return true;
317     }
318     if (address == layout.boot_frame_address or
319         address == layout.request_ring_address or
320         address == layout.event_ring_address)
321     {
322         return true;
323     }
324     return address >= layout.stack_base and address < layout.stack_end;
325 }
326 
327 fn pageAt(
328     bytes: []align(layout.page_bytes) const u8,
329     index: usize,
330 ) *const [layout.page_bytes]u8 {
331     const start = index * layout.page_bytes;
332     std.debug.assert(start + layout.page_bytes <= bytes.len);
333     return @ptrCast(bytes[start..][0..layout.page_bytes]);
334 }
335 
336 fn foldPage(
337     frontier: *[tree_levels + 1]os.abi.Digest,
338     index: usize,
339     page: *const [layout.page_bytes]u8,
340 ) void {
341     var node = canon.leaf(index, canon.page(page));
342     var position = index;
343     var level: usize = 0;
344     while (position & 1 == 1) : (level += 1) {
345         std.debug.assert(level < tree_levels);
346         node = canon.node(@intCast(level), frontier[level], node);
347         position >>= 1;
348     }
349     frontier[level] = node;
350 }
351 
352 fn finishDigest(root: os.abi.Digest) types.MemoryDigest {
353     return .{ .digest = canon.memory(root) };
354 }
355 
356 fn buffersOverlap(left: []const u8, right: []const u8) bool {
357     if (left.len == 0 or right.len == 0) return false;
358     const left_start = @intFromPtr(left.ptr);
359     const right_start = @intFromPtr(right.ptr);
360     const left_end = std.math.add(usize, left_start, left.len) catch return true;
361     const right_end = std.math.add(usize, right_start, right.len) catch return true;
362     return left_start < right_end and right_start < left_end;
363 }
364 
365 fn zeroPage(ram: []u8, address: u64) void {
366     zeroRange(ram, address, layout.page_bytes);
367 }
368 
369 fn validateZeroPage(ram: []const u8, address: u64) error{MemoryNotNormalized}!void {
370     return validateZeroRange(ram, address, layout.page_bytes);
371 }
372 
373 fn zeroRange(ram: []u8, address: u64, bytes: usize) void {
374     const start: usize = @intCast(address);
375     @memset(ram[start..][0..bytes], 0);
376 }
377 
378 fn validateZeroRange(
379     ram: []const u8,
380     address: u64,
381     bytes: usize,
382 ) error{MemoryNotNormalized}!void {
383     const start: usize = @intCast(address);
384     for (ram[start..][0..bytes]) |byte| {
385         if (byte != 0) return error.MemoryNotNormalized;
386     }
387 }
388 
389 comptime {
390     std.debug.assert(layout.boot_frame_address % layout.page_bytes == 0);
391     std.debug.assert(layout.pml4_address % layout.page_bytes == 0);
392     std.debug.assert(layout.pdpt_address % layout.page_bytes == 0);
393     std.debug.assert(layout.page_directory_address % layout.page_bytes == 0);
394     std.debug.assert(layout.request_ring_address % layout.page_bytes == 0);
395     std.debug.assert(layout.event_ring_address % layout.page_bytes == 0);
396     std.debug.assert(layout.stack_base % layout.page_bytes == 0);
397     std.debug.assert(layout.stack_end % layout.page_bytes == 0);
398     std.debug.assert(layout.boot_frame_address + layout.page_bytes <= layout.ram_bytes);
399     std.debug.assert(layout.request_ring_address + layout.page_bytes <= layout.ram_bytes);
400     std.debug.assert(layout.event_ring_address + layout.page_bytes <= layout.ram_bytes);
401     std.debug.assert(layout.stack_base < layout.stack_end);
402     std.debug.assert(layout.stack_end <= layout.ram_bytes);
403     std.debug.assert(std.math.isPowerOfTwo(page_count));
404     std.debug.assert(page_count == 16_384);
405     std.debug.assert(tree_levels == 14);
406 }
407 
408 var delta_parent: [layout.ram_bytes]u8 align(layout.page_bytes) = @splat(0);
409 var delta_oracle: [layout.ram_bytes]u8 align(layout.page_bytes) = @splat(0);
410 var delta_pages: [2 * layout.page_bytes]u8 align(layout.page_bytes) = undefined;
411 const capture_changed_pages = [_]usize{ 128, 129 };
412 const capture_retained_page: usize = @intCast(
413     layout.boot_frame_address / layout.page_bytes,
414 );
415 
416 const CaptureReference = struct {
417     delta: Delta,
418     indices: [capture_changed_pages.len]u16,
419     pages: [capture_changed_pages.len * layout.page_bytes]u8 align(layout.page_bytes),
420 
421     fn capture(
422         parent: []align(layout.page_bytes) const u8,
423         current: []align(layout.page_bytes) const u8,
424     ) CaptureReference {
425         var reference: CaptureReference = .{
426             .delta = facts(parent, current),
427             .indices = undefined,
428             .pages = undefined,
429         };
430         fill(
431             parent,
432             current,
433             &reference.indices,
434             &reference.pages,
435             reference.delta.page_count,
436         );
437         return reference;
438     }
439 
440     fn facts(
441         parent: []align(layout.page_bytes) const u8,
442         current: []align(layout.page_bytes) const u8,
443     ) Delta {
444         var frontier: [tree_levels + 1]os.abi.Digest = undefined;
445         var dirty_count: u16 = 0;
446         for (0..page_count) |index| {
447             const parent_page = pageAt(parent, index);
448             const current_page = referencePage(parent_page, current, index);
449             if (!std.mem.eql(u8, parent_page, current_page)) dirty_count += 1;
450             foldPage(&frontier, index, current_page);
451         }
452         return .{
453             .memory = finishDigest(frontier[tree_levels]),
454             .page_count = dirty_count,
455         };
456     }
457 
458     fn fill(
459         parent: []align(layout.page_bytes) const u8,
460         current: []align(layout.page_bytes) const u8,
461         indices: []u16,
462         pages: []align(layout.page_bytes) u8,
463         dirty_count: u16,
464     ) void {
465         var output_index: usize = 0;
466         for (0..page_count) |index| {
467             const parent_page = pageAt(parent, index);
468             const current_page = referencePage(parent_page, current, index);
469             if (std.mem.eql(u8, parent_page, current_page)) continue;
470             std.debug.assert(output_index < indices.len);
471             indices[output_index] = @intCast(index);
472             const start = output_index * layout.page_bytes;
473             @memcpy(pages[start..][0..layout.page_bytes], current_page);
474             output_index += 1;
475         }
476         std.debug.assert(output_index == dirty_count);
477     }
478 
479     fn referencePage(
480         parent: *const [layout.page_bytes]u8,
481         current: []align(layout.page_bytes) const u8,
482         index: usize,
483     ) *const [layout.page_bytes]u8 {
484         return if (referenceRetainedFromParent(index)) parent else pageAt(current, index);
485     }
486 
487     fn referenceRetainedFromParent(index: usize) bool {
488         std.debug.assert(index < page_count);
489         const address = @as(u64, index) * layout.page_bytes;
490         for (layout.page_table_addresses) |table| {
491             if (address == table) return true;
492         }
493         if (address == layout.boot_frame_address or
494             address == layout.request_ring_address or
495             address == layout.event_ring_address)
496         {
497             return true;
498         }
499         return address >= layout.stack_base and address < layout.stack_end;
500     }
501 };
502 
503 fn prepareCaptureFixture() !provenance.ImmutableImage {
504     const source = @embedFile("machine-k0-elf");
505     const execution = try os.boot.kernel.manifest.parse(
506         @embedFile("machine-k0-manifest"),
507     );
508     const plan = try protection.Plan.init(execution);
509     const image = provenance.fromSource(execution, source);
510     const frame: os.abi.BootFrame = .{
511         .fence = .{
512             .world = @splat(0x11),
513             .generation = 1,
514             .token = @splat(0x22),
515         },
516         .contract_digest = @splat(0x33),
517         .image_digest = image.digest,
518         .ram_bytes = layout.ram_bytes,
519         .request_ring_address = layout.request_ring_address,
520         .event_ring_address = layout.event_ring_address,
521         .initial_time_tick = 0,
522         .entropy_generation = 1,
523         .terminal_offset = 0,
524         .effect_frontier = 0,
525         .block_root = @splat(0x44),
526         .input_frontier = 0,
527         .terminal_input_offset = 0,
528     };
529     var wire: os.abi.BootWire = undefined;
530     try os.abi.encodeBootFrame(frame, &wire);
531     try layout.populate(&delta_parent, source, execution, &plan, frame, &wire);
532     try normalize(&delta_parent, image);
533     @memcpy(&delta_oracle, &delta_parent);
534     return image;
535 }
536 
537 test "capture delta matches the pre-fusion oracle" {
538     const image = try prepareCaptureFixture();
539     for (capture_changed_pages, 0..) |index, value| {
540         delta_oracle[index * layout.page_bytes] = @intCast(0x41 + value);
541     }
542     delta_oracle[capture_retained_page * layout.page_bytes] = 0x51;
543     try std.testing.expect(!std.mem.eql(
544         u8,
545         pageAt(&delta_parent, capture_retained_page),
546         pageAt(&delta_oracle, capture_retained_page),
547     ));
548     const expected = CaptureReference.capture(&delta_parent, &delta_oracle);
549     var indices: [capture_changed_pages.len]u16 = @splat(0);
550     @memset(&delta_pages, 0);
551     const actual = try captureDelta(
552         &delta_parent,
553         &delta_oracle,
554         image,
555         &indices,
556         &delta_pages,
557     );
558     try std.testing.expectEqualDeep(expected.delta.memory, actual.memory);
559     try std.testing.expectEqual(expected.delta.page_count, actual.page_count);
560     try std.testing.expectEqualSlices(u16, &expected.indices, &indices);
561     try std.testing.expectEqualSlices(u8, &expected.pages, &delta_pages);
562     for (capture_changed_pages, indices) |expected_index, actual_index| {
563         try std.testing.expectEqual(@as(u16, @intCast(expected_index)), actual_index);
564     }
565     try std.testing.expect(std.mem.indexOfScalar(
566         u16,
567         &indices,
568         @intCast(capture_retained_page),
569     ) == null);
570 }
571 
572 test "capture delta refuses capacity before writing output" {
573     const image = try prepareCaptureFixture();
574     delta_oracle[capture_changed_pages[0] * layout.page_bytes] = 0x61;
575     delta_oracle[capture_changed_pages[1] * layout.page_bytes] = 0x62;
576     var indices = [_]u16{0xa5a5};
577     const pages = delta_pages[0..layout.page_bytes];
578     @memset(pages, 0x5a);
579     try std.testing.expectError(
580         error.DeltaCapacityExceeded,
581         captureDelta(&delta_parent, &delta_oracle, image, &indices, pages),
582     );
583     try std.testing.expectEqual(@as(u16, 0xa5a5), indices[0]);
584     try std.testing.expect(std.mem.allEqual(u8, pages, 0x5a));
585 }
586 
587 test "dirty page digest equals a full image oracle" {
588     @memset(&delta_parent, 0);
589     @memset(&delta_pages, 0);
590     delta_pages[0] = 0x41;
591     delta_pages[layout.page_bytes] = 0x42;
592     const indices = [_]u16{
593         @intCast(page_count - 2),
594         @intCast(page_count - 1),
595     };
596     const sparse = try deltaDigest(&delta_parent, &indices, &delta_pages);
597     @memcpy(&delta_oracle, &delta_parent);
598     delta_oracle[indices[0] * layout.page_bytes] = 0x41;
599     delta_oracle[indices[1] * layout.page_bytes] = 0x42;
600     try std.testing.expectEqualDeep(digest(&delta_oracle), sparse);
601     try std.testing.expect(!std.meta.eql(digest(&delta_parent), sparse));
602 }
603 
604 test "dirty page digest rejects noncanonical overlays" {
605     @memset(&delta_parent, 0);
606     @memset(&delta_pages, 0x51);
607     const reversed = [_]u16{ 512, 256 };
608     try std.testing.expectError(
609         error.DeltaIndicesInvalid,
610         deltaDigest(&delta_parent, &reversed, &delta_pages),
611     );
612     const normalized = [_]u16{ 1, 256 };
613     try std.testing.expectError(
614         error.DeltaIndicesInvalid,
615         deltaDigest(&delta_parent, &normalized, &delta_pages),
616     );
617 }
618 
619 test "dirty pages cannot replace immutable data" {
620     const manifest = os.boot.kernel.manifest;
621     var image: provenance.ImmutableImage = .{
622         .digest = @splat(0),
623         .initial = undefined,
624         .entry_offset = 0,
625         .load_count = 2,
626         .loads = @splat(.{}),
627     };
628     image.loads[0] = .{
629         .source_offset = 0,
630         .file_bytes = layout.page_bytes,
631         .memory_bytes = layout.page_bytes,
632         .physical_offset = 0,
633         .virtual_offset = 0,
634         .flags = manifest.load_flag_read | manifest.load_flag_execute,
635     };
636     image.loads[1] = .{
637         .source_offset = layout.page_bytes,
638         .file_bytes = layout.page_bytes,
639         .memory_bytes = layout.page_bytes,
640         .physical_offset = layout.page_bytes,
641         .virtual_offset = layout.page_bytes,
642         .flags = manifest.load_flag_read,
643     };
644     const read_only_page = [_]u16{@intCast(
645         (os.boot.kernel.physical_base + layout.page_bytes) / layout.page_bytes,
646     )};
647     try std.testing.expectError(
648         error.DeltaIndicesInvalid,
649         validateDeltaImage(&read_only_page, image),
650     );
651 }