diff --git a/nixos/lib/test-driver/test_driver/efi.py b/nixos/lib/test-driver/test_driver/efi.py new file mode 100644 index 00000000000..05d5d85e395 --- /dev/null +++ b/nixos/lib/test-driver/test_driver/efi.py @@ -0,0 +1,31 @@ +from enum import IntEnum + +class EfiVariableAttributes(IntEnum): + NonVolatile = 0x01 + BootServiceAccess = 0x02 + RuntimeAccess = 0x04 + HardwareErrorRecord = 0x08 + AuthenticatedWriteAccess = 0x10 + TimeBasedAuthenticatedWriteAccess = 0x20 + AppendWrite = 0x40 + EnhancedAuthenticatedAccess = 0x80 + +class EfiVariable: + """ + An EFI variable represented by its attributes and raw value in bytes. + Generally, the value is not encoded in UTF-8, but UCS-2 or UTF-16-LE. + """ + attributes: EfiVariableAttributes + value: bytes + + def __init__(self, value: bytes, attributes: bytes): + self.value = value + self.attributes = EfiVariableAttributes(attributes) + + def value_as_null_terminated_string(self, encoding: str = 'utf-16-le'): + """ + Most often, variables are encoded with a null-terminated \x00. + This function gives you the string in a default encoding of UTF-16-LE + stripped of the null terminator. + """ + return self.value.decode(encoding).rstrip('\x00') diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py index 1a97cedb2e8..20cfde8af4b 100644 --- a/nixos/lib/test-driver/test_driver/machine.py +++ b/nixos/lib/test-driver/test_driver/machine.py @@ -17,6 +17,7 @@ import tempfile import threading import time +from test_driver.efi import EfiVariable from test_driver.logger import rootlog CHAR_TO_KEY = { @@ -1019,6 +1020,33 @@ class Machine: """ self.send_monitor_command(f"hostfwd_add tcp::{host_port}-:{guest_port}") + def running_under_uefi(self) -> bool: + """ + Returns True if the current environment is running under UEFI, False otherwise. + This is achieved by inspecting by the existence of /sys/firmware/efi. + """ + rc, _ = self.execute("test -d /sys/firmware/efi") + return (rc == 0) + + + def read_efi_variable_from_sysfs(self, guid: str, variable_name: str) -> EfiVariable | None: + """ + Read an EFI variable located in efivars sysfs if available. + Returns None if the EFI variable does not exist. + Raises an assertion error if we are not running under an UEFI environment. + """ + assert self.running_under_uefi(), "This machine is not detected under an UEFI environment" + rc, raw_attributes_and_value = self.execute(f"base64 /sys/firmware/efi/efivars/{variable_name}-{guid}") + if rc != 0: return None + # The return value is a string which is in reality a disguised bytes, we re-encode it properly + # using raw_unicode_escape keeping all the escapes properly. + # This is not guaranteed to work for all the cases but is good enough for UTF-8/UTF-16 usecases. + attributes_and_value = base64.b64decode(raw_attributes_and_value) + # First 4 bytes are attributes: https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html + attributes = attributes_and_value[:4] + value = attributes_and_value[4:] + return EfiVariable(value=value, attributes=attributes) + def block(self) -> None: """Make the machine unreachable by shutting down eth1 (the multicast interface used to talk to the other VMs). We keep eth0 up so that