The initramfs and initrd

When the kernel finishes its own setup it has no way to read your disk: the driver for your NVMe controller, the LVM and dm-crypt layers stacking on top of it, the filesystem module for your root partition — none of those are guaranteed to be built in. The initramfs (initial RAM filesystem) breaks this chicken-and-egg by shipping a tiny but complete userspace in memory: a cpio archive that the kernel unpacks directly into its initial root filesystem (a tmpfs/ramfs mounted as /), then executes a program called /init out of. That program has exactly the modules and tools needed to find, assemble, and mount the real root filesystem, after which it pivots onto it and hands control to the real init. Crucially, “all 2.6+ Linux kernels contain a gzipped cpio … archive [that] is extracted into rootfs when the kernel boots up” (ramfs-rootfs-initramfs.rst, v6.12) — so an initramfs is always present; the question is only whether it’s the empty built-in one or a real external one your bootloader loaded. This note explains the modern initramfs mechanism in detail and contrasts it with the legacy block-device initrd it replaced; the deeper history lives in the sibling initramfs vs initrd History.

Mental Model

Think of the early-boot root as a filesystem the kernel fills from a tarball it carries in RAM, rather than one it mounts from a disk. Three distinct things are at play and conflating them causes endless confusion:

  • rootfs is the kernel’s always-present initial root mount — “a special instance of ramfs (or tmpfs, if that’s enabled), which is always present” and which cannot be unmounted because the kernel relies on the mount-tree root never being empty (ramfs-rootfs-initramfs.rst). It exists before any archive is unpacked.
  • initramfs is a cpio archive (the bootloader-supplied external one, and/or one linked into the kernel image) whose contents are extracted into rootfs. The archive is not a filesystem you mount — it is a stream of file records the kernel replays as mkdir/create/write/symlink calls.
  • initrd (the legacy thing) is a wholly different mechanism: a complete filesystem image (ext2/minix/romfs) loaded into a RAM block device /dev/ram0, mounted like a disk.

The single most important rule: after unpacking, “the kernel checks to see if rootfs contains a file ‘init’, and if so it executes it as PID 1” (ramfs-rootfs-initramfs.rst). There is no mount of a block device, no pivot_root performed by the kernel — the kernel simply unpacks, then runs /init.

flowchart TB
  BL["Bootloader (GRUB / EFI stub)<br/>loads kernel + external initramfs;<br/>fills ramdisk_image / ramdisk_size"]
  BL --> RES["setup_arch(): reserve_initrd()<br/>memblock_reserve() the archive,<br/>set initrd_start / initrd_end"]
  RES --> ROOTFS["Kernel mounts rootfs<br/>(tmpfs/ramfs) as /"]
  ROOTFS --> BUILTIN["populate_rootfs():<br/>unpack_to_rootfs(__initramfs_start)<br/>(built-in archive)"]
  BUILTIN --> EXT["unpack_to_rootfs(initrd_start)<br/>(external archive overlays built-in)"]
  EXT --> CHECK{"/init present<br/>in rootfs?"}
  CHECK -->|yes| INIT["run /init as PID 1<br/>(finds + mounts real root,<br/>then switch_root)"]
  CHECK -->|"no (or cpio magic absent)"| FALLBACK["fall through:<br/>treat image as legacy initrd,<br/>or mount root= and run /sbin/init"]

The initramfs path from bootloader to /init. What it shows: the bootloader loads the archive and records its address; the kernel reserves it, mounts rootfs, unpacks the built-in archive then overlays the external one, and finally runs /init if present — otherwise falling back to the legacy initrd / root= path. The insight to take: there are two archives (built-in + external) merged into one rootfs, and the presence or absence of /init is the single switch that decides between the modern and legacy boot paths.

The cpio “newc” Archive Format

An initramfs is a cpio archive in the “newc” format — the kernel deliberately requires this specific portable ASCII variant, not the old binary cpio formats. Building one is literally find . | cpio -H newc -o | gzip, and feeding the kernel any other format makes it error out: the parser explicitly rejects the old “070707” magic with “incorrect cpio method used: use -H newc option” (init/initramfs.c, v6.12).

Each file in the archive is a 110-byte ASCII header followed by the filename and (for regular files) the data. The header is thirteen fields, every one an 8-hex-digit ASCII number except the 6-byte magic (buffer-format.rst, v6.12):

FieldSizeMeaning
c_magic6"070701" (newc) or "070702" (newc+CRC)
c_ino8inode number (used to detect hard links)
c_mode8file type + permission bits (matches stat st_mode)
c_uid / c_gid8 eachowner uid / gid
c_nlink8link count
c_mtime8modification time
c_filesize8size of the data that follows (0 for non-regular files)
c_maj / c_min8 eachdevice major/minor of the file’s containing device
c_rmaj / c_rmin8 eachmajor/minor of a device node (for mknod)
c_namesize8length of the filename including its trailing \0
c_chksum832-bit data checksum (only meaningful for "070702")

After the header comes the null-terminated filename, then the file data. Two alignment rules matter: the filename is padded so the data starts on a 4-byte boundary, and the data is padded so the next header starts on a 4-byte boundary. The kernel encodes this in two terse lines (init/initramfs.c):

#define N_ALIGN(len) ((((len) + 1) & ~3) + 2)
...
next_header = this_header + N_ALIGN(name_len) + body_len;
next_header = (next_header + 3) & ~3;

The archive ends with a sentinel entry whose name is the literal string TRAILER!!! and whose c_filesize is zero — when the unpacker sees strcmp(collected, "TRAILER!!!") == 0 it frees its hard-link tracking table and stops (init/initramfs.c). Because the trailer also resets hard-link tracking, multiple archives can simply be concatenated — a property the kernel relies on to glue the external archive after the built-in one, and which lets distributions append microcode or firmware as a separate uncompressed segment in front of the main compressed one.

How the Kernel Unpacks It — The State Machine

The unpacking lives in init/initramfs.c and is driven by unpack_to_rootfs():

static char * __init unpack_to_rootfs(char *buf, unsigned long len)

It returns NULL on success or an error string on failure. Internally it is a small state machine that walks the byte stream, with an actions[] dispatch table (init/initramfs.c):

static __initdata int (*actions[])(void) = {
    [Start]      = do_start,
    [Collect]    = do_collect,
    [GotHeader]  = do_header,
    [SkipIt]     = do_skip,
    [GotName]    = do_name,
    [CopyFile]   = do_copy,
    [GotSymlink] = do_symlink,
    [Reset]      = do_reset,
};

The flow is: collect 110 bytes, do_header validates the magic and parses the fields via parse_header() (which reads ino, mode, uid, gid, nlink, mtime, body_len/filesize, major, minor, name_len, hdr_csum); do_name creates the object — a directory (mkdir), a device node (mknod), a FIFO, a socket, or, for a regular file, opens it; do_copy streams the file body into it; do_symlink handles links. Hard links are recognised by matching c_ino against earlier entries. Each created object lands directly in rootfs — there is no intermediate filesystem image, the archive is replayed as live VFS operations into the in-memory root. The header_buf is a single kmalloc(110, GFP_KERNEL), which is why the format’s fixed 110-byte header matters.

Compression and Decompression

The archive is almost always compressed, and the kernel auto-detects the compressor from the leading magic bytes rather than being told. unpack_to_rootfs() calls into the generic decompressor layer (init/initramfs.c):

decompress = decompress_method(buf, len, &compress_name);
if (decompress) {
    int res = decompress(buf, len, NULL, flush_buffer, NULL,
                         &my_inptr, error);

decompress_method() sniffs the magic and selects among the compiled-in decompressors — gzip, bzip2, LZMA, XZ, LZO, LZ4, and zstd — and the decompressed cpio stream is fed to the same state machine through flush_buffer. If a segment isn’t compressed (magic 070701 directly), it is parsed in place. If the data is compressed with a method the kernel wasn’t built with, you get “compression method %s not configured.” The choice of compressor is a real trade-off: gzip is universally available and decompresses reasonably fast; zstd has become popular because it gives near-gzip ratios with dramatically faster decompression, shaving measurable time off boot on large initramfs images; XZ/LZMA compress hardest (smallest image) but decompress slowly, which can lengthen boot despite the smaller archive.

Built-in vs External — populate_rootfs()

There are two sources of initramfs content, and the kernel processes both. The built-in archive is linked straight into the kernel image and bounded by linker symbols:

extern char __initramfs_start[];
extern unsigned long __initramfs_size;

Its contents come from CONFIG_INITRAMFS_SOURCE at build time (discussed below); if that config is empty, the built-in archive is essentially empty (just enough to be valid). The external archive is the one your bootloader loaded into memory alongside the kernel; the architecture reserves it and exposes its bounds as initrd_start/initrd_end (the same globals on the modern path — the name is a historical artifact). The orchestration is do_populate_rootfs() (init/initramfs.c):

static void __init do_populate_rootfs(void *unused, async_cookie_t cookie)
{
    char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
    if (err)
        panic_show_mem("%s", err);
 
    if (!initrd_start || IS_ENABLED(CONFIG_INITRAMFS_FORCE))
        goto done;
 
    if (IS_ENABLED(CONFIG_BLK_DEV_RAM))
        printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
    else
        printk(KERN_INFO "Unpacking initramfs...\n");
 
    err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);
    if (err) {
#ifdef CONFIG_BLK_DEV_RAM
        populate_initrd_image(err);
#else
        printk(KERN_EMERG "Initramfs unpacking failed: %s\n", err);
#endif
    }

Reading it line by line: the built-in archive is unpacked first, and a failure there is fatal (panic_show_mem — there is no recovery if the kernel’s own embedded rootfs is corrupt). If there is no external image (!initrd_start), or the build forced built-in-only (CONFIG_INITRAMFS_FORCE), it stops. Otherwise it tries to unpack the external image on top of the built-in one — and because cpio archives overlay, “the files in the external archive will overwrite any conflicting files in the built-in initramfs archive” (ramfs-rootfs-initramfs.rst). The crucial branch is the failure case: if the external image isn’t a valid cpio archive at all and CONFIG_BLK_DEV_RAM is set, populate_initrd_image(err) writes the image out to /initrd.image and the kernel treats it as a legacy initrd block-device image — this is exactly the compatibility fork that lets one mechanism transparently handle both formats. Without CONFIG_BLK_DEV_RAM, an unparseable external image just logs "Initramfs unpacking failed".

The whole thing runs asynchronously. populate_rootfs() schedules it and only blocks if needed:

static int __init populate_rootfs(void)
{
    initramfs_cookie = async_schedule_domain(do_populate_rootfs, NULL,
                                             &initramfs_domain);
    usermodehelper_enable();
    if (!initramfs_async)
        wait_for_initramfs();
    return 0;
}

Registered with rootfs_initcall(populate_rootfs), it runs at the rootfs init level. The async_schedule_domain() lets unpacking proceed in parallel with the rest of late init unless initramfs_async=0 is passed; anything that needs the files calls wait_for_initramfs() first.

How the Bootloader Hands Over the Archive

On x86 the bootloader communicates the external archive’s location through the boot protocol’s setup header. The relevant fields (bootparam.h, v6.12):

__u32 ramdisk_image;   /* in setup_header */
__u32 ramdisk_size;

documented as “The 32-bit linear address of the initial ramdisk or ramfs” and “Size of the initial ramdisk or ramfs. Leave at zero if there is no initial ramdisk/ramfs” (boot.rst, v6.12). Because these are 32-bit, an archive loaded above 4 GiB needs the high halves carried in boot_params:

__u32 ext_ramdisk_image;   /* offset 0x0c0 */
__u32 ext_ramdisk_size;    /* offset 0x0c4 */

The kernel reassembles the full 64-bit address in get_ramdisk_image() (ramdisk_image |= (u64)boot_params.ext_ramdisk_image << 32), and reserve_initrd() then memblock_reserve()s the range and sets initrd_start = ramdisk_image + PAGE_OFFSET; initrd_end = initrd_start + ramdisk_size; (arch/x86/kernel/setup.c, v6.12). The reservation in memblock is what protects the archive from being clobbered while the kernel allocates other early structures. On ARM/RISC-V the same initrd_start/initrd_end are derived from device-tree linux,initrd-start/linux,initrd-end properties instead of a setup header, but the kernel-side variables are identical.

Running /init — and the Fallback

After unpacking, the boot continues in init/main.c. Before the kernel decides what to run, kernel_init_freeable() checks whether the configured ramdisk init program exists; if not, it clears the pointer and falls through to the legacy disk path (init/main.c, v6.12):

if (init_eaccess(ramdisk_execute_command) != 0) {
    ramdisk_execute_command = NULL;
    prepare_namespace();
}

ramdisk_execute_command defaults to "/init" (overridable with the rdinit= kernel parameter). If it does exist, kernel_init() executes it as PID 1:

if (ramdisk_execute_command) {
    ret = run_init_process(ramdisk_execute_command);
    if (!ret)
        return 0;
    pr_err("Failed to execute %s (error %d)\n",
           ramdisk_execute_command, ret);
}

If /init runs and never returns (the normal case), the system is up — /init is the early-userspace init that loads storage modules, assembles RAID/LVM/crypt, mounts the real root, and switch_roots onto it (switch_root and pivot_root at Boot, Mounting the Real Root Filesystem). If there is no /init in rootfs, the kernel “fall[s] through to the older code to locate and mount a root partition, then exec some variant of /sbin/init out of that” (ramfs-rootfs-initramfs.rst) — prepare_namespace() mounts the root= device directly. And if nothing works at all, the famous panic (init/main.c):

panic("No working init found.  Try passing init= option to kernel. "
      "See Linux Documentation/admin-guide/init.rst for guidance.");

Building the Built-in Archive — CONFIG_INITRAMFS_SOURCE

The simplest way to embed content is to point CONFIG_INITRAMFS_SOURCE at a gzipped cpio archive, a directory, or a text specification file that the kernel’s own usr/gen_init_cpio tool turns into an archive at build time (ramfs-rootfs-initramfs.rst). The spec-file grammar is documented in gen_init_cpio’s usage (usr/gen_init_cpio.c, v6.12):

file  <name> <location> <mode> <uid> <gid> [<hard links>]
dir   <name> <mode> <uid> <gid>
nod   <name> <mode> <uid> <gid> <dev_type> <maj> <min>
slink <name> <target> <mode> <uid> <gid>
pipe  <name> <mode> <uid> <gid>
sock  <name> <mode> <uid> <gid>

A minimal spec to bootstrap a busybox-based early userspace:

dir   /dev        0755 0 0
nod   /dev/console 0600 0 0 c 5 1
dir   /bin        0755 0 0
file  /bin/busybox /path/to/busybox 0755 0 0
slink /bin/sh     /bin/busybox 0777 0 0
file  /init       /path/to/init-script 0755 0 0

Line by line: dir /dev creates the device directory; nod /dev/console … c 5 1 creates the console character device (major 5, minor 1) so early printk-to-console redirection and the shell have a controlling terminal; the busybox binary is pulled in from the build host and /bin/sh symlinked to it; and /init — the file the kernel will exec — is installed 0755. Because gen_init_cpio lets you declare ownership and device nodes without needing root on the build host, this is how distributions and embedded builds assemble reproducible early userspaces. Note that the ${} variable expansion in <location> lets the spec reference build-time paths.

In practice almost nobody writes these specs by hand for general-purpose distros — they leave CONFIG_INITRAMFS_SOURCE empty (so the built-in archive is trivial) and let a userspace generator like dracut or mkinitcpio build the external archive at install/update time (dracut and initramfs Generators). The built-in route is mainly for embedded systems that want a single self-contained kernel image with no separate file.

How initrd Differs — The Legacy Path

The legacy initrd (initial RAM disk) predates initramfs and works fundamentally differently. Instead of a cpio archive replayed into rootfs, the bootloader loads a complete filesystem image (typically ext2, minix, or romfs) which the kernel attaches to a RAM block device /dev/ram0 and mounts as root (initrd.rst, v6.12). The kernel then runs /linuxrc (not /init); when linuxrc finishes, the kernel performs the root transition — historically by the change_root mechanism via writing to /proc/sys/kernel/real-root-dev, later by the userspace tool pivot_root. The differences that matter:

  • Format: initrd is a mountable filesystem image and therefore requires that filesystem’s driver to be built into the kernel; initramfs is a cpio archive needing no filesystem driver at all.
  • Where it lives: initrd occupies a fixed-size RAM block device (sized by CONFIG_BLK_DEV_RAM_SIZE); initramfs content lives in tmpfs/ramfs and grows as needed, with no fixed cap and no double-buffering of page-cache vs. block device.
  • The handover program: initrd runs /linuxrc and the kernel does the root switch; initramfs runs /init, which never returns and does the switch itself in userspace.
  • Lifecycle: the legacy code “converts initrd into a ‘normal’ RAM disk and frees the memory used by initrd” after the switch; initramfs’s rootfs simply becomes the overmounted old root that switch_root deletes.

The two are unified by the single fork in do_populate_rootfs() shown earlier: the kernel first tries to parse the external image as a cpio archive (initramfs); only if that fails and CONFIG_BLK_DEV_RAM is configured does it treat the image as a legacy initrd block device. So a modern kernel supports both, and the cpio-magic test is the discriminator. The full historical arc — why initramfs replaced initrd, the ramfs vs tmpfs rootfs detail, the removal of linuxrc from common use — is in initramfs vs initrd History.

Failure Modes and Diagnostics

  • Kernel panic - not syncing: VFS: Unable to mount root fs / No working init found. Almost always the initramfs lacks the driver or tool to reach the real root: the NVMe/virtio-blk module isn’t in the archive, or the dm-crypt/LVM tools are missing, so /init can’t assemble root and either exits or never mounts it. Diagnose by booting with rd.break (dracut) or break= to drop into the early shell and inspect /proc/modules and lsblk.
  • Initramfs unpacking failed: <reason>. The external image is corrupt or uses a compressor not built into the kernel (“compression method %s not configured”). Common when an image is built with zstd but the kernel lacks CONFIG_RD_ZSTD. Rebuild the image with gzip (mkinitcpio/dracut compression setting) or enable the decompressor.
  • “incorrect cpio method used: use -H newc option”. Someone built the archive with a non-newc cpio format. Always use cpio -H newc -o.
  • Stale/wrong initramfs after a kernel or driver update. The external initramfs is built once at install time; if you add a storage driver to the system but don’t regenerate the initramfs (dracut -f / mkinitcpio -P), the old archive still lacks it and boot fails. This is the single most common real-world initramfs failure.
  • Archive too large / out of memory unpacking. Because content is decompressed into tmpfs, a bloated initramfs (e.g. dracut default “host-only=no” pulling in every driver) consumes real RAM during early boot; on tiny systems this can OOM. Use host-only mode to include only the drivers the machine needs.

Uncertain

Verify: that rd.break / break= are the exact early-shell parameters across both dracut and the in-kernel path in the 6.12 era (these are generator-level conventions, not kernel parameters, and differ between dracut and mkinitcpio). Reason: I cited them from general knowledge, not from a primary dracut/mkinitcpio doc fetched in this task. To resolve: check the dracut.cmdline(7) and mkinitcpio man pages for the current syntax. uncertain

Alternatives and When to Skip the initramfs

You do not need an external initramfs at all if the kernel can reach root unaided. If the storage controller driver and root filesystem are built into the kernel (not modules) and the root is a simple partition (no LVM/crypt/RAID/NFS), you can boot with just root=/dev/... and no external archive — prepare_namespace() mounts it directly and runs /sbin/init. This is common in embedded and appliance images where the hardware is fixed and known. General-purpose distributions ship a generated external initramfs because they cannot predict which storage driver any given machine needs, so they ship everything as modules and let early userspace load the right ones (Mounting the Real Root Filesystem). The decision is captured in the parent MOC’s “Do I even need an initramfs?” framework: built-in drivers + a simple root may skip it; anything stacked (encryption, logical volumes, network root) requires it.

See Also