From ac7a47c26afa5ffc236224916961c449aab80df0 Mon Sep 17 00:00:00 2001 From: Andreas Schaafsma Date: Thu, 15 Oct 2026 10:48:08 +0200 Subject: [PATCH] module for recovering shitty realtek controller when it gets stuck in bugged power state --- modules/nixos/rtl8125-recovery/default.nix | 58 ++++++++++++ modules/nixos/rtl8125-recovery/recover.sh | 89 +++++++++++++++++++ .../drivebystation-nix/default.nix | 1 + 3 files changed, 148 insertions(+) create mode 100644 modules/nixos/rtl8125-recovery/default.nix create mode 100644 modules/nixos/rtl8125-recovery/recover.sh diff --git a/modules/nixos/rtl8125-recovery/default.nix b/modules/nixos/rtl8125-recovery/default.nix new file mode 100644 index 0000000..1e46531 --- /dev/null +++ b/modules/nixos/rtl8125-recovery/default.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, namespace, ... }: +let + cfg = config.${namespace}.rtl8125-recovery; + recovery = pkgs.writeShellApplication { + name = "rtl8125-recover"; + runtimeInputs = with pkgs; [ coreutils gnugrep pciutils ethtool systemd ]; + text = builtins.readFile ./recover.sh; + }; +in { + options.${namespace}.rtl8125-recovery = { + enable = lib.mkEnableOption "RTL8125 PCI recovery after boot and resume"; + pciAddress = lib.mkOption { + type = lib.types.str; + default = "0000:0a:00.0"; + description = "PCI address of the onboard RTL8125 controller."; + }; + bridgeAddress = lib.mkOption { + type = lib.types.str; + default = "0000:06:03.0"; + description = "Immediate parent PCI bridge of the controller."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.rtl8125-recovery = { + description = "Recover RTL8125 from failed PCI power restoration"; + wantedBy = [ "multi-user.target" ]; + after = [ "NetworkManager.service" ]; + environment = { + NIC_PCI = cfg.pciAddress; + NIC_BRIDGE = cfg.bridgeAddress; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = lib.getExe recovery; + TimeoutStartSec = "90s"; + StateDirectory = "rtl8125-recovery"; + StateDirectoryMode = "0700"; + UMask = "0077"; + }; + }; + + # sleep.target is stopped after resume. Queue recovery separately so its + # grace period does not hold up the sleep transaction or user sessions. + systemd.services.rtl8125-resume = { + description = "Queue RTL8125 recovery after waking"; + wantedBy = [ "sleep.target" ]; + before = [ "sleep.target" ]; + unitConfig.StopWhenUnneeded = true; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/true"; + ExecStop = "${pkgs.systemd}/bin/systemctl --no-block start rtl8125-recovery.service"; + }; + }; + }; +} diff --git a/modules/nixos/rtl8125-recovery/recover.sh b/modules/nixos/rtl8125-recovery/recover.sh new file mode 100644 index 0000000..d1a257e --- /dev/null +++ b/modules/nixos/rtl8125-recovery/recover.sh @@ -0,0 +1,89 @@ +# Called once after boot or resume. No network access is required. +set -euo pipefail +nic="/sys/bus/pci/devices/$NIC_PCI" +bridge="/sys/bus/pci/devices/$NIC_BRIDGE" +log() { echo "rtl8125-recovery: $*"; } + +# Let normal PCI and network resume finish before diagnosing anything. +sleep 10 +if [[ ! -d "$bridge" ]]; then + log "Parent bridge $NIC_BRIDGE is absent; refusing recovery." + exit 1 +fi +if [[ $(cat "$bridge/vendor") != 0x1022 || $(cat "$bridge/device") != 0x43f5 ]]; then + log "Parent bridge identity changed; refusing recovery." + exit 1 +fi + +if [[ -d "$nic" ]]; then + if [[ $(cat "$nic/vendor") != 0x10ec || $(cat "$nic/device") != 0x8125 ]]; then + log "Device at $NIC_PCI is not an RTL8125; refusing recovery." + exit 1 + fi + if [[ $(dirname "$(readlink -f "$nic")") != "$(readlink -f "$bridge")" ]]; then + log "NIC is no longer directly below the configured bridge; refusing recovery." + exit 1 + fi + power=$(cat "$nic/power_state") + command=$(timeout 5 setpci -s "$NIC_PCI" COMMAND 2>/dev/null || true) + # Both memory decoding and bus mastering should be enabled on this host. + # Disconnected cables do not disable these PCI command bits. + if [[ "$power" != D3cold && "$command" =~ ^[[:xdigit:]]{4}$ ]] && + (( (16#$command & 6) == 6 )); then + log "PCI state healthy ($power, COMMAND=$command); no recovery needed." + exit 0 + fi + # An administratively disabled NIC should not be automatically reset. + netdirs=("$nic"/net/*) + if [[ -e "${netdirs[0]}/flags" ]]; then + flags=$(cat "${netdirs[0]}/flags") + if (( (flags & 1) == 0 )); then + log "Interface is administratively down; leaving it alone." + exit 0 + fi + fi + log "Failed PCI state: power=$power COMMAND=$command." +else + log "NIC missing; attempting one rescan of its known bridge." +fi + +# Retain evidence from the latest failure without accumulating unlimited logs. +evidence="${STATE_DIRECTORY:-/var/lib/rtl8125-recovery}/last-failure.log" +mkdir -p "$(dirname "$evidence")" +{ + date -Is + timeout 5 lspci -s "$NIC_PCI" -vv || true + timeout 5 lspci -s "$NIC_BRIDGE" -vv || true + journalctl -b -k --since '10 minutes ago' -n 250 --no-pager || true +} > "$evidence" 2>&1 + +# This is the remove/rescan recovery that worked on this machine. Never reset +# or remove the parent bridge, which could affect other hardware. +if [[ -d "$nic" ]]; then + echo 1 > "$nic/remove" +fi +echo 1 > "$bridge/rescan" + +for ((attempt=0; attempt<20; attempt++)); do + netdirs=("$nic"/net/*) + if [[ -e "${netdirs[0]}/ifindex" ]]; then + iface=$(basename "${netdirs[0]}") + power=$(cat "$nic/power_state") + command=$(timeout 5 setpci -s "$NIC_PCI" COMMAND 2>/dev/null || true) + if [[ "$power" == D0 && "$command" =~ ^[[:xdigit:]]{4}$ ]] && + (( (16#$command & 6) == 6 )); then + # NetworkManager also reapplies the saved profile. Explicitly + # restore WoL here so recovery does not depend on its timing. + ethtool -s "$iface" wol g + if ! ethtool "$iface" | grep -Eq '^[[:space:]]*Wake-on: g$'; then + log "PCI recovered, but magic-packet WoL could not be confirmed." + exit 1 + fi + log "Recovered $iface in D0; magic-packet WoL enabled. Evidence: $evidence" + exit 0 + fi + fi + sleep 1 +done +log "Recovery did not restore operational PCI state; no further attempt this invocation." +exit 1 diff --git a/systems/x86_64-linux/drivebystation-nix/default.nix b/systems/x86_64-linux/drivebystation-nix/default.nix index 68e3e8a..0baa5f8 100644 --- a/systems/x86_64-linux/drivebystation-nix/default.nix +++ b/systems/x86_64-linux/drivebystation-nix/default.nix @@ -53,6 +53,7 @@ in my-namespace.desktop-environment.hyprland.enable = false; my-namespace.desktop-environment.cosmic.enable = false; my-namespace.performance-tuning.enable = true; + my-namespace.rtl8125-recovery.enable = true; my-namespace.file-organization.enable = true; # GNOME-keyring my-namespace.gnome-keyring.enable = true;