From dbf0213d09f8d455b8a8429ffcd0d14ed7b51f81 Mon Sep 17 00:00:00 2001 From: Sean Rhodes Date: Thu, 27 Aug 2026 02:35:33 +0100 Subject: [PATCH] Add coreboot firmware settings UI Heads has no setup browser for settings described by coreboot CFR. Without a payload UI, users must boot another operating system or carry product-specific policy outside firmware to change an otherwise supported setting. Expose firmware settings through the Linux firmware-attributes ABI when a board explicitly enables CFR support. Validate firmware-provided metadata, confirm every write, and report reboot-required updates. Allow boards to pin exact Linux and coreboot revisions so CFR support can be integrated without changing existing release-based builds. Add a host UI fixture for malformed, read-only, disappearing, and cancelled settings. Link: https://patchew.org/linux/cover.1787003659.git.sean@starlabs.systems/ Link: https://review.coreboot.org/q/topic:heads-cfr-settings TEST=On a Star Labs StarBook Horizon, Heads enumerated the writable Bluetooth setting, changed it from Enabled to Disabled through the confirmation flow, and immediately read back Disabled. Reboot persistence was not tested. Signed-off-by: Sean Rhodes --- .circleci/config.yml | 14 ++ Makefile | 23 ++- doc/cfr.md | 57 ++++++ doc/index.md | 1 + initrd/bin/cfr-settings.sh | 318 +++++++++++++++++++++++++++++++++ initrd/bin/gui-init-basic.sh | 25 ++- initrd/bin/gui-init.sh | 45 +++-- modules/coreboot | 11 ++ modules/linux | 17 +- tests/cfr/README.md | 13 ++ tests/cfr/test_cfr_settings.sh | 130 ++++++++++++++ 11 files changed, 628 insertions(+), 26 deletions(-) create mode 100644 doc/cfr.md create mode 100755 initrd/bin/cfr-settings.sh create mode 100644 tests/cfr/README.md create mode 100755 tests/cfr/test_cfr_settings.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 191855454..348084718 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,6 +65,19 @@ commands: jobs: + cfr_settings_tests: + docker: + # cimg/base:2026.08 provides Bash and ShellCheck for this host-only job. + - image: cimg/base@sha256:e8f07526f593ac5dee29362b7f98c6fec94c412722d6bbece731e4cc885abccb + steps: + - checkout + - run: + name: CFR settings host fixture + command: ./tests/cfr/test_cfr_settings.sh + - run: + name: CFR settings shellcheck + command: shellcheck initrd/bin/cfr-settings.sh tests/cfr/test_cfr_settings.sh + # ═══════════════════════════════════════════════════════════════════════════ # Glossary (see doc/circleci.md for full cache model) # ═══════════════════════════════════════════════════════════════════════════ @@ -415,6 +428,7 @@ workflows: build_and_test: max_auto_reruns: 3 jobs: + - cfr_settings_tests - create_hashes: name: create_hashes [cache keys] diff --git a/Makefile b/Makefile index 49d1c9eac..73523339f 100644 --- a/Makefile +++ b/Makefile @@ -808,8 +808,13 @@ $(COREBOOT_UTIL_DIR)/inteltool/inteltool \ : $(build)/$(coreboot_base_dir)/.canary musl-cross-make +$(call do,MAKE,$(notdir $@),\ $(MAKE) -C "$(dir $@)" $(CROSS_TOOLS) \ + $(if $(COREBOOT_UTIL_CFLAGS),CFLAGS="$(COREBOOT_UTIL_CFLAGS)") \ ) +$(COREBOOT_UTIL_DIR)/cbmem/cbmem: COREBOOT_UTIL_CFLAGS := \ + -O2 -D_GNU_SOURCE -include endian.h -Wall -Wextra \ + -Wmissing-prototypes -Wshadow -Werror -std=gnu2x + # superio depends on zlib and pciutils $(COREBOOT_UTIL_DIR)/superiotool/superiotool: \ $(build)/$(zlib_dir)/.build \ @@ -877,11 +882,20 @@ endif # --- TOOLS.CPIO --- # tools.cpio is built from all binaries, libraries, and config staged in initrd_tools_dir +initrd_feature_markers := +ifeq ($(CONFIG_HEADS_CFR),y) +initrd_feature_markers += $(initrd_tools_dir)/etc/heads-cfr-enabled +$(initrd_tools_dir)/etc/heads-cfr-enabled: $(CONFIG) + @mkdir -p "$(dir $@)" + @printf 'enabled\n' > "$@" +endif + $(build)/$(initrd_dir)/tools.cpio: \ - $(initrd_bins) \ - $(initrd_libs) \ - $(initrd_tools_dir)/etc/config \ - FORCE + $(initrd_bins) \ + $(initrd_libs) \ + $(initrd_tools_dir)/etc/config \ + $(initrd_feature_markers) \ + FORCE $(call do-cpio,$@,$(initrd_tools_dir)) @$(RM) -rf "$(initrd_tools_dir)" @@ -909,7 +923,6 @@ $(initrd_tools_dir)/etc/config: $(CONFIG) echo export CONFIG_BRAND_NAME=$(BRAND_NAME) \ >> $@ ; \ ) - # --- BOARD.CPIO --- # board.cpio is built from the board's initrd/ directory and contains diff --git a/doc/cfr.md b/doc/cfr.md new file mode 100644 index 000000000..6f2915864 --- /dev/null +++ b/doc/cfr.md @@ -0,0 +1,57 @@ +# Coreboot Firmware Settings + +Heads can optionally show the standard Linux firmware-attributes interface +published by the coreboot CFR driver. The feature is disabled unless a board +sets `CONFIG_HEADS_CFR=y` in its board configuration. Its build-generated +`/etc/heads-cfr-enabled` marker is immutable at runtime, so `config.user` +cannot enable or disable the feature. + +The UI reads only: + +```text +/sys/class/firmware-attributes/coreboot-cfr/attributes +``` + +It does not parse the coreboot table, access EFI variables or SMMSTORE, or +invoke an SMI. A board must therefore provide a kernel and firmware stack that +implements this standard interface before enabling the feature. + +The current Heads Linux module supports the historical kernel tarballs, +including 6.1.8. A CFR board must not pretend that the v13 CFR driver works on +6.1.8. For an interim CFR board, set both variables below to a git repository +and an exact commit: + +```make +export CONFIG_HEADS_CFR=y +export CONFIG_LINUX_REPO=https://github.com/REPLACE/linux.git +export CONFIG_LINUX_COMMIT=REPLACE_WITH_CFR_V13_COMMIT +export CONFIG_COREBOOT_REPO=https://github.com/REPLACE/coreboot.git +export CONFIG_COREBOOT_COMMIT=REPLACE_WITH_CFR_SERVICE_COMMIT +``` + +The generic modules fetch the exact revisions, check them out, and use the +board's normal configuration. Existing boards that only set +`CONFIG_LINUX_VERSION` and `CONFIG_COREBOOT_VERSION` continue to use their +verified sources unchanged. + +An integration board may set `CONFIG_LINUX_CONFIG_FRAGMENT` to append a small +Kconfig fragment before `olddefconfig`, and +`CONFIG_COREBOOT_TOOLCHAIN_VERSION` to reuse a compatible, already-defined +coreboot toolchain. Existing boards that do not set these variables are +unchanged. + +The settings menu is available under Options only when both the immutable +feature marker and the firmware-attributes class/device exist. Missing, +malformed, disappearing, or unwritable attributes are handled as unavailable +or read-only. Every selected write requires an explicit confirmation showing +the display name, current value, requested value, and pending-reboot status; +cancelling that confirmation does not write. + +The Linux driver currently exposes writability through the `current_value` mode +and does not publish a `flags` attribute. The UI works with no `flags` files. +It tolerates an optional flags file for forward compatibility, hiding +`inactive`/`suppressed` entries and treating `readonly` as non-writable, but +that file is not part of the required ABI. Enumeration values use the standard +semicolon delimiter; labels containing spaces are preserved. A literal +semicolon cannot be represented unambiguously by that sysfs ABI and is rejected +rather than guessed. diff --git a/doc/index.md b/doc/index.md index 0be106bb1..880586976 100644 --- a/doc/index.md +++ b/doc/index.md @@ -9,6 +9,7 @@ Quick reference: read the relevant doc when working on a topic. | `build-artifacts.md` | ROM filenames, update-package zip layout, LVFS conventions | | `build-freshness.md` | Why rebuilds produce stale artifacts and how to force a full rebuild | | `circleci.md` | CI pipeline: job dependency graph, cache layers, workspace persistence | +| `cfr.md` | Optional coreboot firmware-settings UI and CFR Linux source selection | | `docker.md` | Docker-based build environment with pinned, reproducible images | | `modules.md` | Module system: toolchain and bin modules, inclusion rules, sentinel chain | | `patches.md` | Creating and maintaining source patches for upstream packages | diff --git a/initrd/bin/cfr-settings.sh b/initrd/bin/cfr-settings.sh new file mode 100755 index 000000000..82fc750fc --- /dev/null +++ b/initrd/bin/cfr-settings.sh @@ -0,0 +1,318 @@ +#!/bin/bash +# shellcheck disable=SC1091 + +set -e -o pipefail + +cfr_root=/sys/class/firmware-attributes/coreboot-cfr/attributes +test_root= + +if [ "${1:-}" = "--test-root" ]; then + [ "$#" -eq 2 ] || exit 2 + test_root=$2 + cfr_root="$test_root/sys/class/firmware-attributes/coreboot-cfr/attributes" + . "$test_root/functions.sh" + . "$test_root/gui_functions.sh" +else + [ "$#" -eq 0 ] || exit 2 + [ -r /etc/functions.sh ] && . /etc/functions.sh + [ -r /etc/gui_functions.sh ] && . /etc/gui_functions.sh +fi + +if [ -n "$test_root" ]; then + dialog_output="$test_root/dialog_output" +else + dialog_output=/tmp/cfr-settings-whiptail.$$ +fi +trap 'rm -f "$dialog_output"' EXIT + +cfr_error() { + local message=$1 + if declare -F whiptail_error >/dev/null 2>&1; then + whiptail_error --title 'Firmware Settings Error' --msgbox "$message" 0 80 || true + else + printf '%s\n' "$message" >&2 + fi +} + +cfr_info() { + local message=$1 + if declare -F whiptail_type >/dev/null 2>&1; then + whiptail_type normal --title 'Firmware Settings' --msgbox "$message" 0 80 || true + else + printf '%s\n' "$message" >&2 + fi +} + +cfr_read_single_line() { + local file=$1 value + [ -r "$file" ] || return 1 + value=$(cat -- "$file") || return 1 + case "$value" in + *$'\n'*|*$'\r'*) return 1 ;; + esac + [ -n "$value" ] || return 1 + printf '%s' "$value" +} + +cfr_read_optional_line() { + local file=$1 value + [ -r "$file" ] || return 1 + value=$(cat -- "$file") || return 1 + case "$value" in + *$'\n'*|*$'\r'*) return 1 ;; + esac + printf '%s' "$value" +} + +cfr_number_valid() { + local value=$1 minimum=$2 maximum=$3 step=$4 number + [[ "$value" =~ ^[0-9]+$ ]] || return 1 + number=$((10#$value)) || return 1 + minimum=$((10#$minimum)) || return 1 + maximum=$((10#$maximum)) || return 1 + step=$((10#$step)) || return 1 + (( number >= minimum && number <= maximum )) || return 1 + (( step == 0 || (number - minimum) % step == 0 )) +} + +cfr_enum_values() { + local file=$1 raw value + local -a values + raw=$(cfr_read_single_line "$file") || return 1 + IFS=';' read -r -a values <<< "$raw" + [ "${#values[@]}" -gt 0 ] || return 1 + for value in "${values[@]}"; do + [ -n "$value" ] || return 1 + case "$value" in + *$'\n'*|*$'\r'*) return 1 ;; + esac + printf '%s\n' "$value" + done +} + +cfr_setting_type() { + local setting=$1 type + type=$(cfr_read_single_line "$setting/type") || return 1 + case "$type" in + enumeration|integer) printf '%s' "$type" ;; + *) return 1 ;; + esac +} + +cfr_setting_flags_valid() { + local setting=$1 raw flag + [ -e "$setting/flags" ] || return 0 + raw=$(cfr_read_optional_line "$setting/flags") || return 1 + raw=${raw//,/ } + for flag in $raw; do + case "$flag" in + readonly|inactive|suppressed|volatile) ;; + *) return 1 ;; + esac + done +} + +cfr_setting_has_flag() { + local setting=$1 wanted=$2 raw flag + [ -e "$setting/flags" ] || return 1 + raw=$(cfr_read_optional_line "$setting/flags") || return 1 + raw=${raw//,/ } + for flag in $raw; do + [ "$flag" = "$wanted" ] && return 0 + done + return 1 +} + +cfr_setting_writable() { + local setting=$1 mode + mode=$(stat -c '%a' "$setting/current_value") || return 1 + [[ "$mode" =~ ^[0-7]+$ ]] || return 1 + (( (8#$mode & 0222) != 0 )) +} + +cfr_setting_valid() { + local setting=$1 type current default minimum maximum step value raw_values + local -a values + type=$(cfr_setting_type "$setting") || return 1 + cfr_setting_flags_valid "$setting" || return 1 + cfr_read_single_line "$setting/display_name" >/dev/null || return 1 + current=$(cfr_read_single_line "$setting/current_value") || return 1 + default=$(cfr_read_single_line "$setting/default_value") || return 1 + if [ "$type" = integer ]; then + minimum=$(cfr_read_single_line "$setting/min_value") || return 1 + maximum=$(cfr_read_single_line "$setting/max_value") || return 1 + step=$(cfr_read_single_line "$setting/scalar_increment") || return 1 + cfr_number_valid "$current" "$minimum" "$maximum" "$step" || return 1 + cfr_number_valid "$default" "$minimum" "$maximum" "$step" || return 1 + else + raw_values=$(cfr_enum_values "$setting/possible_values") || return 1 + mapfile -t values <<< "$raw_values" + [ "${#values[@]}" -gt 0 ] || return 1 + local found=1 + for value in "${values[@]}"; do + [ "$value" = "$current" ] && found=0 + done + [ "$found" -eq 0 ] || return 1 + fi +} + +cfr_item() { + local setting=$1 name display current writable + name=${setting##*/} + display=$(cfr_read_single_line "$setting/display_name") || return 1 + current=$(cfr_read_single_line "$setting/current_value") || return 1 + if cfr_setting_writable "$setting" && + ! cfr_setting_has_flag "$setting" readonly; then + writable='' + else + writable=' [read-only]' + fi + cfr_setting_has_flag "$setting" volatile && writable="$writable [volatile]" + printf '%s\t%s%s (current: %s)' "$name" "$display" "$writable" "$current" +} + +cfr_choose_setting() { + local setting item name + local -a menu + menu=() + for setting in "$cfr_root"/*; do + [ -d "$setting" ] || continue + cfr_setting_valid "$setting" || continue + cfr_setting_has_flag "$setting" inactive && continue + cfr_setting_has_flag "$setting" suppressed && continue + item=$(cfr_item "$setting") || continue + name=${item%%$'\t'*} + item=${item#*$'\t'} + menu+=("$name" "$item") + done + + [ "${#menu[@]}" -gt 0 ] || { + cfr_info 'No usable coreboot firmware settings are available.' + return 1 + } + + if ! whiptail_type normal --title 'Coreboot Firmware Settings' \ + --menu 'Select a setting. Values are read again after every write.' \ + 0 100 12 "${menu[@]}" > /dev/null 2>"$dialog_output"; then + return 1 + fi + + [ -r "$dialog_output" ] || return 1 + cat -- "$dialog_output" +} + +cfr_edit_setting() { + local name=$1 setting="$cfr_root/$1" type current default minimum maximum step + local selected value display pending pending_text confirmation + + [ -d "$setting" ] || { + cfr_error 'The selected setting disappeared.' + return 0 + } + if cfr_setting_has_flag "$setting" inactive || + cfr_setting_has_flag "$setting" suppressed; then + cfr_error 'The selected setting is inactive.' + return 0 + fi + cfr_setting_valid "$setting" || { + cfr_error 'The selected setting is unavailable or contains malformed data.' + return 0 + } + type=$(cfr_setting_type "$setting") || return 0 + current=$(cfr_read_single_line "$setting/current_value") || return 0 + default=$(cfr_read_single_line "$setting/default_value") || return 0 + + if ! cfr_setting_writable "$setting" || + cfr_setting_has_flag "$setting" readonly; then + display=$(cfr_read_single_line "$setting/display_name") || return 0 + cfr_info "$(printf '%s\n\nCurrent: %s\nDefault: %s\n\nThis setting is read-only in firmware.' "$display" "$current" "$default")" + return 0 + fi + + if [ "$type" = enumeration ]; then + local -a values choices + local raw_values + raw_values=$(cfr_enum_values "$setting/possible_values") || { + cfr_error 'The setting enumeration is malformed.' + return 0 + } + mapfile -t values <<< "$raw_values" + choices=() + for value in "${values[@]}"; do + choices+=("$value" "$value") + done + if ! whiptail_type normal --title "$(cfr_read_single_line "$setting/display_name")" \ + --menu "Current: $current\nDefault: $default" 0 100 12 \ + "${choices[@]}" 2>"$dialog_output"; then + return 0 + fi + selected=$(cat -- "$dialog_output") || return 0 + value=$selected + else + minimum=$(cfr_read_single_line "$setting/min_value") || return 0 + maximum=$(cfr_read_single_line "$setting/max_value") || return 0 + step=$(cfr_read_single_line "$setting/scalar_increment") || return 0 + if ! whiptail_type normal --title "$(cfr_read_single_line "$setting/display_name")" \ + --inputbox "Current: $current\nDefault: $default\nRange: $minimum-$maximum, step $step" \ + 0 100 "$current" 2>"$dialog_output"; then + return 0 + fi + value=$(cat -- "$dialog_output") || return 0 + cfr_number_valid "$value" "$minimum" "$maximum" "$step" || { + cfr_error 'The number is outside the firmware-provided range.' + return 0 + } + fi + + display=$(cfr_read_single_line "$setting/display_name") || return 0 + pending_text='pending_reboot will be checked after the write.' + if [ -e "$cfr_root/pending_reboot" ]; then + pending=$(cfr_read_single_line "$cfr_root/pending_reboot" || true) + case "$pending" in + 0) pending_text='pending_reboot currently reports no reboot required.' ;; + 1) pending_text='pending_reboot currently reports that a reboot may be required.' ;; + *) pending_text='pending_reboot is unavailable or malformed; it will be checked after the write.' ;; + esac + fi + confirmation=$(printf '%s\n\nCurrent: %s\nRequested: %s\n\n%s\n\nWrite this firmware setting?' \ + "$display" "$current" "$value" "$pending_text") + if ! whiptail_type normal --title 'Confirm Firmware Setting' \ + --yesno "$confirmation" 0 80; then + return 0 + fi + + if ! printf '%s\n' "$value" >"$setting/current_value"; then + cfr_error 'The firmware rejected the setting write.' + return 0 + fi + + current=$(cfr_read_single_line "$setting/current_value") || { + cfr_error 'The setting disappeared after the write.' + return 0 + } + [ "$current" = "$value" ] || { + cfr_error 'Firmware did not retain the requested value.' + return 0 + } + + if [ -e "$cfr_root/pending_reboot" ]; then + local pending + pending=$(cfr_read_single_line "$cfr_root/pending_reboot") || { + cfr_error 'The pending-reboot state is malformed.' + return 0 + } + case "$pending" in + 0) ;; + 1) cfr_info 'The setting was stored. A reboot is required to apply it.' ;; + *) cfr_error 'The pending-reboot state is malformed.' ;; + esac + fi +} + +[ -d "$cfr_root" ] || exit 0 +declare -F whiptail_type >/dev/null 2>&1 || exit 1 + +while true; do + setting=$(cfr_choose_setting) || exit 0 + cfr_edit_setting "$setting" +done diff --git a/initrd/bin/gui-init-basic.sh b/initrd/bin/gui-init-basic.sh index 51e06691d..ea6d3de70 100755 --- a/initrd/bin/gui-init-basic.sh +++ b/initrd/bin/gui-init-basic.sh @@ -105,13 +105,25 @@ show_main_menu() show_options_menu() { TRACE_FUNC + local -a options_menu + options_menu=( + 'b' ' Boot Options -->' + 'c' ' Change configuration settings -->' + 'f' ' Flash/Update the BIOS -->' + ) + if [ -r /etc/heads-cfr-enabled ] && + [ -d /sys/class/firmware-attributes/coreboot-cfr/attributes ]; then + options_menu+=( + 'e' ' Coreboot Firmware Settings -->' + ) + fi + options_menu+=( + 'x' ' Exit to recovery shell' + 'r' ' <-- Return to main menu' + ) whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Basic Options" \ --menu "" 0 80 10 \ - 'b' ' Boot Options -->' \ - 'c' ' Change configuration settings -->' \ - 'f' ' Flash/Update the BIOS -->' \ - 'x' ' Exit to recovery shell' \ - 'r' ' <-- Return to main menu' \ + "${options_menu[@]}" \ 2>/tmp/whiptail || recovery "GUI menu failed" option=$(cat /tmp/whiptail) @@ -122,6 +134,9 @@ show_options_menu() c ) config-gui.sh ;; + e ) + /bin/cfr-settings.sh + ;; f ) flash-gui.sh ;; diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index ce03d953b..f796da063 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -628,22 +628,36 @@ show_main_menu() { show_options_menu() { TRACE_FUNC + local cfr_available='' + if [ -r /etc/heads-cfr-enabled ] && + [ -d /sys/class/firmware-attributes/coreboot-cfr/attributes ]; then + cfr_available='y' + fi + local -a options_menu + options_menu=( + 'b' ' Boot Options -->' + 't' ' TPM/TOTP/HOTP Options -->' + 'i' ' Investigate integrity discrepancies -->' + 'h' ' Change system time' + 'u' ' Update checksums and sign all files in /boot' + 'c' ' Change configuration settings -->' + 'f' ' Flash/Update the BIOS -->' + 'g' ' GPG Options -->' + 'F' ' OEM Factory Reset / Re-Ownership -->' + 'C' ' Reencrypt LUKS container -->' + 'P' ' Change LUKS Disk Recovery Key passphrase ->' + 'R' ' Check/Update file hashes on root disk -->' + ) + [ -n "$cfr_available" ] && options_menu+=( + 'e' ' Coreboot Firmware Settings -->' + ) + options_menu+=( + 'x' ' Exit to recovery shell' + 'r' ' <-- Return to main menu' + ) whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Options" \ --menu "" 0 80 10 \ - 'b' ' Boot Options -->' \ - 't' ' TPM/TOTP/HOTP Options -->' \ - 'i' ' Investigate integrity discrepancies -->' \ - 'h' ' Change system time' \ - 'u' ' Update checksums and sign all files in /boot' \ - 'c' ' Change configuration settings -->' \ - 'f' ' Flash/Update the BIOS -->' \ - 'g' ' GPG Options -->' \ - 'F' ' OEM Factory Reset / Re-Ownership -->' \ - 'C' ' Reencrypt LUKS container -->' \ - 'P' ' Change LUKS Disk Recovery Key passphrase ->' \ - 'R' ' Check/Update file hashes on root disk -->' \ - 'x' ' Exit to recovery shell' \ - 'r' ' <-- Return to main menu' \ + "${options_menu[@]}" \ 2>/tmp/whiptail || recovery "GUI menu failed" option=$(cat /tmp/whiptail) @@ -666,6 +680,9 @@ show_options_menu() { c) config-gui.sh ;; + e) + /bin/cfr-settings.sh + ;; f) flash-gui.sh ;; diff --git a/modules/coreboot b/modules/coreboot index 4fc0a54fa..a3c4df2d0 100644 --- a/modules/coreboot +++ b/modules/coreboot @@ -183,6 +183,17 @@ coreboot-25.09_repo := https://github.com/coreboot/coreboot coreboot-25.09_commit_hash := 07df08836eca9dd755cc32b6d78727760d201605 $(eval $(call coreboot_module,25.09,)) +ifneq "$(strip $(CONFIG_COREBOOT_REPO))" "" +ifeq "$(strip $(CONFIG_COREBOOT_COMMIT))" "" +$(error "$(BOARD): CONFIG_COREBOOT_REPO requires CONFIG_COREBOOT_COMMIT") +endif +coreboot_git_version := git-$(shell printf '%s' "$(CONFIG_COREBOOT_COMMIT)" | cut -c1-12) +coreboot-$(coreboot_git_version)_repo := $(CONFIG_COREBOOT_REPO) +coreboot-$(coreboot_git_version)_commit_hash := $(CONFIG_COREBOOT_COMMIT) +$(eval $(call coreboot_module,$(coreboot_git_version),$(CONFIG_COREBOOT_TOOLCHAIN_VERSION))) +CONFIG_COREBOOT_VERSION := $(coreboot_git_version) +endif + # Check that the board configured the coreboot version correctly ifeq "$(CONFIG_COREBOOT_VERSION)" "" $(error "$(BOARD): does not specify coreboot version under CONFIG_COREBOOT_VERSION") diff --git a/modules/linux b/modules/linux index 785c12491..225b0e2ef 100644 --- a/modules/linux +++ b/modules/linux @@ -10,7 +10,15 @@ else $(error "$(CONFIG_TARGET_ARCH) target isn't supported by this module") endif -ifeq "$(CONFIG_LINUX_VERSION)" "4.14.62" +ifneq "$(strip $(CONFIG_LINUX_REPO))" "" +ifeq "$(strip $(CONFIG_LINUX_COMMIT))" "" +$(error "$(BOARD): CONFIG_LINUX_REPO requires CONFIG_LINUX_COMMIT") +endif +linux_repo := $(CONFIG_LINUX_REPO) +linux_commit_hash := $(CONFIG_LINUX_COMMIT) +linux_version := git-$(shell printf '%s' "$(CONFIG_LINUX_COMMIT)" | cut -c1-12) +linux_base_dir := linux-$(linux_version) +else ifeq "$(CONFIG_LINUX_VERSION)" "4.14.62" linux_version := 4.14.62 linux_hash := 51ca4d7e8ee156dc0f19bc7768915cfae41dbb0b4f251e4fa8b178c5674c22ab else ifeq "$(CONFIG_LINUX_VERSION)" "4.19.139" @@ -38,7 +46,9 @@ else $(error "$(BOARD): does not specify linux kernel version under CONFIG_LINUX_VERSION") endif +ifndef linux_base_dir linux_base_dir := linux-$(linux_version) +endif # input file in the heads config/ dir # Allow board config to specialize Linux configuration if necessary @@ -48,16 +58,19 @@ linux_kconfig := $(or $(CONFIG_LINUX_CONFIG),config/linux.config) # configuration file name, not the board name linux_dir := $(linux_base_dir)/$(notdir $(basename $(linux_kconfig))) +ifeq "$(strip $(CONFIG_LINUX_REPO))" "" linux_tar := linux-$(linux_version).tar.xz linux_major_ver := $(basename $(basename $(CONFIG_LINUX_VERSION))) linux_url := https://cdn.kernel.org/pub/linux/kernel/v$(linux_major_ver).x/$(linux_tar) +endif # Ensure that touching the config file will force a reconfig/rebuild -$(build)/$(linux_dir)/.configured: $(linux_kconfig) +$(build)/$(linux_dir)/.configured: $(linux_kconfig) $(CONFIG_LINUX_CONFIG_FRAGMENT) linux_configure := \ mkdir -p "$(build)/$(linux_dir)" \ && $(call install_config,$(pwd)/$(linux_kconfig),$(build)/$(linux_dir)/.config) \ + $(if $(CONFIG_LINUX_CONFIG_FRAGMENT),&& cat "$(pwd)/$(CONFIG_LINUX_CONFIG_FRAGMENT)" >> "$(build)/$(linux_dir)/.config") \ && $(MAKE) -C .. \ ARCH="$(LINUX_ARCH)" \ CROSS_COMPILE="$(CROSS)" \ diff --git a/tests/cfr/README.md b/tests/cfr/README.md new file mode 100644 index 000000000..ff9f49981 --- /dev/null +++ b/tests/cfr/README.md @@ -0,0 +1,13 @@ +# CFR settings fixture + +`test_cfr_settings.sh` runs the production `cfr-settings.sh` entry point against +a temporary sysfs-shaped tree. Its whiptail stub drives enum and numeric writes, +changes a dependent attribute's writability, presents a read-only attribute, +removes attributes during selection, and supplies malformed enum data. The +fixture starts with `pending_reboot=1`, so a successful authoritative write also +checks the reboot-required path. + +This is a host UI/ABI fixture only. It does not test the Linux +firmware-attributes driver, coreboot CFR parsing, EFI variables, SMMSTORE, or +the SMI/APM apply path. Production board policy and runtime apply callbacks +require hardware validation. diff --git a/tests/cfr/test_cfr_settings.sh b/tests/cfr/test_cfr_settings.sh new file mode 100755 index 000000000..4056229fd --- /dev/null +++ b/tests/cfr/test_cfr_settings.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +set -e -o pipefail + +repo_root=$(cd "$(dirname "$0")/../.." && pwd) +test_root=$(mktemp -d "$repo_root/.cfr-test.XXXXXX") +trap 'rm -rf "$test_root"' EXIT +attributes=$test_root/sys/class/firmware-attributes/coreboot-cfr/attributes +mkdir -p "$attributes" + +printf '%s\n' '#!/bin/bash' >"$test_root/functions.sh" + +make_enum() { + local name=$1 display=$2 current=$3 default=$4 values=$5 mode=${6:-644} + local dir=$attributes/$name + mkdir -p "$dir" + printf '%s\n' enumeration >"$dir/type" + printf '%s\n' "$display" >"$dir/display_name" + printf '%s\n' "$current" >"$dir/current_value" + printf '%s\n' "$default" >"$dir/default_value" + printf '%s\n' "$values" >"$dir/possible_values" + chmod "$mode" "$dir/current_value" +} + +make_integer() { + local name=$1 display=$2 current=$3 default=$4 minimum=$5 maximum=$6 step=$7 mode=${8:-644} + local dir=$attributes/$name + mkdir -p "$dir" + printf '%s\n' integer >"$dir/type" + printf '%s\n' "$display" >"$dir/display_name" + printf '%s\n' "$current" >"$dir/current_value" + printf '%s\n' "$default" >"$dir/default_value" + printf '%s\n' "$minimum" >"$dir/min_value" + printf '%s\n' "$maximum" >"$dir/max_value" + printf '%s\n' "$step" >"$dir/scalar_increment" + chmod "$mode" "$dir/current_value" +} + +make_enum mode 'Performance mode' 'Balanced mode' 'Balanced mode' 'Balanced mode;Performance mode' +make_enum feature 'Optional feature' off off 'off;on' 444 +make_integer number 'Fan limit' 50 50 10 100 5 +make_enum readonly 'Read only' locked locked locked 444 +make_enum missing 'Disappearing setting' old old 'old;new' +make_enum error 'Write error setting' old old 'old;new' +make_enum malformed 'Malformed setting' A A 'A;;B' +make_enum inactive 'Inactive setting' off off 'off;on' +printf '%s\n' inactive >"$attributes/inactive/flags" +make_enum suppressed 'Suppressed setting' off off 'off;on' +printf '%s\n' suppressed >"$attributes/suppressed/flags" +make_enum flagged_readonly 'Flagged read only' off off 'off;on' +printf '%s\n' readonly >"$attributes/flagged_readonly/flags" +make_enum cancelled 'Cancelled setting' old old 'old;new' +printf '%s\n' 1 >"$attributes/pending_reboot" +printf '%s\n' 0 >"$test_root/state" +chmod -R a+rwX "$test_root" +chmod 444 "$attributes/feature/current_value" "$attributes/readonly/current_value" + +cat >"$test_root/gui_functions.sh" <<'EOF' +#!/bin/bash +whiptail_type() { "${WHIPTAIL_BIN:?}" "$@"; } +whiptail_error() { "${WHIPTAIL_BIN:?}" "$@"; } +EOF + +cat >"$test_root/whiptail" <<'EOF' +#!/bin/bash +set -e +root=${CFR_TEST_ROOT:?} +state=${CFR_TEST_STATE:?} +count=$(cat "$state") +count=$((count + 1)) +printf '%s\n' "$count" >"$state" +kind= +for arg in "$@"; do + case "$arg" in + --menu) kind=menu ;; + --inputbox) kind=input ;; + --yesno) kind=yesno ;; + esac +done +if [ "$kind" = menu ]; then + case "$count" in + 1) + # fbwhiptail writes terminal cursor controls to stdout. + printf '\033[?25l\033[?25h' + case " $* " in + *' inactive '*|*' suppressed '*|*' malformed '*) exit 1 ;; + esac + printf '%s\n' mode >&2 + ;; + 2) printf '%s\n' 'Performance mode' >&2 ;; + 5) printf '%s\n' number >&2 ;; + 9) printf '%s\n' cancelled >&2 ;; + 10) printf '%s\n' new >&2 ;; + 12) printf '%s\n' readonly >&2 ;; + 14) rm -rf "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/missing"; printf '%s\n' missing >&2 ;; + 15) rm -rf "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/error"; printf '%s\n' error >&2 ;; + *) exit 1 ;; + esac + [ "$count" -ne 16 ] +elif [ "$kind" = input ]; then + printf '%s\n' 75 >&2 +elif [ "$kind" = yesno ]; then + printf '%s' "$*" >"$root/last_confirmation" + case "$count" in + 3|7) exit 0 ;; + 11) exit 1 ;; + *) exit 1 ;; + esac +fi +if [ "$count" -eq 2 ]; then + chmod 644 "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/feature/current_value" + printf '%s\n' on >"$root/sys/class/firmware-attributes/coreboot-cfr/attributes/feature/current_value" + chmod 644 "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/feature/current_value" +fi +exit 0 +EOF +chmod +x "$test_root/whiptail" + +WHIPTAIL_BIN="$test_root/whiptail" CFR_TEST_ROOT="$test_root" CFR_TEST_STATE="$test_root/state" \ + "$repo_root/initrd/bin/cfr-settings.sh" --test-root "$test_root" + +[ "$(cat "$attributes/mode/current_value")" = 'Performance mode' ] +[ "$(cat "$attributes/number/current_value")" = 75 ] +[ "$(cat "$attributes/feature/current_value")" = on ] +[ "$(cat "$attributes/cancelled/current_value")" = old ] +[ "$(grep -c '^Current: old$' "$test_root/last_confirmation")" -eq 1 ] +[ "$(grep -c '^Requested: new$' "$test_root/last_confirmation")" -eq 1 ] +[ -w "$attributes/feature/current_value" ] +[ -e "$attributes/malformed/current_value" ] +printf '%s\n' 'CFR settings fixture passed'