Fix M*: use __int128 for a genuine 128-bit double-cell product (FABRIC-3.md §XII.4)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

mixed_math_word_m_star() confused "double" (two full cell_t-width cells,
128 bits total on this 64-bit build -- what D+/D-/D./etc. all actually
expect) with "the low/high 32-bit halves of a single 64-bit product" --
code clearly written assuming cell_t is 32-bit. It computed an ordinary
64-bit `long long` product (already wrong for any true product exceeding
64 bits, since long long is the same width as one cell here) and split
that into 32-bit halves via `result & 0xFFFFFFFF` / `result >> 32`. For a
small negative product like -56088, this produced a positive, zero-
extended low cell paired with a correctly-looking dhigh=-1 -- D.'s
overflow check (correctly) rejected the resulting malformed double, on
every architecture, every time (this bug was never architecture-specific,
unlike the D+/D-/DNEGATE/d_compare family already fixed in bea8d74/
1a716c8).

Fixed via __int128 for a genuine 64x64->128-bit multiply, no truncation --
an already-established safe pattern in this codebase for exactly this
operation (src/starkernel/crypto/fe25519.c/scalar25519.c already use it;
src/starkernel/arch/amd64/timer.c's own doc comment confirms __int128
multiply/shift-by-constant compile cleanly with zero undefined symbols on
all three target toolchains -- only division needs unavailable libgcc
support, not used here).

Verified: rebuilt and booted all three architectures clean. T13/T14 both
correct everywhere (56088/-56088). Manual cases confirm genuine 128-bit
precision: -1 -1 M* -> 1; 1000000000000 1000000000000 M* -> dhigh=54210
dlow=2003764205206896640 (10^12 x 10^12 = 10^24, correctly exceeding 64
bits). Full 24-case exerciser reran clean on all three, no regressions.

Found while verifying, NOT fixed (report only, out of scope of this
request): mixed_math_word_m_slash_mod() (M/MOD, the very next function in
the same file) has the identical bug class -- confirmed genuinely broken
for a true wide double (feeding this fix's own correct large-magnitude M*
output into M/MOD produces a result wrong by many orders of magnitude and
the wrong sign). Flagged in FABRIC-3.md for a future fix request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
Robert Allan James
2026-09-11 08:23:51 -04:00
co-authored by Claude Sonnet 5
parent 1a716c8048
commit 9a09949c69
10 changed files with 27136 additions and 13 deletions
+46 -6
View File
@@ -2142,8 +2142,9 @@ against the other two architectures once the campaign completes; not yet root-ca
as a bug on its own.
### XII.4 — Campaign completed: full 27-leg run (9 identities × 3 architectures), two real
FORTH-79 engine bugs found — bug 2 (`D+`/`D-`/`DNEGATE`/`d_compare`) root-caused and FIXED
2026-09-11; bug 1 (`M*`) still open, report only (2026-09-10, after §XIII's WIREBIND fix)
FORTH-79 engine bugs found — BOTH root-caused and FIXED 2026-09-11 (bug 1 `M*`; bug 2 `D+`/`D-`/
`DNEGATE`/`d_compare`); a third, `M/MOD`, found while fixing bug 1, reported not fixed
(2026-09-10 campaign, after §XIII's WIREBIND fix)
All 27 legs run: `zuse` (auto-attached, exercised directly on Hera's own console) plus `rajames`
(the `bob` thumbdrive's actual registered identity -- see the naming-mismatch note below) and
@@ -2169,10 +2170,49 @@ which identity is running them -- expected, and a useful negative result on its
project's standing rule (report, don't fix without being asked):**
1. **`M*` on a negative operand → `D.` reports `DOUBLE-OVERFLOW`, on all three architectures
identically.** `T14: -123 456 M* SWAP D. CR` should print `-56088`; it prints `DOUBLE-OVERFLOW`
on amd64, aarch64, *and* riscv64 -- an engine-level bug (in `M*`'s double-cell result, or in
`D.`'s own overflow check, or both), not architecture-specific. Universal, 100% reproducible
across all 9 identities × 3 architectures.
identically — root-caused and FIXED 2026-09-11.** `T14: -123 456 M* SWAP D. CR` should print
`-56088`; it printed `DOUBLE-OVERFLOW` on amd64, aarch64, *and* riscv64 -- an engine-level bug,
universal, 100% reproducible across all 9 identities × 3 architectures (unlike bug 2 below,
this one really is architecture-independent, since it doesn't involve `long`'s width at all).
**Root cause:** `mixed_math_word_m_star()` (`mixed_arithmetic_words.c`) confused "double" (two
full `cell_t`-width cells, 128 bits total on this 64-bit build -- what `D+`/`D-`/`D.`/etc. all
actually expect) with "the low/high 32-bit halves of a single 64-bit product" -- code clearly
written assuming `cell_t` is 32-bit. It computed an ordinary 64-bit `long long` product
(`(long long) n1 * (long long) n2`, already wrong/truncated for any true product not fitting
in 64 bits, since `long long` is only 64-bit here, the same width as one cell already) and
then split *that* into 32-bit low/high halves via `result & 0xFFFFFFFF` / `result >> 32`. For
a small negative product like `-56088` (fits entirely in one 64-bit cell with sign extension),
this produced a *positive*, zero-extended low cell paired with a correctly-looking `dhigh=-1`
-- the exact same "malformed double, `D.` correctly rejects it" shape as bug 2's `D+` defect,
just from an entirely different, unrelated piece of code.
**Fix:** switched to `__int128` for a genuine 64×64→128-bit multiply (no truncation), an
already-established safe pattern in this codebase for exactly this operation --
`src/starkernel/crypto/fe25519.c`/`scalar25519.c` already use it, and
`src/starkernel/arch/amd64/timer.c`'s own doc comment confirms `__int128`
multiply/shift-by-constant compile cleanly with zero undefined symbols on all three target
toolchains (only *division* needs libgcc support unavailable in this freestanding build --
not needed for `M*`). `dlow` = low 64 bits of the 128-bit product, `dhigh` = high 64 bits
(arithmetic shift, sign-preserving).
**Verified:** rebuilt and booted all three architectures clean. `T13`/`T14` both correct
everywhere now (`56088`/`-56088`). Additional manual cases run live on all three, identical
results: `-1 -1 M*``1` (small negative×negative); `1000000000000 1000000000000 M*`
`dhigh=54210 dlow=2003764205206896640` (10^12 × 10^12 = 10^24, correctly exceeding 64 bits --
proof the multiply itself is now genuinely 128-bit, not just sign-extension-correct for small
values). Full 24-case exerciser reran clean on all three, no regressions.
**Found while verifying, NOT fixed (report only, out of scope of what was requested):**
`mixed_math_word_m_slash_mod()` (`M/MOD`, the very next function in the same file) has the
*identical* bug class -- it reconstructs the dividend from `(dhigh << 32) | (dlow &
0xFFFFFFFF)`, the same wrong "32-bit halves of a 64-bit value" assumption `M*` had. Latent for
small inputs (`T15: 100000 S>D SWAP 7 M/MOD . .` -> `14285 5`, correct, because `100000` fits
entirely within 32 bits so the reconstruction coincidentally works), but confirmed genuinely
broken for a true wide double: feeding the now-fixed `M*`'s own correct large-magnitude output
into `M/MOD` (`1000000000000 1000000000000 M* 1000000 M/MOD . .`, expected quotient
`10^18`) produces `-6845471433603` remainder, `-99710` quotient -- wrong by many orders of
magnitude and the wrong sign. Not touched here; flag for a future fix request.
2. **`D+` on two negative doubles → `D.` reports `DOUBLE-OVERFLOW`, aarch64 only — root-caused
2026-09-11, FIXED 2026-09-11 (`D+`/`D-`/`DNEGATE`).**