NixOS under Firecracker cannot find `/dev/vda`

The problem

Hi all! I’m trying to generate a VM image for Firecracker but am running into some issues. Specifically, the VM complains that the device /dev/vda doesn’t show up.

Setup

Here is the Nix code that I am using to generate the configuration file for Firecracker. It consists of a flake which does most of the heavy lifting: generating a rootfs, extracting the kernel binary & initrd from the guest configuration ,and assembling it all into a configuration file for Firecracker.

flake.nix
{
	description = "Code execution API";

	inputs = {
		nixpkgs.url = "github:NixOS/nixpkgs/24.05";
	};

	outputs = { self, nixpkgs }:
    {
      nixosConfigurations = {
        guest = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            ./guest-configuration.nix
          ];
        };
      };

      packages.x86_64-linux = {
        # VMLinux kernel image to be passed to Firecracker when starting a guest
        guest-vmlinux = self.nixosConfigurations.guest.config.system.build.kernel.dev;

        # Base rootfs to be passed to Firecracker when starting a guest
        guest-rootfs =
          let
            nixosSystem = self.nixosConfigurations.guest;

            pkgs = nixpkgs.legacyPackages.x86_64-linux;
          in
          import "${nixpkgs}/nixos/lib/make-disk-image.nix" {
            inherit pkgs;
            inherit (pkgs) lib;
            diskSize = "auto";
            config = nixosSystem.config;
            additionalSpace = "1K";
            format = "raw";
            partitionTableType = "none";
            installBootLoader = false;
            fsType = "ext4";
            copyChannel = false;
          };

        # A basic VM configuration for testing.
        # It can be run with firecracker like so:
        # firecracker --config-file XXX --no-api
        firecracker-config =
          let
            pkgs = nixpkgs.legacyPackages.x86_64-linux;

            inherit (self.packages.x86_64-linux) guest-vmlinux guest-rootfs;

            config = self.nixosConfigurations.guest.config;

            vm-config = {
              "boot-source" = {
                "kernel_image_path" = "${guest-vmlinux}/vmlinux";
                "boot_args" = "${toString config.boot.kernelParams} init=${config.system.build.toplevel}/init debug boot.trace boot.shell_on_fail";
                "initrd_path" = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
              };
              "drives" = [
                {
                  "drive_id" = "rootfs";
                  "is_root_device" = false;
                  "path_on_host" = "${guest-rootfs}/nixos.img";
                  "is_read_only" = true;
                }
              ];
              "machine-config" = {
                "vcpu_count" = 2;
                "mem_size_mib" = 1024;
                "smt" = false;
                "track_dirty_pages" = false;
                "huge_pages" = "None";
              };
              "cpu-config" = null;
              "balloon" = null;
              "network-interfaces" = [];
              "vsock" = null;
              "logger" = null;
              "metrics" = null;
              "mmds-config" = null;
              "entropy" = null;
            };
          in
          pkgs.writers.writeJSON "vm-config.json" vm-config;

        default = self.packages.x86_64-linux.firecracker-config;
      };
    };
}

The actual guest confiuration is stored in guest-configuration.nix. It doesn’t do a whole lot, except configure a custom kernel.

guest-configuraiton.nix
{ pkgs, config, lib, modulesPath, ... }:

with pkgs;
with lib;

{
  imports = [
    "${modulesPath}/profiles/minimal.nix"
  ];

  # Open up root for easier debugging
  services.openssh = {
    enable = true;
    startWhenNeeded = true;
    settings.PermitRootLogin = "yes";
  };
  users.users.root.password = "root";

  boot = {
    # XXX
    kernelParams = [ "console=ttyS0" "reboot=k" "panic=1" "pci=off" "noapic" ];

    # We are going to pass the kernel, rootfs and initrd directly to firecracker
    # for execution, so there's no need to emit stuff for the boot loader.
    loader.grub.enable = false;

    # These are needed to read disk /dev/vda.
    # FIXME: Doesn't work.
    initrd.kernelModules = [
      "virtio_mmio"
      "virtio_blk"
    ];

    # Load our custom kernel
    kernelPackages =
      let
        version =  "6.1.69";
        majorVersion = head (splitString "." version); # e.g. 6

        kernel = (linuxKernel.manualConfig {
          inherit stdenv lib version;
          src = fetchTarball {
            url = "https://cdn.kernel.org/pub/linux/kernel/v${majorVersion}.x/linux-${version}.tar.xz";
            sha256 = "sha256:1xm83zn39wks7xjvwdm5z00qi5b1r48vyywss0iilbzs4nvdr3qs";
          };
          configfile = ./.config;
          allowImportFromDerivation = true;
        });
      in
      linuxPackagesFor kernel;
  };

  fileSystems."/" = {
    device = "/dev/vda";
  };

  system.stateVersion = "24.05";
}

The kernel config is currently just a slightly (e.g. I removed Bluetooth) stripped version of my host kernel (the default NixOS 24.05 kernel) as found under /proc/config.gz. I obviously want to use a more minimal build in the future, but I figured it was a safer bet to start with a very large kernel.

I hit the upper character limit, so see this paste for .config.

Running it

The default output of the flake is a JSON configuration file for Firecracker.

The final configuration file
$ nix build
$ jq <result
{
  "balloon": null,
  "boot-source": {
    "boot_args": "console=ttyS0 reboot=k panic=1 pci=off noapic loglevel=4 init=/nix/store/49rnwhjfk0q0q99ywivsqlnbig4sj0rb-nixos-system-nixos-24.05.20240531.63dacb4/init debug boot.trace boot.shell_on_fail",
    "initrd_path": "/nix/store/imajf1vvfp2870k1yl77xz5yxzgwzhzy-initrd-linux-6.1.69/initrd",
    "kernel_image_path": "/nix/store/0ja35h6ppbvv2mn7ik5j6sk1vcs3rwf5-linux-6.1.69-dev/vmlinux"
  },
  "cpu-config": null,
  "drives": [
    {
      "drive_id": "rootfs",
      "is_read_only": true,
      "is_root_device": false,
      "path_on_host": "/nix/store/km9h00x98j76m2s0008n5zkzh0mjlmh6-nixos-disk-image/nixos.img"
    }
  ],
  "entropy": null,
  "logger": null,
  "machine-config": {
    "huge_pages": "None",
    "mem_size_mib": 1024,
    "smt": false,
    "track_dirty_pages": false,
    "vcpu_count": 2
  },
  "metrics": null,
  "mmds-config": null,
  "network-interfaces": [],
  "vsock": null
}

Then I can pass the generated configuration file to firecracker for it to run it.

$ nix build
$ firecracker --config-file result --no-api

See this paste for the logs from Firecracker and the guest kernel. I think the major takeaway from the linked logs is this line:

[   22.152128] stage-1-init: [Wed Nov 13 12:05:43 UTC 2024] Timed out waiting for device /dev/vda, trying to mount anyway.

However, that’s where I run into the issue with init in the initramfs not finding /dev/vda. I know /dev/vdX are for virtualised drives, but I don’t know why it isn’t showing up or which system is responsible for making it show up.

Can’t mount /dev/vda

This line seems to indicate that Firecracker is injecting an argument indicating where the virtio device is located.

[    0.000000] Command line: console=ttyS0 reboot=k panic=1 pci=off noapic loglevel=4 init=/nix/store/49rnwhjfk0q0q99ywivsqlnbig4sj0rb-nixos-system-nixos-24.05.20240531.63dacb4/init debug boot.trace boot.shell_on_fail virtio_mmio.device=4K@0xd0000000:5

But then something clearly goes wrong when trying to load that thing, as evidenced by these lines:

[    0.405134] virtio-mmio: Registering device virtio-mmio.0 at 0xd0000000-0xd0000fff, IRQ 5.
[    0.406100] stage-1-init: [Wed Nov 13 12:05:22 UTC 2024] + info 'loading module virtio_mmio...'
[    0.406142] virtio-mmio virtio-mmio.0: can't request region for resource [mem 0xd0000000-0xd0000fff]
[    0.407926] virtio-mmio: probe of virtio-mmio.0 failed with error -16

…and that’s about where I’m at. I don’t know how make the device /dev/vda appear, so I can’t mount it and start actually doing interesting stuff inside the VM.

After asking on the firecracker Slack group it seems I was running into firecracker-microvm/firecracker#4816. Basically, the noacpi option was causing the virtio drivers to not be loaded.

The configuration obviously still does not boot, as it only specifies read-only disks, but (after setting boot.initrd.checkJournalingFS = false) the boot process now fails where I was expecting it to, so I am marking this thread as solved.

1 Like