From 6d5a9e8886196b3565e66ccfb82799e772b7a96d Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Thu, 10 Dec 2020 19:22:45 +0100 Subject: [PATCH] Boot from USB --- nixos-modules/genode-core.nix | 214 ++++++++++++++-------- nixos-modules/lib/make-bootable-image.nix | 69 +++++++ nixos-modules/lib/make-esp-fs.nix | 72 ++++++++ nixos-modules/lib/make-ext2-fs.nix | 126 ++++++------- nixos-modules/nova.nix | 35 +++- nixos-modules/partition-type | 1 + nixos-modules/qemu-vm.nix | 8 +- nixos-modules/store-fs-uuid | 1 + 8 files changed, 370 insertions(+), 156 deletions(-) create mode 100644 nixos-modules/lib/make-bootable-image.nix create mode 100644 nixos-modules/lib/make-esp-fs.nix create mode 100644 nixos-modules/partition-type create mode 100644 nixos-modules/store-fs-uuid diff --git a/nixos-modules/genode-core.nix b/nixos-modules/genode-core.nix index 16cebb1..6447e2c 100644 --- a/nixos-modules/genode-core.nix +++ b/nixos-modules/genode-core.nix @@ -25,14 +25,22 @@ in { prefix = mkOption { type = types.str; example = "hw-pc-"; + description = "String prefix signifying the Genode core in use."; }; supportedSystems = mkOption { type = types.listOf types.str; example = [ "i686-genode" "x86_64-genode" ]; + description = "Hardware supported by this core."; }; - basePackages = mkOption { type = types.listOf types.package; }; + basePackages = mkOption { + type = types.listOf types.package; + description = '' + List of packages to make availabe before the Nix store is ready. + These are baked into . + ''; + }; children = mkOption { type = with types; @@ -57,17 +65,6 @@ in { boot = { - kernel = mkOption { - type = types.path; - default = "${pkgs.genodePackages.bender}/bender"; - }; - - initrd = mkOption { - type = types.str; - default = "${pkgs.genodePackages.bender}/bender"; - description = "Path to an image or a command-line arguments"; - }; - configFile = mkOption { type = types.path; description = '' @@ -84,9 +81,22 @@ in { romModules = mkOption { type = types.attrsOf types.path; + default = { }; description = "Attr set of initial ROM modules"; }; + storeFsUuid = mkOption { + type = types.str; + default = import ./store-fs-uuid; + description = "Custom partition type of the nix-store file-system."; + }; + + storePartUuid = mkOption { + type = types.str; + default = import ./partition-type; + description = "Custom partition type of the nix-store file-system."; + }; + storeBackend = mkOption { type = types.enum [ "tarball" "usb" ]; # "parent"? default = "tarball"; @@ -119,7 +129,6 @@ in { storePaths = mkOption { type = with types; listOf package; - example = literalExample "[ pkgs.genodePackages.vfs_lwp ]"; description = '' Derivations to be included in the Nix store in the generated boot image. ''; @@ -148,20 +157,17 @@ in { }; mergeManifests = inputs: - localPackages.writeTextFile { - name = "manifest.dhall"; - text = with builtins; - let - f = head: input: - if hasAttr "manifest" input then - '' - ${head}, { mapKey = "${ - lib.getName input - }", mapValue = ${input.manifest} }'' - else - abort "${input.pname} does not have a manifest"; - in (foldl' f "[" inputs) + "]"; - }; + with builtins; + let + f = head: input: + if hasAttr "manifest" input then + '' + ${head}, { mapKey = "${ + lib.getName input + }", mapValue = ${input.manifest} }'' + else + abort "${input.pname} does not have a manifest"; + in (foldl' f "[" inputs) + "]"; romDirectories = filterAttrs (_: value: value != null) (mapAttrs (name: value: @@ -181,51 +187,85 @@ in { message = "invalid Genode core for this system"; }]; - genode.core.children.store_fs.configFile = let + genode.core.basePackages = + lib.optional (config.genode.boot.storeBackend == "usb") + pkgs.genodePackages.part_block; - storeVfsConfig = { - tarball = '' - VFS.vfs [ VFS.leafAttrs "tar" (toMap { name = "${config.system.build.tarball.fileName}.tar" }) ] - ''; - usb = '' - VFS.vfs [ VFS.leafAttrs "rump" (toMap { fs = "ext2fs", ram="12M" }) ] - ''; - }.${config.genode.boot.storeBackend}; + genode.core.children = + # Component to steer the store_fs to a specific partition + (if config.genode.boot.storeBackend == "usb" then { + part_block.configFile = builtins.toFile "part_block.dhall" '' + let Genode = env:DHALL_GENODE - storeResources = { - tarball = "Init.Resources.default"; - usb = "Init.Resources::{ caps = 256, ram = Genode.units.MiB 16 }"; - }.${config.genode.boot.storeBackend}; + let Init = Genode.Init - in builtins.toFile "store_fs.dhall" '' - let Genode = env:DHALL_GENODE - - let Init = Genode.Init - - let VFS = Genode.VFS - - in Init.Child.flat - Init.Child.Attributes::{ - , binary = "vfs" - , resources = ${storeResources} - , config = Init.Config::{ - , content = [ ${storeVfsConfig} ] - , policies = - [ Init.Config.Policy::{ - , service = "File_system" - , label = Init.LabelSelector.suffix "nix-store" - , attributes = toMap { root = "/nix/store" } + in Init.Child.flat + Init.Child.Attributes::{ + , binary = "part_block" + , resources = Init.Resources::{ ram = Genode.units.MiB 8 } + , config = Init.Config::{ + , attributes = toMap { ignore_mbr = "yes" } + , policies = + [ Init.Config.Policy::{ + , service = "Block" + , label = Init.LabelSelector.prefix "store_fs" + , attributes = toMap + { partition = "1" + , writeable = "yes" + , TODO = "select by partition UUID" + } + } + ] } - , Init.Config.Policy::{ - , service = "File_system" - , label = Init.LabelSelector.prefix "store_rom" - , attributes = toMap { root = "/" } + } + ''; + } else + { }) // { + store_fs.configFile = let + + storeVfsConfig = { + tarball = '' + VFS.vfs [ VFS.leafAttrs "tar" (toMap { name = "${config.system.build.tarball.fileName}.tar" }) ] + ''; + usb = '' + VFS.vfs [ VFS.leafAttrs "rump" (toMap { fs = "ext2fs", ram="12M" }) ] + ''; + }.${config.genode.boot.storeBackend}; + + storeResources = { + tarball = "Init.Resources.default"; + usb = "Init.Resources::{ caps = 256, ram = Genode.units.MiB 16 }"; + }.${config.genode.boot.storeBackend}; + + in builtins.toFile "store_fs.dhall" '' + let Genode = env:DHALL_GENODE + + let Init = Genode.Init + + let VFS = Genode.VFS + + in Init.Child.flat + Init.Child.Attributes::{ + , binary = "vfs" + , resources = ${storeResources} + , config = Init.Config::{ + , content = [ ${storeVfsConfig} ] + , policies = + [ Init.Config.Policy::{ + , service = "File_system" + , label = Init.LabelSelector.suffix "nix-store" + , attributes = toMap { root = "/nix/store" } + } + , Init.Config.Policy::{ + , service = "File_system" + , label = Init.LabelSelector.prefix "store_rom" + , attributes = toMap { root = "/" } + } + ] + } } - ] - } - , provides = [ "File_system" ] - } - ''; + ''; + }; genode.boot.configFile = let tarball = @@ -236,9 +276,21 @@ in { usb = [ pkgs.genodePackages.rump ]; }.${config.genode.boot.storeBackend}; - manifest = mergeManifests (map addManifest (with pkgs.genodePackages; - config.genode.core.basePackages ++ storeBackendInputs - ++ [ init cached_fs_rom jitter_sponge report_rom vfs ])); + coreInputs = with builtins; + concatMap (getAttr "inputs") (attrValues config.genode.core.children); + + manifest = + # Manifests are Dhall metadata to be attached to every + # package to be used for dynamically buildings enviroments + # using Dhall expressions. Probably not worth pursuing. + pkgs.writeText "manifest.dhall" (mergeManifests (map addManifest + (with pkgs.genodePackages; + config.genode.core.basePackages ++ storeBackendInputs + ++ [ init cached_fs_rom jitter_sponge report_rom vfs ] + ++ coreInputs)) + ''# [ { mapKey = "romModules", mapValue = [ '' + + (toString + (mapAttrsToList (k: v: '', { mapKey = "${k}", mapValue = "${v}" }'') + config.genode.boot.romModules)) + " ] } ]"); storeRomPolicies = mapAttrsToList (name: value: '', { mapKey = "${name}", mapValue = "${value}" }'') @@ -253,7 +305,7 @@ in { Genode.Init.LabelSelector.Type.Partial { prefix = Some "nixos -> ${name}", suffix = Some "${suffix}" } } - , route = Genode.Init.Route.parent (Some "${suffix}") + , route = Genode.Init.Route.parentLabel "${suffix}" } '') value.coreROMs) config.genode.init.children)); @@ -282,8 +334,8 @@ in { EOF ''; - genode.boot.storePaths = [ config.genode.init.configFile ] - ++ (builtins.attrValues romDirectories); + genode.boot.storePaths = with builtins; + [ config.genode.init.configFile ] ++ (attrValues romDirectories); # Create the tarball of the store to live in core ROM system.build.tarball = @@ -316,17 +368,21 @@ in { ''; system.build.bootDriveImage = let - storeFsImage = pkgs.callPackage ./lib/make-ext2-fs.nix { - inherit (config.genode.boot) storePaths; - inherit (config.system.build) qemu; - volumeLabel = "NIXOS_GENODE"; + espImage = import ./lib/make-esp-fs.nix { inherit config pkgs; }; + storeFsImage = + pkgs.callPackage ./lib/make-ext2-fs.nix { inherit config pkgs; }; + bootDriveImage = import ./lib/make-bootable-image.nix { + inherit config pkgs espImage storeFsImage; }; - in storeFsImage; + in bootDriveImage; + + # virtualisation.useEFIBoot = config.genode.boot.storeBackend == "usb"; virtualisation.qemu.options = lib.optionals (config.genode.boot.storeBackend == "usb") [ - "-usb" + "-bios ${pkgs.buildPackages.OVMF.fd}/FV/OVMF.fd" "-drive id=usbdisk,file=${config.system.build.bootDriveImage},if=none,readonly" + "-usb" "-device usb-storage,drive=usbdisk" ]; diff --git a/nixos-modules/lib/make-bootable-image.nix b/nixos-modules/lib/make-bootable-image.nix new file mode 100644 index 0000000..3a12cbf --- /dev/null +++ b/nixos-modules/lib/make-bootable-image.nix @@ -0,0 +1,69 @@ +# Builds a compressed EFI System Partition image +{ config, pkgs, espImage, storeFsImage }: + +pkgs.stdenv.mkDerivation { + name = "boot.qcow2"; + + nativeBuildInputs = with pkgs.buildPackages; [ + config.system.build.qemu + utillinux + zstd + ]; + + buildCommand = '' + img=./temp.raw + blockSize=512 + sectorSize=$(( $blockSize * 1 )) + imgBytes=0 + + espSectorOffset=2048 + esbByteOffset=$(( $espSectorOffset * $sectorSize )) + + # Pad the front of the image + echo "Pad front of image with " $esbByteOffset " bytes" + truncate --size=$esbByteOffset $img + + # Concatentenate the ESP + echo "Concatenate ESP ${espImage}" + zstdcat ${espImage} >> $img + + imgBytes=$(stat --format=%s $img) + echo "Image is $(( $imgBytes >> 20 )) MiB with ESP partition" + + nixSectorOffset=$(( ($imgBytes + $sectorSize - 1) / $sectorSize )) + nixByteOffset=$(( $nixSectorOffset * $sectorSize )) + + # Pad the ESP + echo "Pad end of ESP with " $(( $nixByteOffset - $imgBytes )) " bytes" + truncate --size=$nixByteOffset $img + + # Concatenate the nix partition + echo "Concatenate store ${storeFsImage}" + zstdcat ${storeFsImage} >> $img + + imgBytes=$(stat --format=%s $img) + echo "Image is $(( $imgBytes >> 20 )) MiB with store partition" + + endSectorOffset=$(( ($(stat --format=%s $img) + $sectorSize + 1) / $sectorSize )) + endByteOffset=$(( $endSectorOffset * $sectorSize )) + + # Pad the end of the image + echo "Pad end of store with $(( $endByteOffset - $imgBytes )) bytes" + truncate --size=$endByteOffset $img + + imgBytes=$(stat --format=%s $img) + echo "Image is $(( $imgBytes >> 20 )) MiB with final padding" + + efiUuid=C12A7328-F81F-11D2-BA4B-00A0C93EC93B + + # Create the partition table + sfdisk $img < embedded.cfg + insmod configfile + insmod efi_gop + insmod efi_uga + insmod ext2 + insmod normal + insmod part_gpt + insmod search_fs_uuid + search.fs_uuid ${config.genode.boot.storeFsUuid} root + set prefix=($root)/boot/grub + configfile /boot/grub/grub.cfg + EOF + + grub-script-check embedded.cfg + + ${grub'}/bin/grub-mkimage \ + --config=embedded.cfg \ + --output=$bootdir/boot${targetArch}.efi \ + --prefix=/boot/grub \ + --format=${grub'.grubTarget} \ + $MODULES + + # Make the ESP image twice as large as necessary + imageBytes=$(du --summarize --block-size=4096 --total $bootdir | tail -1 | awk '{ print int($1 * 8192) }') + + truncate --size=$imageBytes $img + mkfs.vfat -n EFIBOOT --invariant $img + mcopy -sv -i $img EFI :: + fsck.vfat -nv $img + + zstd --verbose --no-progress ./$img -o $out + ''; +} diff --git a/nixos-modules/lib/make-ext2-fs.nix b/nixos-modules/lib/make-ext2-fs.nix index 78b273c..b2dc092 100644 --- a/nixos-modules/lib/make-ext2-fs.nix +++ b/nixos-modules/lib/make-ext2-fs.nix @@ -1,83 +1,77 @@ -# Builds an ext2 image containing a populated /nix/store with the closure -# of store paths passed in the storePaths parameter, in addition to the -# contents of a directory that can be populated with commands. The -# generated image is sized to only fit its contents, with the expectation -# that a script resizes the filesystem at boot time. -{ pkgs -, lib -# List of derivations to be included -, storePaths -# Shell commands to populate the ./files directory. -# All files in that directory are copied to the root of the FS. -, populateImageCommands ? "" -, volumeLabel -, uuid ? "44444444-4444-4444-8888-888888888888" -, e2fsprogs -, libfaketime -, perl -, fakeroot -, qemu -}: +{ config, pkgs }: let - sdClosureInfo = pkgs.buildPackages.closureInfo { rootPaths = storePaths; }; -in -pkgs.stdenv.mkDerivation { - name = "ext2-fs.qcow2"; + grub' = pkgs.buildPackages.grub2_efi; + sdClosureInfo = pkgs.buildPackages.closureInfo { + rootPaths = config.genode.boot.storePaths; + }; +in pkgs.stdenv.mkDerivation { + name = "ext2-fs.img.zstd"; - nativeBuildInputs = [ e2fsprogs.bin libfaketime perl fakeroot qemu ]; + nativeBuildInputs = with pkgs.buildPackages; [ + e2fsprogs.bin + grub' + libfaketime + perl + fakeroot + zstd + ]; - buildCommand = - '' - img=temp.raw - ( - mkdir -p ./files - ${populateImageCommands} - ) + buildCommand = '' + img=temp.raw - echo "Preparing store paths for image..." + # Create nix/store before copying path + mkdir -p ./rootImage/boot/grub ./rootImage/nix/store - # Create nix/store before copying path - mkdir -p ./rootImage/nix/store + cat < ./rootImage/boot/grub/grub.cfg + set timeout=3 + set default=0 + set gfxpayload=auto - xargs -I % cp -a --reflink=auto % -t ./rootImage/nix/store/ < ${sdClosureInfo}/store-paths - ( - GLOBIGNORE=".:.." - shopt -u dotglob + ${config.boot.loader.grub.extraEntries} + EOF - for f in ./files/*; do - cp -a --reflink=auto -t ./rootImage/ "$f" - done - ) + grub-script-check ./rootImage/boot/grub/grub.cfg - # Also include a manifest of the closures in a format suitable for nix-store --load-db - cp ${sdClosureInfo}/registration ./rootImage/nix-path-registration + xargs -I % cp -a --reflink=auto % -t ./rootImage/nix/store/ < ${sdClosureInfo}/store-paths + ( + GLOBIGNORE=".:.." + shopt -u dotglob - # Make a crude approximation of the size of the target image. - # If the script starts failing, increase the fudge factors here. - numInodes=$(find ./rootImage | wc -l) - numDataBlocks=$(du -s -c -B 4096 --apparent-size ./rootImage | tail -1 | awk '{ print int($1 * 1.10) }') - bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks)) - echo "Creating an EXT2 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)" + for f in ./files/*; do + cp -a --reflink=auto -t ./rootImage/ "$f" + done + ) - truncate -s $bytes $img + # Also include a manifest of the closures in a format suitable for nix-store --load-db + cp ${sdClosureInfo}/registration ./rootImage/nix-path-registration - faketime -f "1970-01-01 00:00:01" fakeroot mkfs.ext2 -L ${volumeLabel} -U ${uuid} -d ./rootImage $img + # Make a crude approximation of the size of the target image. + # If the script starts failing, increase the fudge factors here. + numInodes=$(find ./rootImage | wc -l) + numDataBlocks=$(du -s -c -B 4096 --apparent-size ./rootImage | tail -1 | awk '{ print int($1 * 1.10) }') + bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks)) + echo "Creating an EXT2 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)" - export EXT2FS_NO_MTAB_OK=yes - # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build. - if ! fsck.ext2 -n -f $img; then - echo "--- Fsck failed for EXT2 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---" - cat errorlog - return 1 - fi + truncate --size=$bytes $img - echo "Resizing to minimum allowed size" - resize2fs -M $img + faketime -f "1970-01-01 00:00:01" fakeroot mkfs.ext2 -L NIXOS_GENODE -U ${config.genode.boot.storeFsUuid} -d ./rootImage $img - # And a final fsck, because of the previous truncating. - fsck.ext2 -n -f $img + export EXT2FS_NO_MTAB_OK=yes + # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build. + if ! fsck.ext2 -n -f $img; then + echo "--- Fsck failed for EXT2 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---" + cat errorlog + return 1 + fi - qemu-img convert $img $out - ''; + echo "Resizing to minimum allowed size" + resize2fs -M $img + + # And a final fsck, because of the previous truncating. + fsck.ext2 -n -f $img + + # Compress to store + zstd --verbose --no-progress ./$img -o $out + ''; } diff --git a/nixos-modules/nova.nix b/nixos-modules/nova.nix index da52d0e..0ba09c4 100644 --- a/nixos-modules/nova.nix +++ b/nixos-modules/nova.nix @@ -7,6 +7,17 @@ let inherit (config.nixpkgs) system localSystem crossSystem; inherit pkgs; }; + + bootDir = pkgs.runCommand "${config.system.name}-bootdir" { } '' + mkdir $out + gz() { + gzip --keep --to-stdout "$1" > "$2" + } + gz ${pkgs.genodePackages.genodeSources}/tool/boot/bender $out/bender.gz + gz ${pkgs.genodePackages.NOVA}/hypervisor-x86_64 $out/hypervisor.gz + gz ${config.genode.boot.image}/image.elf $out/image.elf.gz + ''; + in { genode.core = { prefix = "nova-"; @@ -15,13 +26,27 @@ in { }; genode.boot = { - - initrd = - "'${pkgs.genodePackages.NOVA}/hypervisor-x86_64 arg=iommu novpid serial,${config.genode.boot.image}/image.elf'"; - image = utils.novaImage config.system.name { } config.genode.boot.configFile; - }; + genode.boot.storePaths = + lib.optional (config.genode.boot.storeBackend == "usb") bootDir; + + virtualisation.qemu.options = + lib.optionals (!config.virtualisation.useBootLoader) [ + "-kernel '${pkgs.genodePackages.bender}/share/bender/bender'" + "-initrd '${pkgs.genodePackages.NOVA}/hypervisor-x86_64 arg=iommu logmem novpid serial,${config.genode.boot.image}/image.elf'" + ]; + + boot.loader.grub.extraEntries = '' + menuentry 'Genode on NOVA' { + insmod multiboot2 + insmod gzio + multiboot2 ${bootDir}/bender.gz serial_fallback + module2 ${bootDir}/hypervisor.gz hypervisor iommu logmem novga novpid serial + module2 ${bootDir}/image.elf.gz image.elf + } + ''; + } diff --git a/nixos-modules/partition-type b/nixos-modules/partition-type new file mode 100644 index 0000000..2cca25d --- /dev/null +++ b/nixos-modules/partition-type @@ -0,0 +1 @@ +"24b69406-18a1-428d-908e-d21a1437122c" diff --git a/nixos-modules/qemu-vm.nix b/nixos-modules/qemu-vm.nix index 9f91cb0..d0b0feb 100644 --- a/nixos-modules/qemu-vm.nix +++ b/nixos-modules/qemu-vm.nix @@ -83,9 +83,9 @@ let imap1 (idx: drive: drive // { device = driveDeviceName idx; }); efiPrefix = if (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) then - "${pkgs.OVMF.fd}/FV/OVMF" + "${pkgs.buildPackages.OVMF.fd}/FV/OVMF" else if pkgs.stdenv.isAarch64 then - "${pkgs.OVMF.fd}/FV/AAVMF" + "${pkgs.buildPackages.OVMF.fd}/FV/AAVMF" else throw "No EFI firmware available for platform"; efiFirmware = "${efiPrefix}_CODE.fd"; @@ -516,10 +516,6 @@ in { "-device usb-kbd" "-device usb-tablet" ]) - (mkIf (!cfg.useBootLoader) [ - "-kernel ${config.genode.boot.kernel}" - "-initrd ${config.genode.boot.initrd}" - ]) (mkIf cfg.useEFIBoot [ "-drive if=pflash,format=raw,unit=0,readonly,file=${efiFirmware}" "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS" diff --git a/nixos-modules/store-fs-uuid b/nixos-modules/store-fs-uuid new file mode 100644 index 0000000..7325548 --- /dev/null +++ b/nixos-modules/store-fs-uuid @@ -0,0 +1 @@ +"9668f8dd-d9a0-4398-a55a-0d499d5e5cbb"