Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add firmware validation check and automatic revert #58

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions fwup-mark-valid.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Mark firmware valid for x86_64
#
# To use:
# 1. Run `fwup -c -f fwup-mark-valid.conf -o mark_valid.fw` and copy
# mark_valid.fw to the device. This is done automatically as part of the Nerves
# system build process. The file is stored in `/usr/share/fwup/mark_valid.fw`.
# 2. On the device, run `fwup -t mark_valid mark_valid.fw -d $NERVES_FW_DEVPATH`.
#
# It is critical that this is kept in sync with the main fwup.conf.

require-fwup-version="0.19.0"

#
# Firmware metadata
#

# All of these can be overriden using environment variables of the same name.
#
# Run 'fwup -m' to query values in a .fw file.
# Use 'fw_printenv' to query values on the target.
#
# These are used by Nerves libraries to introspect.
define(NERVES_FW_PRODUCT, "Nerves Firmware")
define(NERVES_FW_DESCRIPTION, "")
define(NERVES_FW_VERSION, "${NERVES_SDK_VERSION}")
define(NERVES_FW_PLATFORM, "x86_64")
define(NERVES_FW_ARCHITECTURE, "x86_64")
define(NERVES_FW_AUTHOR, "The Nerves Team")

# This variable is used to control whether firmware is validated automatically
define(NERVES_FW_AUTOVALIDATE, "1")

# /dev/rootdisk* paths are provided by erlinit to refer to the disk and partitions
# on whatever drive provides the rootfs.
define(NERVES_FW_DEVPATH, "/dev/rootdisk0")
define(NERVES_FW_APPLICATION_PART0_DEVPATH, "/dev/rootdisk0p4") # Linux part number is 1-based
define(NERVES_FW_APPLICATION_PART0_FSTYPE, "ext4")
define(NERVES_FW_APPLICATION_PART0_TARGET, "/root")

# Default paths if not specified via the commandline
define(ROOTFS, "${NERVES_SYSTEM}/images/rootfs.squashfs")

# This configuration file will create an image that has an MBR and the
# following 3 partitions:
#
# +----------------------------+
# | MBR w/ Grub boot code |
# +----------------------------+
# | grub.img (<256K) |
# +----------------------------+
# | Firmware configuration data|
# | (formatted as uboot env) |
# +----------------------------+
# | Boot partition (FAT32) |
# | /boot/grub/grub.cfg |
# | /boot/grub/grubenv |
# +----------------------------+
# | p2: Rootfs A (squashfs) |
# +----------------------------+
# | p3: Rootfs B (squashfs) |
# +----------------------------+
# | p4: Application (ext4) |
# +----------------------------+

define(UBOOT_ENV_OFFSET, 2048)
define(UBOOT_ENV_COUNT, 16) # 8 KB

# The boot partition contains the bootloader configuration
# 16 MB should be plenty for now.
define(BOOT_PART_OFFSET, 4096)
define(BOOT_PART_COUNT, 31232)

# Let the rootfs have room to grow up to 256 MiB
define-eval(ROOTFS_A_PART_OFFSET, "${BOOT_PART_OFFSET} + ${BOOT_PART_COUNT}")
define(ROOTFS_A_PART_COUNT, 524288)
define-eval(ROOTFS_B_PART_OFFSET, "${ROOTFS_A_PART_OFFSET} + ${ROOTFS_A_PART_COUNT}")
define(ROOTFS_B_PART_COUNT, ${ROOTFS_A_PART_COUNT})

# Application data partition (10 GiB). This can be enlarged
# to fill the entire volume.
define-eval(APP_PART_OFFSET, "${ROOTFS_B_PART_OFFSET} + ${ROOTFS_B_PART_COUNT}")
define(APP_PART_COUNT, 20971520)

# Firmware archive metadata
meta-product = ${NERVES_FW_PRODUCT}
meta-description = ${NERVES_FW_DESCRIPTION}
meta-version = ${NERVES_FW_VERSION}
meta-platform = ${NERVES_FW_PLATFORM}
meta-architecture = ${NERVES_FW_ARCHITECTURE}
meta-author = ${NERVES_FW_AUTHOR}
meta-vcs-identifier = ${NERVES_FW_VCS_IDENTIFIER}
meta-misc = ${NERVES_FW_MISC}

file-resource grubenv_a {
host-path = "${NERVES_SYSTEM}/images/grubenv_a_valid_1"
}
file-resource grubenv_b {
host-path = "${NERVES_SYSTEM}/images/grubenv_b_valid_1"
}

mbr mbr {
bootstrap-code-host-path = "${NERVES_SYSTEM}/images/boot.img"
signature = 0x04030201

partition 0 {
block-offset = ${BOOT_PART_OFFSET}
block-count = ${BOOT_PART_COUNT}
type = 0xc # FAT32
boot = true
}
partition 1 {
block-offset = ${ROOTFS_A_PART_OFFSET}
block-count = ${ROOTFS_A_PART_COUNT}
type = 0x83 # Linux
}
partition 2 {
block-offset = ${ROOTFS_B_PART_OFFSET}
block-count = ${ROOTFS_B_PART_COUNT}
type = 0x83 # Linux
}
partition 3 {
block-offset = ${APP_PART_OFFSET}
block-count = ${APP_PART_COUNT}
type = 0x83 # Linux
}
}

# Location where installed firmware information is stored.
# While this is called "u-boot", u-boot isn't involved in this
# setup. It just provides a convenient key/value store format.
uboot-environment uboot-env {
block-offset = ${UBOOT_ENV_OFFSET}
block-count = ${UBOOT_ENV_COUNT}
}

task mark_valid.a {
# Check that the firmware has not already been marked valid
require-uboot-variable(uboot-env, "nerves_fw_validated", "0")

on-resource grubenv_a {
info("Marking firmware valid")
uboot_setenv(uboot-env, "nerves_fw_validated", "1")
fat_write(${BOOT_PART_OFFSET}, "/boot/grub/grubenv")
}
}

task mark_valid.b {
# Check that the firmware has not already been marked valid
require-uboot-variable(uboot-env, "nerves_fw_validated", "0")

on-resource grubenv_b {
info("Marking firmware valid")
uboot_setenv(uboot-env, "nerves_fw_validated", "1")
fat_write(${BOOT_PART_OFFSET}, "/boot/grub/grubenv")
}
}

task mark_valid.already_valid {
on-init {
error("Firmware has already been marked valid")
}
}

# Run "fwup /usr/share/fwup/mark_valid.fw -t status -d /dev/rootdisk0 -q -U" to check the status.
task status.validated {
require-uboot-variable(uboot-env, "nerves_fw_validated", "1")
on-init { info("Validated") }
}
task status.unvalidated {
require-uboot-variable(uboot-env, "nerves_fw_validated", "0")
on-init { info("Unvalidated") }
}
6 changes: 4 additions & 2 deletions fwup-revert.conf
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ task revert.a {
on-resource grubenv_a {
info("Reverting to partition A")

# Switch over
# Switch over
uboot_setenv(uboot-env, "nerves_fw_active", "a")
uboot_setenv(uboot-env, "nerves_fw_validated", "1")
fat_write(${BOOT_PART_OFFSET}, "/boot/grub/grubenv")
}
}
Expand All @@ -161,8 +162,9 @@ task revert.b {
on-resource grubenv_b {
info("Reverting to partition B")

# Switch over
# Switch over
uboot_setenv(uboot-env, "nerves_fw_active", "b")
uboot_setenv(uboot-env, "nerves_fw_validated", "1")
fat_write(${BOOT_PART_OFFSET}, "/boot/grub/grubenv")
}
}
Expand Down
32 changes: 27 additions & 5 deletions fwup.conf
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ define(NERVES_FW_PLATFORM, "x86_64")
define(NERVES_FW_ARCHITECTURE, "x86_64")
define(NERVES_FW_AUTHOR, "The Nerves Team")

# This variable is used to control whether firmware is validated automatically
define(NERVES_FW_AUTOVALIDATE, "1")

# /dev/rootdisk* paths are provided by erlinit to refer to the disk and partitions
# on whatever drive provides the rootfs.
define(NERVES_FW_DEVPATH, "/dev/rootdisk0")
Expand Down Expand Up @@ -101,13 +104,13 @@ file-resource rootfs.img {
assert-size-lte = ${ROOTFS_A_PART_COUNT}
}
file-resource grubenv_a_valid {
host-path = "${NERVES_SYSTEM}/images/grubenv_a_valid"
host-path = "${NERVES_SYSTEM}/images/grubenv_a_valid_1"
}
file-resource grubenv_a {
host-path = "${NERVES_SYSTEM}/images/grubenv_a"
host-path = "${NERVES_SYSTEM}/images/grubenv_a_valid_${NERVES_FW_AUTOVALIDATE}"
}
file-resource grubenv_b {
host-path = "${NERVES_SYSTEM}/images/grubenv_b"
host-path = "${NERVES_SYSTEM}/images/grubenv_b_valid_${NERVES_FW_AUTOVALIDATE}"
}

mbr mbr {
Expand Down Expand Up @@ -164,6 +167,7 @@ task complete {

uboot_setenv(uboot-env, "nerves_fw_active", "a")
uboot_setenv(uboot-env, "nerves_fw_devpath", ${NERVES_FW_DEVPATH})
uboot_setenv(uboot-env, "nerves_fw_validated", "1")
uboot_setenv(uboot-env, "a.nerves_fw_application_part0_devpath", ${NERVES_FW_APPLICATION_PART0_DEVPATH})
uboot_setenv(uboot-env, "a.nerves_fw_application_part0_fstype", ${NERVES_FW_APPLICATION_PART0_FSTYPE})
uboot_setenv(uboot-env, "a.nerves_fw_application_part0_target", ${NERVES_FW_APPLICATION_PART0_TARGET})
Expand Down Expand Up @@ -205,6 +209,10 @@ task upgrade.a {
# on B.
require-uboot-variable(uboot-env, "nerves_fw_active", "b")

# Require that the running version of firmware has been validated.
# If it has not, then failing back is not guaranteed to work.
require-uboot-variable(uboot-env, "nerves_fw_validated", "1")

# Verify the expected platform/architecture
require-uboot-variable(uboot-env, "b.nerves_fw_platform", "${NERVES_FW_PLATFORM}")
require-uboot-variable(uboot-env, "b.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}")
Expand Down Expand Up @@ -244,7 +252,8 @@ task upgrade.a {
uboot_setenv(uboot-env, "a.nerves_fw_misc", ${NERVES_FW_MISC})
uboot_setenv(uboot-env, "a.nerves_fw_uuid", "\${FWUP_META_UUID}")

# Switch over to boot the new firmware
# Reset validation status and switch over to boot the new firmware
uboot_setenv(uboot-env, "nerves_fw_validated", "${NERVES_FW_AUTOVALIDATE}")
uboot_setenv(uboot-env, "nerves_fw_active", "a")
fat_write(${BOOT_PART_OFFSET}, "/boot/grub/grubenv")
}
Expand All @@ -258,6 +267,10 @@ task upgrade.b {
# on A.
require-uboot-variable(uboot-env, "nerves_fw_active", "a")

# Require that the running version of firmware has been validated.
# If it has not, then failing back is not guaranteed to work.
require-uboot-variable(uboot-env, "nerves_fw_validated", "1")

# Verify the expected platform/architecture
require-uboot-variable(uboot-env, "a.nerves_fw_platform", "${NERVES_FW_PLATFORM}")
require-uboot-variable(uboot-env, "a.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}")
Expand Down Expand Up @@ -295,7 +308,8 @@ task upgrade.b {
uboot_setenv(uboot-env, "b.nerves_fw_misc", ${NERVES_FW_MISC})
uboot_setenv(uboot-env, "b.nerves_fw_uuid", "\${FWUP_META_UUID}")

# Switch over to boot the new firmware
# Reset validation status and switch over to boot the new firmware
uboot_setenv(uboot-env, "nerves_fw_validated", "${NERVES_FW_AUTOVALIDATE}")
uboot_setenv(uboot-env, "nerves_fw_active", "b")
fat_write(${BOOT_PART_OFFSET}, "/boot/grub/grubenv")
}
Expand All @@ -304,6 +318,14 @@ task upgrade.b {
}
}

task upgrade.unvalidated {
require-uboot-variable(uboot-env, "nerves_fw_validated", "0")

on-init {
error("Please validate the running firmware before upgrading it again.")
}
}

task upgrade.wrong {
require-uboot-variable(uboot-env, "a.nerves_fw_platform", "${NERVES_FW_PLATFORM}")
require-uboot-variable(uboot-env, "a.nerves_fw_architecture", "${NERVES_FW_ARCHITECTURE}")
Expand Down
24 changes: 19 additions & 5 deletions grub.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,30 @@
#

# Load the environment for the validation/fallback settings
# (image validation/fallback not added here yet...)
load_env

# Boot A or B depending on which one is active
if [ $boot = 1 ]; then
echo "Booting partition B..."
linux (hd0,msdos3)/boot/bzImage root=PARTUUID=04030201-03 rootwait console=tty1
if [ $nerves_fw_booted = 1 ]; then
if [ $nerves_fw_validated = 0 ]; then
# Firmware is invalid and must be reverted
if [ $nerves_fw_active = "a" ]; then
set $nerves_fw_active = "b"
else
set $nerves_fw_active = "a"
fi
set $nerves_fw_validated = 1
save_env
fi
else
set $nerves_fw_booted = 1
save_env
fi

if [ $nerves_fw_active = "a" ]; then
echo "Booting partition A..."
linux (hd0,msdos2)/boot/bzImage root=PARTUUID=04030201-02 rootwait console=tty1
else
echo "Booting partition B..."
linux (hd0,msdos3)/boot/bzImage root=PARTUUID=04030201-03 rootwait console=tty1
fi

# Boot!!!
Expand Down
29 changes: 15 additions & 14 deletions post-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
set -e

# Create the Grub environment blocks
grub-editenv $BINARIES_DIR/grubenv_a create
grub-editenv $BINARIES_DIR/grubenv_a set boot=0
grub-editenv $BINARIES_DIR/grubenv_a set validated=0
grub-editenv $BINARIES_DIR/grubenv_a set booted_once=0
grub-editenv $BINARIES_DIR/grubenv_a_valid_0 create
grub-editenv $BINARIES_DIR/grubenv_a_valid_0 set nerves_fw_active=a
grub-editenv $BINARIES_DIR/grubenv_a_valid_0 set nerves_fw_validated=0
grub-editenv $BINARIES_DIR/grubenv_a_valid_0 set nerves_fw_booted=0

grub-editenv $BINARIES_DIR/grubenv_b create
grub-editenv $BINARIES_DIR/grubenv_b set boot=1
grub-editenv $BINARIES_DIR/grubenv_b set validated=0
grub-editenv $BINARIES_DIR/grubenv_b set booted_once=0
grub-editenv $BINARIES_DIR/grubenv_b_valid_0 create
grub-editenv $BINARIES_DIR/grubenv_b_valid_0 set nerves_fw_active=b
grub-editenv $BINARIES_DIR/grubenv_b_valid_0 set nerves_fw_validated=0
grub-editenv $BINARIES_DIR/grubenv_b_valid_0 set nerves_fw_booted=0

cp $BINARIES_DIR/grubenv_a $BINARIES_DIR/grubenv_a_valid
grub-editenv $BINARIES_DIR/grubenv_a_valid set booted_once=1
grub-editenv $BINARIES_DIR/grubenv_a_valid set validated=1
cp $BINARIES_DIR/grubenv_a_valid_0 $BINARIES_DIR/grubenv_a_valid_1
grub-editenv $BINARIES_DIR/grubenv_a_valid_1 set nerves_fw_validated=1

cp $BINARIES_DIR/grubenv_b $BINARIES_DIR/grubenv_b_valid
grub-editenv $BINARIES_DIR/grubenv_b_valid set booted_once=1
grub-editenv $BINARIES_DIR/grubenv_b_valid set validated=1
cp $BINARIES_DIR/grubenv_b_valid_0 $BINARIES_DIR/grubenv_b_valid_1
grub-editenv $BINARIES_DIR/grubenv_b_valid_1 set nerves_fw_validated=1

# Copy MBR boot code boot.img
cp $HOST_DIR/usr/lib/grub/i386-pc/boot.img $BINARIES_DIR
Expand All @@ -38,5 +36,8 @@ rm -fr $TARGET_DIR/boot/grub/*
mkdir -p $TARGET_DIR/usr/share/fwup
NERVES_SYSTEM=$BASE_DIR $HOST_DIR/usr/bin/fwup -c -f $NERVES_DEFCONFIG_DIR/fwup-revert.conf -o $TARGET_DIR/usr/share/fwup/revert.fw

# Create the mark_valid script for manually marking firmware valid
NERVES_SYSTEM=$BASE_DIR $HOST_DIR/usr/bin/fwup -c -f $NERVES_DEFCONFIG_DIR/fwup-mark-valid.conf -o $TARGET_DIR/usr/share/fwup/mark_valid.fw

# Copy the fwup includes to the images dir
cp -rf $NERVES_DEFCONFIG_DIR/fwup_include $BINARIES_DIR
3 changes: 3 additions & 0 deletions rootfs_overlay/etc/erlinit.config
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@
# shoehorn OTP release up first. If shoehorn isn't around, erlinit fails back
# to the main OTP release.
--boot shoehorn

# Sync U-Boot env with potential changes in Grub env
--pre-run-exec "sh /usr/bin/sync_uboot_env.sh"
14 changes: 14 additions & 0 deletions rootfs_overlay/usr/bin/sync_uboot_env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#! /bin/sh

# If the value of nerves_fw_active stored in the U-Boot environment does not
# match the value in the Grub environment block, then the bootloader has
# reverted to the previous firmware, and we need to sync these changes to the
# U-Boot environment to ensure fwup knows.

grub_nerves_fw_active=$(awk -F= '/nerves_fw_active/ {print $2}')
uboot_nerves_fw_active="$(fw_printenv -n nerves_fw_active)"

if [ "$grub_nerves_fw_active" != "$uboot_nerves_fw_active" ]; then
fw_setenv "nerves_fw_active" "$grub_nerves_fw_active"
fw_setenv "nerves_fw_validated" "1"
fi