This covers setting up hibernation on CachyOS with GNOME, GRUB, btrfs, full-disk encryption (LUKS unlocked at the GRUB prompt), and zram already in use. Verified on a Lenovo ThinkBook with GNOME 49.

Written for CachyOS, but most of this should apply to other Arch-based distributions. A few caveats: CachyOS ships with a systemd-based initrd and zram enabled by default — if your distribution uses a busybox-based initrd (i.e. base and udev hooks rather than systemd), you will need a resume hook in mkinitcpio instead of relying on systemd’s native resume handling. The efivarfs sleep hook in Step 5 is firmware-specific and not distribution-specific. Distributions that use GRUB with LUKS and btrfs in the same way as described here should be able to follow this guide without modification.

There are steps taken in this guide concerning efivars, that are very specific for the laptop it was written for (Lenovo Thinkpad T14 Gen 4)

Take a backup before proceeding. Editing bootloader configuration and the initramfs can leave the system unbootable if something goes wrong. At minimum, know how to boot from a live USB and chroot into your installation to roll back changes.

A few things that differ from what you might find on the Arch wiki:

  • The Arch wiki says to skip resume= kernel parameters when using systemd hooks. This is correct in principle, but certain Lenovo (and possibly other) machines panic on resume due to a bug in EFI runtime services triggered by efivarfs. The fix is a sleep hook that unmounts efivarfs before hibernation and remounts it after. The resume= and resume_offset= parameters are still worth adding as a reliable fallback.
  • zram coexists fine — no need to disable it or do anything special.
  • No changes to mkinitcpio HOOKS are needed. The systemd-based initrd handles resume natively.

Step 1: Create the btrfs swapfile

CachyOS uses zram for swap by default — there is no swap partition. You need a dedicated swapfile for hibernation. It must live in its own btrfs subvolume to avoid COW, compression, and snapshot problems.

Size rule: the swapfile must be at least RAM + zram combined. A swapfile that is too small causes silent failure — the machine boots fresh with no error.

Check your RAM and zram sizes first:

$ free -h | awk '/^Mem/ {print $2}'
$ swapon --show

Create a dedicated subvolume:

$ sudo btrfs subvolume create /swap

Create the swapfile (replace 64g with your size):

$ sudo btrfs filesystem mkswapfile --size 64g --uuid clear /swap/swapfile

Use btrfs filesystem mkswapfile rather than fallocate — on btrfs, fallocate does not disable COW or compression, both of which must be disabled along with checksums.

Verify the file is a single extent (required):

$ sudo filefrag /swap/swapfile
# Must report: 1 extent — if not, recreate the file

Step 2: Activate swap and add to fstab

$ sudo swapon /swap/swapfile
$ swapon --show  # both /swap/swapfile and /dev/zram0 should appear

zram stays as primary swap at priority 100. The swapfile at priority 0 only fills during hibernation — no performance impact during normal use.

Add to /etc/fstab:

/swap/swapfile  none  swap  defaults,pri=0  0  0

Step 3: Get UUID and resume_offset

The kernel needs the UUID of the btrfs filesystem and the physical offset of the swapfile. On btrfs you must use the btrfs-specific tool — filefrag gives virtual addresses and produces a wrong offset.

UUID of the filesystem holding the swapfile:

$ sudo findmnt -no UUID -T /swap/swapfile

With FDE via GRUB this is the UUID of the decrypted btrfs volume (e.g. dm-0), not the LUKS container UUID. They are different — always use the output of this command, not values from lsblk for the LUKS partition.

Physical offset of the swapfile:

$ sudo btrfs inspect-internal map-swapfile -r /swap/swapfile

Note both values down carefully. A single wrong character in either causes silent resume failure — the machine boots fresh with no error.

Step 4: Add kernel parameters to GRUB

Edit /etc/default/grub and append resume= and resume_offset= to GRUB_CMDLINE_LINUX_DEFAULT:

GRUB_CMDLINE_LINUX_DEFAULT="nowatchdog quiet splash loglevel=3 \
  resume=UUID=YOUR-BTRFS-UUID \
  resume_offset=YOUR-OFFSET"

Always use the UUID= form for resume= — device paths like /dev/dm-0 are not stable across reboots.

Regenerate grub.cfg:

$ sudo grub-mkconfig -o /boot/grub/grub.cfg

Verify the parameters are present:

$ grep resume /boot/grub/grub.cfg | head -3

Step 5: Add efivarfs sleep hook

Certain firmware implementations (confirmed on Lenovo, possibly others) have a bug where efivarfs triggers a page fault inside EFI runtime services during resume, causing a kernel panic. The fix is to unmount efivarfs before hibernation and remount it after resume.

This approach preserves full EFI variable functionality — unlike efi=noruntime — so suspend-then-hibernate works correctly.

Create /usr/lib/systemd/system-sleep/efivarfs-hibernate.sh:

#!/bin/bash
case "$1/$2" in
  pre/hibernate|pre/hybrid-sleep|pre/suspend-then-hibernate)
    umount /sys/firmware/efi/efivars
    ;;
  post/hibernate|post/hybrid-sleep|post/suspend-then-hibernate)
    mount -t efivarfs efivarfs /sys/firmware/efi/efivars
    ;;
esac
$ sudo chmod +x /usr/lib/systemd/system-sleep/efivarfs-hibernate.sh

If you still get a kernel panic after adding the hook, fall back to adding efi=noruntime to GRUB_CMDLINE_LINUX_DEFAULT and regenerating grub.cfg. Note that efi=noruntime disables EFI variable writes for the entire session, which breaks suspend-then-hibernate’s automatic transition.

Step 6: Regenerate initramfs

No changes to mkinitcpio.conf are needed. Rebuild to keep things consistent after the config changes:

$ sudo mkinitcpio -P

Your HOOKS should look like this — leave them untouched:

HOOKS=(base systemd autodetect microcode kms modconf block keyboard sd-vconsole plymouth sd-encrypt filesystems)

If you have a resume hook from a previous attempt, remove it — it is redundant with systemd hooks and can cause ordering conflicts.

Step 7: Configure sleep modes

By default systemd only allows sleep modes that are explicitly enabled. Uncomment the relevant lines in /etc/systemd/sleep.conf:

[Sleep]
AllowHibernation=yes
AllowSuspendThenHibernate=yes
AllowHybridSleep=yes
# Optional: delay before suspend-then-hibernate transitions to disk:
HibernateDelaySec=7200    # 2 hours — adjust to taste

GNOME extensions read these settings. If a sleep mode is not allowed here it will not appear in the power menu even if everything else is configured correctly.

Step 8: Expose hibernate in GNOME

GNOME hides hibernate and related options by default. Two things are needed: a polkit rule and a GNOME Shell extension.

Create /etc/polkit-1/rules.d/85-hibernate.rules:

polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.login1.hibernate" ||
        action.id == "org.freedesktop.login1.hibernate-multiple-sessions" ||
        action.id == "org.freedesktop.login1.handle-hibernate-key" ||
        action.id == "org.freedesktop.login1.hibernate-ignore-inhibit")
    {
        if (subject.isInGroup("users")) {
            return polkit.Result.YES;
        }
    }
});

For the GNOME Shell extension, check your shell version first: gnome-shell --version

  • Power off options — supports GNOME 45–49+, exposes hibernate, hybrid sleep, and suspend-then-hibernate. Recommended.
  • Hibernate Status Button — supports GNOME 45–48, hibernate only.

Install gnome-browser-connector if needed (sudo pacman -S gnome-browser-connector) and then install the extension from the GNOME Extensions website.

Step 9: Reboot and test

Clear any stale hibernation image before the first test:

$ sudo swapoff /swap/swapfile && sudo swapon /swap/swapfile
$ sudo reboot

After reboot, verify swap:

$ swapon --show
# Expected: /swap/swapfile (pri=0) and /dev/zram0 (pri=100)

Test hibernation:

$ sudo systemctl hibernate

The machine writes its image to disk and powers off. On power-on: LUKS passphrase → GRUB → session restored.

Verify in the kernel log after resume:

$ dmesg | grep -i 'resume\|hibernate\|swsusp'

What does NOT need to be done

  • No resume hook in mkinitcpio
  • No changes to HOOKS at all
  • No dedicated swap partition
  • No disabling or stopping zram; zram coexists fine at pri=100

Troubleshooting

Kernel panic on resume — CapsLock blinking

The call trace showed efi_call_rts or _pfx_efi_call_rts and a page fault. To see the full trace: at the GRUB menu press e, remove quiet and splash, set loglevel=7.

Primary fix: the efivarfs sleep hook from Step 5. Verify it is executable: ls -la /usr/lib/systemd/system-sleep/efivarfs-hibernate.sh

Fallback: add efi=noruntime to GRUB_CMDLINE_LINUX_DEFAULT and regenerate grub.cfg. A BIOS update does not resolve this on affected Lenovo hardware.

Machine boots fresh instead of resuming

  1. Wrong UUID or offset — re-run both commands from Step 3 and compare verbatim to /etc/default/grub. Verify with grep resume /boot/grub/grub.cfg.
  2. grub.cfg not regenerated — always run sudo grub-mkconfig -o /boot/grub/grub.cfg after editing /etc/default/grub.
  3. Stale image — if you changed parameters between hibernations, run sudo swapoff /swap/swapfile && sudo swapon /swap/swapfile before testing again.
  4. Swapfile too small — check swapon --show. If the RAM image exceeded swapfile capacity the kernel aborts silently. Check dmesg | grep -i 'swap\|hibern'.
  5. Multiple extents — sudo filefrag /swap/swapfile must say 1 extent. If not, recreate the swapfile.

Hibernate option missing from GNOME power menu

  1. sleep.conf — AllowHibernation=yes (and others as needed) must be uncommented.
  2. Extension version — Hibernate Status Button does not work with GNOME 49+. Use Power off options instead.
  3. Polkit rule — verify the file exists at /etc/polkit-1/rules.d/85-hibernate.rules. Check loginctl show-session | grep CanHibernate — should return yes.
  4. Swap not detected — cat /sys/power/state — the word disk must be present. If absent, swap is not active or is too small.

systemctl hibernate returns error or permission denied

Without sudo: check the polkit rule from Step 8 is in place. Verify: loginctl show-session | grep CanHibernate.

With sudo and still failing: cat /sys/power/state — disk must be present.

Journal: journalctl -b -1 | grep -iE 'hibern|swsusp|resume'

Black screen or corrupted display after resume

NVIDIA: enable preservation services and add the memory allocation parameter:

$ sudo systemctl enable nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service

Add nvidia.NVreg_PreserveVideoMemoryAllocations=1 to GRUB_CMDLINE_LINUX_DEFAULT and regenerate grub.cfg.

Intel/AMD: check dmesg | grep -i 'drm\|i915\|amdgpu' after resume. Common workarounds: i915.enable_psr=0 (Intel), amdgpu.vm_update_mode=3 (AMD).

Sources