From 83a8acf5de3ffe54b0d15adec64a4d9cef1f8aa6 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 01:38:22 -0700 Subject: [PATCH 01/54] 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/54] 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/54] 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/54] 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/54] 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 ca6a2d77cba005962ec87fb650f393c8c29c8b69 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 02:21:57 -0700 Subject: [PATCH 06/54] check-hardware.sh reads a hardware report for the listing it carries: reported when it is whole, names its machine and holds no address, UUID or serial number; certified when it also comes from an installed release with Secure Boot on, the state partition in use, a display, a network path and nothing missing --- tools/check-hardware.sh | 166 +++++++++++++++++++++++++++++ tools/tests/check-hardware.sh | 193 ++++++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100755 tools/check-hardware.sh create mode 100755 tools/tests/check-hardware.sh diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh new file mode 100755 index 00000000..c65a68e9 --- /dev/null +++ b/tools/check-hardware.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# What a hardware report (kryptik-hwreport) shows, and the listing it carries +# in docs/hardware/list.tsv (docs/hardware.md): `reported` when it is whole, +# names its machine and holds nothing that is one machine's alone; `certified` +# when it also comes from an installed release with Secure Boot on, a display, +# a network path and nothing missing. +# +# tools/check-hardware.sh REPORT... what each report shows and carries +# tools/check-hardware.sh --list every row of the list against its +# report, and every report against the list +# +# Exit 1 when a report carries no listing, or a row claims more than its +# report shows. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# The fixture suite names a list of its own. +DIR="${KRYPTIK_HARDWARE_DIR:-${ROOT}/docs/hardware}" + +section() { awk -v s="== $1 ==" '$0 == s { on = 1; next } on && /^== .* ==$/ { exit } on' "$2"; } # section NAME REPORT +field() { sed -n "s/^$1: //p" | head -1; } # field KEY, of the section on stdin + +# examine REPORT: MACHINE, RELEASE and FACTS, with NOT_REPORTED and +# NOT_CERTIFIED, each empty or one reason per line. +examine() { + local r="$1" s what lacking + MACHINE=""; RELEASE=""; FACTS=""; NOT_REPORTED=""; NOT_CERTIFIED="" + unfit() { NOT_REPORTED+="$*"$'\n'; } + lacks() { NOT_CERTIFIED+="$*"$'\n'; } + fact() { FACTS+="$(printf ' %-10s %s' "$1" "$2")"$'\n'; } + + if [[ "$(head -1 "$r")" != "kryptik-hwreport 1" ]]; then + unfit "not a report this tool reads: it does not open with 'kryptik-hwreport 1'" + return + fi + for s in system machine firmware cpu missing pci storage display network "kernel log"; do + grep -qxF "== ${s} ==" "$r" || unfit "cut short: no '${s}' section" + done + + local vendor product model bios + vendor="$(section machine "$r" | field sys_vendor)"; product="$(section machine "$r" | field product_name)" + model="$(section machine "$r" | field product_version)" + bios="$(section machine "$r" | field bios_version) $(section machine "$r" | field bios_date)" + [[ -n "$vendor" && -n "$product" ]] || unfit "does not name its machine (sys_vendor and product_name)" + MACHINE="${vendor} ${product}${model:+ (${model})}" + fact machine "${MACHINE}, firmware ${bios}" + + local kernel booted state + RELEASE="$(section system "$r" | field kryptik)"; RELEASE="${RELEASE%% (*}" + kernel="$(section system "$r" | field kernel)" + booted="$(section system "$r" | field booted)"; state="$(section system "$r" | field state)" + [[ -n "$RELEASE" && -n "$kernel" ]] || unfit "does not name its release and kernel" + fact release "${RELEASE}, kernel ${kernel}" + fact booted "${booted}; state ${state:-unknown}" + [[ "$booted" =~ ^slot\ [ab],\ installed$ ]] || lacks "not taken on an installed system (booted: ${booted:-unknown})" + [[ "$state" == persistent ]] || lacks "the state partition is not in use (state: ${state:-unknown})" + [[ "$RELEASE" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || lacks "${RELEASE:-no version} is not a release (a dated build, or none)" + + local uefi sb lock + uefi="$(section firmware "$r" | field uefi)"; sb="$(section firmware "$r" | field 'secure boot')" + lock="$(section firmware "$r" | field lockdown)" + fact firmware "UEFI ${uefi:-unknown}; Secure Boot ${sb:-unknown}; lockdown ${lock:-unknown}" + [[ "$uefi" == yes* ]] || lacks "not booted by UEFI firmware" + [[ "$sb" == on ]] || lacks "Secure Boot is ${sb:-unknown}" + [[ "$lock" == confidentiality ]] || lacks "lockdown is ${lock:-unknown}, not confidentiality" + fact microcode "$(section cpu "$r" | field microcode)" + + # The three lists under `missing`, each " (none)" or its entries. + lacking="$(section missing "$r" | awk ' + /^firmware the kernel asked for/ { what = "firmware the kernel did not find:"; next } + /^devices with no driver/ { what = "no driver for"; next } + /^disks behind a driver/ { what = "a disk behind a module:"; next } + /^ / && $0 != " (none)" { sub(/^ /, ""); print what, $0 }')" + if [[ -z "$lacking" ]]; then fact missing nothing + else + fact missing "$(grep -c . <<<"$lacking") line(s) for firmware.list or boot.fragment" + while IFS= read -r what; do lacks "$what"; done <<<"$lacking" + fi + + local cards outputs + cards="$(section display "$r" | sed -n 's/^card[0-9]* driver //p' | sort -u | paste -sd' ' -)" + outputs="$(section display "$r" | sed -n 's/^card[0-9]*-\([^ ]*\) connected *\(.*\)/\1 \2/p' | paste -sd';' -)" + fact display "${cards:-no DRM device}${outputs:+: ${outputs//;/; }}" + [[ -n "$cards" && -n "$outputs" ]] || lacks "no display the compositor can use: no DRM device with a connected output" + + # A path out: the default route leaves by one of the machine's own + # interfaces, and that one has a carrier. + local via link + via="$(section network "$r" | sed -n 's/^ default route by //p' | head -1)" + link="$(section network "$r" | sed -n '/^in the net zone:$/,$p' \ + | awk -v via="$via" '$1 == via && ($2 == "wired" || $2 == "wireless") && /LOWER_UP/ { print $2, $1 }' | head -1)" + fact network "${link:-no default route by an interface with a carrier}" + [[ -n "$link" ]] || lacks "no network path: no default route by a wired or wireless interface with a carrier in the net zone" + + # By line number, never by what the line holds. + what="$(grep -nE '(^|[^0-9A-Fa-f])([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}($|[^0-9A-Fa-f])|[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}|[Ss]erial ?([Nn]umber|[Nn]o)[.:= ]+[^< ]' "$r" | cut -d: -f1 | tr '\n' ' ')" + [[ -z "$what" ]] || unfit "holds a hardware address, a UUID or a serial number: line(s) ${what% }" +} + +# carries: the level the examined report carries, or nothing. +carries() { + if [[ -n "$NOT_REPORTED" ]]; then return + elif [[ -z "$NOT_CERTIFIED" ]]; then echo certified + else echo reported; fi +} + +show() { # show REPORT: its facts, and why it falls short; 1 when it carries nothing + examine "$1" + echo "${1##*/}" + printf '%s' "$FACTS" + local level; level="$(carries)" + if [[ -z "$level" ]]; then + printf ' %-10s %s\n' carries "no listing:" + sed '/^$/d; s/^/ /' <<<"$NOT_REPORTED" + return 1 + fi + if [[ "$level" == certified ]]; then printf ' %-10s %s\n' carries certified + else + printf ' %-10s %s\n' carries "reported; short of certified:" + sed '/^$/d; s/^/ /' <<<"$NOT_CERTIFIED" + fi +} + +list() { + local tsv="${DIR}/list.tsv" bad=0 n=0 report level by day have f + [[ -f "$tsv" ]] || { echo "no list at ${tsv}"; return 1; } + declare -A seen=() + fail() { echo " FAIL $*"; bad=$((bad + 1)); } + while read -r report level by day _; do + [[ -z "$report" || "$report" == \#* ]] && continue + n=$((n + 1)) + [[ -z "${seen[$report]:-}" ]] || { fail "${report}: listed twice"; continue; } + seen[$report]=1 + [[ "$report" =~ ^[a-z0-9][a-z0-9.-]*\.txt$ ]] || { fail "${report}: a report's name is lower-case letters, digits, dots and dashes, ending .txt"; continue; } + [[ -f "${DIR}/${report}" ]] || { fail "${report}: no such report beside the list"; continue; } + [[ "$level" == reported || "$level" == certified ]] || { fail "${report}: the level is reported or certified, not '${level}'"; continue; } + [[ -n "$by" && "$day" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { fail "${report}: a row names who vouches and the day (YYYY-MM-DD)"; continue; } + examine "${DIR}/${report}" + have="$(carries)" + if [[ -z "$have" ]]; then + fail "${report}: carries no listing:"; sed '/^$/d; s/^/ /' <<<"$NOT_REPORTED" + elif [[ "$level" == certified && "$have" != certified ]]; then + fail "${report}: listed certified, and its report is short of that:"; sed '/^$/d; s/^/ /' <<<"$NOT_CERTIFIED" + else + printf ' ok %-10s %s, %s (%s, %s)\n' "$level" "$MACHINE" "$RELEASE" "$by" "$day" + fi + done < "$tsv" + for f in "$DIR"/*.txt; do + [[ -f "$f" ]] || continue + [[ -n "${seen[${f##*/}]:-}" ]] || fail "${f##*/}: a report with no row in the list" + done + [[ "$bad" -eq 0 ]] || { echo "FAIL: ${bad} row(s) or report(s) the list cannot carry"; return 1; } + echo "ok: ${n} machine(s) listed, each row carried by its report" +} + +case "${1:-}" in + -h|--help|"") sed -n '2,13p' "${BASH_SOURCE[0]}"; [[ -n "${1:-}" ]] ;; + --list) [[ "$#" -eq 1 ]] || { echo "--list takes no report" >&2; exit 2; }; list ;; + -*) echo "unknown argument: $1" >&2; exit 2 ;; + *) + rc=0 + for report in "$@"; do + [[ -f "$report" ]] || { echo "no such report: ${report}" >&2; rc=1; continue; } + show "$report" || rc=1 + done + exit "$rc" ;; +esac diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh new file mode 100755 index 00000000..b2706143 --- /dev/null +++ b/tools/tests/check-hardware.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# Tests for tools/check-hardware.sh: what a report must show to carry a +# listing, each reason it falls short, and the list held to its reports. +# Offline; the reports are written here in kryptik-hwreport's form. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +TOOL="${ROOT}/tools/check-hardware.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 + +# An installed release on a laptop, with nothing wanting. +cat > "$W/good.txt" <<'EOF' +kryptik-hwreport 1 + +== system == +kryptik: 1.0.0 (build 91c3238aabbccddeeff00112233445566778899a) +kernel: 6.18.53-hardened1 +booted: slot a, installed +state: persistent +taken: 2026-10-14T09:12Z + +== machine == +sys_vendor: LENOVO +product_name: 20XWCTO1WW +product_version: ThinkPad X1 Carbon Gen 9 +bios_version: N32ET75W (1.51 ) +bios_date: 12/02/2021 +chassis_type: 10 + +== firmware == +uefi: yes, 64-bit +secure boot: on +setup mode: 0 +lockdown: confidentiality +tpm: version 2 + +== cpu == +model: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz +vendor, family/model/stepping: GenuineIntel, 6/140/1 +microcode: 0xa4 +threads running: 4 +present: 0-7, online: 0-3, smt: off +microcode: Current revision: 0x000000a4 +memory: 15902 MiB + +== missing == +firmware the kernel asked for and did not find, each a line for build/config/firmware.list: + (none) +devices with no driver, each wanting its driver's line in build/config/kernel/boot.fragment: + (none) +disks behind a driver that is a module, which boot.fragment must build in before a root can sit there: + (none) + +== pci == +0000:00:02.0 class 030000 8086:9a49 sub 17aa:22b8 rev 01 driver i915 | VGA compatible controller | Intel Corporation TigerLake-LP GT2 [Iris Xe Graphics] +0000:00:14.3 class 028000 8086:a0f0 sub 8086:0074 rev 20 driver iwlwifi | Network controller | Intel Corporation Wi-Fi 6 AX201 + +== usb == + +== no driver == + +== storage == +nvme0n1 476.9 GiB (the root is here) | SAMSUNG MZVLB512HBJQ-000L7 | nvme (built in) + +== display == +card0 driver i915 +card0-eDP-1 connected 1920x1200 +card0-DP-1 disconnected + +== network == +rfkill0 wlan phy0 soft 0 hard 0 +in the net zone: + wlan0 wireless + kzbr0 virtual + default route by wlan0 + +== input == +"AT Translated Set 2 keyboard" | kbd event0 + +== modules == +i915 iwlmvm iwlwifi + +== firmware messages == + +== kernel log == +[ 0.000000] Linux version 6.18.53-hardened1 +[ 1.400100] usb 1-2: SerialNumber: +[ 3.100000] wlan0: authenticate with xx:xx:xx:xx:xx:xx +EOF + +# see FILE: the tool's words on it in OUT, its status in RC. +see() { OUT="$(bash "$TOOL" "$1" 2>&1)"; RC=$?; } +# vary NAME SED-SCRIPT: the good report, changed, as $W/NAME.txt. +vary() { sed "$2" "$W/good.txt" > "$W/$1.txt"; } +# short NAME SED-SCRIPT REASON: that change leaves a reported listing, for REASON. +short() { + vary "$1" "$2"; see "$W/$1.txt" + if [[ "$RC" -eq 0 ]] && grep -q '^ carries reported; short of certified:$' <<<"$OUT" && grep -qF -- " $3" <<<"$OUT"; then + green "short of certified: $3" + else + red "$1: wanted reported, for '$3'"; sed 's/^/ /' <<<"$OUT" | tail -6 + fi +} +# unfit NAME SED-SCRIPT REASON: that change leaves no listing at all. +unfit() { + vary "$1" "$2"; see "$W/$1.txt" + if [[ "$RC" -ne 0 ]] && grep -q '^ carries no listing:$' <<<"$OUT" && grep -qF -- " $3" <<<"$OUT"; then + green "no listing: $3" + else + red "$1: wanted no listing, for '$3'"; sed 's/^/ /' <<<"$OUT" | tail -6 + fi +} + +echo "-- a report that carries a certified listing" +see "$W/good.txt" +[[ "$RC" -eq 0 ]] && grep -q '^ carries certified$' <<<"$OUT" && green "an installed release with nothing wanting is certified" \ + || { red "the good report: exit ${RC}"; sed 's/^/ /' <<<"$OUT"; } +grep -qF ' machine LENOVO 20XWCTO1WW (ThinkPad X1 Carbon Gen 9), firmware N32ET75W (1.51 ) 12/02/2021' <<<"$OUT" \ + && green "the machine, by maker, model and firmware" || red "machine line: $(grep machine <<<"$OUT")" +grep -qF ' release 1.0.0, kernel 6.18.53-hardened1' <<<"$OUT" && green "the release and kernel" || red "release line" +grep -qF ' display i915: eDP-1 1920x1200' <<<"$OUT" && green "the display driver and the connected output" || red "display line: $(grep display <<<"$OUT")" +grep -qF ' network wireless wlan0' <<<"$OUT" && green "the interface the default route leaves by" || red "network line: $(grep network <<<"$OUT")" + +echo "-- each thing a certified listing needs" +short medium 's/^booted: .*/booted: the usb medium/' "not taken on an installed system (booted: the usb medium)" +short degraded 's/^state: .*/state: degraded/' "the state partition is not in use (state: degraded)" +short dated 's/^kryptik: 1\.0\.0/kryptik: 0.1.20261014.91c3238a/' "0.1.20261014.91c3238a is not a release (a dated build, or none)" +short sboff 's/^secure boot: on/secure boot: off/' "Secure Boot is off" +short lockdown 's/^lockdown: .*/lockdown: integrity/' "lockdown is integrity, not confidentiality" +short firmware '/^firmware the kernel asked/{n;s/.*/ iwlwifi-QuZ-a0-hr-b0-77.ucode/}' "firmware the kernel did not find: iwlwifi-QuZ-a0-hr-b0-77.ucode" +short undriven '/^devices with no driver/{n;s/.*/ 0000:00:1f.6 class 020000 8086:15fb sub 17aa:22b8 rev 20 driver - | Ethernet controller/}' "no driver for 0000:00:1f.6 class 020000 8086:15fb" +short modular '/^disks behind a driver/{n;s/.*/ sda: ahci/}' "a disk behind a module: sda: ahci" +short dark 's/^card0-eDP-1 connected.*/card0-eDP-1 disconnected /' "no display the compositor can use" +short nocarrier 's/^ wlan0 wireless <.*/ wlan0 wireless /' "no network path" +short noroute '/^ default route by /d' "no network path" +short elsewhere 's/^ default route by .*/ default route by kzbr0/' "no network path" + +echo "-- what leaves no listing at all" +unfit other '1s/.*/a hardware report/' "not a report this tool reads" +unfit cut '/^== storage ==$/d' "cut short: no 'storage' section" +unfit nameless 's/^sys_vendor: .*/sys_vendor: /' "does not name its machine" +line() { grep -n -- "$1" "$W/good.txt" | cut -d: -f1; } # the good report's line holding $1 +unfit address 's/authenticate with xx:xx:xx:xx:xx:xx/authenticate with 00:11:22:33:44:55/' "holds a hardware address, a UUID or a serial number: line(s) $(line 'authenticate with')" +grep -q '00:11:22:33:44:55' <<<"$OUT" && red "the address itself is printed" || green "the line is named, never what it holds" +unfit serial 's/SerialNumber: /SerialNumber: 4C530001230506115281/' "holds a hardware address, a UUID or a serial number: line(s) $(line 'SerialNumber: ')" +unfit uuid 's/^\[ 0\.000000\] Linux.*/[ 3.0] EXT4-fs (sda2): mounted filesystem 0a1b2c3d-1111-2222-3333-444455556666 ro/' "holds a hardware address, a UUID or a serial number: line(s) $(line 'Linux version')" + +echo "-- the list, held to its reports" +L="$W/list"; mkdir -p "$L" +lists() { OUT="$(KRYPTIK_HARDWARE_DIR="$L" bash "$TOOL" --list 2>&1)"; RC=$?; } +rows() { printf '# report level who day note\n' > "$L/list.tsv"; printf '%s\n' "$@" >> "$L/list.tsv"; } +bad() { # bad WHAT WORDS: the list as it stands is refused, in WORDS + lists + if [[ "$RC" -ne 0 ]] && grep -qF -- "$2" <<<"$OUT"; then green "$1"; else red "$1"; sed 's/^/ /' <<<"$OUT" | tail -5; fi +} +cp "$W/good.txt" "$L/lenovo-thinkpad-x1-carbon-gen-9.txt" +cp "$W/medium.txt" "$L/dell-xps-13-9310.txt" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14 on its own screen" \ + "dell-xps-13-9310.txt reported DevomB 2026-10-15" +lists +[[ "$RC" -eq 0 ]] && grep -q '^ok: 2 machine(s) listed' <<<"$OUT" && green "a certified row and a reported one, each carried by its report" \ + || { red "the good list: exit ${RC}"; sed 's/^/ /' <<<"$OUT"; } +grep -qF ' ok certified LENOVO 20XWCTO1WW (ThinkPad X1 Carbon Gen 9), 1.0.0 (DevomB, 2026-10-14)' <<<"$OUT" \ + && green "each row says what it lists" || red "row line: $(grep certified <<<"$OUT")" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" "dell-xps-13-9310.txt certified DevomB 2026-10-15" +bad "a row cannot claim certified on a report short of it" "dell-xps-13-9310.txt: listed certified, and its report is short of that:" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" +bad "a report beside the list needs a row" "dell-xps-13-9310.txt: a report with no row in the list" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" "dell-xps-13-9310.txt reported DevomB 2026-10-15" \ + "hp-elitebook-845.txt reported DevomB 2026-10-16" +bad "a row needs its report" "hp-elitebook-845.txt: no such report beside the list" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt tested DevomB 2026-10-14" "dell-xps-13-9310.txt reported DevomB 2026-10-15" +bad "a level is reported or certified" "the level is reported or certified, not 'tested'" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" "dell-xps-13-9310.txt reported DevomB 2026-10-15" \ + "dell-xps-13-9310.txt reported DevomB 2026-10-16" +bad "a report is listed once" "dell-xps-13-9310.txt: listed twice" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB October" "dell-xps-13-9310.txt reported DevomB 2026-10-15" +bad "a row names who vouches and the day" "a row names who vouches and the day (YYYY-MM-DD)" +cp "$W/address.txt" "$L/acer-swift-3.txt" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" "dell-xps-13-9310.txt reported DevomB 2026-10-15" \ + "acer-swift-3.txt reported DevomB 2026-10-16" +bad "a report holding an address is not listed" "acer-swift-3.txt: carries no listing:" + +echo "-- the tree's own list" +OUT="$(bash "$TOOL" --list 2>&1)"; RC=$? +[[ "$RC" -eq 0 ]] && green "docs/hardware/list.tsv: $(tail -1 <<<"$OUT")" || { red "docs/hardware/list.tsv is refused"; sed 's/^/ /' <<<"$OUT"; } + +echo +echo "${PASS} passed, ${FAIL} failed" +[[ "$FAIL" -eq 0 ]] From 406793f263d6e3c92c4d203b0c3d75ad876b83ae Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 02:21:59 -0700 Subject: [PATCH 07/54] The hardware report says whether the state partition is in use, and names the net zone's interfaces by kind, with the one the default route leaves by and no address --- tools/install/kryptik-hwreport.sh | 30 +++++++++++++++++++++--------- tools/tests/hwreport.sh | 31 ++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh index d6c892ee..0283a2ce 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -35,6 +35,11 @@ one() { [ -r "$1" ] && head -n 1 "$1" 2>/dev/null; } # a file's first l 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; } +# netzone NAME COMMAND...: COMMAND in the net zone's network namespace; the +# fixture tree holds its output as netzone/NAME. +netzone() { + if [ -n "$R" ]; then cat "$R/netzone/$1" 2>/dev/null; else shift; nsenter -t "$pid" -n "$@" 2>/dev/null; fi +} sec() { printf '\n== %s ==\n' "$1"; } or_none() { if [ -s "$1" ]; then sed 's/^/ /' "$1"; else echo " (none)"; fi; } @@ -64,6 +69,7 @@ system() { 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 'state: %s\n' "$(sed -n 's/^state=//p' "$id" 2>/dev/null)" printf 'taken: %s\n' "$(date -u +%Y-%m-%dT%H:%MZ)" } @@ -232,15 +238,21 @@ network() { 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 + # The machine's interfaces are the net zone's, in its own namespace: each + # by kind and flags, and the one the default route leaves by. + pid="$(cut -d' ' -f1 "$R/run/kryptik/zones/net/init.pid" 2>/dev/null)" + [ -n "$pid" ] || { echo "the net zone is not running"; return 0; } + echo "in the net zone:" + radios="$(netzone iw-dev iw dev | awk '$1 == "Interface" { print $2 }' | tr '\n' ' ')" + netzone ip-link ip -o -d link | awk -v radios="$radios" ' + BEGIN { n = split(radios, r, " "); for (i = 1; i <= n; i++) radio[r[i]] = 1 } + $2 == "lo:" { next } + { + name = $2; sub(/:$/, "", name); sub(/@.*/, "", name) + kind = ($0 ~ / (bridge|veth|dummy|tun) /) ? "virtual" : (name in radio) ? "wireless" : "wired" + print " " name, kind, $3 + }' + netzone ip-route ip -4 route show default | awk '{ for (i = 1; i < NF; i++) if ($i == "dev") print " default route by " $(i + 1) }' } inputs() { diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index effe681f..aff69201 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -95,6 +95,18 @@ 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" +put run/kryptik/zones/net/init.pid '4242 9876' +mkdir -p "$W/netzone" +cat > "$W/netzone/ip-link" <<'EOF' +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 allmulti 0 minmtu 0 maxmtu 0 +2: eth0: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000\ link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff promiscuity 0 allmulti 0 minmtu 68 maxmtu 9194 parentbus pci parentdev 0000:00:1f.6 +3: wlan0: mtu 1500 qdisc noqueue state DOWN mode DORMANT group default qlen 1000\ link/ether 52:54:00:65:43:21 brd ff:ff:ff:ff:ff:ff promiscuity 0 allmulti 0 minmtu 256 maxmtu 2304 +4: kzbr0: mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000\ link/ether 52:54:00:00:00:01 brd ff:ff:ff:ff:ff:ff promiscuity 0 allmulti 0 minmtu 68 maxmtu 65535 \ bridge forward_delay 1500 hello_time 200 +5: vz-untrusted@if2: mtu 1500 qdisc noqueue master kzbr0 state UP mode DEFAULT group default qlen 1000\ link/ether 52:54:00:00:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 1 promiscuity 1 allmulti 1 minmtu 68 maxmtu 65535 \ veth \ bridge_slave state forwarding +EOF +printf 'phy#0\n\tInterface wlan0\n\t\tifindex 3\n\t\taddr 52:54:00:65:43:21\n\t\ttype managed\n' > "$W/netzone/iw-dev" +put netzone/ip-route 'default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 1002' + cat > "$W/dmesg" <<'EOF' [ 0.000000] Linux version 6.18.53-hardened1 (kryptik@build) [ 0.100000] microcode: Current revision: 0x000000a4 @@ -116,6 +128,7 @@ 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 "whether its state is kept" '^state: tmpfs$' 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$' @@ -136,6 +149,14 @@ want "an interface nothing drives, with what would" '^usb 1-3:1\.0 usb:v8087p002 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 "-- the interfaces the net zone holds" +want "a wired one, by its flags" '^ eth0 wired $' +want "a radio, known from iw" '^ wlan0 wireless $' +want "the zones' bridge is not the machine's" '^ kzbr0 virtual ' +want "nor a zone's link, named without its peer" '^ vz-untrusted virtual ' +want "where the default route leaves" '^ default route by eth0$' +deny "no address of the network it is on" '10\.0\.2\.' + 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; } @@ -148,7 +169,7 @@ if grep -qF '0000:00:00.0' <<<"$missing"; then red "a host bridge is listed as w 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' +deny "no hardware address" '8c:16:45|00:11:22:33:44:55|66:77:88:99:aa:bb|52:54:00' 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' @@ -157,6 +178,14 @@ 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 "-- the listing it carries" +LISTING="$(bash "${ROOT}/tools/check-hardware.sh" "$OUT" 2>&1)"; rc=$? +[[ "$rc" -eq 0 ]] && grep -q '^ carries reported; short of certified:$' <<<"$LISTING" \ + && green "check-hardware.sh reads it: reported" || { red "check-hardware.sh on the report: exit ${rc}"; sed 's/^/ /' <<<"$LISTING" | tail -8; } +grep -qF ' not taken on an installed system (booted: the usb medium)' <<<"$LISTING" && green "short of certified for booting the medium" || red "no word on the medium" +grep -qF ' firmware the kernel did not find: iwlwifi-QuZ-a0-hr-b0-77.ucode' <<<"$LISTING" && green "and for the firmware it lacks" || red "no word on the firmware" +grep -qF ' network wired eth0' <<<"$LISTING" && green "the network path is read from it" || red "network line: $(grep '^ network' <<<"$LISTING")" + 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=$? From b8e75178c2e662f7ee59c48239b096b3e7a70f83 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 02:22:01 -0700 Subject: [PATCH 08/54] docs/hardware.md: the hardware list, what a reported and a certified row say, who may vouch and how a row enters; the list starts empty and check-hardware.sh --list holds each row to its report --- docs/hardware.md | 84 ++++++++++++++++++++++++++++++++++++++++++ docs/hardware/list.tsv | 7 ++++ 2 files changed, 91 insertions(+) create mode 100644 docs/hardware.md create mode 100644 docs/hardware/list.tsv diff --git a/docs/hardware.md b/docs/hardware.md new file mode 100644 index 00000000..065492f6 --- /dev/null +++ b/docs/hardware.md @@ -0,0 +1,84 @@ +# Hardware + +Which machines Kryptik has been run on, who ran it, and what each run +showed. The list is `docs/hardware/list.tsv`; beside it sits one report per +row, made on the machine by `kryptik-hwreport`, and +`tools/check-hardware.sh --list` holds every row to its report. The list is +empty until the hardware tests planned for October 2026. + +What the kernel is built to drive is in the [README](../README.md#hardware). +This page is about what was seen to work. + +## A report + +`kryptik-hwreport` prints what a machine is made of and what the image lacks +for it: the maker and model, the firmware's Secure Boot state, the processor +and its microcode, every PCI and USB device with the driver that took it, the +disks and the drivers under them, the display, network and input devices, the +loaded modules and the kernel log. Its `missing` section is the part to act +on: the firmware the kernel asked for and did not find, each a line for +`build/config/firmware.list`, and the devices and disks that want a driver's +line in `build/config/kernel/boot.fragment`. + +Nothing in it is one machine's alone. No serial number is read, and the +hardware addresses, serial numbers and UUIDs the kernel log prints are struck +before the report is written. Read it before you send it all the same. + +There are two ways to take one: + +- **From the USB medium, with no keyboard or screen.** Write the image to a + stick, then make a folder named `kryptik-report` on the stick's first + partition, from any computer. Boot the stick on the machine, wait a minute + and switch off. The report is `kryptik-report/report-1.txt` on that + partition, and the next boot writes `report-2.txt`. A stick without the + folder is never written. +- **By command.** At the medium's root shell, `kryptik-hwreport --save` writes + the same file. On an installed system, as root, `kryptik-hwreport > FILE` + writes the report where you say. + +## What a listing says + +| level | what its report shows | +| --- | --- | +| `reported` | The machine by maker and model, the release and the kernel that ran, and every section whole, from the medium or an installed system. It says what the machine has and what the image lacks for it, not that Kryptik works there. | +| `certified` | All of that on an installed system: a release, not a dated build; booted by UEFI with Secure Boot on and the kernel locked down; the encrypted state partition in use; a display with an output connected; the default route leaving by a wired or wireless interface that has a carrier; and nothing under `missing`. | + +A `certified` row says that the release installs on that model, boots there +with Secure Boot on, has a display to draw the desktop on and reaches a +network. It does not speak for what 1.0 does not drive (sound, Bluetooth, +suspend), for another firmware version, or for the same model sold with +another network card: the report names the firmware version and every device, +so a reader can compare. + +## Who may vouch + +Whoever ran it on a machine in front of them. A row names them and the day, +and they answer for the report being that machine's and unedited. For +`certified` they installed from the release's own medium, checked as +[releases](releases.md) describes, with the release's Secure Boot certificate +enrolled, and took the report on the installed system. + +A row enters the list as a pull request that adds the report and its row. It +is read by someone other than its author, who runs `tools/check-hardware.sh` +on the report and compares what it prints with the row. + +## The list + +`docs/hardware/list.tsv` has one row per report: + +```text +report level who day note +``` + +`report` is the file beside the list, named for the machine in lower case, +such as `lenovo-thinkpad-x1-carbon-gen-9.txt`. `level` is `reported` or +`certified`. `who` vouches, `day` is the day the report was taken, and the +note says what a reader should know that the report cannot: a dock, a +firmware setting that had to change. A machine that runs a later release gets +a new report and a new row. + +`tools/check-hardware.sh REPORT` prints what a report shows and the level it +carries, with each reason it falls short of `certified`. `--list` checks the +whole list: every row's report is there and carries the level the row claims, +no report holds a hardware address, a UUID or a serial number, and no report +is without a row. diff --git a/docs/hardware/list.tsv b/docs/hardware/list.tsv new file mode 100644 index 00000000..3f8fde75 --- /dev/null +++ b/docs/hardware/list.tsv @@ -0,0 +1,7 @@ +# Machines Kryptik has been run on (docs/hardware.md). Read by +# tools/check-hardware.sh --list. +# +# report level who day note +# +# report: the kryptik-hwreport file beside this list, which must carry the +# level. level: reported or certified. who vouches, and the day it was taken. From 38e2c7fd39231e1216dc4315281743bef441daec Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 2 Oct 2026 02:22:02 -0700 Subject: [PATCH 09/54] Acceptance holds the reports it takes to the listing they carry: the medium's is reported, and the installed system's, taken under Secure Boot with a display and a network, is certified or short of it only as a dated build --- tools/image/hwreport-test.sh | 38 +++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh index dff943a0..73af15e3 100755 --- a/tools/image/hwreport-test.sh +++ b/tools/image/hwreport-test.sh @@ -15,6 +15,9 @@ # installed disk # step 5 kryptik-hwreport --save at the medium's root shell, on a stick # nobody prepared +# step 6 the installed disk under Secure Boot, with a display and a +# network: the report taken there carries a certified listing +# (tools/check-hardware.sh), or falls short only as a dated build # # Every boot is of a copy this script makes; the medium itself is only read. set -uo pipefail @@ -28,7 +31,7 @@ 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 ;; + -h|--help) sed -n '2,22p' "${BASH_SOURCE[0]}"; exit 0 ;; *) die "unknown argument: $1" ;; esac done @@ -95,6 +98,9 @@ want "$R1" 'Linux version .*hardened' "from its first li 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" +LISTING="$("${SELF}/../check-hardware.sh" "$R1" 2>&1)" +if grep -q '^ carries reported; short of certified:$' <<<"$LISTING"; then green "check-hardware.sh reads it: a reported listing" +else red "check-hardware.sh does not read the medium's report as reported"; sed 's/^/ /' <<<"$LISTING" | tail -8; fi 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 @@ -155,5 +161,35 @@ 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" +# ----------------------------------------------------------------- step 6 -- +step "step 6: the installed system under Secure Boot, and the listing its report carries" +CERT="${VMDIR}/hwreport-sb.crt" +mtype -i "$(esp_of "$USB")" ::/kryptik/kryptik-sb.crt > "$CERT" 2>/dev/null +if "${SELF}/ovmf-vars.sh" --cert "$CERT" --out "${VMDIR}/hwreport-vars" > /dev/null 2>&1; then + VARSF="${VMDIR}/hwreport-vars.fd"; cp "${VMDIR}/hwreport-vars/enrolled.fd" "$VARSF" + start_vm hwreport-installed --net user --gpu + python3 "$DRV" --serial "$SER" --timeout 300 \ + "expect:KRYPTIK_SMOKE: END" "login:${TUSER}:${TPASS}" "sleep:5" \ + "$(ROOTSH 'kryptik-hwreport > /var/lib/kryptik/hwreport.txt')" \ + "su:${RPASS}:poweroff" "expect:Power down" "wait-exit" + drc=$? + stop_vm + [[ "$drc" -eq 0 ]] && green "the installed system booted with the medium's certificate enrolled and took its report" || red "the serial drive failed (see above)" + IR="${VMDIR}/hwreport-installed.txt"; MNT="${VMDIR}/hwreport-state"; mkdir -p "$MNT"; rm -f "$IR" + if open_state "$DISK" "$MNT"; then cp "$MNT/lib/kryptik/hwreport.txt" "$IR" 2>/dev/null; close_state "$MNT"; fi + LISTING="$("${SELF}/../check-hardware.sh" "$IR" 2>&1)" + sed 's/^/ /' <<<"$LISTING" + if grep -q '^ carries certified$' <<<"$LISTING"; then + green "its report carries a certified listing" + elif grep -q '^ carries reported; short of certified:$' <<<"$LISTING" \ + && [[ "$(grep -c '^ ' <<<"$LISTING")" -eq 1 ]] && grep -q '^ .* is not a release' <<<"$LISTING"; then + green "its report falls short of certified in one thing: a dated build is not a release" + else + red "the installed system's report carries less than it should" + fi +else + red "no variable store with the medium's certificate enrolled" +fi + printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" [[ "$FAIL" -eq 0 ]] || exit 1 From fdc8e883212d92f84ccd77a891afd5e2058484c0 Mon Sep 17 00:00:00 2001 From: DevomB Date: Mon, 5 Oct 2026 08:32:30 -0700 Subject: [PATCH 10/54] 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 1124d614d3f876dbdb515a77b69f5364d2f62fbe Mon Sep 17 00:00:00 2001 From: DevomB Date: Mon, 5 Oct 2026 08:32:30 -0700 Subject: [PATCH 11/54] 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 0283a2ce..d8e9bd8a 100755 --- a/tools/install/kryptik-hwreport.sh +++ b/tools/install/kryptik-hwreport.sh @@ -330,7 +330,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 aff69201..012a3594 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -193,6 +193,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 0df30cc3fd18b68fb15c67da37837bfabca3dabc Mon Sep 17 00:00:00 2001 From: DevomB Date: Mon, 5 Oct 2026 08:34:29 -0700 Subject: [PATCH 12/54] The hardware list holds machines on the day their report was taken: a report from a virtual machine is refused, and so is a row whose day is not its report's; the page keeps to the one sentence on hardware testing --- docs/hardware.md | 6 +++--- tools/check-hardware.sh | 8 +++++++- tools/tests/check-hardware.sh | 11 ++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 065492f6..75adb7d9 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -4,7 +4,7 @@ Which machines Kryptik has been run on, who ran it, and what each run showed. The list is `docs/hardware/list.tsv`; beside it sits one report per row, made on the machine by `kryptik-hwreport`, and `tools/check-hardware.sh --list` holds every row to its report. The list is -empty until the hardware tests planned for October 2026. +empty. Testing on physical hardware is planned for October 2026. What the kernel is built to drive is in the [README](../README.md#hardware). This page is about what was seen to work. @@ -80,5 +80,5 @@ a new report and a new row. `tools/check-hardware.sh REPORT` prints what a report shows and the level it carries, with each reason it falls short of `certified`. `--list` checks the whole list: every row's report is there and carries the level the row claims, -no report holds a hardware address, a UUID or a serial number, and no report -is without a row. +was taken on the row's day and not in a virtual machine, no report holds a +hardware address, a UUID or a serial number, and no report is without a row. diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index c65a68e9..fa590433 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -121,7 +121,7 @@ show() { # show REPORT: its facts, and why it falls short; 1 when it carries n } list() { - local tsv="${DIR}/list.tsv" bad=0 n=0 report level by day have f + local tsv="${DIR}/list.tsv" bad=0 n=0 report level by day have taken f [[ -f "$tsv" ]] || { echo "no list at ${tsv}"; return 1; } declare -A seen=() fail() { echo " FAIL $*"; bad=$((bad + 1)); } @@ -136,10 +136,16 @@ list() { [[ -n "$by" && "$day" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { fail "${report}: a row names who vouches and the day (YYYY-MM-DD)"; continue; } examine "${DIR}/${report}" have="$(carries)" + taken="$(section system "${DIR}/${report}" | field taken)" if [[ -z "$have" ]]; then fail "${report}: carries no listing:"; sed '/^$/d; s/^/ /' <<<"$NOT_REPORTED" elif [[ "$level" == certified && "$have" != certified ]]; then fail "${report}: listed certified, and its report is short of that:"; sed '/^$/d; s/^/ /' <<<"$NOT_CERTIFIED" + # The list is of machines, each on the day its report was taken. + elif section cpu "${DIR}/${report}" | field 'of note' | grep -qw hypervisor; then + fail "${report}: taken in a virtual machine, and the list holds machines" + elif [[ "$taken" != "${day}T"* ]]; then + fail "${report}: its row says ${day}, and the report was taken ${taken:-on no day it names}" else printf ' ok %-10s %s, %s (%s, %s)\n' "$level" "$MACHINE" "$RELEASE" "$by" "$day" fi diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index b2706143..43a9b7b8 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -157,7 +157,7 @@ bad() { # bad WHAT WORDS: the list as it stands is refused, in WORDS if [[ "$RC" -ne 0 ]] && grep -qF -- "$2" <<<"$OUT"; then green "$1"; else red "$1"; sed 's/^/ /' <<<"$OUT" | tail -5; fi } cp "$W/good.txt" "$L/lenovo-thinkpad-x1-carbon-gen-9.txt" -cp "$W/medium.txt" "$L/dell-xps-13-9310.txt" +sed 's/^taken: 2026-10-14/taken: 2026-10-15/' "$W/medium.txt" > "$L/dell-xps-13-9310.txt" rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14 on its own screen" \ "dell-xps-13-9310.txt reported DevomB 2026-10-15" lists @@ -183,6 +183,15 @@ cp "$W/address.txt" "$L/acer-swift-3.txt" rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" "dell-xps-13-9310.txt reported DevomB 2026-10-15" \ "acer-swift-3.txt reported DevomB 2026-10-16" bad "a report holding an address is not listed" "acer-swift-3.txt: carries no listing:" +rm "$L/acer-swift-3.txt" +# The processor's own word for it: a report whole in every other way. +sed '/^threads running/a of note: nx smep smap hypervisor' "$W/good.txt" > "$L/qemu-standard-pc.txt" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-14" "dell-xps-13-9310.txt reported DevomB 2026-10-15" \ + "qemu-standard-pc.txt certified DevomB 2026-10-14" +bad "a report taken in a virtual machine is not listed" "qemu-standard-pc.txt: taken in a virtual machine" +rm "$L/qemu-standard-pc.txt" +rows "lenovo-thinkpad-x1-carbon-gen-9.txt certified DevomB 2026-10-13" "dell-xps-13-9310.txt reported DevomB 2026-10-15" +bad "a row's day is the day its report was taken" "lenovo-thinkpad-x1-carbon-gen-9.txt: its row says 2026-10-13, and the report was taken 2026-10-14T09:12Z" echo "-- the tree's own list" OUT="$(bash "$TOOL" --list 2>&1)"; RC=$? From 597affce34d183e9d08c859d8babde66cdcfef88 Mon Sep 17 00:00:00 2001 From: DevomB Date: Tue, 13 Oct 2026 09:47:00 -0700 Subject: [PATCH 13/54] 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 14/54] 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 5e88b351d299a09dc92bba9710479fd9d47330cb Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 04:41:59 -0700 Subject: [PATCH 15/54] The hardware list says what a certified row leaves out as 1.0 stands Since the laptop stack, 1.0 drives sound and suspend, so a certified row leaves them out because the report does not exercise them, not because nothing drives them; a camera is the same. Bluetooth is what 1.0 does not drive. --- docs/hardware.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 75adb7d9..08e990d6 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -45,10 +45,10 @@ There are two ways to take one: A `certified` row says that the release installs on that model, boots there with Secure Boot on, has a display to draw the desktop on and reaches a -network. It does not speak for what 1.0 does not drive (sound, Bluetooth, -suspend), for another firmware version, or for the same model sold with -another network card: the report names the firmware version and every device, -so a reader can compare. +network. It does not speak for what the report does not exercise (sound, +suspend, a camera), for Bluetooth, which 1.0 does not drive, for another +firmware version, or for the same model sold with another network card: the +report names the firmware version and every device, so a reader can compare. ## Who may vouch From eba7f9f1aa229efd30ce0604ec83c12a3f96d451 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 06:43:07 -0700 Subject: [PATCH 16/54] 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 17/54] 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 377c61b88c31a552b2d0258f46e6526663cc2fc0 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 09:56:28 -0700 Subject: [PATCH 18/54] A listing names the machine's sound by kind, with what drives each, and no level asks for it check-hardware.sh reads the report's new audio section into one line: the HD Audio codecs, the graphics' HDMI ones left out; digital microphones the NHLT describes for Intel's DSP; SoundWire parts; microphones on AMD's coprocessor, with the ACPI names its drivers look for; amplifiers by their ACPI name. Each says what drives it, or "not driven", SOF counting for what sits behind Intel's DSP and the ACPI device splitter not counting for an amplifier. A report from before the section says so. Neither level asks for any of it, as 1.0 drives none of these kinds (docs/design/laptop-audio.md). tools/tests/check-hardware.sh: the good report's line, the same report driven by SOF, and one with no audio section that stays certified. tools/tests/hwreport.sh gives each of the report's five kinds the listing it carries. docs/hardware.md names the sound hardware among what a report holds, and what a listing says of it. --- docs/hardware.md | 15 +++++++++----- tools/check-hardware.sh | 38 +++++++++++++++++++++++++++++++++++ tools/tests/check-hardware.sh | 14 +++++++++++++ tools/tests/hwreport.sh | 13 ++++++++++++ 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 08e990d6..30cae973 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -14,11 +14,11 @@ This page is about what was seen to work. `kryptik-hwreport` prints what a machine is made of and what the image lacks for it: the maker and model, the firmware's Secure Boot state, the processor and its microcode, every PCI and USB device with the driver that took it, the -disks and the drivers under them, the display, network and input devices, the -loaded modules and the kernel log. Its `missing` section is the part to act -on: the firmware the kernel asked for and did not find, each a line for -`build/config/firmware.list`, and the devices and disks that want a driver's -line in `build/config/kernel/boot.fragment`. +disks and the drivers under them, the display, the sound hardware, network and +input devices, the loaded modules and the kernel log. Its `missing` section is +the part to act on: the firmware the kernel asked for and did not find, each a +line for `build/config/firmware.list`, and the devices and disks that want a +driver's line in `build/config/kernel/boot.fragment`. Nothing in it is one machine's alone. No serial number is read, and the hardware addresses, serial numbers and UUIDs the kernel log prints are struck @@ -50,6 +50,11 @@ suspend, a camera), for Bluetooth, which 1.0 does not drive, for another firmware version, or for the same model sold with another network card: the report names the firmware version and every device, so a reader can compare. +A listing also names the machine's sound: its HD Audio codecs, and whether +microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire or +amplifiers in front of the speakers, each with what drives it. No level asks +for any of it. + ## Who may vouch Whoever ran it on a machine in front of them. A row names them and the day, diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index fa590433..5273f047 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -18,6 +18,43 @@ DIR="${KRYPTIK_HARDWARE_DIR:-${ROOT}/docs/hardware}" section() { awk -v s="== $1 ==" '$0 == s { on = 1; next } on && /^== .* ==$/ { exit } on' "$2"; } # section NAME REPORT field() { sed -n "s/^$1: //p" | head -1; } # field KEY, of the section on stdin +took() { if [[ -n "$1" ]]; then echo "driven by $1"; else echo "not driven"; fi; } # took DRIVER + +# audio_of REPORT: the machine's sound in one line, by the kinds of +# docs/design/laptop-audio.md, each with what drives it. No level asks for any. +audio_of() { + local a sof codecs sdw acp names amps drv parts=() + a="$(section audio "$1")" + [[ -n "$a" ]] || { echo "not in this report"; return; } + # SOF in place of snd-hda-intel drives what sits behind Intel's DSP. + sof="$(awk '$1 == "controller" && $NF ~ /^sof-/ { print $NF; exit }' <<<"$a")" + # The graphics' HDMI codecs (Intel's, AMD's, NVIDIA's) are not the machine's own. + codecs="$(awk '$1 == "codec" && $2 !~ /^(8086|1002|10de)/ { print $2 }' <<<"$a" | sort -u | paste -sd' ' -)" + if [[ -n "$codecs" ]]; then parts+=("HD Audio codec ${codecs}") + elif grep -q '^codec ' <<<"$a"; then parts+=("HD Audio for HDMI alone") + elif grep -q '^controller ' <<<"$a"; then parts+=("no HD Audio codec found") + fi + if awk '$1 == "nhlt:" && $5 + 0 > 0 { found = 1 } END { exit !found }' <<<"$a"; then + parts+=("digital microphones on Intel's DSP, $(took "$sof")") + fi + sdw="$(awk '$1 == "soundwire" && $5 == "part" { print $6 }' <<<"$a" | paste -sd' ' -)" + [[ -z "$sdw" ]] || parts+=("SoundWire ${sdw}, $(took "$sof")") + acp="$(awk '$1 == "acp" { print $7, ($9 == "-" ? "" : $9); exit }' <<<"$a")" + if [[ -n "$acp" ]]; then + names="$(sed -n 's/^acpi names: //p' <<<"$a" | head -1)" + drv="${acp#* }" + if [[ -n "$names" ]]; then parts+=("microphones on AMD's coprocessor (rev ${acp%% *}, ${names// /, }), $(took "$drv")") + else parts+=("AMD's coprocessor (rev ${acp%% *}) with no microphones its ACPI names"); fi + fi + amps="$(awk '$1 == "amplifier" { sub(/:.*/, "", $2); print $2 }' <<<"$a" | sort -u | paste -sd' ' -)" + if [[ -n "$amps" ]]; then + # The ACPI device splitter alone drives no amplifier. + drv="$(awk '$1 == "amplifier" { for (i = 4; i <= NF; i++) if ($i != "-" && $i != "serial-multi-instantiate") print $i }' <<<"$a" | sort -u | paste -sd' ' -)" + parts+=("amplifiers ${amps}, $(took "$drv")") + fi + if [[ "${#parts[@]}" -eq 0 ]]; then echo "no audio device" + else local IFS=';'; sed 's/;/; /g' <<<"${parts[*]}"; fi +} # examine REPORT: MACHINE, RELEASE and FACTS, with NOT_REPORTED and # NOT_CERTIFIED, each empty or one reason per line. @@ -81,6 +118,7 @@ examine() { outputs="$(section display "$r" | sed -n 's/^card[0-9]*-\([^ ]*\) connected *\(.*\)/\1 \2/p' | paste -sd';' -)" fact display "${cards:-no DRM device}${outputs:+: ${outputs//;/; }}" [[ -n "$cards" && -n "$outputs" ]] || lacks "no display the compositor can use: no DRM device with a connected output" + fact audio "$(audio_of "$r")" # A path out: the default route leaves by one of the machine's own # interfaces, and that one has a carrier. diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 43a9b7b8..600e3c5a 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -70,6 +70,12 @@ card0 driver i915 card0-eDP-1 connected 1920x1200 card0-DP-1 disconnected +== audio == +controller 0000:00:1f.3 class 040380 8086:a0c8 rev 20 driver snd_hda_intel +codec 10ec0287 sub 17aa22e6 +codec 80862812 sub 80860101 +nhlt: 2 endpoints, pdm 1, ssp 1 + == network == rfkill0 wlan phy0 soft 0 hard 0 in the net zone: @@ -123,6 +129,14 @@ grep -qF ' machine LENOVO 20XWCTO1WW (ThinkPad X1 Carbon Gen 9), firmware N3 grep -qF ' release 1.0.0, kernel 6.18.53-hardened1' <<<"$OUT" && green "the release and kernel" || red "release line" grep -qF ' display i915: eDP-1 1920x1200' <<<"$OUT" && green "the display driver and the connected output" || red "display line: $(grep display <<<"$OUT")" grep -qF ' network wireless wlan0' <<<"$OUT" && green "the interface the default route leaves by" || red "network line: $(grep network <<<"$OUT")" +grep -qF " audio HD Audio codec 10ec0287; digital microphones on Intel's DSP, not driven" <<<"$OUT" \ + && green "the sound, by kind, and what is not driven" || red "audio line: $(grep audio <<<"$OUT")" +vary sof 's/driver snd_hda_intel$/driver sof-audio-pci-intel-tgl/'; see "$W/sof.txt" +grep -qF "digital microphones on Intel's DSP, driven by sof-audio-pci-intel-tgl" <<<"$OUT" \ + && green "and what drives it, once a driver does" || red "audio line with SOF: $(grep audio <<<"$OUT")" +vary noaudio '/^== audio ==$/,/^$/d'; see "$W/noaudio.txt" +grep -qF ' audio not in this report' <<<"$OUT" && grep -q '^ carries certified$' <<<"$OUT" \ + && green "a report from before the section: said so, and no level asks for it" || red "a report with no audio section: $(grep -E 'audio|carries' <<<"$OUT")" echo "-- each thing a certified listing needs" short medium 's/^booted: .*/booted: the usb medium/' "not taken on an installed system (booted: the usb medium)" diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh index d8ef07c9..af25deec 100755 --- a/tools/tests/hwreport.sh +++ b/tools/tests/hwreport.sh @@ -225,6 +225,11 @@ audio_now() { } 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; } +# listed WHAT FACT: the audio line check-hardware.sh gives the kind just reported. +listed() { + local got; got="$(bash "${ROOT}/tools/check-hardware.sh" "$W/report-audio.txt" 2>&1 | sed -n 's/^ audio //p')" + [[ "$got" == "$2" ]] && green "$1" || red "$1: '${got}'" +} # 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() { @@ -248,6 +253,7 @@ 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) ' +listed "the listing names the codec, not the graphics'" "HD Audio codec 10ec0287" quiet pci 0000:00:1f.3 0x040380 8086 a0c8 snd_hda_intel @@ -260,6 +266,8 @@ mkdir -p "$W/sys/firmware/acpi/tables" 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$' +listed "the listing: microphones on the DSP, which snd-hda-intel does not drive" \ + "HD Audio codec 10ec0287; digital microphones on Intel's DSP, not driven" quiet pci 0000:00:1f.3 0x040380 8086 a0c8 snd_hda_intel @@ -277,6 +285,8 @@ 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")" +listed "the listing: HDMI alone on HD Audio, and the SoundWire parts" \ + "HD Audio for HDMI alone; SoundWire 025d:0711 025d:1316, not driven" quiet pci 0000:04:00.5 0x048000 1022 15e2 @@ -289,6 +299,8 @@ 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$' +listed "the listing: microphones on AMD's coprocessor" \ + "HD Audio codec 10ec0257; microphones on AMD's coprocessor (rev 6f, AcpDmicConnected), not driven" quiet pci 0000:00:1f.3 0x040300 8086 a0c8 snd_hda_intel @@ -302,6 +314,7 @@ 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' +listed "the listing: the amplifiers, by their ACPI name" "HD Audio codec 10ec0287; amplifiers CSC3551, not driven" echo echo "${PASS} passed, ${FAIL} failed" From 4252f1ab3488b2350e90203d6e741e3d84642123 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 21:34:13 -0700 Subject: [PATCH 19/54] 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 f9de61f1da9925d56b36a5d43b0bb42de84e4c7f Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 15 Oct 2026 21:34:35 -0700 Subject: [PATCH 20/54] The VM test reads the listing's sound line for the medium's report: no audio device, as QEMU has none there --- tools/image/hwreport-test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh index 2e9fef7f..d2f9088e 100755 --- a/tools/image/hwreport-test.sh +++ b/tools/image/hwreport-test.sh @@ -104,6 +104,8 @@ deny "$R1" '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' "no UU LISTING="$("${SELF}/../check-hardware.sh" "$R1" 2>&1)" if grep -q '^ carries reported; short of certified:$' <<<"$LISTING"; then green "check-hardware.sh reads it: a reported listing" else red "check-hardware.sh does not read the medium's report as reported"; sed 's/^/ /' <<<"$LISTING" | tail -8; fi +grep -qF ' audio no audio device' <<<"$LISTING" && green "the listing's sound line: no audio device, as QEMU has none here" \ + || red "audio line: $(grep '^ audio' <<<"$LISTING")" 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 From 4186c5af917995a6d0c23682bd1a5afd0430fe53 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:29:13 -0700 Subject: [PATCH 21/54] 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 22/54] 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 ed7f8afc0787f3728ccd68706a2562a9e3677b88 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:36:14 -0700 Subject: [PATCH 23/54] The hardware listing says what a laptop's parts, its sleep, its camera and its Bluetooth are, and what drives them A report's laptop section, usb devices and input switches showed a laptop's parts, but the listing read none of them, so a reader comparing October's reports had to dig each one out. check-hardware.sh prints four more lines, beside the sound's, none of which a level asks for: - laptop: each backlight and its kind, the machine's own batteries (a mouse's or a headset's, scope Device, is not counted) and mains, and the lid switch; a machine with none of them says so; - sleep: what a sleep to memory is, deep or s2idle, and the other kinds offered, or that there is none; - camera: a USB video device by its IDs and what drives it, and a MIPI camera's parts: Intel's image processing unit and what drives it, and its sensors, behind a Visual Sensing Controller where there is one; - bluetooth: each controller on USB (e0/01/01) and what drives it. A report from before the laptop section says so on its three lines. tools/tests/check-hardware.sh's laptop gains a USB camera, an unbound Bluetooth controller, a laptop section and a lid switch, and the cases vary each: a device's battery, a two-word status, a desktop, s2idle alone, no mem, a MIPI camera, none, btusb bound, no Bluetooth, and no section. docs/hardware.md names the lines. --- docs/hardware.md | 10 +++-- tools/check-hardware.sh | 84 +++++++++++++++++++++++++++++++++++ tools/tests/check-hardware.sh | 52 ++++++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 30cae973..04eb1fd7 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -14,8 +14,9 @@ This page is about what was seen to work. `kryptik-hwreport` prints what a machine is made of and what the image lacks for it: the maker and model, the firmware's Secure Boot state, the processor and its microcode, every PCI and USB device with the driver that took it, the -disks and the drivers under them, the display, the sound hardware, network and -input devices, the loaded modules and the kernel log. Its `missing` section is +disks and the drivers under them, the display, the sound hardware, a laptop's +backlights, batteries, sleep states and camera parts, network and input +devices, the loaded modules and the kernel log. Its `missing` section is the part to act on: the firmware the kernel asked for and did not find, each a line for `build/config/firmware.list`, and the devices and disks that want a driver's line in `build/config/kernel/boot.fragment`. @@ -83,7 +84,10 @@ firmware setting that had to change. A machine that runs a later release gets a new report and a new row. `tools/check-hardware.sh REPORT` prints what a report shows and the level it -carries, with each reason it falls short of `certified`. `--list` checks the +carries, with each reason it falls short of `certified`. One line each says +what the machine's sound, laptop parts, sleep, camera and Bluetooth are, and +what drives them; no level asks for any of them, as 1.0 drives few of them on +a laptop. `--list` checks the whole list: every row's report is there and carries the level the row claims, was taken on the row's day and not in a virtual machine, no report holds a hardware address, a UUID or a serial number, and no report is without a row. diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index 5273f047..0d8eb8d3 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -56,6 +56,86 @@ audio_of() { else local IFS=';'; sed 's/;/; /g' <<<"${parts[*]}"; fi } +# laptop_of REPORT: what a laptop has that a desktop lacks: each backlight +# and its kind, the machine's own batteries and its mains, and the lid switch. +laptop_of() { + local l bl bat mains parts=() + grep -qxF '== laptop ==' "$1" || { echo "not in this report"; return; } + l="$(section laptop "$1")" + bl="$(awk '$1 == "backlight" { printf "%s%s (%s)", sep, $2, $4; sep = ", " }' <<<"$l")" + [[ -z "$bl" ]] || parts+=("backlight ${bl}") + # A mouse's or a headset's battery is the device's, not the machine's. The + # kind is the line's last word, as a status may be two ("Not charging"). + bat="$(awk '$1 == "power_supply" && $4 == "Battery" && $6 == "System" { + printf "%s%s%s", sep, $2, ($NF == "-" ? "" : " (" $NF ")"); sep = ", " }' <<<"$l")" + [[ -z "$bat" ]] || parts+=("battery ${bat}") + mains="$(awk '$1 == "power_supply" && $4 == "Mains" { printf "%s%s", sep, $2; sep = " " }' <<<"$l")" + [[ -z "$mains" ]] || parts+=("mains ${mains}") + if grep -q '^"Lid Switch"' <<<"$(section input "$1")"; then parts+=("lid switch"); fi + if [[ "${#parts[@]}" -eq 0 ]]; then echo "none of a laptop's parts: no backlight, battery or lid switch" + else local IFS=';'; sed 's/;/; /g' <<<"${parts[*]}"; fi +} + +# sleep_of REPORT: what a sleep to memory is on the machine, deep (S3) or +# s2idle, and the other kinds it offers. +sleep_of() { + local l states mem chosen others + grep -qxF '== laptop ==' "$1" || { echo "not in this report"; return; } + l="$(section laptop "$1")" + states="$(field 'sleep states' <<<"$l")"; mem="$(field 'sleep mem' <<<"$l")" + grep -qw mem <<<"$states" || { echo "no sleep to memory (offered: ${states:-nothing})"; return; } + chosen="$(grep -o '\[[a-z0-9]*\]' <<<"$mem" | tr -d '[]')" + others="$(tr ' ' '\n' <<<"$mem" | grep -v '^\[' | grep . | paste -sd' ' -)" + if [[ -z "$chosen" ]]; then echo "mem offered, of no kind the report names" + elif [[ -z "$others" ]]; then echo "mem is ${chosen}, the only kind offered" + else echo "mem is ${chosen}; ${others} also offered"; fi +} + +# camera_of REPORT: a USB video device and what drives it; and a MIPI +# camera's parts: Intel's image processing unit and what drives it, and the +# sensors ACPI names, behind a Visual Sensing Controller where it has one. +camera_of() { + local l uvc ipu sensors parts=() + grep -qxF '== laptop ==' "$1" && grep -qxF '== usb ==' "$1" || { echo "not in this report"; return; } + l="$(section laptop "$1")" + # Video's control interface (0e/01), which uvcvideo binds, under its device's IDs. + uvc="$(section usb "$1" | awk ' + $2 != "interface" { id[$1] = $2; next } + $4 ~ /^0e\/01\// { d = $1; sub(/:.*/, "", d); if (!(d in seen)) { seen[d] = 1; n++; dev[n] = d; drv[n] = $6 } } + END { for (i = 1; i <= n; i++) print (dev[i] in id ? id[dev[i]] : "?"), (drv[i] == "-" ? "" : drv[i]) }')" + while read -r u d; do + [[ -z "$u" ]] || parts+=("USB video ${u}, $(took "$d")") + done <<<"$uvc" + ipu="$(awk '$1 == "camera" && $2 ~ /^ipu/ { print toupper($2), $4, ($6 == "-" ? "" : $6) }' <<<"$l")" + while read -r k i d; do + [[ -z "$k" ]] || parts+=("${k} ${i}, $(took "$d")") + done <<<"$ipu" + sensors="$(awk '$1 == "camera" && $2 == "acpi" && $4 == "sensor" && NF == 4 { sub(/:.*/, "", $3); print $3 }' <<<"$l" | sort -u | paste -sd' ' -)" + if [[ -n "$sensors" ]]; then + [[ "$sensors" == *" "* ]] && sensors="sensors ${sensors}" || sensors="sensor ${sensors}" + grep -q '^camera acpi .* visual sensing controller$' <<<"$l" && sensors+=", behind a Visual Sensing Controller" + parts+=("$sensors") + fi + if [[ "${#parts[@]}" -eq 0 ]]; then echo "no camera found" + else local IFS=';'; sed 's/;/; /g' <<<"${parts[*]}"; fi +} + +# bluetooth_of REPORT: each Bluetooth controller on USB (e0/01/01) and what +# drives it. +bluetooth_of() { + local bt out=() + grep -qxF '== usb ==' "$1" || { echo "not in this report"; return; } + bt="$(section usb "$1" | awk ' + $2 != "interface" { id[$1] = $2; next } + $4 == "e0/01/01" { d = $1; sub(/:.*/, "", d); if (!(d in seen)) { seen[d] = 1; n++; dev[n] = d; drv[n] = $6 } } + END { for (i = 1; i <= n; i++) print (dev[i] in id ? id[dev[i]] : "?"), (drv[i] == "-" ? "" : drv[i]) }')" + while read -r u d; do + [[ -z "$u" ]] || out+=("USB ${u}, $(took "$d")") + done <<<"$bt" + if [[ "${#out[@]}" -eq 0 ]]; then echo "no Bluetooth controller on USB" + else local IFS=';'; sed 's/;/; /g' <<<"${out[*]}"; fi +} + # examine REPORT: MACHINE, RELEASE and FACTS, with NOT_REPORTED and # NOT_CERTIFIED, each empty or one reason per line. examine() { @@ -119,6 +199,10 @@ examine() { fact display "${cards:-no DRM device}${outputs:+: ${outputs//;/; }}" [[ -n "$cards" && -n "$outputs" ]] || lacks "no display the compositor can use: no DRM device with a connected output" fact audio "$(audio_of "$r")" + fact laptop "$(laptop_of "$r")" + fact sleep "$(sleep_of "$r")" + fact camera "$(camera_of "$r")" + fact bluetooth "$(bluetooth_of "$r")" # A path out: the default route leaves by one of the machine's own # interfaces, and that one has a carrier. diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 600e3c5a..96058265 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -59,6 +59,12 @@ disks behind a driver that is a module, which boot.fragment must build in before 0000:00:14.3 class 028000 8086:a0f0 sub 8086:0074 rev 20 driver iwlwifi | Network controller | Intel Corporation Wi-Fi 6 AX201 == usb == +3-4 13d3:5411 speed 480 | Azurewave Integrated Camera +3-4:1.0 interface class 0e/01/00 driver uvcvideo +3-4:1.1 interface class 0e/02/00 driver uvcvideo +3-10 8087:0026 speed 12 | Intel Corp. AX201 Bluetooth +3-10:1.0 interface class e0/01/01 driver - +3-10:1.1 interface class e0/01/01 driver - == no driver == @@ -76,6 +82,13 @@ codec 10ec0287 sub 17aa22e6 codec 80862812 sub 80860101 nhlt: 2 endpoints, pdm 1, ssp 1 +== laptop == +backlight intel_backlight type raw max 19200 +power_supply AC type Mains online 1 +power_supply BAT0 type Battery scope System status Discharging capacity 87 technology Li-ion +sleep states: freeze mem +sleep mem: s2idle [deep] + == network == rfkill0 wlan phy0 soft 0 hard 0 in the net zone: @@ -84,6 +97,7 @@ in the net zone: default route by wlan0 == input == +"Lid Switch" | event1 "AT Translated Set 2 keyboard" | kbd event0 == modules == @@ -138,6 +152,44 @@ vary noaudio '/^== audio ==$/,/^$/d'; see "$W/noaudio.txt" grep -qF ' audio not in this report' <<<"$OUT" && grep -q '^ carries certified$' <<<"$OUT" \ && green "a report from before the section: said so, and no level asks for it" || red "a report with no audio section: $(grep -E 'audio|carries' <<<"$OUT")" +echo "-- a laptop's parts, its sleep, its camera and its Bluetooth" +# has NAME LINE: the tool's words on $W/NAME.txt hold LINE whole. +has() { see "$W/$1.txt"; if grep -qxF -- "$2" <<<"$OUT"; then green "$3"; else red "$3: $(grep -E '^ (laptop|sleep|camera|bluetooth) ' <<<"$OUT" | tr '\n' '|')"; fi; } +cp "$W/good.txt" "$W/as-is.txt" +has as-is " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); mains AC; lid switch" "the backlight and its kind, the battery, mains and the lid" +has as-is " sleep mem is deep; s2idle also offered" "what a sleep to memory is, and what else is offered" +has as-is " camera USB video 13d3:5411, driven by uvcvideo" "a USB camera, once, by its device's IDs" +has as-is " bluetooth USB 8087:0026, not driven" "a Bluetooth controller, once, and that nothing drives it" +vary mouse '/^power_supply BAT0/a power_supply hidpp_battery_0 type Battery scope Device status Discharging capacity 40 technology -' +has mouse " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); mains AC; lid switch" "a mouse's battery is not the machine's" +vary resting 's/status Discharging/status Not charging/' +has resting " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); mains AC; lid switch" "a status of two words leaves the kind" +vary desktop '/^backlight /d; /^power_supply /d; /^"Lid Switch"/d' +has desktop " laptop none of a laptop's parts: no backlight, battery or lid switch" "a desktop has none of them, and says so" +vary s2idle 's/^sleep mem: .*/sleep mem: [s2idle]/' +has s2idle " sleep mem is s2idle, the only kind offered" "s2idle with no deep beside it" +vary nomem 's/^sleep states: .*/sleep states: freeze disk/' +has nomem " sleep no sleep to memory (offered: freeze disk)" "no mem at all" +vary mipi '/^3-4/d +/^sleep mem:/a\ +camera ipu6 0000:00:05.0 8086:a75d driver -\ +camera acpi OVTI01A0:00 sensor\ +camera acpi INT3472:01 sensor power\ +camera acpi INTC1009:00 visual sensing controller' +has mipi " camera IPU6 8086:a75d, not driven; sensor OVTI01A0, behind a Visual Sensing Controller" "a MIPI camera: the IPU, its sensor and the controller between" +vary nocamera '/^3-4/d' +has nocamera " camera no camera found" "no camera, and says so" +vary btusb 's/^\(3-10:1\.[01] interface class e0\/01\/01 driver\) -$/\1 btusb/' +has btusb " bluetooth USB 8087:0026, driven by btusb" "and what drives it, once a driver does" +vary nobt '/^3-10/d' +has nobt " bluetooth no Bluetooth controller on USB" "no Bluetooth, and says so" +vary nolaptop '/^== laptop ==$/,/^$/d'; see "$W/nolaptop.txt" +if [[ "$(grep -cE '^ (laptop|sleep|camera) +not in this report$' <<<"$OUT")" -eq 3 ]] && grep -q '^ carries certified$' <<<"$OUT"; then + green "a report from before the section: said so, and no level asks for them" +else + red "a report with no laptop section: $(grep -E '^ (laptop|sleep|camera|carries) ' <<<"$OUT" | tr '\n' '|')" +fi + echo "-- each thing a certified listing needs" short medium 's/^booted: .*/booted: the usb medium/' "not taken on an installed system (booted: the usb medium)" short degraded 's/^state: .*/state: degraded/' "the state partition is not in use (state: degraded)" From 79f9e3faa1c3842490145154efdf9ce8e1571a59 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:45:52 -0700 Subject: [PATCH 24/54] The status page has a row for the hardware report and the hardware list, and what tests each The report, its acceptance item and the listing's check had no row, so the page did not say that a report's identifiers are struck under test, that the medium is booted to take one, or that CI holds the list to its reports. The row says which of its lines wait for the laptops. --- docs/status.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/status.md b/docs/status.md index b6356114..39b5c8d3 100644 --- a/docs/status.md +++ b/docs/status.md @@ -20,6 +20,7 @@ that ship, under QEMU with OVMF firmware. | Stage 05: hardened kernel | **tested**: the stage refuses a config that drops a fragment line, turns on an option a fragment sets off, or that kernel-hardening-checker faults beyond the accepted list; every VM suite boots the kernel (EFI stub, compiled-in command line with `CMDLINE_OVERRIDE`), `make integrity-test` its dm-init verity root, and `make zones-test` Landlock, seccomp, cgroup v2 and nftables' flow count on it. The count, which holds each routed zone to a share of the net zone's flow table, needs `NFT_CONNLIMIT` and so `NETFILTER_ADVANCED`, whose SCTP and UDP-Lite trackers the fragment keeps off | | Stage 06: install media and release payloads | **tested**: the firmware, install, integrity and update suites boot its USB image and ISO, whose kernels are signed with the build's Secure Boot key; the stage fails the build on any setuid bit or file capability its allowlists do not justify (`tools/tests/audit-setuid.sh`) and, in a development build given the statement key, checks that the image's trust anchor refuses a statement signed by the release key | | Firmware boot of the media | **tested**: `make media-smoke-usb` / `media-smoke-iso`, firmware discovery only (no `-kernel`, `-initrd`, `-append` or host filesystem; acceptance reads the recorded QEMU commands back) | +| Hardware reports and the hardware list | **tested**: `tools/tests/hwreport.sh` runs `kryptik-hwreport` on made-up machines, the hardware addresses, serial numbers and UUIDs it strikes among its checks: one of each kind of laptop sound (HD Audio alone, microphones on Intel's DSP, SoundWire, AMD's coprocessor, amplifiers) and a laptop's backlights, batteries, sleep states and camera parts, a battery's serial number never read; acceptance's `hardware-report` item boots the USB medium under OVMF, where an unprepared stick stays byte for byte what it was, a prepared one gets a report and then a second, a write-protected one none, and an install from it leaves none on the disk; `tools/check-hardware.sh` holds each report to the listing it carries, `reported` or `certified`, and `docs/hardware/list.tsv` to its reports (`tools/tests/check-hardware.sh`, which CI runs on the tree's own list). Its lines for a laptop's sound, parts, sleep, camera and Bluetooth wait for the tests on physical hardware, as QEMU has none of them | | Installation and the state partition | **tested**: `make install-test` (install, boot alone, reboot, cold boot, the firmware's entry for slot a naming the ESP by its PARTUUID, no release named for the empty slot b, refusals including an injected I/O error and a disk with 4096-byte logical sectors), `make state-test` (first-boot setup answered at the console, a cloned disk, ambiguous labels, a corrupt or missing state partition: the system boots degraded and says so; `kryptik state passphrase` refusing a wrong current passphrase and a new one under 8 characters, then changing it), the medium's console suite `tools/image/medium-shell-test.sh` (with a NIC attached, no net zone starts and the NIC stays down in zone 0; typed at the medium's root shell: a dry run that writes nothing, a plan naming what each of the disk's partitions holds, ERASE and the passphrase asked before the first write and a passphrase under 8 characters refused, the refusal of a partition and of a disk with a partition mounted, used as swap or held open by device-mapper, then the user guide's install, state header backup and recovery as it gives them), `make keyboard-test` (the layout a firmware variable names loads before the passphrase is asked; a name this release lacks is said, and the passphrase asked under `us`) | | Boot integrity | **tested**: `make media-smoke-secureboot`, `make media-refused-foreign-keys` (Microsoft keys refuse the medium), `make integrity-test` (a foreign-signed boot file refused and put back by `kryptik-recover --commit-slot`, a tampered root refused by dm-verity, recovery from the medium with state intact, also when the disk's ESP no longer mounts, and the state partition's header saved, wiped and put back) | | Zones, network and encrypted storage on the installed kernel | **tested**: `make zones-test`, running `zones-check.sh` (the net zone associating over a `mac80211_hwsim` radio; its DHCP client split into an unprivileged user's processes, with no capability in an empty root, and a helper that stays its root; its DNS server as nobody with only `CAP_NET_BIND_SERVICE`, and its clock query and update fetcher with no capability; its CPU limit read back, and each routed zone's share of its flow table measured; a NIC it renamed, readdressed or gave altnames, and a radio it left stations on, reaching the next net zone reset; the installed root's setuid bits and file capabilities against the allowlists, and every sysctl read back) and the compartment suites as root on the Kryptik kernel | From fd0e2646b9e007da2a71bf2ee04cfee02b02c1cc Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:51:13 -0700 Subject: [PATCH 25/54] 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 6d64947bc4afce7a6bfd6e662abd9179907efc81 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 00:51:18 -0700 Subject: [PATCH 26/54] The listing reads a battery's kind from its place, now that the status ends the line --- tools/check-hardware.sh | 5 ++--- tools/tests/check-hardware.sh | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index 0d8eb8d3..97f237fb 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -64,10 +64,9 @@ laptop_of() { l="$(section laptop "$1")" bl="$(awk '$1 == "backlight" { printf "%s%s (%s)", sep, $2, $4; sep = ", " }' <<<"$l")" [[ -z "$bl" ]] || parts+=("backlight ${bl}") - # A mouse's or a headset's battery is the device's, not the machine's. The - # kind is the line's last word, as a status may be two ("Not charging"). + # A mouse's or a headset's battery is the device's, not the machine's. bat="$(awk '$1 == "power_supply" && $4 == "Battery" && $6 == "System" { - printf "%s%s%s", sep, $2, ($NF == "-" ? "" : " (" $NF ")"); sep = ", " }' <<<"$l")" + printf "%s%s%s", sep, $2, ($10 == "-" ? "" : " (" $10 ")"); sep = ", " }' <<<"$l")" [[ -z "$bat" ]] || parts+=("battery ${bat}") mains="$(awk '$1 == "power_supply" && $4 == "Mains" { printf "%s%s", sep, $2; sep = " " }' <<<"$l")" [[ -z "$mains" ]] || parts+=("mains ${mains}") diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 96058265..7339c4d3 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -85,7 +85,7 @@ nhlt: 2 endpoints, pdm 1, ssp 1 == laptop == backlight intel_backlight type raw max 19200 power_supply AC type Mains online 1 -power_supply BAT0 type Battery scope System status Discharging capacity 87 technology Li-ion +power_supply BAT0 type Battery scope System capacity 87 technology Li-ion status Discharging sleep states: freeze mem sleep mem: s2idle [deep] @@ -160,10 +160,10 @@ has as-is " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); has as-is " sleep mem is deep; s2idle also offered" "what a sleep to memory is, and what else is offered" has as-is " camera USB video 13d3:5411, driven by uvcvideo" "a USB camera, once, by its device's IDs" has as-is " bluetooth USB 8087:0026, not driven" "a Bluetooth controller, once, and that nothing drives it" -vary mouse '/^power_supply BAT0/a power_supply hidpp_battery_0 type Battery scope Device status Discharging capacity 40 technology -' +vary mouse '/^power_supply BAT0/a power_supply hidpp_battery_0 type Battery scope Device capacity 40 technology - status Discharging' has mouse " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); mains AC; lid switch" "a mouse's battery is not the machine's" vary resting 's/status Discharging/status Not charging/' -has resting " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); mains AC; lid switch" "a status of two words leaves the kind" +has resting " laptop backlight intel_backlight (raw); battery BAT0 (Li-ion); mains AC; lid switch" "a status of two words, last on the line, leaves the kind" vary desktop '/^backlight /d; /^power_supply /d; /^"Lid Switch"/d' has desktop " laptop none of a laptop's parts: no backlight, battery or lid switch" "a desktop has none of them, and says so" vary s2idle 's/^sleep mem: .*/sleep mem: [s2idle]/' From 525733109ec41db6659d4b817ad1741bfef6bfd0 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:00:58 -0700 Subject: [PATCH 27/54] The hardware page lists what to try on a laptop beyond the report, and what each should do A listing's levels speak for installing, Secure Boot, the display and the network, and the status page names the laptop's parts that wait for physical hardware: the lid, the keys, the sound past HD Audio, the power source and the camera. The page now says how to try each in a minute and what the row's note should then say, so the October laptops are tried alike. --- docs/hardware.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/hardware.md b/docs/hardware.md index 04eb1fd7..02c49767 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -56,6 +56,28 @@ microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire or amplifiers in front of the speakers, each with what drives it. No level asks for any of it. +## Trying a laptop + +A listing's levels speak for installing, booting with Secure Boot, the display +and the network. Much of what a laptop adds, the virtual machines that test +each release cannot show ([status](status.md) says which parts wait for +physical hardware). On a laptop, try these too, on the installed system, and +let the row's note name what did not work: + +| try | what should happen | +| --- | --- | +| close the lid, then open it | the screen locks and the machine sleeps; it wakes to the lock asking for the state passphrase, and the zones carry on once it is typed | +| the power key, and `kryptik suspend` | the same; the listing's `sleep` line says whether that sleep is deep or s2idle | +| the brightness keys | the backlight steps up and down and never goes dark; of several, the kind the kernel prefers (firmware, then platform, then raw) | +| the volume, mute and microphone keys | the volume steps, mutes, and the microphone is taken back, under the lock too | +| a zone playing sound | the built-in speakers play it where the listing's `audio` line names an HD Audio codec and nothing beyond it | +| unplug the mains, then plug it in | the menu's Power line follows, with the battery's charge | +| `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | + +A laptop that wakes from the lid and from `kryptik suspend` is the proof that +the idle sleep, `~/.config/kryptik/lock-sleep`, waits for ([the user +guide](user-guide.md)). + ## Who may vouch Whoever ran it on a machine in front of them. A row names them and the day, From e28f699a5dcb3989e07f0a98b79fc85335b72aae Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:07:59 -0700 Subject: [PATCH 28/54] 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 1d828aa00cdb69b02865374ae89daf90c32599d2 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:08:31 -0700 Subject: [PATCH 29/54] The hardware listing says what confines a device's DMA, and each Thunderbolt domain's level A machine whose firmware hides or turns off its IOMMU boots as one that has it, every device's DMA reaching all of memory; the report now says which, and the listing's dma line reads it: on, with the IOMMUs and their groups by how they translate, or none, then each Thunderbolt domain's security level. No level asks for it. The suite's laptop has two IOMMUs and a Thunderbolt domain, and the cases vary it to none and to a report from before the line. --- docs/hardware.md | 11 ++++++++--- tools/check-hardware.sh | 10 ++++++++++ tools/tests/check-hardware.sh | 8 ++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 02c49767..9f9950a9 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -51,6 +51,11 @@ suspend, a camera), for Bluetooth, which 1.0 does not drive, for another firmware version, or for the same model sold with another network card: the report names the firmware version and every device, so a reader can compare. +A listing also says what confines a device's DMA: the IOMMUs in use, and +how a Thunderbolt port lets a device in. The kernel turns the IOMMU on and +strict, but a machine whose firmware hides it boots all the same, with every +device able to reach all of memory. + A listing also names the machine's sound: its HD Audio codecs, and whether microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire or amplifiers in front of the speakers, each with what drives it. No level asks @@ -107,9 +112,9 @@ a new report and a new row. `tools/check-hardware.sh REPORT` prints what a report shows and the level it carries, with each reason it falls short of `certified`. One line each says -what the machine's sound, laptop parts, sleep, camera and Bluetooth are, and -what drives them; no level asks for any of them, as 1.0 drives few of them on -a laptop. `--list` checks the +what confines a device's DMA, and what the machine's sound, laptop parts, +sleep, camera and Bluetooth are and what drives them; no level asks for any +of them yet. `--list` checks the whole list: every row's report is there and carries the level the row claims, was taken on the row's day and not in a virtual machine, no report holds a hardware address, a UUID or a serial number, and no report is without a row. diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index 97f237fb..d629962d 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -179,6 +179,16 @@ examine() { [[ "$sb" == on ]] || lacks "Secure Boot is ${sb:-unknown}" [[ "$lock" == confidentiality ]] || lacks "lockdown is ${lock:-unknown}, not confidentiality" fact microcode "$(section cpu "$r" | field microcode)" + # What confines a device's DMA, and Thunderbolt's level. No level asks for it. + local iommu tb dma + iommu="$(section firmware "$r" | field iommu)" + tb="$(section firmware "$r" | sed -n 's/^thunderbolt //p' | paste -sd';' -)" + case "$iommu" in + "") dma="not in this report" ;; + none) dma="none: nothing confines a device's DMA" ;; + *) dma="on: ${iommu}" ;; + esac + fact dma "${dma}${tb:+; thunderbolt ${tb//;/; thunderbolt }}" # The three lists under `missing`, each " (none)" or its entries. lacking="$(section missing "$r" | awk ' diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 7339c4d3..69e9e046 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -36,6 +36,8 @@ secure boot: on setup mode: 0 lockdown: confidentiality tpm: version 2 +iommu: dmar0 dmar1; groups: 17 DMA +thunderbolt domain0: security user, iommu protection 1 == cpu == model: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz @@ -190,6 +192,12 @@ else red "a report with no laptop section: $(grep -E '^ (laptop|sleep|camera|carries) ' <<<"$OUT" | tr '\n' '|')" fi +has as-is " dma on: dmar0 dmar1; groups: 17 DMA; thunderbolt domain0: security user, iommu protection 1" "what confines a device's DMA, and Thunderbolt's level" +vary nodma 's/^iommu: .*/iommu: none/; /^thunderbolt /d' +has nodma " dma none: nothing confines a device's DMA" "a machine whose IOMMU is off, said so" +vary olddma '/^iommu: /d; /^thunderbolt /d' +has olddma " dma not in this report" "a report from before the line" + echo "-- each thing a certified listing needs" short medium 's/^booted: .*/booted: the usb medium/' "not taken on an installed system (booted: the usb medium)" short degraded 's/^state: .*/state: degraded/' "the state partition is not in use (state: degraded)" From 1e90cf2be70f1693126371b3785aca4c3849696d Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:13:34 -0700 Subject: [PATCH 30/54] The laptop table has the touchpad: a tap clicks, two fingers scroll, typing stills it --- docs/hardware.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hardware.md b/docs/hardware.md index 9f9950a9..ebb926ff 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -73,6 +73,7 @@ let the row's note name what did not work: | --- | --- | | close the lid, then open it | the screen locks and the machine sleeps; it wakes to the lock asking for the state passphrase, and the zones carry on once it is typed | | the power key, and `kryptik suspend` | the same; the listing's `sleep` line says whether that sleep is deep or s2idle | +| the touchpad | the pointer moves, a tap clicks, two fingers scroll, and typing stills it for a moment | | the brightness keys | the backlight steps up and down and never goes dark; of several, the kind the kernel prefers (firmware, then platform, then raw) | | the volume, mute and microphone keys | the volume steps, mutes, and the microphone is taken back, under the lock too | | a zone playing sound | the built-in speakers play it where the listing's `audio` line names an HD Audio codec and nothing beyond it | From 8eb95d8b376c398e27a035a24b741a10210a575d Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:14:31 -0700 Subject: [PATCH 31/54] 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 c857a96ab3d740cdea78ff2d6ae69fa3352c383b Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:15:12 -0700 Subject: [PATCH 32/54] The listing's sleep line says whether an s2idle machine's last sleep reached the hardware's sleep state Read from the report's new sleep stats line: after a sleep, an s2idle machine whose hardware time asleep is 0 never got its SoC to its deepest state and drains its battery asleep. Deep sleep counts no such time and is not judged by it. The laptop table says to take the report after the lid's sleep for this. --- docs/hardware.md | 2 +- tools/check-hardware.sh | 18 +++++++++++++----- tools/tests/check-hardware.sh | 6 ++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index ebb926ff..e6b1d657 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -72,7 +72,7 @@ let the row's note name what did not work: | try | what should happen | | --- | --- | | close the lid, then open it | the screen locks and the machine sleeps; it wakes to the lock asking for the state passphrase, and the zones carry on once it is typed | -| the power key, and `kryptik suspend` | the same; the listing's `sleep` line says whether that sleep is deep or s2idle | +| the power key, and `kryptik suspend` | the same; the listing's `sleep` line says whether that sleep is deep or s2idle, and of a report taken after an s2idle sleep, whether that sleep reached the hardware's sleep state | | the touchpad | the pointer moves, a tap clicks, two fingers scroll, and typing stills it for a moment | | the brightness keys | the backlight steps up and down and never goes dark; of several, the kind the kernel prefers (firmware, then platform, then raw) | | the volume, mute and microphone keys | the volume steps, mutes, and the microphone is taken back, under the lock too | diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index d629962d..f35ac5cf 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -76,18 +76,26 @@ laptop_of() { } # sleep_of REPORT: what a sleep to memory is on the machine, deep (S3) or -# s2idle, and the other kinds it offers. +# s2idle, and the other kinds it offers; for s2idle, after a sleep, whether +# the last one reached the hardware's own sleep state. sleep_of() { - local l states mem chosen others + local l states mem chosen others out ok hw grep -qxF '== laptop ==' "$1" || { echo "not in this report"; return; } l="$(section laptop "$1")" states="$(field 'sleep states' <<<"$l")"; mem="$(field 'sleep mem' <<<"$l")" grep -qw mem <<<"$states" || { echo "no sleep to memory (offered: ${states:-nothing})"; return; } chosen="$(grep -o '\[[a-z0-9]*\]' <<<"$mem" | tr -d '[]')" others="$(tr ' ' '\n' <<<"$mem" | grep -v '^\[' | grep . | paste -sd' ' -)" - if [[ -z "$chosen" ]]; then echo "mem offered, of no kind the report names" - elif [[ -z "$others" ]]; then echo "mem is ${chosen}, the only kind offered" - else echo "mem is ${chosen}; ${others} also offered"; fi + if [[ -z "$chosen" ]]; then out="mem offered, of no kind the report names" + elif [[ -z "$others" ]]; then out="mem is ${chosen}, the only kind offered" + else out="mem is ${chosen}; ${others} also offered"; fi + # Deep sleep counts no hardware time, so only s2idle's last sleep is judged. + read -r ok hw <<<"$(awk '$1 == "sleep" && $2 == "stats:" { print $3, $9 }' <<<"$l")" + if [[ "$chosen" == s2idle && "${ok:-}" =~ ^[0-9]+$ && "$ok" -gt 0 && "${hw:-}" =~ ^[0-9]+$ ]]; then + if [[ "$hw" -eq 0 ]]; then out+="; its last sleep never reached the hardware's sleep state, which drains the battery" + else out+="; its last sleep kept the hardware asleep $(( hw / 1000000 )) s"; fi + fi + echo "$out" } # camera_of REPORT: a USB video device and what drives it; and a MIPI diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 69e9e046..349fa71b 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -170,6 +170,12 @@ vary desktop '/^backlight /d; /^power_supply /d; /^"Lid Switch"/d' has desktop " laptop none of a laptop's parts: no backlight, battery or lid switch" "a desktop has none of them, and says so" vary s2idle 's/^sleep mem: .*/sleep mem: [s2idle]/' has s2idle " sleep mem is s2idle, the only kind offered" "s2idle with no deep beside it" +vary s2slept 's/^sleep mem: .*/sleep mem: [s2idle]/; /^sleep mem:/a sleep stats: 2 successful, 0 failed, hardware asleep 1800000000 us last, 3600000000 us in all' +has s2slept " sleep mem is s2idle, the only kind offered; its last sleep kept the hardware asleep 1800 s" "an s2idle sleep that reached the hardware's state, and for how long" +vary s2awake 's/^sleep mem: .*/sleep mem: [s2idle]/; /^sleep mem:/a sleep stats: 1 successful, 0 failed, hardware asleep 0 us last, 0 us in all' +has s2awake " sleep mem is s2idle, the only kind offered; its last sleep never reached the hardware's sleep state, which drains the battery" "one that never got there" +vary deepslept '/^sleep mem:/a sleep stats: 1 successful, 0 failed, hardware asleep 0 us last, 0 us in all' +has deepslept " sleep mem is deep; s2idle also offered" "deep sleep, which counts no hardware time, is not judged by it" vary nomem 's/^sleep states: .*/sleep states: freeze disk/' has nomem " sleep no sleep to memory (offered: freeze disk)" "no mem at all" vary mipi '/^3-4/d From a8952cf209f5e4229d4976a3a8a3b5bded6f722a Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:17:03 -0700 Subject: [PATCH 33/54] 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 9005471bb95d0a42fe6597e8d578e1ac40109c25 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:17:08 -0700 Subject: [PATCH 34/54] The listing claims a draining s2idle sleep only where a power controller driver counted it --- tools/check-hardware.sh | 11 +++++++---- tools/tests/check-hardware.sh | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index f35ac5cf..f49e8c2d 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -79,7 +79,7 @@ laptop_of() { # s2idle, and the other kinds it offers; for s2idle, after a sleep, whether # the last one reached the hardware's own sleep state. sleep_of() { - local l states mem chosen others out ok hw + local l states mem chosen others out ok hw pmc grep -qxF '== laptop ==' "$1" || { echo "not in this report"; return; } l="$(section laptop "$1")" states="$(field 'sleep states' <<<"$l")"; mem="$(field 'sleep mem' <<<"$l")" @@ -89,11 +89,14 @@ sleep_of() { if [[ -z "$chosen" ]]; then out="mem offered, of no kind the report names" elif [[ -z "$others" ]]; then out="mem is ${chosen}, the only kind offered" else out="mem is ${chosen}; ${others} also offered"; fi - # Deep sleep counts no hardware time, so only s2idle's last sleep is judged. + # Deep sleep counts no hardware time, so only s2idle's last sleep is judged, and a time of 0 + # says something only where a power controller driver counts it. read -r ok hw <<<"$(awk '$1 == "sleep" && $2 == "stats:" { print $3, $9 }' <<<"$l")" + pmc="$(field 'power controller' <<<"$l")" if [[ "$chosen" == s2idle && "${ok:-}" =~ ^[0-9]+$ && "$ok" -gt 0 && "${hw:-}" =~ ^[0-9]+$ ]]; then - if [[ "$hw" -eq 0 ]]; then out+="; its last sleep never reached the hardware's sleep state, which drains the battery" - else out+="; its last sleep kept the hardware asleep $(( hw / 1000000 )) s"; fi + if [[ "$hw" -gt 0 ]]; then out+="; its last sleep kept the hardware asleep $(( hw / 1000000 )) s" + elif [[ -n "$pmc" && "$pmc" != none ]]; then out+="; its last sleep never reached the hardware's sleep state, which drains the battery" + else out+="; no power controller driver counts how long its sleeps reach the hardware's sleep state"; fi fi echo "$out" } diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 349fa71b..6ef6f6a6 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -172,8 +172,10 @@ vary s2idle 's/^sleep mem: .*/sleep mem: [s2idle]/' has s2idle " sleep mem is s2idle, the only kind offered" "s2idle with no deep beside it" vary s2slept 's/^sleep mem: .*/sleep mem: [s2idle]/; /^sleep mem:/a sleep stats: 2 successful, 0 failed, hardware asleep 1800000000 us last, 3600000000 us in all' has s2slept " sleep mem is s2idle, the only kind offered; its last sleep kept the hardware asleep 1800 s" "an s2idle sleep that reached the hardware's state, and for how long" -vary s2awake 's/^sleep mem: .*/sleep mem: [s2idle]/; /^sleep mem:/a sleep stats: 1 successful, 0 failed, hardware asleep 0 us last, 0 us in all' -has s2awake " sleep mem is s2idle, the only kind offered; its last sleep never reached the hardware's sleep state, which drains the battery" "one that never got there" +vary s2awake 's/^sleep mem: .*/sleep mem: [s2idle]/; /^sleep mem:/a sleep stats: 1 successful, 0 failed, hardware asleep 0 us last, 0 us in all\npower controller: amd_pmc' +has s2awake " sleep mem is s2idle, the only kind offered; its last sleep never reached the hardware's sleep state, which drains the battery" "one that never got there, by the power controller's count" +vary s2uncounted 's/^sleep mem: .*/sleep mem: [s2idle]/; /^sleep mem:/a sleep stats: 1 successful, 0 failed, hardware asleep 0 us last, 0 us in all\npower controller: none' +has s2uncounted " sleep mem is s2idle, the only kind offered; no power controller driver counts how long its sleeps reach the hardware's sleep state" "a time no driver counts proves nothing" vary deepslept '/^sleep mem:/a sleep stats: 1 successful, 0 failed, hardware asleep 0 us last, 0 us in all' has deepslept " sleep mem is deep; s2idle also offered" "deep sleep, which counts no hardware time, is not judged by it" vary nomem 's/^sleep states: .*/sleep states: freeze disk/' From d7f3f49a6a05b671c142f7264346195a45fbd653 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:19:09 -0700 Subject: [PATCH 35/54] 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 b9a23e51f23b1261b41720af48bacde5c23d968e Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:19:13 -0700 Subject: [PATCH 36/54] The listing's dma line says when the IOMMU is on but every group is passthrough Groups whose type is identity translate nothing, so an IOMMU with no DMA or DMA-FQ group confines no device: the line says so instead of 'on'. Strict is the kernel's default here, so identity comes only from a command line or a quirk. --- tools/check-hardware.sh | 3 +++ tools/tests/check-hardware.sh | 2 ++ 2 files changed, 5 insertions(+) diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index f49e8c2d..b607fede 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -194,9 +194,12 @@ examine() { local iommu tb dma iommu="$(section firmware "$r" | field iommu)" tb="$(section firmware "$r" | sed -n 's/^thunderbolt //p' | paste -sd';' -)" + # Groups that are all identity (passthrough) translate nothing, IOMMU or not. case "$iommu" in "") dma="not in this report" ;; none) dma="none: nothing confines a device's DMA" ;; + *"; groups: "*DMA*) dma="on: ${iommu}" ;; + *"; groups: "*) dma="on, but in passthrough: no group translates a device's DMA (${iommu})" ;; *) dma="on: ${iommu}" ;; esac fact dma "${dma}${tb:+; thunderbolt ${tb//;/; thunderbolt }}" diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index 6ef6f6a6..ce66be8f 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -203,6 +203,8 @@ fi has as-is " dma on: dmar0 dmar1; groups: 17 DMA; thunderbolt domain0: security user, iommu protection 1" "what confines a device's DMA, and Thunderbolt's level" vary nodma 's/^iommu: .*/iommu: none/; /^thunderbolt /d' has nodma " dma none: nothing confines a device's DMA" "a machine whose IOMMU is off, said so" +vary passthrough 's/^iommu: .*/iommu: dmar0; groups: 12 identity/; /^thunderbolt /d' +has passthrough " dma on, but in passthrough: no group translates a device's DMA (dmar0; groups: 12 identity)" "an IOMMU whose every group is identity, said so" vary olddma '/^iommu: /d; /^thunderbolt /d' has olddma " dma not in this report" "a report from before the line" From 72489ac0d9c4f230702d9d534df87df71c322459 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 01:21:12 -0700 Subject: [PATCH 37/54] The dma line calls an IOMMU passthrough only for identity groups, not for a VFIO or a detaching one --- tools/check-hardware.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index b607fede..ee088897 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -199,7 +199,7 @@ examine() { "") dma="not in this report" ;; none) dma="none: nothing confines a device's DMA" ;; *"; groups: "*DMA*) dma="on: ${iommu}" ;; - *"; groups: "*) dma="on, but in passthrough: no group translates a device's DMA (${iommu})" ;; + *"; groups: "*identity*) dma="on, but in passthrough: no group translates a device's DMA (${iommu})" ;; *) dma="on: ${iommu}" ;; esac fact dma "${dma}${tb:+; thunderbolt ${tb//;/; thunderbolt }}" From 06a61f9eddb09966b4fe0d96b532d0435ff6809f Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:03:36 -0700 Subject: [PATCH 38/54] 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 99f102a842befe3036a8f41382599e450d130917 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:04:41 -0700 Subject: [PATCH 39/54] A listing says whether a program built for shadow stacks runs with one, and a laptop's tries include Firefox A program built for shadow stacks runs with one only on a processor that has them (Intel's from Tiger Lake, AMD's from Zen 3), and the virtual machines that test each release have none. So only a real machine shows whether Firefox, built for them and loading libraries built without, starts there. check-hardware.sh's listing gains a shstk line from the report's "shadow stacks" line: yes, no, or not in this report. No level asks for it. docs/hardware.md says what the line means, and its "Trying a laptop" table gains a row: Firefox in untrusted from the menu, where a failure on a machine whose listing says yes names SHSTK. Checked: tools/tests/check-hardware.sh's report fixture, an i7-1165G7, says yes; varied to no, and with the line removed, the listing says each. The expected lines were held against fact()'s padding. bash -n on both scripts. --- docs/hardware.md | 7 +++++++ tools/check-hardware.sh | 5 +++++ tools/tests/check-hardware.sh | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/docs/hardware.md b/docs/hardware.md index e6b1d657..64e30882 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -61,6 +61,12 @@ microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire or amplifiers in front of the speakers, each with what drives it. No level asks for any of it. +Its `shstk` line says whether a program built for shadow stacks runs with +one there: the kernel turns them on where the processor has them, Intel's +from Tiger Lake and AMD's from Zen 3. The virtual machines that test each +release have none, so only such a machine shows how a program built for +them fares with a library built without. No level asks for it. + ## Trying a laptop A listing's levels speak for installing, booting with Secure Boot, the display @@ -79,6 +85,7 @@ let the row's note name what did not work: | a zone playing sound | the built-in speakers play it where the listing's `audio` line names an HD Audio codec and nothing beyond it | | unplug the mains, then plug it in | the menu's Power line follows, with the battery's charge | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | +| Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | A laptop that wakes from the lid and from `kryptik suspend` is the proof that the idle sleep, `~/.config/kryptik/lock-sleep`, waits for ([the user diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index ee088897..6b2906ca 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -190,6 +190,11 @@ examine() { [[ "$sb" == on ]] || lacks "Secure Boot is ${sb:-unknown}" [[ "$lock" == confidentiality ]] || lacks "lockdown is ${lock:-unknown}, not confidentiality" fact microcode "$(section cpu "$r" | field microcode)" + # Whether a program built for shadow stacks runs with one, which only a machine shows. + # No level asks for it. + local shstk + shstk="$(section cpu "$r" | field 'shadow stacks')" + fact shstk "${shstk:-not in this report}" # What confines a device's DMA, and Thunderbolt's level. No level asks for it. local iommu tb dma iommu="$(section firmware "$r" | field iommu)" diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index ce66be8f..abf9f017 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -44,6 +44,7 @@ model: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz vendor, family/model/stepping: GenuineIntel, 6/140/1 microcode: 0xa4 threads running: 4 +shadow stacks: yes present: 0-7, online: 0-3, smt: off microcode: Current revision: 0x000000a4 memory: 15902 MiB @@ -207,6 +208,11 @@ vary passthrough 's/^iommu: .*/iommu: dmar0; groups: 12 identity/; /^thunderbolt has passthrough " dma on, but in passthrough: no group translates a device's DMA (dmar0; groups: 12 identity)" "an IOMMU whose every group is identity, said so" vary olddma '/^iommu: /d; /^thunderbolt /d' has olddma " dma not in this report" "a report from before the line" +has as-is " shstk yes" "whether a program built for shadow stacks runs with one" +vary noshstk 's/^shadow stacks: yes$/shadow stacks: no/' +has noshstk " shstk no" "and a machine where none does, said so" +vary oldshstk '/^shadow stacks: /d' +has oldshstk " shstk not in this report" "a report from before the line" echo "-- each thing a certified listing needs" short medium 's/^booted: .*/booted: the usb medium/' "not taken on an installed system (booted: the usb medium)" From 79c156bc1c3de3c1b8000c0b2cb61630eb2842ce Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:48:20 -0700 Subject: [PATCH 40/54] 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 5c0d246c89002584ef0106d3cb89198740ade38d Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:49:22 -0700 Subject: [PATCH 41/54] A listing names SoundWire behind AMD's coprocessor, the coprocessor's to drive The report now lists SoundWire parts under AMD's coprocessor as well as under HD Audio. check-hardware's audio line credited every SoundWire part to SOF, Intel's driver, which does not drive AMD's. audio_of keeps the parts under HD Audio as SOF's. Parts under the coprocessor's PCI address join its own part of the line: "SoundWire PARTS behind AMD's coprocessor", driven or not as the coprocessor is. docs/hardware.md says a listing names both. Checked: tools/tests/check-hardware.sh gains amdsdw, a report whose audio is a revision 70 coprocessor with an RT722 on its SoundWire, and wants "AMD's coprocessor (rev 70) with no microphones its ACPI names; SoundWire 025d:0722 behind AMD's coprocessor, not driven". audio_of was also run on that report and on an Intel SoundWire one, which still says "SoundWire 025d:1316, not driven". bash -n on both scripts. --- docs/hardware.md | 6 +++--- tools/check-hardware.sh | 8 ++++++-- tools/tests/check-hardware.sh | 3 +++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 64e30882..0b0d8772 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -57,9 +57,9 @@ strict, but a machine whose firmware hides it boots all the same, with every device able to reach all of memory. A listing also names the machine's sound: its HD Audio codecs, and whether -microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire or -amplifiers in front of the speakers, each with what drives it. No level asks -for any of it. +microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire +(under HD Audio or behind AMD's coprocessor) or amplifiers in front of the +speakers, each with what drives it. No level asks for any of it. Its `shstk` line says whether a program built for shadow stacks runs with one there: the kernel turns them on where the processor has them, Intel's diff --git a/tools/check-hardware.sh b/tools/check-hardware.sh index 6b2906ca..35152942 100755 --- a/tools/check-hardware.sh +++ b/tools/check-hardware.sh @@ -23,7 +23,7 @@ took() { if [[ -n "$1" ]]; then echo "driven by $1"; else echo "not driven"; fi; # audio_of REPORT: the machine's sound in one line, by the kinds of # docs/design/laptop-audio.md, each with what drives it. No level asks for any. audio_of() { - local a sof codecs sdw acp names amps drv parts=() + local a sof codecs sdw asdw acpdev acp names amps drv parts=() a="$(section audio "$1")" [[ -n "$a" ]] || { echo "not in this report"; return; } # SOF in place of snd-hda-intel drives what sits behind Intel's DSP. @@ -37,7 +37,9 @@ audio_of() { if awk '$1 == "nhlt:" && $5 + 0 > 0 { found = 1 } END { exit !found }' <<<"$a"; then parts+=("digital microphones on Intel's DSP, $(took "$sof")") fi - sdw="$(awk '$1 == "soundwire" && $5 == "part" { print $6 }' <<<"$a" | paste -sd' ' -)" + # SoundWire under HD Audio is SOF's to drive; under AMD's coprocessor, the coprocessor's driver's. + acpdev="$(awk '$1 == "acp" { print $2; exit }' <<<"$a")" + sdw="$(awk -v acp="$acpdev" '$1 == "soundwire" && $5 == "part" && $2 != acp { print $6 }' <<<"$a" | paste -sd' ' -)" [[ -z "$sdw" ]] || parts+=("SoundWire ${sdw}, $(took "$sof")") acp="$(awk '$1 == "acp" { print $7, ($9 == "-" ? "" : $9); exit }' <<<"$a")" if [[ -n "$acp" ]]; then @@ -45,6 +47,8 @@ audio_of() { drv="${acp#* }" if [[ -n "$names" ]]; then parts+=("microphones on AMD's coprocessor (rev ${acp%% *}, ${names// /, }), $(took "$drv")") else parts+=("AMD's coprocessor (rev ${acp%% *}) with no microphones its ACPI names"); fi + asdw="$(awk -v acp="$acpdev" '$1 == "soundwire" && $5 == "part" && $2 == acp { print $6 }' <<<"$a" | paste -sd' ' -)" + [[ -z "$asdw" ]] || parts+=("SoundWire ${asdw} behind AMD's coprocessor, $(took "$drv")") fi amps="$(awk '$1 == "amplifier" { sub(/:.*/, "", $2); print $2 }' <<<"$a" | sort -u | paste -sd' ' -)" if [[ -n "$amps" ]]; then diff --git a/tools/tests/check-hardware.sh b/tools/tests/check-hardware.sh index abf9f017..29bc5534 100755 --- a/tools/tests/check-hardware.sh +++ b/tools/tests/check-hardware.sh @@ -151,6 +151,9 @@ grep -qF " audio HD Audio codec 10ec0287; digital microphones on Intel's D vary sof 's/driver snd_hda_intel$/driver sof-audio-pci-intel-tgl/'; see "$W/sof.txt" grep -qF "digital microphones on Intel's DSP, driven by sof-audio-pci-intel-tgl" <<<"$OUT" \ && green "and what drives it, once a driver does" || red "audio line with SOF: $(grep audio <<<"$OUT")" +vary amdsdw 's/^controller 0000:00:1f\.3 .*$/acp 0000:04:00.5 class 048000 1022:15e2 rev 70 driver -\nsoundwire 0000:04:00.5 link 0 part 025d:0722/; /^codec /d; /^nhlt: /d'; see "$W/amdsdw.txt" +grep -qF " audio AMD's coprocessor (rev 70) with no microphones its ACPI names; SoundWire 025d:0722 behind AMD's coprocessor, not driven" <<<"$OUT" \ + && green "SoundWire behind AMD's coprocessor, the coprocessor's to drive" || red "audio line, AMD SoundWire: $(grep audio <<<"$OUT")" vary noaudio '/^== audio ==$/,/^$/d'; see "$W/noaudio.txt" grep -qF ' audio not in this report' <<<"$OUT" && grep -q '^ carries certified$' <<<"$OUT" \ && green "a report from before the section: said so, and no level asks for it" || red "a report with no audio section: $(grep -E 'audio|carries' <<<"$OUT")" From 5731ccd6cf277c322606fde2847b07370aee9c6f Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:56:04 -0700 Subject: [PATCH 42/54] The laptop's tries include a weak charger, an AMD laptop's sleep, the built-in SD reader and a USB headset Four changes made for laptops can only be shown on one, so the "Trying a laptop" table names each, with what should happen: - a charger too weak for the machine, where the Power line says on battery though plugged in and the warnings come; - on an AMD laptop that sleeps to idle, a two-minute sleep that wakes to the lock rather than a reset, as the watchdog no longer feeds AMD's TCO timer there; - an SD card in the laptop's own reader, which opens the window that asks which zone gets it; - a USB headset, which plays a zone's sound and records once granted. Docs only. --- docs/hardware.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/hardware.md b/docs/hardware.md index 0b0d8772..612423d6 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -84,6 +84,10 @@ let the row's note name what did not work: | the volume, mute and microphone keys | the volume steps, mutes, and the microphone is taken back, under the lock too | | a zone playing sound | the built-in speakers play it where the listing's `audio` line names an HD Audio codec and nothing beyond it | | unplug the mains, then plug it in | the menu's Power line follows, with the battery's charge | +| a charger too weak for the machine, such as a phone's USB-C one | the Power line says on battery though plugged in, and the warnings at 10% and 5% come as they do unplugged | +| on an AMD laptop whose `sleep` line says s2idle, a sleep of two minutes | it wakes to the lock, not to a fresh boot: the watchdog does not feed AMD's TCO timer there, as it would count through the sleep | +| an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | +| a USB headset, and `c` and a zone's number in the menu | it plays what the zone plays, and records there once granted | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | | Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | From fd0c71bad832a632398d235c13f25d572a48e4fc Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:59:11 -0700 Subject: [PATCH 43/54] 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 41d1904c60d99b867a9ce1f6fbea80b51f4d98a7 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 04:59:31 -0700 Subject: [PATCH 44/54] The hardware page names the report's watchdog lines The report now lists each watchdog, whether it is fed and whether it reset the boot before. The page's account of a report names them, and the AMD laptop's sleep row says the report's line for AMD's TCO timer reads inactive there. Docs only. --- docs/hardware.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 612423d6..626dd07d 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -12,8 +12,9 @@ This page is about what was seen to work. ## A report `kryptik-hwreport` prints what a machine is made of and what the image lacks -for it: the maker and model, the firmware's Secure Boot state, the processor -and its microcode, every PCI and USB device with the driver that took it, the +for it: the maker and model, the firmware's Secure Boot state, the watchdogs +and whether one reset the boot before, the processor and its microcode, every +PCI and USB device with the driver that took it, the disks and the drivers under them, the display, the sound hardware, a laptop's backlights, batteries, sleep states and camera parts, network and input devices, the loaded modules and the kernel log. Its `missing` section is @@ -85,7 +86,7 @@ let the row's note name what did not work: | a zone playing sound | the built-in speakers play it where the listing's `audio` line names an HD Audio codec and nothing beyond it | | unplug the mains, then plug it in | the menu's Power line follows, with the battery's charge | | a charger too weak for the machine, such as a phone's USB-C one | the Power line says on battery though plugged in, and the warnings at 10% and 5% come as they do unplugged | -| on an AMD laptop whose `sleep` line says s2idle, a sleep of two minutes | it wakes to the lock, not to a fresh boot: the watchdog does not feed AMD's TCO timer there, as it would count through the sleep | +| on an AMD laptop whose `sleep` line says s2idle, a sleep of two minutes | it wakes to the lock, not to a fresh boot: the watchdog does not feed AMD's TCO timer there, as it would count through the sleep, and the report's line for it says inactive | | an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | | a USB headset, and `c` and a zone's number in the menu | it plays what the zone plays, and records there once granted | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | From 922c127eeb9fc2486ff6fc15081f7a9423165994 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 05:41:51 -0700 Subject: [PATCH 45/54] A laptop's tries include an external monitor on a second GPU dwl draws with pixman into dumb buffers, without Mesa. On a laptop whose external ports hang off a second GPU, an AMD one beside the built-in, those outputs show the desktop only through wlroots copying each frame between the GPUs. QEMU has one card, so nothing tests it. The "Trying a laptop" table gains the row; a dark monitor's note names both GPUs, as the listing's display line does. NVIDIA's ports have no driver at all, which decisions.md already says. Docs only. --- docs/hardware.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hardware.md b/docs/hardware.md index 626dd07d..2e24e9d7 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -89,6 +89,7 @@ let the row's note name what did not work: | on an AMD laptop whose `sleep` line says s2idle, a sleep of two minutes | it wakes to the lock, not to a fresh boot: the watchdog does not feed AMD's TCO timer there, as it would count through the sleep, and the report's line for it says inactive | | an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | | a USB headset, and `c` and a zone's number in the menu | it plays what the zone plays, and records there once granted | +| an external monitor on a laptop with two GPUs, an AMD one beside the built-in | it shows the desktop, whose frames wlroots copies to the second GPU's ports; dark, the row's note names both, as the listing's `display` line does | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | | Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | From 0bc611119c853dcb7ca5d12c039e034eaddba054 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 06:08:31 -0700 Subject: [PATCH 46/54] A laptop's tries include a dock and the boot passphrase on the built-in keyboard 1.0 builds no Thunderbolt or USB4 driver, so a Thunderbolt or USB4 dock's own USB ports and Ethernet stay dark, while a monitor over USB-C shows where the laptop drives the port itself. The state passphrase is asked before any module loads, so a built-in keyboard on I2C types nothing there. The "Trying a laptop" table gains a row for each. The DMA paragraph says a listing names Thunderbolt's level only on a build with its driver. Docs only. --- docs/hardware.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index 2e24e9d7..827313e5 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -53,9 +53,10 @@ firmware version, or for the same model sold with another network card: the report names the firmware version and every device, so a reader can compare. A listing also says what confines a device's DMA: the IOMMUs in use, and -how a Thunderbolt port lets a device in. The kernel turns the IOMMU on and -strict, but a machine whose firmware hides it boots all the same, with every -device able to reach all of memory. +how a Thunderbolt port lets a device in, on a build with a Thunderbolt +driver, which 1.0 is not. The kernel turns the IOMMU on and strict, but a +machine whose firmware hides it boots all the same, with every device able +to reach all of memory. A listing also names the machine's sound: its HD Audio codecs, and whether microphones sit on Intel's DSP or AMD's coprocessor, codecs on SoundWire @@ -90,6 +91,8 @@ let the row's note name what did not work: | an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | | a USB headset, and `c` and a zone's number in the menu | it plays what the zone plays, and records there once granted | | an external monitor on a laptop with two GPUs, an AMD one beside the built-in | it shows the desktop, whose frames wlroots copies to the second GPU's ports; dark, the row's note names both, as the listing's `display` line does | +| a USB-C or Thunderbolt dock: its monitor, its USB ports and its Ethernet | the monitor shows where the laptop drives USB-C itself; a Thunderbolt or USB4 dock's own USB ports and Ethernet stay dark, as 1.0 builds no Thunderbolt driver, and the row's note says which worked | +| the state passphrase at boot, typed on the built-in keyboard | it unlocks; a built-in keyboard that types nothing there is one on I2C, and a USB keyboard is the way through | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | | Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | From 4bf6c78ac9c46553bfa63acea939c066c167d3b8 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:08:59 -0700 Subject: [PATCH 47/54] 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 d932a3da5aabd5e4e466fd42eb9280c8dc254fc0 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:10:53 -0700 Subject: [PATCH 48/54] A laptop's tries include NVIDIA graphics beside the built-in, and undocking shut The hardware list says what a listing's gpu lines give: each graphics controller's power state. An NVIDIA chip that nothing here drives stays in D0 beside Intel's or AMD's, and only such a laptop shows what that costs its battery. Two rows join "Trying a laptop". On a laptop with NVIDIA's graphics beside the built-in, the desktop shows on the built-in screen, a port the NVIDIA chip drives stays dark, and the gpu line says whether the chip stayed powered. A laptop closed on its dock sleeps within seconds of the dock being unplugged, which the compositor's undock check does. Checked: check-hardware.sh reads only the display section's card lines, so the report's new gpu lines leave every listing as it was. --- docs/hardware.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/hardware.md b/docs/hardware.md index 827313e5..474e2272 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -15,7 +15,8 @@ This page is about what was seen to work. for it: the maker and model, the firmware's Secure Boot state, the watchdogs and whether one reset the boot before, the processor and its microcode, every PCI and USB device with the driver that took it, the -disks and the drivers under them, the display, the sound hardware, a laptop's +disks and the drivers under them, the display and each graphics controller's +power state, the sound hardware, a laptop's backlights, batteries, sleep states and camera parts, network and input devices, the loaded modules and the kernel log. Its `missing` section is the part to act on: the firmware the kernel asked for and did not find, each a @@ -69,6 +70,12 @@ from Tiger Lake and AMD's from Zen 3. The virtual machines that test each release have none, so only such a machine shows how a program built for them fares with a library built without. No level asks for it. +Its `gpu` lines give each graphics controller's power state. Nothing here +drives NVIDIA's graphics, and the kernel lets a device nothing drives sleep +only once runtime PM is allowed it (`control auto`), so beside Intel's or +AMD's on a laptop an NVIDIA chip stays powered, in D0. Only such a laptop +shows what that costs its battery. No level asks for it. + ## Trying a laptop A listing's levels speak for installing, booting with Secure Boot, the display @@ -91,7 +98,9 @@ let the row's note name what did not work: | an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | | a USB headset, and `c` and a zone's number in the menu | it plays what the zone plays, and records there once granted | | an external monitor on a laptop with two GPUs, an AMD one beside the built-in | it shows the desktop, whose frames wlroots copies to the second GPU's ports; dark, the row's note names both, as the listing's `display` line does | +| a laptop with NVIDIA's graphics beside Intel's or AMD's | the desktop shows on the built-in screen; a monitor on a port the NVIDIA chip drives stays dark, as 1.0 drives none, and the listing's `gpu` line says whether that chip stayed powered | | a USB-C or Thunderbolt dock: its monitor, its USB ports and its Ethernet | the monitor shows where the laptop drives USB-C itself; a Thunderbolt or USB4 dock's own USB ports and Ethernet stay dark, as 1.0 builds no Thunderbolt driver, and the row's note says which worked | +| closed on its dock, then the dock unplugged | it sleeps within seconds, as a lid closed undocked does | | the state passphrase at boot, typed on the built-in keyboard | it unlocks; a built-in keyboard that types nothing there is one on I2C, and a USB keyboard is the way through | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | | Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | From eb4a5655fc550f4da1ea1254a3fcd147b3e46619 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:34:23 -0700 Subject: [PATCH 49/54] A laptop's headset try plugs the headset in while a zone plays The mixer now moves a playing stream to a USB card plugged in under it (#604), so the try for a USB headset plugs it in while a zone plays: it takes the sound within a second, then records once granted. A headset plugged in during a call is how the guide says calls work. --- docs/hardware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hardware.md b/docs/hardware.md index 474e2272..c844a8f0 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -96,7 +96,7 @@ let the row's note name what did not work: | a charger too weak for the machine, such as a phone's USB-C one | the Power line says on battery though plugged in, and the warnings at 10% and 5% come as they do unplugged | | on an AMD laptop whose `sleep` line says s2idle, a sleep of two minutes | it wakes to the lock, not to a fresh boot: the watchdog does not feed AMD's TCO timer there, as it would count through the sleep, and the report's line for it says inactive | | an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | -| a USB headset, and `c` and a zone's number in the menu | it plays what the zone plays, and records there once granted | +| a USB headset, plugged in while a zone plays, then `c` and a zone's number in the menu | it takes what the zone plays within a second, and records there once granted | | an external monitor on a laptop with two GPUs, an AMD one beside the built-in | it shows the desktop, whose frames wlroots copies to the second GPU's ports; dark, the row's note names both, as the listing's `display` line does | | a laptop with NVIDIA's graphics beside Intel's or AMD's | the desktop shows on the built-in screen; a monitor on a port the NVIDIA chip drives stays dark, as 1.0 drives none, and the listing's `gpu` line says whether that chip stayed powered | | a USB-C or Thunderbolt dock: its monitor, its USB ports and its Ethernet | the monitor shows where the laptop drives USB-C itself; a Thunderbolt or USB4 dock's own USB ports and Ethernet stay dark, as 1.0 builds no Thunderbolt driver, and the row's note says which worked | From a40c7121e559ac85903397936224d93703041871 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:37:57 -0700 Subject: [PATCH 50/54] 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$' From bf6248edd4b6b3585dcb6493e0ddfcd69d0365aa Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:38:42 -0700 Subject: [PATCH 51/54] The hardware list says a report gives what each battery draws A report's battery lines give each battery's power, so a listing of a laptop with an NVIDIA chip beside the built-in one shows what the chip left in D0 costs it. check-hardware.sh reads only the power_supply lines, so every listing stays as it was. --- docs/hardware.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/hardware.md b/docs/hardware.md index c844a8f0..24626578 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -17,7 +17,8 @@ and whether one reset the boot before, the processor and its microcode, every PCI and USB device with the driver that took it, the disks and the drivers under them, the display and each graphics controller's power state, the sound hardware, a laptop's -backlights, batteries, sleep states and camera parts, network and input +backlights, batteries and what they draw, sleep states and camera parts, +network and input devices, the loaded modules and the kernel log. Its `missing` section is the part to act on: the firmware the kernel asked for and did not find, each a line for `build/config/firmware.list`, and the devices and disks that want a @@ -74,7 +75,8 @@ Its `gpu` lines give each graphics controller's power state. Nothing here drives NVIDIA's graphics, and the kernel lets a device nothing drives sleep only once runtime PM is allowed it (`control auto`), so beside Intel's or AMD's on a laptop an NVIDIA chip stays powered, in D0. Only such a laptop -shows what that costs its battery. No level asks for it. +shows what that costs its battery, which the report's `battery` lines give. +No level asks for it. ## Trying a laptop From 3b3249e83c8e1590bc7981331f34937b98b1da8e Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:39:48 -0700 Subject: [PATCH 52/54] A laptop's tries include a USB camera pulled out while a zone has it The launch daemon ends a camera grant when udev removes the granted node (#596). The virtual machines show that with vivid unloaded, and a real camera pulled out takes the USB path instead. The row asks for both halves: the grant ends at once, and the camera plugged back in waits for k again. --- docs/hardware.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hardware.md b/docs/hardware.md index 24626578..1b4df360 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -105,6 +105,7 @@ let the row's note name what did not work: | closed on its dock, then the dock unplugged | it sleeps within seconds, as a lid closed undocked does | | the state passphrase at boot, typed on the built-in keyboard | it unlocks; a built-in keyboard that types nothing there is one on I2C, and a USB keyboard is the way through | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | +| a USB camera given to a zone with `k` and its number, then pulled out | the grant ends at once, and the menu's Camera line says no zone has it; plugged back in, it waits for `k` again | | Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | A laptop that wakes from the lid and from `kryptik suspend` is the proof that From 570f0629a770bb5ac6eef60be53512175867d841 Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 16 Oct 2026 08:40:31 -0700 Subject: [PATCH 53/54] A laptop's tries include an external monitor plugged in while the desktop runs The tries had a monitor only on a laptop with two GPUs. The common case is one port on the laptop's own GPU: plugged in while the desktop runs, the desktop reaches it, and kryptik display lists it with its scale, which the session's display watcher sets as the user chose. --- docs/hardware.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/hardware.md b/docs/hardware.md index 1b4df360..7a4cea41 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -99,6 +99,7 @@ let the row's note name what did not work: | on an AMD laptop whose `sleep` line says s2idle, a sleep of two minutes | it wakes to the lock, not to a fresh boot: the watchdog does not feed AMD's TCO timer there, as it would count through the sleep, and the report's line for it says inactive | | an SD card in the laptop's own reader | the window that asks which zone gets a stick opens, as for a USB stick | | a USB headset, plugged in while a zone plays, then `c` and a zone's number in the menu | it takes what the zone plays within a second, and records there once granted | +| an external monitor on HDMI or DisplayPort, plugged in while the desktop runs | the desktop reaches it within seconds, and `kryptik display` lists it with its scale | | an external monitor on a laptop with two GPUs, an AMD one beside the built-in | it shows the desktop, whose frames wlroots copies to the second GPU's ports; dark, the row's note names both, as the listing's `display` line does | | a laptop with NVIDIA's graphics beside Intel's or AMD's | the desktop shows on the built-in screen; a monitor on a port the NVIDIA chip drives stays dark, as 1.0 drives none, and the listing's `gpu` line says whether that chip stayed powered | | a USB-C or Thunderbolt dock: its monitor, its USB ports and its Ethernet | the monitor shows where the laptop drives USB-C itself; a Thunderbolt or USB4 dock's own USB ports and Ethernet stay dark, as 1.0 builds no Thunderbolt driver, and the row's note says which worked | From 2fafcbd9d69800b6dd5fc60ef887a16e5fc08e4e Mon Sep 17 00:00:00 2001 From: DevomB Date: Fri, 23 Oct 2026 00:06:09 -0700 Subject: [PATCH 54/54] The roadmap and the README point at the hardware list The roadmap said the machines would start a hardware list in the README, and the README did not link the list, which lives in docs/hardware.md beside its report and check. Both now point there. --- README.md | 3 ++- docs/roadmap.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47950d87..42cb1d44 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,8 @@ Microsoft's keys, so run Kryptik there with Secure Boot off. A machine outside that list boots a kernel that cannot find its disk or its network. Adding a driver is one line in the fragment, a firmware file one line -in the list. +in the list. Which machines Kryptik has been run on, and what each run showed, +is in the [hardware list](docs/hardware.md). ## How it fits together diff --git a/docs/roadmap.md b/docs/roadmap.md index 165c1a03..7f575894 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -30,7 +30,7 @@ work in zones from a terminal and a text browser, and keep it up to date. - [ ] **Physical hardware.** Installed and booted with Secure Boot on at least three machines: a wired desktop, an Intel laptop on Wi-Fi alone, an AMD laptop. What each lacked becomes a line in `boot.fragment` or - `firmware.list`, and the machines start a hardware list in the README. + `firmware.list`, and the machines start the [hardware list](hardware.md). The laptops also try what no virtual machine has: the lid, the microphone and brightness keys, the backlight, a built-in panel and a USB Video Class camera.