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 }