FreeBSD upgrade procedure using GPT

Feb. 1, 2019, 1:34 a.m.

You have a few different ways to upgrade your operating system. For example, if you are using ZFS as your root file system you can use boot environments to accomplish this task in a quick, simple and safe way. Boot environments are the new ‘it’ thing in FreeBSD. However, today we will discuss a more old-school approach to this problem using the GPT flags. This approach may be used to upgrade your operating system if you don't use ZFS, and you want your system to be on the read-only partition, for example when you are building an appliance.

To analyze this challenge, we will use the GPT poster made by Jared Atkinson.

The GPT is divided into two parts: GPT Header and partition array. As we can see every partition entry contains some attributes. Each operating system may implement a different one. According to Wikipedia Microsoft is already using bits 60 (read-only), 62 (hidden) and 63 (do not automount). FreeBSD implements three extra ones: BOOTME, BOOTONCE and BOOTFAILED, that respectively use the 59th, 58th and 57th bit - which are not used by antyhing else. The BOOTME attribute points to the partition which should be used to boot from. If there is a partition with two flags, BOOTME and BOOTONCE, then this partition is chosen to be booted from, but it will be used only once. How does bootloader accomplish that? Simply by removing the BOOTME attribute from it. The BOOTFAILED attribute is used to indicate that this partition was used once but the booting process was interrupted.

Let's look at a sample scenario of a failed boot. Let's create a GPT table which contains 4 partitions:
# gpart create -s GPT md0
# gpart add -t freebsd-boot -s64k md0
# gpart add -t freebsd-ufs -s4G md0
# gpart add -t freebsd-ufs md0
# gpart set -a bootme -i2 md0
# gpart show md0
=>      40  25165744  md0  GPT  (12G)
        40       128    1  freebsd-boot  (64K)
       168   8388608    2  freebsd-ufs  [bootme]  (4.0G)
   8388776   8388608    3  freebsd-ufs (4.0G)
  16777384   8388400    4  freebsd-ufs (4.0G)

The scenario goes like:

  • Boot from partition UFS with flag BOOTME (md0p2)
  • Upload upgrade and override UFS partition
    # dd if=some.img of=/dev/md0p3
  • Set BOOTONCE and BOOTME flags on md0p3. Setting the BOOTONCE will automatically also set BOOTME attribute. # gpart set -a bootonce -i3 md0
    # gpart show md0
    =>      40  25165744  md0  GPT  (12G)
            40       128    1  freebsd-boot (64K)
           168   8388608    2  freebsd-ufs [bootme] (4.0G)
       8388776   8388608    3  freebsd-ufs [bootonce,bootme] (4.0G)
      16777384   8388400    4  freebsd-ufs (4.0G)
  • Reboot machine
  • The BOOTME flag is removed by the bootloader
           168   8388608    2  freebsd-ufs [bootme] (4.0G)
       8388776   8388608    3  freebsd-ufs [bootonce] (4.0G)
  • Upgrade error accrued (for example we were unable to boot kernel)
  • Machine is rebooted
  • Bootloader will replace the BOOTONCE flag with BOOTFAILED
           168   8388608    2  freebsd-ufs [bootme] (4.0G)
       8388776   8388608    3  freebsd-ufs [bootfailed] (4.0G)
  • If there is another partition with BOOTONCE and BOOTME attributes it will be tried now
  • Boot from partition UFS with flag BOOTME
  • Report failure

When an upgrade was successful the difference is that the system will remove the BOOTONCE flag and report the success. The reporting is done by the startup script /etc/rc.d/gptboot. If we are using the BOOTONCE flag as an upgrade procedure, it is a good idea to modify this script and if the upgrade was successful change BOOTONCE to a BOOTME flag and remove other BOOTME flags.

The logic in the bootloader is also more complicated than presented here. We can have multiple BOOTONCE and BOOTME attributes. If these are set and the only problem is with the booting kernel there is no need to reboot the machine; the loader will try other kernels. I pointed out that I prefer to do the reboot because it is useful if our upgrade process is handling more complicated things like for example upgrading the database. We can imagine that we have one additional partition with data which contains the database instance. In one of the startup scripts, we are upgrading the database. If the upgrade fails, we roll back the changes and boot from the old partition.