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/build/config/image-programs.txt b/build/config/image-programs.txt index c5663858..38736c64 100644 --- a/build/config/image-programs.txt +++ b/build/config/image-programs.txt @@ -1117,6 +1117,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 @@ -1217,6 +1218,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/docs/hardware.md b/docs/hardware.md new file mode 100644 index 00000000..7a4cea41 --- /dev/null +++ b/docs/hardware.md @@ -0,0 +1,150 @@ +# 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. 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. + +## 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 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 and each graphics controller's +power state, the sound hardware, a laptop's +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 +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 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. + +A listing also says what confines a device's DMA: the IOMMUs in use, and +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 +(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 +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, which the report's `battery` lines give. +No level asks for 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, 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 | +| 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, 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 | +| 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 +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, +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`. One line each says +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/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. 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. diff --git a/docs/status.md b/docs/status.md index 382a020d..6a26ee12 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 | 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/check-hardware.sh b/tools/check-hardware.sh new file mode 100755 index 00000000..35152942 --- /dev/null +++ b/tools/check-hardware.sh @@ -0,0 +1,326 @@ +#!/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 +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 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. + 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 + # 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 + 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 + 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 + # 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 +} + +# 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. + bat="$(awk '$1 == "power_supply" && $4 == "Battery" && $6 == "System" { + 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}") + 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; 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 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")" + 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 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, 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" -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" +} + +# 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() { + 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)" + # 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)" + 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: "*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 }}" + + # 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" + 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. + 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 taken 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)" + 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 + 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/image/hwreport-test.sh b/tools/image/hwreport-test.sh new file mode 100755 index 00000000..35ce022c --- /dev/null +++ b/tools/image/hwreport-test.sh @@ -0,0 +1,204 @@ +#!/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 +# 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 +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,22p' "${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" +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 +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" + +# ----------------------------------------------------------------- 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 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..492d4403 --- /dev/null +++ b/tools/install/kryptik-hwreport.sh @@ -0,0 +1,544 @@ +#!/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; } +# 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; } + +# 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 'state: %s\n' "$(sed -n 's/^state=//p' "$id" 2>/dev/null)" + 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: 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() { + 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 356d4117..da01278a 100755 --- a/tools/install/kryptik-install.sh +++ b/tools/install/kryptik-install.sh @@ -420,6 +420,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/check-hardware.sh b/tools/tests/check-hardware.sh new file mode 100755 index 00000000..29bc5534 --- /dev/null +++ b/tools/tests/check-hardware.sh @@ -0,0 +1,295 @@ +#!/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 +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 +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 + +== 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 == +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 == + +== 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 + +== 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 + +== laptop == +backlight intel_backlight type raw max 19200 +power_supply AC type Mains online 1 +power_supply BAT0 type Battery scope System capacity 87 technology Li-ion status Discharging +sleep states: freeze mem +sleep mem: s2idle [deep] + +== network == +rfkill0 wlan phy0 soft 0 hard 0 +in the net zone: + wlan0 wireless + kzbr0 virtual + default route by wlan0 + +== input == +"Lid Switch" | event1 +"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")" +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 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")" + +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 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, 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]/' +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\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/' +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 + +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" +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)" +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" +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 +[[ "$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:" +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=$? +[[ "$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 ]] diff --git a/tools/tests/hwreport.sh b/tools/tests/hwreport.sh new file mode 100755 index 00000000..6e0a8d33 --- /dev/null +++ b/tools/tests/hwreport.sh @@ -0,0 +1,449 @@ +#!/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" + +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 +[ 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 "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$' +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 "-- 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; } +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|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' +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 "-- 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=$? +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; } +# 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() { + 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) ' +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 +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$' +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 +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")" +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 +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$' +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: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' +listed "the listing: the amplifiers, by their ACPI name" "HD Audio codec 10ec0287; amplifiers CSC3551, not driven" + +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 3a66acdd..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,12 +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" - # The loop reads every name before it answers: one that stopped at the first would close the - # pipe on xargs, which pipefail then counts as a failure. - 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 | { f=1; while read -r u; do - grep -rqF "/usr/libexec/kryptik/${u}" "$SRC"/*/up "$SRC"/*/run 2>/dev/null && f=0; done; exit "$f"; }; 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"