hwreport: describe a machine onto a USB stick, from a Linux-style /sys

hwreport writes one folder to the first read-write FAT mount, or mounts
the first USB stick that takes vfat itself and unmounts it afterwards:
summary.txt (machine, firmware, CPU, memory, every PCI and USB device
with its driver or "-- no driver"), dmesg.txt, lspci.txt in the shape of
lspci -nnk -xxx, lsusb.txt, dmi.txt, memmap.txt, the ACPI tables as raw
.dat files iasl -d reads, and copies of /proc. Serials, the UUID and the
MSDM/SLIC tables (a Windows licence key) are left out.

It reads /sys, which is new: sysfs at Linux's paths and in Linux's
formats -- /sys/bus/pci/devices with uevent, config and resource,
/sys/bus/usb/devices, /sys/firmware/acpi/tables, dmi/tables, memmap
and efi, /sys/class/dmi/id. toybox's lspci and lsusb work unmodified
against it. The USB core now keeps a table of attached devices for it.

The machine's name comes from SMBIOS (kernel/dev/dmi.c). The UEFI loader
hands over the entry point from the configuration table, the kernel
falls back to the 0xF0000 scan, and the log gets Linux's "DMI:" line.

Two bugs found on the way:

- The loader handed over at most 40 memory-map entries, in firmware
  order. A laptop's UEFI map is 60-150 descriptors with RAM above 4 GB
  listed last, so that is what was dropped. The entries now have their
  own area at 0x31000 with room for 256, are sorted before merging, and
  the kernel's 32 usable regions keep the largest when there are more.
- MemTotal and sysinfo's totalram were PHYSTOP, an address, which counts
  the PCI hole: a 6 GB machine reported 8. Now the usable RAM, summed.

usb.ids moves to /usr/share/hwdata, where lsusb looks for it, and
pci.ids joins it (`make pci-ids`, BSD-3-Clause like usb.ids).

Checked under QEMU q35: the report written to a FAT32 stick, fsck.fat
clean, DSDT.dat disassembled by iasl on the host, lspci -k and lsusb
showing names, 6 GB of RAM seen as 6137 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 12:54:11 +02:00
commit eab13241a0
25 changed files with 2520 additions and 65 deletions

View file

@ -170,6 +170,11 @@ struct inode {
// The synthetic /dev. See kernel/fs/devfs.c.
#define DEVFS_DEV 81
// The synthetic /sys. See kernel/fs/sysfs.c.
#define SYSFS_DEV 82
// Graft it over `target`; 0 or a negative errno. In sysfs.c.
int sysfs_mount(const char *target);
// Mounted FAT volumes, FAT_DEV onward. See include/fat.h.
// Linux's limits, for every filesystem behind the VFS. A path component can be

63
include/hwinfo.h Normal file
View file

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// What the firmware and the buses said about this machine, kept so /sys can
// say it again. kernel/fs/sysfs.c is the reader; each producer is named
// beside its part.
//
// Read-only views, copied out under whatever lock the producer has, so a
// caller never holds a pointer into a table that may change under it.
// --- the memory map, as the loader handed it over (kernel/arch/x86_64/e820.c)
struct e820_rec {
uint64 base, len;
uint32 type; // E820: 1 RAM, 2 reserved, 3 ACPI, 4 NVS, 5 bad
};
int e820_nth(int i, struct e820_rec *out); // 0, or -1 past the end
// The RAM the page allocator was given, in bytes: MemTotal and sysinfo's
// totalram. Not PHYSTOP, which is an address and counts the PCI hole.
uint64 ram_total_bytes(void);
// --- ACPI tables (kernel/arch/x86_64/acpi.c)
//
// Every table the RSDT or XSDT lists, then the DSDT and FACS, which only the
// FADT points at. `sig` gets four characters, no terminator. 0 for a table,
// 1 for an empty slot (keep counting), -1 past the end.
int acpi_table_nth(int i, uint64 *pa, uint32 *len, char sig[4]);
// --- SMBIOS (kernel/dev/dmi.c)
void dmi_init(void);
// The entry point structure and the table it describes, both physical.
int dmi_entry(uint64 *pa, uint32 *len);
int dmi_table(uint64 *pa, uint32 *len);
// Bytes of physical memory, through the direct map or ioremap. 0 on failure.
void *phys_view(uint64 pa, uint64 len);
// The strings Linux publishes in /sys/class/dmi/id, under its names. Serial
// numbers and the UUID are left out on purpose: nothing here needs them, and a
// hardware report is meant to be handed to somebody else.
enum {
DMI_BIOS_VENDOR, DMI_BIOS_VERSION, DMI_BIOS_DATE, DMI_BIOS_RELEASE,
DMI_SYS_VENDOR, DMI_PRODUCT_NAME, DMI_PRODUCT_VERSION, DMI_PRODUCT_SKU,
DMI_PRODUCT_FAMILY,
DMI_BOARD_VENDOR, DMI_BOARD_NAME, DMI_BOARD_VERSION,
DMI_CHASSIS_VENDOR, DMI_CHASSIS_TYPE, DMI_CHASSIS_VERSION,
DMI_NFIELDS
};
extern const char *const dmi_field_names[DMI_NFIELDS];
// Length written (no terminator counted), or -1 if the machine does not say.
int dmi_field(int f, char *out, int cap);
// 1 if the UEFI loader booted this kernel.
int boot_is_efi(void);
// --- USB devices (kernel/drivers/usb/core/usb.c), one per addressed device
struct usb_info {
int bus, devnum, speed; // speed: USB_SPEED_*
uint16 vid, pid, bcd;
uint8 cls, subcls, proto; // the device descriptor's
uint8 nconf;
char path[24]; // Linux's name: "1-3", "2-1.4"
char driver[16]; // the first interface's, or empty
};
int usb_info_nth(int i, struct usb_info *out);

View file

@ -85,16 +85,18 @@
#define SCHBOOT_MEDIA_SEG 0x3000
#define SCHBOOT_MEDIA_OFF 0x101
// The E820 memory map: a uint32 count, then that many 24-byte entries. Capped
// at 32 so a pathological firmware cannot run past the buffer; the map ends at
// 0x508, which is what leaves 0x600 free below.
// The E820 memory map: a uint32 count at BASE+0x200, and the entries, 24 bytes
// each, in a page-and-a-half of their own from BASE+0x1000.
//
// The entries used to sit between the count and the RSDP field, which left
// room for 40. A laptop's UEFI map is 60 to 150 descriptors, and even after
// the loader merges neighbours of the same type the RAM above 4 GB -- which
// the firmware lists last -- was what fell off the end: a 16 GB machine came
// up with 2 or 3 of it. 256 is more than any map seen merges to, and the area
// it takes, 0x31000-0x32800, is clear of everything else at BASE.
#define SCHBOOT_E820_COUNT (SCHBOOT_BASE + 0x200)
#define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x208)
/* 40, not 32. The map lives at SCHBOOT_E820_MAP (BASE+0x208) and the next
* field is SCHBOOT_RSDP_ADDR at BASE+0x600, so there are 1016 bytes and each
* entry is 24 -- 42 fit. Real UEFI firmware produces far more descriptors than
* QEMU's, and the headroom costs nothing that was being used. */
#define SCHBOOT_E820_MAX 40
#define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x1000)
#define SCHBOOT_E820_MAX 256
// Where the loader leaves the ACPI RSDP, as a little-endian 64-bit physical
// address. Zero means "not provided, go and look for it yourself".
@ -164,6 +166,17 @@
// BGRX in memory, so format 0 is the one that needs its channels swapped.
#define SCHBOOT_FB_FORMAT (SCHBOOT_BASE + 0x634) /* uint32 */
// The SMBIOS entry point, from the UEFI configuration table: the 64-bit
// "_SM3_" one when the firmware has it, else the 32-bit "_SM_". Zero means
// none was handed over, and the kernel scans 0xF0000-0xFFFFF the BIOS way.
// It is what names the machine -- vendor, model, firmware version -- in
// /sys/class/dmi/id, and on UEFI the scan alone finds nothing on most laptops.
#define SCHBOOT_SMBIOS_ADDR (SCHBOOT_BASE + 0x638) /* uint64, physical */
// SCHBOOT_EFI_MAGIC when the UEFI loader booted this kernel, which is what
// /sys/firmware/efi existing means on Linux.
#define SCHBOOT_EFI_FLAG (SCHBOOT_BASE + 0x640) /* uint32 */
#define SCHBOOT_EFI_MAGIC 0x21494645u /* "EFI!" */
#ifndef __ASSEMBLER__
struct sch64_header {

View file

@ -121,6 +121,9 @@ int rt_sigaction(int, void*, void*, uint64);
// kernel's internal one. Returns -EINTR always, as sigsuspend must.
int rt_sigsuspend(const uint64 *mask);
int mount(const char *src, const char *tgt, const char *fstype, uint64 flags, void *data);
int umount2(const char *tgt, int flags);
int sync(void);
int fsync(int fd);
// Directory reading. The kernel has had getdents64 since the Linux ABI landed,
// but nothing declared it here, so first-party programs had no way to list a