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/ramfsmounted as/), then executes a program called/initout 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 realinit. 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/symlinkcalls. - 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):
| Field | Size | Meaning |
|---|---|---|
c_magic | 6 | "070701" (newc) or "070702" (newc+CRC) |
c_ino | 8 | inode number (used to detect hard links) |
c_mode | 8 | file type + permission bits (matches stat st_mode) |
c_uid / c_gid | 8 each | owner uid / gid |
c_nlink | 8 | link count |
c_mtime | 8 | modification time |
c_filesize | 8 | size of the data that follows (0 for non-regular files) |
c_maj / c_min | 8 each | device major/minor of the file’s containing device |
c_rmaj / c_rmin | 8 each | major/minor of a device node (for mknod) |
c_namesize | 8 | length of the filename including its trailing \0 |
c_chksum | 8 | 32-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 intmpfs/ramfsand grows as needed, with no fixed cap and no double-buffering of page-cache vs. block device. - The handover program: initrd runs
/linuxrcand 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_rootdeletes.
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/initcan’t assemble root and either exits or never mounts it. Diagnose by booting withrd.break(dracut) orbreak=to drop into the early shell and inspect/proc/modulesandlsblk.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 lacksCONFIG_RD_ZSTD. Rebuild the image with gzip (mkinitcpio/dracutcompression 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.dracutdefault “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
- Early Memory and Boot Allocators —
reserve_initrd()usesmemblock_reserve()to protect the archive and publishesinitrd_start/initrd_end - initramfs vs initrd History — the deep history of why initramfs replaced the legacy block-device initrd
- The Early Userspace init Script — what the
/initthe kernel execs actually does - Loading the Kernel and initramfs into Memory — how the bootloader places the archive and fills
ramdisk_image/ramdisk_size - switch_root and pivot_root at Boot — how
/initpivots from rootfs onto the real root - Mounting the Real Root Filesystem — the storage/LVM/crypt work
/initperforms - dracut and initramfs Generators — the userspace tools that build the external archive
- tmpfs In-Memory Filesystem — the filesystem rootfs is an instance of
- Linux Boot and Init MOC — the parent map (§4 Early Userspace)