lib/machine/src/world/restore.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const canon = @import("canon.zig");
2 const checkpoint = @import("../checkpoint/root.zig");
3 const fabric = @import("../fabric/root.zig");
4 const instance = @import("../instance/root.zig");
5 const moment = @import("moment.zig");
6 const os = @import("os");
7 const profile = @import("../profile/root.zig");
8 const std = @import("std");
9 const types = @import("types.zig");
10
11 const RestoreOwnerError = error{
12 FabricMismatch,
13 MachineBoundaryUnavailable,
14 MachineModeMismatch,
15 NodeCountMismatch,
16 NodeMismatch,
17 ProfileMismatch,
18 RestoreAlias,
19 SemanticBoundaryMismatch,
20 };
21
22 const StableOwnerError = error{
23 CapabilitySetMismatch,
24 };
25
26 pub const Error = canon.Error ||
27 checkpoint.Error ||
28 fabric.Error ||
29 instance.RestoreError ||
30 moment.Error ||
31 profile.Error ||
32 RestoreOwnerError;
33
34 pub const StableError = fabric.Error || moment.Error || StableOwnerError;
35
36 /// What restoring one live node requires: the activation fence, the K0 execution
37 /// manifest, the execution profile, instance storage, and aligned guest RAM. A caller
38 /// fills one of these in for every node it wants running again. The caller owns
39 /// the storage and the RAM, and both are borrowed for the life of the instance.
40 pub const Target = struct {
41 profile: profile.Profile,
42 execution_manifest: []const u8,
43 fence: os.abi.ActivationFence,
44 storage: *instance.Storage,
45 ram: []align(instance.ram_alignment) u8,
46 };
47
48 /// The form a node takes when it comes back. A caller sets this per node to say
49 /// whether that node comes back running. Live rebuilds an instance from caller storage,
50 /// and retained constructs no instance.
51 pub const Mode = union(enum) {
52 live: Target,
53 retained,
54 };
55
56 /// What one node brings to a restore: its identity, the checkpoint it restores from,
57 /// and its mode. A caller builds one of these per node and hands the array to `restore`.
58 pub const Binding = struct {
59 node: fabric.NodeId,
60 checkpoint: checkpoint.Source,
61 mode: Mode,
62 };
63
64 /// A node after restore, naming itself and carrying a live instance if live was
65 /// the mode asked for. A caller reaches through these to drive one machine of a
66 /// restored world.
67 pub const Node = struct {
68 id: fabric.NodeId,
69 machine: ?instance.Instance,
70 };
71
72 /// A world rebuilt out of a cut, holding the origin root, the ledger the caller
73 /// supplied, the moment it stands at, and an entry for every node. Every turn owner
74 /// takes this value to own the live machines. Turn owners mutate a restored world
75 /// in place. `deinit` tears down every live instance and clears each entry.
76 pub const Restored = struct {
77 root: types.Root,
78 fabric: *fabric.Fabric,
79 moment: moment.Moment,
80 node_count: u8,
81 nodes: [types.node_limit]Node,
82
83 /// Checks a stored moment against both the origin and the position the ledger
84 /// holds, then hands it back. A caller reads the world's position through this,
85 /// because the read verifies before it returns.
86 pub fn currentMoment(self: *const @This()) moment.Error!moment.Moment {
87 try moment.verify(self.moment, self.root, self.fabric.root());
88 return self.moment;
89 }
90
91 pub fn deinit(self: *@This()) void {
92 for (&self.nodes) |*node| {
93 if (node.machine) |*machine| machine.deinit();
94 node.machine = null;
95 }
96 }
97 };
98
99 /// A node whose backend is missing on this host, together with the reason that backend
100 /// gives, so a caller tells a host that lacks the backend apart from a world that
101 /// failed to check out.
102 pub const Unavailable = struct {
103 node: fabric.NodeId,
104 backend: instance.Unavailable,
105 };
106
107 /// A named failure, carrying the node involved whenever the code can name one, so
108 /// a caller reads the value to learn which node a failure concerns.
109 pub const Rejection = struct {
110 node: ?fabric.NodeId,
111 failure: Error,
112 };
113
114 /// The restore outcome, switched on by a caller so the caller takes ownership of
115 /// the live machines in exactly one arm. Ownership of the live instances passes
116 /// to the caller under `ready` and under no other outcome. The other two arms leave
117 /// nothing constructed, because construction tears down what it built before returning
118 /// them.
119 pub const Result = union(enum) {
120 ready: Restored,
121 unavailable: Unavailable,
122 rejected: Rejection,
123 };
124
125 /// Establishes that a restored world may be mutated safely and returns the moment
126 /// it verified, called first by every turn owner so a world that drifted is caught
127 /// before anything moves. The moment must still match the origin and the ledger
128 /// position. Every ledger boundary must agree with the live instance set: an available
129 /// node needs a live, active instance, and an unavailable node needs none. The node
130 /// count must equal the count the world root recorded, and every entry past it must
131 /// hold the empty value.
132 pub fn validateStable(restored: *const Restored) StableError!moment.Moment {
133 const current = try restored.currentMoment();
134 if (restored.node_count == 0 or
135 restored.node_count > restored.nodes.len or
136 restored.node_count != restored.root.node_count)
137 {
138 return error.CapabilitySetMismatch;
139 }
140 const cut = try restored.fabric.cut();
141 if (cut.node_count != restored.node_count) {
142 return error.CapabilitySetMismatch;
143 }
144 for (cut.nodes[0..cut.node_count], 0..) |boundary, index| {
145 const node = &restored.nodes[index];
146 if (!std.meta.eql(boundary.id, node.id)) {
147 return error.CapabilitySetMismatch;
148 }
149 if (boundary.available) {
150 const machine = if (node.machine) |*value|
151 value
152 else
153 return error.CapabilitySetMismatch;
154 if (!machine.active()) return error.CapabilitySetMismatch;
155 } else if (node.machine != null) {
156 return error.CapabilitySetMismatch;
157 }
158 }
159 for (restored.nodes[restored.node_count..]) |node| {
160 if (!std.meta.eql(node, emptyNode())) {
161 return error.CapabilitySetMismatch;
162 }
163 }
164 return current;
165 }
166
167 /// Brings a world back from a sealed cut so a caller brings a stored boundary back
168 /// as running machines. The ledger has to sit at precisely the position the cut
169 /// recorded. Each binding is checked against both the cut and the ledger boundary,
170 /// covering the identity, the checkpoint root, the semantic digest, the contract
171 /// fingerprint, the profile fingerprint, and the fence. Live storage buffers must
172 /// not overlap each other, the ledger, or any checkpoint. The first backend missing
173 /// on this host halts construction, and everything built so far is torn down.
174 pub fn restore(
175 cut: types.Cut,
176 fabric_owner: *fabric.Fabric,
177 bindings: []const Binding,
178 ) Result {
179 canon.verify(cut) catch |failure| return reject(null, failure);
180 const fabric_cut = fabric_owner.cut() catch |failure|
181 return reject(null, failure);
182 if (!std.meta.eql(fabric_cut.root, cut.root.fabric)) {
183 return reject(null, error.FabricMismatch);
184 }
185 if (bindings.len != cut.root.node_count or
186 bindings.len != fabric_cut.node_count)
187 {
188 return reject(null, error.NodeCountMismatch);
189 }
190 var owned: [types.node_limit]Binding = undefined;
191 @memcpy(owned[0..bindings.len], bindings);
192 validateAliases(fabric_owner, owned[0..bindings.len]) catch |failure|
193 return reject(null, failure);
194 for (owned[0..bindings.len], 0..) |binding, index| {
195 const boundary = fabric_cut.nodes[index];
196 const node = cut.nodes[index];
197 if (!std.meta.eql(binding.node, node.id) or
198 !std.meta.eql(binding.node, boundary.id))
199 {
200 return reject(binding.node, error.NodeMismatch);
201 }
202 if (boundary.machine.kind != .semantic) {
203 return reject(binding.node, error.MachineBoundaryUnavailable);
204 }
205 const identity = switch (binding.mode) {
206 .live => |target| live: {
207 if (!boundary.available) {
208 return reject(binding.node, error.MachineModeMismatch);
209 }
210 validateTarget(cut.root, node.machine, target) catch |failure|
211 return reject(binding.node, failure);
212 break :live instance.validateRestore(
213 target.storage,
214 target.ram,
215 .{
216 .checkpoint = binding.checkpoint,
217 .expected_root = node.machine,
218 .profile = target.profile,
219 .execution_manifest = target.execution_manifest,
220 .fence = target.fence,
221 },
222 ) catch |failure| return reject(binding.node, failure);
223 },
224 .retained => retained: {
225 if (boundary.available) {
226 return reject(binding.node, error.MachineModeMismatch);
227 }
228 break :retained binding.checkpoint.identity() catch |failure|
229 return reject(binding.node, failure);
230 },
231 };
232 if (!std.meta.eql(identity.root, node.machine)) {
233 return reject(binding.node, error.CheckpointRootMismatch);
234 }
235 if (!std.mem.eql(u8, &identity.semantic, &boundary.machine.digest)) {
236 return reject(binding.node, error.SemanticBoundaryMismatch);
237 }
238 }
239 const initial_moment = moment.prepare(cut.root, fabric_cut.root) catch |failure|
240 return reject(null, failure);
241 return construct(
242 cut,
243 fabric_owner,
244 initial_moment,
245 owned[0..bindings.len],
246 );
247 }
248
249 fn construct(
250 cut: types.Cut,
251 fabric_owner: *fabric.Fabric,
252 initial_moment: moment.Moment,
253 bindings: []const Binding,
254 ) Result {
255 std.debug.assert(bindings.len == cut.root.node_count);
256 var restored: Restored = .{
257 .root = cut.root,
258 .fabric = fabric_owner,
259 .moment = initial_moment,
260 .node_count = cut.root.node_count,
261 .nodes = @splat(emptyNode()),
262 };
263 for (bindings, 0..) |binding, index| {
264 restored.nodes[index].id = binding.node;
265 const target = switch (binding.mode) {
266 .retained => continue,
267 .live => |value| value,
268 };
269 const attempt = instance.Instance.restore(
270 target.storage,
271 target.ram,
272 .{
273 .checkpoint = binding.checkpoint,
274 .expected_root = cut.nodes[index].machine,
275 .profile = target.profile,
276 .execution_manifest = target.execution_manifest,
277 .fence = target.fence,
278 },
279 );
280 switch (attempt) {
281 .ready => |machine| restored.nodes[index].machine = machine,
282 .unavailable => |backend| {
283 restored.deinit();
284 return .{ .unavailable = .{
285 .node = binding.node,
286 .backend = backend,
287 } };
288 },
289 .rejected => |failure| {
290 restored.deinit();
291 return reject(binding.node, failure);
292 },
293 }
294 }
295 return .{ .ready = restored };
296 }
297
298 fn validateTarget(
299 root: types.Root,
300 machine: checkpoint.Root,
301 target: Target,
302 ) Error!void {
303 try profile.validate(target.profile);
304 const contract = try profile.contractFingerprint(target.profile);
305 if (!std.meta.eql(contract, root.machine_contract)) {
306 return error.ContractMismatch;
307 }
308 const fingerprint = try profile.profileFingerprint(target.profile);
309 if (!std.meta.eql(fingerprint, machine.profile)) {
310 return error.ProfileMismatch;
311 }
312 try os.abi.wire.validateFence(target.fence);
313 }
314
315 fn validateAliases(
316 fabric_owner: *const fabric.Fabric,
317 bindings: []const Binding,
318 ) Error!void {
319 const fabric_bytes = std.mem.asBytes(fabric_owner);
320 for (bindings, 0..) |binding, index| {
321 const target = switch (binding.mode) {
322 .retained => continue,
323 .live => |value| value,
324 };
325 const storage = &target.storage.bytes;
326 if (buffersOverlap(storage, target.ram) or
327 buffersOverlap(storage, fabric_bytes) or
328 buffersOverlap(target.ram, fabric_bytes))
329 {
330 return error.RestoreAlias;
331 }
332 for (bindings, 0..) |other, other_index| {
333 if (other.checkpoint.aliases(storage) or
334 other.checkpoint.aliases(target.ram))
335 {
336 return error.RestoreAlias;
337 }
338 const other_target = switch (other.mode) {
339 .retained => continue,
340 .live => |value| value,
341 };
342 if (buffersOverlap(storage, other_target.execution_manifest) or
343 buffersOverlap(target.ram, other_target.execution_manifest))
344 {
345 return error.RestoreAlias;
346 }
347 if (other_index <= index) continue;
348 if (buffersOverlap(storage, &other_target.storage.bytes) or
349 buffersOverlap(storage, other_target.ram) or
350 buffersOverlap(target.ram, &other_target.storage.bytes) or
351 buffersOverlap(target.ram, other_target.ram))
352 {
353 return error.RestoreAlias;
354 }
355 }
356 }
357 }
358
359 fn reject(node: ?fabric.NodeId, failure: Error) Result {
360 return .{ .rejected = .{ .node = node, .failure = failure } };
361 }
362
363 fn emptyNode() Node {
364 return .{
365 .id = .{ .bytes = @splat(0) },
366 .machine = null,
367 };
368 }
369
370 fn buffersOverlap(left: []const u8, right: []const u8) bool {
371 if (left.len == 0 or right.len == 0) return false;
372 const left_start = @intFromPtr(left.ptr);
373 const right_start = @intFromPtr(right.ptr);
374 const left_end = std.math.add(usize, left_start, left.len) catch return true;
375 const right_end = std.math.add(usize, right_start, right.len) catch return true;
376 return left_start < right_end and right_start < left_end;
377 }