diff --git a/build/config/image-programs.txt b/build/config/image-programs.txt index 70899666..d5958df0 100644 --- a/build/config/image-programs.txt +++ b/build/config/image-programs.txt @@ -1116,6 +1116,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/image-store.sh usr/libexec/kryptik/installer-run.sh usr/libexec/kryptik/keyboard.sh @@ -1215,6 +1216,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 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/service-scripts/hw-report.sh b/build/service-scripts/hw-report.sh new file mode 100755 index 00000000..b1e0a26f --- /dev/null +++ b/build/service-scripts/hw-report.sh @@ -0,0 +1,35 @@ +#!/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" +# 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" +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 b95d847d..6b254cdc 100644 --- a/build/services/default/contents +++ b/build/services/default/contents @@ -12,6 +12,7 @@ power 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/build/stages/04-base-system.sh b/build/stages/04-base-system.sh index 40acf06a..9f48035d 100755 --- a/build/stages/04-base-system.sh +++ b/build/stages/04-base-system.sh @@ -207,6 +207,7 @@ PACKAGES=( "efiboot" "s_efiboot $(sha256_of "${KRYPTIK_ROOT}/tools/efi/kryptik-efiboot.c" 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)" "stick" "s_stick ${KRYPTIK_STICK_BIN:-none} $([[ -f "${KRYPTIK_STICK_BIN:-}" ]] && sha256_of "${KRYPTIK_STICK_BIN}" || echo absent)" # 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. diff --git a/tools/acceptance.sh b/tools/acceptance.sh index 7efb8f62..0440a902 100755 --- a/tools/acceptance.sh +++ b/tools/acceptance.sh @@ -405,6 +405,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; } @@ -476,6 +477,7 @@ item build image-programs M host 0 it_programs need_root_image 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 install keyboard-test M vm 10 it_keyboard need_vm diff --git a/tools/image/hwreport-test.sh b/tools/image/hwreport-test.sh new file mode 100755 index 00000000..4b5d069d --- /dev/null +++ b/tools/image/hwreport-test.sh @@ -0,0 +1,166 @@ +#!/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: 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 -- +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" '^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" +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" +# 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" +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 9f8fcfe3..263a1500 100755 --- a/tools/image/run-ovmf.sh +++ b/tools/image/run-ovmf.sh @@ -2,14 +2,16 @@ # 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] # [--second-head] [--s3] [--sound WAV] [--usb-port] [--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 @@ -53,10 +55,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; HEAD2=0; S3=0; SOUND=""; ALLOW_REBOOT=0; NAME="vm" -DISK_RO=0; DISK_4K=0; BLKDEBUG=""; UNTIL=""; USB_PORT=0 +DISK_RO=0; DISK_4K=0; BLKDEBUG=""; UNTIL=""; USB_PORT=0; 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 ;; @@ -80,7 +83,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,46p' "${BASH_SOURCE[0]}"; exit 0 ;; + -h|--help) sed -n '2,49p' "${BASH_SOURCE[0]}"; exit 0 ;; *) die "unknown argument: $1" ;; esac done @@ -190,7 +193,7 @@ if [[ -n "$USB" || "$USB_PORT" -eq 1 ]]; then fi if [[ -n "$USB" ]]; then ARGS+=( - -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 diff --git a/tools/install/kryptik-hwreport.sh b/tools/install/kryptik-hwreport.sh new file mode 100755 index 00000000..b748f022 --- /dev/null +++ b/tools/install/kryptik-hwreport.sh @@ -0,0 +1,532 @@ +#!/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 +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; } +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. 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' \ + | 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 +# 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" + # 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" ] || continue + 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 +# there does glibc run a program built for them with one, refusing it a library built without. +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" + 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")" + 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 + # 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() { + 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 +} + +# 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 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")" + 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:--}" + 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")" + 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 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")" = "$2" ] || 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] + }' +} + +# 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")" + # 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:--}" + fi + 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:--}" + # 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 + [ "$(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' \ + -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 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 + 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 -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" +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/install/kryptik-install.sh b/tools/install/kryptik-install.sh index e123d76f..e5840b78 100755 --- a/tools/install/kryptik-install.sh +++ b/tools/install/kryptik-install.sh @@ -408,6 +408,8 @@ esp_place "$MNT_BASE/tesp/EFI/kryptik/kryptik-a.efi" "$MNT_BASE/tesp/EFI/BOOT/BO || die "BOOTX64.EFI could not be made the slot A kernel" esp_put "$MNT_BASE/tesp/kryptik/committed-slot" a || die "slot A could not be recorded as committed" 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" # Slot b was cleared above: the medium's kernel and version for it would name a release it does not hold. rm -f "$MNT_BASE/tesp/EFI/kryptik/kryptik-b.efi" "$MNT_BASE/tesp/kryptik/version-b" sync diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh new file mode 100755 index 00000000..4d56239e --- /dev/null +++ b/tools/tests/hwreport.sh @@ -0,0 +1,407 @@ +#!/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/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" +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 +# 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 +put "$stick/idVendor" 0781; put "$stick/idProduct" 5581; put "$stick/speed" 5000 +# 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 +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 "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$' + +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' +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 "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' +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 "-- 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 "-- 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: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 +# 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 "-- 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 '' +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 "-- 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" +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/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. +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]' +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 +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 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$' +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$' +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 ]] diff --git a/tools/tests/services.sh b/tools/tests/services.sh index 1312e8b5..6b395287 100755 --- a/tools/tests/services.sh +++ b/tools/tests/services.sh @@ -180,6 +180,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")" @@ -192,10 +201,7 @@ for s in "${SCRIPTS}"/*.sh; do # or by the installer or the update tools, or bound to a key of dwl's. 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" elif grep -lqE "^\s*\. +/usr/libexec/kryptik/${n}" "$ROOT"/tools/install/*.sh "$ROOT"/tools/update/kryptik-* 2>/dev/null; then green "${n}: sourced by the installer or the update tools"