feat(tpm): TPM 2.0 CRB kernel driver + syscall ABI for WasmOS-on-Nanos - #1
feat(tpm): TPM 2.0 CRB kernel driver + syscall ABI for WasmOS-on-Nanos#1zacharywhitley wants to merge 8 commits into
Conversation
Introduces a TPM 2.0 Command Response Buffer (CRB) transport driver
for use by WasmOS on Nanos. This patch is scaffolded per the
companion design in the wasmos repository:
docs/design/nanos-tpm-crb-transport.md (Sections 4, 6, 8)
Structure:
- Portable MMIO register access layer (crb_mmio_ops) with a fake-MMIO
seam for driver unit tests (Section 8.1).
- tpm_state state machine + nanos_tpm object (Section 4.1, 4.2).
- nanos_tpm_transmit() single-in-flight transport with distinct
transport vs TPM error classes, deadline handling, and buffer
zeroization (Section 4.3).
- Discovery pipeline: ACPI TPM2 -> platform description -> fixed
QEMU dev address, all guarded by interface-register validation
(Section 4.4).
- Recovery flow: cancel -> reset -> revalidate -> mark unhealthy
(Section 4.5).
- Per-instance timeouts for locality / readiness / execution /
cancellation / recovery (Section 4.6).
Portability note: this patch INTENTIONALLY DOES NOT wire the driver
into the Nanos build system. That step (Makefile / module list /
init-order registration) is Nanos-fork-specific and must be applied
against a chosen upstream Nanos SHA by the maintainer.
The header comment at the top of each new file names the intended
Nanos tree path; adjust to fit the target checkout.
Reviewed-by: WasmOS ADR-0019 (Phase N2)
Adds the two application-facing syscalls specified in the companion
design doc §5:
long nanos_tpm_command(const void *command,
size_t command_length,
void *response,
size_t response_capacity,
size_t *response_length,
uint64_t timeout_ns);
long nanos_tpm_status(struct nanos_tpm_status *out);
Design constraints honoured here:
- Argument validation at the syscall boundary (§5.2). Every buffer
address is bounds-checked against the calling process's memory map
BEFORE the underlying driver is invoked. No user pointer reaches
the MMIO layer directly.
- Command and response bytes are copied into kernel-owned buffers
allocated per-call, then zeroed after use — the CRB command buffer
may still contain secrets after a signing operation.
- Distinct errno-style return codes (0 / -EINVAL / -ENOTSUP /
-ETIMEDOUT / -EIO / -EAGAIN) map 1:1 from the driver's TPM_ERR_*
values, per §5.2.
- nanos_tpm_status returns the SAME enum values the kernel driver
uses internally (§5.3) so wasmos-platform-nanos can compare
against symbolic names.
Portability note: this patch INTENTIONALLY DOES NOT wire the
syscalls into a specific Nanos syscall table. The integration
commit against the target Nanos SHA must register the two functions
in whatever the kernel's syscall-dispatch mechanism is (a static
table, a runtime registration, etc.).
Reviewed-by: WasmOS ADR-0019 (Phase N2)
Adapts the two scaffold patches (2d79412, 8d8d4ec) to the actual Nanos source layout and internal APIs, and wires them into the build, syscall dispatch table, and kernel-init sequence. Relocation ---------- kernel/tpm/ -> src/tpm/ Nanos keeps kernel source under src/, not kernel/. Files are relocated via git mv so review tooling can render them as renames; the syscall files were substantially rewritten and appear as delete + add. Nanos-internal API adaptations ------------------------------ The scaffold patches used a plausible-but-not-real Nanos API surface. The following substitutions were required against the real tree: scaffold identifier real Nanos identifier ------------------- --------------------- status (int type) int + TPM_ERR_* (Nanos's `status` is a tuple - see src/runtime/status.h) mutex_new(), allocate_mutex(heap, mutex_free() spin_iterations); deallocate() over sizeof(struct mutex) allocate(size), allocate(heap, size), allocate_zero(size), allocate_zero(heap, size), deallocate(ptr, sz) deallocate(heap, ptr, sz) heap is heap_locked(get_kernel_heaps()) now() now(CLOCK_ID_MONOTONIC_RAW) kernel_yield() kern_pause() TIMESTAMP_INFINITY, Nanos uses fixed-point timestamps deadline_min() (1s = 1<<32); timeouts convert via nanoseconds() and compare directly map_mmio_region(), allocate() from heap_virtual_page then unmap_mmio_region() map()/unmap() with pageflags_device() process_copy_from_user, copy_from_user, copy_to_user, process_copy_to_user, validate_process_memory process_check_user_range ENOTSUP EOPNOTSUPP (Nanos uses the POSIX-2001 spelling in src/kernel/errno.h) process p argument current->p via <unix_internal.h> Header layout ------------- Nanos headers deliberately have no #ifndef include guards and rely on runtime.h being #included exactly once by each translation unit. The scaffold's guarded, self-including headers were re-shaped to match: tpm_crb.h and tpm_crb_mmio.h drop their guards and their <kernel.h> include; the .c files pull in <kernel.h> or <unix_internal.h> and then the tpm/ headers via <tpm/...>. Build wiring ------------ platform/pc/Makefile adds src/tpm/tpm_crb.c and src/tpm/tpm_syscall.c to SRCS-kernel.elf. The virt (aarch64) and riscv-virt platforms are NOT wired: the wasmos-on-Nanos design (ADR-0019, docs/design/ nanos-tpm-crb-transport.md) is x86_64/QEMU-only, and the syscall number allocation is x86_64-specific. Syscall dispatch ---------------- src/x86_64/unix_syscalls.h allocates: SYS_nanos_tpm_command 500 SYS_nanos_tpm_status 501 SYS_MAX 502 (was 451) Both numbers sit well above the current Linux top-of-table so a future Nanos rebase against a newer Linux syscall set cannot collide. src/unix/ unix.c invokes register_tpm_syscalls(linux_syscalls) from init_syscalls, guarded by __x86_64__. Kernel init ----------- platform/pc/service.c detect_devices() calls init_tpm(kh) after init_acpi(kh). The driver's discovery pipeline runs (ACPI -> platform -> manifest -> QEMU fixed base); on hosts with no TPM the fallback declines cleanly and the syscall layer reports -EOPNOTSUPP. Verified -------- make PLATFORM=pc kernel builds a stripped kernel.img (1.55 MB) with all TPM symbols present in kernel.elf: init_tpm, nanos_sys_tpm_command, nanos_sys_tpm_status, nanos_tpm_default, nanos_tpm_discover, nanos_tpm_get_health, nanos_tpm_transmit, register_tpm_syscalls, the_default_tpm. `make image` fails only because the host-side mkfs tool has a pre-existing macOS PATH_MAX collision (reproducible on master before these changes); it does not affect the kernel binary. Not landed in this commit ------------------------- - ACPI TPM2 table lookup (try_discover_acpi) still returns NO_DEVICE; QEMU fixed-base fallback is the sole live discovery path. Filed for a follow-up commit against Nanos's AcpiGetTable interface. - The tests/ scaffolds shipped alongside the two patches remain outside the kernel tree; they need placement against Nanos's test harness convention and are the subject of a separate integration patch per the design doc sec 8.1. Companion design: docs/design/nanos-tpm-crb-transport.md (wasmos repo). Reviewed-by: WasmOS ADR-0019 (Phase N2).
Replaces the placeholder try_discover_acpi() with a real ACPICA-backed
lookup of the TPM2 table. When present, the table's ControlAddress
supplies the CRB MMIO base, replacing the QEMU-only 0xFED40000
hard-code that had been the sole live discovery path.
Discovery flow (design doc sec 4.4):
1. AcpiGetTable(ACPI_SIG_TPM2, ...) - primary.
2. Platform description - stub (per-platform follow-up).
3. Boot manifest - stub (per-platform follow-up).
4. QEMU fixed 0xFED40000 base - final fallback, preserved for
bare-hardware and dev QEMU setups that omit an ACPI TPM2 table.
Only StartMethod 7 (COMMAND_BUFFER / CRB) and 11 (CRB with ARM SMC)
are accepted. Any other start method returns the new TPM_ERR_UNSUPPORTED
so callers can distinguish "no TPM" from "TPM present but not CRB".
The fallback chain now short-circuits on TPM_ERR_OK rather than only
falling through on TPM_ERR_NO_DEVICE, so an unsupported ACPI TPM2 or a
transient mapping failure still lets the QEMU fallback attempt discovery.
Uses ACPICA's ACPI_TABLE_TPM2 definition
(vendor/acpica/source/include/actbl3.h); no new struct definitions
required. The MMIO window length isn't reported by the ACPI table, so
we map the same 0x5000 span used by the QEMU fallback - enough for the
CRB register block plus localities 0..4 per TCG PC Client CRB spec
Table 8-1.
|
Follow-up landed as commit 5902ec8:
No new struct definitions were needed - ACPICA already exposes Verified structurally only - runtime observation against a QEMU-emitted TPM2 table is left for the boot-side follow-up. |
ACPI TPM2 ControlAddress is CRB-control-area-aligned (0x40 offset per TCG PC Client CRB Table 8-1), not page-aligned. Nanos's map() asserts page-aligned physical bases and panicked in Q1'26 boot verify. Round the base down, pad the length, offset register accesses accordingly. Fixes the assertion at src/kernel/page.c:551 hit from nanos_tpm_discover -> tpm_map_mmio -> map() call chain on ACPI-driven discovery.
|
Q1'26 boot-verify follow-up: pushed a page-alignment fix (881e9cf) after hitting an assertion panic on ACPI-driven discovery. Panic trace (before fix): Root cause: Fix: Not-fixed follow-up (out of scope for this PR): post-alignment-fix, boot verification reaches the wasmos-nanos-node JSON summary with
Recommend a separate follow-up PR with tracing added to each discovery stage so we can confirm which of the two above (or a third factor) is the real blocker before landing further changes. Alignment fix is standalone-mergeable per the panic trace above. |
…U/hardware Two ACPI-path bugs surfaced during Phase N3 boot verification, both of which left nanos_tpm_default() == NULL despite a working tpm-crb device attached to QEMU: 1. Locality-vs-Control-Area offset. The ACPI TPM2 table's ControlAddress field points at the CRB Control Area, which per TCG PC Client CRB Interface Spec Table 8-1 lives at (locality_base + 0x40). The driver's CRB_REG_* offsets are all locality-base-relative (LOC_STATE at 0x00, INTF_ID at 0x30, CTRL_REQ at 0x40, ...), so storing ControlAddress as mmio_base landed every subsequent register read 0x40 bytes past its intended target. try_discover_acpi() now subtracts CRB_LOC_CTRL_AREA_OFFSET before mapping. 2. Interface-type strictness. crb_interface_plausible() only accepted TPM_INTERFACE_TYPE == 0x1 (pure CRB). QEMU's tpm-crb device and real Intel PTT parts report 0xF (combined FIFO+CRB), where CRB is one of several selectable interface modes. Both discovery paths were rejecting the device before the rest of the register block was ever consulted. The check now accepts 0x1 or 0xF, matching the Linux tpm_crb driver. Also add single-line rprintf diagnostics at every discovery failure point (silent on success): the absence of any observability into which stage rejected the device was what forced the prior boot-verify pass to disassemble the kernel to diagnose. init_tpm() now emits one terminal line summarising the outcome so future regressions surface immediately in the serial log.
|
Follow-up commit Bug A (locality-vs-Control-Area offset) — Bug B (interface-type strictness) — Diagnostic addition: Rebuilt clean with |
Real TPM hardware and swtpm both refuse every command with TPM_RC_INITIALIZE (0x100) until Startup has been called once per power cycle. Kernel-side driver init is the correct home for that call - having every userspace consumer emit its own Startup as a workaround does not scale (wasmos-platform-nanos was doing exactly that; that workaround now becomes belt-and-suspenders). After discovery reaches TPM_STATE_READY, submit the 12-byte TPM2_Startup(TPM_SU_CLEAR) command through nanos_tpm_transmit() using the driver's per-instance execution timeout (design doc sec 4.6, no new hard-coded deadline). Treat responseCode 0x00 and 0x100 as success - the latter is the harmless "already started" idempotency case, and occurs in practice because QEMU / swtpm may auto-start on connection. Any other responseCode, or a transport failure, is logged via rprintf and marks the driver TPM_STATE_FAILED so the status syscall surfaces the condition; the driver does NOT panic - TPM policy is a userspace concern (design doc sec 4.5). Kernel boot under QEMU tpm-crb + swtpm now emits: tpm: discovery ok (source=1 cmd=0xf80 rsp=0xf80) tpm: startup ok (rc=0x100) and the wasmos-platform-nanos TPM2_GetCapability probe continues to return manufacturer="IBM" as before.
Add two userspace unit-test binaries for the TPM 2.0 CRB driver:
test/unit/tpm_crb_test - exercises the abstract MMIO ops table
(src/tpm/tpm_crb_mmio.h) via a table-driven fake shim, pins the
TCG PC Client CRB register offsets and bit fields, and exercises
the TPM 2.0 response-header length decode with valid, truncated,
device-oversize, and caller-undersize inputs.
test/unit/tpm_syscall_test - pins the nanos_tpm_status_abi struct
layout, size, and version at the userspace/kernel boundary; pins
the TPM_STATE_* / TPM_INTERFACE_* / TPM_DISCOVERY_* / TPM_ERR_*
enumeration values; and exhaustively exercises the driver-error
to POSIX-errno mapping used by src/tpm/tpm_syscall.c.
Scope note: the underlying tpm_crb.c and tpm_syscall.c translation
units depend on kernel-only headers (<kernel.h>, mutex, ACPICA,
kernel heaps, validate_process_memory) that this userspace test
harness deliberately does not link. Tests whose intent requires
that linkage are reported as SKIP at run time with a per-test
rationale, so the design-doc sec 8.1 coverage signal is honest
rather than hidden. Extending coverage to those items requires a
future in-kernel test facility (test/runtime or a new test/kernel)
which is out of scope here.
Supersedes the plausible-API scaffold at wasmos
deploy/nanos/patches/tests/.
tpm_crb_test: 21 pass, 0 fail, 10 skip
tpm_syscall_test: 15 pass, 0 fail, 10 skip
Summary
Adds a TPM 2.0 Command Response Buffer (CRB) transport driver plus a
two-syscall user ABI to the Nanos kernel, per WasmOS ADR-0019
(Phase N2) and the accepting design at
docs/design/nanos-tpm-crb-transport.mdin the wasmos repository.The change lands as four commits:
tpm: add TPM 2.0 CRB transport driver- state machine, single-in-flighttransport, discovery pipeline, recovery flow, per-instance timeouts.
Companion design sections 4 and 8.
tpm: add nanos_tpm_command / nanos_tpm_status syscalls- user-facingABI with argument validation, per-call kernel-owned scratch buffers,
errno mapping. Companion design section 5.
feat(tpm): wire TPM 2.0 CRB driver + syscalls into Nanos build-Nanos-integration commit: relocates from
kernel/tpm/tosrc/tpm/,adapts the two scaffold patches to real Nanos internals, wires the
files into the
pcplatform Makefile, registers the syscalldispatch entries, and calls
init_tpm(kh)fromdetect_devices().feat(tpm): implement ACPI TPM2 table discovery- replaces theplaceholder
try_discover_acpi()with a real ACPICA-backed lookupof the TPM2 table (design sec 4.4). Adds
TPM_ERR_UNSUPPORTEDfornon-CRB start methods and changes the discovery fallback chain to
fall through on any non-OK stage so the QEMU fixed-base remains a
safety net.
The first two commits are the scaffold patches from the wasmos repo
applied verbatim (see
deploy/nanos/patches/README.mdthere); commitsthree and four are where the Nanos-specific wiring lives.
User-facing ABI (design section 5)
Syscall numbers (x86_64 only):
SYS_nanos_tpm_command = 500,SYS_nanos_tpm_status = 501,SYS_MAX = 502.Return codes:
0,-EINVAL,-EOPNOTSUPP,-ETIMEDOUT,-EIO,-EAGAIN,-EFAULT. These map 1-to-1 to the driver's internalTPM_ERR_*values per design section 5.2.Scope
pcplatform only. The virt (aarch64) and riscv-virtplatforms deliberately do NOT get the driver or syscall wiring; the
wasmos-on-Nanos design targets x86_64 QEMU exclusively.
management, policy, and session state lives above the kernel in the
wasmos-security crate.
syscall layer returns
-EOPNOTSUPP. This is a supported deploymentshape.
Discovery order (design sec 4.4)
AcpiGetTable(ACPI_SIG_TPM2, ...). AcceptsStartMethod7 (COMMAND_BUFFER / CRB) and 11 (CRB with ARM SMC);any other start method surfaces as
TPM_ERR_UNSUPPORTED.follow-up.
0xFED40000as the final safety net.Deviations from the wasmos-side design forced by Nanos internals
The scaffold patches (commits 1 and 2) assumed a plausible-but-not-real
Nanos API surface. Commit 3 documents each substitution; the notable
ones are:
statusis a tuple type. The driver's error-code return typeis
intusing theTPM_ERR_*enumeration.allocate_mutex(heap, spin_iterations)and freed with
deallocate(heap, mutex, sizeof(struct mutex)).now(CLOCK_ID_MONOTONIC_RAW), which returns afixed-point 32.32 timestamp (1s = 1<<32). The driver stores configured
timeouts in nanoseconds (matching the user ABI) and converts via
nanoseconds()at deadline-evaluation time.allocate()fromheap_virtual_page()followed bymap()withpageflags_writable(pageflags_device()).validate_process_memory()andcopy_from_user/copy_to_user; no user pointer reaches the MMIOlayer.
errno.husesEOPNOTSUPP(95) rather thanENOTSUP; the syscall layer emits the former.#ifndefinclude guards - the tpm/ headerstherefore have none either and must be included from a
.cfilethat has already brought in
<kernel.h>/<unix_internal.h>.ACPI_TABLE_TPM2structfrom
vendor/acpica/source/include/actbl3.h; no new structdefinitions were required.
Not landed in this PR
tests/fake_mmio.{h,c},tests/test_tpm_crb.c,tests/test_tpm_syscall.cunderdeploy/nanos/patches/tests/. Placement against Nanos's test-harnessconvention is a separate integration patch, per design section 8.1.
Verification
make PLATFORM=pc kernelbuilds a strippedkernel.img.nm kernel.elf | grep tpmshows every expected symbol:init_tpm,nanos_sys_tpm_command,nanos_sys_tpm_status,nanos_tpm_default,nanos_tpm_discover,nanos_tpm_get_health,nanos_tpm_transmit,register_tpm_syscalls,the_default_tpm.tpm_crb.oshows relocations forAcpiGetTableandAcpiPutTableinsidenanos_tpm_discover,emitted before the
0xFED40000immediate that identifies theQEMU-fixed fallback path - confirming ACPI is tried first.
make imagefails only because the host-sidemkfstool has apre-existing macOS
PATH_MAXcollision (reproducible on masterbefore these changes); the kernel binary itself is fine.
system-level testing and is deliberately deferred. ACPI discovery
is verified structurally only; runtime observation against a
QEMU-emitted TPM2 table is left for the boot-side follow-up.
Test plan
make PLATFORM=pc kernelsucceeds locally onLinux (macOS blocks at
mkfsfor unrelated reasons).against any local Nanos syscall-table policy.
-device tpm-crb,tpmdev=tpm0 -tpmdev emulator,id=tpm0, chardev=chrtpm -chardev socket,id=chrtpm,path=/tmp/mytpm/swtpm.sockand confirm
init_tpmbinds the default instance via the ACPITPM2 table (not the QEMU fixed-base fallback); then invoke the
syscalls from a WasmOS ELF built against
wasmos-platform-nanos.References
patches ratified in wasmos commit
db19646).docs/design/nanos-tpm-crb-transport.mdin the wasmosrepo. All commit messages cite section numbers.
shape (
ACPI_TABLE_TPM2in ACPICA'sactbl3.h).