927 lines
32 KiB
C
927 lines
32 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.
|
||
|
||
*/
|
||
|
||
/**
|
||
* parity.c - Parity packet collection and canonical hash
|
||
*
|
||
* Implements M7 parity validation for hosted vs kernel comparison.
|
||
*
|
||
* M7 Normative Rules enforced here:
|
||
* - Rule 1: word_id is monotonic creation index
|
||
* - Rule 2: Colon bodies hashed as word_id sequence
|
||
* - Rule 3: Dictionary traversal in creation order
|
||
*/
|
||
|
||
#include "starkernel/vm/parity.h"
|
||
#include "vm.h"
|
||
|
||
#ifdef __STARKERNEL__
|
||
#include "console.h"
|
||
#include "starkernel/hal/hal.h"
|
||
#else
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#endif
|
||
|
||
#include <stdbool.h>
|
||
#include <string.h>
|
||
|
||
#ifndef SK_PARITY_DEBUG
|
||
#define SK_PARITY_DEBUG 0
|
||
#endif
|
||
|
||
static void print_u32(uint32_t val);
|
||
static void print_hex64(uint64_t val);
|
||
static void print_str(const char *s);
|
||
static void print_nl(void);
|
||
|
||
#if SK_PARITY_DEBUG
|
||
#define SK_PARITY_DEBUG_PREFIX "SKPD:"
|
||
enum sk_parity_ptr_region {
|
||
SK_PTR_REGION_NULL = 0,
|
||
SK_PTR_REGION_VM_ARENA,
|
||
SK_PTR_REGION_TEXT,
|
||
SK_PTR_REGION_RODATA,
|
||
SK_PTR_REGION_DATA,
|
||
SK_PTR_REGION_BSS,
|
||
SK_PTR_REGION_DIRECTMAP,
|
||
SK_PTR_REGION_UNKNOWN
|
||
};
|
||
|
||
static void sk_parity_debug_log_msg(const char *msg);
|
||
static bool sk_parity_debug_is_canonical(uint64_t addr);
|
||
static enum sk_parity_ptr_region sk_parity_classify_region(struct VM *vm, const void *ptr);
|
||
static const char *sk_parity_region_name(enum sk_parity_ptr_region region);
|
||
static void sk_parity_debug_print_word_name(const DictEntry *entry);
|
||
static void sk_parity_debug_panic(struct VM *vm,
|
||
const char *reason,
|
||
const DictEntry *entry,
|
||
uint32_t word_index,
|
||
const void *header_ptr,
|
||
const void *xt_ptr,
|
||
const void *bad_ptr,
|
||
enum sk_parity_ptr_region region,
|
||
bool canonical);
|
||
static void sk_parity_debug_log_ptr(const char *label,
|
||
const void *ptr,
|
||
enum sk_parity_ptr_region region,
|
||
bool canonical);
|
||
static void sk_parity_debug_check_ptr(struct VM *vm,
|
||
const char *label,
|
||
const DictEntry *entry,
|
||
uint32_t word_index,
|
||
const void *header_ptr,
|
||
const void *xt_ptr,
|
||
const void *ptr);
|
||
#else
|
||
#define sk_parity_debug_log_msg(msg) do { (void)(msg); } while (0)
|
||
#define sk_parity_debug_check_ptr(vm,label,entry,idx,hptr,xt,ptr) \
|
||
do { (void)(vm); (void)(label); (void)(entry); (void)(idx); (void)(hptr); (void)(xt); (void)(ptr); } while (0)
|
||
#endif
|
||
|
||
/* Maximum dictionary entries for traversal array */
|
||
#define MAX_DICT_ENTRIES 2048
|
||
|
||
#if SK_PARITY_DEBUG
|
||
#define SK_PARITY_CANONICAL_MASK 0xffff800000000000ULL
|
||
#define SK_PARITY_DIRECTMAP_BASE 0xffff800000000000ULL
|
||
/* Note: For __STARKERNEL__ builds, we use sk_hal_text_start/end() from hal.h
|
||
* to avoid GOT indirection issues with -fPIC. The rodata/data/bss section
|
||
* checks are skipped since XT pointers should only be in text section. */
|
||
|
||
/**
|
||
* @brief Emit a @c SK_PARITY_DEBUG_PREFIX-prefixed diagnostic message line.
|
||
*
|
||
* Prints @c "SKPD:<msg>\n" to the kernel console or stdout. Used throughout
|
||
* parity traversal to trace the hash walk when @c SK_PARITY_DEBUG=1.
|
||
* Prints @c "<null>" if @p msg is @c NULL.
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero; all call sites are
|
||
* macro-eliminated otherwise.
|
||
*
|
||
* @param msg Null-terminated diagnostic message string.
|
||
*/
|
||
static void sk_parity_debug_log_msg(const char *msg)
|
||
{
|
||
print_str(SK_PARITY_DEBUG_PREFIX);
|
||
print_str(msg ? msg : "<null>");
|
||
print_nl();
|
||
}
|
||
|
||
/**
|
||
* @brief Test whether a 64-bit address is a canonical x86-64 virtual address.
|
||
*
|
||
* On x86-64, bits [63:48] must all equal bit 47 (sign-extend). This function
|
||
* checks that invariant: extracts bit 47, then verifies that the upper 16 bits
|
||
* (@c SK_PARITY_CANONICAL_MASK) are either all-zero (user space) or all-one
|
||
* (kernel space).
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
|
||
*
|
||
* @param addr 64-bit virtual address to test.
|
||
* @return @c true if @p addr is canonical, @c false otherwise.
|
||
*/
|
||
static bool sk_parity_debug_is_canonical(uint64_t addr)
|
||
{
|
||
uint64_t sign = (addr >> 47) & 1ULL;
|
||
uint64_t mask = SK_PARITY_CANONICAL_MASK;
|
||
return sign ? ((addr & mask) == mask) : ((addr & mask) == 0);
|
||
}
|
||
|
||
/**
|
||
* @brief Classify a pointer into a known memory region for debug diagnostics.
|
||
*
|
||
* Checks @p ptr against known address ranges in the following priority order:
|
||
* 1. @c NULL → @c SK_PTR_REGION_NULL
|
||
* 2. VM arena [@c vm->memory, @c vm->memory + VM_MEMORY_SIZE) →
|
||
* @c SK_PTR_REGION_VM_ARENA
|
||
* 3. Kernel .text section [sk_hal_text_start(), sk_hal_text_end()) →
|
||
* @c SK_PTR_REGION_TEXT (kernel build only; rodata/data/bss skipped)
|
||
* 4. Direct-map region (≥ @c SK_PARITY_DIRECTMAP_BASE) →
|
||
* @c SK_PTR_REGION_DIRECTMAP (kernel build only)
|
||
* 5. Anything else → @c SK_PTR_REGION_UNKNOWN
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
|
||
*
|
||
* @param vm Active VM (used for arena base/size); may be @c NULL.
|
||
* @param ptr Pointer to classify.
|
||
* @return One of the @c sk_parity_ptr_region enum values.
|
||
*/
|
||
static enum sk_parity_ptr_region sk_parity_classify_region(struct VM *vm, const void *ptr)
|
||
{
|
||
if (!ptr) {
|
||
return SK_PTR_REGION_NULL;
|
||
}
|
||
uintptr_t addr = (uintptr_t)ptr;
|
||
if (vm && vm->memory) {
|
||
uintptr_t arena_start = (uintptr_t)vm->memory;
|
||
uintptr_t arena_end = arena_start + VM_MEMORY_SIZE;
|
||
if (addr >= arena_start && addr < arena_end) {
|
||
return SK_PTR_REGION_VM_ARENA;
|
||
}
|
||
}
|
||
#ifdef __STARKERNEL__
|
||
/* Use HAL getters to avoid GOT indirection issues with -fPIC */
|
||
uintptr_t text_start = (uintptr_t)sk_hal_text_start();
|
||
uintptr_t text_end = (uintptr_t)sk_hal_text_end();
|
||
if (addr >= text_start && addr < text_end) {
|
||
return SK_PTR_REGION_TEXT;
|
||
}
|
||
/* Note: rodata/data/bss section checks skipped for kernel builds.
|
||
* Function pointers (XTs) should only be in the text section anyway. */
|
||
if (addr >= SK_PARITY_DIRECTMAP_BASE) {
|
||
return SK_PTR_REGION_DIRECTMAP;
|
||
}
|
||
#endif
|
||
return SK_PTR_REGION_UNKNOWN;
|
||
}
|
||
|
||
/**
|
||
* @brief Return a human-readable name for a @c sk_parity_ptr_region value.
|
||
*
|
||
* Used by debug print helpers to label pointer regions in diagnostic output.
|
||
* Returns @c "unknown" for any value not listed in the enum.
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
|
||
*
|
||
* @param region Enum value from @c sk_parity_classify_region().
|
||
* @return Pointer to a static string literal naming the region.
|
||
*/
|
||
static const char *sk_parity_region_name(enum sk_parity_ptr_region region)
|
||
{
|
||
switch (region) {
|
||
case SK_PTR_REGION_NULL: return "null";
|
||
case SK_PTR_REGION_VM_ARENA: return "vm_arena";
|
||
case SK_PTR_REGION_TEXT: return "text";
|
||
case SK_PTR_REGION_RODATA: return "rodata";
|
||
case SK_PTR_REGION_DATA: return "data";
|
||
case SK_PTR_REGION_BSS: return "bss";
|
||
case SK_PTR_REGION_DIRECTMAP: return "directmap";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Print the name of a dictionary entry to the console for debug output.
|
||
*
|
||
* Copies @p entry->name into a local null-terminated buffer (bounded by
|
||
* @c WORD_NAME_MAX) and emits it via @c print_str(). Prints @c "<none>" if
|
||
* @p entry is @c NULL. Used by @c sk_parity_debug_panic() to identify the
|
||
* word being hashed when a pointer violation is detected.
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
|
||
*
|
||
* @param entry Dictionary entry whose name to print; may be @c NULL.
|
||
*/
|
||
static void sk_parity_debug_print_word_name(const DictEntry *entry)
|
||
{
|
||
if (!entry) {
|
||
print_str("<none>");
|
||
return;
|
||
}
|
||
uint8_t len = entry->name_len;
|
||
if (len == 0 || len > WORD_NAME_MAX) {
|
||
len = (len > WORD_NAME_MAX) ? WORD_NAME_MAX : len;
|
||
}
|
||
char buf[WORD_NAME_MAX + 1];
|
||
if (len > 0) {
|
||
memcpy(buf, entry->name, len);
|
||
}
|
||
buf[len] = '\0';
|
||
print_str(buf);
|
||
}
|
||
|
||
/**
|
||
* @brief Log a pointer's address, canonicality, and region to the console.
|
||
*
|
||
* Emits a single debug line in the format:
|
||
* @c "SKPD:<label>=0xADDR canon=Y/N region=<region_name>\n".
|
||
* Used by @c sk_parity_debug_check_ptr() to trace each pointer inspected
|
||
* during the canonical hash walk.
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
|
||
*
|
||
* @param label Short string label for the pointer (e.g., "colon_entry").
|
||
* @param ptr Pointer whose address is to be printed.
|
||
* @param region Region classification from @c sk_parity_classify_region().
|
||
* @param canonical Whether @p ptr is a canonical x86-64 address.
|
||
*/
|
||
static void sk_parity_debug_log_ptr(const char *label,
|
||
const void *ptr,
|
||
enum sk_parity_ptr_region region,
|
||
bool canonical)
|
||
{
|
||
print_str(SK_PARITY_DEBUG_PREFIX);
|
||
print_str(label ? label : "ptr");
|
||
print_str("=");
|
||
print_hex64((uint64_t)(uintptr_t)ptr);
|
||
print_str(" canon=");
|
||
print_str(canonical ? "Y" : "N");
|
||
print_str(" region=");
|
||
print_str(sk_parity_region_name(region));
|
||
print_nl();
|
||
}
|
||
|
||
/**
|
||
* @brief Emit a parity pointer-violation diagnostic and halt.
|
||
*
|
||
* Called when @c sk_parity_debug_check_ptr() finds a pointer that is either
|
||
* non-canonical or in an unexpected memory region during the hash walk.
|
||
* Prints a multi-line @c "SK_PARITY_PANIC:" report containing:
|
||
* - the violation reason string
|
||
* - the word's creation-order index and name
|
||
* - the entry's header and XT pointers
|
||
* - the bad pointer, its canonicality flag, and its region
|
||
* - the VM's current @c HERE and @c LATEST values
|
||
*
|
||
* Then calls @c sk_hal_panic() in kernel builds or @c abort() in hosted
|
||
* builds; never returns.
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero.
|
||
*
|
||
* @param vm Active VM (for HERE/LATEST diagnostics); may be @c NULL.
|
||
* @param reason Short string describing the violation (e.g., "colon_entry").
|
||
* @param entry Dictionary entry being processed when the violation occurred.
|
||
* @param word_index Creation-order ordinal of @p entry.
|
||
* @param header_ptr Pointer to the dictionary entry header.
|
||
* @param xt_ptr Pointer to the word's execution token (func pointer).
|
||
* @param bad_ptr The offending pointer that failed the check.
|
||
* @param region Region classification of @p bad_ptr.
|
||
* @param canonical Whether @p bad_ptr is a canonical x86-64 address.
|
||
*/
|
||
static void sk_parity_debug_panic(struct VM *vm,
|
||
const char *reason,
|
||
const DictEntry *entry,
|
||
uint32_t word_index,
|
||
const void *header_ptr,
|
||
const void *xt_ptr,
|
||
const void *bad_ptr,
|
||
enum sk_parity_ptr_region region,
|
||
bool canonical)
|
||
{
|
||
print_str("SK_PARITY_PANIC: ");
|
||
print_str(reason ? reason : "unknown");
|
||
print_nl();
|
||
|
||
print_str(" word_idx=");
|
||
print_u32(word_index);
|
||
print_str(" name=");
|
||
sk_parity_debug_print_word_name(entry);
|
||
print_nl();
|
||
|
||
print_str(" header_ptr=");
|
||
print_hex64((uint64_t)(uintptr_t)header_ptr);
|
||
print_str(" xt_ptr=");
|
||
print_hex64((uint64_t)(uintptr_t)xt_ptr);
|
||
print_nl();
|
||
|
||
print_str(" bad_ptr=");
|
||
print_hex64((uint64_t)(uintptr_t)bad_ptr);
|
||
print_str(" canon=");
|
||
print_str(canonical ? "Y" : "N");
|
||
print_str(" region=");
|
||
print_str(sk_parity_region_name(region));
|
||
print_nl();
|
||
|
||
if (vm) {
|
||
print_str(" HERE=");
|
||
print_hex64((uint64_t)vm->here);
|
||
print_str(" LATEST=");
|
||
print_hex64((uint64_t)(uintptr_t)vm->latest);
|
||
print_nl();
|
||
}
|
||
#ifdef __STARKERNEL__
|
||
sk_hal_panic("parity pointer violation");
|
||
#else
|
||
fprintf(stderr, "Parity pointer violation\n");
|
||
abort();
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief Validate a pointer and panic if it is non-canonical or out-of-region.
|
||
*
|
||
* Classifies @p ptr with @c sk_parity_classify_region() and tests it for
|
||
* canonicality with @c sk_parity_debug_is_canonical(). Logs the result via
|
||
* @c sk_parity_debug_log_ptr(). If the pointer is non-canonical or in the
|
||
* @c SK_PTR_REGION_UNKNOWN region, calls @c sk_parity_debug_panic() which
|
||
* never returns.
|
||
*
|
||
* This is the central guard point called at every pointer dereference in
|
||
* the hash walk to catch dictionary corruption or bad function pointers
|
||
* before they cause silent incorrect results.
|
||
*
|
||
* Only compiled when @c SK_PARITY_DEBUG is non-zero; call sites are
|
||
* macro-eliminated in release builds.
|
||
*
|
||
* @param vm Active VM (passed to @c sk_parity_classify_region()).
|
||
* @param label Short label string identifying which pointer is being checked.
|
||
* @param entry Current dictionary entry being hashed.
|
||
* @param word_index Creation-order ordinal of @p entry.
|
||
* @param header_ptr Pointer to the entry header (for panic diagnostics).
|
||
* @param xt_ptr Pointer to the entry's XT (for panic diagnostics).
|
||
* @param ptr The pointer to validate.
|
||
*/
|
||
static void sk_parity_debug_check_ptr(struct VM *vm,
|
||
const char *label,
|
||
const DictEntry *entry,
|
||
uint32_t word_index,
|
||
const void *header_ptr,
|
||
const void *xt_ptr,
|
||
const void *ptr)
|
||
{
|
||
enum sk_parity_ptr_region region = sk_parity_classify_region(vm, ptr);
|
||
bool canonical = sk_parity_debug_is_canonical((uint64_t)(uintptr_t)ptr);
|
||
sk_parity_debug_log_ptr(label, ptr, region, canonical);
|
||
if (!canonical || region == SK_PTR_REGION_UNKNOWN) {
|
||
sk_parity_debug_panic(vm, label, entry, word_index, header_ptr, xt_ptr, ptr, region, canonical);
|
||
}
|
||
}
|
||
#endif /* SK_PARITY_DEBUG */
|
||
|
||
/**
|
||
* @brief FNV-1a 64-bit incremental hash over a byte buffer.
|
||
*
|
||
* Updates a running FNV-1a hash by folding @p len bytes from @p data into
|
||
* @p hash. The caller supplies the current hash state so that multiple
|
||
* buffers can be chained: pass @c FNV1A_64_OFFSET_BASIS for the first call
|
||
* and the returned value as @p hash for subsequent calls.
|
||
*
|
||
* Algorithm: for each byte @c b, @c hash ^= b then @c hash *= FNV1A_64_PRIME.
|
||
*
|
||
* @param data Pointer to the byte buffer to hash.
|
||
* @param len Number of bytes to consume.
|
||
* @param hash Running hash state; seed with @c FNV1A_64_OFFSET_BASIS.
|
||
* @return Updated hash state after processing all @p len bytes.
|
||
*/
|
||
uint64_t fnv1a_64(const uint8_t *data, size_t len, uint64_t hash) {
|
||
for (size_t i = 0; i < len; i++) {
|
||
hash ^= data[i];
|
||
hash *= FNV1A_64_PRIME;
|
||
}
|
||
return hash;
|
||
}
|
||
|
||
/**
|
||
* @brief FNV-1a 64-bit incremental hash over a single byte.
|
||
*
|
||
* Convenience wrapper for folding one byte into a running FNV-1a hash state.
|
||
* Used by @c fnv1a_64_u32() to hash multi-byte values byte-by-byte in a
|
||
* defined endian order.
|
||
*
|
||
* @param byte Single byte to fold into the hash.
|
||
* @param hash Running hash state.
|
||
* @return Updated hash state.
|
||
*/
|
||
static uint64_t fnv1a_64_u8(uint8_t byte, uint64_t hash) {
|
||
hash ^= byte;
|
||
hash *= FNV1A_64_PRIME;
|
||
return hash;
|
||
}
|
||
|
||
/**
|
||
* @brief FNV-1a 64-bit incremental hash over a 32-bit value (little-endian).
|
||
*
|
||
* Folds all four bytes of @p val into @p hash in little-endian order
|
||
* (least-significant byte first) via four @c fnv1a_64_u8() calls. Used to
|
||
* hash @c word_id fields — which are @c uint32_t — into the canonical
|
||
* dictionary hash in a byte-order-stable way.
|
||
*
|
||
* @param val 32-bit value to hash.
|
||
* @param hash Running FNV-1a hash state.
|
||
* @return Updated hash state after consuming all four bytes.
|
||
*/
|
||
static uint64_t fnv1a_64_u32(uint32_t val, uint64_t hash) {
|
||
hash = fnv1a_64_u8((uint8_t)(val & 0xFF), hash);
|
||
hash = fnv1a_64_u8((uint8_t)((val >> 8) & 0xFF), hash);
|
||
hash = fnv1a_64_u8((uint8_t)((val >> 16) & 0xFF), hash);
|
||
hash = fnv1a_64_u8((uint8_t)((val >> 24) & 0xFF), hash);
|
||
return hash;
|
||
}
|
||
|
||
/**
|
||
* @brief Count the number of entries in the VM dictionary.
|
||
*
|
||
* Walks the linked list starting at @c vm->latest, following each entry's
|
||
* @c link pointer until @c NULL. Stops early and returns @c MAX_DICT_ENTRIES
|
||
* if the list exceeds that limit, preventing runaway traversal on a corrupt
|
||
* dictionary.
|
||
*
|
||
* @param vm Active VM whose dictionary will be counted; returns 0 if @c NULL.
|
||
* @return Number of dictionary entries found, capped at @c MAX_DICT_ENTRIES.
|
||
*/
|
||
uint32_t sk_dict_word_count(struct VM *vm) {
|
||
if (!vm) return 0;
|
||
|
||
uint32_t count = 0;
|
||
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
|
||
count++;
|
||
if (count >= MAX_DICT_ENTRIES) break;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
/**
|
||
* @brief Hash the compiled body of a colon definition as a sequence of word IDs.
|
||
*
|
||
* Implements M7 Rule 2: colon bodies are hashed as their @c word_id sequence,
|
||
* not as raw code pointers, ensuring the hash is stable across address-space
|
||
* layout changes (ASLR, relocation, different build addresses).
|
||
*
|
||
* Traversal reads each cell from the threaded body in order:
|
||
* - A non-NULL @c DictEntry pointer → its @c word_id is hashed.
|
||
* - An @c EXIT entry → its @c word_id is hashed and traversal stops.
|
||
* - A @c LIT entry → its @c word_id is hashed and the following cell (the
|
||
* literal value) is hashed as eight individual bytes (little-endian).
|
||
* - A @c NULL cell → traversal stops (end-of-body sentinel).
|
||
*
|
||
* Returns @p hash unchanged if @p entry lacks the @c WORD_COMPILED flag or
|
||
* has no valid data-field. Capped at 1024 cells per body as a safety limit.
|
||
*
|
||
* @param vm Active VM instance.
|
||
* @param entry Dictionary entry whose body will be hashed; must have
|
||
* @c WORD_COMPILED set.
|
||
* @param word_index Creation-order ordinal of @p entry (used by debug mode).
|
||
* @param hash Running FNV-1a hash state from the caller.
|
||
* @return Updated hash state after processing the colon body.
|
||
*/
|
||
static uint64_t hash_colon_body(struct VM *vm, DictEntry *entry, uint32_t word_index, uint64_t hash) {
|
||
if (!vm || !entry || !(entry->flags & WORD_COMPILED)) {
|
||
return hash;
|
||
}
|
||
#if !SK_PARITY_DEBUG
|
||
(void)word_index;
|
||
#endif
|
||
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("hash_colon_body:entry");
|
||
sk_parity_debug_check_ptr(vm, "colon_entry", entry, word_index, entry, entry->func, entry);
|
||
#endif
|
||
/* Get body address from data field */
|
||
cell_t *df = vm_dictionary_get_data_field(entry);
|
||
if (!df) {
|
||
return hash;
|
||
}
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "data_field", entry, word_index, entry, entry->func, df);
|
||
#endif
|
||
|
||
vaddr_t body_addr = (vaddr_t)(uint64_t)(*df);
|
||
if (body_addr == 0 || body_addr >= VM_MEMORY_SIZE) {
|
||
return hash;
|
||
}
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "body_addr_ptr", entry, word_index, entry, entry->func,
|
||
(vm->memory && body_addr < VM_MEMORY_SIZE) ? (vm->memory + body_addr) : NULL);
|
||
#endif
|
||
|
||
/* Traverse threaded code until we hit EXIT or end */
|
||
/* Each cell is a DictEntry* (word to call) */
|
||
/* LIT is followed by a literal value */
|
||
|
||
DictEntry *lit_entry = vm_find_word(vm, "LIT", 3);
|
||
DictEntry *exit_entry = vm_find_word(vm, "EXIT", 4);
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "lit_entry", lit_entry, word_index, entry, lit_entry ? lit_entry->func : NULL, lit_entry);
|
||
sk_parity_debug_check_ptr(vm, "exit_entry", exit_entry, word_index, entry, exit_entry ? exit_entry->func : NULL, exit_entry);
|
||
#endif
|
||
|
||
cell_t *ip = (cell_t *)vm_ptr(vm, body_addr);
|
||
if (!ip) {
|
||
return hash;
|
||
}
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "colon_body_ip", entry, word_index, entry, entry->func, ip);
|
||
#endif
|
||
|
||
/* Safety limit on body length */
|
||
size_t max_cells = (VM_MEMORY_SIZE - body_addr) / sizeof(cell_t);
|
||
if (max_cells > 1024) max_cells = 1024;
|
||
|
||
for (size_t i = 0; i < max_cells; i++) {
|
||
DictEntry *w = (DictEntry *)(uintptr_t)(ip[i]);
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "body_xt", w, word_index, entry, w ? w->func : NULL, w);
|
||
#endif
|
||
|
||
if (w == NULL) {
|
||
break; /* End of body */
|
||
}
|
||
|
||
if (w == exit_entry) {
|
||
/* Hash EXIT's word_id and stop */
|
||
hash = fnv1a_64_u32(w->word_id, hash);
|
||
break;
|
||
}
|
||
|
||
/* Hash the called word's word_id */
|
||
hash = fnv1a_64_u32(w->word_id, hash);
|
||
|
||
if (w == lit_entry) {
|
||
/* Next cell is literal value - hash it */
|
||
i++;
|
||
if (i < max_cells) {
|
||
cell_t lit_val = ip[i];
|
||
/* Hash as 64-bit value */
|
||
for (int b = 0; b < 8; b++) {
|
||
hash = fnv1a_64_u8((uint8_t)(lit_val & 0xFF), hash);
|
||
lit_val >>= 8;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return hash;
|
||
}
|
||
|
||
/**
|
||
* @brief Compute the canonical FNV-1a 64-bit hash of the VM dictionary.
|
||
*
|
||
* Implements M7 Rule 3: dictionary traversal is in creation order (oldest
|
||
* word first). Because @c vm->latest is the newest word, the function first
|
||
* builds a reverse array of entries and then hashes them in reverse-array
|
||
* order (oldest → newest).
|
||
*
|
||
* For each entry the following structural fields are folded into the hash:
|
||
* 1. @c flags (1 byte)
|
||
* 2. @c name_len (1 byte)
|
||
* 3. @c name (name_len bytes)
|
||
* 4. @c acl_default (1 byte)
|
||
* 5. @c word_id (4 bytes, little-endian)
|
||
* 6. Colon body (via @c hash_colon_body()) if @c WORD_COMPILED is set
|
||
*
|
||
* Pointer-valued fields (@c func, @c link) are intentionally excluded so
|
||
* the hash is stable across different build addresses and address-space
|
||
* layouts.
|
||
*
|
||
* Traversal is capped at @c MAX_DICT_ENTRIES (2048) entries to guard
|
||
* against corruption.
|
||
*
|
||
* @param vm Active VM whose dictionary will be hashed; returns 0 if @c NULL.
|
||
* @return 64-bit FNV-1a canonical hash of the dictionary structure.
|
||
*/
|
||
uint64_t sk_dict_canonical_hash(struct VM *vm) {
|
||
if (!vm) return 0;
|
||
|
||
/* Build array of entries in reverse order (latest to oldest) */
|
||
DictEntry *entries[MAX_DICT_ENTRIES];
|
||
uint32_t count = 0;
|
||
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("sk_dict_canonical_hash:walk_latest");
|
||
sk_parity_debug_check_ptr(vm, "latest_head", vm->latest, 0, vm->latest, vm->latest ? vm->latest->func : NULL, vm->latest);
|
||
#endif
|
||
for (DictEntry *e = vm->latest; e != NULL; e = e->link) {
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "dict_entry", e, count, e, e->func, e);
|
||
#endif
|
||
if (count < MAX_DICT_ENTRIES) {
|
||
entries[count++] = e;
|
||
}
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "dict_link", e->link, count, e, e->func, e->link);
|
||
#endif
|
||
}
|
||
|
||
/* Hash in creation order (oldest to newest = reverse of array) */
|
||
uint64_t hash = FNV1A_64_OFFSET_BASIS;
|
||
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("sk_dict_canonical_hash:hash_order");
|
||
#endif
|
||
for (int i = (int)count - 1; i >= 0; i--) {
|
||
DictEntry *e = entries[i];
|
||
uint32_t ordinal = (uint32_t)((count - 1) - i);
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_check_ptr(vm, "hash_entry", e, ordinal, e, e ? e->func : NULL, e);
|
||
sk_parity_debug_check_ptr(vm, "hash_xt", e, ordinal, e, e ? e->func : NULL, e ? e->func : NULL);
|
||
#endif
|
||
|
||
/* Hash structural fields only */
|
||
|
||
/* 1. flags (1 byte) */
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("hash_field:flags");
|
||
#endif
|
||
hash = fnv1a_64_u8(e->flags, hash);
|
||
|
||
/* 2. name_len (1 byte) */
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("hash_field:name_len");
|
||
#endif
|
||
hash = fnv1a_64_u8(e->name_len, hash);
|
||
|
||
/* 3. name (name_len bytes) */
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("hash_field:name_bytes");
|
||
#endif
|
||
hash = fnv1a_64((const uint8_t *)e->name, e->name_len, hash);
|
||
|
||
/* 4. acl_default (1 byte) */
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("hash_field:acl");
|
||
#endif
|
||
hash = fnv1a_64_u8(e->acl_default, hash);
|
||
|
||
/* 5. word_id (4 bytes) */
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("hash_field:word_id");
|
||
#endif
|
||
hash = fnv1a_64_u32(e->word_id, hash);
|
||
|
||
/* 6. For colon definitions: hash body as word_id sequence */
|
||
if (e->flags & WORD_COMPILED) {
|
||
hash = hash_colon_body(vm, e, ordinal, hash);
|
||
}
|
||
}
|
||
|
||
return hash;
|
||
}
|
||
|
||
/**
|
||
* @brief Collect M7 parity data from the VM into a @c ParityPacket.
|
||
*
|
||
* Populates @p out with a snapshot of the VM's structural state for offline
|
||
* determinism verification. The packet contains:
|
||
*
|
||
* - M7.1a fields: @c word_count, @c here_offset, @c latest_word_id, and the
|
||
* canonical dictionary hash (@c header_hash64) computed by
|
||
* @c sk_dict_canonical_hash().
|
||
* - M7.1b fields: test result counters (@c tests_total / @c tests_passed /
|
||
* @c tests_failed / @c tests_skipped / @c tests_errors) populated from
|
||
* @c global_test_stats when @c STARFORTH_ENABLE_TESTS is defined.
|
||
*
|
||
* If @p vm is @c NULL, sets @c out->bootstrap_result to
|
||
* @c SK_BOOTSTRAP_INIT_FAIL and returns with all other fields zeroed. If
|
||
* @p out is @c NULL the function is a no-op.
|
||
*
|
||
* @param vm Active VM to collect parity data from; may be @c NULL.
|
||
* @param out Output @c ParityPacket to fill; caller must allocate.
|
||
*/
|
||
void sk_parity_collect(struct VM *vm, ParityPacket *out) {
|
||
if (!out) return;
|
||
#if SK_PARITY_DEBUG
|
||
sk_parity_debug_log_msg("sk_parity_collect:enter");
|
||
#endif
|
||
|
||
/* Clear packet */
|
||
out->word_count = 0;
|
||
out->here_offset = 0;
|
||
out->latest_word_id = 0;
|
||
out->header_hash64 = 0;
|
||
out->tests_total = 0;
|
||
out->tests_passed = 0;
|
||
out->tests_failed = 0;
|
||
out->tests_skipped = 0;
|
||
out->tests_errors = 0;
|
||
out->window_hash64 = 0;
|
||
out->bootstrap_result = SK_BOOTSTRAP_OK;
|
||
|
||
if (!vm) {
|
||
out->bootstrap_result = SK_BOOTSTRAP_INIT_FAIL;
|
||
return;
|
||
}
|
||
|
||
/* M7.1a fields */
|
||
out->word_count = sk_dict_word_count(vm);
|
||
out->here_offset = (uint32_t)vm->here;
|
||
out->latest_word_id = vm->latest ? vm->latest->word_id : 0;
|
||
out->header_hash64 = sk_dict_canonical_hash(vm);
|
||
|
||
/* M7.1b fields - from global test stats (if tests were run) */
|
||
#ifdef STARFORTH_ENABLE_TESTS
|
||
extern TestStats global_test_stats;
|
||
out->tests_total = (uint32_t)global_test_stats.total_tests;
|
||
out->tests_passed = (uint32_t)global_test_stats.total_pass;
|
||
out->tests_failed = (uint32_t)global_test_stats.total_fail;
|
||
out->tests_skipped = (uint32_t)global_test_stats.total_skip;
|
||
out->tests_errors = (uint32_t)global_test_stats.total_error;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief Print a @c uint32_t value as decimal to the kernel console or stdout.
|
||
*
|
||
* Converts @p val to a decimal ASCII string using a local buffer (no libc
|
||
* required) and emits it via @c console_puts() in kernel builds or
|
||
* @c printf() in hosted builds. Used by @c sk_parity_print() to format
|
||
* count and ID fields in parity output lines.
|
||
*
|
||
* @param val 32-bit unsigned value to print.
|
||
*/
|
||
static void print_u32(uint32_t val) {
|
||
char buf[16];
|
||
int i = 15;
|
||
buf[i--] = '\0';
|
||
if (val == 0) {
|
||
buf[i--] = '0';
|
||
} else {
|
||
while (val > 0 && i >= 0) {
|
||
buf[i--] = '0' + (val % 10);
|
||
val /= 10;
|
||
}
|
||
}
|
||
#ifdef __STARKERNEL__
|
||
console_puts(&buf[i + 1]);
|
||
#else
|
||
printf("%s", &buf[i + 1]);
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief Print a @c uint64_t value as a 16-digit hex string with "0x" prefix.
|
||
*
|
||
* Formats @p val as "0xNNNNNNNNNNNNNNNN" using a fixed-size local buffer
|
||
* (no libc required) and emits it via @c console_puts() in kernel builds or
|
||
* @c printf() in hosted builds. Used by @c sk_parity_print() to format hash
|
||
* and address fields in parity output lines.
|
||
*
|
||
* @param val 64-bit value to print as lowercase hex.
|
||
*/
|
||
static void print_hex64(uint64_t val) {
|
||
char buf[19];
|
||
buf[0] = '0';
|
||
buf[1] = 'x';
|
||
for (int i = 17; i >= 2; i--) {
|
||
int d = val & 0xF;
|
||
buf[i] = (d < 10) ? ('0' + d) : ('a' + d - 10);
|
||
val >>= 4;
|
||
}
|
||
buf[18] = '\0';
|
||
#ifdef __STARKERNEL__
|
||
console_puts(buf);
|
||
#else
|
||
printf("%s", buf);
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief Print a null-terminated string to the kernel console or stdout.
|
||
*
|
||
* Emits @p s via @c console_puts() in kernel builds or @c printf() in hosted
|
||
* builds. Serves as a single dispatch point so parity output compiles cleanly
|
||
* in both environments without scattering @c ifdef guards through the
|
||
* formatting code.
|
||
*
|
||
* @param s Null-terminated string to print.
|
||
*/
|
||
static void print_str(const char *s) {
|
||
#ifdef __STARKERNEL__
|
||
console_puts(s);
|
||
#else
|
||
printf("%s", s);
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief Print a newline to the kernel console or stdout.
|
||
*
|
||
* Emits a line terminator via @c console_println("") in kernel builds or
|
||
* @c printf("\n") in hosted builds. Paired with @c print_str() to avoid
|
||
* scattering @c ifdef guards through parity formatting code.
|
||
*/
|
||
static void print_nl(void) {
|
||
#ifdef __STARKERNEL__
|
||
console_println("");
|
||
#else
|
||
printf("\n");
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* @brief Print a @c ParityPacket to the kernel console or stdout.
|
||
*
|
||
* Emits two or three structured lines that can be captured in the QEMU serial
|
||
* log for offline determinism verification:
|
||
*
|
||
* - @c "PARITY:M7.1a word_count=N here=0xH latest_id=N hash=0xH" — always
|
||
* emitted; contains the structural dictionary fingerprint.
|
||
* - @c "PARITY:M7.1b tests=N pass=N fail=N skip=N err=N" — emitted only
|
||
* when @c pkt->tests_total > 0; contains POST result counts.
|
||
* - @c "PARITY:OK" or @c "PARITY:FAIL code=N" — result verdict line.
|
||
*
|
||
* All output goes through @c print_str() / @c print_u32() / @c print_hex64()
|
||
* / @c print_nl() so the function compiles cleanly in both kernel and hosted
|
||
* builds. No-op if @p pkt is @c NULL.
|
||
*
|
||
* @param pkt Pointer to the @c ParityPacket to display; may be @c NULL.
|
||
*/
|
||
void sk_parity_print(const ParityPacket *pkt) {
|
||
if (!pkt) return;
|
||
|
||
/* M7.1a line */
|
||
print_str("PARITY:M7.1a word_count=");
|
||
print_u32(pkt->word_count);
|
||
print_str(" here=");
|
||
print_hex64(pkt->here_offset);
|
||
print_str(" latest_id=");
|
||
print_u32(pkt->latest_word_id);
|
||
print_str(" hash=");
|
||
print_hex64(pkt->header_hash64);
|
||
print_nl();
|
||
|
||
/* M7.1b line (only if tests were run) */
|
||
if (pkt->tests_total > 0) {
|
||
print_str("PARITY:M7.1b tests=");
|
||
print_u32(pkt->tests_total);
|
||
print_str(" pass=");
|
||
print_u32(pkt->tests_passed);
|
||
print_str(" fail=");
|
||
print_u32(pkt->tests_failed);
|
||
print_str(" skip=");
|
||
print_u32(pkt->tests_skipped);
|
||
print_str(" err=");
|
||
print_u32(pkt->tests_errors);
|
||
print_nl();
|
||
}
|
||
|
||
/* Result line */
|
||
if (pkt->bootstrap_result == SK_BOOTSTRAP_OK &&
|
||
pkt->tests_failed == 0 && pkt->tests_errors == 0) {
|
||
print_str("PARITY:OK");
|
||
} else {
|
||
print_str("PARITY:FAIL code=");
|
||
print_u32(pkt->bootstrap_result);
|
||
}
|
||
print_nl();
|
||
}
|