I installed Ubuntu 26.04 on a retired MacBook 12-inch (MacBook10,1, 2017). Closing the lid and opening it again brought up an authentication error on screen, with the keyboard completely unresponsive. The only way out was holding the power button.
The short version: the cause was putting the Apple NVMe SSD and its PCIe root port into D3cold. The machine hangs in the noirq phase of suspend and never comes back. A udev rule setting d3cold_allowed=0 fixes it.
The authentication error and the dead keyboard were both symptoms, not the cause. I burned a lot of time suspecting the keyboard driver first, so I’m writing down the whole narrowing-down process too.
Environment
| Item | Value |
|---|---|
| Model | MacBook10,1 (12-inch, 2017) |
| OS | Ubuntu 26.04 LTS |
| Kernel | 7.0.0-28-generic |
| SSD | APPLE SSD AP0512J (Apple S3X NVMe Controller, 106b:2003) |
| Desktop | GNOME / Wayland |
Symptoms
Closing the lid suspends the machine, and opening it brings the display back. But the screen shows an authentication error, and typing a password does nothing because the keyboard is unresponsive. Same for the trackpad. Holding the power button was the only option.
My First Guess Was Wrong
On the MacBook 12-inch, the keyboard and trackpad are connected over SPI rather than USB, driven by a module called applespi.
lsmod | grep -E 'applespi|spi_pxa'applespi 57344 0spi_pxa2xx_platform 12288 0spi_pxa2xx_core 32768 1 spi_pxa2xx_platformSearch around and you’ll find no shortage of reports that the applespi keyboard dies after resume. The standard workaround is to unload and reload the module around suspend. Drop a hook in /usr/lib/systemd/system-sleep/ and it gets called with pre before sleeping and post after waking.
#!/bin/shcase "$1" in pre) modprobe -r applespi ;; post) modprobe applespi ;;esacIt didn’t help. Neither did also unloading the SPI controller itself (spi_pxa2xx_platform, spi_pxa2xx_core). As it turned out later, applespi had nothing to do with the problem.
Not a Single Line of Log Survives
This was the first wall I hit when I tried to investigate properly. Looking at past boots with journalctl, every single one cuts off at exactly the same line.
journalctl -b -1 --no-pager -n 3Jul 27 17:47:20 systemd-sleep[6306]: Performing sleep operation 'suspend'...Jul 27 17:47:20 kernel: PM: suspend entry (deep)Nothing after resume. Not one line. I checked five boots and they were all identical.
That means no disk write goes through after resume. The display comes back but nothing gets logged — at this point the “it’s the keyboard” theory starts to look very shaky.
When investigating anything that hangs the machine, you have to sync after every log write. Anything left in the page cache is gone once you cut the power. Every script below does this.
say() { echo "$(date '+%F %T') $*" >> "$LOG"; sync; }Narrowing It Down With pm_test
If your kernel has CONFIG_PM_DEBUG=y, you get /sys/power/pm_test. It runs the suspend sequence partway through without actually sleeping, then returns after five seconds — exactly what you want here.
cat /sys/power/pm_test[none] core processors platform devices freezerWork from the shallowest stage upward.
for stage in freezer devices platform processors core; do echo "$stage" | sudo tee /sys/power/pm_test echo freeze | sudo tee /sys/power/statedoneHere’s what happened.
<<< stage=freezer passed<<< stage=devices passed>>> stage=platform starting <- never returnsTwo runs, identical result. That narrowed things down enormously.
What each stage covers:
| Stage | Covers |
|---|---|
freezer | Freezing processes only |
devices | + the normal suspend/resume phase of every driver |
platform | + the late/noirq phases and platform (ACPI) handling |
processors | + disabling non-boot CPUs |
core | + core kernel suspend |
devices passing means applespi, the NVMe, the wireless card, i915 — every driver’s normal-phase suspend/resume works fine. The keyboard-driver theory was dead.
What platform adds is the late/noirq phases, which is where PCI devices get dropped into low-power states (D3).
Worth noting: at that point the console is already down, so adding no_console_suspend and watching the screen shows you nothing. I wasted a fair bit of time there.
Suspecting D3cold
Since power transitions in the noirq phase looked like the problem, I stopped devices from being fully powered off (D3cold). There’s a sysfs knob for it.
for d in /sys/bus/pci/devices/*/d3cold_allowed; do echo 0 | sudo tee "$d"; doneRunning the platform stage in that state — it passed.
2026-07-28 10:43:08 === set d3cold_allowed=0 on 19 devices2026-07-28 10:43:09 >>> running platform stage (condition: no-d3cold)2026-07-28 10:43:17 <<< PASSED condition: no-d3coldConfirmed: PCIe D3cold transitions in the noirq phase.
Finding the Culprit
Disabling it everywhere works, but it wastes power. Time to find the specific device. The PCI tree looks like this:
lspci -t -v-[0000:00]-+-02.0 Intel Kaby Lake-Y GT2 [HD Graphics 615] +-1c.0-[01]----00.0 Apple Inc. S3X NVMe Controller +-1d.0-[02]----00.0 Broadcom BCM4350 802.11ac Wireless +-1d.1-[03]----00.0 Broadcom 720p FaceTime HD Camera ...There’s an efficient way to do this: allow D3cold for exactly one group at a time and forbid it everywhere else. Innocent groups just pass, leaving the machine alive to continue, so the group you’re on when it hangs is the culprit.
hunt_group() { label="$1"; shift for d in /sys/bus/pci/devices/*/d3cold_allowed; do echo 0 > "$d"; done for a in "$@"; do echo 1 > "/sys/bus/pci/devices/$a/d3cold_allowed"; done say ">>>>>> group [$label] only, D3cold allowed" echo platform > /sys/power/pm_test sync echo freeze > /sys/power/state && say "<<< passed"}
hunt_group "NVMe" 0000:00:1c.0 0000:01:00.0hunt_group "wireless" 0000:00:1d.0 0000:02:00.0hunt_group "camera" 0000:00:1d.1 0000:03:00.0It stopped on the very first group.
2026-07-28 10:51:02 >>>>>> group [NVMe] only, D3cold allowed (0000:00:1c.0 0000:01:00.0)2026-07-28 10:51:02 >>> running platform stage (condition: only:NVMe)(nothing after this)The culprit is the Apple S3X NVMe (01:00.0) and its PCIe root port (00:1c.0). After a forced reboot I checked the remaining three groups, and wireless, camera and everything else all passed.
<<< PASSED condition: only:wireless<<< PASSED condition: only:camera<<< PASSED condition: only:othersWhy It Looked Like an Authentication Error
This explains the original symptoms.
- The NVMe doesn’t come back on resume
- All disk I/O dies
- PAM can’t read
/etc/shadow, so the lock screen reports an authentication error - journald can’t write either, so the log cuts off at
PM: suspend entry - The lock screen is stuck, so the keyboard looks dead
The display (GNOME Shell) lives in memory, so it still draws. That’s why you get “the screen comes up but nothing works” — a symptom that looks exactly like an input device problem.
The Fix
Three pieces.
Switch to s2idle
Apple’s EFI was built for macOS, and there are plenty of reports that the S3 (deep) resume path doesn’t work under Linux. s2idle avoids the firmware path entirely.
[Sleep]MemorySleepMode=s2idleTo be clear, I added this early during the investigation and never went back to check whether deep works now that D3cold is fixed. s2idle works fine, so I left it.
Forbid D3cold on the NVMe Path
This is the actual fix. A udev rule applies it at boot.
ACTION=="add", SUBSYSTEM=="pci", KERNEL=="0000:00:1c.0", ATTR{d3cold_allowed}="0"ACTION=="add", SUBSYSTEM=="pci", KERNEL=="0000:01:00.0", ATTR{d3cold_allowed}="0"Apply with sudo udevadm control --reload and a reboot.
Belt and Braces, Plus Logging
In case udev misses it, a hook reapplies the setting right before every suspend. It also records each resume.
#!/bin/shLOG=/var/log/macbook-sleep.logNVME_PATH="0000:00:1c.0 0000:01:00.0"case "$1" in pre) echo s2idle > /sys/power/mem_sleep 2>/dev/null for a in $NVME_PATH; do echo 0 > "/sys/bus/pci/devices/$a/d3cold_allowed" 2>/dev/null done echo "$(date '+%F %T') pre op=$2" >> "$LOG"; sync ;; post) echo "$(date '+%F %T') post op=$2 resumed OK" >> "$LOG"; sync ;;esacDon’t forget chmod +x.
Results
Lid open/close now resumes without trouble.
2026-07-28 11:04:48 pre op=suspend2026-07-28 11:05:50 post op=suspend resumed OK2026-07-28 11:09:46 pre op=suspend2026-07-28 11:09:47 post op=suspend resumed OKPost-resume journal entries survive now, of course, and they show the kernel-side resume finishing in half a second.
11:05:50.071 kernel: Freezing user space processes11:05:50.077 kernel: nvme nvme0: 1/0/0 default/read/poll queues11:05:50.486 kernel: PM: suspend exit11:05:50.675 systemd[1]: Finished grub2-common.serviceThe cost is slightly higher power draw while suspended, since D3cold fully powers off the PCIe device and we’re stopping at D3hot instead.
What Didn’t Help
pm_trace
With CONFIG_PM_TRACE=y you get /sys/power/pm_trace. It writes a hash of each device callback during suspend/resume into the RTC, which survives a power cut, so the next boot tells you which device it hung on. That’s the idea, anyway.
I had high hopes. Here’s what I got:
PM: Magic number: 8:1:0PM: hash matches drivers/base/power/main.c:1988memory memory48: hash matchesmemory memory48 is a textbook hash-collision false positive, and /sys/power/pm_trace_dev_match only matched input and memory. The hash space is small enough that this happens easily, and when it does the tool is useless.
It also corrupts your hardware clock as a side effect — as the log above shows, the RTC got overwritten to 2024-01-01. Ubuntu 26.04 doesn’t ship hwclock by default, so you fix the clock with timedatectl after NTP resyncs.
no_console_suspend
The plan was to drop quiet splash, add no_console_suspend ignore_loglevel, and read the last line left on screen when it hung. But the noirq phase where it was actually hanging happens after the console is already down, so nothing appeared.
Also, leaving ignore_loglevel on renders every kernel message to the framebuffer, which makes resume noticeably slower. Take it off when you’re done debugging.
Still Open
The panel still takes a while to light up after resume. This is a separate issue though:
- The kernel-side resume completes in 0.5 seconds, with no gap in the journal
- So the system is running underneath and only the screen is dark
The only backlight device is acpi_video0, and the boot log has this line:
i915 0000:00:02.0: [drm] Skipping intel_backlight registrationACPI (LNXVIDEO:00) owns brightness control and i915’s native control is disabled. That fits with acpi LNXVIDEO:00: Restoring backlight state appearing on every resume.
I tried the obvious acpi_backlight=native and it didn’t improve things, so this one is unsolved. It does resume, which is good enough for now. I’ll update this post if I get anywhere.
Takeaways
- For anything that won’t resume, start by splitting the sequence with
/sys/power/pm_test:freezer→devices→platform. That alone shrinks the search space dramatically syncafter every log write, or you lose the evidence- If it hangs at
platform, suspect the noirq phase — PCI power transitions.d3cold_allowedis testable without rebooting - The “allow one group at a time” approach keeps reboots to a minimum, since innocent groups just pass
- Guessing the cause from the symptoms (authentication error, dead keyboard) gets you nowhere