Geometric black device with glowing blue power cable in server environment

Fixing Wake-on-LAN on the Linux atlantic Driver

Summary: Diagnose and fix a one-line kernel bug that prevents Wake-on-LAN from S5 (full poweroff) on Aquantia/Marvell AQC-series NICs, then deploy the patched driver via DKMS so it survives kernel updates automatically.

Example Values Used in This Tutorial

Key Value
Motherboard ASUS PROART X870E
10GbE NIC Marvell AQR113C
10GbE driver atlantic
10GbE interface enp14s0
10GbE MAC address 60:cf:84:8a:7c:3d
OS Linux Mint 22.3 (Ubuntu 24.04 base)
Kernel 6.8.0-111-generic
DKMS package name atlantic-wol-fix
DKMS version 1.0
DKMS source directory /usr/src/atlantic-wol-fix-1.0/

0. Prerequisites

  • An Aquantia or Marvell NIC using the atlantic driver (AQC107, AQC108, AQC111, AQC112, AQC113, or AQR113C)
  • A Linux distro based on Ubuntu 22.04 or 24.04, or any distro that supports DKMS
  • ethtool installed
  • A second machine on the same network to send the magic packet
  • BIOS settings: ErP disabled, PCIe wake enabled, Restore AC Power Loss set to "Power On"

1. The symptoms

The hardware: an ASUS PROART X870E motherboard with two onboard NICs:

  • Marvell AQR113C — 10GbE, driver atlantic, MAC 60:cf:84:8a:7c:3d
  • Intel I226-V — 2.5GbE, driver igc, MAC bc:fc:e7:b2:77:1c

Wake-on-LAN was already working on Windows on the same hardware. On Linux Mint 22.3 (Ubuntu 24.04 base, kernel 6.8.0-111-generic) it claimed to be configured:

sudo ethtool enp14s0 | grep Wake-onCode language: Shell Session (shell)
Supports Wake-on: pg Wake-on: g

But on sudo poweroff, the magic packet fired from another machine never woke the system. The PSU LED was off, the NIC's link LED was off, and the magic packet was dropping into a black hole.

Things ruled out before touching code:

  • Magic packet arrived on the wire (tcpdump from the firewall confirmed it)
  • BIOS settings correct: ErP off, Restore AC Power Loss = "Power On", PCIe wake enabled
  • No tlp installed (which can disable WOL on shutdown)
  • ACPI PCI wakeup enabled
  • Switch supports lower-speed link maintenance

So it was clearly software. Two relevant GitHub issues existed: Aquantia/AQtion #70 and #54. Both open. Both unanswered. The trail looked cold.


2. The smoking gun

Issue #54 contained a single observation that flipped the whole problem:

"if I have not loaded it [the atlantic driver], then my board can wake on LAN normally. But if I load it and set the interface up, then my board can not wake on LAN after poweroff."

That changes everything. It means:

  • The NIC's firmware/PHY natively supports WOL — it's the default state
  • atlantic is actively breaking it during shutdown — not failing to enable it, actively unsetting it

So the bug isn't somewhere we need to add code. It's somewhere we need to not break what already works. That narrows the investigation to one specific place: the driver's shutdown path.


3. Reading the source

Pull the in-tree atlantic driver from upstream:

mkdir -p ~/atlantic && cd ~/atlantic
git clone --depth 1 --filter=blob:none --sparse https://github.com/torvalds/linux.git
cd linux
git sparse-checkout set drivers/net/ethernet/aquantia/atlanticCode language: Shell Session (shell)

The PCI-layer entry points live in aq_pci_func.c. The shutdown handler is short:

static void aq_pci_shutdown(struct pci_dev *pdev)
{
    struct aq_nic_s *self = pci_get_drvdata(pdev);

    aq_nic_shutdown(self);

    pci_disable_device(pdev);

    if (system_state == SYSTEM_POWER_OFF) {
        pci_wake_from_d3(pdev, false);
        pci_set_power_state(pdev, PCI_D3hot);
    }
}Code language: Arduino (arduino)

aq_nic_shutdown() eventually calls aq_nic_set_power(), which programs the NIC firmware to listen for magic packets when WOL is enabled. That part is correct.

But then pci_wake_from_d3(pdev, false) clears the PCI PME_En bit. PME_En is the standard PCIe mechanism for a device in D3 to signal a wake event to the platform. Even if the firmware detects a magic packet, the PCI subsystem won't relay it — because the driver just told PCI "do not wake from D3."

The two halves of the driver are working against each other:

  • Firmware: "I'll wake the system if I see a magic packet."
  • PCI layer: "Don't wake the system."

Net result: nothing wakes.

For comparison, look at the suspend path (aq_suspend_common), which handles S3 sleep and works for WOL on the same hardware:

static int aq_suspend_common(struct device *dev)
{
    struct aq_nic_s *nic = pci_get_drvdata(to_pci_dev(dev));
    /* ... */
    aq_nic_deinit(nic, !nic->aq_hw->aq_nic_cfg->wol);
    aq_nic_set_power(nic);
    /* ... */
}Code language: Arduino (arduino)

There is no pci_wake_from_d3(pdev, false) call here. The kernel's PM core manages wake configuration based on device_may_wakeup(). That's why S3 wake works and S5 wake doesn't.


4. The fix

A single-line change — pass the configured WOL state through instead of unconditionally false:

--- a/drivers/net/ethernet/aquantia/atlantic/aq_pci_func.c
+++ b/drivers/net/ethernet/aquantia/atlantic/aq_pci_func.c
@@ -374,7 +374,7 @@ static void aq_pci_shutdown(struct pci_dev *pdev)
        pci_disable_device(pdev);
 
        if (system_state == SYSTEM_POWER_OFF) {
-               pci_wake_from_d3(pdev, false);
+               pci_wake_from_d3(pdev, self->aq_hw->aq_nic_cfg->wol);
                pci_set_power_state(pdev, PCI_D3hot);
        }
 }Code language: Diff (diff)

The struct path self->aq_hw->aq_nic_cfg->wol is the same one used elsewhere in the driver (e.g. line 1503 of aq_nic.c), so it's a known working accessor. When WOL is enabled, this preserves the PCI PME_En bit so the firmware's detected magic packet can actually generate a wake event. When WOL is disabled, the call becomes pci_wake_from_d3(pdev, false) — identical to the original behavior.


5. Building it as a DKMS module

Building the patched module out-of-tree means no kernel rebuild and automatic re-application across kernel updates.

Source layout: /usr/src/atlantic-wol-fix-1.0/ containing the patched atlantic source, an out-of-tree Makefile, and a dkms.conf.

The out-of-tree Makefile (the in-tree one uses obj-$(CONFIG_AQTION) which won't work standalone):

obj-m += atlantic.o
ccflags-y += -I$(src)
atlantic-objs := aq_main.o aq_nic.o aq_pci_func.o aq_vec.o aq_ring.o \
    aq_hw_utils.o aq_ethtool.o aq_drvinfo.o aq_filters.o aq_phy.o \
    hw_atl/hw_atl_a0.o hw_atl/hw_atl_b0.o hw_atl/hw_atl_utils.o \
    hw_atl/hw_atl_utils_fw2x.o hw_atl/hw_atl_llh.o \
    hw_atl2/hw_atl2.o hw_atl2/hw_atl2_utils.o hw_atl2/hw_atl2_utils_fw.o \
    hw_atl2/hw_atl2_llh.o macsec/macsec_api.o
atlantic-$(CONFIG_MACSEC) += aq_macsec.o
atlantic-$(CONFIG_PTP_1588_CLOCK) += aq_ptp.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
	$(MAKE) -C $(KDIR) M=$(CURDIR) modules
clean:
	$(MAKE) -C $(KDIR) M=$(CURDIR) cleanCode language: Makefile (makefile)

The dkms.conf:

PACKAGE_NAME=”atlantic-wol-fix” PACKAGE_VERSION=”1.0″ BUILT_MODULE_NAME[0]=”atlantic” DEST_MODULE_LOCATION[0]=”/updates/dkms” AUTOINSTALL=”yes” MAKE[0]=”make KDIR=/lib/modules/${kernelver}/build” CLEAN=”make clean”

The crucial detail is DEST_MODULE_LOCATION[0]="/updates/dkms". This places the module under /lib/modules/<kver>/updates/dkms/atlantic.ko, which depmod ranks higher than the in-tree /lib/modules/<kver>/kernel/drivers/.../atlantic.ko. The kernel loads the patched module automatically — no blacklist or modprobe override needed.

Install the build dependencies and register the module with DKMS:

sudo apt install -y dkms build-essential linux-headers-$(uname -r)
sudo dkms add    -m atlantic-wol-fix -v 1.0
sudo dkms build  -m atlantic-wol-fix -v 1.0
sudo dkms install -m atlantic-wol-fix -v 1.0 --force
sudo depmod -aCode language: Shell Session (shell)

After reboot, confirm the patched module is loaded:

modinfo atlantic | grep filenameCode language: Shell Session (shell)
filename: /lib/modules/6.8.0-111-generic/updates/dkms/atlantic.ko.zst

6. The actual test

Arm WOL and confirm it is set:

sudo ethtool -s enp14s0 wol g
ethtool enp14s0 | grep Wake-onCode language: Shell Session (shell)
Supports Wake-on: pg Wake-on: g

Power off the machine:

sudo poweroffCode language: Shell Session (shell)

From another machine on the same network, send the magic packet:

wakeonlan 60:cf:84:8a:7c:3dCode language: Shell Session (shell)

Result: the machine powers on. PSU fans spin, the link LED comes back, and SSH is available within ~30 seconds. A feature that should have worked all along, finally working.


7. Making WOL persistent via NetworkManager

The ethtool -s command sets WOL for the current session only. NetworkManager can reset it on profile reactivation or across reboots. To make the setting survive permanently, configure it in the connection profile directly.

Find the active connection profile for the 10GbE interface:

nmcli -f NAME,DEVICE,TYPE connection show --activeCode language: Shell Session (shell)

Set Wake-on-LAN to magic packet mode, substituting the profile name from the NAME column:

sudo nmcli connection modify "<profile-name>" 802-3-ethernet.wake-on-lan magicCode language: Shell Session (shell)

Apply the change without dropping the link:

sudo nmcli device reapply enp14s0Code language: Shell Session (shell)

Note: reapply is safe over SSH. If it fails on an older NetworkManager version, use connection down then connection up instead — but not over SSH unless you have another way back in.

Verify:

nmcli connection show "<profile-name>" | grep -i wake-on-lan
sudo ethtool enp14s0 | grep -E "Wake-on|Link detected"Code language: Shell Session (shell)

Expected output:

802-3-ethernet.wake-on-lan: magic Wake-on: g Link detected: yes

With the atlantic DKMS patch installed and WOL set in the connection profile, no extra systemd service is required to re-arm WOL on each boot.


8. Why this bug survived for years

A few observations on how something this small avoided being caught:

  • Ownership transition. The atlantic driver was handed off from Aquantia to Marvell. The original Aquantia GitHub repo is essentially abandoned, and issues sit open for months with no response.
  • No regression test. Testing WOL from S5 in CI requires actually powering off and back on real hardware. Most kernel network test suites focus on packet flows, not power management.
  • Suspend works, shutdown doesn't. S3 (suspend-to-RAM) WOL works correctly because that path doesn't have the bug. Many users probably never notice — they suspend instead of fully shutting down.
  • Confusing user-visible state. ethtool happily reports Wake-on: g. Everything says it's armed. There is no failure indicator at any layer until you actually test it.
  • The fix looks risky. Passing wol to pci_wake_from_d3() might appear to affect users with WOL disabled. It doesn't — when wol is 0 the call is identical to the original. A casual reviewer could miss that immediately.

The diagnostic that made this tractable was Issue #54's observation: "works without the driver loaded, breaks once the driver runs." That single sentence pointed straight at the shutdown teardown path. Without it, finding this would have meant reading a lot more code.

Lesson for future driver bug hunts: read every issue on the relevant repo, even the closed or abandoned ones. Sometimes a user already did the bisection in a comment from two years ago.


9. Patch status and open questions

The one-line fix has been submitted to the Linux netdev mailing list. You can follow the review thread here:

https://lore.kernel.org/netdev/[email protected]/

We're excited to see it land upstream — once accepted, every atlantic user gets the fix automatically in any kernel that includes it, no DKMS required.

Note: Wake-on-LAN behavior on the Intel I226-V (igc driver) on this same board is still under investigation. Results and findings will be shared once testing is complete.


Summary

You traced a Wake-on-LAN failure on the atlantic driver to a single line in aq_pci_func.c that unconditionally cleared the PCI PME_En bit on shutdown, counteracting the firmware's own WOL configuration. The one-line fix passes the configured WOL state to pci_wake_from_d3() so the PME_En bit is only cleared when WOL is actually disabled. The patched driver is deployed as a DKMS module, placed under /updates/dkms/ so it takes precedence over the in-tree version and rebuilds automatically across kernel updates.

  • Source file changed: drivers/net/ethernet/aquantia/atlantic/aq_pci_func.c
  • Key insight: the suspend path (aq_suspend_common) already handled this correctly — the shutdown path (aq_pci_shutdown) did not
  • DKMS module placement in /updates/dkms/ is what ensures the patched module wins without any blacklist entry

Similar Posts

Leave a Reply