{"id":1139,"date":"2018-04-19T17:34:51","date_gmt":"2018-04-20T00:34:51","guid":{"rendered":"https:\/\/outflux.net\/blog\/?p=1139"},"modified":"2020-11-30T22:05:41","modified_gmt":"2020-12-01T06:05:41","slug":"uefi-booting-and-raid1","status":"publish","type":"post","link":"https:\/\/outflux.net\/blog\/archives\/2018\/04\/19\/uefi-booting-and-raid1\/","title":{"rendered":"UEFI booting and RAID1"},"content":{"rendered":"<p>I spent some time yesterday building out a UEFI server that didn&#8217;t have on-board hardware RAID for its system drives. In these situations, I always use Linux&#8217;s <code>md<\/code> RAID1 for the root filesystem (and\/or <code>\/boot<\/code>). This worked well for BIOS booting since BIOS just transfers control blindly to the MBR of whatever disk it sees (modulo finding a &#8220;bootable partition&#8221; flag, etc, etc). This means that BIOS doesn&#8217;t really care what&#8217;s on the drive, it&#8217;ll hand over control to the GRUB code in the MBR.<\/p>\n<p>With UEFI, the boot firmware is actually examining the GPT partition table, looking for the partition marked with the &#8220;EFI System Partition&#8221; (ESP) UUID. Then it looks for a FAT32 filesystem there, and does more things like looking at NVRAM boot entries, or just running <code>BOOT\/EFI\/BOOTX64.EFI<\/code> from the FAT32. Under Linux, this <code>.EFI<\/code> code is either GRUB itself, or Shim which loads GRUB.<\/p>\n<p>So, if I want RAID1 for my root filesystem, that&#8217;s fine (GRUB will read <code>md<\/code>, LVM, etc), but how do I handle <code>\/boot\/efi<\/code> (the UEFI ESP)? In everything I found answering this question, the answer was &#8220;oh, just manually make an ESP on each drive in your RAID and copy the files around, add a separate NVRAM entry (with <code>efibootmgr<\/code>) for each drive, and you&#8217;re fine!&#8221; I did not like this one bit since it meant things could get out of sync between the copies, etc.<\/p>\n<p>The current implementation of Linux&#8217;s <code>md<\/code> RAID puts metadata at the <em>front<\/em> of a partition. This solves more problems than it creates, but it means the RAID isn&#8217;t &#8220;invisible&#8221; to something that doesn&#8217;t know about the metadata. In fact, <code>mdadm<\/code> warns about this pretty loudly:<\/p>\n<pre><blockquote># mdadm --create \/dev\/md0 --level 1 --raid-disks 2 \/dev\/sda1 \/dev\/sdb1\r\nmdadm: Note: this array has metadata at the start and\r\n    may not be suitable as a boot device.  If you plan to\r\n    store '\/boot' on this device please ensure that\r\n    your boot-loader understands md\/v1.x metadata, or use\r\n    --metadata=0.90\r\n<\/blockquote><\/pre>\n<p>Reading from the mdadm man page:<\/p>\n<pre><blockquote>   -e, --metadata=\r\n...\r\n          1, 1.0, 1.1, 1.2 default\r\n                 Use  the new version-1 format superblock.  This has fewer\r\n                 restrictions.  It can easily be moved between hosts  with\r\n                 different  endian-ness,  and  a recovery operation can be\r\n                 checkpointed and restarted.  The  different  sub-versions\r\n                 store  the  superblock  at  different  locations  on  the\r\n                 device, either at the end (for 1.0), at  the  start  (for\r\n                 1.1)  or  4K from the start (for 1.2).  \"1\" is equivalent\r\n                 to \"1.2\" (the commonly preferred 1.x format).   \"default\"\r\n                 is equivalent to \"1.2\".\r\n<\/blockquote><\/pre>\n<p>First we toss a FAT32 on the RAID (<code>mkfs.fat -F32 \/dev\/md0<\/code>), and looking at the results, the first 4K is entirely zeros, and <code>file<\/code> doesn&#8217;t see a filesystem:<\/p>\n<pre><blockquote># dd if=\/dev\/sda1 bs=1K count=5 status=none | hexdump -C\r\n00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|\r\n*\r\n00001000  fc 4e 2b a9 01 00 00 00  00 00 00 00 00 00 00 00  |.N+.............|\r\n...\r\n# file -s \/dev\/sda1\r\n\/dev\/sda1: Linux Software RAID version 1.2 ...\r\n<\/blockquote><\/pre>\n<p>So, instead, we&#8217;ll use <code>--metadata 1.0<\/code> to put the RAID metadata at the end:<\/p>\n<pre><blockquote># mdadm --create \/dev\/md0 --level 1 --raid-disks 2 --metadata 1.0 \/dev\/sda1 \/dev\/sdb1\r\n...\r\n# mkfs.fat -F32 \/dev\/md0\r\n# dd if=\/dev\/sda1 bs=1 skip=80 count=16 status=none | xxd\r\n00000000: 2020 4641 5433 3220 2020 0e1f be77 7cac    FAT32   ...w|.\r\n# file -s \/dev\/sda1\r\n\/dev\/sda1: ... FAT (32 bit)\r\n<\/blockquote><\/pre>\n<p>Now we have a visible FAT32 filesystem on the ESP. UEFI should be able to boot whatever disk hasn&#8217;t failed, and <code>grub-install<\/code> will write to the RAID mounted at <code>\/boot\/efi<\/code>.<\/p>\n<p>However, we&#8217;re left with a new problem: on (at least) Debian and Ubuntu, <code>grub-install<\/code> attempts to run <code>efibootmgr<\/code> to record which disk UEFI should boot from. This fails, though, since it <a href=\"https:\/\/bugs.launchpad.net\/ubuntu\/+source\/grub2\/+bug\/1765484\">expects a single disk<\/a>, not a RAID set. In fact, it returns <em>nothing<\/em>, and tries to run <code>efibootmgr<\/code> with an empty <code>-d<\/code> argument:<\/p>\n<pre><blockquote>Installing for x86_64-efi platform.\r\nefibootmgr: option requires an argument -- 'd'\r\n...\r\ngrub-install: error: efibootmgr failed to register the boot entry: Operation not permitted.\r\nFailed: grub-install --target=x86_64-efi  \r\nWARNING: Bootloader is not properly installed, system may not be bootable\r\n<\/blockquote><\/pre>\n<p>Luckily my UEFI boots without NVRAM entries, and I can disable the NVRAM writing via the &#8220;Update NVRAM variables to automatically boot into Debian?&#8221; debconf prompt when running: <code>dpkg-reconfigure -p low grub-efi-amd64<\/code><\/p>\n<p>So, now my system will boot with both or either drive present, and updates from Linux to <code>\/boot\/efi<\/code> are visible on all RAID members at boot-time. <strong>HOWEVER<\/strong> there is one nasty risk with this setup: if UEFI writes anything to one of the drives (which this firmware <em>did<\/em> when it wrote out a &#8220;boot variable cache&#8221; file), it may lead to corrupted results once Linux mounts the RAID (since the member drives won&#8217;t have identical block-level copies of the FAT32 any more).<\/p>\n<p>To deal with this &#8220;external write&#8221; situation, I see some solutions:<\/p>\n<ul>\n<li>Make the partition read-only when not under Linux. (I don&#8217;t think this is a thing.)<\/li>\n<li>Create higher-level knowledge of the root-filesystem RAID configuration is needed to keep a collection of filesystems manually synchronized instead of doing block-level RAID. (Seems like a lot of work and would need redesign of <code>\/boot\/efi<\/code> into something like <code>\/boot\/efi\/booted<\/code>, <code>\/boot\/efi\/spare1<\/code>, <code>\/boot\/efi\/spare2<\/code>, etc)<\/li>\n<li>Prefer one RAID member&#8217;s copy of <code>\/boot\/efi<\/code> and rebuild the RAID at every boot. If there were no external writes, there&#8217;s no issue. (Though what&#8217;s really the right way to pick the copy to prefer?)<\/li>\n<\/ul>\n<p>Since <code>mdadm<\/code> has the &#8220;<code>--update=resync<\/code>&#8221; assembly option, I can actually do the latter option. This required updating <code>\/etc\/mdadm\/mdadm.conf<\/code> to add <a href=\"https:\/\/manpages.ubuntu.com\/mdadm.conf\"><code>&lt;ignore&gt;<\/code> on the RAID&#8217;s <code>ARRAY<\/code> line<\/a> to keep it from auto-starting:<\/p>\n<pre><blockquote>ARRAY &lt;ignore&gt; metadata=1.0 UUID=123...\r\n<\/blockquote><\/pre>\n<p>(Since it&#8217;s ignored, I&#8217;ve chosen <code>\/dev\/md100<\/code> for the manual assembly below.) Then I added the <code>noauto<\/code> option to the <code>\/boot\/efi<\/code> entry in <code>\/etc\/fstab<\/code>:<\/p>\n<pre><blockquote>\/dev\/md100 \/boot\/efi vfat noauto,defaults 0 0\r\n<\/blockquote><\/pre>\n<p>And finally I added a <code>systemd<\/code> oneshot service that assembles the RAID with resync and mounts it:<\/p>\n<pre><blockquote>[Unit]\r\nDescription=Resync \/boot\/efi RAID\r\nDefaultDependencies=no\r\nAfter=local-fs.target\r\n\r\n[Service]\r\nType=oneshot\r\nExecStart=\/sbin\/mdadm -A \/dev\/md100 --uuid=123... --update=resync\r\nExecStart=\/bin\/mount \/boot\/efi\r\nRemainAfterExit=yes\r\n\r\n[Install]\r\nWantedBy=sysinit.target\r\n<\/blockquote><\/pre>\n<p>(And don&#8217;t forget to run &#8220;<code>update-initramfs -u<\/code>&#8221; so the initramfs has an updated copy of <code>\/dev\/mdadm\/mdadm.conf<\/code>.)<\/p>\n<p>If <code>mdadm.conf<\/code> supported an &#8220;<code>update=<\/code>&#8221; option for <code>ARRAY<\/code> lines, this would have been trivial. Looking at the source, though, that kind of change doesn&#8217;t look easy. I can dream!<\/p>\n<p>And if I wanted to keep a &#8220;pristine&#8221; version of <code>\/boot\/efi<\/code> that UEFI couldn&#8217;t update I could rearrange things more dramatically to keep the primary RAID member as a loopback device on a file in the root filesystem (e.g. <code>\/boot\/efi.img<\/code>). This would make all external changes in the real ESPs disappear after resync. Something like:<\/p>\n<pre><blockquote>\r\n# truncate --size 512M \/boot\/efi.img\r\n# losetup -f --show \/boot\/efi.img\r\n\/dev\/loop0\r\n# mdadm --create \/dev\/md100 --level 1 --raid-disks 3 --metadata 1.0 \/dev\/loop0 \/dev\/sda1 \/dev\/sdb1\r\n<\/blockquote><\/pre>\n<p>And at boot just rebuild it from <code>\/dev\/loop0<\/code>, though I&#8217;m not sure how to &#8220;prefer&#8221; that partition&#8230;<\/p>\n<p>Notes: commands I used to muck with grub-install:<\/p>\n<ul>\n<li><code>echo \"grub-pc grub2\/update_nvram boolean false\" | debconf-set-selections<\/code><\/li>\n<li><code>echo \"grub-pc grub-efi\/install_devices multiselect \/dev\/md0\" | debconf-set-selections && dpkg-reconfigure -p low grub-efi-amd64-signed<\/code><\/li>\n<\/ul>\n<p>Update: Ubuntu Focal 20.04 now provides a way for GRUB to install to an arbitrary collection of devices, so no RAID needed. Whew.<\/p>\n<p style='text-align:left'>&copy; 2018 &#8211; 2020, <a href=\"https:\/\/outflux.net\/blog\/\">Kees Cook<\/a>. This work is licensed under a <a rel=\"license\" href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/4.0\/\">Creative Commons Attribution-ShareAlike 4.0 License<\/a>.<br \/><a rel=\"license\" href=\"http:\/\/creativecommons.org\/licenses\/by-sa\/4.0\/\"><img decoding=\"async\" alt=\"CC BY-SA 4.0\" style=\"border-width:0\" src=\"https:\/\/i.creativecommons.org\/l\/by-sa\/4.0\/88x31.png\" \/><\/a> <\/p>\n","protected":false},"excerpt":{"rendered":"<p>I spent some time yesterday building out a UEFI server that didn&#8217;t have on-board hardware RAID for its system drives. In these situations, I always use Linux&#8217;s md RAID1 for the root filesystem (and\/or \/boot). This worked well for BIOS booting since BIOS just transfers control blindly to the MBR of whatever disk it sees [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18,21,14,19],"tags":[],"_links":{"self":[{"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/posts\/1139"}],"collection":[{"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/comments?post=1139"}],"version-history":[{"count":29,"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/posts\/1139\/revisions"}],"predecessor-version":[{"id":1492,"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/posts\/1139\/revisions\/1492"}],"wp:attachment":[{"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/media?parent=1139"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/categories?post=1139"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/outflux.net\/blog\/wp-json\/wp\/v2\/tags?post=1139"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}