New freestanding, verify-only Ed25519 (RFC 8032) implementation:
include/starkernel/{sha512,fe25519,scalar25519,ed25519}.h +
src/starkernel/crypto/{sha512,fe25519,scalar25519,ed25519}.c, wired into
Makefile.starkernel. Kernel never signs or generates keys -- only
ed25519_verify() is needed; signing happens in the host-side mkcapsule
build tool via libsodium/OpenSSL.
Confirmed __int128 multiply/add/shift-by-constant compile with zero
undefined symbols on all three target toolchains (only division needs
libgcc's __udivti3, per timer.c's existing documented finding -- that
file's comment updated to narrow the claim, since it had been read as
"avoid __int128 entirely"). This enabled the standard 5-limb radix-2^51
field arithmetic representation.
An abandoned first attempt (10-limb radix-2^26, avoiding __int128 out of
premature caution) hit two real bugs, both invisible on inspection and
found only by property-based testing against Python's own bignum
arithmetic: a non-uniform-radix limb misalignment in multiplication, and
a double-counted carry. Verification chain: SHA-512 against known +
boundary vectors (7/7); field arithmetic property-tested 25,045 cases;
scalar-mod-L arithmetic 300 cases (L confirmed prime via Miller-Rabin
first); full verify() end-to-end against 110 real signatures from
Python's cryptography library, including tampered inputs and the RFC
8032 S>=L malleability attack -- all correctly accepted/rejected.
Compiles clean (zero warnings) and links on all three architectures,
confirmed via the mandatory three-arch QEMU boot. The code is linked but
not yet called from anywhere -- wiring into capsule_birth.c needs a
from-scratch X.509/DER parser first (Captain Bob chose real X.509 over a
raw-blob cert format this session), which is the next open item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
1394 lines
48 KiB
C
1394 lines
48 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
/**
|
||
* timer.c - Timer policy + TSC calibration (amd64)
|
||
*
|
||
* The kernel will always run with the best time source available, but will only
|
||
* make determinism claims when the hardware contract is provably satisfied.
|
||
*
|
||
* Policy split:
|
||
* - VM path: avoid HPET MMIO. Prefer invariant TSC for ABSOLUTE time. Derive TSC Hz
|
||
* from CPUID leaves (0x15/0x16) when available; otherwise fall back to ACPI PM Timer
|
||
* (port I/O, stable under QEMU/KVM) to obtain TSC Hz. If invariant TSC is absent,
|
||
* kernel continues in RELATIVE time mode (PM Timer-based), unless strict mode is enabled.
|
||
* - Bare metal path: calibrate TSC using HPET as primary reference, with optional PIT
|
||
* cross-check. Fail hard on non-convergence.
|
||
*
|
||
* Failure mode: PANIC/HALT when invariants are not met.
|
||
*
|
||
* Strict VM policy:
|
||
* -DTIMER_VM_STRICT_INVARIANT_TSC=1 enforces invariant TSC in VM mode (fail-hard).
|
||
*/
|
||
|
||
#include <stdint.h>
|
||
#include "timer.h"
|
||
#include "console.h"
|
||
#include "vmm.h"
|
||
|
||
/* HPET registers */
|
||
#define HPET_GEN_CAP_ID 0x000
|
||
#define HPET_GEN_CONFIG 0x010
|
||
#define HPET_MAIN_COUNTER 0x0F0
|
||
|
||
#define HPET_PHYS_BASE 0xFED00000ull
|
||
#define HPET_VIRT_BASE 0xFED00000ull /* identity-mapped in this setup */
|
||
|
||
/* PIT (bare metal cross-check only; optional) */
|
||
#define PIT_CHANNEL0_DATA 0x40
|
||
#define PIT_COMMAND 0x43
|
||
#define PIT_FREQ_HZ 1193182u
|
||
|
||
/* PM Timer (VM-friendly reference; 3.579545 MHz; 24-bit typical).
|
||
* The I/O port is machine-type-dependent (0x408 on legacy PIIX4/i440fx,
|
||
* 0x608 on Q35's ICH9 LPC) and is looked up from the ACPI FADT at runtime
|
||
* by fadt_find_pm_tmr_port() in timer_init(); this is only the last-resort
|
||
* fallback if FADT parsing fails. Hardcoding a single port here silently
|
||
* reads an unmapped port (returns all-ones) on any machine type that
|
||
* doesn't match, freezing every RELATIVE-mode timestamp at a constant
|
||
* value -- found via VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev h. */
|
||
#define PMTIMER_IO_PORT_DEFAULT 0x408
|
||
static uint16_t pmtimer_port = PMTIMER_IO_PORT_DEFAULT;
|
||
#define PMTIMER_FREQ_HZ 3579545ull
|
||
#define PMTIMER_WIDTH_BITS 24
|
||
#define PMTIMER_MASK ((1ull << PMTIMER_WIDTH_BITS) - 1ull)
|
||
|
||
/* Convergence parameters (bare metal path) */
|
||
#define WARMUP_WINDOWS 2
|
||
#define SAMPLE_WINDOWS 8
|
||
#define WINDOW_TICKS_HPET 10000ull /* ~1ms at 10MHz nominal */
|
||
#define WINDOW_TICKS_PIT 4000u /* ~3.3ms nominal */
|
||
#define CV_EPSILON_PPM 1000ull /* 0.1% */
|
||
#define CV_STABLE_COUNT 6
|
||
#define DRIFT_EPSILON_PPM 2000ull
|
||
|
||
#ifndef TIMER_REQUIRE_PIT
|
||
#define TIMER_REQUIRE_PIT 0
|
||
#endif
|
||
|
||
#ifndef TIMER_VM_STRICT_INVARIANT_TSC
|
||
#define TIMER_VM_STRICT_INVARIANT_TSC 0
|
||
#endif
|
||
|
||
static volatile uint64_t *hpet_regs = 0;
|
||
static uint64_t hpet_freq_hz = 0;
|
||
|
||
static uint64_t tsc_hz_locked = 0;
|
||
static uint64_t pit_tsc_hz_mean = 0;
|
||
static uint32_t windows_used = 0;
|
||
|
||
static timer_calibration_record_t calib_record = {0};
|
||
static int pit_available = 0;
|
||
|
||
/* VM RELATIVE time base (PM Timer anchored) */
|
||
static uint32_t vm_pm_start = 0;
|
||
static uint64_t vm_ns_base = 0;
|
||
|
||
/* ------------ tiny utils (serial-friendly) ------------ */
|
||
|
||
/**
|
||
* @brief Compute @c (a × b) / c using 128-bit intermediate precision.
|
||
*
|
||
* Uses the x86-64 @c MUL instruction to produce a 128-bit product in
|
||
* @c RDX:RAX, then @c DIV to produce a 64-bit quotient. This avoids the
|
||
* overflow that would occur with a 64-bit-only intermediate when @p a and
|
||
* @p b are both large (e.g., nanosecond conversion of multi-GHz tick counts).
|
||
* No libgcc dependency — pure inline assembly.
|
||
*
|
||
* FABRIC.md item 4.5d, 2026-08-11: this asm previously declared @c RDX as a
|
||
* plain output (@c "=d"(hi)), which tells GCC only "I want to read RDX's
|
||
* value after this block" — nothing told it that @c mulq writes RDX *before*
|
||
* @c divq needs to read a *different* value (the divisor @c c) out of it.
|
||
* Nothing stopped the register allocator from placing @c c itself in RDX,
|
||
* which @c mulq then overwrote with the product's high 64 bits before
|
||
* @c divq ever read it. At @c -O0 this happened not to manifest; at @c -O2,
|
||
* GCC's more aggressive allocation actually did it, and for a product like
|
||
* `1000 * 1000000000` (fits entirely in the low 64 bits, so mulq's
|
||
* high-word output is 0), that corrupted the intended divisor to 0 -- a
|
||
* genuine #DE (divide error) confirmed via QEMU `-d int` tracing, cascading
|
||
* to a double fault and then a triple fault (silently exited by
|
||
* `-no-reboot`, which is why this looked like an infinite hang rather than
|
||
* a crash at first). A `unsigned __int128` rewrite was tried first but
|
||
* needs libgcc's `__udivti3` for the general 128÷64 DIVISION case,
|
||
* undefined in this freestanding, `-nostdlib` build — not viable for that
|
||
* reason specifically. (Confirmed narrower during the Ed25519 crypto work,
|
||
* 2026-08-22: `__int128` multiply/add/shift-by-constant DO compile cleanly
|
||
* with zero undefined symbols on all three target toolchains — it's only
|
||
* division, needing a multi-instruction libgcc routine the compiler won't
|
||
* inline, that's unavailable here. Don't read this comment as "avoid
|
||
* __int128 entirely" — src/starkernel/crypto/fe25519.c uses it safely for
|
||
* exactly that reason.) Fixed instead the way the
|
||
* Linux kernel's own `mul_u64_u64_div_u64` does it: declare `rdx` as a pure
|
||
* *clobber*, not an output. A clobber tells GCC the register is used
|
||
* internally by the whole asm block and must never be allocated to any
|
||
* input or output operand for its duration — exactly the guarantee the
|
||
* previous constraint list was missing.
|
||
*
|
||
* Undefined behaviour if @c a × b / c overflows @c uint64_t (i.e., the
|
||
* true quotient exceeds @c UINT64_MAX). Callers must ensure this cannot
|
||
* happen with their inputs.
|
||
*
|
||
* @param a Multiplicand.
|
||
* @param b Multiplier.
|
||
* @param c Divisor (must be non-zero; division by zero causes a #DE fault).
|
||
* @return Exact quotient @c (a * b) / c, truncated toward zero.
|
||
*/
|
||
static inline uint64_t muldiv64(uint64_t a, uint64_t b, uint64_t c)
|
||
{
|
||
uint64_t result;
|
||
__asm__ volatile (
|
||
"mulq %2\n\t" /* RDX:RAX = a * b */
|
||
"divq %3\n\t" /* RAX = RDX:RAX / c, RDX = remainder (discarded) */
|
||
: "=a"(result)
|
||
: "a"(a), "rm"(b), "rm"(c)
|
||
: "rdx", "cc"
|
||
);
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* @brief Print a @c uint64_t value as decimal digits to the kernel console.
|
||
*
|
||
* Uses a local reverse-digit buffer and @c console_putc(); no libc or
|
||
* heap allocation required. Prints @c "0" for a zero value. Used throughout
|
||
* the timer subsystem to emit calibration results without depending on
|
||
* @c printf().
|
||
*
|
||
* @param val Value to print in decimal.
|
||
*/
|
||
static void print_dec(uint64_t val)
|
||
{
|
||
char buf[32];
|
||
int i = 0;
|
||
if (val == 0) {
|
||
console_putc('0');
|
||
return;
|
||
}
|
||
while (val > 0 && i < (int)sizeof(buf)) {
|
||
buf[i++] = (char)('0' + (val % 10));
|
||
val /= 10;
|
||
}
|
||
while (i-- > 0) {
|
||
console_putc(buf[i]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Read the x86-64 Time Stamp Counter without serialisation (RDTSC).
|
||
*
|
||
* Executes the @c RDTSC instruction and combines @c EDX:EAX into a 64-bit
|
||
* result. May be reordered relative to surrounding instructions by the
|
||
* out-of-order execution unit; use @c rdtscp_aux() where ordering matters.
|
||
*
|
||
* @return Current TSC value in CPU cycles.
|
||
*/
|
||
static inline uint64_t rdtsc(void)
|
||
{
|
||
uint32_t lo, hi;
|
||
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
|
||
return ((uint64_t)hi << 32) | lo;
|
||
}
|
||
|
||
/**
|
||
* @brief Read TSC and processor ID with weak serialisation (RDTSCP).
|
||
*
|
||
* @c RDTSCP is weakly serialising: all prior loads are complete before the
|
||
* read, but it does not wait for prior stores. Optionally captures the
|
||
* @c IA32_TSC_AUX MSR value (processor ID / NUMA node) in @p aux. Falls
|
||
* back to @c rdtsc() at call sites that check @c cpu_has_rdtscp() first.
|
||
*
|
||
* @param aux If non-NULL, receives the @c ECX value from @c RDTSCP
|
||
* (processor/socket ID programmed by the OS or hypervisor).
|
||
* @return Current TSC value in CPU cycles.
|
||
*/
|
||
static inline uint64_t rdtscp_aux(uint32_t *aux)
|
||
{
|
||
uint32_t lo, hi, a;
|
||
__asm__ volatile ("rdtscp" : "=a"(lo), "=d"(hi), "=c"(a) ::);
|
||
if (aux) *aux = a;
|
||
return ((uint64_t)hi << 32) | lo;
|
||
}
|
||
|
||
/**
|
||
* @brief Execute the x86-64 @c CPUID instruction and capture all output registers.
|
||
*
|
||
* Passes @p leaf in @c EAX and @p subleaf in @c ECX before executing @c CPUID.
|
||
* Any of the output pointers may be @c NULL; the corresponding register value
|
||
* is then discarded.
|
||
*
|
||
* @param leaf CPUID function leaf (EAX input).
|
||
* @param subleaf CPUID sub-leaf (ECX input, for structured extended leaves).
|
||
* @param a If non-NULL, receives @c EAX on return.
|
||
* @param b If non-NULL, receives @c EBX on return.
|
||
* @param c If non-NULL, receives @c ECX on return.
|
||
* @param d If non-NULL, receives @c EDX on return.
|
||
*/
|
||
static inline void cpuid(uint32_t leaf, uint32_t subleaf,
|
||
uint32_t *a, uint32_t *b, uint32_t *c, uint32_t *d)
|
||
{
|
||
uint32_t eax, ebx, ecx, edx;
|
||
__asm__ volatile ("cpuid"
|
||
: "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
|
||
: "a"(leaf), "c"(subleaf));
|
||
if (a) *a = eax;
|
||
if (b) *b = ebx;
|
||
if (c) *c = ecx;
|
||
if (d) *d = edx;
|
||
}
|
||
|
||
/**
|
||
* @brief Write a byte to an x86 I/O port (OUT instruction).
|
||
*
|
||
* @param port I/O port address.
|
||
* @param val Byte value to write.
|
||
*/
|
||
static inline void outb(uint16_t port, uint8_t val)
|
||
{
|
||
__asm__ volatile ("outb %0, %1" : : "a"(val), "Nd"(port));
|
||
}
|
||
|
||
/**
|
||
* @brief Read a byte from an x86 I/O port (IN instruction).
|
||
*
|
||
* @param port I/O port address.
|
||
* @return Byte value read from the port.
|
||
*/
|
||
static inline uint8_t inb(uint16_t port)
|
||
{
|
||
uint8_t ret;
|
||
__asm__ volatile ("inb %1, %0" : "=a"(ret) : "Nd"(port));
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @brief Read a 32-bit dword from an x86 I/O port (INL instruction).
|
||
*
|
||
* Used to read the ACPI PM Timer register which is a 24-bit counter
|
||
* accessed as a 32-bit I/O port.
|
||
*
|
||
* @param port I/O port address.
|
||
* @return 32-bit value read from the port.
|
||
*/
|
||
static inline uint32_t inl(uint16_t port)
|
||
{
|
||
uint32_t ret;
|
||
__asm__ volatile ("inl %1, %0" : "=a"(ret) : "Nd"(port));
|
||
return ret;
|
||
}
|
||
|
||
/**
|
||
* @brief Read a 64-bit HPET register at @p offset bytes from the HPET base.
|
||
*
|
||
* Accesses @c hpet_regs[] (the identity-mapped MMIO region) using a
|
||
* naturally-aligned 64-bit read. @p offset must be a multiple of 8.
|
||
*
|
||
* @param offset Byte offset of the target register (e.g., @c HPET_MAIN_COUNTER = 0x0F0).
|
||
* @return 64-bit register value.
|
||
*/
|
||
static inline uint64_t hpet_read(uint32_t offset)
|
||
{
|
||
return hpet_regs[offset / 8];
|
||
}
|
||
|
||
/**
|
||
* @brief Write a 64-bit value to an HPET register at @p offset bytes from base.
|
||
*
|
||
* @param offset Byte offset of the target register (must be a multiple of 8).
|
||
* @param val 64-bit value to write.
|
||
*/
|
||
static inline void hpet_write(uint32_t offset, uint64_t val)
|
||
{
|
||
hpet_regs[offset / 8] = val;
|
||
}
|
||
|
||
/**
|
||
* @brief Emit a fatal timer error message and halt the processor permanently.
|
||
*
|
||
* Prints @p msg followed by @c "Timer subsystem fatal. Halting." to the
|
||
* kernel console, then enters an infinite @c HLT loop. Never returns.
|
||
* Called when a hard timer invariant cannot be satisfied (e.g., HPET
|
||
* counter not advancing, TSC calibration failed to converge).
|
||
*
|
||
* @param msg Null-terminated error description to print before halting.
|
||
*/
|
||
static void timer_fatal(const char *msg)
|
||
{
|
||
console_println(msg);
|
||
console_println("Timer subsystem fatal. Halting.");
|
||
while (1) {
|
||
__asm__ volatile ("hlt");
|
||
}
|
||
}
|
||
|
||
/* ------------ CPU capability / environment detection ------------ */
|
||
|
||
/**
|
||
* @brief Test whether the processor supports Invariant TSC (non-stop TSC).
|
||
*
|
||
* Reads @c CPUID.80000007H:EDX[8]. Invariant TSC means the TSC counts at a
|
||
* constant rate regardless of CPU frequency scaling or sleep states — a
|
||
* requirement for using RDTSC as a reliable nanosecond source.
|
||
*
|
||
* @return 1 if invariant TSC is supported, 0 otherwise.
|
||
*/
|
||
static int cpu_has_invariant_tsc(void)
|
||
{
|
||
/* CPUID.80000007H:EDX[8] = Invariant TSC */
|
||
uint32_t a, b, c, d;
|
||
cpuid(0x80000007u, 0, &a, &b, &c, &d);
|
||
return (d & (1u << 8)) ? 1 : 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Test whether the processor supports the RDTSCP instruction.
|
||
*
|
||
* Reads @c CPUID.80000001H:EDX[27]. RDTSCP provides weak serialisation and
|
||
* simultaneously reads both the TSC and the @c IA32_TSC_AUX MSR (which the
|
||
* OS programs to hold a per-CPU/socket identifier). Used in @c timer_now_ns()
|
||
* to prefer RDTSCP over the non-serialising RDTSC when available.
|
||
*
|
||
* @return 1 if RDTSCP is supported, 0 otherwise.
|
||
*/
|
||
static int cpu_has_rdtscp(void)
|
||
{
|
||
/* CPUID.80000001H:EDX[27] = RDTSCP */
|
||
uint32_t a, b, c, d;
|
||
cpuid(0x80000001u, 0, &a, &b, &c, &d);
|
||
return (d & (1u << 27)) ? 1 : 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Detect whether the kernel is running under a hypervisor.
|
||
*
|
||
* Reads @c CPUID.1:ECX[31] (Hypervisor Present bit). When set, the kernel
|
||
* switches to VM mode: HPET MMIO calibration is disabled (VM-exits would
|
||
* poison timing), and TSC frequency is derived from CPUID leaves 0x15/0x16
|
||
* or the ACPI PM Timer instead.
|
||
*
|
||
* @return 1 if a hypervisor is present (including QEMU/TCG), 0 on bare metal.
|
||
*/
|
||
static int running_under_hypervisor(void)
|
||
{
|
||
/* CPUID.1:ECX[31] = Hypervisor present */
|
||
uint32_t a, b, c, d;
|
||
cpuid(1u, 0, &a, &b, &c, &d);
|
||
return (c & (1u << 31)) ? 1 : 0;
|
||
}
|
||
|
||
/* ------------ PM Timer port discovery (ACPI FADT) ------------ */
|
||
|
||
/* Minimal ACPI structures needed to locate PM_TMR_BLK. Mirrors the
|
||
* RSDP/XSDT walk in pci.c's parse_mcfg(), independently duplicated here
|
||
* (kernel has no shared ACPI header) rather than touching already-working
|
||
* PCI enumeration code. */
|
||
typedef struct {
|
||
uint8_t signature[8];
|
||
uint8_t checksum;
|
||
uint8_t oem_id[6];
|
||
uint8_t revision;
|
||
uint32_t rsdt_address;
|
||
uint32_t length;
|
||
uint64_t xsdt_address;
|
||
uint8_t extended_checksum;
|
||
uint8_t reserved[3];
|
||
} __attribute__((packed)) TimerRsdp2;
|
||
|
||
typedef struct {
|
||
uint8_t signature[4];
|
||
uint32_t length;
|
||
uint8_t revision;
|
||
uint8_t checksum;
|
||
uint8_t oem_id[6];
|
||
uint8_t oem_table_id[8];
|
||
uint32_t oem_revision;
|
||
uint32_t creator_id;
|
||
uint32_t creator_revision;
|
||
} __attribute__((packed)) TimerAcpiHeader;
|
||
|
||
/* Generic Address Structure (ACPI 2.0+). address_space_id: 0=SystemMemory,
|
||
* 1=SystemIO. */
|
||
typedef struct {
|
||
uint8_t address_space_id;
|
||
uint8_t register_bit_width;
|
||
uint8_t register_bit_offset;
|
||
uint8_t access_size;
|
||
uint64_t address;
|
||
} __attribute__((packed)) TimerAcpiGas;
|
||
|
||
static int timer_sig4_eq(const uint8_t *p, const char *s) {
|
||
return p[0] == (uint8_t)s[0] && p[1] == (uint8_t)s[1] &&
|
||
p[2] == (uint8_t)s[2] && p[3] == (uint8_t)s[3];
|
||
}
|
||
static int timer_sig8_eq(const uint8_t *p, const char *s) {
|
||
int i;
|
||
for (i = 0; i < 8; i++)
|
||
if (p[i] != (uint8_t)s[i]) return 0;
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* @brief Look up the ACPI PM_TMR_BLK I/O port from the FADT ("FACP" table).
|
||
*
|
||
* Walks RSDP -> XSDT -> FADT exactly like pci.c's MCFG lookup. Prefers the
|
||
* ACPI 2.0+ X_PM_TMR_BLK Generic Address Structure (offset 208) when it
|
||
* names a valid SystemIO address; falls back to the legacy 32-bit
|
||
* PM_TMR_BLK field (offset 76) otherwise. This port is machine-type
|
||
* dependent -- QEMU's i440fx puts it at 0x408, Q35's ICH9 LPC at 0x608 --
|
||
* so it must be discovered rather than assumed.
|
||
*
|
||
* @param rsdp BootInfo->acpi_table pointer from the UEFI configuration
|
||
* table (ACPI 2.0 GUID), or NULL.
|
||
* @return The discovered I/O port, or 0 if the FADT/XSDT/RSDP could not be
|
||
* parsed (caller should keep its hardcoded fallback in that case).
|
||
*/
|
||
static uint16_t fadt_find_pm_tmr_port(void *rsdp)
|
||
{
|
||
if (!rsdp) return 0;
|
||
|
||
TimerRsdp2 *r = (TimerRsdp2 *)rsdp;
|
||
if (!timer_sig8_eq(r->signature, "RSD PTR ")) return 0;
|
||
if (r->revision < 2 || r->xsdt_address == 0) return 0;
|
||
|
||
TimerAcpiHeader *xsdt = (TimerAcpiHeader *)(uintptr_t)r->xsdt_address;
|
||
if (!timer_sig4_eq(xsdt->signature, "XSDT")) return 0;
|
||
|
||
uint32_t hdr_len = xsdt->length;
|
||
if (hdr_len <= 36u) return 0;
|
||
uint32_t n_entries = (hdr_len - 36u) / 8u;
|
||
uint64_t *entries = (uint64_t *)((uint8_t *)xsdt + 36u);
|
||
|
||
uint32_t i;
|
||
for (i = 0; i < n_entries; i++) {
|
||
TimerAcpiHeader *sdt = (TimerAcpiHeader *)(uintptr_t)entries[i];
|
||
if (!timer_sig4_eq(sdt->signature, "FACP")) continue;
|
||
|
||
uint8_t *fadt = (uint8_t *)sdt;
|
||
uint32_t fadt_len = sdt->length;
|
||
|
||
/* Prefer X_PM_TMR_BLK (GAS at offset 208) if present and SystemIO. */
|
||
if (fadt_len >= 220u) {
|
||
TimerAcpiGas *x_pm_tmr = (TimerAcpiGas *)(fadt + 208);
|
||
if (x_pm_tmr->address_space_id == 1 /* SystemIO */ &&
|
||
x_pm_tmr->address != 0 &&
|
||
x_pm_tmr->address <= 0xFFFFu) {
|
||
return (uint16_t)x_pm_tmr->address;
|
||
}
|
||
}
|
||
|
||
/* Fall back to the legacy 32-bit PM_TMR_BLK field (offset 76). */
|
||
if (fadt_len >= 80u) {
|
||
uint32_t legacy_port;
|
||
/* Unaligned read: FADT is a packed byte blob, offset 76 has no
|
||
* alignment guarantee. */
|
||
legacy_port = (uint32_t)fadt[76] | ((uint32_t)fadt[77] << 8) |
|
||
((uint32_t)fadt[78] << 16) | ((uint32_t)fadt[79] << 24);
|
||
if (legacy_port != 0 && legacy_port <= 0xFFFFu) {
|
||
return (uint16_t)legacy_port;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ------------ PM Timer (VM-friendly reference) ------------ */
|
||
|
||
/**
|
||
* @brief Read the raw ACPI PM Timer counter from I/O port @c pmtimer_port.
|
||
*
|
||
* The PM Timer runs at 3.579545 MHz and is a 24-bit counter (bits [23:0])
|
||
* in most systems. The returned 32-bit value may contain non-zero bits
|
||
* above bit 23 on 32-bit PM Timer implementations; callers must mask with
|
||
* @c PMTIMER_MASK before use.
|
||
*
|
||
* @return Raw 32-bit PM Timer register value.
|
||
*/
|
||
static inline uint32_t pmtimer_read(void)
|
||
{
|
||
return inl(pmtimer_port);
|
||
}
|
||
|
||
/**
|
||
* @brief Compute elapsed PM Timer ticks from @p start to @p cur, handling wrap-around.
|
||
*
|
||
* Masks both values to @c PMTIMER_WIDTH_BITS (24 bits) before comparison.
|
||
* When the counter wraps — i.e., @p cur < @p start after masking — the
|
||
* function returns the distance through the wrap: @c (MASK - start + 1 + cur).
|
||
*
|
||
* @param start PM Timer value at the beginning of the interval (unmasked).
|
||
* @param cur PM Timer value at the end of the interval (unmasked).
|
||
* @return Number of PM Timer ticks elapsed, accounting for a single wrap.
|
||
*/
|
||
static uint64_t pmtimer_delta(uint32_t start, uint32_t cur)
|
||
{
|
||
uint32_t s = start & (uint32_t)PMTIMER_MASK;
|
||
uint32_t c = cur & (uint32_t)PMTIMER_MASK;
|
||
|
||
if (c >= s) return (uint64_t)(c - s);
|
||
return (uint64_t)((PMTIMER_MASK - s) + 1u + c);
|
||
}
|
||
|
||
/**
|
||
* @brief Calibrate TSC frequency using the ACPI PM Timer as reference.
|
||
*
|
||
* Waits for 1000 PM Timer ticks (~279 µs at 3.58 MHz), measures the TSC
|
||
* delta over the same interval, then converts to Hz using @c muldiv64().
|
||
* Includes a 5-million-iteration timeout to avoid hanging under TCG or
|
||
* broken PM Timer implementations.
|
||
*
|
||
* Used by the VM path when CPUID leaves 0x15/0x16 do not provide a TSC
|
||
* frequency. The PM Timer is port I/O-based and does not cause VM-exits on
|
||
* most hypervisors (unlike HPET MMIO), making it suitable for VM calibration.
|
||
*
|
||
* @return TSC frequency in Hz, or 0 if calibration timed out or produced an
|
||
* invalid result.
|
||
*/
|
||
static uint64_t calibrate_tsc_with_pmtimer(void)
|
||
{
|
||
/* Use a small delta to avoid long waits under TCG */
|
||
const uint32_t target_ticks = 1000u; /* ~279us at 3.58MHz */
|
||
const uint64_t timeout_iter = 5000000ull;
|
||
|
||
uint32_t start = pmtimer_read() & (uint32_t)PMTIMER_MASK;
|
||
uint64_t start_tsc = rdtsc();
|
||
uint64_t iters = 0;
|
||
|
||
while (1) {
|
||
uint32_t cur = pmtimer_read() & (uint32_t)PMTIMER_MASK;
|
||
uint64_t delta = pmtimer_delta(start, cur);
|
||
|
||
if (delta >= target_ticks) {
|
||
uint64_t end_tsc = rdtsc();
|
||
uint64_t elapsed_ticks = delta;
|
||
|
||
uint64_t elapsed_ns = muldiv64(elapsed_ticks, 1000000000ull, PMTIMER_FREQ_HZ);
|
||
if (elapsed_ns == 0) return 0;
|
||
|
||
/* FABRIC.md item 4.5d, 2026-08-11: this file's own comments already
|
||
* flag TSC non-monotonicity as a real risk under TCG ("invariant
|
||
* TSC not present under hypervisor... no determinism guarantees").
|
||
* If end_tsc < start_tsc, this subtraction wraps to a huge unsigned
|
||
* value; muldiv64()'s own contract is explicit that an overflowing
|
||
* quotient is caller-UB, and DIVQ raises #DE on overflow -- exactly
|
||
* the divide-error / double-fault / triple-fault cascade found by
|
||
* QEMU '-d int' tracing an -O2 boot stall that turned out not to be
|
||
* a hang at all. Treat non-monotonic TSC as a calibration failure,
|
||
* the same as the elapsed_ns==0 case just above, instead of feeding
|
||
* an underflowed value into the division. */
|
||
if (end_tsc < start_tsc) return 0;
|
||
|
||
uint64_t delta_tsc = end_tsc - start_tsc;
|
||
return muldiv64(delta_tsc, 1000000000ull, elapsed_ns);
|
||
}
|
||
|
||
if (++iters > timeout_iter) {
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Initialise the VM relative-time base anchored to the PM Timer.
|
||
*
|
||
* Records the current PM Timer counter value in @c vm_pm_start and resets
|
||
* @c vm_ns_base to zero. Subsequent calls to @c timer_now_ns_vm_relative()
|
||
* compute elapsed nanoseconds from this anchor point.
|
||
*
|
||
* Called during @c init_timer_vm_mode() after the calibration phase, so the
|
||
* first call to @c timer_now_ns() returns a small positive value rather than
|
||
* a large absolute-time offset.
|
||
*/
|
||
static void vm_timebase_init_from_pmtimer(void)
|
||
{
|
||
vm_pm_start = pmtimer_read() & (uint32_t)PMTIMER_MASK;
|
||
vm_ns_base = 0;
|
||
}
|
||
|
||
/* ------------ HPET helpers (bare metal path) ------------ */
|
||
|
||
/**
|
||
* @brief Enable the HPET main counter by setting the ENABLE_CNF bit.
|
||
*
|
||
* Reads the @c HPET_GEN_CONFIG register, sets bit 0 (ENABLE_CNF), and
|
||
* writes it back. Must be called before the main counter is used for
|
||
* calibration. The HPET must be identity-mapped at @c HPET_VIRT_BASE
|
||
* before this function is called.
|
||
*/
|
||
static void enable_hpet(void)
|
||
{
|
||
uint64_t cfg = hpet_read(HPET_GEN_CONFIG);
|
||
cfg |= 1ull;
|
||
hpet_write(HPET_GEN_CONFIG, cfg);
|
||
}
|
||
|
||
/**
|
||
* @brief Verify the HPET main counter is advancing (quick sanity check).
|
||
*
|
||
* Reads the @c HPET_MAIN_COUNTER repeatedly up to 200,000 times. If the
|
||
* counter value changes at least once, the check passes silently. If the
|
||
* counter never changes — indicating a hardware fault, frozen HPET, or
|
||
* mapping error — calls @c timer_fatal() which halts the processor.
|
||
*
|
||
* Only used on the bare-metal path; skipped in VM mode.
|
||
*/
|
||
static void sanity_check_hpet_running_quick(void)
|
||
{
|
||
uint64_t start = hpet_read(HPET_MAIN_COUNTER);
|
||
for (int i = 0; i < 200000; ++i) {
|
||
if (hpet_read(HPET_MAIN_COUNTER) != start) {
|
||
return;
|
||
}
|
||
}
|
||
timer_fatal("HPET counter not advancing.");
|
||
}
|
||
|
||
/* ------------ PIT helpers (bare metal only) ------------ */
|
||
|
||
/**
|
||
* @brief Programme the 8253/8254 PIT Channel 0 with a 16-bit reload value.
|
||
*
|
||
* Issues the mode command byte (0x34 = channel 0, lo/hi access, mode 2
|
||
* rate generator) followed by the low and high bytes of @p reload to the
|
||
* PIT data port. After this call the PIT downcounts at @c PIT_FREQ_HZ
|
||
* (1193182 Hz) and reloads automatically.
|
||
*
|
||
* @param reload 16-bit initial counter value (higher = longer period).
|
||
*/
|
||
static void pit_program_reload(uint16_t reload)
|
||
{
|
||
outb(PIT_COMMAND, 0x34);
|
||
outb(PIT_CHANNEL0_DATA, (uint8_t)(reload & 0xFF));
|
||
outb(PIT_CHANNEL0_DATA, (uint8_t)((reload >> 8) & 0xFF));
|
||
}
|
||
|
||
/**
|
||
* @brief Read the current 16-bit counter value from PIT Channel 0.
|
||
*
|
||
* Sends a latch command (0x00) to the PIT command port to atomically snap
|
||
* the current count, then reads the low and high bytes from the Channel 0
|
||
* data port in two @c INB instructions. Returns the counter as a 16-bit
|
||
* value; note the PIT counter counts down from the reload value.
|
||
*
|
||
* @return Current PIT Channel 0 counter value (decreasing, wraps at 0).
|
||
*/
|
||
static uint16_t pit_read_counter16(void)
|
||
{
|
||
outb(PIT_COMMAND, 0x00);
|
||
uint8_t lo = inb(PIT_CHANNEL0_DATA);
|
||
uint8_t hi = inb(PIT_CHANNEL0_DATA);
|
||
return (uint16_t)((hi << 8) | lo);
|
||
}
|
||
|
||
/**
|
||
* @brief Probe whether the PIT is available and counting.
|
||
*
|
||
* Programs the PIT with reload value 0xFFFF and polls up to 20,000 times
|
||
* for a counter change. Returns 1 if the counter advances (PIT running),
|
||
* 0 if it never changes (PIT absent, frozen, or emulation disabled). Used
|
||
* on the bare-metal path to decide whether to enable the PIT cross-check
|
||
* for TSC convergence.
|
||
*
|
||
* @return 1 if the PIT is running, 0 if it is unavailable.
|
||
*/
|
||
static int pit_probe_running(void)
|
||
{
|
||
pit_program_reload(0xFFFF);
|
||
uint16_t last = pit_read_counter16();
|
||
for (uint32_t i = 0; i < 20000u; ++i) {
|
||
uint16_t cur = pit_read_counter16();
|
||
if (cur != last) return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ------------ stats / convergence (bare metal) ------------ */
|
||
|
||
/**
|
||
* @brief Compute mean and standard deviation of an array of @c uint64_t samples.
|
||
*
|
||
* Uses 64-bit integer arithmetic throughout. For TSC-frequency samples
|
||
* (~3 GHz) with ≤16 windows, both the sum and variance accumulator remain
|
||
* within @c uint64_t range so long as deviations are below ~1 MHz (which is
|
||
* well within the @c CV_EPSILON_PPM = 1000 ppm convergence bound).
|
||
*
|
||
* Standard deviation is approximated via integer Newton-Raphson square root
|
||
* (10 iterations) over the variance.
|
||
*
|
||
* Sets @c *mean = @c *stddev = 0 when @p count is 0.
|
||
*
|
||
* @param samples Array of @p count frequency samples in Hz.
|
||
* @param count Number of valid samples.
|
||
* @param mean Output: arithmetic mean of @p samples.
|
||
* @param stddev Output: population standard deviation of @p samples.
|
||
*/
|
||
static void update_stats(const uint64_t *samples, uint32_t count, uint64_t *mean, uint64_t *stddev)
|
||
{
|
||
if (count == 0) {
|
||
*mean = 0;
|
||
*stddev = 0;
|
||
return;
|
||
}
|
||
|
||
/* For small sample counts (<=16) of Hz values (~3GHz), 64-bit sum is safe */
|
||
uint64_t sum = 0;
|
||
for (uint32_t i = 0; i < count; ++i) sum += samples[i];
|
||
uint64_t m = sum / count;
|
||
|
||
/* Variance: sum of (sample - mean)^2 / count
|
||
* Each (sample - mean) is at most ~1MHz for converged data.
|
||
* (1e6)^2 * 16 = 16e12, fits in 64 bits. */
|
||
uint64_t var_acc = 0;
|
||
for (uint32_t i = 0; i < count; ++i) {
|
||
int64_t d = (int64_t)samples[i] - (int64_t)m;
|
||
var_acc += (uint64_t)(d * d);
|
||
}
|
||
|
||
uint64_t variance = var_acc / count;
|
||
*mean = m;
|
||
|
||
if (variance == 0) {
|
||
*stddev = 0;
|
||
return;
|
||
}
|
||
|
||
/* Integer square root via Newton-Raphson */
|
||
uint64_t x = variance;
|
||
uint64_t r = x;
|
||
for (int i = 0; i < 10; ++i) {
|
||
r = (r + x / r) / 2;
|
||
}
|
||
*stddev = r;
|
||
}
|
||
|
||
/**
|
||
* @brief Measure TSC frequency over a fixed HPET tick window.
|
||
*
|
||
* Spins until the HPET main counter advances by @p target_ticks ticks,
|
||
* measuring the TSC delta over the same interval. Converts the result to Hz
|
||
* via @c muldiv64(). Returns 0 if the elapsed nanoseconds are zero (which
|
||
* indicates an invalid HPET frequency or overflow).
|
||
*
|
||
* Used exclusively on the bare-metal path; the tight spin loop would cause
|
||
* excessive VM-exits if called under a hypervisor.
|
||
*
|
||
* @param target_ticks HPET ticks to wait (~1 ms at 10 MHz HPET).
|
||
* @return Estimated TSC frequency in Hz, or 0 on error.
|
||
*/
|
||
static uint64_t calibrate_window_hpet(uint64_t target_ticks)
|
||
{
|
||
uint64_t start_cnt = hpet_read(HPET_MAIN_COUNTER);
|
||
uint64_t start_tsc = rdtsc();
|
||
|
||
while ((hpet_read(HPET_MAIN_COUNTER) - start_cnt) < target_ticks) {
|
||
/* tight loop; bare metal only */
|
||
}
|
||
|
||
uint64_t end_tsc = rdtsc();
|
||
uint64_t elapsed_ticks = hpet_read(HPET_MAIN_COUNTER) - start_cnt;
|
||
|
||
uint64_t elapsed_ns = muldiv64(elapsed_ticks, 1000000000ull, hpet_freq_hz);
|
||
if (elapsed_ns == 0) return 0;
|
||
|
||
uint64_t delta_tsc = end_tsc - start_tsc;
|
||
return muldiv64(delta_tsc, 1000000000ull, elapsed_ns);
|
||
}
|
||
|
||
/**
|
||
* @brief Measure TSC frequency over a fixed PIT tick window.
|
||
*
|
||
* Programs Channel 0 with reload 0xFFFF and polls until @p target_ticks
|
||
* PIT ticks have elapsed (up to 200,000 poll iterations). Returns the TSC
|
||
* Hz measured over the interval, or 0 if @c pit_available is false or the
|
||
* poll timed out.
|
||
*
|
||
* @param target_ticks Number of PIT ticks to measure over (~3.3 ms at 1.19 MHz).
|
||
* @return Estimated TSC frequency in Hz, or 0 if PIT is unavailable or timed out.
|
||
*/
|
||
static uint64_t calibrate_window_pit(uint32_t target_ticks)
|
||
{
|
||
if (!pit_available) return 0;
|
||
|
||
pit_program_reload(0xFFFF);
|
||
uint16_t start_cnt = pit_read_counter16();
|
||
uint64_t start_tsc = rdtsc();
|
||
|
||
for (uint32_t it = 0; it < 200000u; ++it) {
|
||
uint16_t cur = pit_read_counter16();
|
||
uint16_t delta = (uint16_t)((start_cnt - cur) & 0xFFFF);
|
||
if (delta >= (uint16_t)target_ticks) {
|
||
uint64_t end_tsc = rdtsc();
|
||
uint64_t elapsed_ns = muldiv64((uint64_t)delta, 1000000000ull, PIT_FREQ_HZ);
|
||
if (elapsed_ns == 0) return 0;
|
||
uint64_t delta_tsc = end_tsc - start_tsc;
|
||
return muldiv64(delta_tsc, 1000000000ull, elapsed_ns);
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Converge TSC frequency on bare metal using HPET + optional PIT cross-check.
|
||
*
|
||
* Runs @c WARMUP_WINDOWS (2) warm-up windows then up to @c SAMPLE_WINDOWS (8)
|
||
* measurement windows, alternating HPET and PIT samples. Declares convergence
|
||
* when @c CV_STABLE_COUNT (6) consecutive windows all have CV below
|
||
* @c CV_EPSILON_PPM (1000 ppm). If both HPET and PIT are active the inter-source
|
||
* drift must also be < @c CV_EPSILON_PPM before the stable counter increments.
|
||
*
|
||
* Fills @c calib_record on success. Calls @c timer_fatal() — never returns — on
|
||
* any hard error (HPET window failure, PIT required but unavailable).
|
||
*
|
||
* @param locked_hz Output: converged TSC frequency in Hz.
|
||
* @param pit_mean_out Output: mean PIT-derived TSC Hz (0 if PIT disabled).
|
||
* @return 0 on successful convergence, -1 if the sample limit was exhausted.
|
||
*/
|
||
static int converge_tsc_bare_metal(uint64_t *locked_hz, uint64_t *pit_mean_out)
|
||
{
|
||
uint64_t hpet_samples[SAMPLE_WINDOWS];
|
||
uint64_t pit_samples[SAMPLE_WINDOWS];
|
||
uint32_t hpet_count = 0;
|
||
uint32_t pit_count = 0;
|
||
|
||
console_println("Timer: warm-up...");
|
||
for (int i = 0; i < WARMUP_WINDOWS; ++i) {
|
||
uint64_t h = calibrate_window_hpet(WINDOW_TICKS_HPET);
|
||
if (h == 0) timer_fatal("Timer warm-up failed (HPET window).");
|
||
|
||
if (pit_available) {
|
||
uint64_t p = calibrate_window_pit(WINDOW_TICKS_PIT);
|
||
if (p == 0) {
|
||
if (TIMER_REQUIRE_PIT) timer_fatal("Timer warm-up failed (PIT window).");
|
||
pit_available = 0;
|
||
console_println("Timer: PIT disabled (warm-up stall).");
|
||
}
|
||
}
|
||
console_putc('.');
|
||
}
|
||
console_println("");
|
||
|
||
console_println("Timer: convergence...");
|
||
uint32_t stable_streak = 0;
|
||
|
||
for (int i = 0; i < SAMPLE_WINDOWS; ++i) {
|
||
uint64_t h = calibrate_window_hpet(WINDOW_TICKS_HPET);
|
||
if (h == 0) timer_fatal("Timer window failed (HPET).");
|
||
hpet_samples[hpet_count++] = h;
|
||
|
||
if (pit_available) {
|
||
uint64_t p = calibrate_window_pit(WINDOW_TICKS_PIT);
|
||
if (p == 0) {
|
||
if (TIMER_REQUIRE_PIT) timer_fatal("Timer window failed (PIT).");
|
||
pit_available = 0;
|
||
console_println("Timer: PIT disabled (convergence stall).");
|
||
} else {
|
||
pit_samples[pit_count++] = p;
|
||
}
|
||
}
|
||
|
||
if (hpet_count < 4) {
|
||
console_putc('.');
|
||
continue;
|
||
}
|
||
|
||
uint64_t mean_h = 0, std_h = 0;
|
||
update_stats(hpet_samples, hpet_count, &mean_h, &std_h);
|
||
uint64_t cv_h_ppm = (mean_h == 0) ? UINT64_MAX : muldiv64(std_h, 1000000ull, mean_h);
|
||
|
||
if (!pit_available) {
|
||
if (cv_h_ppm < CV_EPSILON_PPM) stable_streak++;
|
||
else stable_streak = 0;
|
||
|
||
if (stable_streak >= CV_STABLE_COUNT) {
|
||
windows_used = i + 1;
|
||
*locked_hz = mean_h;
|
||
*pit_mean_out = 0;
|
||
|
||
calib_record.hpet_hz = hpet_freq_hz;
|
||
calib_record.tsc_hz_mean = mean_h;
|
||
calib_record.pit_hz_mean = 0;
|
||
calib_record.cv_hpet_ppm = cv_h_ppm;
|
||
calib_record.cv_pit_ppm = 0;
|
||
calib_record.diff_ppm = 0;
|
||
calib_record.windows_used = windows_used;
|
||
calib_record.converged = 1;
|
||
calib_record.vm_mode = 0;
|
||
calib_record.trust = TIMER_TRUST_ABSOLUTE;
|
||
|
||
console_puts("Timer converged (HPET-only): TSC=");
|
||
print_dec(mean_h);
|
||
console_puts(" Hz, HPET CV=");
|
||
print_dec(cv_h_ppm);
|
||
console_println(" ppm");
|
||
return 0;
|
||
}
|
||
|
||
console_putc('.');
|
||
continue;
|
||
}
|
||
|
||
if (pit_count < 4) {
|
||
console_putc('.');
|
||
continue;
|
||
}
|
||
|
||
uint64_t mean_p = 0, std_p = 0;
|
||
update_stats(pit_samples, pit_count, &mean_p, &std_p);
|
||
|
||
uint64_t cv_p_ppm = (mean_p == 0) ? UINT64_MAX : muldiv64(std_p, 1000000ull, mean_p);
|
||
|
||
uint64_t min_hz = (mean_h < mean_p) ? mean_h : mean_p;
|
||
uint64_t max_hz = (mean_h > mean_p) ? mean_h : mean_p;
|
||
uint64_t diff_ppm = (min_hz == 0) ? UINT64_MAX : muldiv64(max_hz - min_hz, 1000000ull, min_hz);
|
||
|
||
if (cv_h_ppm < CV_EPSILON_PPM && cv_p_ppm < CV_EPSILON_PPM && diff_ppm < CV_EPSILON_PPM) stable_streak++;
|
||
else stable_streak = 0;
|
||
|
||
if (stable_streak >= CV_STABLE_COUNT) {
|
||
windows_used = i + 1;
|
||
*locked_hz = mean_h;
|
||
*pit_mean_out = mean_p;
|
||
|
||
calib_record.hpet_hz = hpet_freq_hz;
|
||
calib_record.tsc_hz_mean = mean_h;
|
||
calib_record.pit_hz_mean = mean_p;
|
||
calib_record.cv_hpet_ppm = cv_h_ppm;
|
||
calib_record.cv_pit_ppm = cv_p_ppm;
|
||
calib_record.diff_ppm = diff_ppm;
|
||
calib_record.windows_used = windows_used;
|
||
calib_record.converged = 1;
|
||
calib_record.vm_mode = 0;
|
||
calib_record.trust = TIMER_TRUST_ABSOLUTE;
|
||
|
||
console_puts("Timer converged: TSC=");
|
||
print_dec(mean_h);
|
||
console_puts(" Hz, HPET CV=");
|
||
print_dec(cv_h_ppm);
|
||
console_puts(" ppm, PIT CV=");
|
||
print_dec(cv_p_ppm);
|
||
console_puts(" ppm, diff=");
|
||
print_dec(diff_ppm);
|
||
console_println(" ppm");
|
||
return 0;
|
||
}
|
||
|
||
console_putc('.');
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
/* ------------ VM path ------------ */
|
||
|
||
/**
|
||
* @brief Derive TSC frequency from CPUID leaves 0x15 and 0x16.
|
||
*
|
||
* Checks leaf 0x15 (TSC/Crystal Ratio):
|
||
* - If EAX, EBX, and ECX are all non-zero: @c hz = @c ECX * @c EBX / @c EAX.
|
||
*
|
||
* Falls back to leaf 0x16 (base frequency in MHz) if leaf 0x15 gives 0.
|
||
*
|
||
* Both leaves are available on Broadwell and later Intel CPUs; older CPUs
|
||
* and AMD processors may return 0. Under QEMU/KVM, leaf 0x15 is usually
|
||
* populated; under QEMU/TCG it often is not.
|
||
*
|
||
* @return TSC frequency in Hz if derivable, 0 otherwise.
|
||
*/
|
||
static uint64_t derive_tsc_hz_from_cpuid(void)
|
||
{
|
||
uint32_t a, b, c, d;
|
||
|
||
/* CPUID.15H: TSC/Crystal ratio */
|
||
cpuid(0x15u, 0, &a, &b, &c, &d);
|
||
if (a != 0 && b != 0 && c != 0) {
|
||
/* hz = (c * b) / a using muldiv64 to avoid overflow */
|
||
uint64_t hz = muldiv64((uint64_t)c, (uint64_t)b, (uint64_t)a);
|
||
if (hz > 0) return hz;
|
||
}
|
||
|
||
/* CPUID.16H: base freq in MHz (best-effort) */
|
||
cpuid(0x16u, 0, &a, &b, &c, &d);
|
||
if (a != 0) {
|
||
uint64_t mhz = (uint64_t)(a & 0xFFFFu);
|
||
if (mhz > 0) return mhz * 1000000ull;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Initialise the timer subsystem in VM (hypervisor-present) mode.
|
||
*
|
||
* Skips HPET MMIO entirely (VM-exits would corrupt timing measurements).
|
||
* Instead:
|
||
* 1. Checks for invariant TSC. If absent and @c TIMER_VM_STRICT_INVARIANT_TSC
|
||
* is set, calls @c timer_fatal(). Otherwise logs a warning and uses
|
||
* @c TIMER_TRUST_RELATIVE mode.
|
||
* 2. Attempts to derive TSC Hz from CPUID leaves 0x15/0x16.
|
||
* 3. Falls back to @c calibrate_tsc_with_pmtimer() if CPUID yields 0.
|
||
* 4. If TSC Hz is still 0, logs a warning and forces @c TIMER_TRUST_RELATIVE
|
||
* (PM Timer only; no TSC→ns conversion).
|
||
* 5. Calls @c vm_timebase_init_from_pmtimer() to anchor relative time.
|
||
* 6. Fills @c calib_record with @c vm_mode = 1.
|
||
*/
|
||
static void init_timer_vm_mode(void)
|
||
{
|
||
console_println("Timer: VM mode detected (hypervisor present).");
|
||
console_println("Timer: HPET calibration disabled (VM-exit MMIO would poison timing).");
|
||
|
||
int inv = cpu_has_invariant_tsc();
|
||
|
||
if (!inv) {
|
||
if (TIMER_VM_STRICT_INVARIANT_TSC) {
|
||
timer_fatal("Timer: invariant TSC required under hypervisor (strict).");
|
||
}
|
||
|
||
console_println("Timer: WARNING: invariant TSC not present under hypervisor.");
|
||
console_println("Timer: continuing in RELATIVE mode (no determinism guarantees).");
|
||
calib_record.trust = TIMER_TRUST_RELATIVE;
|
||
} else {
|
||
calib_record.trust = TIMER_TRUST_ABSOLUTE;
|
||
}
|
||
|
||
if (!cpu_has_rdtscp()) {
|
||
console_println("Timer: RDTSCP not present; using RDTSC (less serialized).");
|
||
}
|
||
|
||
/* Try to lock TSC Hz anyway (useful even in RELATIVE mode) */
|
||
tsc_hz_locked = derive_tsc_hz_from_cpuid();
|
||
if (tsc_hz_locked == 0) {
|
||
console_println("Timer: CPUID frequency unavailable; trying PM Timer...");
|
||
tsc_hz_locked = calibrate_tsc_with_pmtimer();
|
||
}
|
||
|
||
if (tsc_hz_locked == 0) {
|
||
/* Without a frequency, TSC->ns is meaningless. In VM RELATIVE mode we can still
|
||
provide PM Timer-based ns. */
|
||
console_println("Timer: WARNING: could not derive TSC frequency; PM Timer will be used for RELATIVE ns.");
|
||
calib_record.trust = TIMER_TRUST_RELATIVE;
|
||
}
|
||
|
||
/* Initialize VM PM Timer timebase for RELATIVE ns */
|
||
vm_timebase_init_from_pmtimer();
|
||
|
||
calib_record.hpet_hz = 0;
|
||
calib_record.tsc_hz_mean = tsc_hz_locked;
|
||
calib_record.pit_hz_mean = 0;
|
||
calib_record.cv_hpet_ppm = 0;
|
||
calib_record.cv_pit_ppm = 0;
|
||
calib_record.diff_ppm = 0;
|
||
calib_record.windows_used = 0;
|
||
calib_record.converged = 1;
|
||
calib_record.vm_mode = 1;
|
||
|
||
console_puts("Timer: trust=");
|
||
print_dec((uint64_t)calib_record.trust);
|
||
console_puts(" (0=NONE,1=REL,2=ABS), TSC=");
|
||
print_dec(tsc_hz_locked);
|
||
console_println(" Hz");
|
||
}
|
||
|
||
/* ------------ public API ------------ */
|
||
|
||
/**
|
||
* @brief Initialise the kernel timer subsystem (M5).
|
||
*
|
||
* Detects the execution environment (bare metal vs. hypervisor) and
|
||
* follows the appropriate calibration path:
|
||
*
|
||
* - **VM / hypervisor path** (CPUID.1:ECX[31] set): calls
|
||
* @c init_timer_vm_mode() — HPET MMIO disabled, TSC from CPUID/PM Timer.
|
||
* - **Bare metal path**: maps HPET MMIO, enables the counter, optionally
|
||
* probes the PIT, then calls @c converge_tsc_bare_metal() until TSC Hz
|
||
* converges within @c CV_EPSILON_PPM ppm. Calls @c timer_fatal() on any
|
||
* hard failure.
|
||
*
|
||
* On success, @c tsc_hz_locked holds the calibrated TSC frequency and
|
||
* @c calib_record is fully populated. @c timer_now_ns() is safe to call
|
||
* after this function returns.
|
||
*
|
||
* @param boot_info BootInfo pointer from the UEFI loader; @c acpi_table is
|
||
* used to discover the ACPI PM_TMR_BLK port at runtime
|
||
* (see @c fadt_find_pm_tmr_port()) rather than assuming a
|
||
* fixed machine type's legacy address.
|
||
* @return 0 on success; calls @c timer_fatal() (never returns) on hard failure.
|
||
*/
|
||
int timer_init(BootInfo *boot_info)
|
||
{
|
||
console_println("Timer: init start");
|
||
|
||
/* default record */
|
||
calib_record.trust = TIMER_TRUST_NONE;
|
||
calib_record.converged = 0;
|
||
calib_record.vm_mode = 0;
|
||
|
||
if (boot_info) {
|
||
uint16_t discovered = fadt_find_pm_tmr_port(boot_info->acpi_table);
|
||
if (discovered != 0) {
|
||
pmtimer_port = discovered;
|
||
console_puts("Timer: PM_TMR_BLK discovered from FADT at port ");
|
||
print_dec((uint64_t)pmtimer_port);
|
||
console_println("");
|
||
} else {
|
||
console_puts("Timer: WARNING: FADT PM_TMR_BLK lookup failed; using fallback port ");
|
||
print_dec((uint64_t)pmtimer_port);
|
||
console_println("");
|
||
}
|
||
}
|
||
|
||
if (running_under_hypervisor()) {
|
||
init_timer_vm_mode();
|
||
return 0;
|
||
}
|
||
|
||
/* Bare metal path */
|
||
if (vmm_map_range(HPET_VIRT_BASE, HPET_PHYS_BASE, 0x1000, VMM_FLAG_WRITABLE) != 0) {
|
||
timer_fatal("Failed to map HPET MMIO.");
|
||
}
|
||
hpet_regs = (volatile uint64_t *)(uintptr_t)HPET_VIRT_BASE;
|
||
|
||
uint64_t cap = hpet_read(HPET_GEN_CAP_ID);
|
||
uint64_t period_fs = cap >> 32;
|
||
if (period_fs == 0) {
|
||
timer_fatal("HPET period invalid.");
|
||
}
|
||
hpet_freq_hz = 1000000000000000ull / period_fs;
|
||
|
||
enable_hpet();
|
||
sanity_check_hpet_running_quick();
|
||
|
||
pit_available = pit_probe_running() ? 1 : 0;
|
||
if (!pit_available && TIMER_REQUIRE_PIT) {
|
||
timer_fatal("PIT required but not available.");
|
||
}
|
||
|
||
if (converge_tsc_bare_metal(&tsc_hz_locked, &pit_tsc_hz_mean) != 0) {
|
||
timer_fatal("TSC did not converge.");
|
||
}
|
||
|
||
console_puts("Timer: HPET freq = ");
|
||
print_dec(hpet_freq_hz);
|
||
console_println(" Hz");
|
||
|
||
console_puts("Timer: TSC locked = ");
|
||
print_dec(tsc_hz_locked);
|
||
console_println(" Hz");
|
||
|
||
if (pit_available) {
|
||
console_puts("Timer: PIT mean = ");
|
||
print_dec(pit_tsc_hz_mean);
|
||
console_println(" Hz");
|
||
} else {
|
||
console_println("Timer: PIT cross-check skipped.");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Return the locked TSC frequency in Hz.
|
||
*
|
||
* Returns the value established by @c timer_init(). Zero means calibration
|
||
* has not completed or failed to derive a TSC frequency (PM Timer relative
|
||
* mode only).
|
||
*
|
||
* @return TSC frequency in Hz, or 0 if unknown.
|
||
*/
|
||
uint64_t timer_tsc_hz(void)
|
||
{
|
||
return tsc_hz_locked;
|
||
}
|
||
|
||
/**
|
||
* @brief Return nanoseconds since boot using the ACPI PM Timer (VM relative mode).
|
||
*
|
||
* Reads the current PM Timer counter, computes ticks elapsed since
|
||
* @c vm_pm_start (set by @c vm_timebase_init_from_pmtimer()), converts to
|
||
* nanoseconds via @c muldiv64(), and adds @c vm_ns_base. Used when the
|
||
* calibration record shows @c vm_mode = 1 and @c trust < TIMER_TRUST_ABSOLUTE.
|
||
*
|
||
* @return Nanoseconds elapsed since @c vm_timebase_init_from_pmtimer() was called.
|
||
*/
|
||
static uint64_t timer_now_ns_vm_relative(void)
|
||
{
|
||
/* Monotonic-ish RELATIVE ns based on ACPI PM Timer (port I/O). */
|
||
uint32_t cur = pmtimer_read() & (uint32_t)PMTIMER_MASK;
|
||
uint64_t ticks = pmtimer_delta(vm_pm_start, cur);
|
||
|
||
uint64_t ns = muldiv64(ticks, 1000000000ull, PMTIMER_FREQ_HZ);
|
||
return vm_ns_base + ns;
|
||
}
|
||
|
||
/**
|
||
* @brief Return the current monotonic time in nanoseconds.
|
||
*
|
||
* Selects the best available time source based on the calibration record:
|
||
*
|
||
* - @c TIMER_TRUST_NONE: returns 0 (no time base yet).
|
||
* - VM mode + trust < @c TIMER_TRUST_ABSOLUTE: returns PM Timer relative ns.
|
||
* - Absolute mode (@c tsc_hz_locked > 0): reads TSC (RDTSCP if supported,
|
||
* else RDTSC) and converts to ns via @c muldiv64().
|
||
*
|
||
* Called by the HAL (@c sk_hal_time_ns()) to feed the physics engine.
|
||
*
|
||
* @return Nanoseconds since boot, or 0 if the timer is not yet calibrated.
|
||
*/
|
||
uint64_t timer_now_ns(void)
|
||
{
|
||
/* If we don't have a time base yet, be honest. */
|
||
if (calib_record.trust == TIMER_TRUST_NONE) return 0;
|
||
|
||
/* VM RELATIVE mode: return PM Timer derived ns */
|
||
if (calib_record.vm_mode && calib_record.trust < TIMER_TRUST_ABSOLUTE) {
|
||
return timer_now_ns_vm_relative();
|
||
}
|
||
|
||
/* ABSOLUTE mode: use TSC -> ns */
|
||
if (tsc_hz_locked == 0) return 0;
|
||
|
||
uint64_t t;
|
||
if (cpu_has_rdtscp()) {
|
||
uint32_t aux;
|
||
t = rdtscp_aux(&aux);
|
||
} else {
|
||
t = rdtsc();
|
||
}
|
||
|
||
return muldiv64(t, 1000000000ull, tsc_hz_locked);
|
||
}
|
||
|
||
/**
|
||
* @brief Check TSC drift against the HPET reference on bare metal.
|
||
*
|
||
* Takes a single HPET calibration window and compares the measured TSC Hz
|
||
* to @c tsc_hz_locked. If the drift exceeds @c DRIFT_EPSILON_PPM (2000 ppm),
|
||
* calls @c timer_fatal() — never returns. Returns -1 if either reading is
|
||
* invalid.
|
||
*
|
||
* Skipped in VM mode (@c timer_check_drift_now() returns 0 immediately).
|
||
*
|
||
* @return 0 if drift is within bound, -1 on measurement failure.
|
||
*/
|
||
static int timer_drift_check_bare_metal(void)
|
||
{
|
||
uint64_t current = calibrate_window_hpet(WINDOW_TICKS_HPET);
|
||
if (current == 0 || tsc_hz_locked == 0) return -1;
|
||
|
||
uint64_t min_hz = (current < tsc_hz_locked) ? current : tsc_hz_locked;
|
||
uint64_t max_hz = (current > tsc_hz_locked) ? current : tsc_hz_locked;
|
||
|
||
uint64_t diff_ppm = (min_hz == 0) ? UINT64_MAX
|
||
: muldiv64(max_hz - min_hz, 1000000ull, min_hz);
|
||
|
||
if (diff_ppm > DRIFT_EPSILON_PPM) {
|
||
timer_fatal("Timer drift exceeded runtime bound.");
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Perform a runtime TSC drift check (no-op in VM mode).
|
||
*
|
||
* In bare-metal mode delegates to @c timer_drift_check_bare_metal(). In VM
|
||
* mode returns 0 immediately: VM drift checking requires a VM-friendly
|
||
* reference (such as pvclock) that is not yet implemented.
|
||
*
|
||
* @return 0 if drift is acceptable or in VM mode, -1 on bare-metal
|
||
* measurement failure.
|
||
*/
|
||
int timer_check_drift_now(void)
|
||
{
|
||
if (calib_record.vm_mode) {
|
||
/* VM policy: drift checks require a VM-friendly reference (e.g., pvclock) */
|
||
return 0;
|
||
}
|
||
return timer_drift_check_bare_metal();
|
||
}
|
||
|
||
/**
|
||
* @brief Return a pointer to the current timer calibration record.
|
||
*
|
||
* Provides read-only access to the @c calib_record populated by
|
||
* @c timer_init(). Callers can inspect fields such as @c trust,
|
||
* @c tsc_hz_mean, @c cv_hpet_ppm, and @c converged for diagnostic output
|
||
* or to determine whether absolute timing guarantees are available.
|
||
*
|
||
* @return Pointer to the static @c timer_calibration_record_t (never NULL).
|
||
*/
|
||
const timer_calibration_record_t *timer_calibration_record(void)
|
||
{
|
||
return &calib_record;
|
||
}
|
||
|
||
/* ============================================================================
|
||
* M5 Heartbeat Subsystem
|
||
* ============================================================================
|
||
*
|
||
* Implements TIME-TICKS and TIME-TRUST:
|
||
* - TIME-TICKS: Monotonic heartbeat count
|
||
* - TIME-TRUST: Continuous Q48.16 confidence derived from timing variance
|
||
*
|
||
* On each tick:
|
||
* 1. Sample TSC
|
||
* 2. Compute delta from last tick
|
||
* 3. Record deviation in rolling window
|
||
* 4. Compute variance from window
|
||
* 5. Derive TIME-TRUST from variance
|
||
*/
|
||
|
||
/**
|
||
* @brief Read the raw counter the amd64 heartbeat is paced against.
|
||
*
|
||
* Item 0.8 (FABRIC.md §25.1): the shared heartbeat.c owns
|
||
* heartbeat_init()/heartbeat_tick()/heartbeat_service()/heartbeat_ticks()/
|
||
* heartbeat_trust()/heartbeat_state() and the variance/trust math that used
|
||
* to live in this file. This is the one piece that stays per-architecture
|
||
* -- the same @c rdtsc() the rest of this file's TSC calibration already
|
||
* uses, not a separate/different source.
|
||
*
|
||
* @return Current TSC value.
|
||
*/
|
||
uint64_t heartbeat_read_counter(void)
|
||
{
|
||
return rdtsc();
|
||
}
|