Linux Initramfs, With and Without Yocto

I was doing background research for the topic of the next blog text in the Yocto hardening series and realized that I’m going to need an initramfs. However, I haven’t written a blog text about initramfs, so I’ll do that first. Let’s get started with an introduction to what I’m even talking about, and then get into the more technical stuff.

Initramfs

Initramfs is a small file system that the Linux kernel loads into RAM before the actual root file system is mounted. The kernel then executes the initialization program that is located in the initramfs. That program can perform a variety of actions, but the primary goal is to mount the actual root file system, switch the root there, and launch the actual init. Other tasks performed by the program may be early system initialization, hardware initialization, kernel module loading, attestation, recovery, firmware upgrade, etc. The possibilities are endless, but since it is a critical system initialization program it is best to keep the functionality simple to have as few failure points as possible.

The only winning move is not to play.

When is initramfs required? For example, if the root file system is encrypted the kernel cannot mount it by itself and requires assistance from userspace tooling that can perform the decryption. Sometimes it may be required that certain hardware is initialized before the actual init program takes over. Initramfs may also perform device discovery, although in the embedded Linux world this is usually handled with a device tree. The list goes on.

However, if you have not identified a need for an initramfs, you should not add it “just because”. If you don’t use the initramfs, the regular root file system gets mounted just fine and the init process gets launched from there as usual. Life could be easy and simple.

Initrd

Everything makes sense so far, I hope. However, the thing that often confuses me is initrd. Initrd is an older version of the same idea. The main technical difference is that the initramfs is a cpio archive file of a directory structure, whereas initrd is a mountable file system image. Initrd isn’t commonly used anymore as initramfs is simpler, more performant and more flexible.

The support for the initramfs came already in Linux kernel version 2.6.13, released in 2005, so unless you’ve used a time machine and travelled back into the past, the mentions to initrd in practice mean initramfs. The same applies to the term initial ramdisk. Sometimes the term “early user space” is also used. But if there are some options, documentation, etc. that mention these terms don’t get too confused.

Creating the Initramfs for Linux

First, to enable the initramfs support in Linux we need to set the CONFIG_BLK_DEV_INITRD to y in the kernel configuration. Here’s the confusing initrd again. Still, the aforementioned flag enables the initramfs support. If we wanted to enable the initrd support we would need to additionally set the CONFIG_BLK_DEV_RAM, but that’s not our goal in this text.

As mentioned earlier, initramfs in the Linux kernel is a cpio archive file. The kernel can handle loading the initramfs in two different ways: either initramfs is bundled into the kernel during the build and unpacked into the memory by the kernel itself when it launches, or an external archive is loaded into the memory by the bootloader and a pointer to it is passed to the kernel. The kernel then unpacks the image located in the given pointer address. Embedding the initramfs naturally increases the size of the kernel, but eases the burden of the bootloader and makes the boot process simpler.

Bundling the Initramfs into the Kernel

To bundle the initramfs into the kernel, CONFIG_INITRAMFS_SOURCE configuration option is used. The option can contain different types of values:

  • A path to a pre-made cpio archive (optionally also pre-compressed)
  • A directory containing the desired initramfs contents. This directory will get archived as a part of the kernel build process.
  • A path to a configuration file defining the directories and/or files to create and/or copy. The configuration file format is documented in the Linux documentation.

If a directory or a configuration file is used, multiple space-separated values can be defined. The resulting initramfs will be an aggregate of the given information.

CONFIG_INITRAMFS_COMPRESSION_* configuration options can be used to compress the initramfs. The default value is CONFIG_INITRAMFS_COMPRESSION_NONE, meaning that the initramfs won’t be compressed. To decompress the initramfs, the matching CONFIG_RD_* option should be enabled. This also applies to external initramfs archives. See usr/Makefile in the Linux source for all the compression options. Compressing the initramfs makes the resulting kernel smaller, but increases both the build and boot times.

Creating an External Initramfs

If you on the other hand choose to go the external initramfs route, there are a few ways of creating the image. You could use the cpio command to create the archive, and then compress it with the desired tool, as is shown here in Gentoo wiki. However, I’d recommend using the gen_initramfs.sh script in the Linux kernel source.

The script is located in usr/gen_initramfs.sh, and it assumes that gen_init_cpio binary is located in usr/ directory as well. This means that you first need to compile the kernel (or at least the binary). It also means that you should run the script with your working directory set to the root of the build directory, not the source directory if building out-of-tree.

The reason why I recommend this script is that you can use the same types of sources for the initramfs image as you can when bundling the initramfs. This means you can use multiple directories and configuration files to define the contents without necessarily having to construct the whole directory tree yourself. Also, it is a tool used in the kernel build process, so it can’t be bad, right?

A script worth a thumbs-up. Once you get it working, that is.

Initramfs in Yocto

Now, Yocto makes this all quite simple. According to the manual, you only need to set two variables in your build configuration: INITRAMFS_IMAGE and INITRAMFS_IMAGE_BUNDLE. The first variable defines the name of the image you want to use as an initramfs, and the second variable should be set to 1 if you want to bundle the initramfs into the kernel.

Writing the initramfs image recipe has a few differences compared to writing a regular image recipe. The core-image-minimal-initramfs recipe displays these differences quite well. The most important things are that the image features are removed, the kernel image is excluded, IMAGE_FSTYPES is assigned to the value of INITRAMFS_FSTYPES, and PACKAGE_INSTALL is used instead of IMAGE_INSTALL to select packages. I made an even more minimal initramfs recipe for a starting point:

Yocto has a neat modular initramfs init-script system that is installed in initramfs-framework-base package. I gave it a quick go, and it didn’t quite work as I expected. When I say that, I mean that the device failed to boot. Most likely this was a user issue, but as I’ve mentioned a few times already, in my opinion, it’s best to keep the boot logic minimal and create problems in the user space where there are more options for recovery without panicking the whole system.

So, I wrote another minimal thing (with help from Claude AI). It’s a minimal initramfs init-script that just mounts the special filesystems and the given root device with the given root flags. It doesn’t handle all the parameters, just the root= and rootflags=. Also, passing a device label or UUID doesn’t work either (a different Gentoo Wiki article shows a way of achieving that, among many other things). However, this script can be expanded further with the other desired features. Or the existing features could be improved.

And here’s a Yocto recipe for it. The custom initramfs image recipe shown earlier already installs this recipe. Note how the mount points for the special file systems and the console node are installed in this recipe:

That should be all that’s required to add the early user space into your machine. If you set INITRAMFS_IMAGE value to the example custom-initramfs and INITRAMFS_IMAGE_BUNDLE to 1 and boot the machine, the minimal initramfs script will run and mount the root file system (or crash and drop to emergency shell). The boot log should contain the line “Switching to real root” to indicate this.

If on the other hand, you didn’t set INITRAMFS_IMAGE_BUNDLE, nothing special will happen. The kernel will boot as it did before. However, the initramfs gets built as expected. To make the initramfs actually run, you’ll need to integrate it into your firmware and instruct the bootloader to load and pass it to the kernel.

Example of an External Initramfs with U-Boot

For this example, I’ll use the multipartition QEMU Arm machine from my older blog post. It uses U-Boot as the bootloader. The blog text provides a meta-layer for supporting the multiple partitions and U-Boot, I checked out the hash cf4372a of that repository.

To install the initramfs image to the boot partition of the wic image, I added custom-initramfs-qemuarm-uboot.cpio.gz to IMAGE_BOOT_FILES variable in machine configuration. The do_image_wic task should also depend on custom-initramfs:do_image_complete task. Finally, custom-initramfs.bb recipe should remove wic.qcow2 from IMAGE_FSTYPES because the qemuarm-uboot machine appends it there, causing a circular dependency:

# Add this to initramfs image recipe
IMAGE_FSTYPES:remove = "wic.qcow2"

After these steps and compiling the image, the QEMU machine can be started with the special command that was shown in the other blog text. To load the initramfs image to the memory, we can add the following commands to the boot script:

# Disclaimer: I used the first address that happened to work,
# so it isn't necessarily the optimal choice
setenv loadaddr_initramfs 0x45000000
fatload virtio 0:1 ${loadaddr_initramfs} custom-initramfs-qemuarm-uboot.cpio.gz

Then, the boot command can be modified from:

bootz ${loadaddr} - ${loadaddr_dtb}

To:

# $filesize contains the size of the initramfs after fatload command
bootz ${loadaddr} ${loadaddr_initramfs}:${filesize} ${loadaddr_dtb}

After doing this, the same “Switching to real root” message should appear when the initramfs init runs. The second parameter of bootz is used to define initrd, or initramfs in our case. Since we are using a raw image, we need to add size to the parameter as well. If initrd is not used, it can be omitted with a hyphen (as is done in the first bootz command).

When googling around for U-Boot initramfs instructions I found out that the following U-Boot configuration items may need to be active for this to work. They were already set to y in my default configuration so I’m not certain if that’s true or not, but in case you see issues try playing around with these:

CONFIG_LEGACY_IMAGE_FORMAT=y
CONFIG_SUPPORT_RAW_INITRD=y

Conclusion

Thanks for reading my summary of initramfs, and instructions on how to create an initramfs for Linux and how to use it with Yocto. Hopefully, you found something useful in this text. If not, I recommend checking out the links in the recommended reading section below, because those surely explain initramfs more in depth.

Recommended Reading

Share