From 83a8acf5de3ffe54b0d15adec64a4d9cef1f8aa6 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 01:38:22 -0700 Subject: [PATCH 01/23] kryptik-hwreport prints what a machine is made of and what the image lacks for it: each device with the driver that took it, the firmware the kernel asked for and did not find as lines for firmware.list, the devices and disks that want a line in boot.fragment, and the kernel log, with serial numbers, hardware addresses and UUIDs struck --- build/recipes/hwreport.sh | 22 ++ build/stages/04-base-system.sh | 1 + tools/install/kryptik-hwreport.sh | 336 ++++++++++++++++++++++++++++++ tools/tests/hwreport.sh | 169 +++++++++++++++ 4 files changed, 528 insertions(+) create mode 100644 build/recipes/hwreport.sh create mode 100755 tools/install/kryptik-hwreport.sh create mode 100755 tools/tests/hwreport.sh diff --git a/build/recipes/hwreport.sh b/build/recipes/hwreport.sh new file mode 100644 index 00000000..c2588958 --- /dev/null +++ b/build/recipes/hwreport.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# hwreport: a stage 04 recipe, sourced by build/stages/04-base-system.sh, +# which runs it in the order its list gives. + +# kryptik-hwreport runs on the machine under test; here it only has to parse +# and to head a report of an empty tree, not of the build machine. +s_hwreport() { + local src="${KRYPTIK_ROOT}/tools/install/kryptik-hwreport.sh" + [[ -f "$src" ]] || { echo "no report tool at ${src}"; return 1; } + echo "source sha256: ${1:-unknown}" + install -D -m 0755 "$src" /usr/sbin/kryptik-hwreport + sh -n /usr/sbin/kryptik-hwreport || { echo "the report tool does not parse under the target sh"; return 1; } + local empty out + empty="$(mktemp -d)" + # Captured, not piped into head: pipefail would report the closed pipe. + out="$(KRYPTIK_HWREPORT_ROOT="$empty" /usr/sbin/kryptik-hwreport 2>&1 || true)" + rmdir "$empty" + case "$out" in + "kryptik-hwreport 1"*"== missing =="*"== kernel log =="*) echo "ok: kryptik-hwreport runs" ;; + *) echo "FAIL: kryptik-hwreport does not run: ${out}"; return 1 ;; + esac +} diff --git a/build/stages/04-base-system.sh b/build/stages/04-base-system.sh index a06260bc..210c70f6 100755 --- a/build/stages/04-base-system.sh +++ b/build/stages/04-base-system.sh @@ -311,6 +311,7 @@ PACKAGES=( "updater" "s_updater $(sha256_of "${KRYPTIK_ROOT}/tools/update/kryptik-update" 2>/dev/null || echo none) $(sha256_of "${KRYPTIK_ROOT}/tools/update/kryptik-recover" 2>/dev/null || echo none)" "netzone" "s_netzone $(sha256_of "${KRYPTIK_ROOT}/tools/net/netzone-init.sh" 2>/dev/null || echo none)-$(sha256_of "${KRYPTIK_ROOT}/tools/net/sntp-offset.py" 2>/dev/null || echo none)-$(sha256_of "${KRYPTIK_ROOT}/tools/net/update-fetch.py" 2>/dev/null || echo none)" "installer" "s_installer $(sha256_of "${KRYPTIK_ROOT}/tools/install/kryptik-install.sh" 2>/dev/null || echo none)" + "hwreport" "s_hwreport $(sha256_of "${KRYPTIK_ROOT}/tools/install/kryptik-hwreport.sh" 2>/dev/null || echo none)" # The binary's path and hash, and a digest of the zone files: kryptikd # validates them at install time, so the two must move together. "kryptikd" "s_kryptikd ${KRYPTIK_KRYPTIKD_BIN:-none} $([[ -f "${KRYPTIK_KRYPTIKD_BIN:-}" ]] && sha256_of "${KRYPTIK_KRYPTIKD_BIN}" || echo absent) $(tree_digest "${KRYPTIK_ROOT}"/compartments/zones/*.toml "${KRYPTIK_ROOT}"/compartments/zones/policy/*) $(sha256_of "${KRYPTIK_ROOT}/tools/kryptik" 2>/dev/null || echo none)" diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh new file mode 100755 index 00000000..d6c892ee --- /dev/null +++ b/tools/install/kryptik-hwreport.sh @@ -0,0 +1,336 @@ +#!/bin/sh +# kryptik-hwreport: what this machine is made of and what the image lacks for +# it: the machine, each device and the driver that took it, the firmware the +# kernel asked for and did not find, and the kernel log. Serial numbers, +# hardware addresses and UUIDs are left out, and struck from the log. +# +# kryptik-hwreport print the report +# kryptik-hwreport --save write it onto the USB medium this system booted +# from, as kryptik-report/report-N.txt on the +# stick's first partition +# +# A stick whose first partition holds a kryptik-report directory gets a report +# at every boot (hw-report.sh). Nothing else writes to a medium. +set -u + +PROG=kryptik-hwreport +say() { printf '%s: %s\n' "$PROG" "$*"; } +die() { printf '%s: FAILED: %s\n' "$PROG" "$*" >&2; exit 1; } + +# A tree read in place of the running system's, for tools/tests/hwreport.sh. +R="${KRYPTIK_HWREPORT_ROOT:-}" +SYS="$R/sys" + +SAVE=0 +case "${1:-}" in + "") ;; + --save) SAVE=1 ;; + -h|--help) sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) die "unknown argument: $1" ;; +esac +[ "$#" -le 1 ] || die "one argument at most" +[ -n "$R" ] || [ "$(id -u)" = 0 ] || die "must run as root: the kernel log is root's to read" + +one() { [ -r "$1" ] && head -n 1 "$1" 2>/dev/null; } # a file's first line +hex() { v="$(one "$1")"; v="${v#0x}"; printf '%s' "${v:--}"; } # the same, without 0x +target() { [ -e "$1" ] && basename "$(readlink -f "$1")"; } # what a link names +kmsg() { if [ -n "$R" ]; then cat "$R/dmesg" 2>/dev/null; else dmesg 2>/dev/null; fi; } +sec() { printf '\n== %s ==\n' "$1"; } +or_none() { if [ -s "$1" ]; then sed 's/^/ /' "$1"; else echo " (none)"; fi; } + +# The log prints what must not leave the machine: a network driver its +# address, USB a device's serial number, a filesystem its UUID. +scrub() { + sed -E \ + -e ':a' -e 's/(^|[^0-9A-Fa-f])([0-9A-Fa-f]{2}:){5,}[0-9A-Fa-f]{2}($|[^0-9A-Fa-f])/\1xx:xx:xx:xx:xx:xx\3/' -e 'ta' \ + -e 's/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}//g' \ + -e 's/([Ss]erial ?([Nn]umber|[Nn]o)[.:= ]+)[^ ,;]+/\1/g' \ + -e 's/(S\/N[:= ]+)[^ ,;]+/\1/g' +} + +# A driver that is a loadable module cannot hold the root: there is no +# initramfs to load it from (ADR-013). +kind_of() { # kind_of DRIVER-DIR + km="$(target "$1/module")" + if [ -n "$km" ] && [ -e "$SYS/module/$km/initstate" ]; then echo module; else echo "built in"; fi +} + +system() { + os="$R/etc/os-release" + printf 'kryptik: %s (build %s)\n' "$(sed -n 's/^VERSION_ID=//p' "$os" 2>/dev/null | tr -d '"')" \ + "$(sed -n 's/^BUILD_ID=//p' "$os" 2>/dev/null)" + printf 'kernel: %s\n' "$(one "$R/proc/sys/kernel/osrelease")" + id="$R/run/kryptik/boot-identity" + media="$(sed -n 's/^media=//p' "$id" 2>/dev/null)" + if [ -n "$media" ]; then printf 'booted: the %s medium\n' "$media" + else printf 'booted: slot %s, installed\n' "$(sed -n 's/^slot=//p' "$id" 2>/dev/null)"; fi + printf 'taken: %s\n' "$(date -u +%Y-%m-%dT%H:%MZ)" +} + +machine() { + for k in sys_vendor product_name product_version product_family product_sku \ + board_vendor board_name board_version bios_vendor bios_version bios_date chassis_type; do + printf '%s: %s\n' "$k" "$(one "$SYS/class/dmi/id/$k")" + done +} + +efi_byte() { # the value byte of one of the firmware's own variables + od -An -tu1 -j4 -N1 "$SYS/firmware/efi/efivars/$1-8be4df61-93ca-11d2-aa0d-00e098032b8c" 2>/dev/null | tr -d ' ' +} +firmware() { + if [ -d "$SYS/firmware/efi" ]; then printf 'uefi: yes, %s-bit\n' "$(one "$SYS/firmware/efi/fw_platform_size")" + else echo "uefi: no"; fi + case "$(efi_byte SecureBoot)" in 1) echo "secure boot: on" ;; 0) echo "secure boot: off" ;; *) echo "secure boot: unreadable" ;; esac + printf 'setup mode: %s\n' "$(efi_byte SetupMode)" + printf 'lockdown: %s\n' "$(sed -n 's/.*\[\(.*\)\].*/\1/p' "$SYS/kernel/security/lockdown" 2>/dev/null)" + tpm=none + for t in "$SYS"/class/tpm/tpm*; do [ -e "$t" ] && tpm="version $(one "$t/tpm_version_major")"; done + printf 'tpm: %s\n' "$tpm" +} + +cpu() { + awk -F': *' ' + /^model name/ { name = $2 } + /^vendor_id/ { vendor = $2 } + /^cpu family/ { family = $2 } + /^model\t/ { model = $2 } + /^stepping/ { stepping = $2 } + /^microcode/ { microcode = $2 } + /^flags/ { flags = $2 } + /^processor/ { n++ } + END { + printf "model: %s\n", name + printf "vendor, family/model/stepping: %s, %s/%s/%s\n", vendor, family, model, stepping + printf "microcode: %s\n", microcode + printf "threads running: %d\n", n + printf "of note:" + split("nx smep smap umip pku ibt user_shstk rdrand rdseed aes sha_ni la57 hypervisor", want, " ") + split(flags, have, " ") + for (i in have) has[have[i]] = 1 + for (i = 1; i in want; i++) if (want[i] in has) printf " %s", want[i] + printf "\n" + }' "$R/proc/cpuinfo" 2>/dev/null + printf 'present: %s, online: %s, smt: %s\n' "$(one "$SYS/devices/system/cpu/present")" \ + "$(one "$SYS/devices/system/cpu/online")" "$(one "$SYS/devices/system/cpu/smt/control")" + kmsg | sed -n 's/^\[[^]]*\] *\(microcode: .*\)/\1/p' | head -n 3 + for f in "$SYS"/devices/system/cpu/vulnerabilities/*; do + [ -r "$f" ] && printf '%s: %s\n' "${f##*/}" "$(one "$f")" + done + printf 'memory: %s MiB\n' "$(awk '/^MemTotal/ { printf "%d", $2 / 1024 }' "$R/proc/meminfo" 2>/dev/null)" +} + +# address class vendor device subsystem-vendor subsystem-device revision driver modalias +pci_raw() { + for d in "$SYS"/bus/pci/devices/*; do + [ -r "$d/vendor" ] || continue + drv="$(target "$d/driver")" + printf '%s %s %s %s %s %s %s %s %s\n' "${d##*/}" "$(hex "$d/class")" "$(hex "$d/vendor")" "$(hex "$d/device")" \ + "$(hex "$d/subsystem_vendor")" "$(hex "$d/subsystem_device")" "$(hex "$d/revision")" "${drv:--}" "$(one "$d/modalias")" + done +} +# The same, with the names hwdata's list gives the class and the device. +pci_named() { # pci_named RAW-FILE + awk -v ids="$R/usr/share/hwdata/pci.ids" ' + BEGIN { + while ((getline l < ids) > 0) { + if (l ~ /^C [0-9a-f][0-9a-f] /) { classes = 1; base = substr(l, 3, 2); cname[base] = substr(l, 7); continue } + if (classes) { + if (l ~ /^\t[0-9a-f][0-9a-f] /) cname[base substr(l, 2, 2)] = substr(l, 6) + continue + } + if (l ~ /^[0-9a-f][0-9a-f][0-9a-f][0-9a-f] /) { v = substr(l, 1, 4); vname[v] = substr(l, 7); continue } + if (l ~ /^\t[0-9a-f][0-9a-f][0-9a-f][0-9a-f] /) dname[v ":" substr(l, 2, 4)] = substr(l, 8) + } + } + { + what = (substr($2, 1, 4) in cname) ? cname[substr($2, 1, 4)] : cname[substr($2, 1, 2)] + name = vname[$3] + if (($3 ":" $4) in dname) name = name " " dname[$3 ":" $4] + printf "%s class %s %s:%s sub %s:%s rev %s driver %s", $1, $2, $3, $4, $5, $6, $7, $8 + if (what != "") printf " | %s", what + if (name != "") printf " | %s", name + printf "\n" + }' "$1" +} + +usb() { + for d in "$SYS"/bus/usb/devices/*; do + n="${d##*/}" + if [ -r "$d/idVendor" ]; then + printf '%s %s:%s speed %s | %s %s\n' "$n" "$(one "$d/idVendor")" "$(one "$d/idProduct")" "$(one "$d/speed")" \ + "$(one "$d/manufacturer")" "$(one "$d/product")" + elif [ -r "$d/bInterfaceClass" ]; then + drv="$(target "$d/driver")" + printf '%s interface class %s/%s/%s driver %s\n' "$n" "$(one "$d/bInterfaceClass")" \ + "$(one "$d/bInterfaceSubClass")" "$(one "$d/bInterfaceProtocol")" "${drv:--}" + fi + done +} + +# Every device that names what would drive it and has nothing bound. +unbound() { + for bus in pci usb platform i2c hid serio sdio mmc spi virtio thunderbolt; do + for d in "$SYS/bus/$bus"/devices/*; do + [ -e "$d/driver" ] && continue + alias="$(one "$d/modalias")" + [ -n "$alias" ] && printf '%s %s %s\n' "$bus" "${d##*/}" "$alias" + done + done +} + +# Each disk with the drivers between it and the machine; a disk behind a +# module goes to $T/modular as well. +disks() { + rootdisk="$(sed -n 's/^root_disk=//p' "$R/run/kryptik/boot-identity" 2>/dev/null)" + for b in "$SYS"/block/*; do + n="${b##*/}" + case "$n" in loop*|ram*|dm-*|zram*|fd*) continue ;; esac + [ -e "$b/device" ] || continue + chain=""; last=""; modular="" + p="$(readlink -f "$b/device")" + while [ -n "$p" ] && [ "$p" != "$SYS" ] && [ "$p" != / ]; do + if [ -e "$p/driver" ]; then + drv="$(target "$p/driver")"; k="$(kind_of "$p/driver")" + [ "$drv" = "$last" ] || chain="${chain}${chain:+, }${drv} (${k})" + last="$drv" + [ "$k" = module ] && modular="${modular}${modular:+ }$(target "$p/driver/module")" + fi + p="${p%/*}" + done + note="" + [ "$(one "$b/removable")" = 1 ] && note=" removable" + [ "/dev/$n" = "$rootdisk" ] && note="${note} (the root is here)" + printf '%s %s GiB%s | %s | %s\n' "$n" "$(awk -v s="$(one "$b/size")" 'BEGIN { printf "%.1f", s * 512 / 1073741824 }')" \ + "$note" "$(one "$b/device/model" | sed 's/ *$//')" "$chain" + [ -z "$modular" ] || printf '%s: %s\n' "$n" "$modular" >> "$T/modular" + done +} + +display() { + for c in "$SYS"/class/drm/card*; do + [ -e "$c" ] || continue + n="${c##*/}" + case "$n" in + *-*) printf '%s %s %s\n' "$n" "$(one "$c/status")" "$(one "$c/modes")" ;; + *) printf '%s driver %s\n' "$n" "$(target "$c/device/driver")" ;; + esac + done + for f in "$SYS"/class/graphics/fb[0-9]*; do + [ -e "$f" ] && printf '%s %s %s\n' "${f##*/}" "$(one "$f/name")" "$(one "$f/virtual_size")" + done +} + +network() { + for i in "$SYS"/class/net/*; do + n="${i##*/}" + [ -e "$i" ] && [ "$n" != lo ] || continue + kind=wired + { [ -e "$i/wireless" ] || [ -e "$i/phy80211" ]; } && kind=wireless + [ -e "$i/device" ] || kind=virtual + printf '%s %s driver %s state %s\n' "$n" "$kind" "$(target "$i/device/driver")" "$(one "$i/operstate")" + done + for r in "$SYS"/class/rfkill/rfkill*; do + [ -e "$r" ] && printf '%s %s %s soft %s hard %s\n' "${r##*/}" "$(one "$r/type")" "$(one "$r/name")" "$(one "$r/soft")" "$(one "$r/hard")" + done + # The machine's interfaces are the net zone's, in its own namespace. + [ -z "$R" ] || return 0 + pid="$(cut -d' ' -f1 /run/kryptik/zones/net/init.pid 2>/dev/null)" + if [ -n "$pid" ]; then + echo "in the net zone:" + nsenter -t "$pid" -n ip -o link 2>/dev/null | awk '$2 != "lo:" { print " " $2, $3 }' + else + echo "the net zone is not running" + fi +} + +inputs() { + awk -F= '/^N: Name/ { name = $2 } /^H: Handlers/ { print name " | " $2 }' "$R/proc/bus/input/devices" 2>/dev/null +} + +missing_firmware() { + kmsg | sed -n \ + -e 's/.*Direct firmware load for \(.*\) failed with error.*/\1/p' \ + -e 's/.*[Uu]nable to load firmware \([^ ,]*\).*/\1/p' \ + -e 's/.*Failed to load DMC firmware \([^ ,]*\).*/\1/p' \ + -e 's/.*maximum version supported: \(iwlwifi-[^ ]*\).*/\1.ucode/p' \ + | sort -u +} + +report() { + pci_raw > "$T/pci" + unbound > "$T/unbound" + disks > "$T/disks" + missing_firmware > "$T/firmware" + # Storage, network, display, an SD host, input, a USB host, a radio. + awk '$8 == "-" && $2 ~ /^(01|02|03|0805|09|0c03|0d)/' "$T/pci" > "$T/undriven" + pci_named "$T/undriven" > "$T/undriven.named" + + echo "kryptik-hwreport 1" + sec system; system + sec machine; machine + sec firmware; firmware + sec cpu; cpu + sec missing + echo "firmware the kernel asked for and did not find, each a line for build/config/firmware.list:" + or_none "$T/firmware" + echo "devices with no driver, each wanting its driver's line in build/config/kernel/boot.fragment:" + or_none "$T/undriven.named" + echo "disks behind a driver that is a module, which boot.fragment must build in before a root can sit there:" + or_none "$T/modular" + sec pci; pci_named "$T/pci" + sec usb; usb + sec "no driver"; cat "$T/unbound" + sec storage; cat "$T/disks" + sec display; display + sec network; network + sec input; inputs + sec modules; awk '{ print $1 }' "$R/proc/modules" 2>/dev/null | sort | tr '\n' ' ' | fold -s -w 78; echo + sec "firmware messages"; kmsg | grep -i firmware + sec "kernel log"; kmsg +} + +T="$(mktemp -d)" || die "no temporary directory" +lock=""; mnt="" +# rmdir, never rm -r: a partition that would not unmount is still under $lock. +cleanup() { + rm -rf "$T" + [ -n "$lock" ] || return 0 + mountpoint -q "$mnt" 2>/dev/null && umount "$mnt" 2>/dev/null + rmdir "$mnt" "$lock" 2>/dev/null +} +trap cleanup EXIT +trap 'exit 1' INT TERM + +if [ "$SAVE" = 0 ]; then + report | scrub + exit 0 +fi + +grep -qs '^media=usb$' "$R/run/kryptik/boot-identity" \ + || die "--save writes to the USB medium this system booted from, and this is not one: redirect the output instead" +. /usr/libexec/kryptik/devices.sh +esp="$(kryptik_part kryptik-esp)" || esp="" +[ -b "$esp" ] || die "no single kryptik-esp partition on the medium this system booted from" +[ "$(one "/sys/class/block/${esp##*/}/ro")" = 0 ] || die "the stick is write-protected: no report was written" + +umask 077 +report | scrub > "$T/report.txt" +mkdir /run/kryptik/hwreport.lock 2>/dev/null || die "another report is being written (/run/kryptik/hwreport.lock)" +lock=/run/kryptik/hwreport.lock; mnt="$lock/esp" +mkdir "$mnt" +mount -o rw,nosuid,nodev,noexec "$esp" "$mnt" 2>/dev/null \ + || die "could not mount ${esp}: is kryptik-install running, or the partition damaged?" +# mount falls back to read-only on a device that refuses writes. +[ -w "$mnt" ] || die "the stick is write-protected: no report was written" +dir="$mnt/kryptik-report" +mkdir -p "$dir" || die "could not make kryptik-report on ${esp}" +n=1 +while [ -e "$dir/report-$n.txt" ]; do n=$((n + 1)); done +# Written, fsynced, then renamed: a stick pulled early holds whole reports only. +if ! { cp "$T/report.txt" "$dir/report-$n.tmp" && sync "$dir/report-$n.tmp" && mv "$dir/report-$n.tmp" "$dir/report-$n.txt"; }; then + rm -f "$dir/report-$n.tmp" + die "could not write the report onto ${esp}: is it full?" +fi +sync -f "$dir" +umount "$mnt" || die "report-$n.txt is written, but ${esp} did not unmount: do not pull the stick yet" +say "written kryptik-report/report-$n.txt on ${esp}, which is unmounted again" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh new file mode 100755 index 00000000..effe681f --- /dev/null +++ b/tools/tests/hwreport.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# Tests for tools/install/kryptik-hwreport.sh on a made-up machine: a tree +# shaped like /sys and /proc and a kernel log, with a serial number, hardware +# addresses and a UUID in the places a real machine has them. Offline. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TOOL="${ROOT}/tools/install/kryptik-hwreport.sh" + +PASS=0; FAIL=0 +green() { printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)); } +red() { printf ' FAIL %s\n' "$1"; FAIL=$((FAIL + 1)); } +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +OUT="$W/report.txt" +want() { if grep -qE -- "$2" "$OUT"; then green "$1"; else red "$1"; fi; } +deny() { if grep -qE -- "$2" "$OUT"; then red "$1"; else green "$1"; fi; } +put() { mkdir -p "$(dirname "$W/$1")"; printf '%s\n' "$2" > "$W/$1"; } + +# --- the machine ------------------------------------------------------------ +put etc/os-release 'VERSION_ID="0.1.20261002.abcdef12"' +echo 'BUILD_ID=abcdef1234' >> "$W/etc/os-release" +put proc/sys/kernel/osrelease 6.18.53-hardened1 +put run/kryptik/boot-identity 'slot=' +printf 'media=usb\nstate=tmpfs\nstate_dev=\nroot_disk=/dev/sdb\n' >> "$W/run/kryptik/boot-identity" +put sys/class/dmi/id/sys_vendor LENOVO +put sys/class/dmi/id/product_name 20XWCTO1WW +put sys/class/dmi/id/product_version 'ThinkPad X1 Carbon Gen 9' +put sys/class/dmi/id/product_serial PF-SECRET-1 +put sys/class/dmi/id/product_uuid 4c4c4544-0042-3010-8051-b4c04f595931 +put sys/class/dmi/id/board_serial L1-SECRET-2 +put sys/firmware/efi/fw_platform_size 64 +mkdir -p "$W/sys/firmware/efi/efivars" +printf '\006\000\000\000\001' > "$W/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c" +printf '\006\000\000\000\000' > "$W/sys/firmware/efi/efivars/SetupMode-8be4df61-93ca-11d2-aa0d-00e098032b8c" +put sys/kernel/security/lockdown 'none integrity [confidentiality]' +put sys/class/tpm/tpm0/tpm_version_major 2 +for n in 0 1; do + printf 'processor\t: %s\nvendor_id\t: GenuineIntel\ncpu family\t: 6\nmodel\t\t: 140\nmodel name\t: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz\nstepping\t: 1\nmicrocode\t: 0xa4\nflags\t\t: fpu nx smep smap umip ibt rdrand aes\n\n' "$n" +done > "$W/proc/cpuinfo" +put sys/devices/system/cpu/present 0-7 +put sys/devices/system/cpu/online 0-3 +put sys/devices/system/cpu/smt/control off +put sys/devices/system/cpu/vulnerabilities/meltdown 'Not affected' +put proc/meminfo 'MemTotal: 16284420 kB' + + +# --- devices: an Ethernet card on a module, a radio and a host bridge with no +# driver, a SATA controller whose driver is a module, and the stick. The +# links are absolute, which the tool resolves as it does sysfs's own. ------ +driver() { # driver BUS NAME [module]: loadable with "module", else built in + mkdir -p "$W/sys/bus/$1/drivers/$2" + [[ "${3:-}" == module ]] || return 0 + put "sys/module/$2/initstate" live + ln -s "$W/sys/module/$2" "$W/sys/bus/$1/drivers/$2/module" +} +pci() { # pci ADDRESS CLASS VENDOR DEVICE [DRIVER] + local d="sys/devices/pci0000:00/$1" + put "$d/class" "$2"; put "$d/vendor" "0x$3"; put "$d/device" "0x$4" + put "$d/subsystem_vendor" 0x17aa; put "$d/subsystem_device" 0x22b8; put "$d/revision" 0x20 + put "$d/modalias" "pci:v0000${3}d0000${4}sv000017AAsd000022B8bc00sc00i00" + mkdir -p "$W/sys/bus/pci/devices" + ln -s "$W/$d" "$W/sys/bus/pci/devices/$1" + [[ -z "${5:-}" ]] || ln -s "$W/sys/bus/pci/drivers/$5" "$W/$d/driver" +} +disk() { # disk NAME UNDER SIZE REMOVABLE MODEL: a SCSI disk below the device UNDER + local s="$2/host0/target0:0:0/0:0:0:0" + put "$s/model" "$5" + ln -s "$W/sys/bus/scsi/drivers/sd" "$W/$s/driver" + put "$s/block/$1/size" "$3"; put "$s/block/$1/removable" "$4" + ln -s "$W/$s" "$W/$s/block/$1/device" + mkdir -p "$W/sys/block" + ln -s "$W/$s/block/$1" "$W/sys/block/$1" +} +driver pci e1000e module; driver pci ahci module; driver pci xhci_hcd; driver scsi sd; driver usb usb-storage +pci 0000:00:00.0 0x060000 8086 9a14 +pci 0000:00:14.0 0x0c0330 8086 a0ed xhci_hcd +pci 0000:00:14.3 0x028000 8086 a0f0 +pci 0000:00:17.0 0x010601 8086 a0d3 ahci +pci 0000:00:1f.6 0x020000 8086 15fb e1000e +disk sda sys/devices/pci0000:00/0000:00:17.0/ata1 1000215216 0 'Samsung SSD 860 ' + +stick=sys/devices/pci0000:00/0000:00:14.0/usb1/1-2 +put "$stick/idVendor" 0781; put "$stick/idProduct" 5581; put "$stick/speed" 5000 +put "$stick/manufacturer" SanDisk; put "$stick/product" Ultra; put "$stick/serial" 4C530001230506115281 +put "$stick/1-2:1.0/bInterfaceClass" 08; put "$stick/1-2:1.0/bInterfaceSubClass" 06; put "$stick/1-2:1.0/bInterfaceProtocol" 50 +ln -s "$W/sys/bus/usb/drivers/usb-storage" "$W/$stick/1-2:1.0/driver" +disk sdb "$stick/1-2:1.0" 30031872 1 Ultra +radio=sys/devices/pci0000:00/0000:00:14.0/usb1/1-3 +put "$radio/1-3:1.0/bInterfaceClass" e0; put "$radio/1-3:1.0/bInterfaceSubClass" 01; put "$radio/1-3:1.0/bInterfaceProtocol" 01 +put "$radio/1-3:1.0/modalias" usb:v8087p0026d0002dcE0dsc01dp01icE0isc01ip01in00 +mkdir -p "$W/sys/bus/usb/devices" +ln -s "$W/$stick" "$W/sys/bus/usb/devices/1-2" +ln -s "$W/$stick/1-2:1.0" "$W/sys/bus/usb/devices/1-2:1.0" +ln -s "$W/$radio/1-3:1.0" "$W/sys/bus/usb/devices/1-3:1.0" + +mkdir -p "$W/usr/share/hwdata" +printf '# a comment\n8086 Intel Corporation\n\t15fb Ethernet Connection (13) I219-LM\n\ta0f0 Wi-Fi 6 AX201\n\t\t8086 0074 a subsystem line\n\ta0d3 Tiger Lake-LP SATA Controller\n10ec Realtek\n\ta0f0 not this vendor\nC 01 Mass storage controller\n\t06 SATA controller\n\t\t01 AHCI 1.0\nC 02 Network controller\n\t00 Ethernet controller\n\t80 Network controller\nC 06 Bridge\n' > "$W/usr/share/hwdata/pci.ids" + +cat > "$W/dmesg" <<'EOF' +[ 0.000000] Linux version 6.18.53-hardened1 (kryptik@build) +[ 0.100000] microcode: Current revision: 0x000000a4 +[ 1.200000] pci 0000:00:1f.6: [8086:15fb] type 00 class 0x020000 +[ 1.300000] e1000e 0000:00:1f.6 eth0: (PCI Express:2.5GT/s:Width x1) 8c:16:45:aa:bb:cc +[ 1.400000] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3 +[ 1.400100] usb 1-2: SerialNumber: 4C530001230506115281 +[ 2.000000] iwlwifi 0000:00:14.3: Direct firmware load for iwlwifi-QuZ-a0-hr-b0-77.ucode failed with error -2 +[ 2.100000] r8169 0000:03:00.0: Unable to load firmware rtl_nic/rtl8168h-2.fw (-2) +[ 2.200000] brcmfmac 0000:02:00.0: Direct firmware load for brcm/brcmfmac4350-pcie.Dell Inc.-XPS 13 9350.bin failed with error -2 +[ 3.000000] EXT4-fs (sdc1): mounted filesystem 0a1b2c3d-1111-2222-3333-444455556666 ro with ordered data mode +[ 3.100000] wlan0: authenticate with 00:11:22:33:44:55 (local address=66:77:88:99:aa:bb) +EOF + +KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" > "$OUT" 2> "$W/err"; rc=$? +[[ "$rc" -eq 0 && ! -s "$W/err" ]] && green "the report is made without a complaint" || { red "exit ${rc}: $(head -3 "$W/err")"; } + +echo "-- what the machine is" +[[ "$(head -1 "$OUT")" == "kryptik-hwreport 1" ]] && green "it opens with its name and format" || red "first line: $(head -1 "$OUT")" +want "the release and the kernel" '^kryptik: 0\.1\.20261002\.abcdef12 \(build abcdef1234\)$' +want "what booted" '^booted: the usb medium$' +want "the maker and the model" '^product_version: ThinkPad X1 Carbon Gen 9$' +want "Secure Boot, read from the firmware" '^secure boot: on$' +want "the lockdown in force" '^lockdown: confidentiality$' +want "the TPM" '^tpm: version 2$' +want "the processor" '^vendor, family/model/stepping: GenuineIntel, 6/140/1$' +want "its microcode, and what loaded it" '^microcode: 0xa4$' +want "the loader's own line" '^microcode: Current revision: 0x000000a4$' +want "the features that matter here" '^of note: nx smep smap umip ibt rdrand aes$' +want "a mitigation as the kernel states it" '^meltdown: Not affected$' +want "the memory" '^memory: 15902 MiB$' + +echo "-- devices and drivers" +want "a device, its driver and its name" '^0000:00:1f\.6 class 020000 8086:15fb sub 17aa:22b8 rev 20 driver e1000e \| Ethernet controller \| Intel Corporation Ethernet Connection \(13\) I219-LM$' +want "a device with no driver" '^0000:00:14\.3 class 028000 8086:a0f0 .* driver - \| Network controller \| Intel Corporation Wi-Fi 6 AX201$' +want "a USB device by its own strings" '^1-2 0781:5581 speed 5000 \| SanDisk Ultra$' +want "a USB interface and its driver" '^1-2:1\.0 interface class 08/06/50 driver usb-storage$' +want "an interface nothing drives, with what would" '^usb 1-3:1\.0 usb:v8087p0026' +want "the disk, through its drivers" '^sda 476\.9 GiB \| Samsung SSD 860 \| sd \(built in\), ahci \(module\)$' +want "the stick the root is on" '^sdb 14\.3 GiB removable \(the root is here\) \| Ultra \| sd \(built in\), usb-storage \(built in\), xhci_hcd \(built in\)$' + +echo "-- what the image lacks" +missing="$(sed -n '/^== missing ==$/,/^== pci ==$/p' "$OUT")" +has() { if grep -qF -- "$2" <<<"$missing"; then green "$1"; else red "$1"; fi; } +has "firmware the kernel named" ' iwlwifi-QuZ-a0-hr-b0-77.ucode' +has "a driver's own wording of it" ' rtl_nic/rtl8168h-2.fw' +has "a name with spaces, whole" ' brcm/brcmfmac4350-pcie.Dell Inc.-XPS 13 9350.bin' +has "the radio that wants a driver" ' 0000:00:14.3 class 028000 8086:a0f0' +has "the disk a root cannot sit on yet" ' sda: ahci' +if grep -qF '0000:00:00.0' <<<"$missing"; then red "a host bridge is listed as wanting a driver"; else green "a host bridge wants none"; fi +if grep -qF 'sdb:' <<<"$missing"; then red "the stick is listed as behind a module"; else green "a disk behind built-in drivers is not listed"; fi + +echo "-- what stays on the machine" +deny "no hardware address" '8c:16:45|00:11:22:33:44:55|66:77:88:99:aa:bb' +want "each struck where it stood" 'eth0: \(PCI Express:2\.5GT/s:Width x1\) xx:xx:xx:xx:xx:xx$' +want "two on one line, both" 'authenticate with xx:xx:xx:xx:xx:xx \(local address=xx:xx:xx:xx:xx:xx\)$' +deny "no serial number from the log or sysfs" '4C530001230506115281|PF-SECRET-1|L1-SECRET-2' +want "the log line kept, the number gone" 'usb 1-2: SerialNumber: $' +deny "no UUID" '0a1b2c3d-1111|4c4c4544-0042' +want "the filesystem line kept" 'mounted filesystem ro' +want "a PCI address is not an address to strike" 'pci 0000:00:1f\.6: \[8086:15fb\] type 00 class 0x020000$' + +echo "-- where it may be written" +sed -i 's/^media=usb$/media=/' "$W/run/kryptik/boot-identity" +KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" --save > "$W/save.out" 2>&1; rc=$? +if [[ "$rc" -ne 0 ]] && grep -q 'this is not one' "$W/save.out"; then green "--save refuses anywhere but a USB medium"; else red "--save off a medium: exit ${rc}: $(head -2 "$W/save.out")"; fi +KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" --elsewhere > "$W/save.out" 2>&1; rc=$? +[[ "$rc" -ne 0 ]] && green "an unknown argument is refused" || red "an unknown argument was taken" + +echo +echo "${PASS} passed, ${FAIL} failed" +[[ "$FAIL" -eq 0 ]] From 1f4a2597f1d8a4959a649878c7a0e9a1c1b85091 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 01:38:27 -0700 Subject: [PATCH 02/23] The services check finds a sourced script without a pipeline: leaving one at the first match killed its writer, which pipefail reported as a script no service runs --- tools/tests/services.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/tests/services.sh b/tools/tests/services.sh index d9e9a0bf..3bbdeebc 100755 --- a/tools/tests/services.sh +++ b/tools/tests/services.sh @@ -183,6 +183,15 @@ done # --- scripts --------------------------------------------------------------- echo echo "-- the scripts the services name" +# Whether a script some service runs sources NAME. Not a pipeline: leaving one +# at the first match kills its writer, which pipefail calls a failure. +sourced() { # sourced NAME + local u + while IFS= read -r u; do + grep -rqF "/usr/libexec/kryptik/${u##*/}" "$SRC"/*/up "$SRC"/*/run 2>/dev/null && return 0 + done < <(grep -lE "^\s*\. +/usr/libexec/kryptik/$1" "${SCRIPTS}"/*.sh 2>/dev/null) + return 1 +} for s in "${SCRIPTS}"/*.sh; do [[ -f "$s" ]] || continue n="$(basename "$s")" @@ -194,10 +203,7 @@ for s in "${SCRIPTS}"/*.sh; do # Each script must be run by a service, or sourced by a script that is. if grep -rqF "/usr/libexec/kryptik/${n}" "$SRC"/*/up "$SRC"/*/run 2>/dev/null; then green "${n}: referenced by a service" - elif grep -lqE "^\s*\. +/usr/libexec/kryptik/${n}" "${SCRIPTS}"/*.sh 2>/dev/null \ - && grep -lE "^\s*\. +/usr/libexec/kryptik/${n}" "${SCRIPTS}"/*.sh \ - | xargs -r -n1 basename | while read -r u; do - grep -rqF "/usr/libexec/kryptik/${u}" "$SRC"/*/up "$SRC"/*/run 2>/dev/null && exit 0; done; then + elif sourced "$n"; then green "${n}: sourced by a script a service runs" else red "${n}: installed by the stage but no service runs it" From 3ffc9581f8a26205cdfbc285b024a2f2920da371 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 01:38:32 -0700 Subject: [PATCH 03/23] A USB medium whose first partition carries a kryptik-report directory gets the report at boot, and kryptik-hwreport --save writes one from its root shell: a temporary file, fsynced, renamed, and the partition unmounted again. No other stick is written, and the installer leaves the reports on the stick --- build/service-scripts/hw-report.sh | 33 +++++++++++++++++++++++ build/services/default/contents | 1 + build/services/hw-report/dependencies | 2 ++ build/services/hw-report/type | 1 + build/services/hw-report/up | 1 + build/services/installer-run/dependencies | 1 + tools/install/kryptik-install.sh | 2 ++ 7 files changed, 41 insertions(+) create mode 100755 build/service-scripts/hw-report.sh create mode 100644 build/services/hw-report/dependencies create mode 100644 build/services/hw-report/type create mode 100644 build/services/hw-report/up diff --git a/build/service-scripts/hw-report.sh b/build/service-scripts/hw-report.sh new file mode 100755 index 00000000..04fc6de6 --- /dev/null +++ b/build/service-scripts/hw-report.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# A hardware report onto the USB medium, for a stick whose first partition +# carries a kryptik-report directory: whoever prepared it asked, and a machine +# with no working screen or keyboard can still be reported on. No other stick +# is written, nor a CD, nor an installed system. Never fails: the boot goes on. +set -u +. /usr/libexec/kryptik/devices.sh +. /usr/libexec/kryptik/ask.sh + +grep -qs '^media=usb$' /run/kryptik/boot-identity || exit 0 +esp="$(kryptik_part kryptik-esp)" || exit 0 +[ -b "$esp" ] || exit 0 + +look=/run/kryptik/hwreport-look +mkdir -p "$look" +mount -o ro,nosuid,nodev,noexec "$esp" "$look" 2>/dev/null || exit 0 +asked=no +[ -d "$look/kryptik-report" ] && asked=yes +umount "$look" +rmdir "$look" 2>/dev/null +[ "$asked" = yes ] || exit 0 + +echo "hw-report: this stick asks for a hardware report: one is written once the drivers have settled" +# Drivers load at coldplug and ask for their firmware as they start; the ten +# seconds are for the slow ones, so the log the report quotes has their words. +/usr/sbin/udevadm settle --timeout=30 || true +sleep 10 +if out="$(/usr/sbin/kryptik-hwreport --save 2>&1)"; then + tell "" "$out" "The report is on the stick: switch the machine off, or pull the stick." "" +else + tell "" "$out" "No hardware report was written." "" +fi +exit 0 diff --git a/build/services/default/contents b/build/services/default/contents index ee9de364..dfb60374 100644 --- a/build/services/default/contents +++ b/build/services/default/contents @@ -10,6 +10,7 @@ seatd net-zone getty-tty1 getty-tty2 +hw-report installer-run boot-success boot-smoke diff --git a/build/services/hw-report/dependencies b/build/services/hw-report/dependencies new file mode 100644 index 00000000..f658fe2b --- /dev/null +++ b/build/services/hw-report/dependencies @@ -0,0 +1,2 @@ +sysinit +eudev-trigger diff --git a/build/services/hw-report/type b/build/services/hw-report/type new file mode 100644 index 00000000..bdd22a18 --- /dev/null +++ b/build/services/hw-report/type @@ -0,0 +1 @@ +oneshot diff --git a/build/services/hw-report/up b/build/services/hw-report/up new file mode 100644 index 00000000..bf101b74 --- /dev/null +++ b/build/services/hw-report/up @@ -0,0 +1 @@ +/usr/libexec/kryptik/hw-report.sh diff --git a/build/services/installer-run/dependencies b/build/services/installer-run/dependencies index f658fe2b..ce22f5d6 100644 --- a/build/services/installer-run/dependencies +++ b/build/services/installer-run/dependencies @@ -1,2 +1,3 @@ sysinit eudev-trigger +hw-report diff --git a/tools/install/kryptik-install.sh b/tools/install/kryptik-install.sh index d802adaf..a52d0b3c 100755 --- a/tools/install/kryptik-install.sh +++ b/tools/install/kryptik-install.sh @@ -313,6 +313,8 @@ mv -f "$MNT_BASE/tesp/EFI/BOOT/BOOTX64.EFI.new" "$MNT_BASE/tesp/EFI/BOOT/BOOTX64 cmp -s "$MNT_BASE/tesp/EFI/BOOT/BOOTX64.EFI" "$MNT_BASE/tesp/EFI/kryptik/kryptik-a.efi" || die "BOOTX64.EFI is not the slot A kernel" printf 'a\n' > "$MNT_BASE/tesp/kryptik/committed-slot" rm -f "$MNT_BASE/tesp/kryptik/media-kernel" +# The stick's hardware reports (kryptik-hwreport) are not the installation's. +rm -rf "$MNT_BASE/tesp/kryptik-report" sync umount "$MNT_BASE/tesp" || die "could not unmount the new ESP" From 4fe4e79a70a9b1bb62b90972255932a2e1bb2556 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 01:38:36 -0700 Subject: [PATCH 04/23] Acceptance boots copies of the USB medium for the hardware report: an unprepared stick stays byte for byte what it was, a prepared one gets whole reports on a clean filesystem, a write-protected one is told why it gets none, an install from it carries none over, and the command writes one from the root shell --- tools/acceptance.sh | 2 + tools/image/hwreport-test.sh | 159 +++++++++++++++++++++++++++++++++++ tools/image/run-ovmf.sh | 13 +-- 3 files changed, 169 insertions(+), 5 deletions(-) create mode 100755 tools/image/hwreport-test.sh diff --git a/tools/acceptance.sh b/tools/acceptance.sh index abc733fd..5b920426 100755 --- a/tools/acceptance.sh +++ b/tools/acceptance.sh @@ -375,6 +375,7 @@ it_medium_shell() { "${IMG}/medium-shell-test.sh" "${m[@]}" } it_smoke_iso() { "${IMG}/media-smoke.sh" --iso "$MEDIA_ISO" --vars clean; } +it_hwreport() { "${IMG}/hwreport-test.sh" --usb "$MEDIA_USB"; } it_smoke_sb() { "${IMG}/media-smoke.sh" --usb "$MEDIA_USB" --vars enrolled; } it_refused() { "${IMG}/media-smoke.sh" --usb "$MEDIA_USB" --vars ms --expect-refused; } it_install() { "${IMG}/install-test.sh" --usb "$MEDIA_USB" --vars clean; } @@ -437,6 +438,7 @@ item build support-status M host 0 it_support_status item boot media-smoke-usb M vm 25 it_smoke_usb need_vm item boot medium-shell M vm 3 it_medium_shell need_vm item boot media-smoke-iso M vm 25 it_smoke_iso need_vm_iso +item boot hardware-report M vm 20 it_hwreport need_vm item install install-test M vm 10 it_install need_vm item install state-test M vm 10 it_state need_vm item integrity ovmf-vars M host 0 it_ovmf_vars need_usb diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh new file mode 100755 index 00000000..d1a6496e --- /dev/null +++ b/tools/image/hwreport-test.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# The hardware report on the USB medium (tools/install/kryptik-hwreport.sh): +# written only onto a stick that asks for one, or by the command; whole; +# naming the machine's parts and nothing that is the machine's alone; and +# left behind by the installer. +# +# tools/image/hwreport-test.sh --usb IMG [--timeout N] +# +# step 1 a stick nobody prepared, booted writable: not a byte changes +# step 2 a stick with a kryptik-report directory: report-1.txt at boot, the +# filesystem clean and the boot file as it was; the next boot adds +# report-2.txt +# step 3 that stick write-protected: no report, and the boot says why +# step 4 an install from that stick: a third report on it, none on the +# installed disk +# step 5 kryptik-hwreport --save at the medium's root shell, on a stick +# nobody prepared +# +# Every boot is of a copy this script makes; the medium itself is only read. +set -uo pipefail +SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=/dev/null +source "${SELF}/../../build/lib/common.sh" +trap - ERR; set +e + +USB=""; TIMEOUT=600 +while [[ "$#" -gt 0 ]]; do + case "$1" in + --usb) USB="${2:?}"; shift 2 ;; + --timeout) TIMEOUT="${2:?}"; shift 2 ;; + -h|--help) sed -n '2,19p' "${BASH_SOURCE[0]}"; exit 0 ;; + *) die "unknown argument: $1" ;; + esac +done +[[ -n "$USB" && -f "$USB" ]] || die "--usb IMG is required and must exist" +VMDIR="${KRYPTIK_WORK}/vm"; mkdir -p "$VMDIR" +for t in python3 sfdisk mmd mtype mdir fsck.vfat truncate; do have "$t" || die "required tool not found: $t"; done + +# shellcheck source=tools/image/suite-lib.sh +source "${SELF}/suite-lib.sh" +want() { if grep -qE -- "$2" "$1"; then green "$3"; else red "$3"; fi; } +deny() { if grep -qE -- "$2" "$1"; then red "$3"; else green "$3"; fi; } +part_size() { sfdisk -d "$1" 2>/dev/null | awk -v n="$2" -F'[ ,]+' '$1 ~ n"$" {for(i=1;i<=NF;i++) if($i=="size=") print $(i+1)}'; } +sha() { sha256sum "$1" | cut -c1-64; } + +# The stick's first partition, as mtools addresses it inside an image. +export MTOOLS_SKIP_CHECK=1 +ESP_START="$(part_start "$USB" 1)"; ESP_SECTORS="$(part_size "$USB" 1)" +[[ -n "$ESP_START" && -n "$ESP_SECTORS" ]] || die "could not read the medium's partition table" +esp_of() { printf '%s@@%s' "$1" "$(( ESP_START * 512 ))"; } +report_of() { mtype -i "$(esp_of "$1")" "::/kryptik-report/report-$2.txt" 2>/dev/null; } # report_of IMAGE N +fsck_esp() { # fsck_esp IMAGE: its first partition, checked and not repaired + dd if="$1" of="${VMDIR}/hwreport-esp.img" bs=512 skip="$ESP_START" count="$ESP_SECTORS" status=none \ + && fsck.vfat -n "${VMDIR}/hwreport-esp.img" > "${VMDIR}/hwreport-fsck.txt" 2>&1 +} + +PLAIN="${VMDIR}/hwreport-plain.img" # a copy nobody prepared +ASKED="${VMDIR}/hwreport-asked.img" # a copy with the directory +CTL="${VMDIR}/testctl-hwreport.img" +"${SELF}/mk-testctl.sh" --out "$CTL" --key "$TESTCTL_KEY" smoke_poweroff=1 > /dev/null || die "could not make the control disk" +BOOT_FILE="$(mtype -i "$(esp_of "$USB")" ::/EFI/BOOT/BOOTX64.EFI | sha256sum | cut -c1-64)" +fsck_esp "$USB"; PRISTINE_FSCK=$? + +# ----------------------------------------------------------------- step 1 -- +step "step 1: a stick nobody prepared is not written, though it could be" +cp --sparse=always "$USB" "$PLAIN" || die "could not copy the medium" +smoke hwreport-plain --usb "$PLAIN" --usb-writable --testctl "$CTL" --timeout "$TIMEOUT" > /dev/null +P1="${VMDIR}/hwreport-p1.txt"; boot_txt > "$P1" +want "$P1" 'KRYPTIK_SMOKE: END' "the medium booted" +want "$P1" 'Power down' "and powered off" +deny "$P1" 'hw-report:|kryptik-hwreport:' "no report was offered or written" +if cmp -s "$USB" "$PLAIN"; then green "the stick is byte for byte what it was"; else red "the stick changed, and nobody asked"; fi + +# ----------------------------------------------------------------- step 2 -- +step "step 2: a stick with a kryptik-report directory gets a report at boot" +cp --sparse=always "$USB" "$ASKED" || die "could not copy the medium" +mmd -i "$(esp_of "$ASKED")" ::/kryptik-report || die "could not make the directory on the copy" +smoke hwreport-asked --usb "$ASKED" --usb-writable --testctl "$CTL" --net user --timeout "$TIMEOUT" > /dev/null +P2="${VMDIR}/hwreport-p2.txt"; boot_txt > "$P2" +want "$P2" 'hw-report: this stick asks for a hardware report' "the boot saw the request" +want "$P2" 'kryptik-hwreport: written kryptik-report/report-1\.txt' "and said what it wrote" +want "$P2" 'Power down' "the medium powered off afterwards" +R1="${VMDIR}/hwreport-1.txt"; report_of "$ASKED" 1 > "$R1" +[[ "$(head -1 "$R1")" == "kryptik-hwreport 1" ]] && green "report-1.txt is on the stick, whole" || red "no report-1.txt on the stick, or not a report" +want "$R1" '^booted: the usb medium$' "it says what booted" +want "$R1" '^sys_vendor: QEMU' "the machine, by its maker" +want "$R1" '^secure boot: off$' "Secure Boot as the firmware has it" +want "$R1" '^microcode: ' "the processor's microcode" +want "$R1" ' class 0c0330 .* driver xhci_hcd' "a PCI device with the driver that took it" +want "$R1" ' interface class 08/06/50 driver usb-storage$' "the stick's own interface, by its driver" +want "$R1" '^sd[a-z] .*\(the root is here\).*usb-storage \(built in\)' "the disk the root is on, through built-in drivers" +want "$R1" '^== missing ==$' "what the image lacks has its place" +want "$R1" '^== kernel log ==$' "the kernel log is there" +want "$R1" 'Linux version .*hardened' "from its first line" +deny "$R1" '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "no hardware address" +deny "$R1" '[Ss]erial ?[Nn]umber[:=] ?[^< ]' "no serial number" +deny "$R1" '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' "no UUID" +if [[ "$PRISTINE_FSCK" -eq 0 ]]; then + if fsck_esp "$ASKED"; then green "the stick's filesystem is clean after the write" + else red "the stick's filesystem is not clean after the write"; sed 's/^/ /' "${VMDIR}/hwreport-fsck.txt" | head -8; fi +else + echo " (fsck.vfat faults the medium's own partition here, so it is not asked about the copy)" +fi +[[ "$(mtype -i "$(esp_of "$ASKED")" ::/EFI/BOOT/BOOTX64.EFI | sha256sum | cut -c1-64)" == "$BOOT_FILE" ]] \ + && green "the signed boot file is as it was" || red "the boot file changed" +smoke hwreport-again --usb "$ASKED" --usb-writable --testctl "$CTL" --timeout "$TIMEOUT" > /dev/null +want "$BOOTLOG" 'kryptik-hwreport: written kryptik-report/report-2\.txt' "the next boot writes report-2.txt" +if [[ "$(report_of "$ASKED" 2 | head -1)" == "kryptik-hwreport 1" ]] && report_of "$ASKED" 1 | cmp -s - "$R1"; then + green "beside report-1.txt, which is as it was" +else + red "the second report is missing, or the first changed" +fi + +# ----------------------------------------------------------------- step 3 -- +step "step 3: the same stick, write-protected" +before="$(sha "$ASKED")" +smoke hwreport-protected --usb "$ASKED" --testctl "$CTL" --timeout "$TIMEOUT" > /dev/null +want "$BOOTLOG" 'kryptik-hwreport: FAILED: the stick is write-protected' "the boot says the stick is write-protected" +want "$BOOTLOG" 'No hardware report was written' "and that no report was written" +want "$BOOTLOG" 'KRYPTIK_SMOKE: END' "and goes on" +[[ "$(sha "$ASKED")" == "$before" ]] && green "nothing on the stick changed" || red "a write-protected stick changed" + +# ----------------------------------------------------------------- step 4 -- +step "step 4: the installer leaves the reports on the stick" +DISK="${VMDIR}/hwreport-installed.img" +fresh_disk "$USB" +if install_disk hwreport-install "$ASKED" --usb-writable; then green "an install from the stick succeeded"; else red "the install from the stick failed"; fi +P4="${VMDIR}/hwreport-p4.txt"; boot_txt > "$P4" +want "$P4" 'kryptik-hwreport: written kryptik-report/report-3\.txt' "the report was on the stick before the installer copied its partition" +want "$P4" 'KRYPTIK_INSTALL: verify: esp_files=.*EFI/BOOT/BOOTX64.EFI' "the installed ESP has its boot file" +deny "$P4" 'KRYPTIK_INSTALL: verify: esp_files=.*kryptik-report' "and no report among its files" +TESP="${DISK}@@$(( $(part_start "$DISK" 1) * 512 ))" +if mdir -i "$TESP" ::/kryptik > /dev/null 2>&1 && ! mdir -i "$TESP" ::/kryptik-report > /dev/null 2>&1; then + green "from the host: the installed disk carries no kryptik-report" +else + red "from the host: the installed disk carries kryptik-report, or its ESP cannot be read" +fi +[[ "$(report_of "$ASKED" 3 | head -1)" == "kryptik-hwreport 1" ]] && green "the stick keeps all three" || red "report-3.txt is not on the stick" + +# ----------------------------------------------------------------- step 5 -- +step "step 5: kryptik-hwreport --save at the medium's root shell" +cp --sparse=always "$USB" "$PLAIN" || die "could not copy the medium" +out="$("${SELF}/run-ovmf.sh" --usb "$PLAIN" --usb-writable --mode serve --name hwreport-shell)" +SER="$(sed -n 's/^serial=//p' <<<"$out")"; PIDF="$(sed -n 's/^pid=//p' <<<"$out")" +[[ -S "$SER" ]] || die "no serial socket: ${out}" +# The shell's own arithmetic shows it is listening; a getty drops what is +# typed in its first second, so the line goes twice. +python3 "$DRV" --serial "$SER" --timeout 300 \ + "expect:KRYPTIK_SMOKE: END" "sleep:3" \ + 'send:echo shell-$((6 * 7))' "sleep:2" 'send:echo shell-$((6 * 7))' "expect:shell-42" \ + "send:kryptik-hwreport --save" "expect:kryptik-hwreport: written kryptik-report/report-1\.txt" \ + "send:poweroff" "expect:Power down" "wait-exit" +drc=$? +stop_vm +[[ "$drc" -eq 0 ]] && green "the command wrote a report and said where" || red "the serial drive failed (see above)" +[[ "$(report_of "$PLAIN" 1 | head -1)" == "kryptik-hwreport 1" ]] && green "and it is on the stick" || red "no report on the stick" + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[[ "$FAIL" -eq 0 ]] || exit 1 diff --git a/tools/image/run-ovmf.sh b/tools/image/run-ovmf.sh index 6ac6dc47..f2ce1883 100755 --- a/tools/image/run-ovmf.sh +++ b/tools/image/run-ovmf.sh @@ -2,13 +2,15 @@ # Boot Kryptik media or an installed disk under OVMF, as real firmware would: # no -kernel, -initrd, -append or host filesystem sharing. # -# tools/image/run-ovmf.sh (--usb IMG | --iso ISO | --no-media) +# tools/image/run-ovmf.sh (--usb IMG [--usb-writable] | --iso ISO | --no-media) # [--disk FILE]... [--testctl FILE] [--vars clean|enrolled|ms|FILE] # [--vars-file FILE] [--mode console|smoke|serve] [--timeout N] # [--log FILE] [--net none|user] [--mem MB] [--cpus N] [--gpu] # [--allow-reboot] [--until REGEX] [--name TAG] # -# --usb IMG the medium as a USB mass-storage device (removable) +# --usb IMG the medium as a USB mass-storage device (removable), +# write-protected unless --usb-writable says the guest may +# write to it: for a copy a test made, never the medium # --iso ISO the medium as a SATA CD-ROM (/dev/sr0 in the guest) # --no-media boot only the --disk(s): an installed system # --disk FILE a virtio disk (repeatable; the first is the install target @@ -39,10 +41,11 @@ source "${SELF}/../../build/lib/common.sh" USB=""; ISO=""; NOMEDIA=0; DISKS=(); TESTCTL=""; VARS="clean"; VARS_FILE="" MODE="smoke"; TIMEOUT=300; LOG=""; NET="none"; MEM=2048; CPUS=2; GPU=0; ALLOW_REBOOT=0; NAME="vm" -DISK_RO=0; BLKDEBUG=""; UNTIL="" +DISK_RO=0; BLKDEBUG=""; UNTIL=""; USB_RO=on while [[ "$#" -gt 0 ]]; do case "$1" in --usb) USB="${2:?}"; shift 2 ;; + --usb-writable) USB_RO=off; shift ;; --iso) ISO="${2:?}"; shift 2 ;; --no-media) NOMEDIA=1; shift ;; --disk) DISKS+=("${2:?}"); shift 2 ;; @@ -61,7 +64,7 @@ while [[ "$#" -gt 0 ]]; do --allow-reboot) ALLOW_REBOOT=1; shift ;; --until) UNTIL="${2:?}"; shift 2 ;; --name) NAME="${2:?}"; shift 2 ;; - -h|--help) sed -n '2,33p' "${BASH_SOURCE[0]}"; exit 0 ;; + -h|--help) sed -n '2,35p' "${BASH_SOURCE[0]}"; exit 0 ;; *) die "unknown argument: $1" ;; esac done @@ -143,7 +146,7 @@ case "$NET" in esac if [[ -n "$USB" ]]; then ARGS+=( -device qemu-xhci,id=xhci - -drive "if=none,id=usbmedia,format=raw,readonly=on,file=${USB}" + -drive "if=none,id=usbmedia,format=raw,readonly=${USB_RO},file=${USB}" -device usb-storage,bus=xhci.0,drive=usbmedia,removable=on ) fi if [[ -n "$ISO" ]]; then From a1f52f1b42a7d933ec88943d5cf271c8544e8ffc Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 08:13:49 -0700 Subject: [PATCH 05/23] The first step looks for the report's own lines, not the service manager's mention of the service --- tools/image/hwreport-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh index d1a6496e..dff943a0 100755 --- a/tools/image/hwreport-test.sh +++ b/tools/image/hwreport-test.sh @@ -68,7 +68,7 @@ smoke hwreport-plain --usb "$PLAIN" --usb-writable --testctl "$CTL" --timeout "$ P1="${VMDIR}/hwreport-p1.txt"; boot_txt > "$P1" want "$P1" 'KRYPTIK_SMOKE: END' "the medium booted" want "$P1" 'Power down' "and powered off" -deny "$P1" 'hw-report:|kryptik-hwreport:' "no report was offered or written" +deny "$P1" 'hw-report: this stick|kryptik-hwreport: |hardware report' "no report was offered or written" if cmp -s "$USB" "$PLAIN"; then green "the stick is byte for byte what it was"; else red "the stick changed, and nobody asked"; fi # ----------------------------------------------------------------- step 2 -- From fdc8e883212d92f84ccd77a891afd5e2058484c0 Mon Sep 17 00:00:00 2001 From: DevomB Date: Mon, 5 Oct 2026 08:32:30 -0700 Subject: [PATCH 06/23] The hardware report mounts the stick as FAT and nothing else: on another filesystem under the stick's label, a link at the report's name would carry root's write to a disk of the machine --- build/service-scripts/hw-report.sh | 4 +++- tools/install/kryptik-hwreport.sh | 2 +- tools/tests/hwreport.sh | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/build/service-scripts/hw-report.sh b/build/service-scripts/hw-report.sh index 04fc6de6..b1e0a26f 100755 --- a/build/service-scripts/hw-report.sh +++ b/build/service-scripts/hw-report.sh @@ -13,7 +13,9 @@ esp="$(kryptik_part kryptik-esp)" || exit 0 look=/run/kryptik/hwreport-look mkdir -p "$look" -mount -o ro,nosuid,nodev,noexec "$esp" "$look" 2>/dev/null || exit 0 +# As FAT and nothing else, here and where the report is written: FAT holds no +# links, so a prepared stick cannot send root's write anywhere but itself. +mount -t vfat -o ro,nosuid,nodev,noexec "$esp" "$look" 2>/dev/null || exit 0 asked=no [ -d "$look/kryptik-report" ] && asked=yes umount "$look" diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index d6c892ee..e25bab9b 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -318,7 +318,7 @@ report | scrub > "$T/report.txt" mkdir /run/kryptik/hwreport.lock 2>/dev/null || die "another report is being written (/run/kryptik/hwreport.lock)" lock=/run/kryptik/hwreport.lock; mnt="$lock/esp" mkdir "$mnt" -mount -o rw,nosuid,nodev,noexec "$esp" "$mnt" 2>/dev/null \ +mount -t vfat -o rw,nosuid,nodev,noexec "$esp" "$mnt" 2>/dev/null \ || die "could not mount ${esp}: is kryptik-install running, or the partition damaged?" # mount falls back to read-only on a device that refuses writes. [ -w "$mnt" ] || die "the stick is write-protected: no report was written" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index effe681f..d514a3c5 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -164,6 +164,14 @@ if [[ "$rc" -ne 0 ]] && grep -q 'this is not one' "$W/save.out"; then green "--s KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" --elsewhere > "$W/save.out" 2>&1; rc=$? [[ "$rc" -ne 0 ]] && green "an unknown argument is refused" || red "an unknown argument was taken" +echo "-- how the stick is mounted" +# A link on another filesystem under the stick's label would carry the write +# to a disk of the machine. +for f in build/service-scripts/hw-report.sh tools/install/kryptik-hwreport.sh; do + m="$(grep -cE '^[[:space:]]*mount ' "${ROOT}/${f}")"; v="$(grep -cE '^[[:space:]]*mount -t vfat ' "${ROOT}/${f}")" + [[ "$m" -ge 1 && "$m" -eq "$v" ]] && green "${f}: mounts the stick as FAT and nothing else" || red "${f}: ${m} mount(s), ${v} of them as FAT" +done + echo echo "${PASS} passed, ${FAIL} failed" [[ "$FAIL" -eq 0 ]] From 597affce34d183e9d08c859d8babde66cdcfef88 Mon Sep 17 00:00:00 2001 From: DevomB Date: Tue, 13 Oct 2026 09:47:00 -0700 Subject: [PATCH 07/23] The report's fixture holds a chassis serial, asset tags and a panel's EDID serial, none of which may reach the report The tool reads an allowlist of DMI fields and a connector's state and modes, never its EDID; the fixture now plants each identifier a real machine has there and denies it in the report, and wants the panel named by connector, state and mode. --- tools/tests/hwreport.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index d514a3c5..2a141a15 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -27,6 +27,13 @@ put sys/class/dmi/id/product_version 'ThinkPad X1 Carbon Gen 9' put sys/class/dmi/id/product_serial PF-SECRET-1 put sys/class/dmi/id/product_uuid 4c4c4544-0042-3010-8051-b4c04f595931 put sys/class/dmi/id/board_serial L1-SECRET-2 +put sys/class/dmi/id/chassis_serial CH-SECRET-3 +put sys/class/dmi/id/chassis_asset_tag AT-SECRET-4 +put sys/class/dmi/id/board_asset_tag BA-SECRET-5 +# A panel whose EDID carries its serial, as a monitor's does: the report reads its state and modes. +put sys/class/drm/card0-eDP-1/status connected +put sys/class/drm/card0-eDP-1/modes 2256x1504 +{ printf '\000\377\377\377\377\377\377\000'; printf 'EDIDSER-6'; head -c 111 /dev/zero; } > "$W/sys/class/drm/card0-eDP-1/edid" put sys/firmware/efi/fw_platform_size 64 mkdir -p "$W/sys/firmware/efi/efivars" printf '\006\000\000\000\001' > "$W/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c" @@ -152,6 +159,8 @@ deny "no hardware address" '8c:16:45|00:11:22:33:44:55|66:77:88 want "each struck where it stood" 'eth0: \(PCI Express:2\.5GT/s:Width x1\) xx:xx:xx:xx:xx:xx$' want "two on one line, both" 'authenticate with xx:xx:xx:xx:xx:xx \(local address=xx:xx:xx:xx:xx:xx\)$' deny "no serial number from the log or sysfs" '4C530001230506115281|PF-SECRET-1|L1-SECRET-2' +deny "no chassis serial, asset tag or EDID serial" 'CH-SECRET-3|AT-SECRET-4|BA-SECRET-5|EDIDSER-6' +want "the panel by its connector, state and mode" 'card0-eDP-1 connected 2256x1504' want "the log line kept, the number gone" 'usb 1-2: SerialNumber: $' deny "no UUID" '0a1b2c3d-1111|4c4c4544-0042' want "the filesystem line kept" 'mounted filesystem ro' From e486d76462d1c5692aa8569a87ee60af6a5a0bf4 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 04:41:37 -0700 Subject: [PATCH 08/23] The hardware report drops control characters from what devices say of themselves A USB device's manufacturer and product strings, DMI fields and an interface's name reach the report as their makers wrote them: the kernel filters no control characters from usb_string(), and an interface name may hold an ESC. A stick naming itself with an escape sequence would have it run on the terminal of whoever prints or reads the report. scrub(), which every line passes, now drops C0 controls but tab and newline, DEL, and C1 controls in UTF-8 (U+0080 to U+009F, CSI among them). --- tools/install/kryptik-hwreport.sh | 8 ++++++-- tools/tests/hwreport.sh | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index e25bab9b..7d5f2eb3 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -39,13 +39,17 @@ sec() { printf '\n== %s ==\n' "$1"; } or_none() { if [ -s "$1" ]; then sed 's/^/ /' "$1"; else echo " (none)"; fi; } # The log prints what must not leave the machine: a network driver its -# address, USB a device's serial number, a filesystem its UUID. +# address, USB a device's serial number, a filesystem its UUID. And a +# device's strings, DMI fields and interface names are their makers' own +# words: control characters, C1's in UTF-8 too, are dropped so no escape +# sequence reaches the terminal the report is read on. scrub() { sed -E \ -e ':a' -e 's/(^|[^0-9A-Fa-f])([0-9A-Fa-f]{2}:){5,}[0-9A-Fa-f]{2}($|[^0-9A-Fa-f])/\1xx:xx:xx:xx:xx:xx\3/' -e 'ta' \ -e 's/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}//g' \ -e 's/([Ss]erial ?([Nn]umber|[Nn]o)[.:= ]+)[^ ,;]+/\1/g' \ - -e 's/(S\/N[:= ]+)[^ ,;]+/\1/g' + -e 's/(S\/N[:= ]+)[^ ,;]+/\1/g' \ + | LC_ALL=C tr -d '\000-\010\013-\037\177' | LC_ALL=C sed 's/\xc2[\x80-\x9f]//g' } # A driver that is a loadable module cannot hold the root: there is no diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index 2a141a15..bcc40b65 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -87,7 +87,8 @@ disk sda sys/devices/pci0000:00/0000:00:17.0/ata1 1000215216 0 'Samsung SSD 860 stick=sys/devices/pci0000:00/0000:00:14.0/usb1/1-2 put "$stick/idVendor" 0781; put "$stick/idProduct" 5581; put "$stick/speed" 5000 -put "$stick/manufacturer" SanDisk; put "$stick/product" Ultra; put "$stick/serial" 4C530001230506115281 +# Its strings end in an ESC and a CSI in UTF-8, which the report must not pass to a terminal. +put "$stick/manufacturer" $'SanDisk\033'; put "$stick/product" $'Ultra\302\233'; put "$stick/serial" 4C530001230506115281 put "$stick/1-2:1.0/bInterfaceClass" 08; put "$stick/1-2:1.0/bInterfaceSubClass" 06; put "$stick/1-2:1.0/bInterfaceProtocol" 50 ln -s "$W/sys/bus/usb/drivers/usb-storage" "$W/$stick/1-2:1.0/driver" disk sdb "$stick/1-2:1.0" 30031872 1 Ultra @@ -160,6 +161,10 @@ want "each struck where it stood" 'eth0: \(PCI Express:2\.5GT/s:Width want "two on one line, both" 'authenticate with xx:xx:xx:xx:xx:xx \(local address=xx:xx:xx:xx:xx:xx\)$' deny "no serial number from the log or sysfs" '4C530001230506115281|PF-SECRET-1|L1-SECRET-2' deny "no chassis serial, asset tag or EDID serial" 'CH-SECRET-3|AT-SECRET-4|BA-SECRET-5|EDIDSER-6' +ctl="$(LC_ALL=C tr -cd '\000-\010\013-\037\177' < "$OUT" | wc -c)" +c1="$(LC_ALL=C grep -c $'\302[\200-\237]' "$OUT")" +[[ "$ctl" -eq 0 && "$c1" -eq 0 ]] && green "no control character from a device's strings" \ + || red "control characters in the report: ${ctl} C0 or DEL, ${c1} line(s) with a C1" want "the panel by its connector, state and mode" 'card0-eDP-1 connected 2256x1504' want "the log line kept, the number gone" 'usb 1-2: SerialNumber: $' deny "no UUID" '0a1b2c3d-1111|4c4c4544-0042' From eba7f9f1aa229efd30ce0604ec83c12a3f96d451 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 06:43:07 -0700 Subject: [PATCH 09/23] The image's program list names the hardware report's two programs Stage 06 checks the root's executable files against image-programs.txt both ways, and the report added /usr/sbin/kryptik-hwreport and the medium's /usr/libexec/kryptik/hw-report.sh without naming them, so a build of this branch stopped at its rootfs step. --- build/config/image-programs.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/config/image-programs.txt b/build/config/image-programs.txt index dbe8969e..f439fddb 100644 --- a/build/config/image-programs.txt +++ b/build/config/image-programs.txt @@ -1115,6 +1115,7 @@ usr/libexec/kryptik/dhcpcd-hook usr/libexec/kryptik/esp-records.sh usr/libexec/kryptik/eudev-trigger.sh usr/libexec/kryptik/firstboot.sh +usr/libexec/kryptik/hw-report.sh usr/libexec/kryptik/installer-run.sh usr/libexec/kryptik/keyboard.sh usr/libexec/kryptik/kryptik-sound @@ -1213,6 +1214,7 @@ usr/sbin/integritysetup usr/sbin/ip usr/sbin/iw usr/sbin/kryptik-efiboot +usr/sbin/kryptik-hwreport usr/sbin/kryptik-install usr/sbin/kryptik-recover usr/sbin/kryptik-update From 393de8bb804aaae158fab034ca9652bc87a4f9fc Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 09:53:51 -0700 Subject: [PATCH 10/23] The hardware report says what a machine's sound is made of, by the kinds the laptop sound note sorts The report listed every PCI device, so the audio controller's class was there, but not what tells one kind of laptop sound from another: whether the firmware describes digital microphones for Intel's DSP, SoundWire parts, speaker amplifiers, or microphones on AMD's coprocessor. The October machines' reports are meant to answer that. A new `audio` section: each audio controller on PCI with its class and the driver that took it, AMD's coprocessor among them; the HD Audio codecs by maker and subsystem; the NHLT table's endpoints by link type (pdm, ssp); the SoundWire controller ACPI describes at _ADR 0x40000000 under an HD Audio controller, as the kernel finds it, and each part on it by link, maker and part; the speaker amplifiers ACPI names (Cirrus's CSC3551/4/6/7 and CLSA0100/1, TI's TIAS2781, TXNW2770/2781/5825 and INT8866) with any driver bound to them; and which of AcpDmicConnected, acp-audio-device-type and _WOV the ACPI tables hold, the names AMD's drivers look for. Model and part IDs only. tools/tests/hwreport.sh builds each kind on the fixture machine: HD Audio alone, a DSP with an NHLT of a microphone array and an I2S link, a SoundWire controller with two parts beside an ACPI node that is not one, AMD's coprocessor with AcpDmicConnected in its DSDT, and two CS35L41s described as one device beside one the firmware calls absent. --- tools/install/kryptik-hwreport.sh | 81 ++++++++++++++++++++++++++++ tools/tests/hwreport.sh | 88 +++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 7d5f2eb3..7038970a 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -251,6 +251,86 @@ inputs() { awk -F= '/^N: Name/ { name = $2 } /^H: Handlers/ { print name " | " $2 }' "$R/proc/bus/input/devices" 2>/dev/null } +# The machine's sound, sorted as docs/design/laptop-audio.md sorts laptops: +# the audio controllers on PCI and what took them, AMD's coprocessor among +# them, the HD Audio codecs, the endpoints the NHLT table describes for +# Intel's DSP, the SoundWire parts ACPI describes, the speaker amplifiers ACPI +# names, and the names AMD's drivers look for to find microphones. Model and +# part IDs only, which no machine has alone. +audio() { + for d in "$SYS"/bus/pci/devices/*; do + c="$(hex "$d/class")"; v="$(hex "$d/vendor")" + case "$c" in + 0401*|0403*) what=controller ;; + 0480*) [ "$v" = 1022 ] || continue; what=acp ;; + *) continue ;; + esac + drv="$(target "$d/driver")" + printf '%s %s class %s %s:%s rev %s driver %s\n' "$what" "${d##*/}" "$c" "$v" "$(hex "$d/device")" \ + "$(hex "$d/revision")" "${drv:--}" + [ "$what" = acp ] || soundwire "$d" + done + for c in "$SYS"/bus/hdaudio/devices/*; do + [ -r "$c/vendor_id" ] && printf 'codec %s sub %s\n' "$(hex "$c/vendor_id")" "$(hex "$c/subsystem_id")" + done + nhlt + for a in "$SYS"/bus/acpi/devices/*; do + case "${a##*/}" in + CSC355[1467]:*|CLSA010[01]:*|TIAS2781:*|TXNW2770:*|TXNW2781:*|TXNW5825:*|INT8866:*) ;; + *) continue ;; + esac + # Not one the firmware describes and calls absent (_STA 0). + [ "$(one "$a/status")" != 0 ] || continue + drv="" + for n in "$a"/physical_node*; do + x="$(target "$n/driver")"; [ -z "$x" ] || drv="$drv $x" + done + printf 'amplifier %s driver%s\n' "${a##*/}" "${drv:- -}" + done + names="" + for k in AcpDmicConnected acp-audio-device-type _WOV; do + grep -qaF -- "$k" "$SYS"/firmware/acpi/tables/DSDT "$SYS"/firmware/acpi/tables/SSDT* 2>/dev/null && names="$names $k" + done + [ -z "$names" ] || echo "acpi names:$names" +} + +# soundwire DEVICE: the SoundWire controller ACPI describes under an HD Audio +# controller, which the kernel finds at _ADR 0x40000000, and each part on its +# links: MIPI's address holds the link in bits 51-48, the maker in 39-24 and +# the part in 23-8. +soundwire() { + for s in "$1"/firmware_node/*/; do + [ "$(one "${s}adr")" = 0x40000000 ] || continue + found="" + for p in "$s"*/; do + x="$(one "${p}adr")" + case "$x" in 0x????????????????) ;; *) continue ;; esac + printf 'soundwire %s link %d part %04x:%04x\n' "${1##*/}" \ + $(( ($x >> 48) & 15 )) $(( ($x >> 24) & 65535 )) $(( ($x >> 8) & 65535 )) + found=1 + done + [ -n "$found" ] || printf 'soundwire %s no parts\n' "${1##*/}" + done +} + +# The endpoints the NHLT table describes for Intel's DSP: a 36-byte header, a +# count, then each endpoint's length and link type, 2 a digital microphone +# array (PDM) and 3 an I2S link (SSP). A length that cannot be ends the walk. +nhlt() { + if [ ! -r "$SYS/firmware/acpi/tables/NHLT" ]; then echo "nhlt: none"; return 0; fi + od -An -v -t u1 "$SYS/firmware/acpi/tables/NHLT" | awk ' + { for (i = 1; i <= NF; i++) b[n++] = $i } + END { + at = 37 + for (e = 0; e < b[36] && at + 4 < n; e++) { + k[b[at + 4]]++ + len = b[at] + 256 * b[at + 1] + 65536 * b[at + 2] + 16777216 * b[at + 3] + at += (len > 4) ? len : n + } + printf "nhlt: %d endpoints, pdm %d, ssp %d\n", e, k[2], k[3] + }' +} + missing_firmware() { kmsg | sed -n \ -e 's/.*Direct firmware load for \(.*\) failed with error.*/\1/p' \ @@ -286,6 +366,7 @@ report() { sec "no driver"; cat "$T/unbound" sec storage; cat "$T/disks" sec display; display + sec audio; audio sec network; network sec input; inputs sec modules; awk '{ print $1 }' "$R/proc/modules" 2>/dev/null | sort | tr '\n' ' ' | fold -s -w 78; echo diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index bcc40b65..e782b48d 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -186,6 +186,94 @@ for f in build/service-scripts/hw-report.sh tools/install/kryptik-hwreport.sh; d [[ "$m" -ge 1 && "$m" -eq "$v" ]] && green "${f}: mounts the stick as FAT and nothing else" || red "${f}: ${m} mount(s), ${v} of them as FAT" done +echo "-- the sound, by the kinds of docs/design/laptop-audio.md" +want "the sound has its place" '^== audio ==$' +A="$W/audio.txt" +# The report's audio section, from the tree as it stands. +audio_now() { + KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" > "$W/report-audio.txt" 2>&1 + awk '$0 == "== audio ==" { on = 1; next } on && /^== .* ==$/ { exit } on' "$W/report-audio.txt" > "$A" +} +aud() { if grep -qE -- "$2" "$A"; then green "$1"; else red "$1: $(paste -sd'|' "$A")"; fi; } +noaud() { if grep -qE -- "$2" "$A"; then red "$1: $(paste -sd'|' "$A")"; else green "$1"; fi; } +# Each kind on a machine with no other sound: the controllers, codecs, tables +# and ACPI devices of the kind before are taken away first. +quiet() { + local p + for p in 0000:00:1f.3 0000:04:00.5 0000:04:00.6; do + rm -rf "$W/sys/devices/pci0000:00/$p" "$W/sys/bus/pci/devices/$p" + done + rm -rf "$W/sys/bus/hdaudio" "$W/sys/firmware/acpi" "$W/sys/bus/acpi" "$W/sys/devices/LNXSYSTM:00" "$W/sys/devices/platform" +} +codec() { # codec NAME VENDOR SUBSYSTEM: an HD Audio codec on the bus its controller's driver makes + put "sys/bus/hdaudio/devices/$1/vendor_id" "0x$2"; put "sys/bus/hdaudio/devices/$1/subsystem_id" "0x$3" +} +driver pci snd_hda_intel module + +quiet +pci 0000:00:1f.3 0x040300 8086 a0c8 snd_hda_intel +codec hdaudioC0D0 10ec0287 17aa22e6; codec hdaudioC0D2 80862812 80860101 +audio_now +aud "HD Audio with no DSP, and the driver that took it" '^controller 0000:00:1f\.3 class 040300 8086:a0c8 rev 20 driver snd_hda_intel$' +aud "its codec, by maker and subsystem" '^codec 10ec0287 sub 17aa22e6$' +aud "and the graphics' own" '^codec 80862812 sub 80860101$' +aud "no NHLT table, said so" '^nhlt: none$' +noaud "nothing on SoundWire, no amplifier, no coprocessor" '^(soundwire|amplifier|acp) ' + +quiet +pci 0000:00:1f.3 0x040380 8086 a0c8 snd_hda_intel +codec hdaudioC0D0 10ec0287 17aa22e6 +# A 36-byte header, then two endpoints of 16 bytes: a microphone array (2) and an I2S link (3). +mkdir -p "$W/sys/firmware/acpi/tables" +{ printf 'NHLT'; head -c 32 /dev/zero; printf '\002' + printf '\020\000\000\000\002'; head -c 11 /dev/zero + printf '\020\000\000\000\003'; head -c 11 /dev/zero; } > "$W/sys/firmware/acpi/tables/NHLT" +audio_now +aud "a controller with a DSP, by its class" '^controller 0000:00:1f\.3 class 040380 ' +aud "the NHLT's microphone array and I2S link" '^nhlt: 2 endpoints, pdm 1, ssp 1$' + +quiet +pci 0000:00:1f.3 0x040380 8086 a0c8 snd_hda_intel +codec hdaudioC0D2 80862812 80860101 +# The SoundWire controller under the HD Audio one, at _ADR 0x40000000, with a +# Realtek RT711 on link 0 and an RT1316 on link 1, beside a node that is not it. +acpi=sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:20 +put "$acpi/device:21/adr" 0x00000000 +put "$acpi/device:21/device:25/adr" 0x000030025d071101 +put "$acpi/device:22/adr" 0x40000000 +put "$acpi/device:22/device:23/adr" 0x000030025d071101 +put "$acpi/device:22/device:24/adr" 0x000131025d131601 +ln -s "$W/$acpi" "$W/sys/devices/pci0000:00/0000:00:1f.3/firmware_node" +audio_now +aud "a part on SoundWire link 0, by maker and part" '^soundwire 0000:00:1f\.3 link 0 part 025d:0711$' +aud "and one on link 1" '^soundwire 0000:00:1f\.3 link 1 part 025d:1316$' +[[ "$(grep -c '^soundwire ' "$A")" -eq 2 ]] && green "only under the controller at 0x40000000" || red "SoundWire lines: $(grep -c '^soundwire ' "$A")" + +quiet +pci 0000:04:00.5 0x048000 1022 15e2 +put sys/devices/pci0000:00/0000:04:00.5/revision 0x6f +pci 0000:04:00.6 0x040300 1022 15e3 snd_hda_intel +codec hdaudioC1D0 10ec0257 17aa3802 +mkdir -p "$W/sys/firmware/acpi/tables" +{ printf 'DSDT'; head -c 60 /dev/zero; printf 'AcpDmicConnected'; head -c 8 /dev/zero; } > "$W/sys/firmware/acpi/tables/DSDT" +audio_now +aud "AMD's coprocessor, its revision, and no driver" '^acp 0000:04:00\.5 class 048000 1022:15e2 rev 6f driver -$' +aud "the property its driver looks for" '^acpi names: AcpDmicConnected$' +aud "and the HD Audio beside it" '^controller 0000:04:00\.6 class 040300 1022:15e3 rev 20 driver snd_hda_intel$' + +quiet +pci 0000:00:1f.3 0x040300 8086 a0c8 snd_hda_intel +codec hdaudioC0D0 10ec0287 17aa22e6 +# Two Cirrus CS35L41 amplifiers ACPI describes as one device, which nothing +# drives, and another the firmware says is absent. +put sys/bus/acpi/devices/CSC3551:00/status 15 +mkdir -p "$W/sys/devices/platform/CSC3551:00" +ln -s "$W/sys/devices/platform/CSC3551:00" "$W/sys/bus/acpi/devices/CSC3551:00/physical_node" +put sys/bus/acpi/devices/CSC3551:01/status 0 +audio_now +aud "the amplifiers, and that nothing drives them" '^amplifier CSC3551:00 driver -$' +noaud "not those the firmware calls absent" '^amplifier CSC3551:01' + echo echo "${PASS} passed, ${FAIL} failed" [[ "$FAIL" -eq 0 ]] From 4252f1ab3488b2350e90203d6e741e3d84642123 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 21:34:13 -0700 Subject: [PATCH 11/23] The hardware report's VM test finds its audio section, and no NHLT where QEMU's firmware has none The report's own suite builds each kind of sound on a made-up machine; this is the section on a booted one: the medium's report in acceptance has an audio section, and with no sound device and no NHLT in QEMU's firmware it says 'nhlt: none'. --- tools/image/hwreport-test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh index dff943a0..cbe7b34e 100755 --- a/tools/image/hwreport-test.sh +++ b/tools/image/hwreport-test.sh @@ -92,6 +92,9 @@ want "$R1" '^sd[a-z] .*\(the root is here\).*usb-storage \(built in\)' "the disk want "$R1" '^== missing ==$' "what the image lacks has its place" want "$R1" '^== kernel log ==$' "the kernel log is there" want "$R1" 'Linux version .*hardened' "from its first line" +# QEMU here has no sound device and its firmware no NHLT: the section is there, and says so. +want "$R1" '^== audio ==$' "the sound has its place" +want "$R1" '^nhlt: none$' "and no DSP microphones, as QEMU's firmware describes none" deny "$R1" '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "no hardware address" deny "$R1" '[Ss]erial ?[Nn]umber[:=] ?[^< ]' "no serial number" deny "$R1" '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' "no UUID" From 4186c5af917995a6d0c23682bd1a5afd0430fe53 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:29:13 -0700 Subject: [PATCH 12/23] The hardware report says what a laptop has: its backlights, power supplies, sleep states and a camera's parts behind Intel's IPU The report had no place for the parts docs/design/laptop.md and camera.md drive or name as gaps, so a laptop's report could not say which backlight brightness.sh would choose, whether the kernel offers deep sleep, or whether its camera is a MIPI one the image cannot drive yet. A laptop section lists each backlight with its kind and range; each power supply, a battery's scope, status, charge and kind but never its serial number; /sys/power's state and mem_sleep; Intel's image processing units by PCI ID (IPU6 and IPU7 as 6.18 binds them, IPU8 as staging ipu7 names it) with their driver; and in ACPI the sensors ipu-bridge knows, the INT3472 power controller and the Visual Sensing Controller, skipping any the firmware calls absent. A USB camera, the lid and a Bluetooth controller already show under usb and input. tools/tests/hwreport.sh gains a fixture laptop: two backlights, a battery whose serial must not appear, mains, a mouse's battery with scope Device, sleep states, an IPU6 at 8086:a75d, and ACPI's OVTI01A0, INT3472 and INTC1009, with an OVTI02C1 whose _STA is 0. --- tools/install/kryptik-hwreport.sh | 52 +++++++++++++++++++++++++++++++ tools/tests/hwreport.sh | 46 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 7038970a..80893212 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -331,6 +331,57 @@ nhlt() { }' } +# What a laptop has that a desktop and a virtual machine lack, for the parts the image drives or +# names as a gap (docs/design/laptop.md, camera.md): each backlight and its kind, which +# brightness.sh chooses among; each power supply, a battery's charge and kind but never its +# serial number; the sleep states the kernel offers; and a camera's parts behind Intel's image +# processing unit and in ACPI: its sensors, their power controller and the Visual Sensing +# Controller between the two. A USB camera, the lid and a Bluetooth controller show under usb and +# input. +laptop() { + for b in "$SYS"/class/backlight/*; do + [ -e "$b" ] && printf 'backlight %s type %s max %s\n' "${b##*/}" "$(one "$b/type")" "$(one "$b/max_brightness")" + done + for p in "$SYS"/class/power_supply/*; do + [ -e "$p" ] || continue + t="$(one "$p/type")" + if [ "$t" = Battery ]; then + s="$(one "$p/scope")"; c="$(one "$p/capacity")"; k="$(one "$p/technology")"; st="$(one "$p/status")" + printf 'power_supply %s type Battery scope %s status %s capacity %s technology %s\n' "${p##*/}" \ + "${s:-System}" "${st:--}" "${c:--}" "${k:--}" + else + o="$(one "$p/online")" + printf 'power_supply %s type %s online %s\n' "${p##*/}" "${t:--}" "${o:--}" + fi + done + printf 'sleep states: %s\n' "$(one "$SYS/power/state")" + printf 'sleep mem: %s\n' "$(one "$SYS/power/mem_sleep")" + # Intel's image processing units by PCI ID: the IPU6 and IPU7 ones 6.18's drivers bind (ipu6.h, + # staging ipu7.h), and IPU8's, which staging ipu7 names but does not bind yet. + for d in "$SYS"/bus/pci/devices/*; do + [ "$(hex "$d/vendor")" = 8086 ] || continue + case "$(hex "$d/device")" in + 9a19|4e19|465d|462e|a75d|7d19) k=ipu6 ;; + 645d|b05d) k=ipu7 ;; + d719) k=ipu8 ;; + *) continue ;; + esac + drv="$(target "$d/driver")" + printf 'camera %s %s 8086:%s driver %s\n' "$k" "${d##*/}" "$(hex "$d/device")" "${drv:--}" + done + # Sensors by the IDs ipu-bridge knows, OmniVision's and Himax's whole ranges with them. + for a in "$SYS"/bus/acpi/devices/*; do + case "${a##*/}" in + OVTI*:*|HIMX*:*|INT0310:*|INT33BE:*|INT33F0:*|INT3474:*|INT3479:*|INT347A:*|INT347E:*|INT3537:*|INTC10C5:*|XMCC0003:*) w=sensor ;; + INT3472:*) w="sensor power" ;; + INTC1009:*|INTC1058:*|INTC1094:*|INTC10D0:*) w="visual sensing controller" ;; + *) continue ;; + esac + [ "$(one "$a/status")" != 0 ] || continue + printf 'camera acpi %s %s\n' "${a##*/}" "$w" + done +} + missing_firmware() { kmsg | sed -n \ -e 's/.*Direct firmware load for \(.*\) failed with error.*/\1/p' \ @@ -367,6 +418,7 @@ report() { sec storage; cat "$T/disks" sec display; display sec audio; audio + sec laptop; laptop sec network; network sec input; inputs sec modules; awk '{ print $1 }' "$R/proc/modules" 2>/dev/null | sort | tr '\n' ' ' | fold -s -w 78; echo diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index e782b48d..279e4d48 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -274,6 +274,52 @@ audio_now aud "the amplifiers, and that nothing drives them" '^amplifier CSC3551:00 driver -$' noaud "not those the firmware calls absent" '^amplifier CSC3551:01' +echo "-- what a laptop has that the others lack" +want "the laptop has its place" '^== laptop ==$' +L="$W/laptop.txt" +laptop_now() { + KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" > "$W/report-laptop.txt" 2>&1 + awk '$0 == "== laptop ==" { on = 1; next } on && /^== .* ==$/ { exit } on' "$W/report-laptop.txt" > "$L" +} +lap() { if grep -qE -- "$2" "$L"; then green "$1"; else red "$1: $(paste -sd'|' "$L")"; fi; } +nolap() { if grep -qE -- "$2" "$W/report-laptop.txt"; then red "$1"; else green "$1"; fi; } +put sys/class/backlight/intel_backlight/type raw +put sys/class/backlight/intel_backlight/max_brightness 19200 +put sys/class/backlight/acpi_video0/type firmware +put sys/class/backlight/acpi_video0/max_brightness 100 +put sys/class/power_supply/BAT0/type Battery +put sys/class/power_supply/BAT0/status Discharging +put sys/class/power_supply/BAT0/capacity 87 +put sys/class/power_supply/BAT0/technology Li-ion +put sys/class/power_supply/BAT0/serial_number BAT-SECRET-7 +put sys/class/power_supply/AC/type Mains +put sys/class/power_supply/AC/online 1 +# A mouse's battery, which is the device's and not the machine's. +put sys/class/power_supply/hidpp_battery_0/type Battery +put sys/class/power_supply/hidpp_battery_0/scope Device +put sys/class/power_supply/hidpp_battery_0/capacity 40 +put sys/power/state 'freeze mem disk' +put sys/power/mem_sleep 's2idle [deep]' +pci 0000:00:05.0 0x048000 8086 a75d +put sys/bus/acpi/devices/OVTI01A0:00/status 15 +put sys/bus/acpi/devices/INT3472:01/status 15 +put sys/bus/acpi/devices/INTC1009:00/status 15 +put sys/bus/acpi/devices/OVTI02C1:00/status 0 +laptop_now +lap "each backlight with its kind and range" '^backlight intel_backlight type raw max 19200$' +lap "the firmware's as well" '^backlight acpi_video0 type firmware max 100$' +lap "the battery's charge and kind" '^power_supply BAT0 type Battery scope System status Discharging capacity 87 technology Li-ion$' +lap "mains, and whether they are on" '^power_supply AC type Mains online 1$' +lap "a device's battery, named as the device's" '^power_supply hidpp_battery_0 type Battery scope Device ' +nolap "never a battery's serial number" 'BAT-SECRET-7' +lap "the sleep states the kernel offers" '^sleep states: freeze mem disk$' +lap "and which one mem is" '^sleep mem: s2idle \[deep\]$' +lap "an IPU6 by its PCI ID" '^camera ipu6 0000:00:05\.0 8086:a75d driver -$' +lap "its sensor, by its ACPI ID" '^camera acpi OVTI01A0:00 sensor$' +lap "the sensor's power controller" '^camera acpi INT3472:01 sensor power$' +lap "and the Visual Sensing Controller" '^camera acpi INTC1009:00 visual sensing controller$' +nolap "not a sensor the firmware calls absent" 'OVTI02C1' + echo echo "${PASS} passed, ${FAIL} failed" [[ "$FAIL" -eq 0 ]] From 03ec17c54403515c1b5927fbcd2e26c9c35c15a6 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:35:28 -0700 Subject: [PATCH 13/23] The hardware report's VM test finds its laptop section, with the kernel's sleep states QEMU has no backlight, battery or camera, so the section's place and its sleep line are what the medium's real sysfs can show; the fixture laptop in tools/tests/hwreport.sh covers the rest. --- tools/image/hwreport-test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh index cbe7b34e..10d04d97 100755 --- a/tools/image/hwreport-test.sh +++ b/tools/image/hwreport-test.sh @@ -95,6 +95,9 @@ want "$R1" 'Linux version .*hardened' "from its first li # QEMU here has no sound device and its firmware no NHLT: the section is there, and says so. want "$R1" '^== audio ==$' "the sound has its place" want "$R1" '^nhlt: none$' "and no DSP microphones, as QEMU's firmware describes none" +# QEMU has no backlight, battery or camera; the section is there with the kernel's sleep states. +want "$R1" '^== laptop ==$' "a laptop's parts have their place" +want "$R1" '^sleep states: ' "with the sleep states the kernel offers" deny "$R1" '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "no hardware address" deny "$R1" '[Ss]erial ?[Nn]umber[:=] ?[^< ]' "no serial number" deny "$R1" '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' "no UUID" From fd0e2646b9e007da2a71bf2ee04cfee02b02c1cc Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:51:13 -0700 Subject: [PATCH 14/23] A battery's line in the hardware report ends with its status, which may be two words So every field before it keeps its place for a reader that counts them; read by 89, a nit. --- tools/install/kryptik-hwreport.sh | 5 +++-- tools/tests/hwreport.sh | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 80893212..569d58cd 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -347,8 +347,9 @@ laptop() { t="$(one "$p/type")" if [ "$t" = Battery ]; then s="$(one "$p/scope")"; c="$(one "$p/capacity")"; k="$(one "$p/technology")"; st="$(one "$p/status")" - printf 'power_supply %s type Battery scope %s status %s capacity %s technology %s\n' "${p##*/}" \ - "${s:-System}" "${st:--}" "${c:--}" "${k:--}" + # The status last, as it may be two words ("Not charging"). + printf 'power_supply %s type Battery scope %s capacity %s technology %s status %s\n' "${p##*/}" \ + "${s:-System}" "${c:--}" "${k:--}" "${st:--}" else o="$(one "$p/online")" printf 'power_supply %s type %s online %s\n' "${p##*/}" "${t:--}" "${o:--}" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index 279e4d48..8da371f1 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -308,7 +308,7 @@ put sys/bus/acpi/devices/OVTI02C1:00/status 0 laptop_now lap "each backlight with its kind and range" '^backlight intel_backlight type raw max 19200$' lap "the firmware's as well" '^backlight acpi_video0 type firmware max 100$' -lap "the battery's charge and kind" '^power_supply BAT0 type Battery scope System status Discharging capacity 87 technology Li-ion$' +lap "the battery's charge and kind" '^power_supply BAT0 type Battery scope System capacity 87 technology Li-ion status Discharging$' lap "mains, and whether they are on" '^power_supply AC type Mains online 1$' lap "a device's battery, named as the device's" '^power_supply hidpp_battery_0 type Battery scope Device ' nolap "never a battery's serial number" 'BAT-SECRET-7' From e28f699a5dcb3989e07f0a98b79fc85335b72aae Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:07:59 -0700 Subject: [PATCH 15/23] The hardware report says what confines a device's DMA: the IOMMUs in use, their groups, and Thunderbolt's level The kernel turns the IOMMU on and strict from boot (hardening.fragment), but a machine whose firmware hides or disables it boots all the same, with every device's DMA reaching all of memory, and the report did not say which a machine was. The firmware section gains an iommu line, the IOMMUs in /sys/class/iommu and the groups counted by how they translate (DMA strict, DMA-FQ lazy, identity not at all), or none; and one line for each Thunderbolt domain, with its security level and whether the firmware asks the IOMMU to protect it. tools/tests/hwreport.sh gives its machine two IOMMUs, three groups and a domain; the VM test finds the line. --- tools/image/hwreport-test.sh | 1 + tools/install/kryptik-hwreport.sh | 11 +++++++++++ tools/tests/hwreport.sh | 13 +++++++++++++ 3 files changed, 25 insertions(+) diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh index 10d04d97..4b5d069d 100755 --- a/tools/image/hwreport-test.sh +++ b/tools/image/hwreport-test.sh @@ -86,6 +86,7 @@ want "$R1" '^booted: the usb medium$' "it says what boot want "$R1" '^sys_vendor: QEMU' "the machine, by its maker" want "$R1" '^secure boot: off$' "Secure Boot as the firmware has it" want "$R1" '^microcode: ' "the processor's microcode" +want "$R1" '^iommu: ' "what confines a device's DMA" want "$R1" ' class 0c0330 .* driver xhci_hcd' "a PCI device with the driver that took it" want "$R1" ' interface class 08/06/50 driver usb-storage$' "the stick's own interface, by its driver" want "$R1" '^sd[a-z] .*\(the root is here\).*usb-storage \(built in\)' "the disk the root is on, through built-in drivers" diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 569d58cd..8b402b71 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -90,6 +90,17 @@ firmware() { tpm=none for t in "$SYS"/class/tpm/tpm*; do [ -e "$t" ] && tpm="version $(one "$t/tpm_version_major")"; done printf 'tpm: %s\n' "$tpm" + # What confines a device's DMA: the IOMMUs in use, their groups by how they translate (DMA + # strict, DMA-FQ lazy, identity not at all), and what Thunderbolt lets a plugged device do. + units="" + for u in "$SYS"/class/iommu/*; do [ -e "$u" ] && units="${units:+$units }${u##*/}"; done + groups="$(for g in "$SYS"/kernel/iommu_groups/*/type; do one "$g"; done | sort | uniq -c \ + | awk '{ printf "%s%s %s", s, $1, $2; s = ", " }')" + printf 'iommu: %s\n' "${units:-none}${groups:+; groups: $groups}" + for d in "$SYS"/bus/thunderbolt/devices/domain*; do + [ -e "$d" ] && printf 'thunderbolt %s: security %s, iommu protection %s\n' "${d##*/}" \ + "$(one "$d/security")" "$(one "$d/iommu_dma_protection")" + done } cpu() { diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index 8da371f1..dfcf1248 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -274,6 +274,19 @@ audio_now aud "the amplifiers, and that nothing drives them" '^amplifier CSC3551:00 driver -$' noaud "not those the firmware calls absent" '^amplifier CSC3551:01' +echo "-- what confines a device's DMA" +put sys/class/iommu/dmar0/uevent '' +put sys/class/iommu/dmar1/uevent '' +put sys/kernel/iommu_groups/0/type DMA +put sys/kernel/iommu_groups/1/type DMA +put sys/kernel/iommu_groups/2/type identity +put sys/bus/thunderbolt/devices/domain0/security user +put sys/bus/thunderbolt/devices/domain0/iommu_dma_protection 1 +KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" > "$W/report-dma.txt" 2>&1 +dma() { if grep -qE -- "$2" "$W/report-dma.txt"; then green "$1"; else red "$1: $(grep -E '^(iommu|thunderbolt)' "$W/report-dma.txt" | paste -sd'|')"; fi; } +dma "the IOMMUs in use, and their groups by how they translate" '^iommu: dmar0 dmar1; groups: 2 DMA, 1 identity$' +dma "and what Thunderbolt lets a plugged device do" '^thunderbolt domain0: security user, iommu protection 1$' + echo "-- what a laptop has that the others lack" want "the laptop has its place" '^== laptop ==$' L="$W/laptop.txt" From 8eb95d8b376c398e27a035a24b741a10210a575d Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:14:31 -0700 Subject: [PATCH 16/23] The hardware report counts the sleeps since boot, and how long the last kept the hardware asleep An s2idle laptop whose SoC never reaches its deepest state drains its battery while it sleeps, and nothing in the report showed it. The laptop section gains a sleep stats line from /sys/power/suspend_stats: the sleeps that succeeded and failed, and last_hw_sleep and total_hw_sleep, the time the hardware spent in that state, which deep sleep does not count. Taken on the installed system after a sleep, it says whether that sleep got there. --- tools/install/kryptik-hwreport.sh | 6 ++++++ tools/tests/hwreport.sh | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 8b402b71..15bfac2b 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -368,6 +368,12 @@ laptop() { done printf 'sleep states: %s\n' "$(one "$SYS/power/state")" printf 'sleep mem: %s\n' "$(one "$SYS/power/mem_sleep")" + # The sleeps since boot, and how long the last kept the hardware in its deepest state: an + # s2idle laptop that never gets there drains its battery asleep. Deep sleep counts no time here. + st="$SYS/power/suspend_stats" + ok="$(one "$st/success")"; ko="$(one "$st/fail")"; last="$(one "$st/last_hw_sleep")"; all="$(one "$st/total_hw_sleep")" + printf 'sleep stats: %s successful, %s failed, hardware asleep %s us last, %s us in all\n' \ + "${ok:--}" "${ko:--}" "${last:--}" "${all:--}" # Intel's image processing units by PCI ID: the IPU6 and IPU7 ones 6.18's drivers bind (ipu6.h, # staging ipu7.h), and IPU8's, which staging ipu7 names but does not bind yet. for d in "$SYS"/bus/pci/devices/*; do diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index dfcf1248..ae27865b 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -313,6 +313,10 @@ put sys/class/power_supply/hidpp_battery_0/scope Device put sys/class/power_supply/hidpp_battery_0/capacity 40 put sys/power/state 'freeze mem disk' put sys/power/mem_sleep 's2idle [deep]' +put sys/power/suspend_stats/success 3 +put sys/power/suspend_stats/fail 1 +put sys/power/suspend_stats/last_hw_sleep 3480000000 +put sys/power/suspend_stats/total_hw_sleep 9000000000 pci 0000:00:05.0 0x048000 8086 a75d put sys/bus/acpi/devices/OVTI01A0:00/status 15 put sys/bus/acpi/devices/INT3472:01/status 15 @@ -327,6 +331,7 @@ lap "a device's battery, named as the device's" '^power_supply hidpp_battery_0 t nolap "never a battery's serial number" 'BAT-SECRET-7' lap "the sleep states the kernel offers" '^sleep states: freeze mem disk$' lap "and which one mem is" '^sleep mem: s2idle \[deep\]$' +lap "the sleeps, and the hardware's time asleep" '^sleep stats: 3 successful, 1 failed, hardware asleep 3480000000 us last, 9000000000 us in all$' lap "an IPU6 by its PCI ID" '^camera ipu6 0000:00:05\.0 8086:a75d driver -$' lap "its sensor, by its ACPI ID" '^camera acpi OVTI01A0:00 sensor$' lap "the sensor's power controller" '^camera acpi INT3472:01 sensor power$' From a8952cf209f5e4229d4976a3a8a3b5bded6f722a Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:17:03 -0700 Subject: [PATCH 17/23] The hardware report names the power controller driver that counts the hardware's time asleep A hardware time asleep of 0 says a sleep never reached the SoC's deepest state only where Intel's or AMD's power controller driver is bound to count it; elsewhere it says nothing. The laptop section's power controller line names intel_pmc_core or amd_pmc when bound, or none. --- tools/install/kryptik-hwreport.sh | 10 ++++++++++ tools/tests/hwreport.sh | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 15bfac2b..1ea9b038 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -374,6 +374,16 @@ laptop() { ok="$(one "$st/success")"; ko="$(one "$st/fail")"; last="$(one "$st/last_hw_sleep")"; all="$(one "$st/total_hw_sleep")" printf 'sleep stats: %s successful, %s failed, hardware asleep %s us last, %s us in all\n' \ "${ok:--}" "${ko:--}" "${last:--}" "${all:--}" + # Who counts that time: Intel's or AMD's power controller driver, bound to its device. + pmc="" + for d in "$SYS"/bus/platform/drivers/intel_pmc_core "$SYS"/bus/platform/drivers/amd_pmc; do + for l in "$d"/*; do + [ -L "$l" ] || continue + case "${l##*/}" in module) continue ;; esac + pmc="${pmc:+$pmc }${d##*/}"; break + done + done + printf 'power controller: %s\n' "${pmc:-none}" # Intel's image processing units by PCI ID: the IPU6 and IPU7 ones 6.18's drivers bind (ipu6.h, # staging ipu7.h), and IPU8's, which staging ipu7 names but does not bind yet. for d in "$SYS"/bus/pci/devices/*; do diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index ae27865b..d6675dc0 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -317,6 +317,9 @@ put sys/power/suspend_stats/success 3 put sys/power/suspend_stats/fail 1 put sys/power/suspend_stats/last_hw_sleep 3480000000 put sys/power/suspend_stats/total_hw_sleep 9000000000 +mkdir -p "$W/sys/bus/platform/drivers/intel_pmc_core" "$W/sys/devices/platform/intel_pmc_core.0" +ln -s "$W/sys/devices/platform/intel_pmc_core.0" "$W/sys/bus/platform/drivers/intel_pmc_core/intel_pmc_core.0" +mkdir -p "$W/sys/bus/platform/drivers/amd_pmc" pci 0000:00:05.0 0x048000 8086 a75d put sys/bus/acpi/devices/OVTI01A0:00/status 15 put sys/bus/acpi/devices/INT3472:01/status 15 @@ -332,6 +335,7 @@ nolap "never a battery's serial number" 'BAT-SECRET-7' lap "the sleep states the kernel offers" '^sleep states: freeze mem disk$' lap "and which one mem is" '^sleep mem: s2idle \[deep\]$' lap "the sleeps, and the hardware's time asleep" '^sleep stats: 3 successful, 1 failed, hardware asleep 3480000000 us last, 9000000000 us in all$' +lap "and the power controller driver that counts it" '^power controller: intel_pmc_core$' lap "an IPU6 by its PCI ID" '^camera ipu6 0000:00:05\.0 8086:a75d driver -$' lap "its sensor, by its ACPI ID" '^camera acpi OVTI01A0:00 sensor$' lap "the sensor's power controller" '^camera acpi INT3472:01 sensor power$' From d7f3f49a6a05b671c142f7264346195a45fbd653 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:19:09 -0700 Subject: [PATCH 18/23] A Thunderbolt domain that lacks a file prints - for it, as the report's other lines do --- tools/install/kryptik-hwreport.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 1ea9b038..58ec4e22 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -98,8 +98,9 @@ firmware() { | awk '{ printf "%s%s %s", s, $1, $2; s = ", " }')" printf 'iommu: %s\n' "${units:-none}${groups:+; groups: $groups}" for d in "$SYS"/bus/thunderbolt/devices/domain*; do - [ -e "$d" ] && printf 'thunderbolt %s: security %s, iommu protection %s\n' "${d##*/}" \ - "$(one "$d/security")" "$(one "$d/iommu_dma_protection")" + [ -e "$d" ] || continue + s="$(one "$d/security")"; p="$(one "$d/iommu_dma_protection")" + printf 'thunderbolt %s: security %s, iommu protection %s\n' "${d##*/}" "${s:--}" "${p:--}" done } From 06a61f9eddb09966b4fe0d96b532d0435ff6809f Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:03:36 -0700 Subject: [PATCH 19/23] The hardware report says whether the processor gives programs shadow stacks Where the kernel shows user_shstk, the CPU has shadow stacks and the kernel turns them on, and glibc then runs a program built for them with one, refusing it a library built without. The report listed user_shstk under "of note" only when present. A report from a machine without them said nothing either way, and a reader had to know the flag to see that the machine could not show that refusal. The cpu section gains "shadow stacks: yes|no". Checked: tools/tests/hwreport.sh wants "no" for its fixture, whose kernel shows no user_shstk, and "yes", with user_shstk in "of note", once the flag is added. Also one report run over a tree holding only a cpuinfo, with and without the flag. --- tools/install/kryptik-hwreport.sh | 3 +++ tools/tests/hwreport.sh | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 58ec4e22..edb9751a 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -104,6 +104,8 @@ firmware() { done } +# Shadow stacks: the kernel shows user_shstk where the CPU has them and it turns them on, and only +# there does glibc run a program built for them with one, refusing it a library built without. cpu() { awk -F': *' ' /^model name/ { name = $2 } @@ -125,6 +127,7 @@ cpu() { for (i in have) has[have[i]] = 1 for (i = 1; i in want; i++) if (want[i] in has) printf " %s", want[i] printf "\n" + printf "shadow stacks: %s\n", ("user_shstk" in has) ? "yes" : "no" }' "$R/proc/cpuinfo" 2>/dev/null printf 'present: %s, online: %s, smt: %s\n' "$(one "$SYS/devices/system/cpu/present")" \ "$(one "$SYS/devices/system/cpu/online")" "$(one "$SYS/devices/system/cpu/smt/control")" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index d6675dc0..d3d60546 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -132,6 +132,7 @@ want "the processor" '^vendor, family/model/stepping: Gen want "its microcode, and what loaded it" '^microcode: 0xa4$' want "the loader's own line" '^microcode: Current revision: 0x000000a4$' want "the features that matter here" '^of note: nx smep smap umip ibt rdrand aes$' +want "no shadow stacks, as its kernel shows none" '^shadow stacks: no$' want "a mitigation as the kernel states it" '^meltdown: Not affected$' want "the memory" '^memory: 15902 MiB$' @@ -287,6 +288,15 @@ dma() { if grep -qE -- "$2" "$W/report-dma.txt"; then green "$1"; else red "$1: dma "the IOMMUs in use, and their groups by how they translate" '^iommu: dmar0 dmar1; groups: 2 DMA, 1 identity$' dma "and what Thunderbolt lets a plugged device do" '^thunderbolt domain0: security user, iommu protection 1$' +echo "-- shadow stacks" +sed -i 's/^flags\t\t: .*/& user_shstk/' "$W/proc/cpuinfo" +KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" > "$W/report-shstk.txt" 2>&1 +if grep -qx 'shadow stacks: yes' "$W/report-shstk.txt" && grep -qx 'of note: nx smep smap umip ibt user_shstk rdrand aes' "$W/report-shstk.txt"; then + green "a CPU whose kernel shows user_shstk has them" +else + red "with user_shstk: $(grep -E '^(of note|shadow stacks)' "$W/report-shstk.txt" | paste -sd'|')" +fi + echo "-- what a laptop has that the others lack" want "the laptop has its place" '^== laptop ==$' L="$W/laptop.txt" From 79c156bc1c3de3c1b8000c0b2cb61630eb2842ce Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:48:20 -0700 Subject: [PATCH 20/23] The hardware report lists SoundWire parts behind AMD's audio coprocessor too The report's soundwire() looked for a SoundWire controller only under an HD Audio controller, at Intel's _ADR 0x40000000. Some AMD laptops, from the coprocessor's 6.3 generation on, put their codecs on SoundWire behind the coprocessor. 6.18's sound/soc/amd/ps driver finds that controller at the coprocessor's ACPI child with _ADR 5 (ACP63_SDW_ADDR, for revisions 6.3 to 7.2). A report from such a laptop named the coprocessor and no parts, so it did not show why the laptop was silent. soundwire() takes the controller's address: 0x40000000 under an HD Audio controller, 5 under the coprocessor. The parts print as before, under the coprocessor's PCI address. Checked: tools/tests/hwreport.sh gains a coprocessor at revision 70, with the microphones' node at _ADR 2 and a controller at 5 holding a Realtek RT722 on link 0. It wants that part alone. The Intel case still wants its two parts under 0x40000000 and none elsewhere. Also one report run over a small tree holding both kinds. The full fixture suite runs in CI. --- tools/install/kryptik-hwreport.sh | 15 ++++++++------- tools/tests/hwreport.sh | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index edb9751a..22cc76c7 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -269,9 +269,9 @@ inputs() { # The machine's sound, sorted as docs/design/laptop-audio.md sorts laptops: # the audio controllers on PCI and what took them, AMD's coprocessor among # them, the HD Audio codecs, the endpoints the NHLT table describes for -# Intel's DSP, the SoundWire parts ACPI describes, the speaker amplifiers ACPI -# names, and the names AMD's drivers look for to find microphones. Model and -# part IDs only, which no machine has alone. +# Intel's DSP, the SoundWire parts ACPI describes under either, the speaker +# amplifiers ACPI names, and the names AMD's drivers look for to find +# microphones. Model and part IDs only, which no machine has alone. audio() { for d in "$SYS"/bus/pci/devices/*; do c="$(hex "$d/class")"; v="$(hex "$d/vendor")" @@ -283,7 +283,7 @@ audio() { drv="$(target "$d/driver")" printf '%s %s class %s %s:%s rev %s driver %s\n' "$what" "${d##*/}" "$c" "$v" "$(hex "$d/device")" \ "$(hex "$d/revision")" "${drv:--}" - [ "$what" = acp ] || soundwire "$d" + if [ "$what" = acp ]; then soundwire "$d" 0x00000005; else soundwire "$d" 0x40000000; fi done for c in "$SYS"/bus/hdaudio/devices/*; do [ -r "$c/vendor_id" ] && printf 'codec %s sub %s\n' "$(hex "$c/vendor_id")" "$(hex "$c/subsystem_id")" @@ -309,13 +309,14 @@ audio() { [ -z "$names" ] || echo "acpi names:$names" } -# soundwire DEVICE: the SoundWire controller ACPI describes under an HD Audio -# controller, which the kernel finds at _ADR 0x40000000, and each part on its +# soundwire DEVICE ADR: the SoundWire controller ACPI describes under a PCI +# device at ADR, which the kernel finds there: Intel's at 0x40000000 under its +# HD Audio controller, AMD's at 5 under its coprocessor. Then each part on its # links: MIPI's address holds the link in bits 51-48, the maker in 39-24 and # the part in 23-8. soundwire() { for s in "$1"/firmware_node/*/; do - [ "$(one "${s}adr")" = 0x40000000 ] || continue + [ "$(one "${s}adr")" = "$2" ] || continue found="" for p in "$s"*/; do x="$(one "${p}adr")" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index d3d60546..dc3d4914 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -262,6 +262,20 @@ aud "AMD's coprocessor, its revision, and no driver" '^acp 0000:04:00\.5 class 0 aud "the property its driver looks for" '^acpi names: AcpDmicConnected$' aud "and the HD Audio beside it" '^controller 0000:04:00\.6 class 040300 1022:15e3 rev 20 driver snd_hda_intel$' +quiet +pci 0000:04:00.5 0x048000 1022 15e2 +put sys/devices/pci0000:00/0000:04:00.5/revision 0x70 +# AMD's SoundWire controller under its coprocessor, at _ADR 5, with a Realtek +# RT722 on link 0, beside the microphones' node at _ADR 2. +acpi=sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:30 +put "$acpi/device:31/adr" 0x00000002 +put "$acpi/device:32/adr" 0x00000005 +put "$acpi/device:32/device:33/adr" 0x000030025d072201 +ln -s "$W/$acpi" "$W/sys/devices/pci0000:00/0000:04:00.5/firmware_node" +audio_now +aud "a part on SoundWire behind AMD's coprocessor" '^soundwire 0000:04:00\.5 link 0 part 025d:0722$' +[[ "$(grep -c '^soundwire ' "$A")" -eq 1 ]] && green "only under the controller at 5" || red "SoundWire lines: $(grep -c '^soundwire ' "$A")" + quiet pci 0000:00:1f.3 0x040300 8086 a0c8 snd_hda_intel codec hdaudioC0D0 10ec0287 17aa22e6 From fd0c71bad832a632398d235c13f25d572a48e4fc Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:59:11 -0700 Subject: [PATCH 21/23] The hardware report names each watchdog, whether it is fed, and whether it reset the last boot On an AMD laptop that sleeps to idle, the feeder now leaves AMD's TCO timer unfed, as the timer would count through the sleep. A report could not show that, nor that a watchdog had reset the boot before, which is what a laptop that rebooted asleep would show. firmware() gains a line for each watchdog under /sys/class/watchdog: its identity, its timeout, its state (active once something feeds it), and ", reset the last boot" where bootstatus has WDIOF_CARDRESET (32), which sp5100_tco sets when its timer fired. Checked against 6.18's watchdog_dev.c for the formats (bootstatus and timeout decimal, state active or inactive). tools/tests/hwreport.sh wants an SP5100 timer that is inactive and reset the last boot, and a software watchdog that is active. Also one report run over a tree holding only the two. --- tools/install/kryptik-hwreport.sh | 10 ++++++++++ tools/tests/hwreport.sh | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index 22cc76c7..a1142754 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -102,6 +102,16 @@ firmware() { s="$(one "$d/security")"; p="$(one "$d/iommu_dma_protection")" printf 'thunderbolt %s: security %s, iommu protection %s\n' "${d##*/}" "${s:--}" "${p:--}" done + # Each watchdog: whether it is fed, and whether it reset the machine the time before + # (WDIOF_CARDRESET, 32, in bootstatus), as one that rebooted a laptop asleep would have. + for w in "$SYS"/class/watchdog/watchdog*; do + [ -e "$w" ] || continue + b="$(one "$w/bootstatus")" + case "$b" in ''|*[!0-9]*) b=0 ;; esac + r=""; [ $(( b & 32 )) -eq 0 ] || r=", reset the last boot" + printf 'watchdog %s: %s, timeout %s s, %s%s\n' "${w##*/}" "$(one "$w/identity")" "$(one "$w/timeout")" \ + "$(one "$w/state")" "$r" + done } # Shadow stacks: the kernel shows user_shstk where the CPU has them and it turns them on, and only diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index dc3d4914..741ca86b 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -289,6 +289,20 @@ audio_now aud "the amplifiers, and that nothing drives them" '^amplifier CSC3551:00 driver -$' noaud "not those the firmware calls absent" '^amplifier CSC3551:01' +echo "-- the watchdogs" +put sys/class/watchdog/watchdog0/identity 'SP5100 TCO timer' +put sys/class/watchdog/watchdog0/timeout 60 +put sys/class/watchdog/watchdog0/state inactive +put sys/class/watchdog/watchdog0/bootstatus 32 +put sys/class/watchdog/watchdog1/identity 'Software Watchdog' +put sys/class/watchdog/watchdog1/timeout 60 +put sys/class/watchdog/watchdog1/state active +put sys/class/watchdog/watchdog1/bootstatus 0 +KRYPTIK_HWREPORT_ROOT="$W" sh "$TOOL" > "$W/report-wd.txt" 2>&1 +wd() { if grep -qxF -- "$2" "$W/report-wd.txt"; then green "$1"; else red "$1: $(grep '^watchdog' "$W/report-wd.txt" | paste -sd'|')"; fi; } +wd "a watchdog nothing feeds, and that it reset the last boot" 'watchdog watchdog0: SP5100 TCO timer, timeout 60 s, inactive, reset the last boot' +wd "and one that is fed" 'watchdog watchdog1: Software Watchdog, timeout 60 s, active' + echo "-- what confines a device's DMA" put sys/class/iommu/dmar0/uevent '' put sys/class/iommu/dmar1/uevent '' From 4bf6c78ac9c46553bfa63acea939c066c167d3b8 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:08:59 -0700 Subject: [PATCH 22/23] The hardware report gives each graphics controller's power state No driver here takes NVIDIA's graphics. On a laptop with an NVIDIA chip beside Intel's or AMD's, that chip stays in D0, drawing power, unless runtime PM is allowed it and the port above it then sleeps. The PCI core forbids that by default for a device nothing drives. The report said nothing of it, so October's reports could not tell what that costs a battery. The display section gains a gpu line per PCI display controller: its address, ids, driver, power state, and runtime PM's status and control. These are values the kernel keeps, so reading them wakes nothing. Checked: the report run on a tree of two GPUs, Intel's driven and let sleep and NVIDIA's with no driver, prints both lines as the suite's two new checks want them. sh -n and bash -n pass. CI runs the suite. --- tools/install/kryptik-hwreport.sh | 10 ++++++++++ tools/tests/hwreport.sh | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index a1142754..ead02157 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -247,6 +247,16 @@ display() { for f in "$SYS"/class/graphics/fb[0-9]*; do [ -e "$f" ] && printf '%s %s %s\n' "${f##*/}" "$(one "$f/name")" "$(one "$f/virtual_size")" done + # Each graphics controller's power state. One nothing drives, as NVIDIA's beside the built-in + # one, stays in D0 and draws power unless runtime PM is allowed it (control auto) and the port + # above it then sleeps. + for d in "$SYS"/bus/pci/devices/*; do + case "$(one "$d/class")" in 0x03*) ;; *) continue ;; esac + drv="$(target "$d/driver")"; ps="$(one "$d/power_state")" + rs="$(one "$d/power/runtime_status")"; pc="$(one "$d/power/control")" + printf 'gpu %s %s:%s driver %s power %s runtime %s control %s\n' "${d##*/}" "$(hex "$d/vendor")" \ + "$(hex "$d/device")" "${drv:--}" "${ps:--}" "${rs:--}" "${pc:--}" + done } network() { diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index 741ca86b..24a9eee1 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -83,6 +83,17 @@ pci 0000:00:14.0 0x0c0330 8086 a0ed xhci_hcd pci 0000:00:14.3 0x028000 8086 a0f0 pci 0000:00:17.0 0x010601 8086 a0d3 ahci pci 0000:00:1f.6 0x020000 8086 15fb e1000e +# Two GPUs: Intel's built-in one, driven and let sleep, and NVIDIA's beside it, which nothing +# drives and which stays in D0. +driver pci i915 module +pci 0000:00:02.0 0x030000 8086 9a49 i915 +pci 0000:01:00.0 0x030200 10de 25a2 +put sys/devices/pci0000:00/0000:00:02.0/power_state D0 +put sys/devices/pci0000:00/0000:00:02.0/power/runtime_status active +put sys/devices/pci0000:00/0000:00:02.0/power/control auto +put sys/devices/pci0000:00/0000:01:00.0/power_state D0 +put sys/devices/pci0000:00/0000:01:00.0/power/runtime_status active +put sys/devices/pci0000:00/0000:01:00.0/power/control on disk sda sys/devices/pci0000:00/0000:00:17.0/ata1 1000215216 0 'Samsung SSD 860 ' stick=sys/devices/pci0000:00/0000:00:14.0/usb1/1-2 @@ -167,6 +178,8 @@ c1="$(LC_ALL=C grep -c $'\302[\200-\237]' "$OUT")" [[ "$ctl" -eq 0 && "$c1" -eq 0 ]] && green "no control character from a device's strings" \ || red "control characters in the report: ${ctl} C0 or DEL, ${c1} line(s) with a C1" want "the panel by its connector, state and mode" 'card0-eDP-1 connected 2256x1504' +want "a GPU, its driver and its power" '^gpu 0000:00:02\.0 8086:9a49 driver i915 power D0 runtime active control auto$' +want "a GPU nothing drives, left in D0" '^gpu 0000:01:00\.0 10de:25a2 driver - power D0 runtime active control on$' want "the log line kept, the number gone" 'usb 1-2: SerialNumber: $' deny "no UUID" '0a1b2c3d-1111|4c4c4544-0042' want "the filesystem line kept" 'mounted filesystem ro' From a40c7121e559ac85903397936224d93703041871 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:37:57 -0700 Subject: [PATCH 23/23] The hardware report gives what each battery draws The laptop section gave a battery's charge and status but not its power, so a report could not show what a laptop costs to run idle, or what an NVIDIA chip left in D0 adds. Each battery now gets a line of its own with its power in milliwatts. That is power_now where the battery reports it, otherwise current_now times voltage_now, which are the two ways ACPI batteries report it. A value that is not a whole number gives no line. The battery's own line keeps its format, and check-hardware.sh reads only that one. Checked: the report run on a tree of two batteries, one with power_now and one with current and voltage, and a device's battery with a malformed value, prints 7213 mW and 17100 mW and nothing for the third, as the suite's three new checks want. sh -n and bash -n pass. --- tools/install/kryptik-hwreport.sh | 6 ++++++ tools/tests/hwreport.sh | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index ead02157..b748f022 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -32,6 +32,7 @@ esac [ -n "$R" ] || [ "$(id -u)" = 0 ] || die "must run as root: the kernel log is root's to read" one() { [ -r "$1" ] && head -n 1 "$1" 2>/dev/null; } # a file's first line +int() { v="$(one "$1")"; case "$v" in ''|-|*[!0-9-]*|?*-*) ;; *) printf '%s' "$v" ;; esac; } # a whole number or nothing hex() { v="$(one "$1")"; v="${v#0x}"; printf '%s' "${v:--}"; } # the same, without 0x target() { [ -e "$1" ] && basename "$(readlink -f "$1")"; } # what a link names kmsg() { if [ -n "$R" ]; then cat "$R/dmesg" 2>/dev/null; else dmesg 2>/dev/null; fi; } @@ -386,6 +387,11 @@ laptop() { # The status last, as it may be two words ("Not charging"). printf 'power_supply %s type Battery scope %s capacity %s technology %s status %s\n' "${p##*/}" \ "${s:-System}" "${c:--}" "${k:--}" "${st:--}" + # What it gives or takes now: power_now where it reports that, otherwise current_now + # times voltage_now, the two ways ACPI batteries report it. Microwatts, then milliwatts. + pw="$(int "$p/power_now")"; i="$(int "$p/current_now")"; u="$(int "$p/voltage_now")" + [ -n "$pw" ] || { [ -n "$i" ] && [ -n "$u" ] && pw=$(( i * u / 1000000 )); } + [ -z "$pw" ] || printf 'battery %s power %s mW\n' "${p##*/}" "$(( pw / 1000 ))" else o="$(one "$p/online")" printf 'power_supply %s type %s online %s\n' "${p##*/}" "${t:--}" "${o:--}" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index 24a9eee1..4d56239e 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -356,6 +356,12 @@ put sys/class/power_supply/BAT0/status Discharging put sys/class/power_supply/BAT0/capacity 87 put sys/class/power_supply/BAT0/technology Li-ion put sys/class/power_supply/BAT0/serial_number BAT-SECRET-7 +put sys/class/power_supply/BAT0/power_now 7213000 +# A second battery that reports current and voltage instead of power. +put sys/class/power_supply/BAT1/type Battery +put sys/class/power_supply/BAT1/status Discharging +put sys/class/power_supply/BAT1/current_now 1500000 +put sys/class/power_supply/BAT1/voltage_now 11400000 put sys/class/power_supply/AC/type Mains put sys/class/power_supply/AC/online 1 # A mouse's battery, which is the device's and not the machine's. @@ -381,6 +387,9 @@ lap "each backlight with its kind and range" '^backlight intel_backlight type ra lap "the firmware's as well" '^backlight acpi_video0 type firmware max 100$' lap "the battery's charge and kind" '^power_supply BAT0 type Battery scope System capacity 87 technology Li-ion status Discharging$' lap "mains, and whether they are on" '^power_supply AC type Mains online 1$' +lap "what the battery draws, by its power" '^battery BAT0 power 7213 mW$' +lap "or by its current and voltage" '^battery BAT1 power 17100 mW$' +nolap "no draw for a battery that reports none" '^battery hidpp_battery_0 ' lap "a device's battery, named as the device's" '^power_supply hidpp_battery_0 type Battery scope Device ' nolap "never a battery's serial number" 'BAT-SECRET-7' lap "the sleep states the kernel offers" '^sleep states: freeze mem disk$'