danos/test/system/services/pci-cap-test/pci-cap-test.zig

179 lines
10 KiB
Zig

//! pci-cap-test — QEMU fixture for the driver-side PCI library (library/device/pci).
//! The pci-caps test case boots with an extra `-device e1000e` NIC that no danos driver
//! claims; this fixture claims it and exercises the whole claimed-function surface
//! against real (emulated) hardware: header accessors, command bits, the capability
//! walk, MSI programming (the first driver-side `msi_bind` use), the MSI-X table,
//! extended capabilities, power state, and — where offered — function-level reset.
//! Every check prints `pci-cap-test: <check> ok` or `pci-cap-test: FAIL <check>`; the
//! harness asserts on the final `all checks passed` marker (test/qemu_test.py).
const std = @import("std");
const device = @import("driver");
const ipc = @import("ipc");
const time = @import("time");
const logging = @import("logging");
const mmio = @import("mmio");
const pci = @import("pci");
const pci_class = @import("pci-class");
/// QEMU's e1000e: Intel 82574L.
const intel_vendor: u16 = 0x8086;
const e1000e_device: u16 = 0x10D3;
const ethernet_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.network),
.subclass = @intFromEnum(pci_class.network.SubClass.ethernet),
.prog_if = 0,
});
/// `pci.Function` keeps a pointer to the descriptor, so it must outlive the stack frame
/// that found it.
var descriptor: device.DeviceDescriptor = undefined;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = logging.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
/// Print the check's verdict; the caller returns on false to stop at the first failure.
fn check(comptime name: []const u8, ok: bool) bool {
if (ok) {
_ = logging.write("pci-cap-test: " ++ name ++ " ok\n");
} else {
_ = logging.write("pci-cap-test: FAIL " ++ name ++ "\n");
}
return ok;
}
pub fn main() void {
// The bus scan runs in another process; poll until the NIC shows up.
const nic_id: u64 = found: {
var tries: u32 = 0;
while (tries < 150) : (tries += 1) {
var descriptors: [64]device.DeviceDescriptor = undefined;
const total = device.enumerate(&descriptors);
for (descriptors[0..@min(total, descriptors.len)]) |*entry| {
if (entry.class == @intFromEnum(device.DeviceClass.pci_device) and entry.pci_class == ethernet_class) {
descriptor = entry.*;
break :found entry.id;
}
}
time.sleepMillis(100);
}
_ = logging.write("pci-cap-test: FAIL no ethernet function found\n");
return;
};
writeLine("pci-cap-test: claiming ethernet function (device {d})\n", .{nic_id});
if (!check("claim", device.claim(nic_id))) return;
var function = pci.Function.map(nic_id, &descriptor) orelse {
_ = logging.write("pci-cap-test: FAIL config-space map\n");
return;
};
// Identity: the header accessors against known e1000e values.
if (!check("vendor/device id", function.vendorId() == intel_vendor and function.deviceId() == e1000e_device)) return;
if (!check("class code", function.classCode().pack() == ethernet_class)) return;
if (!check("subsystem ids readable", function.subsystemVendorId() != 0xFFFF and function.subsystemId() != 0xFFFF)) return;
// Command bits: enable, read back, quiesce, read back, re-enable.
function.enableMemoryAndBusMaster();
if (!check("memory+bus-master enable", function.command() & pci_class.command_memory_and_bus_master == pci_class.command_memory_and_bus_master)) return;
function.disableBusMaster();
if (!check("bus-master disable", function.command() & pci_class.command_bus_master == 0)) return;
function.enableMemoryAndBusMaster();
// The capability walk: e1000e advertises PM, MSI, PCIe, and MSI-X.
var seen_power = false;
var seen_msi = false;
var seen_pci_express = false;
var seen_msix = false;
var walk = function.capabilities();
while (walk.next()) |capability| {
switch (capability.id) {
@intFromEnum(pci_class.CapabilityId.power_management) => seen_power = true,
@intFromEnum(pci_class.CapabilityId.msi) => seen_msi = true,
@intFromEnum(pci_class.CapabilityId.pci_express) => seen_pci_express = true,
@intFromEnum(pci_class.CapabilityId.msix) => seen_msix = true,
else => {},
}
}
if (!check("capability walk", seen_power and seen_msi and seen_pci_express and seen_msix)) return;
if (!check("findCapability", function.findCapability(.msi) != null and function.findCapability(.pci_express) != null)) return;
// MSI: bind a vector (the syscall's first driver-side use), program the capability,
// and read the registers straight back.
const endpoint = ipc.createIpcEndpoint() orelse {
_ = logging.write("pci-cap-test: FAIL endpoint creation\n");
return;
};
const message = device.msiBind(nic_id, endpoint) orelse {
_ = logging.write("pci-cap-test: FAIL msi_bind\n");
return;
};
if (!check("msi_bind address", message.address == 0xFEE0_0000)) return;
if (!check("programMsi", function.programMsi(message))) return;
const msi_cap = function.findCapability(.msi).?;
const msi_control = mmio.readRegister(u16, msi_cap.offset + pci_class.msi.control);
const msi_data_offset: usize = if (msi_control & pci_class.msi.control_64bit_capable != 0) pci_class.msi.data_64 else pci_class.msi.data_32;
if (!check("msi registers read back", msi_control & pci_class.msi.control_enable != 0 and
msi_control & pci_class.msi.control_multiple_message_enable_mask == 0 and
mmio.readRegister(u32, msi_cap.offset + pci_class.msi.address) == @as(u32, @truncate(message.address)) and
mmio.readRegister(u16, msi_cap.offset + msi_data_offset) == @as(u16, @truncate(message.data)))) return;
if (!check("intx disabled with msi", function.command() & pci_class.command_interrupt_disable != 0)) return;
function.disableMsi();
if (!check("disableMsi", mmio.readRegister(u16, msi_cap.offset + pci_class.msi.control) & pci_class.msi.control_enable == 0)) return;
// MSI-X: map the table, program entry 0, exercise the masks. Never enable — this
// proves the programming surface, not delivery.
const msix_table = function.msix() orelse {
_ = logging.write("pci-cap-test: FAIL msix table map\n");
return;
};
writeLine("pci-cap-test: msix table has {d} entries\n", .{msix_table.entry_count});
if (!check("msix entry count", msix_table.entry_count >= 1)) return;
if (!check("msix programEntry", msix_table.programEntry(0, message))) return;
if (!check("msix entry reads back", mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_address) == @as(u32, @truncate(message.address)) and
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_data) == message.data and
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked != 0)) return;
if (!check("msix unmask entry", msix_table.unmaskEntry(0) and
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked == 0)) return;
if (!check("msix re-mask entry", msix_table.maskEntry(0) and
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked != 0)) return;
msix_table.setFunctionMask();
if (!check("msix function mask", mmio.readRegister(u16, msix_table.capability + pci_class.msix.control) & pci_class.msix.control_function_mask != 0)) return;
msix_table.clearFunctionMask();
if (!check("msix function unmask", mmio.readRegister(u16, msix_table.capability + pci_class.msix.control) & pci_class.msix.control_function_mask == 0)) return;
if (!check("msix out-of-range rejected", !msix_table.programEntry(msix_table.entry_count, message))) return;
// Extended capabilities: the walk must terminate cleanly; the count is informative
// (don't hard-bind to QEMU's exact extended-capability set).
var extended_count: u32 = 0;
var extended = function.extendedCapabilities();
while (extended.next()) |_| extended_count += 1;
writeLine("pci-cap-test: {d} extended capabilities\n", .{extended_count});
if (!check("extended walk terminates", extended_count < 480)) return;
// Power: QEMU leaves the function in D0; ensurePowerStateD0 must agree and not
// disturb the PMCSR.
const power_cap = function.findCapability(.power_management).?;
const pmcsr_before = mmio.readRegister(u16, power_cap.offset + pci_class.power_management.control_status);
if (!check("power state is D0", pmcsr_before & pci_class.power_management.control_status_power_state_mask == pci_class.power_management.power_state_d0)) return;
function.ensurePowerStateD0();
if (!check("ensurePowerStateD0 is a no-op at D0", mmio.readRegister(u16, power_cap.offset + pci_class.power_management.control_status) == pmcsr_before)) return;
// Function-level reset, where the device offers it: afterwards the function must be
// readable with its identity intact, and bring-up must work again.
const express_cap = function.findCapability(.pci_express).?;
const device_capabilities = mmio.readRegister(u32, express_cap.offset + pci_class.pci_express.device_capabilities);
if (device_capabilities & pci_class.pci_express.device_capabilities_flr != 0) {
if (!check("functionLevelReset", function.functionLevelReset())) return;
if (!check("identity after flr", function.vendorId() == intel_vendor and function.deviceId() == e1000e_device)) return;
function.enableMemoryAndBusMaster();
if (!check("re-enable after flr", function.command() & pci_class.command_memory_and_bus_master == pci_class.command_memory_and_bus_master)) return;
} else {
_ = logging.write("pci-cap-test: flr not offered, skipped\n");
}
_ = logging.write("pci-cap-test: all checks passed\n");
}