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'