From 90630fc15678b05946a89f9a9d932410be58ac67 Mon Sep 17 00:00:00 2001 From: Sean Rhodes Date: Thu, 27 Aug 2026 02:35:33 +0100 Subject: [PATCH] initrd: 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. Keep the generic feature disabled by default and leave kernel, coreboot, and board selection to board-specific integration. Add a host UI fixture for malformed metadata and defaults, read-only and disappearing settings, cancelled changes, and rejected writes. 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 | 9 + doc/cfr.md | 37 ++++ doc/index.md | 1 + initrd/bin/cfr-settings.sh | 338 +++++++++++++++++++++++++++++++++ initrd/bin/gui-init-basic.sh | 25 ++- initrd/bin/gui-init.sh | 45 +++-- tests/cfr/README.md | 15 ++ tests/cfr/test_cfr_settings.sh | 167 ++++++++++++++++ 9 files changed, 632 insertions(+), 19 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..526cd24fb 100644 --- a/Makefile +++ b/Makefile @@ -877,10 +877,19 @@ 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 \ + $(initrd_feature_markers) \ FORCE $(call do-cpio,$@,$(initrd_tools_dir)) @$(RM) -rf "$(initrd_tools_dir)" diff --git a/doc/cfr.md b/doc/cfr.md new file mode 100644 index 000000000..377988bf4 --- /dev/null +++ b/doc/cfr.md @@ -0,0 +1,37 @@ +# 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 board configuration must select kernel and coreboot revisions that provide +the required CFR firmware-attributes ABI. Source selection and board enablement +are intentionally outside this generic UI change. + +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` or `volatile` 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..c36670f2a 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 | | `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..af8bbaa86 --- /dev/null +++ b/initrd/bin/cfr-settings.sh @@ -0,0 +1,338 @@ +#!/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_line() { + local file=$1 allow_empty=$2 value extra fd + [ -r "$file" ] || return 1 + exec {fd}<"$file" || return 1 + if ! IFS= read -r value <&"$fd" && [ -z "$value" ]; then + exec {fd}<&- + return 1 + fi + if IFS= read -r extra <&"$fd" || [ -n "$extra" ]; then + exec {fd}<&- + return 1 + fi + exec {fd}<&- + case "$value" in + *$'\n'*|*$'\r'*) return 1 ;; + esac + [ "$allow_empty" = true ] || [ -n "$value" ] || return 1 + printf '%s' "$value" +} + +cfr_read_single_line() { + cfr_read_line "$1" false +} + +cfr_read_optional_line() { + cfr_read_line "$1" true +} + +cfr_number_valid() { + local value=$1 minimum=$2 maximum=$3 step=$4 number + local candidate + for candidate in "$value" "$minimum" "$maximum" "$step"; do + [[ "$candidate" =~ ^[0-9]{1,10}$ ]] || return 1 + (( 10#$candidate <= 4294967295 )) || return 1 + done + 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 + case "$raw" in + ';'*|*';'|*';;'*) return 1 ;; + esac + 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 current_found default_found + 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 + current_found=1 + default_found=1 + for value in "${values[@]}"; do + [ "$value" = "$current" ] && current_found=0 + [ "$value" = "$default" ] && default_found=0 + done + [ "$current_found" -eq 0 ] || return 1 + [ "$default_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 && + ! cfr_setting_has_flag "$setting" volatile; 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 || + cfr_setting_has_flag "$setting" volatile; 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. Firmware reports that a reboot is pending.' ;; + *) 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/tests/cfr/README.md b/tests/cfr/README.md new file mode 100644 index 000000000..a21748f18 --- /dev/null +++ b/tests/cfr/README.md @@ -0,0 +1,15 @@ +# 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, rejects malformed enum and unsigned +32-bit numeric metadata, and exercises an actual failed write through +`/dev/full`. The +fixture starts with `pending_reboot=1`, so a successful authoritative write also +checks the aggregate pending-reboot report. + +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..f4c937481 --- /dev/null +++ b/tests/cfr/test_cfr_settings.sh @@ -0,0 +1,167 @@ +#!/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 trailing_empty 'Trailing empty enum' A A 'A;' +make_enum trailing_lines 'Trailing metadata lines' A A 'A;B' +printf 'enumeration\n\n' >"$attributes/trailing_lines/type" +make_enum invalid_default 'Invalid default setting' A C 'A;B' +make_integer expression_range 'Expression range' 5 5 '1+2' 10 1 +make_integer oversized_range 'Oversized range' 5 5 0 99999999999999999999 1 +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 flagged_volatile 'Flagged volatile' off off 'off;on' +printf '%s\n' volatile >"$attributes/flagged_volatile/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" +printf '%s\t%s\n' "$count" "$*" >>"$root/whiptail.log" +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 '*|*' trailing_empty '*|\ + *' trailing_lines '*|\ + *' invalid_default '*|*' expression_range '*|*' oversized_range '*) 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 ;; + 16) printf '%s\n' error >&2 ;; + 17) printf '%s\n' new >&2 ;; + 20) printf '%s\n' flagged_volatile >&2 ;; + *) exit 1 ;; + esac +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 ;; + 18) + rm -f "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/error/current_value" + ln -s /dev/full "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/error/current_value" + exit 0 + ;; + 11) exit 1 ;; + *) exit 1 ;; + esac +fi +case " $* " in + *' The firmware rejected the setting write. '*) + rm -f "$root/sys/class/firmware-attributes/coreboot-cfr/attributes/error/current_value" + printf '%s\n' old >"$root/sys/class/firmware-attributes/coreboot-cfr/attributes/error/current_value" + printf '%s\n' seen >"$root/write_error_seen" + ;; +esac +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" ] +[ -e "$attributes/trailing_empty/current_value" ] +[ -e "$attributes/trailing_lines/current_value" ] +[ -e "$attributes/invalid_default/current_value" ] +[ -e "$attributes/expression_range/current_value" ] +[ -e "$attributes/oversized_range/current_value" ] +[ -e "$test_root/write_error_seen" ] || { + cat "$test_root/whiptail.log" >&2 + false +} +[ "$(cat "$attributes/error/current_value")" = old ] +[ "$(cat "$attributes/flagged_volatile/current_value")" = off ] +grep -q 'flagged_volatile.*\[read-only\] \[volatile\]' "$test_root/whiptail.log" +grep -q 'Firmware reports that a reboot is pending.' "$test_root/whiptail.log" +printf '%s\n' 'CFR settings fixture passed'