lib/sys/src/kvm/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("../root.zig");
3 const kvm = @import("root.zig");
4 const system_backend = @import("system.zig");
5 const types = @import("types.zig");
6
7 const abi = kvm.abi;
8 const Availability = kvm.Availability;
9 const CapabilityStage = kvm.CapabilityStage;
10 const Descriptor = types.Descriptor;
11 const Errno = types.Errno;
12 const MapResult = types.MapResult;
13 const RawResult = types.RawResult;
14 const maximum_interrupt_retries = types.maximum_interrupt_retries;
15 const page_size = types.page_size;
16
17 test "KVM namespace exposes a no-libc virtualization boundary" {
18 std.testing.refAllDecls(kvm);
19 try std.testing.expectEqual(
20 sys.capabilities.CapabilityTier.no_libc,
21 kvm.required_capabilities.tier,
22 );
23 try std.testing.expect(kvm.required_capabilities.requires(.descriptors));
24 try std.testing.expect(kvm.required_capabilities.requires(.memory_mapping));
25 try std.testing.expect(kvm.required_capabilities.requires(.virtualization));
26 try std.testing.expectEqual(@as(usize, 1), kvm.System.descriptor_capacity);
27 try std.testing.expectEqual(@as(usize, 1), kvm.Vm.descriptor_capacity);
28 try std.testing.expectEqual(@as(usize, 1), kvm.Vcpu.descriptor_capacity);
29 try std.testing.expectEqual(@as(usize, 1), kvm.Vcpu.mapping_capacity);
30 }
31
32 test "KVM host probe never substitutes an unavailable backend" {
33 const receipt = kvm.probe();
34 if (!kvm.supported) {
35 try std.testing.expectEqual(kvm.Availability.unsupported, receipt.availability);
36 try std.testing.expectEqual(kvm.CapabilityStage.platform, receipt.stage);
37 return;
38 }
39 if (receipt.availability == .available) {
40 try std.testing.expectEqual(kvm.CapabilityStage.complete, receipt.stage);
41 try std.testing.expectEqual(kvm.abi.api_version, receipt.api_version);
42 try std.testing.expect(receipt.extensions.user_memory != 0);
43 } else {
44 try std.testing.expect(receipt.stage != .complete);
45 }
46 }
47
48 const TestAction = enum {
49 open,
50 control,
51 map,
52 unmap,
53 close,
54 };
55
56 const TestRecord = struct {
57 action: TestAction,
58 descriptor: Descriptor = -1,
59 request: u32 = 0,
60 argument: usize = 0,
61 };
62
63 const FakeOperations = struct {
64 platform_available: bool = true,
65 open_failure: ?Errno = null,
66 control_failure_request: ?u32 = null,
67 control_failure: Errno = .IO,
68 map_failure: ?Errno = null,
69 api: u32 = abi.api_version,
70 run_bytes: u32 = 4096,
71 user_memory: u32 = 1,
72 records: [192]TestRecord = undefined,
73 record_count: usize = 0,
74 registered_region: ?abi.UserMemoryRegion = null,
75 run_storage: [4096]u8 align(page_size) = @splat(0),
76
77 pub fn platformSupported(self: *@This()) bool {
78 return self.platform_available;
79 }
80
81 pub fn openDevice(self: *@This()) RawResult {
82 self.record(.{ .action = .open });
83 if (self.open_failure) |code| return .{ .failure = code };
84 return .{ .value = 10 };
85 }
86
87 pub fn control(
88 self: *@This(),
89 descriptor: Descriptor,
90 request: u32,
91 argument: usize,
92 ) RawResult {
93 self.record(.{
94 .action = .control,
95 .descriptor = descriptor,
96 .request = request,
97 .argument = argument,
98 });
99 if (self.control_failure_request == request) {
100 return .{ .failure = self.control_failure };
101 }
102 return self.controlResult(request, argument);
103 }
104
105 pub fn mapRun(self: *@This(), descriptor: Descriptor, len: usize) MapResult {
106 self.record(.{ .action = .map, .descriptor = descriptor, .argument = len });
107 if (self.map_failure) |code| return .{ .failure = code };
108 if (len > self.run_storage.len) return .{ .failure = .NOMEM };
109 return .{ .mapping = self.run_storage[0..len] };
110 }
111
112 pub fn unmap(self: *@This(), mapping: []align(page_size) u8) void {
113 std.debug.assert(mapping.ptr == self.run_storage[0..].ptr);
114 self.record(.{ .action = .unmap, .argument = mapping.len });
115 }
116
117 pub fn close(self: *@This(), descriptor: Descriptor) void {
118 self.record(.{ .action = .close, .descriptor = descriptor });
119 }
120
121 fn controlResult(self: *@This(), request: u32, argument: usize) RawResult {
122 if (request == abi.request.get_api_version) return .{ .value = self.api };
123 if (request == abi.request.get_vcpu_mmap_size) return .{ .value = self.run_bytes };
124 if (request == abi.request.check_extension) {
125 return .{ .value = self.extensionValue(argument) };
126 }
127 if (request == abi.request.create_vm) return .{ .value = 20 };
128 if (request == abi.request.create_vcpu) return .{ .value = 30 };
129 if (request == abi.request.set_user_memory_region) {
130 const region: *const abi.UserMemoryRegion = @ptrFromInt(argument);
131 self.registered_region = region.*;
132 return .{ .value = 0 };
133 }
134 if (request == abi.request.run) {
135 const header: *abi.RunHeader = @ptrCast(&self.run_storage);
136 header.exit_reason = @backingInt(kvm.ExitReason.halted);
137 return .{ .value = 0 };
138 }
139 return self.stateResult(request, argument);
140 }
141
142 fn extensionValue(self: *@This(), raw_extension: usize) usize {
143 const extension_value: u32 = @intCast(raw_extension);
144 const extension: abi.Extension = @fromBackingInt(extension_value);
145 return switch (extension) {
146 .user_memory => self.user_memory,
147 .recommended_vcpus => 2,
148 .memory_slots => 32,
149 .maximum_vcpus => 4,
150 .maximum_vcpu_id => 16,
151 .xsave_2 => @sizeOf(abi.Xsave),
152 else => 1,
153 };
154 }
155
156 fn stateResult(self: *@This(), request: u32, argument: usize) RawResult {
157 if (request == abi.request.get_registers) {
158 clear(abi.Registers, argument).rip = 0x1234;
159 } else if (request == abi.request.get_special_registers) {
160 _ = clear(abi.SpecialRegisters, argument);
161 } else if (request == abi.request.get_fpu) {
162 _ = clear(abi.Fpu, argument);
163 } else if (request == abi.request.get_vcpu_events) {
164 _ = clear(abi.VcpuEvents, argument);
165 } else if (request == abi.request.get_debug_registers) {
166 _ = clear(abi.DebugRegisters, argument);
167 } else if (request == abi.request.get_xsave) {
168 _ = clear(abi.Xsave, argument);
169 } else if (request == abi.request.get_xcrs) {
170 _ = clear(abi.Xcrs, argument);
171 } else if (request == abi.request.get_mp_state) {
172 _ = clear(abi.MpState, argument);
173 } else if (request == abi.request.get_clock) {
174 clear(abi.Clock, argument).clock = 99;
175 } else if (request == abi.request.get_msrs) {
176 return getMsrs(argument);
177 } else if (request == abi.request.set_msrs) {
178 return msrCount(argument);
179 } else if (request == abi.request.get_supported_cpuid) {
180 return self.supportedCpuid(argument);
181 } else if (isStateWrite(request) or isCpuid(request)) {
182 return .{ .value = 0 };
183 } else {
184 return .{ .failure = .NOTTY };
185 }
186 return .{ .value = 0 };
187 }
188
189 fn supportedCpuid(_: *const @This(), address: usize) RawResult {
190 const header: *abi.CpuidHeader = @ptrFromInt(address);
191 if (header.count == 0) return .{ .failure = .@"2BIG" };
192 const entries: [*]abi.CpuidEntry = @ptrFromInt(address + @sizeOf(abi.CpuidHeader));
193 header.count = 1;
194 entries[0] = std.mem.zeroes(abi.CpuidEntry);
195 entries[0].function = 0x8000_0001;
196 entries[0].edx = @as(u32, 1) << 20;
197 return .{ .value = 0 };
198 }
199
200 fn record(self: *@This(), value: TestRecord) void {
201 std.debug.assert(self.record_count < self.records.len);
202 self.records[self.record_count] = value;
203 self.record_count += 1;
204 }
205
206 fn actionCount(self: *const @This(), action: TestAction) usize {
207 var count: usize = 0;
208 for (self.records[0..self.record_count]) |entry| {
209 if (entry.action == action) count += 1;
210 }
211 return count;
212 }
213
214 fn descriptorCloseCount(self: *const @This(), descriptor: Descriptor) usize {
215 var count: usize = 0;
216 for (self.records[0..self.record_count]) |entry| {
217 if (entry.action == .close and entry.descriptor == descriptor) count += 1;
218 }
219 return count;
220 }
221
222 fn clear(comptime T: type, address: usize) *T {
223 const destination: *T = @ptrFromInt(address);
224 destination.* = std.mem.zeroes(T);
225 return destination;
226 }
227
228 fn getMsrs(address: usize) RawResult {
229 const header: *abi.MsrHeader = @ptrFromInt(address);
230 std.debug.assert(header.count <= 16);
231 const entries: [*]abi.MsrEntry = @ptrFromInt(address + @sizeOf(abi.MsrHeader));
232 for (entries[0..header.count], 0..) |*entry, index| {
233 entry.data = 0x1000 + index;
234 }
235 return .{ .value = header.count };
236 }
237
238 fn msrCount(address: usize) RawResult {
239 const header: *const abi.MsrHeader = @ptrFromInt(address);
240 return .{ .value = header.count };
241 }
242
243 fn isStateWrite(request: u32) bool {
244 return request == abi.request.set_registers or
245 request == abi.request.set_special_registers or
246 request == abi.request.set_fpu or
247 request == abi.request.set_vcpu_events or
248 request == abi.request.set_debug_registers or
249 request == abi.request.set_xsave or
250 request == abi.request.set_xcrs or
251 request == abi.request.set_mp_state or
252 request == abi.request.set_clock;
253 }
254
255 fn isCpuid(request: u32) bool {
256 return request == abi.request.get_supported_cpuid or
257 request == abi.request.get_cpuid or
258 request == abi.request.set_cpuid;
259 }
260 };
261
262 fn readySystem(operations: *FakeOperations) !system_backend.System {
263 return switch (system_backend.acquire(types.operationsFor(operations))) {
264 .ready => |system| system,
265 .rejected => error.UnexpectedRejection,
266 };
267 }
268
269 fn expectDeviceRejection(code: Errno, availability: Availability) !void {
270 var operations: FakeOperations = .{ .open_failure = code };
271 const attempt = system_backend.acquire(types.operationsFor(&operations));
272 switch (attempt) {
273 .ready => return error.UnexpectedAvailability,
274 .rejected => |receipt| {
275 try std.testing.expectEqual(availability, receipt.availability);
276 try std.testing.expectEqual(CapabilityStage.device, receipt.stage);
277 try std.testing.expectEqual(
278 @as(u16, @intCast(@backingInt(code))),
279 receipt.errno_code,
280 );
281 },
282 }
283 try std.testing.expectEqual(@as(usize, 0), operations.actionCount(.close));
284 const expected_opens: usize = if (code == .INTR) maximum_interrupt_retries else 1;
285 try std.testing.expectEqual(expected_opens, operations.actionCount(.open));
286 }
287
288 test "KVM capability receipt classifies device rejection" {
289 try expectDeviceRejection(.NOENT, .unavailable);
290 try expectDeviceRejection(.NODEV, .unavailable);
291 try expectDeviceRejection(.ACCES, .denied);
292 try expectDeviceRejection(.NOSYS, .unsupported);
293 try expectDeviceRejection(.IO, .failed);
294 try expectDeviceRejection(.INTR, .failed);
295
296 var operations: FakeOperations = .{ .platform_available = false };
297 const attempt = system_backend.acquire(types.operationsFor(&operations));
298 try std.testing.expectEqual(
299 Availability.unsupported,
300 attempt.rejected.availability,
301 );
302 try std.testing.expectEqual(CapabilityStage.platform, attempt.rejected.stage);
303 try std.testing.expectEqual(@as(usize, 0), operations.actionCount(.open));
304 }
305
306 test "KVM capability receipt closes rejected system descriptor" {
307 var incompatible: FakeOperations = .{ .api = abi.api_version + 1 };
308 const version_attempt = system_backend.acquire(types.operationsFor(&incompatible));
309 try std.testing.expectEqual(
310 Availability.unsupported,
311 version_attempt.rejected.availability,
312 );
313 try std.testing.expectEqual(CapabilityStage.api_version, version_attempt.rejected.stage);
314 try std.testing.expectEqual(@as(usize, 1), incompatible.descriptorCloseCount(10));
315
316 var missing_memory: FakeOperations = .{ .user_memory = 0 };
317 const memory_attempt = system_backend.acquire(types.operationsFor(&missing_memory));
318 try std.testing.expectEqual(
319 Availability.unsupported,
320 memory_attempt.rejected.availability,
321 );
322 try std.testing.expectEqual(CapabilityStage.extensions, memory_attempt.rejected.stage);
323 try std.testing.expectEqual(@as(usize, 1), missing_memory.descriptorCloseCount(10));
324
325 var invalid_mapping: FakeOperations = .{ .run_bytes = 3000 };
326 const mapping_attempt = system_backend.acquire(types.operationsFor(&invalid_mapping));
327 try std.testing.expectEqual(
328 Availability.unsupported,
329 mapping_attempt.rejected.availability,
330 );
331 try std.testing.expectEqual(CapabilityStage.run_mapping, mapping_attempt.rejected.stage);
332 try std.testing.expectEqual(@as(usize, 1), invalid_mapping.descriptorCloseCount(10));
333
334 var denied_extension: FakeOperations = .{
335 .control_failure_request = abi.request.check_extension,
336 .control_failure = .ACCES,
337 };
338 const extension_attempt = system_backend.acquire(types.operationsFor(&denied_extension));
339 try std.testing.expectEqual(Availability.denied, extension_attempt.rejected.availability);
340 try std.testing.expectEqual(CapabilityStage.extensions, extension_attempt.rejected.stage);
341 try std.testing.expectEqual(@as(usize, 1), denied_extension.descriptorCloseCount(10));
342 }
343
344 test "KVM system acquisition returns measured capabilities and closes once" {
345 var operations: FakeOperations = .{};
346 var system = try readySystem(&operations);
347 try std.testing.expectEqual(Availability.available, system.receipt.availability);
348 try std.testing.expectEqual(CapabilityStage.complete, system.receipt.stage);
349 try std.testing.expectEqual(abi.api_version, system.receipt.api_version);
350 try std.testing.expectEqual(@as(u32, 4096), system.receipt.run_mapping_bytes);
351 try std.testing.expectEqual(@as(u32, 32), system.receipt.extensions.memory_slots);
352 try std.testing.expectEqual(@as(u32, 4), system.receipt.extensions.maximum_vcpus);
353 try std.testing.expectEqual(@as(u32, 32), system.memorySlotCapacity());
354 try std.testing.expect(system.supportsReadOnlyMemory());
355
356 system.deinit();
357 system.deinit();
358 try std.testing.expectEqual(@as(usize, 1), operations.descriptorCloseCount(10));
359 }
360
361 test "KVM owners route memory run and state through typed controls" {
362 var operations: FakeOperations = .{};
363 var system = try readySystem(&operations);
364 defer system.deinit();
365 var vm = try system.createVm(0);
366 defer vm.deinit();
367
368 var guest: [page_size]u8 align(page_size) = @splat(0);
369 try vm.registerMemory(.{
370 .slot = 3,
371 .guest_physical_address = 0x4000,
372 .memory = guest[0..],
373 .flags = .{ .read_only = true },
374 });
375 try std.testing.expectEqual(@as(u32, 3), operations.registered_region.?.slot);
376 const registered = operations.registered_region.?;
377 try std.testing.expectEqual(@as(u64, 0x4000), registered.guest_physical_address);
378 try std.testing.expectEqual(@as(u64, page_size), registered.memory_size);
379 try std.testing.expectEqual(@intFromPtr(&guest), registered.userspace_address);
380 try vm.unregisterMemory(3);
381 try std.testing.expectEqual(@as(u64, 0), operations.registered_region.?.memory_size);
382
383 var vcpu = try vm.createVcpu(0);
384 defer vcpu.deinit();
385 try vcpu.setRunControls(.{
386 .request_interrupt_window = true,
387 .cr8 = 3,
388 .apic_base = 0xfee00000,
389 });
390 try vcpu.setImmediateExit(false);
391 const before_run = try vcpu.runStatus();
392 try std.testing.expectEqual(@as(u64, 3), before_run.cr8);
393 try std.testing.expectEqual(@as(u64, 0xfee00000), before_run.apic_base);
394 const run_exit = try vcpu.run();
395 try std.testing.expectEqual(.halted, std.meta.activeTag(run_exit));
396
397 var snapshot = try vcpu.capture(.all);
398 try std.testing.expectEqual(@as(u64, 0x1234), snapshot.registers.rip);
399 try vcpu.restore(&snapshot);
400
401 const Msrs = abi.MsrBuffer(2);
402 var msrs = try Msrs.init(&.{ 0x10, 0x1b });
403 try vcpu.readMsrs(&msrs);
404 try std.testing.expectEqual(@as(u64, 0x1000), msrs.entries[0].data);
405 try vcpu.writeMsrs(&msrs);
406
407 const Cpuids = abi.CpuidBuffer(4);
408 var cpuids = Cpuids.init();
409 try system.supportedCpuid(&cpuids);
410 try vcpu.readCpuid(&cpuids);
411 try vcpu.writeCpuid(&cpuids);
412
413 const clock = try vm.getClock();
414 try std.testing.expectEqual(@as(u64, 99), clock.clock);
415 try vm.setClock(&clock);
416
417 vcpu.deinit();
418 vm.deinit();
419 system.deinit();
420 const tail = operations.records[operations.record_count - 4 .. operations.record_count];
421 try std.testing.expectEqual(TestAction.unmap, tail[0].action);
422 try std.testing.expectEqual(TestAction.close, tail[1].action);
423 try std.testing.expectEqual(@as(Descriptor, 30), tail[1].descriptor);
424 try std.testing.expectEqual(@as(Descriptor, 20), tail[2].descriptor);
425 try std.testing.expectEqual(@as(Descriptor, 10), tail[3].descriptor);
426 }
427
428 test "KVM vcpu map failure closes only the new descriptor" {
429 var operations: FakeOperations = .{ .map_failure = .NOMEM };
430 var system = try readySystem(&operations);
431 defer system.deinit();
432 var vm = try system.createVm(0);
433 defer vm.deinit();
434
435 try std.testing.expectError(error.ResourceLimit, vm.createVcpu(0));
436 try std.testing.expectEqual(@as(usize, 1), operations.descriptorCloseCount(30));
437 try std.testing.expectEqual(@as(usize, 0), operations.actionCount(.unmap));
438 try std.testing.expectEqual(@as(usize, 0), operations.descriptorCloseCount(20));
439 try std.testing.expectEqual(@as(usize, 0), operations.descriptorCloseCount(10));
440 }
441
442 test "KVM owners reject capacity violations before kernel control" {
443 var operations: FakeOperations = .{};
444 var system = try readySystem(&operations);
445 defer system.deinit();
446 var vm = try system.createVm(0);
447 defer vm.deinit();
448 const controls_before = operations.actionCount(.control);
449
450 var guest: [page_size]u8 align(page_size) = @splat(0);
451 try std.testing.expectError(error.InvalidSlot, vm.registerMemory(.{
452 .slot = vm.extensions.memory_slots,
453 .guest_physical_address = 0,
454 .memory = guest[0..],
455 }));
456 vm.extensions.read_only_memory = 0;
457 try std.testing.expectError(error.UnsupportedFlags, vm.registerMemory(.{
458 .slot = 0,
459 .guest_physical_address = 0,
460 .memory = guest[0..],
461 .flags = .{ .read_only = true },
462 }));
463 try std.testing.expectError(error.InvalidAlignment, vm.registerMemory(.{
464 .slot = 0,
465 .guest_physical_address = 1,
466 .memory = guest[0..],
467 }));
468 try std.testing.expectError(
469 error.UnsupportedOperation,
470 vm.createVcpu(vm.extensions.maximum_vcpu_id),
471 );
472 try std.testing.expectEqual(controls_before, operations.actionCount(.control));
473 }
474
475 test "KVM state buffers and run interruption fail before unsafe reuse" {
476 var operations: FakeOperations = .{};
477 var system = try readySystem(&operations);
478 defer system.deinit();
479 var vm = try system.createVm(0);
480 defer vm.deinit();
481 var vcpu = try vm.createVcpu(0);
482 defer vcpu.deinit();
483
484 const Msrs = abi.MsrBuffer(1);
485 var msrs = try Msrs.init(&.{0x10});
486 msrs.count = 2;
487 try std.testing.expectError(error.CapacityExceeded, vcpu.readMsrs(&msrs));
488 vcpu.extensions.xsave = 0;
489 try std.testing.expectError(error.UnsupportedState, vcpu.capture(.{ .xsave = true }));
490 vcpu.extensions.immediate_exit = 0;
491 try std.testing.expectError(error.UnsupportedOperation, vcpu.setImmediateExit(true));
492
493 operations.control_failure_request = abi.request.run;
494 operations.control_failure = .INTR;
495 try std.testing.expectError(error.Interrupted, vcpu.run());
496 }