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 }