First code for StarForth v4 (JUSTIFICATION.md section 10, step 1): one node of the 32-instruction core as a C99 model, with cell width as a build parameter. - Node: P, A, B, F18 circular stacks (10 and 9 deep, D-2), word-addressed memory (D-1), 5% guard bands on every bounded list. - Instruction word: six 5-bit slots in 32 bits at every cell width. - Executor: all 32 opcodes of DECOMPOSITION.md 1.3. Cell arithmetic wraps explicitly; no signed overflow or implementation-defined shift. - Heat: per-opcode and per-call-target counters and the anti-clock, driven by instruction retirement (1.4, D-6 interim). - Slot packer and runner for tests, and a reference unsigned multiply in plain C99 with no 128-bit type. Tests run at 32- and 64-bit cells, and under ASan and UBSan. They cover every opcode and execute the first section 4 definitions (NIP SWAP OR NEGATE ROT 0< 0= 2DUP - U<) against the C operation each stands for. UM* as written in section 4 is exact only while u1 <= 2^(n-2). Two known failing cases are pinned in test_foundation.c until it is rewritten. DECOMPOSITION.md: record D-9, the instruction word is 32 bits at every cell width (ruled 2026-10-02). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
4.5 KiB
C
93 lines
4.5 KiB
C
/* guard.h -- 5% safety boundaries at each end of every bounded list.
|
|
*
|
|
* The convention used throughout this project is that a bounded list carries a
|
|
* safety boundary at each end rather than trusting its own bounds. The kernel
|
|
* arena is the reference instance (kernel/src/vm/arena.c):
|
|
*
|
|
* #define SK_VM_GUARD_SIZE PMM_PAGE_SIZE
|
|
* #define SK_VM_GUARD_PATTERN_HEAD 0x5ac0ffeed0c0ffeeULL
|
|
* #define SK_VM_GUARD_PATTERN_TAIL 0x0bad0badf00df00dULL
|
|
*
|
|
* with a guard_fill / guard_check pair, and total size = payload + 2 * guard.
|
|
* Two properties of that instance are kept here deliberately:
|
|
*
|
|
* - HEAD and TAIL carry *different* patterns, so a check that fails can say
|
|
* which end was overrun. A single pattern tells you that something ran
|
|
* off the end and nothing about where.
|
|
* - the boundary is placed immediately adjacent to the payload, because a
|
|
* boundary separated from what it protects catches nothing.
|
|
*
|
|
* This header supplies the same facility at a size that scales with the list
|
|
* (5% at each end) rather than a fixed page, and reduces it to two helpers so
|
|
* that no list in v4 has to hand-roll the arithmetic.
|
|
*
|
|
* WHY THIS IS LOAD-BEARING HERE, NOT DECORATIVE. AddressSanitizer already
|
|
* catches a genuine out-of-struct access, so a guard band cannot be justified
|
|
* as "catching overflow" -- ASan does that. What ASan does *not* catch is an
|
|
* index that is wrong but still lands inside the same object, which is exactly
|
|
* the failure mode of a circular buffer driven by unsigned index arithmetic:
|
|
*
|
|
* head = (head + V4_DATA_RING - 1u) % V4_DATA_RING; correct
|
|
* head = (head + V4_DATA_RING - 1u) & (V4_DATA_RING - 1u); correct today
|
|
*
|
|
* The second line is identical today *only because 8 is a power of two*. It
|
|
* is the sort of "simplification" that gets made, it is right for the current
|
|
* depths, and it silently breaks the first time a ring size stops being a power
|
|
* of two. Both forms stay in range, so ASan stays quiet. A guard band with a
|
|
* 5% margin either side is what turns "the tests happened to cover it" into a
|
|
* check that fires. The tests in tests/test_stack.c exercise that directly:
|
|
* they index past each end on purpose and require the check to catch it.
|
|
*
|
|
* The boundary is sized with ceiling division and a floor of one element, so a
|
|
* short list still gets a real boundary. A 5% margin on a 3-element list
|
|
* rounds to 0, and a zero-width guard is not a guard -- it is a comment.
|
|
*/
|
|
#ifndef V4_GUARD_H
|
|
#define V4_GUARD_H
|
|
|
|
#include "v4/cell.h"
|
|
|
|
/* Fraction of the payload reserved as a boundary at each end. */
|
|
#define V4_GUARD_PCT 5u
|
|
|
|
/* Boundary width, in elements, for a payload of n elements: 5% rounded up,
|
|
* never less than one. Written as a macro so that a struct can size its own
|
|
* inline boundary at compile time -- the boundary has to be storage the object
|
|
* owns, not something a helper allocates later, because a mesh node has no
|
|
* heap to allocate it from.
|
|
*
|
|
* n = 8 -> 1 (5% = 0.4, rounds up)
|
|
* n = 32 -> 2 (5% = 1.6, rounds up)
|
|
* n = 100 -> 5
|
|
* n = 1000 -> 50
|
|
* n = 3 -> 1 (5% = 0.15, rounds up to 1 -- not 0)
|
|
*/
|
|
#define V4_GUARD_ELEMS(n) \
|
|
(((n) * V4_GUARD_PCT + 99u) / 100u)
|
|
|
|
/* The kernel arena's patterns, carried over so that a hex dump from a mesh
|
|
* node and one from the kernel can be read with the same key. Casting to
|
|
* v4_ucell truncates cleanly at 32-bit cells: the arena's own constants are
|
|
* 64-bit, and their low halves are as distinct as the full words --
|
|
* 0xd0c0ffee against 0xf00df00d -- so "which end" stays answerable at both
|
|
* widths. */
|
|
#define V4_GUARD_PATTERN_HEAD ((v4_ucell)0x5ac0ffeed0c0ffeeULL)
|
|
#define V4_GUARD_PATTERN_TAIL ((v4_ucell)0x0bad0badf00df00dULL)
|
|
|
|
/* If these ever compare equal the boundary has stopped saying which end was
|
|
* overrun, which is half its purpose. C99 has no _Static_assert. */
|
|
typedef char v4_guard_patterns_are_distinct
|
|
[(V4_GUARD_PATTERN_HEAD != V4_GUARD_PATTERN_TAIL) ? 1 : -1];
|
|
|
|
/* Write one boundary band. `band` is the start of the band, `n` its width in
|
|
* elements. */
|
|
void v4_guard_fill(v4_cell *band, unsigned n, v4_ucell pattern);
|
|
|
|
/* 1 if the whole band still holds `pattern`, else 0. Checks every element, not
|
|
* just the ends: the band is 5% wide precisely so there is something to check
|
|
* in it, and a single word is what the kernel arena can afford at page
|
|
* granularity. */
|
|
int v4_guard_intact(const v4_cell *band, unsigned n, v4_ucell pattern);
|
|
|
|
#endif /* V4_GUARD_H */
|