blob: ac6e10ea9515e971058f00a49280c13edce6881c [file] [log] [blame]
# SPDX-License-Identifier: MIT
"""
This module contains the s0i3 prerequisite validator for amd-debug-tools.
"""
import configparser
import logging
import os
import platform
import re
import shutil
import subprocess
import tempfile
import struct
from datetime import datetime
from packaging import version
import pyudev
from amd_debug.wake import WakeIRQ
from amd_debug.display import Display
from amd_debug.kernel import get_kernel_log, SystemdLogger, CySystemdLogger, DmesgLogger
from amd_debug.common import (
apply_prefix_wrapper,
BIT,
clear_temporary_message,
find_ip_version,
get_distro,
get_pretty_distro,
get_property_pyudev,
is_root,
minimum_kernel,
print_color,
print_temporary_message,
read_file,
read_msr,
AmdTool,
)
from amd_debug.battery import Batteries
from amd_debug.database import SleepDatabase
from amd_debug.failures import (
AcpiNvmeStorageD3Enable,
AmdHsmpBug,
AmdgpuPpFeatureMask,
ASpmWrong,
DeepSleep,
DevSlpDiskIssue,
DevSlpHostIssue,
DMArNotEnabled,
DmcubTooOld,
DmiNotSetup,
FadtWrong,
I2CHidBug,
KernelRingBufferWrapped,
LimitedCores,
MissingAmdgpu,
MissingAmdgpuFirmware,
MissingAmdPmc,
MissingGpu,
MissingDriver,
MissingIommuACPI,
MissingIommuPolicy,
MissingThunderbolt,
MissingXhciHcd,
MSRFailure,
RogAllyMcuPowerSave,
RogAllyOldMcu,
SleepModeWrong,
SMTNotEnabled,
TaintedKernel,
UnservicedGpio,
UnsupportedModel,
WCN6855Bug,
)
# test if fwupd can report device firmware versions
try:
import gi
from gi.repository import GLib as _
gi.require_version("Fwupd", "2.0")
from gi.repository import Fwupd # pylint: disable=wrong-import-position
FWUPD = True
except ImportError:
FWUPD = False
except ValueError:
FWUPD = False
class Headers:
"""Headers for the script"""
NvmeSimpleSuspend = "platform quirk: setting simple suspend"
RootError = "Must be executed by root user"
BrokenPrerequisites = "Your system does not meet s2idle prerequisites!"
ExplanationReport = "Explanations for your system"
class PrerequisiteValidator(AmdTool):
"""Class to validate the prerequisites for s2idle"""
def __init__(self, tool_debug):
log_prefix = "s2idle" if tool_debug else None
super().__init__(log_prefix)
self.kernel_log = get_kernel_log()
if not is_root():
raise PermissionError("not root user")
self.cpu_family = None
self.cpu_model = None
self.cpu_model_string = None
self.pyudev = pyudev.Context()
self.failures = []
self.db = SleepDatabase()
self.db.start_cycle(datetime.now())
self.debug = tool_debug
self.distro = get_distro()
self.cmdline = read_file(os.path.join("/proc", "cmdline"))
self.irqs = []
self.smu_version = ""
self.smu_program = ""
self.display = Display()
def capture_once(self):
"""Capture the prerequisites once"""
if not self.db.get_last_prereq_ts():
self.run()
def capture_nvidia(self):
"""Capture the NVIDIA GPU state"""
p = os.path.join("/", "proc", "driver", "nvidia", "version")
if not os.path.exists(p):
return True
try:
self.db.record_debug_file(p)
except PermissionError:
self.db.record_prereq("NVIDIA GPU version not readable", "👀")
return True
p = os.path.join("/", "proc", "driver", "nvidia", "gpus")
if not os.path.exists(p):
return True
for root, _dirs, files in os.walk(p, topdown=False):
for f in files:
try:
self.db.record_debug(f"NVIDIA {f}")
self.db.record_debug_file(os.path.join(root, f))
except PermissionError:
self.db.record_prereq("NVIDIA GPU {f} not readable", "👀")
return True
def capture_edid(self):
"""Capture and decode the EDID data"""
edids = self.display.get_edid()
if len(edids) == 0:
self.db.record_debug("No EDID data found")
return True
for p in edids:
output = None
for tool in ["di-edid-decode", "edid-decode"]:
try:
cmd = [tool, p]
output = subprocess.check_output(
cmd, stderr=subprocess.DEVNULL
).decode("utf-8", errors="ignore")
break
except FileNotFoundError:
self.db.record_debug(f"{cmd} not installed")
except subprocess.CalledProcessError as _e:
pass
if not output:
self.db.record_prereq("Failed to capture EDID table", "👀")
else:
self.db.record_debug(apply_prefix_wrapper(f"EDID for {p}:", output))
return True
def check_amdgpu(self):
"""Check for the AMDGPU driver"""
count = 0
for device in self.pyudev.list_devices(subsystem="pci"):
klass = device.properties.get("PCI_CLASS")
if klass not in ["30000", "38000"]:
continue
pci_id = device.properties.get("PCI_ID")
if not pci_id.startswith("1002"):
continue
count += 1
if device.properties.get("DRIVER") != "amdgpu":
self.db.record_prereq("GPU driver `amdgpu` not loaded", "❌")
self.failures += [MissingAmdgpu()]
return False
slot = device.properties.get("PCI_SLOT_NAME")
self.db.record_prereq(f"GPU driver `amdgpu` bound to {slot}", "✅")
if count == 0:
self.db.record_prereq("Integrated GPU not found", "❌")
self.failures += [MissingGpu()]
return False
return True
def check_amdgpu_parameters(self):
"""Check for AMDGPU parameters"""
p = os.path.join("/", "sys", "module", "amdgpu", "parameters", "ppfeaturemask")
if os.path.exists(p):
v = read_file(p)
if v != "0xfff7bfff":
self.db.record_prereq(f"AMDGPU ppfeaturemask overridden to {v}", "❌")
self.failures += [AmdgpuPpFeatureMask()]
return False
if not self.kernel_log:
message = "Unable to test for amdgpu from kernel log"
self.db.record_prereq(message, "🚦")
return True
self.kernel_log.seek()
match = self.kernel_log.match_pattern("Direct firmware load for amdgpu.*failed")
if match and not "amdgpu/isp" in match:
self.db.record_prereq("GPU firmware missing", "❌")
self.failures += [MissingAmdgpuFirmware([match])]
return False
return True
def check_wcn6855_bug(self):
"""Check if WCN6855 WLAN is affected by a bug that causes spurious wakeups"""
if not self.kernel_log:
message = "Unable to test for wcn6855 bug from kernel log"
self.db.record_prereq(message, "🚦")
return True
wcn6855 = False
self.kernel_log.seek()
if self.kernel_log.match_pattern("ath11k_pci.*wcn6855"):
match = self.kernel_log.match_pattern("ath11k_pci.*fw_version")
if match:
self.db.record_debug(f"WCN6855 version string: {match}")
objects = match.split()
for i, obj in enumerate(objects):
if obj == "fw_build_id":
wcn6855 = objects[i + 1]
if wcn6855:
components = wcn6855.split(".")
if int(components[-1]) >= 37 or int(components[-1]) == 23:
self.db.record_prereq(
f"WCN6855 WLAN (fw build id {wcn6855})",
"✅",
)
else:
self.db.record_prereq(
f"WCN6855 WLAN may cause spurious wakeups (fw version {wcn6855})",
"❌",
)
self.failures += [WCN6855Bug()]
return True
def check_storage(self):
"""Check if storage devices are supported"""
has_sata = False
has_ahci = False
valid_nvme = {}
invalid_nvme = {}
valid_sata = False
valid_ahci = False
if not self.kernel_log:
message = "Unable to test storage from kernel log"
self.db.record_prereq(message, "🚦")
return True
for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="nvme"):
# https://git.kernel.org/torvalds/c/e79a10652bbd3
if minimum_kernel(6, 10):
break
pci_slot_name = dev.properties["PCI_SLOT_NAME"]
vendor = dev.properties.get("ID_VENDOR_FROM_DATABASE", "")
model = dev.properties.get("ID_MODEL_FROM_DATABASE", "")
message = f"{vendor} {model}"
self.kernel_log.seek()
pattern = f"{pci_slot_name}.*{Headers.NvmeSimpleSuspend}"
if self.kernel_log.match_pattern(pattern):
valid_nvme[pci_slot_name] = message
if pci_slot_name not in valid_nvme:
invalid_nvme[pci_slot_name] = message
for dev in self.pyudev.list_devices(subsystem="pci", DRIVER="ahci"):
has_ahci = True
break
for dev in self.pyudev.list_devices(subsystem="block", ID_BUS="ata"):
has_sata = True
break
# Test AHCI
if has_ahci:
self.kernel_log.seek()
pattern = "ahci.*flags.*sadm.*sds"
if self.kernel_log.match_pattern(pattern):
valid_ahci = True
# Test SATA
if has_sata:
self.kernel_log.seek()
pattern = "ata.*Features.*Dev-Sleep"
if self.kernel_log.match_pattern(pattern):
valid_sata = True
if invalid_nvme:
for disk, _name in invalid_nvme.items():
message = f"NVME {invalid_nvme[disk].strip()} is not configured for s2idle in BIOS"
self.db.record_prereq(message, "❌")
num = len(invalid_nvme) + len(valid_nvme)
self.failures += [AcpiNvmeStorageD3Enable(invalid_nvme[disk], num)]
if valid_nvme:
for disk, _name in valid_nvme.items():
message = (
f"NVME {valid_nvme[disk].strip()} is configured for s2idle in BIOS"
)
self.db.record_prereq(message, "✅")
if has_sata:
if valid_sata:
message = "SATA supports DevSlp feature"
self.db.record_prereq(message, "✅")
else:
message = "SATA does not support DevSlp feature"
self.db.record_prereq(message, "❌")
self.failures += [DevSlpDiskIssue()]
if has_ahci:
if valid_ahci:
message = "AHCI is configured for DevSlp in BIOS"
self.db.record_prereq(message, "✅")
else:
message = "AHCI is not configured for DevSlp in BIOS"
self.db.record_prereq(message, "🚦")
self.failures += [DevSlpHostIssue()]
return (
(len(invalid_nvme) == 0)
and (valid_sata or not has_sata)
and (valid_ahci or not has_sata)
)
def check_amd_hsmp(self):
"""Check for AMD HSMP driver"""
# not needed to check in newer kernels
# see https://github.com/torvalds/linux/commit/77f1972bdcf7513293e8bbe376b9fe837310ee9c
if minimum_kernel(6, 10):
return True
f = os.path.join("/", "boot", f"config-{platform.uname().release}")
if os.path.exists(f):
kconfig = read_file(f)
if "CONFIG_AMD_HSMP=y" in kconfig:
self.db.record_prereq(
"HSMP driver `amd_hsmp` driver may conflict with amd_pmc",
"❌",
)
self.failures += [AmdHsmpBug()]
return False
cmdline = read_file(os.path.join("/proc", "cmdline"))
blocked = "initcall_blacklist=hsmp_plt_init" in cmdline
p = os.path.join("/", "sys", "module", "amd_hsmp")
if os.path.exists(p) and not blocked:
self.db.record_prereq("`amd_hsmp` driver may conflict with amd_pmc", "❌")
self.failures += [AmdHsmpBug()]
return False
self.db.record_prereq(
f"HSMP driver `amd_hsmp` not detected (blocked: {blocked})",
"✅",
)
return True
def check_amd_pmc(self):
"""Check if the amd_pmc driver is loaded"""
for device in self.pyudev.list_devices(subsystem="platform", DRIVER="amd_pmc"):
message = "PMC driver `amd_pmc` loaded"
p = os.path.join(device.sys_path, "smu_program")
v = os.path.join(device.sys_path, "smu_fw_version")
if os.path.exists(v):
try:
self.smu_version = read_file(v)
self.smu_program = read_file(p)
except TimeoutError:
self.db.record_prereq(
"failed to communicate using `amd_pmc` driver", "❌"
)
return False
message += f" (Program {self.smu_program} Firmware {self.smu_version})"
self.db.record_prereq(message, "✅")
return True
self.failures += [MissingAmdPmc()]
self.db.record_prereq(
"PMC driver `amd_pmc` did not bind to any ACPI device", "❌"
)
return False
def check_wlan(self):
"""Checks for WLAN device"""
for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="28000"):
slot = device.properties["PCI_SLOT_NAME"]
driver = device.properties.get("DRIVER")
if not driver:
self.db.record_prereq(f"WLAN device in {slot} missing driver", "🚦")
self.failures += [MissingDriver(slot)]
return False
self.db.record_prereq(f"WLAN driver `{driver}` bound to {slot}", "✅")
return True
def check_usb3(self):
"""Check for the USB4 controller"""
slots = []
for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0330"):
slot = device.properties["PCI_SLOT_NAME"]
if device.properties.get("DRIVER") != "xhci_hcd":
self.db.record_prereq(
f"USB3 controller for {slot} not using `xhci_hcd` driver", "❌"
)
self.failures += [MissingXhciHcd()]
return False
slots += [slot]
if slots:
self.db.record_prereq(
f"USB3 driver `xhci_hcd` bound to {', '.join(slots)}", "✅"
)
return True
def check_dpia_pg_dmcub(self):
"""Check if DMUB is new enough to PG DPIA when no USB4 present"""
usb4_found = False
for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0340"):
usb4_found = True
break
if usb4_found:
self.db.record_debug("USB4 routers found, no need to check DMCUB version")
return True
# Check if matching DCN present
for device in self.pyudev.list_devices(subsystem="pci"):
current = None
klass = device.properties.get("PCI_CLASS")
if klass not in ["30000", "38000"]:
continue
pci_id = device.properties.get("PCI_ID")
if not pci_id.startswith("1002"):
continue
hw_ver = {"major": 3, "minor": 5, "revision": 0}
if not find_ip_version(device.sys_path, "DMU", hw_ver):
continue
# DCN was found, lookup version from sysfs
p = os.path.join(device.sys_path, "fw_version", "dmcub_fw_version")
if os.path.exists(p):
current = int(read_file(p), 16)
# no sysfs, try to look for version from debugfs
if not current:
slot = device.properties["PCI_SLOT_NAME"]
p = os.path.join(
"/", "sys", "kernel", "debug", "dri", slot, "amdgpu_firmware_info"
)
contents = read_file(p)
for line in contents.split("\n"):
if not line.startswith("DMCUB"):
continue
current = int(line.split()[-1], 16)
if current:
expected = 0x09001B00
if current >= expected:
return True
self.db.record_prereq("DMCUB Firmware is outdated", "❌")
self.failures += [DmcubTooOld(current, expected)]
return False
return True
def check_usb4(self):
"""Check if the thunderbolt driver is loaded"""
slots = []
for device in self.pyudev.list_devices(subsystem="pci", PCI_CLASS="C0340"):
slot = device.properties["PCI_SLOT_NAME"]
if device.properties.get("DRIVER") != "thunderbolt":
self.db.record_prereq("USB4 driver `thunderbolt` missing", "❌")
self.failures += [MissingThunderbolt()]
return False
slots += [slot]
if slots:
self.db.record_prereq(
f"USB4 driver `thunderbolt` bound to {', '.join(slots)}", "✅"
)
return True
def check_sleep_mode(self):
"""Check if the system is configured for s2idle"""
fn = os.path.join("/", "sys", "power", "mem_sleep")
if not os.path.exists(fn):
self.db.record_prereq("Kernel doesn't support sleep", "❌")
return False
cmdline = read_file(os.path.join("/proc", "cmdline"))
if "mem_sleep_default=deep" in cmdline:
self.db.record_prereq(
"Kernel command line is configured for 'deep' sleep", "❌"
)
self.failures += [DeepSleep()]
return False
if "[s2idle]" not in read_file(fn):
self.failures += [SleepModeWrong()]
self.db.record_prereq(
"System isn't configured for s2idle in firmware setup", "❌"
)
return False
self.db.record_prereq("System is configured for s2idle", "✅")
return True
def capture_smbios(self):
"""Capture the SMBIOS (DMI) information"""
p = os.path.join("/", "sys", "class", "dmi", "id")
if not os.path.exists(p):
self.db.record_prereq("DMI data was not setup", "🚦")
self.failures += [DmiNotSetup()]
return False
else:
keys = {}
filtered = [
"product_serial",
"board_serial",
"board_asset_tag",
"chassis_asset_tag",
"chassis_serial",
"modalias",
"uevent",
"product_uuid",
]
for root, _dirs, files in os.walk(p, topdown=False):
files.sort()
for fname in files:
if "power" in root:
continue
if fname in filtered:
continue
contents = read_file(os.path.join(root, fname))
keys[fname] = contents
if (
"sys_vendor" not in keys
or "product_name" not in keys
or "product_family" not in keys
):
self.db.record_prereq("DMI data not found", "❌")
self.failures += [DmiNotSetup()]
return False
self.db.record_prereq(
f"{keys['sys_vendor']} {keys['product_name']} ({keys['product_family']})",
"💻",
)
debug_str = "DMI|value\n"
for key, value in keys.items():
if (
"product_name" in key
or "sys_vendor" in key
or "product_family" in key
):
continue
debug_str += f"{key}| {value}\n"
self.db.record_debug(debug_str)
return True
def check_lps0(self):
"""Check if LPS0 is enabled"""
for m in ["acpi", "acpi_x86"]:
p = os.path.join("/", "sys", "module", m, "parameters", "sleep_no_lps0")
if not os.path.exists(p):
continue
fail = read_file(p) == "Y"
if fail:
self.db.record_prereq("LPS0 _DSM disabled", "❌")
else:
self.db.record_prereq("LPS0 _DSM enabled", "✅")
return not fail
self.db.record_prereq("LPS0 _DSM not found", "👀")
return False
def get_cpu_vendor(self) -> str:
"""Fetch information about the CPU vendor"""
p = os.path.join("/", "proc", "cpuinfo")
vendor = ""
cpu = read_file(p)
for line in cpu.split("\n"):
if "vendor_id" in line:
vendor = line.split()[-1]
continue
elif "cpu family" in line:
self.cpu_family = int(line.split()[-1])
continue
elif "model name" in line:
self.cpu_model_string = line.split(":")[-1].strip()
continue
elif "model" in line:
self.cpu_model = int(line.split()[-1])
continue
if self.cpu_family and self.cpu_model and self.cpu_model_string:
self.db.record_prereq(
f"{self.cpu_model_string} "
f"(family {self.cpu_family:x} model {self.cpu_model:x})",
"💻",
)
break
return vendor
# See https://github.com/torvalds/linux/commit/ec6c0503190417abf8b8f8e3e955ae583a4e50d4
def check_fadt(self):
"""Check the kernel emitted a message indicating FADT had a bit set."""
found = False
if not self.kernel_log:
message = "Unable to test FADT from kernel log"
self.db.record_prereq(message, "🚦")
else:
self.kernel_log.seek()
matches = ["Low-power S0 idle used by default for system suspend"]
found = self.kernel_log.match_line(matches)
# try to look at FACP directly if not found (older kernel compat)
if not found:
self.db.record_debug("Fetching low power idle bit directly from FADT")
target = os.path.join("/", "sys", "firmware", "acpi", "tables", "FACP")
try:
with open(target, "rb") as r:
r.seek(0x70)
found = struct.unpack("<I", r.read(4))[0] & BIT(21)
except FileNotFoundError:
self.db.record_prereq("FADT check unavailable", "🚦")
return True
except PermissionError:
self.db.record_prereq("FADT check unavailable", "🚦")
return True
if found:
message = "ACPI FADT supports Low-power S0 idle"
self.db.record_prereq(message, "✅")
else:
message = "ACPI FADT doesn't support Low-power S0 idle"
self.db.record_prereq(message, "❌")
self.failures += [FadtWrong()]
return found
def capture_kernel_version(self):
"""Log the kernel version used"""
self.db.record_prereq(f"{get_pretty_distro()}", "🐧")
self.db.record_prereq(f"Kernel {platform.uname().release}", "🐧")
def capture_irq(self):
"""Capture the IRQs to the log"""
p = os.path.join("/sys", "kernel", "irq")
for directory in os.listdir(p):
if os.path.isdir(os.path.join(p, directory)):
wake = WakeIRQ(directory, self.pyudev)
self.irqs.append([int(directory), str(wake)])
self.irqs.sort()
self.db.record_debug("Interrupts")
for irq in self.irqs:
# set prefix if last IRQ
prefix = "│ " if irq != self.irqs[-1] else "└─"
self.db.record_debug(f"{prefix}{irq[0]}: {irq[1]}")
return True
def capture_disabled_pins(self):
"""Capture disabled pins from pinctrl-amd"""
base = os.path.join("/", "sys", "module", "gpiolib_acpi", "parameters")
debug_str = ""
for parameter in ["ignore_wake", "ignore_interrupt"]:
f = os.path.join(base, parameter)
if not os.path.exists(f):
continue
with open(f, "r", encoding="utf-8") as r:
d = r.read().rstrip()
if d != "(null)":
debug_str += f"{f} is configured to {d}\n"
if debug_str:
debug_str = "Disabled pins:\n" + debug_str
self.db.record_debug(debug_str)
def check_logger(self):
"""Check the source for kernel logs"""
if isinstance(self.kernel_log, SystemdLogger):
self.db.record_prereq("Logs are provided via systemd", "✅")
elif isinstance(self.kernel_log, CySystemdLogger):
self.db.record_prereq("Logs are provided via cysystemd", "✅")
elif isinstance(self.kernel_log, DmesgLogger):
self.db.record_prereq(
"Logs are provided via dmesg, timestamps may not be accurate over multiple cycles",
"🚦",
)
header = self.kernel_log.capture_header()
if not re.search(r"Linux version .*", header):
self.db.record_prereq(
"Kernel ring buffer has wrapped, unable to accurately validate pre-requisites",
"❌",
)
self.failures += [KernelRingBufferWrapped()]
return False
return True
def check_permissions(self):
"""Check if the user has permissions to write to /sys/power/state"""
p = os.path.join("/", "sys", "power", "state")
try:
with open(p, "w", encoding="utf-8") as _w:
pass
except PermissionError:
self.db.record_prereq(f"{Headers.RootError}", "👀")
return False
except FileNotFoundError:
self.db.record_prereq("Kernel doesn't support power management", "❌")
return False
return True
def capture_linux_firmware(self):
"""Capture the linux-firmware package version"""
for num in range(0, 2):
p = os.path.join(
"/", "sys", "kernel", "debug", "dri", f"{num}", "amdgpu_firmware_info"
)
if os.path.exists(p):
self.db.record_debug_file(p)
def check_amd_cpu_hpet_wa(self):
"""Check if the CPU offers the HPET workaround"""
show_warning = False
if self.cpu_family == 0x17:
if self.cpu_model in [0x68, 0x60]:
show_warning = True
elif self.cpu_family == 0x19:
if self.cpu_model == 0x50:
if self.smu_version:
show_warning = version.parse(self.smu_version) < version.parse(
"64.53.0"
)
if show_warning:
self.db.record_prereq(
"Timer based wakeup doesn't work properly for your "
"ASIC/firmware, please manually wake the system",
"🚦",
)
return True
def check_pinctrl_amd(self):
"""Check if the pinctrl_amd driver is loaded"""
debug_str = ""
for _device in self.pyudev.list_devices(
subsystem="platform", DRIVER="amd_gpio"
):
self.db.record_prereq("GPIO driver `pinctrl_amd` available", "✅")
p = os.path.join("/", "sys", "kernel", "debug", "gpio")
try:
contents = read_file(p)
except FileNotFoundError:
self.db.record_prereq("GPIO debugfs not available", "👀")
contents = None
except PermissionError:
self.db.record_debug(f"Unable to capture {p}")
contents = None
header = False
if contents:
for line in contents.split("\n"):
if "WAKE_INT_MASTER_REG:" in line:
val = "en" if int(line.split()[1], 16) & BIT(15) else "dis"
self.db.record_debug(f"Windows GPIO 0 debounce: {val}abled")
continue
if not header and re.search("trigger", line):
debug_str += line + "\n"
header = True
if re.search("edge", line) or re.search("level", line):
debug_str += line + "\n"
if "🔥" in line:
self.failures += [UnservicedGpio()]
return False
if debug_str:
self.db.record_debug(debug_str)
return True
self.db.record_prereq("GPIO driver `pinctrl_amd` not loaded", "❌")
return False
def check_network(self):
"""Check network devices for s2idle support"""
for device in self.pyudev.list_devices(subsystem="net", ID_NET_DRIVER="r8169"):
interface = device.properties.get("INTERFACE")
cmd = ["ethtool", interface]
wol_supported = False
try:
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode(
"utf-8"
)
except FileNotFoundError:
self.db.record_prereq("ethtool is missing", "👀")
return True
for line in output.split("\n"):
if "Supports Wake-on" in line:
val = line.split(":")[1].strip()
if "g" in val:
self.db.record_debug(f"{interface} supports WoL")
wol_supported = True
else:
self.db.record_debug(f"{interface} doesn't support WoL ({val})")
elif "Wake-on" in line and wol_supported:
val = line.split(":")[1].strip()
if "g" in val:
self.db.record_prereq(f"{interface} has WoL enabled", "✅")
else:
self.db.record_prereq(
"Platform may have low hardware sleep residency "
"with Wake-on-lan disabled. Run `ethtool -s "
f"{interface} wol g` to enable it if necessary.",
"🚦",
)
return True
def check_asus_rog_ally(self):
"""Check for MCU version on ASUS ROG Ally devices"""
for dev in self.pyudev.list_devices(subsystem="hid", DRIVER="asus_rog_ally"):
p = os.path.join(dev.sys_path, "mcu_version")
if not os.path.exists(p):
continue
v = int(read_file(p))
hid_id = get_property_pyudev(dev.properties, "HID_ID", "")
if "1ABE" in hid_id:
minv = 319
elif "1B4C" in hid_id:
minv = 313
else:
minv = None
if minv and v < minv:
self.db.record_prereq("ROG Ally MCU firmware too old", "❌")
self.failures += [RogAllyOldMcu(minv, v)]
return False
else:
self.db.record_debug("ASUS ROG MCU found with MCU version %d", v)
for dev in self.pyudev.list_devices(subsystem="firmware-attributes"):
p = os.path.join(
dev.sys_path, "attributes", "mcu_powersave", "current_value"
)
if not os.path.exists(p):
continue
v = int(read_file(p))
if v < 1:
self.db.record_prereq(
"Rog Ally doesn't have MCU powersave enabled", "❌"
)
self.failures += [RogAllyMcuPowerSave()]
return False
return True
def check_device_firmware(self):
"""Check for device firmware issues"""
if not FWUPD:
self.db.record_debug(
"Device firmware checks unavailable without gobject introspection"
)
return True
client = Fwupd.Client()
devices = client.get_devices()
for device in devices:
# Dictionary of instance id to firmware version mappings that
# have been "reported" to be problematic
device_map = {
# https://gitlab.freedesktop.org/drm/amd/-/issues/3443
"8c36f7ee-cc11-4a36-b090-6363f54ecac2": "0.1.26",
}
interesting_plugins = ["nvme", "tpm", "uefi_capsule"]
if device.get_plugin() in interesting_plugins:
logging.debug(
"%s %s firmware version: '%s'",
device.get_vendor(),
device.get_name(),
device.get_version(),
)
logging.debug("| %s", device.get_guids())
logging.debug("└─%s", device.get_instance_ids())
for item, ver in device_map.items():
if (
item in device.get_guids() or item in device.get_instance_ids()
) and ver in device.get_version():
self.db.record_prereq(
"Platform may have problems resuming. Upgrade the "
f"firmware for '{device.get_name()}' if you have problems.",
"🚦",
)
return True
def check_aspm(self):
"""Check if ASPM has been overridden"""
p = os.path.join("/", "sys", "module", "pcie_aspm", "parameters", "policy")
contents = read_file(p)
policy = ""
for word in contents.split(" "):
if word.startswith("["):
policy = word
break
if policy != "[default]":
self.db.record_prereq(f"ASPM policy set to {policy}", "❌")
self.failures += [ASpmWrong()]
return False
self.db.record_prereq("ASPM policy set to 'default'", "✅")
return True
def check_i2c_hid(self):
"""Check for I2C HID devices"""
devices = []
for dev in self.pyudev.list_devices(subsystem="input"):
if "NAME" not in dev.properties:
continue
parent = dev.find_parent(subsystem="i2c")
if parent is None:
continue
devices.append(dev)
if not devices:
return True
ret = True
debug_str = "I2C HID devices:\n"
for dev in devices:
name = dev.properties["NAME"]
parent = dev.find_parent(subsystem="i2c")
p = os.path.join(parent.sys_path, "firmware_node", "path")
if os.path.exists(p):
acpi_path = read_file(p)
else:
acpi_path = ""
p = os.path.join(parent.sys_path, "firmware_node", "hid")
if os.path.exists(p):
acpi_hid = read_file(p)
else:
acpi_hid = ""
# set prefix if last device
prefix = "│ " if dev != devices[-1] else "└─"
debug_str += f"{prefix}{name} [{acpi_hid}] : {acpi_path}\n"
if "IDEA5002" in name:
remediation = (
f"echo {parent.sys_path.split('/')[-1]} | "
f"sudo tee /sys/bus/i2c/drivers/{parent.driver}/unbind"
)
self.db.record_prereq(f"{name} may cause spurious wakeups", "❌")
self.failures += [I2CHidBug(name, remediation)]
ret = False
self.db.record_debug(debug_str)
return ret
def capture_pci_acpi(self):
"""Map ACPI to PCI devices"""
devices = []
for dev in self.pyudev.list_devices(subsystem="pci"):
devices.append(dev)
debug_str = "PCI Slot | Vendor | Class | ID | ACPI path\n"
for dev in devices:
pci_id = dev.properties["PCI_ID"].lower()
pci_slot_name = dev.properties["PCI_SLOT_NAME"]
database_class = get_property_pyudev(
dev.properties, "ID_PCI_SUBCLASS_FROM_DATABASE", ""
)
database_vendor = get_property_pyudev(
dev.properties, "ID_VENDOR_FROM_DATABASE", ""
)
if dev.parent.subsystem != "pci":
if dev == devices[-1]:
prefix = "└─"
else:
prefix = "│ "
else:
if dev == devices[-1]:
prefix = "└─"
else:
prefix = "├─ "
p = os.path.join(dev.sys_path, "firmware_node", "path")
if os.path.exists(p):
acpi = read_file(p)
else:
acpi = ""
debug_str += (
f"{prefix}{pci_slot_name} | "
f"{database_vendor} | {database_class} | {pci_id} | {acpi}\n"
)
if debug_str:
self.db.record_debug(debug_str)
def map_acpi_path(self):
"""Map of ACPI devices to ACPI paths"""
devices = []
for dev in self.pyudev.list_devices(subsystem="acpi"):
p = os.path.join(dev.sys_path, "path")
if not os.path.exists(p):
continue
p = os.path.join(dev.sys_path, "status")
if os.path.exists(p):
status = int(read_file(p))
if status == 0:
continue
devices.append(dev)
debug_str = "ACPI name | ACPI path | Kernel driver\n"
for dev in devices:
p = os.path.join(dev.sys_path, "path")
pth = read_file(p)
p = os.path.join(dev.sys_path, "physical_node", "driver")
if os.path.exists(p):
driver = os.path.basename(os.readlink(p))
else:
driver = None
debug_str += f"{dev.sys_name} | {pth} | {driver}\n"
if debug_str:
self.db.record_debug(debug_str)
return True
def capture_acpi(self):
"""Capture ACPI tables"""
base = os.path.join("/", "sys", "firmware", "acpi", "tables")
for root, _dirs, files in os.walk(base, topdown=False):
for fname in files:
target = os.path.join(root, fname)
if "SSDT" in fname:
with open(target, "rb") as f:
s = f.read()
if s.find(b"_AEI") < 0:
continue
elif "IVRS" in fname:
pass
else:
continue
try:
tmpd = tempfile.mkdtemp()
prefix = os.path.join(tmpd, "acpi")
subprocess.check_call(
["iasl", "-p", prefix, "-d", target],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
self.db.record_debug_file(f"{prefix}.dsl")
except FileNotFoundError as e:
self.db.record_prereq(f"Failed to capture ACPI table: {e}", "👀")
except subprocess.CalledProcessError as e:
self.db.record_prereq(
f"Failed to capture ACPI table: {e.output}", "👀"
)
finally:
shutil.rmtree(tmpd)
return True
def capture_cstates(self):
"""Capture ACPI C state information for the first CPU (assumes the same for all CPUs)"""
base = os.path.join("/", "sys", "bus", "cpu", "devices", "cpu0", "cpuidle")
paths = {}
for root, _dirs, files in os.walk(base, topdown=False):
for fname in files:
target = os.path.join(root, fname)
with open(target, "rb") as f:
paths[target] = f.read()
debug_str = "ACPI C-state information\n"
for path, data in paths.items():
prefix = "│ " if path != list(paths.keys())[-1] else "└─"
debug_str += f"{prefix}{path}: {data.decode('utf-8', 'ignore')}"
self.db.record_debug(debug_str)
def capture_battery(self):
"""Capture battery information"""
obj = Batteries()
for bat in obj.get_batteries():
desc = obj.get_description_string(bat)
self.db.record_prereq(desc, "🔋")
def capture_logind(self):
"""Capture logind.conf settings"""
base = os.path.join("/", "etc", "systemd", "logind.conf")
if not os.path.exists(base):
return True
config = configparser.ConfigParser()
config.read(base)
section = config["Login"]
if not section.keys():
self.db.record_debug("LOGIND: no configuration changes")
return True
self.db.record_debug("LOGIND: configuration changes:")
for key in section.keys():
self.db.record_debug(f"\t{key}: {section[key]}")
def check_cpu(self):
"""Check if the CPU is supported"""
def read_cpuid(cpu, leaf, subleaf):
"""Read CPUID using kernel userspace interface"""
p = os.path.join("/", "dev", "cpu", f"{cpu}", "cpuid")
if not os.path.exists(p):
os.system("modprobe cpuid")
with open(p, "rb") as f:
position = (subleaf << 32) | leaf
f.seek(position)
data = f.read(16)
return struct.unpack("4I", data)
valid = True
# check for supported models
if self.cpu_family == 0x17:
if self.cpu_model in range(0x30, 0x3F):
valid = False
if self.cpu_family == 0x19:
if self.cpu_model in [0x08, 0x18]:
valid = False
if not valid:
self.failures += [UnsupportedModel()]
self.db.record_prereq(
"This CPU model does not support hardware sleep over s2idle",
"❌",
)
return False
# check for artificially limited CPUs
p = os.path.join("/", "sys", "devices", "system", "cpu", "kernel_max")
max_cpus = int(read_file(p)) + 1 # 0 indexed
# https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24594.pdf
# Extended Topology Enumeration (NumLogCores)
# CPUID 0x80000026 subleaf 1
try:
_, cpu_count, _, _ = read_cpuid(0, 0x80000026, 1)
if cpu_count > max_cpus:
self.db.record_prereq(
f"The kernel has been limited to {max_cpus} CPU cores, "
f"but the system has {cpu_count} cores",
"❌",
)
self.failures += [LimitedCores(cpu_count, max_cpus)]
return False
self.db.record_debug(f"CPU core count: {cpu_count} max: {max_cpus}")
except FileNotFoundError:
self.db.record_prereq(
"Unable to check CPU topology: cpuid kernel module not loaded", "❌"
)
return False
except PermissionError:
self.db.record_prereq("CPUID checks unavailable", "🚦")
return True
def check_msr(self):
"""Check if PC6 or CC6 has been disabled"""
def check_bits(value, mask):
return value & mask
expect = {
0xC0010292: BIT(32), # PC6
0xC0010296: (BIT(22) | BIT(14) | BIT(6)), # CC6
}
try:
for reg, expect_val in expect.items():
val = read_msr(reg, 0)
if not check_bits(val, expect_val):
self.failures += [MSRFailure()]
return False
self.db.record_prereq("PC6 and CC6 enabled", "✅")
except FileNotFoundError:
self.db.record_prereq(
"Unable to check MSRs: MSR kernel module not loaded", "❌"
)
return False
except PermissionError:
self.db.record_prereq("MSR checks unavailable", "🚦")
return True
def check_smt(self):
"""Check if SMT is enabled"""
p = os.path.join("/", "sys", "devices", "system", "cpu", "smt", "control")
v = read_file(p)
self.db.record_debug(f"SMT control: {v}")
if v == "notsupported":
return True
p = os.path.join("/", "sys", "devices", "system", "cpu", "smt", "active")
v = read_file(p)
if v == "0":
self.failures += [SMTNotEnabled()]
self.db.record_prereq("SMT is not enabled", "❌")
return False
self.db.record_prereq("SMT enabled", "✅")
return True
def check_iommu(self):
"""Check IOMMU configuration"""
affected_1a = (
list(range(0x20, 0x2F)) + list(range(0x60, 0x6F)) + list(range(0x70, 0x7F))
)
debug_str = ""
if self.cpu_family == 0x1A and self.cpu_model in affected_1a:
found_iommu = False
found_acpi = False
for dev in self.pyudev.list_devices(subsystem="iommu"):
found_iommu = True
debug_str += f"Found IOMMU {dev.sys_path}\n"
break
# User turned off IOMMU, no problems
if not found_iommu:
self.db.record_prereq("IOMMU disabled", "✅")
return True
# Look for MSFT0201 in DSDT/SSDT
for dev in self.pyudev.list_devices(subsystem="acpi"):
if "MSFT0201" in dev.sys_path:
found_acpi = True
if not found_acpi:
self.db.record_prereq(
"IOMMU is misconfigured: missing MSFT0201 ACPI device", "❌"
)
self.failures += [MissingIommuACPI("MSFT0201")]
return False
# Check that policy is bound to it
for dev in self.pyudev.list_devices(subsystem="platform"):
if "MSFT0201" in dev.sys_path:
p = os.path.join(dev.sys_path, "iommu")
if not os.path.exists(p):
self.failures += [MissingIommuPolicy("MSFT0201")]
return False
# Check pre-boot DMA
p = os.path.join("/", "sys", "firmware", "acpi", "tables", "IVRS")
with open(p, "rb") as f:
data = f.read()
if len(data) < 40:
raise ValueError(
"IVRS table appears too small to contain virtualization info."
)
virt_info = struct.unpack_from("I", data, 36)[0]
debug_str += f"IVRS: Virtualization info: 0x{virt_info:x}\n"
found_ivrs_dmar = (virt_info & 0x2) != 0
# check for MSFT0201 in IVRS (alternative to pre-boot DMA)
target_bytes = "MSFT0201".encode("utf-8")
found_ivrs_msft0201 = data.find(target_bytes) != -1
debug_str += f"IVRS: Found MSFT0201: {found_ivrs_msft0201}"
self.db.record_debug(debug_str)
if not found_ivrs_dmar and not found_ivrs_msft0201:
self.db.record_prereq(
"IOMMU is misconfigured: Pre-boot DMA protection not enabled", "❌"
)
self.failures += [DMArNotEnabled()]
return False
self.db.record_prereq("IOMMU properly configured", "✅")
return True
def check_port_pm_override(self):
"""Check for PCIe port power management override"""
if self.cpu_family != 0x19:
return True
if self.cpu_model not in [0x74, 0x78]:
return True
if not self.smu_version:
return True
if version.parse(self.smu_version) > version.parse("76.60.0"):
return True
if version.parse(self.smu_version) < version.parse("76.18.0"):
return True
cmdline = read_file(os.path.join("/proc", "cmdline"))
if "pcie_port_pm=off" in cmdline:
return True
self.db.record_prereq(
"Platform may hang resuming. "
"Upgrade your firmware or add pcie_port_pm=off to kernel command "
"line if you have problems.",
"🚦",
)
return False
def check_taint(self):
"""Check if the kernel is tainted"""
fn = os.path.join("/", "proc", "sys", "kernel", "tainted")
taint = int(read_file(fn))
# ignore kernel warnings
taint &= ~BIT(9)
if taint != 0:
self.db.record_prereq(f"Kernel is tainted: {taint}", "🚦")
self.failures += [TaintedKernel()]
return True
def run(self):
"""Run the prerequisites check"""
msg = "Checking prerequisites, please wait"
print_temporary_message(msg)
info = [
self.capture_smbios,
self.capture_kernel_version,
self.capture_battery,
self.capture_linux_firmware,
self.capture_logind,
self.capture_pci_acpi,
self.capture_edid,
self.capture_nvidia,
self.capture_cstates,
]
checks = []
vendor = self.get_cpu_vendor()
if vendor == "AuthenticAMD":
info += [
self.capture_disabled_pins,
]
checks += [
self.check_aspm,
self.check_i2c_hid,
self.check_pinctrl_amd,
self.check_amd_hsmp,
self.check_amd_pmc,
self.check_amd_cpu_hpet_wa,
self.check_port_pm_override,
self.check_usb3,
self.check_usb4,
self.check_sleep_mode,
self.check_storage,
self.check_wcn6855_bug,
self.check_amdgpu,
self.check_amdgpu_parameters,
self.check_cpu,
self.check_msr,
self.check_smt,
self.check_iommu,
self.check_asus_rog_ally,
self.check_dpia_pg_dmcub,
]
checks += [
self.check_fadt,
self.check_logger,
self.check_lps0,
self.check_permissions,
self.check_wlan,
self.check_taint,
self.capture_acpi,
self.map_acpi_path,
self.check_device_firmware,
self.check_network,
]
for i in info:
i()
result = True
for check in checks:
if not check():
result = False
if not result:
self.db.record_prereq(Headers.BrokenPrerequisites, "🚫")
self.db.sync()
clear_temporary_message(len(msg))
return result
def report(self) -> None:
"""Print a report of the results of the checks."""
ts = self.db.get_last_prereq_ts()
t0 = datetime.strptime(str(ts), "%Y%m%d%H%M%S")
for row in self.db.report_prereq(t0):
print_color(row[2], row[3])
for row in self.db.report_debug(t0):
for line in row[0].split("\n"):
if self.debug:
print_color(line, "🦟")
else:
logging.debug(line)
if len(self.failures) == 0:
return
print_color(Headers.ExplanationReport, "🗣️")
for item in self.failures:
item.get_failure()