MacBook 12-inch + Ubuntu 26.04 Won't Resume From Suspend


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

ItemValue
ModelMacBook10,1 (12-inch, 2017)
OSUbuntu 26.04 LTS
Kernel7.0.0-28-generic
SSDAPPLE SSD AP0512J (Apple S3X NVMe Controller, 106b:2003)
DesktopGNOME / 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.

Terminal window
lsmod | grep -E 'applespi|spi_pxa'
applespi 57344 0
spi_pxa2xx_platform 12288 0
spi_pxa2xx_core 32768 1 spi_pxa2xx_platform

Search 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.

/usr/lib/systemd/system-sleep/applespi-reload
#!/bin/sh
case "$1" in
pre) modprobe -r applespi ;;
post) modprobe applespi ;;
esac

It 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.

Terminal window
journalctl -b -1 --no-pager -n 3
Jul 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.

Terminal window
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.

Terminal window
cat /sys/power/pm_test
[none] core processors platform devices freezer

Work from the shallowest stage upward.

Terminal window
for stage in freezer devices platform processors core; do
echo "$stage" | sudo tee /sys/power/pm_test
echo freeze | sudo tee /sys/power/state
done

Here’s what happened.

<<< stage=freezer passed
<<< stage=devices passed
>>> stage=platform starting <- never returns

Two runs, identical result. That narrowed things down enormously.

What each stage covers:

StageCovers
freezerFreezing 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.

Terminal window
for d in /sys/bus/pci/devices/*/d3cold_allowed; do echo 0 | sudo tee "$d"; done

Running the platform stage in that state — it passed.

2026-07-28 10:43:08 === set d3cold_allowed=0 on 19 devices
2026-07-28 10:43:09 >>> running platform stage (condition: no-d3cold)
2026-07-28 10:43:17 <<< PASSED condition: no-d3cold

Confirmed: 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:

Terminal window
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.

Terminal window
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.0
hunt_group "wireless" 0000:00:1d.0 0000:02:00.0
hunt_group "camera" 0000:00:1d.1 0000:03:00.0

It 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:others

Why It Looked Like an Authentication Error

This explains the original symptoms.

  1. The NVMe doesn’t come back on resume
  2. All disk I/O dies
  3. PAM can’t read /etc/shadow, so the lock screen reports an authentication error
  4. journald can’t write either, so the log cuts off at PM: suspend entry
  5. 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.

/etc/systemd/sleep.conf.d/10-macbook10-s2idle.conf
[Sleep]
MemorySleepMode=s2idle

To 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.

/etc/udev/rules.d/60-macbook-no-d3cold.rules
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.

/usr/lib/systemd/system-sleep/macbook-no-d3cold
#!/bin/sh
LOG=/var/log/macbook-sleep.log
NVME_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
;;
esac

Don’t forget chmod +x.

Results

Lid open/close now resumes without trouble.

/var/log/macbook-sleep.log
2026-07-28 11:04:48 pre op=suspend
2026-07-28 11:05:50 post op=suspend resumed OK
2026-07-28 11:09:46 pre op=suspend
2026-07-28 11:09:47 post op=suspend resumed OK

Post-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 processes
11:05:50.077 kernel: nvme nvme0: 1/0/0 default/read/poll queues
11:05:50.486 kernel: PM: suspend exit
11:05:50.675 systemd[1]: Finished grub2-common.service

The 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:0
PM: hash matches drivers/base/power/main.c:1988
memory memory48: hash matches

memory 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 registration

ACPI (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: freezerdevicesplatform. That alone shrinks the search space dramatically
  • sync after every log write, or you lose the evidence
  • If it hangs at platform, suspect the noirq phase — PCI power transitions. d3cold_allowed is 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

References


Author

me

Fumito Iriya

Scientist (Ph.D.), Programer, Web Developer, Guitarist, Photographer

more...