Post

Validating CONFIG_CRASH_DM_CRYPT on x86_64

Validating CONFIG_CRASH_DM_CRYPT on x86_64

Validating CONFIG_CRASH_DM_CRYPT on x86_64: Encrypted kdump Without a Passphrase Prompt

Introduction

When a Linux system crashes, kdump boots a small second kernel — the crash kernel — into memory that was reserved ahead of time, and that second kernel writes a memory snapshot of the dying system (vmcore) to disk. This works cleanly when the dump target is a plain filesystem. It becomes a hard problem the moment the dump target sits on a LUKS-encrypted volume, because the crash kernel boots into a minimal, often unattended environment: there is no one to type a passphrase, a virtual keyboard may not exist, and even if a passphrase were available, LUKS2’s default Argon2 key-derivation function is deliberately memory-hard — re-deriving the volume key during a crash can require far more RAM than a typical crashkernel= reservation provides.

CONFIG_CRASH_DM_CRYPT solves this by having the first kernel do the expensive unlock once, while it is still healthy, and then hand the already-unlocked volume key to the crash kernel through a small, purpose-built channel: a kernel keyring, a configfs interface, and a reserved slice of crash memory. The crash kernel never runs Argon2 and never asks for a passphrase — it simply reuses the key that was proven to work moments before the panic.

This document is a complete, standalone record of validating that mechanism end-to-end on x86_64, using QEMU under TCG emulation on an Apple Silicon host. It assumes no prior context: every kernel option, every script, every file, and every keyring concept involved is explained from first principles, with the real command output from a passing run used as evidence throughout. The scope here is x86_64 only — aarch64 and ppc64le use a different mechanism (a device-tree property instead of a kernel command-line parameter) and are explicitly out of scope for this write-up.

Throughout this document, the two kernels involved are referred to consistently as:

  • the first kernel — the normal, running production kernel, healthy and in control until the moment of the panic.
  • the second kernel (crash) — the kdump-capture kernel, loaded by the first kernel ahead of time and jumped into only after a panic. This is what upstream documentation calls the “crash kernel” or “dump-capture kernel”; this document uses second kernel (crash) as the standard term to keep the two roles visually distinct.

Table of Contents


1. Big-Picture Architecture

The test runs entirely inside one QEMU virtual machine. The host is an arm64 Mac; because Apple’s hypervisor (HVF) cannot accelerate an x86_64 guest, QEMU falls back to TCG (pure software emulation), which is slower but fully correct for this purpose.

The guest has two virtio block devices:

  • /dev/vda — the root filesystem the first kernel boots from.
  • /dev/vdb — a raw, initially-empty disk that becomes the LUKS2-encrypted kdump target.

Both the first kernel and the second kernel (crash) are the same bzImage file. What differs is how and with what command line each boot happens: the first boot is a normal QEMU -kernel boot; the second boot is triggered internally, from inside the guest, by kexec.

flowchart TB
    classDef hostStyle fill:#1f2937,stroke:#111827,color:#f9fafb,stroke-width:2px
    classDef qemuStyle fill:#eef2ff,stroke:#4338ca,color:#1e1b4b,stroke-width:2px
    classDef diskStyle fill:#ecfdf5,stroke:#059669,color:#064e3b,stroke-width:2px
    classDef firstStyle fill:#eff6ff,stroke:#2563eb,color:#1e3a8a,stroke-width:2px
    classDef panicStyle fill:#fef2f2,stroke:#dc2626,color:#7f1d1d,stroke-width:3px
    classDef secondStyle fill:#fff7ed,stroke:#ea580c,color:#7c2d12,stroke-width:2px

    HOST["Host: Apple Silicon Mac, arm64"]:::hostStyle
    HOST --> QEMU["qemu-system-x86_64<br/>-accel tcg -machine q35<br/>software emulation"]:::qemuStyle

    subgraph GUEST [" QEMU guest machine, x86_64 "]
        direction TB
        VDA["Disk: /dev/vda<br/>rootfs-luks.raw<br/>first-kernel root filesystem"]:::diskStyle
        VDB["Disk: /dev/vdb<br/>kdump-vdb.raw<br/>LUKS2 dump target"]:::diskStyle

        subgraph FK [" First kernel — running production kernel "]
            SETUP["luks-kdump-test.sh<br/>format /dev/vdb as LUKS2<br/>link volume key as a logon key<br/>register key in configfs<br/>kexec -p -s, load second kernel"]:::firstStyle
        end

        PANIC(("panic()<br/>triggered by SysRq c")):::panicStyle

        subgraph SK [" Second kernel, crash — dump-capture kernel "]
            CINIT["kdump-init<br/>restore key as a user key<br/>unlock /dev/vdb, no passphrase<br/>mount ext4, dd /proc/vmcore"]:::secondStyle
        end
    end

    QEMU --> GUEST
    VDA --> FK
    FK -->|"1 — format + link key"| VDB
    FK --> PANIC
    PANIC --> SK
    SK -->|"2 — unlock + write vmcore"| VDB

The same bzImage, loaded twice, behaves as two different roles because of what each boot’s command line and configfs state tell it to do. Every arrow in this diagram points forward in time — /dev/vdb receives two separate, clearly numbered actions (format-and-link from the first kernel, then unlock-and-write from the second kernel) rather than being referenced back and forth. Everything else in this document explains the mechanics behind each arrow.

1.1 The Same Architecture, as a Step-by-Step Timeline

The diagram above is a structural view — it shows every component once, color-coded by which kernel owns it, so you can see at a glance how the pieces relate to each other. The diagram below shows exactly the same components (the host, QEMU, both virtio disks, both kernels) but rearranged as a single, strictly top-to-bottom timeline, with every step numbered in the order it actually happens on the wire. Where the diagram above answers “what talks to what,” this one answers “what happens, and when.”

flowchart TD
    classDef hostStyle fill:#1f2937,stroke:#111827,color:#f9fafb,stroke-width:2px
    classDef firstStyle fill:#eff6ff,stroke:#2563eb,color:#1e3a8a,stroke-width:2px
    classDef panicStyle fill:#fef2f2,stroke:#dc2626,color:#7f1d1d,stroke-width:3px
    classDef secondStyle fill:#fff7ed,stroke:#ea580c,color:#7c2d12,stroke-width:2px
    classDef resultStyle fill:#ecfdf5,stroke:#059669,color:#064e3b,stroke-width:2px

    HOST["Host: Apple Silicon Mac, arm64"]:::hostStyle
    HOST --> LAUNCH["Launch qemu-system-x86_64 -accel tcg -machine q35<br/>software emulation, since HVF cannot run an x86_64 guest"]:::hostStyle
    LAUNCH --> VM["QEMU virtual machine starts<br/>1 GiB RAM, two virtio disks attached:<br/>/dev/vda = rootfs-luks.raw  ·  /dev/vdb = kdump-vdb.raw, empty"]:::hostStyle

    VM --> BOOT1["Step 1 — First kernel boots<br/>bzImage, loaded by QEMU's -kernel flag<br/>root filesystem = /dev/vda"]:::firstStyle

    subgraph FIRST [" FIRST KERNEL — healthy, running production kernel "]
        direction TB
        BOOT1 --> S1["Step 2 — Format /dev/vdb as LUKS2<br/>cryptsetup luksFormat, Argon2id + passphrase"]:::firstStyle
        S1 --> S2["Step 3 — Unlock it and link the volume key<br/>cryptsetup open --link-vk-to-keyring<br/>creates a LOGON key in the user keyring"]:::firstStyle
        S2 --> S3["Step 4 — Register the key in configfs<br/>mkdir + description file under<br/>/sys/kernel/config/crash_dm_crypt_keys/&lt;uuid&gt;"]:::firstStyle
        S3 --> S4["Step 5 — Load the second kernel<br/>kexec -p -s bzImage --initrd=kdump.cpio<br/>kexec_file_load copies the key into<br/>crash-reserved memory, sets dmcryptkeys="]:::firstStyle
        S4 --> S5["Step 6 — Trigger the panic<br/>echo c > /proc/sysrq-trigger"]:::firstStyle
    end

    S5 --> PANIC(("panic()<br/>control jumps to the loaded image")):::panicStyle

    subgraph SECOND [" SECOND KERNEL, CRASH — the dump-capture kernel "]
        direction TB
        PANIC --> S6["Step 7 — Second kernel boots from crash-reserved memory<br/>command line already contains<br/>elfcorehdr=... dmcryptkeys=0x...<br/>root = kdump.cpio initramfs, not any disk"]:::secondStyle
        S6 --> S7["Step 8 — Restore the key<br/>echo yes > .../crash_dm_crypt_keys/restore<br/>creates a USER key with the same bytes"]:::secondStyle
        S7 --> S8["Step 9 — Unlock /dev/vdb, no passphrase<br/>cryptsetup open --volume-key-keyring<br/>digest check only, no Argon2"]:::secondStyle
        S8 --> S9["Step 10 — Mount the ext4 filesystem<br/>that was created inside that LUKS volume<br/>back in Step 2"]:::secondStyle
        S9 --> S10["Step 11 — Copy the dump<br/>dd if=/proc/vmcore of=/mnt/vmcore"]:::secondStyle
    end

    S10 --> RESULT["Result — encrypted vmcore<br/>sitting on /dev/vdb, readable only<br/>after the volume is unlocked again"]:::resultStyle

Reading it top to bottom: the host launches one QEMU machine with two disks; everything inside the first kernel box happens before the panic and touches /dev/vdb only to format and lock in the key; the panic is the single hand-off point; everything inside the second kernel (crash) box happens after the panic and touches /dev/vdb only to unlock it (using the key handed off through the panic) and write the dump. The two kernels never run at the same time, and /dev/vdb is the only thing both halves of the timeline share.


2. Key Concepts and Terminology

A handful of kernel and cryptsetup concepts recur throughout this test. Getting these precise up front avoids confusion later, especially because several of them share the word “key” or “user” while meaning very different things.

TermWhat it actually is
Volume keyThe raw symmetric key (for example, AES-XTS key material) that dm-crypt uses to encrypt and decrypt data on the block device. It is not the passphrase — the passphrase only unwraps the volume key from a LUKS keyslot.
Argon2 / Argon2idThe password-based key derivation function (PBKDF) LUKS2 uses by default to turn a human passphrase into something strong enough to unwrap the volume key. It is deliberately slow and deliberately memory-hard. It is not itself a key — see the dedicated question below.
Logon keyA kernel key type (key_type_logon) whose payload cannot be read back out by a userspace process via the keyctl/read() syscalls. Only privileged, in-kernel code paths can access its raw payload. This is the type the first kernel uses to hold the volume key.
User keyA different kernel key type ("user"), whose payload can be read by a userspace process that has permission. This is the type the second kernel (crash) creates when it restores the key — because its userspace cryptsetup process needs to read the raw bytes.
User keyring (@u)A keyring — a special kind of key that holds references to other keys — scoped to the calling UID (KEY_SPEC_USER_KEYRING, written as @u in keyctl syntax). Both the logon key (in the first kernel) and the user key (in the second kernel) are linked into this keyring. “User keyring” is a keyring, not a key type — do not confuse it with the “user” key type above, even though both use the word “user.”
configfsAn in-kernel, writable filesystem interface (distinct from sysfs) used here to register which logon keys the second kernel (crash) will need, at /sys/kernel/config/crash_dm_crypt_keys.
dmcryptkeys=An x86_64 kernel boot parameter, not a key itself — a pointer (physical address) telling the second kernel (crash) where to find the serialized key data inside the crash-reserved memory region.
kexec_file_loadThe kexec syscall path invoked by kexec -s. This is the only path that copies dm-crypt keys into crash memory. The legacy kexec_load path (no -s) does not do this.

3. The Key Lifecycle, End to End

This is the heart of the feature: how a volume key goes from “unlocked by a passphrase in the first kernel” to “available to cryptsetup in the second kernel (crash), with no passphrase and no Argon2.”

sequenceDiagram
    participant User as First-kernel script
    participant CS1 as cryptsetup (first kernel)
    participant KR1 as First-kernel keyring (@u)
    participant CFG as configfs<br/>crash_dm_crypt_keys
    participant K1 as First kernel (kexec_file_load)
    participant MEM as Crash-reserved memory
    participant K2 as Second kernel (crash) boot
    participant CFG2 as configfs<br/>restore attribute
    participant KR2 as Second-kernel keyring (@u)
    participant CS2 as cryptsetup (second kernel)
    participant DM as dm-crypt mapping

    rect rgb(239, 246, 255)
    Note over User,DM: First kernel — healthy, running production kernel
    User->>CS1: cryptsetup luksFormat (Argon2id, passphrase)
    User->>CS1: cryptsetup open --key-file ... --link-vk-to-keyring "@u::%logon:cryptsetup:<uuid>"
    CS1->>CS1: Unwrap volume key via PBKDF (Argon2id)
    CS1->>KR1: Create LOGON key "cryptsetup:<uuid>" with raw volume key
    CS1->>DM: Activate dm-crypt mapping (kdump_luks)
    User->>CFG: mkdir crash_dm_crypt_keys/<uuid>; echo "cryptsetup:<uuid>" > description
    User->>K1: kexec -p -s bzImage (kexec_file_load)
    K1->>CFG: Read registered key descriptions
    K1->>KR1: request_key(logon, "cryptsetup:<uuid>")
    K1->>K1: Copy raw key bytes into keys_header struct
    K1->>MEM: kexec_add_buffer() places keys_header at random address
    K1->>K1: Append "dmcryptkeys=0x<addr>" to crash kernel cmdline
    end

    rect rgb(254, 242, 242)
    Note over User,DM: Panic — control hands off to the second kernel
    User->>User: echo c > /proc/sysrq-trigger
    end

    rect rgb(255, 247, 237)
    Note over User,DM: Second kernel, crash — dump-capture kernel
    K2->>K2: Boot with elfcorehdr= and dmcryptkeys=0x<addr>
    K2->>K2: early_param "dmcryptkeys" stores addr
    User->>CFG2: mount configfs; echo yes > restore
    CFG2->>MEM: dm_crypt_keys_read() from reserved address
    CFG2->>KR2: Create USER key "cryptsetup:<uuid>" with same raw bytes
    User->>CS2: cryptsetup open --volume-key-keyring "%user:cryptsetup:<uuid>"
    CS2->>KR2: Read USER key payload directly (no passphrase)
    CS2->>CS2: Verify key digest against LUKS2 header (no KDF)
    CS2->>DM: Activate dm-crypt mapping (kdump_luks)
    User->>DM: dd if=/proc/vmcore of=/mnt/vmcore
    end

The two halves of this diagram map directly onto the two kernels: everything above the panic line happens in the first kernel; everything below happens in the second kernel (crash). Note that the volume key’s raw bytes cross the panic boundary exactly once, inside the opaque keys_header blob sitting in crash-reserved memory — never through a file, never through a passphrase, and never through a network.


4. Deep-Dive Questions and Answers

The following questions came up directly while working through this test. Each is answered in depth, grounded in the kernel source (kernel/crash_dump_dm_crypt.c, arch/x86/kernel/kexec-bzimage64.c) and in the actual output captured from the passing run.

What is Argon2 (an “Argon2 key”), and what is it used for?

“Argon2 key” is a convenient shorthand, but it is worth being precise about what Argon2 actually is, because it is not a key at all — it is a key derivation function (KDF): an algorithm that takes a low-entropy input, like a human-chosen passphrase, and turns it into a cryptographically strong output, deliberately slowly. Argon2 won the Password Hashing Competition in 2015, and LUKS2 uses its hybrid variant, Argon2id, as the default PBKDF (password-based KDF) for new volumes.

The property that matters most for this entire feature is that Argon2 is memory-hard: computing it requires a configurable, often large, amount of RAM — not just CPU time. This is a deliberate defense against brute-force attacks: an attacker trying millions of candidate passphrases on GPUs or custom ASICs is bottlenecked by how much fast memory they can afford per parallel guess, not by raw compute, which is cheap and highly parallelizable. The downside of that same property is the one this document keeps returning to: whichever kernel actually has to run Argon2 needs a correspondingly large amount of free RAM available to it at that moment.

Where Argon2 sits in the unlock chain — and why “the Argon2 key” is a slightly loose way to describe it — is that there are really three distinct secrets layered on top of each other, and Argon2 only ever touches the outermost one:

1
2
3
passphrase  --[ Argon2id, using this keyslot's stored salt/memory/iterations ]-->  key-encryption key (KEK)
KEK         --[ decrypts / unwraps ]-->  volume key  (stored, encrypted, inside the LUKS2 keyslot)
volume key  --[ used directly by ]-->  dm-crypt, for AES-XTS data encryption

Each LUKS2 keyslot stores Argon2id’s parameters (a random salt, a memory cost, a time cost, and a parallelism degree) in the header — never a key. When a passphrase is supplied, cryptsetup runs Argon2id over that passphrase using those stored parameters to produce the KEK, then uses the KEK to decrypt the real volume key sitting in that keyslot. The volume key — not anything Argon2 itself outputs — is what ends up inside the logon key described above, and what dm-crypt actually uses to read and write encrypted blocks.

Where this test actually invokes Argon2 is in exactly one place: the first-kernel setup script, during luksFormat:

1
2
3
4
/sbin/cryptsetup luksFormat --type luks2 --batch-mode --use-urandom \
    --pbkdf argon2id --pbkdf-memory 32768 --pbkdf-parallel 1 \
    --pbkdf-force-iterations 4 \
    --key-file /root/kdump-luks.key /dev/vdb
  • --pbkdf argon2id selects LUKS2’s own default variant explicitly.
  • --pbkdf-memory 32768 caps the memory cost at 32 MiB, instead of the roughly 1 GiB a default LUKS2 keyslot would otherwise request.
  • --pbkdf-force-iterations 4 fixes the time cost at 4 passes rather than letting cryptsetup auto-tune against its usual ~2-second target.
  • --pbkdf-parallel 1 uses a single thread.

These flags exist purely to make the one-time, first-kernel luksFormat step finish quickly under QEMU’s software-emulated TCG — they make repeated test runs practical. They do not weaken what is actually being validated, because the property under test is not “how expensive is the KDF” — it is “does the second kernel (crash) ever have to run the KDF at all.” With these reduced parameters or with LUKS2’s full ~1 GiB defaults, the second kernel in this test invokes Argon2 zero times either way: as the “Why is --volume-key-keyring used?” answer above shows, the restored key goes straight into the dm-crypt mapping via a cheap digest comparison, with no PBKDF call whatsoever.

This also explains a detail mentioned elsewhere in this project’s documentation: production crash-kernel sizing guidance sometimes calls out an extra “~1.3 GiB for LUKS” on top of a normal crashkernel= reservation. That extra memory is only needed when CONFIG_CRASH_DM_CRYPT is not in effect and the second kernel is forced to re-run the real Argon2id pass itself, at the keyslot’s full memory cost, to re-derive the volume key from a passphrase it has no way to collect. Avoiding that exact cost — not making Argon2 itself any weaker — is the entire reason this feature exists.

What is the role of luksUUID, and what is LUKS_UUID?

cryptsetup luksUUID /dev/vdb is a read-only command that prints the UUID stored inside the LUKS2 header of that device — a unique identifier generated once, when the volume was formatted, and baked permanently into the header’s metadata. It requires no key and no passphrase; it is pure metadata.

LUKS_UUID (or UUID in the scripts) is simply the shell variable that captures that string, for example:

1
UUID=$(/sbin/cryptsetup luksUUID /dev/vdb)

Its importance in this test is as the shared naming anchor between the two kernels. Both the configfs key description (cryptsetup:<uuid>) and the keyring key name must match exactly between the first kernel (which creates and registers the logon key) and the second kernel (which requests and restores it). The UUID is what guarantees that the key the first kernel linked is the same key the second kernel asks for, even though the two kernels never share any other runtime state. In the captured run this was:

1
LUKS_UUID=228c9d1a-ba95-4def-b36e-767ff075df9e

What is the role of dmcryptkeys?

dmcryptkeys= is a kernel boot parameter, present only on x86_64, appended automatically to the second kernel’s command line by the first kernel’s kexec_file_load() implementation (arch/x86/kernel/kexec-bzimage64.c, function setup_cmdline()). Its value is a physical memory address:

1
dmcryptkeys=0x34781000

That address points into the crashkernel= reserved region and marks where the first kernel placed the serialized key blob (a keys_header structure containing the key count, each key’s description, and each key’s raw bytes). The second kernel’s early boot code (setup_dmcryptkeys(), registered via early_param("dmcryptkeys", ...)) parses this value into the kernel variable dm_crypt_keys_addr before any userspace runs. Later, when userspace writes yes to the restore configfs attribute, the kernel uses that stored address to read the blob back out of reserved memory.

It is worth being precise that dmcryptkeys= is not a key — it carries no secret material itself. It is a pointer. The actual secret bytes never appear in any command line, any log, or /proc/cmdline — only their location does, and that location is inside memory the first kernel itself cannot map (it is marked not-present on x86 specifically to keep a compromised or inspected first kernel from reading it back out).

Why is --volume-key-keyring used?

In the second kernel (crash), cryptsetup needs the volume key to activate the dm-crypt mapping, but it has no passphrase, no TPM-unsealing session, and — critically — not nearly enough free RAM in a 256 MB crashkernel= reservation to run Argon2id at its normal memory cost (which can be hundreds of megabytes to over a gigabyte).

--volume-key-keyring <key description> tells cryptsetup to skip the entire passphrase-and-PBKDF path and instead read the volume key directly out of a kernel keyring key by name:

1
cryptsetup --batch-mode open --volume-key-keyring "%user:cryptsetup:${UUID}" /dev/vdb kdump_luks

cryptsetup still performs one check — it verifies the supplied key’s digest against the digest stored in the LUKS2 header, confirming this is genuinely the correct volume key for this device — but that digest check is a cheap hash comparison, not a memory-hard KDF. Once verified, cryptsetup activates the dm-crypt mapping directly with those raw bytes. This is the single mechanism that makes the entire feature possible: it converts “unlock a LUKS volume” from an expensive, interactive operation into a cheap, deterministic keyring lookup.

Why is kdump_luks used?

kdump_luks is simply the device-mapper name chosen for the activated mapping — the string that becomes /dev/mapper/kdump_luks once cryptsetup open succeeds:

1
cryptsetup open ... /dev/vdb kdump_luks

There is nothing special baked into the kernel about this particular string; any valid device-mapper name would work mechanically. It is used here purely for consistency and clarity: the same name is reused by both the first-kernel setup script and the second-kernel (crash) restore script, so every step after cryptsetup open — mke2fs, mount, and the final dd — can refer to a fixed, predictable path (/dev/mapper/kdump_luks) rather than having to rediscover it.

They are related only in the sense that one is written into the other — they are not the same mechanism and do not depend on each other to exist.

  • /proc/vmcore is a virtual file exposed by the running second kernel (crash) itself, made possible by CONFIG_CRASH_DUMP. It is a live view, constructed from the first kernel’s physical memory, formatted as an ELF core file (using the elfcorehdr= region the first kernel also set up). It exists the moment the second kernel boots, regardless of where — or whether — you choose to save it.
  • /dev/mapper/kdump_luks is the decrypted block device backing the encrypted disk /dev/vdb, with an ext4 filesystem mounted from it at /mnt.

The test’s actual dump step is nothing more than copying one into the other:

1
dd if=/proc/vmcore of=/mnt/vmcore bs=1048576

In other words: /proc/vmcore is the data (the crash dump itself); kdump_luks is the encrypted destination it gets written to. CONFIG_CRASH_DM_CRYPT only concerns itself with getting kdump_luks unlockable without a passphrase — /proc/vmcore’s existence is entirely a separate, standard kdump mechanism.

What is kdump.cpio used for, and what does it contain?

kdump.cpio is the initramfs for the second kernel (crash) — the minimal root filesystem it boots with, built with the cpio “newc” archive format and passed to kexec via --initrd=:

1
/usr/bin/kexec -p -s /boot/bzImage --initrd=/boot/kdump.cpio --append="... rdinit=/init" ...

Because rdinit=/init is set, the second kernel runs /init from this archive as its very first userspace process, instead of pivoting to any real root filesystem. Its contents are deliberately minimal and deliberately exclude any secret material:

Path inside kdump.cpioPurpose
/initThe crash-time logic (kdump-init): restore the key, open LUKS, mount ext4, dd /proc/vmcore, power off.
/bin/busybox (+ applet symlinks)Minimal shell, mount, dd, sync, wc, cmp, poweroff, etc.
/sbin/cryptsetupStatically-resolvable cryptsetup 2.7.5 binary (dynamically linked against musl).
/usr/lib/libcryptsetup.so.12, /lib/ld-musl-x86_64.so.1Shared libraries cryptsetup needs at runtime.
/usr/lib/cryptsetup/LUKS2 token-plugin directory (copied for completeness; unused by this test).

Notably absent: there is no passphrase file anywhere in this archive. The second kernel (crash) has no way to unlock the volume by brute-force or by any human-provided secret — it is architecturally restricted to only the keyring-restore path.

Why are kdump-vdb.raw, rootfs-luks.raw, and rootfs.raw used?

These three disk images serve three distinct, non-overlapping purposes:

ImageGuest deviceRole
rootfs.raw/dev/vda (in the plain run-qemu-vanilla-x86.sh boot)The original, pre-existing vanilla busybox root filesystem used for basic kexec sanity checks. It has no cryptsetup and is not used by this LUKS test at all — kept untouched as a baseline.
rootfs-luks.raw/dev/vdaThe first kernel’s root filesystem for this test: busybox, cryptsetup, mke2fs, keyctl, the static kexec, the setup script luks-kdump-test.sh, and — embedded at /boot/ — a copy of bzImage and kdump.cpio, ready for kexec to load.
kdump-vdb.raw/dev/vdbA raw, sparse, initially empty disk. It starts with no filesystem and no LUKS header at all; the first-kernel script formats it with cryptsetup luksFormat at runtime. This is the actual dump target — the disk that ends up holding the encrypted vmcore.

The reason kdump-vdb.raw must be a real, persistent block device rather than a loop-mounted file deserves emphasis, because it is the single most common way to get a false negative in this kind of test: a loop device (losetup) is a mapping that exists only inside the kernel that created it. When the first kernel hands off to the second kernel (crash) via kexec, that loop mapping does not carry over — the second kernel has no idea the loop device ever existed. A raw virtio disk, by contrast, is a property of the QEMU machine, visible identically to whichever kernel happens to be running at the time. That is why /dev/vdb here is a second virtio drive, not a loop file inside /dev/vda.

What is a logon key?

A logon key is a Linux kernel key type (key_type_logon, defined in security/keys/), purpose-built for holding credentials that kernel-internal consumers need but that userspace processes must never be able to read back out directly. Concretely, its read() method is restricted such that even the keyctl_read() syscall — the normal way a process inspects a key’s payload — returns an error for a logon-type key.

This matters enormously for the design of this feature: when cryptsetup --link-vk-to-keyring "@u::%logon:cryptsetup:<uuid>" creates the volume key as a logon key, the raw key bytes become invisible to every ordinary process on the first kernel, including root processes casually poking around with keyctl show or keyctl print. keyctl show @u will happily show that the key exists (its description, its serial number), but nothing short of in-kernel code can extract its payload.

That restriction is exactly what is bypassed, deliberately and only once, by the kernel’s own kexec_file_load() path: the function read_key_from_user_keyring() in kernel/crash_dump_dm_crypt.c calls request_key(&key_type_logon, description, NULL) and then reads the payload via the internal user_key_payload_locked() accessor — code running as part of the kexec_file_load syscall itself, inside the kernel, not as an ordinary userspace read. The restriction is a syscall-boundary restriction, not a kernel-internal one, so this in-kernel code path can still legitimately access the bytes to copy them into the crash-reserved key blob.

Is the second kernel loaded automatically by the first kernel, or does something have to trigger it?

This question hides two completely different steps inside the single word “load,” and only one of those two steps is actually automatic.

Step A — placing the crash image into memory — is not triggered by anything. It is a deliberate, proactive action that must be performed in advance, while the first kernel is still healthy, with no crash in sight. In this test, the setup script does it explicitly, as its own line, well before anything goes wrong:

1
2
3
/usr/bin/kexec -p -s /boot/bzImage \
    --initrd=/boot/kdump.cpio \
    --append="console=ttyS0 nokaslr irqpoll nr_cpus=1 reset_devices rdinit=/init"

Nothing inside the running kernel decides on its own to run this. If it is never run, the kernel’s internal pointer to “the image to jump to on panic” (kexec_crash_image) simply stays NULL. A later panic then behaves like an ordinary, un-instrumented panic: no second kernel boots, no vmcore is produced, and whatever generic panic policy is configured (reboot, halt, hang) takes over instead.

On a real production system, this same step still has to happen — it is just performed for you, automatically from the administrator’s point of view, by the kdump systemd service, which runs the equivalent of kexec -p -s once at boot (and again whenever kdumpctl restart or kdumpctl reload runs). That is a convenience of the init system, not a behavior of the kernel — it is still userspace, proactively loading the image ahead of any crash, exactly like the manual kexec -p -s line in this test.

Step B — jumping into that already-loaded image once a panic actually happens — is fully automatic, and this part genuinely is the kernel acting entirely on its own. panic() (in kernel/panic.c) synchronously calls into the kexec-on-panic path — crash_kexec() → machine_crash_shutdown() → machine_kexec() — and, if an image was loaded by Step A, jumps straight into it. No userspace process runs kexec a second time; no daemon makes a decision; nothing needs to be “listening” for the crash. In this test, that is exactly what echo c > /proc/sysrq-trigger demonstrates: that single line forces the panic, and the second kernel’s boot messages appear on the serial console immediately afterward, with no further command issued by anything.

StepPerformed byWhen it happensAutomatic?
Load the crash image (kexec -p -s)This test’s setup script; kdump.service in productionAny time before a panic, while the first kernel is healthyNo — must be explicitly triggered once, ahead of time
Jump into the crash imageThe kernel itself, inside panic()The instant panic() runsYes — fully automatic, zero userspace action

So, directly: the first kernel does not load the second kernel “automatically” in response to anything happening — loading is a one-time, proactive setup step that has to be completed beforehand (by hand in this test, by kdump.service in production). What is automatic, requiring nothing further from anyone, is the kernel’s own handling of the actual hand-off from a live panic into that already-loaded second kernel.

One single cryptsetup invocation does both things at once:

1
2
3
4
5
cryptsetup --batch-mode open \
  --key-file /root/kdump-luks.key \
  --disable-external-tokens \
  --link-vk-to-keyring "@u::%logon:cryptsetup:${UUID}" \
  /dev/vdb kdump_luks

Step by step:

  1. cryptsetup reads the passphrase from --key-file and reads the LUKS2 header from /dev/vdb to find which keyslot that passphrase can unlock.
  2. It runs the keyslot’s PBKDF — in this test, Argon2id — to derive a key-encryption key from the passphrase, then uses that to decrypt (unwrap) the actual volume key stored in the keyslot.
  3. It activates the dm-crypt mapping /dev/mapper/kdump_luks using that volume key, via the standard device-mapper ioctl path.
  4. Because --link-vk-to-keyring "@u::%logon:cryptsetup:<uuid>" was supplied, cryptsetup also creates a logon-type key named cryptsetup:<uuid>, containing the raw volume key bytes, and links it into the per-UID user keyring (@u) — so the key persists in the kernel’s keyring subsystem even after the cryptsetup process itself exits.

From this point forward, the volume key exists in two places simultaneously: active inside the dm-crypt mapping’s in-kernel state, and parked, inaccessible to userspace, inside the logon key — waiting for kexec_file_load to harvest it.

On panic, how does the second kernel (crash) restore that key and open the disk with no passphrase and no second Argon2 run?

This is the payoff of everything set up beforehand, and it happens in four stages once the panic fires:

  1. Boot with the pointer already in hand. The second kernel (crash) boots with elfcorehdr=0x2f000000 dmcryptkeys=0x34781000 on its command line (the exact values from the captured run). The early-boot parameter handler stores that address before any driver or filesystem code runs.

  2. Explicit restore trigger. The minimal init script mounts configfs and writes:

    1
    
    echo yes > /sys/kernel/config/crash_dm_crypt_keys/restore
    

    This write calls restore_dm_crypt_keys_to_thread_keyring(), which reads the keys_header blob directly out of the crash-reserved memory at the dmcryptkeys= address (using dm_crypt_keys_read(), which — on confidential-computing platforms — correctly routes through the “old memory” accessor so it still works even when the first kernel’s RAM is memory-encrypted). For every key in that blob, it calls add_key_to_keyring(), which creates a user-type key (not logon — this is the crucial type change) with the identical description, and links it into the second kernel’s own user keyring. The captured run confirms this succeeded: RESTORE=1.

  3. No-passphrase unlock. The script reads the device’s UUID (a header-only operation, needs no key) and then runs:

    1
    
    cryptsetup --batch-mode open --volume-key-keyring "%user:cryptsetup:<uuid>" /dev/vdb kdump_luks
    

    Because the restored key is type user, not logon, cryptsetup running as an ordinary userspace process can read its payload. It verifies the key’s digest against the LUKS2 header (a fast hash check, not a KDF) and activates the dm-crypt mapping directly.

  4. No second Argon2 run. Because the raw key bytes were supplied directly, cryptsetup never touches the keyslot’s Argon2id parameters at all in this second kernel. The 256 MB crash reservation never needs to hold the roughly 1 GB an Argon2id pass can otherwise require — confirming the central promise of CONFIG_CRASH_DM_CRYPT.

Advanced note — key reuse across a reload. If userspace reloads the crash image after a CPU or memory hotplug event, the kernel can skip re-deriving the key blob from scratch. Writing true to crash_dm_crypt_keys/reuse makes crash_load_dm_crypt_keys() call get_keys_from_kdump_reserved_memory() instead of rebuilding the blob: it temporarily calls arch_kexec_unprotect_crashkres() to make the already-reserved key page readable, copies it, and re-protects the page afterward. This test does not exercise that path — it builds a fresh key blob on every kexec -p -s — but it is the same underlying mechanism applied to a reload instead of a first load.

What is the difference between dmcryptkeys, logon keys, and the user keyring?

These three terms are frequently conflated because they are all part of the same handoff, but each one is a different kind of thing:

TermKind of thingLives inHolds secret bytes?
dmcryptkeys=A kernel boot parameter (a string on the command line)The second kernel’s /proc/cmdlineNo — it is only a physical memory address, a pointer to where the secret bytes are stored.
Logon keyA kernel key type (key_type_logon)The first kernel’s user keyring (@u)Yes — holds the raw volume key, but cannot be read back out by any ordinary userspace syscall.
User keyA different kernel key type ("user")The second kernel’s user keyring (@u)Yes — holds the same raw volume key bytes after restore, and can be read by userspace (which is exactly what lets cryptsetup --volume-key-keyring work).
User keyring (@u)A keyring (a container key that links to other keys), per-UIDBoth kernels, independentlyNo — it is a collection; the keys linked inside it (logon or user type) are what hold the bytes.

Put simply: dmcryptkeys= tells the second kernel where to look; the logon key is how the secret is hidden while it still lives in the first kernel; the user key is how the secret becomes usable again once it is safely inside the second kernel; and the user keyring is just the shelf both of those key types sit on, in their respective kernels.


5. Kernel Configuration Requirements

build-vanilla-x86-kernel.sh enables the first four options below on every build and refuses to continue unless each one lands as =y in the resulting .config. The remainder were already present from the baseline vanilla x86_64 defconfig.

Kernel optionWhat it does in this test
CONFIG_CRASH_DM_CRYPT=yRegisters the /sys/kernel/config/crash_dm_crypt_keys configfs subsystem. Drives the entire key-harvest-and-restore mechanism described in Section 3.
CONFIG_DM_CRYPT=yBuilds the device-mapper crypt target directly into the kernel (not as a module), so the second kernel (crash) — which has no module-loading infrastructure in this minimal initramfs — can still create /dev/mapper/kdump_luks itself.
CONFIG_CONFIGFS_FS=yConfigfs must be built-in, not a module, because the key-registration interface needs to exist from very early boot — this is itself a requirement enforced by the CRASH_DM_CRYPT_CONFIGS Kconfig helper.
CONFIG_CRYPTO_XTS=yLUKS2’s default cipher mode is aes-xts-plain64. dm-crypt’s Kconfig selects CBC and ESSIV automatically, but not XTS — without this, cryptsetup open fails even with a perfectly valid key.
CONFIG_KEXEC_FILE=yEnables kexec_file_load(), the only syscall path that calls crash_load_dm_crypt_keys(). The legacy kexec_load() path never copies keys.
CONFIG_KEXEC=yThe basic kexec infrastructure that lets a panic jump execution into a pre-loaded second kernel.
CONFIG_CRASH_DUMP=yExposes /proc/vmcore in the second kernel (crash).
CONFIG_CRASH_RESERVE=yHonors crashkernel= on the command line and reserves that physical memory so the first kernel’s own allocators never touch it. The key blob lives inside this reservation.
CONFIG_RELOCATABLE=yLets the same bzImage boot correctly whether it lands at its normal load address (first kernel) or at the reserved crash address (second kernel).
CONFIG_KEYS=yThe kernel keyring subsystem itself — without it, there is no logon key, no user key, and no keyring to link either into.
CONFIG_KEXEC_SIG (unset)This bzImage is unsigned; enforcing kexec image signatures would reject kexec -s outright.
CONFIG_VIRTIO, CONFIG_VIRTIO_PCI, CONFIG_VIRTIO_BLK=yBoth /dev/vda and /dev/vdb are virtio-blk devices. Built-in (not modules) so the second kernel sees them immediately with no module load step.
CONFIG_EXT4_FS=yFilesystem for both the root disk and the filesystem created inside the LUKS mapping.
CONFIG_BLK_DEV_DM=yDevice-mapper core, which dm-crypt sits on top of.
CONFIG_DEVTMPFS=y, CONFIG_DEVTMPFS_MOUNT=yThe kernel itself populates /dev/vda and /dev/vdb. The guest’s /init script must avoid mounting a tmpfs over /dev afterward, or those device nodes vanish.
CONFIG_SERIAL_8250=y, CONFIG_SERIAL_8250_CONSOLE=ySerial console, the only observation point for the panic and the second kernel’s boot.
CONFIG_MAGIC_SYSRQ=y (default enable 0x1)Allows echo c > /proc/sysrq-trigger to force the panic that starts the whole sequence.
CONFIG_CRYPTO_AES=y, CONFIG_CRYPTO_SHA256=yAES is the cipher; SHA-256 backs the LUKS2 header’s digest checks. (Argon2 itself runs in userspace, inside cryptsetup, via libargon2 — only in the first kernel.)
Debuginfo options disabledKeeps the cross-build fast and small. crash(8) analysis is intentionally out of scope for this pass/fail bar.

A subtlety worth calling out: CONFIG_CRASH_DM_CRYPT depends on KEXEC_FILE, CRASH_DUMP, DM_CRYPT, and KEYS all being satisfied simultaneously. Enabling it before DM_CRYPT is enabled causes make olddefconfig to silently turn it back off. The build script therefore enables DM_CRYPT, CRYPTO_XTS, and CONFIGFS_FS in the same scripts/config invocation as CRASH_DM_CRYPT, then runs olddefconfig, then verifies all four landed as =y before compiling.


6. QEMU and Memory Requirements

RequirementWhat it provides
qemu-system-x86_64, -machine q35, -accel tcg, -cpu maxBoots the x86_64 image under software emulation on the arm64 host.
-m 10241 GiB of guest RAM total.
crashkernel=256M on the first kernel’s command lineReserves 256 MiB of physical memory up front. In the captured run this landed at 0x2f000000–0x3f000000. /sys/kernel/kexec_crash_size must read non-zero before attempting kexec -p — a zero value means the reservation never happened, and there is no memory for either the second kernel or the key blob.
First virtio drive, raw, rootfs-luks.raw/dev/vda, mounted as the first kernel’s /.
Second virtio drive, raw, kdump-vdb.raw/dev/vdb. Starts as an empty 1536 MiB sparse file; formatted as LUKS2 entirely at runtime by the guest script.
-device virtio-rng-pciSupplies entropy so luksFormat --use-urandom does not stall waiting on /dev/random.
-nographic, console=ttyS0The serial stream is the only record of the panic and the second kernel’s boot; it is captured verbatim into a log file by the test driver.
-no-rebootEnsures a reboot request exits QEMU cleanly rather than re-entering the first kernel and interleaving two boots in one log.
nokaslrFixed virtual addresses, purely to make the serial log easier to read and diff between runs.

Why 256 MiB is enough here, when some product kernels reserve far more for exactly this scenario: that larger reservation exists to cover the case where the second kernel has to re-run Argon2id from scratch (hundreds of megabytes to over a gigabyte, depending on PBKDF parameters). Because this feature reuses the already-unlocked key, the second kernel never runs that KDF at all — 256 MiB only needs to cover the kernel image, the initramfs, and ordinary kdump bookkeeping.


7. Userspace Tooling: First Kernel vs. Second Kernel (Crash)

Both kernels need cryptsetup, but not the same capabilities from it, and the second kernel (crash) deliberately carries less. The table below makes that split explicit.

Tool / filePresent in first kernel (rootfs-luks.raw)Present in second kernel, crash (kdump.cpio)Why
cryptsetup (2.7+)YesYesBoth kernels invoke it, but for different operations — luksFormat / open --link-vk-to-keyring in the first; open --volume-key-keyring in the second.
libcryptsetup.so.12, musl ld.so, LUKS2 token-plugin dirYesYesRuntime dependencies of the cryptsetup binary in both environments.
mke2fsYesNoThe filesystem inside the LUKS volume only needs to be created once, by the first kernel, before the panic. The second kernel only ever mounts it.
keyctlOptionalNoUsed only to print the first kernel’s keyring state for diagnostic logging (keyctl show @u). cryptsetup talks to the keyring subsystem directly and never needs this binary at runtime.
Passphrase file (/root/kdump-luks.key)YesNever presentThe first kernel needs the passphrase to perform the one-time luksFormat / unwrap. The second kernel must be architecturally incapable of unlocking the volume by any means other than the restored keyring key — so this file is never copied into kdump.cpio.
kexec binaryYesNoOnly the first kernel loads a crash image; the second kernel, already running as the loaded image, has no further kexec step to perform.
/init logicluks-kdump-test.sh (setup, format, link, register, load, panic)kdump-init (restore, open, mount, dump, power off)Each kernel’s init script performs exactly the half of the lifecycle that belongs to its role.
Boots fromPersistent root filesystem on /dev/vdaVolatile cpio initramfs, no pivot to any disk-backed rootThe second kernel’s entire environment disappears at the next power-off — nothing it does leaves residue beyond what it explicitly writes to /mnt/vmcore.

8. Disk and Image Artifacts

flowchart LR
    classDef buildStyle fill:#eef2ff,stroke:#4338ca,color:#1e1b4b,stroke-width:2px
    classDef diskStyle fill:#ecfdf5,stroke:#059669,color:#064e3b,stroke-width:2px
    classDef unusedStyle fill:#f3f4f6,stroke:#6b7280,color:#374151,stroke-width:2px
    classDef actionStyle fill:#fff7ed,stroke:#ea580c,color:#7c2d12,stroke-width:2px

    subgraph BUILD [" Build-time artifacts "]
        BZI["bzImage<br/>one file, used as both kernels"]:::buildStyle
        KCPIO["kdump.cpio<br/>second-kernel initramfs"]:::buildStyle
        KEXECBIN["kexec<br/>static binary"]:::buildStyle
    end

    subgraph DISKS [" Guest disk images "]
        RLK["rootfs-luks.raw<br/>→ /dev/vda<br/>first-kernel root filesystem"]:::diskStyle
        VDB["kdump-vdb.raw<br/>→ /dev/vdb<br/>empty at boot"]:::diskStyle
        RF["rootfs.raw<br/>unused by this test<br/>plain busybox baseline"]:::unusedStyle
    end

    FORMAT["cryptsetup luksFormat<br/>runtime action, by the first kernel"]:::actionStyle

    BZI -->|copied to /boot/bzImage inside| RLK
    KCPIO -->|copied to /boot/kdump.cpio inside| RLK
    KEXECBIN -->|copied to /usr/bin/kexec inside| RLK
    RLK -.->|"kexec -p -s /boot/bzImage --initrd=/boot/kdump.cpio"| KCPIO
    RLK --> FORMAT
    FORMAT -->|formats, empty to LUKS2| VDB

rootfs-luks.raw is self-contained: it carries its own copies of the exact bzImage and kdump.cpio that the live kernel will later kexec into. This is intentional — it guarantees the second kernel loaded at crash time is always the same build that is currently running, with the same CONFIG_CRASH_DM_CRYPT support, rather than depending on anything external to the guest at panic time.


9. Step-by-Step Walkthrough

The three commands that run this whole test, from /Users/mpilaniy/pilaniya/work/cursor/kernel:

1
2
3
./qemu/build-vanilla-x86-kernel.sh
./qemu/make-luks-rootfs-x86.sh
./qemu/luks-kdump-x86-test.py

Step 1 — build-vanilla-x86-kernel.sh

Cross-compiles the x86_64 bzImage from the CentOS Stream 10 source tree inside an arm64 Ubuntu container, using the persistent Docker build volume linux-vanilla-x86-build. Applies the Section 5 options, runs olddefconfig, verifies all four new options landed as =y, then builds and copies out bzImage and kernel.config. Produces the kernel; touches no disk image and boots nothing.

Step 2 — make-luks-rootfs-x86.sh

Refuses to proceed unless kernel.config already shows CONFIG_CRASH_DM_CRYPT=y. An Alpine container installs busybox-static, cryptsetup 2.7.5, e2fsprogs, and keyutils, confirms cryptsetup --help advertises both --link-vk-to-keyring and --volume-key-keyring, then stages two separate trees — one destined for rootfs-luks.raw, one destined for kdump.cpio — copying each binary together with every shared library ldd reports. The initramfs tree is packed into kdump.cpio; a second, privileged container builds a fresh 512 MiB ext4 image and copies the rootfs tree, bzImage, kexec, and kdump.cpio into it, producing rootfs-luks.raw.

Step 3 — luks-kdump-x86-test.py driving run-qemu-luks-x86.sh

A Python driver spawns the QEMU shell script on a pseudo-terminal, recording every byte of serial output to build-out/vanilla-x86/luks-kdump.log. The shell script creates kdump-vdb.raw as a sparse 1536 MiB file if it does not already exist, then launches QEMU with both drives attached, crashkernel=256M on the command line, and the console routed entirely to the serial port.

The driver waits for the login banner, then sends sh /root/luks-kdump-test.sh — the first-kernel script that performs every step in the top half of the Section 3 sequence diagram: format /dev/vdb as LUKS2, link the volume key as a logon key, register it in configfs, kexec -p -s the crash image, and finally trigger the panic via SysRq.

The panic message (Kernel panic - not syncing: sysrq triggered crash) is the expected, successful transition point — not a failure — and the driver treats it as such before waiting for the second kernel’s kdump-init to print CRASH_CMDLINE=, RESTORE=1, VMCORE_BYTES=..., and finally LUKS_KDUMP_VMCORE_OK.

Evidence from the passing run

1
2
3
4
5
6
7
8
elfcorehdr=0x2f000000 dmcryptkeys=0x34781000 console=ttyS0 nokaslr irqpoll nr_cpus=1 reset_devices rdinit=/init
RESTORE=1
LUKS_UUID=228c9d1a-ba95-4def-b36e-767ff075df9e
807+1 records in
807+1 records out
846635008 bytes (807.4MB) copied, 23.109443 seconds, 34.9MB/s
VMCORE_BYTES=846635008
LUKS_KDUMP_VMCORE_OK

10. Pass/Fail Determination

A run is a pass only when all three of the following hold true simultaneously:

  1. The second kernel (crash) prints LUKS_KDUMP_VMCORE_OK.
  2. The serial log contains dmcryptkeys= on the crash command line.
  3. The serial log never contains Enter passphrase.

Any of the following is treated as an unconditional failure, regardless of whether a later reboot “recovers”:

SymptomWhat it means
kexec_crash_size=0crashkernel= never reserved memory — do not even attempt the panic.
crash_dm_crypt_keys directory missing after mounting configfsThe running bzImage was not built with CONFIG_CRASH_DM_CRYPT=y.
no /dev/vdbThe second virtio disk is missing, or something mounted a tmpfs over /dev and hid the device node.
No such logon key during kexec -p -sThe configfs description did not match any existing logon key — often because the key was linked after the configfs entry was created, or the UUID strings do not match exactly.
elfcorehdr= present, dmcryptkeys= absentThe crash image loaded, but no key was ever copied into reserved memory.
Enter passphrase appearing after the second kernel bootsThe keyring restore step was skipped or failed; cryptsetup is about to attempt an interactive unlock that this environment cannot satisfy.
vmcore found only on the plain root filesystemThe dump bypassed LUKS entirely — this is ordinary kdump, not evidence that CONFIG_CRASH_DM_CRYPT works.
Missing LUKS_KDUMP_VMCORE_OK, non-ELF magic, or file under 1 MiBThe dd copy did not complete, or /proc/vmcore was not a valid dump when it was copied.

11. Running It Yourself

1
2
3
4
cd /Users/mpilaniy/pilaniya/work/cursor/kernel
./qemu/build-vanilla-x86-kernel.sh
./qemu/make-luks-rootfs-x86.sh
./qemu/luks-kdump-x86-test.py

To stop short of the panic and poke around manually:

1
2
3
./qemu/run-qemu-luks-x86.sh
# at the busybox prompt:
sh /root/luks-kdump-test.sh

Quit QEMU with Ctrl-a, then x. The full serial transcript of every run is written to build-out/vanilla-x86/luks-kdump.log.

This post is licensed under CC BY 4.0 by the author.