Root-cause the aarch64-only D+ DOUBLE-OVERFLOW bug (FABRIC-3.md §XII.4)
Confirmed the exact mechanism behind the aarch64-specific D+/D. divergence
found in the std79 exerciser campaign (commit cc81edf). The earlier
writeup's cell_t-width check was real but answered the wrong question --
cell_t is 64-bit everywhere, but double_word_d_plus()'s carry-detection
casts through plain `unsigned long`, and sizeof(unsigned long) is 4 (32-bit)
on this aarch64 bare-metal cross-compile target specifically (amd64 and
riscv64 both happen to have a 64-bit long). The low-cell addition silently
truncates to 32 bits, then widens back to cell_t via ordinary (non-sign-
extending) conversion, producing a wrong positive result_low whenever the
true 64-bit sum is negative -- confirmed live by splitting result_low into
hi/lo 32-bit halves: the real pushed value for `-5 S>D 3 S>D D+` is
+4294967294, not -2. D.'s overflow check is correct and is faithfully
reporting a genuinely malformed double; D+ is the actual defect.
vm.h already defines ucell_t for exactly this class of problem (same
conditional as cell_t, guaranteed width-matched on every target) --
print_number_formatted() (format_words.c) already uses it correctly, D+
doesn't. Also flagged (not exercised by the campaign, same bare-`unsigned
long` pattern, same latent risk): D-, DNEGATE, and the d_compare() helper
behind DMAX/DMIN/D</D=.
Report only, per this project's standing rule (report bugs, don't fix
without being asked) -- no source change in this commit, debug probes
added during the investigation were reverted after use.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cc81edf00a
commit
91f7b39d3c
+49
-18
@@ -2179,25 +2179,56 @@ project's standing rule (report, don't fix without being asked):**
|
||||
9 identities, 100% consistently. A genuine cross-ISA divergence, not a flaky/intermittent
|
||||
result.
|
||||
|
||||
Investigated (not fixed): read `double_word_d_plus()` (`double_words.c`) and
|
||||
`format_word_d_dot()` (`format_words.c`). `D.`'s check is `(dhigh == 0) || (dhigh == -1 &&
|
||||
dlow < 0)` -- correct FORTH-79 double-overflow semantics for a properly sign-extended result.
|
||||
`D+`'s carry-detection casts through `unsigned long` for the low-cell addition, which would be
|
||||
a real per-architecture bug *if* `unsigned long`'s width differed from `cell_t`'s on any of
|
||||
these targets -- checked and ruled out: `cell_t` is `int64_t` on amd64 and aarch64 explicitly
|
||||
(`vm.h`'s `#if defined(__amd64__) || ... || defined(__aarch64__) || ...`), and `signed
|
||||
long`/`unsigned long` (the `#else` branch, since `__riscv` isn't in that `#if`) on riscv64 --
|
||||
confirmed 64-bit on all three empirically via `T08: -1 1 RSHIFT` printing the identical
|
||||
19-digit `9223372036854775807` on every architecture, which only a genuine 64-bit arithmetic
|
||||
shift can produce. So this is not a cell-width mismatch; the actual mechanism producing a wrong
|
||||
`dhigh` specifically on aarch64, for this specific input, is still open.
|
||||
**Root-caused 2026-09-11 (still not fixed -- report only, per this project's standing
|
||||
rule).** First pass (2026-09-10) checked `cell_t`'s width and found it 64-bit on all three
|
||||
architectures (`T08: -1 1 RSHIFT` prints the identical 19-digit `9223372036854775807`
|
||||
everywhere), and wrongly concluded from that alone that `D+`'s `unsigned long` carry-detection
|
||||
cast couldn't be a width-mismatch bug -- **that conclusion was wrong**: `cell_t`'s width and
|
||||
plain C `long`'s width are two different things, and only the first was actually checked.
|
||||
Confirmed directly via three targeted probes (`double_word_s_to_d()`, `double_word_d_plus()`,
|
||||
`format_word_d_dot()` -- added, used, reverted): `sizeof(unsigned long)` prints `4` on this
|
||||
aarch64 build. `long`/`unsigned long` is 32-bit on this platform's aarch64 cross-compile
|
||||
target (bare-metal freestanding, not a hosted Linux/LP64 environment where 64-bit `long`
|
||||
would be guaranteed) -- amd64 and riscv64 both happen to use a 64-bit `long`, so the same code
|
||||
only breaks here.
|
||||
|
||||
**Next step, if picked up:** trace `D+`'s carry computation with a targeted probe on aarch64
|
||||
specifically for the `-5 S>D 3 S>D D+` sequence, comparing the pushed `result_high` against the
|
||||
expected `-1` right before `SWAP D.` runs -- narrows whether the defect is in `D+` itself, in
|
||||
`S>D`'s sign-extension, or somewhere in the stack-manipulation path between them. `M*`'s
|
||||
universal failure (bug 1) is a separate, likely unrelated defect and would need its own trace
|
||||
starting from `M*`'s own double-cell multiply, not `D+`'s.
|
||||
Traced the actual data live for `-5 S>D 3 S>D D+`: `d1=(low=-5,high=-1)`, `d2=(low=3,high=0)`
|
||||
-- correct inputs, `S>D`'s sign-extension is fine. Inside `D+`,
|
||||
`unsigned long ud1low = (unsigned long) d1low;` truncates `d1low` (a 64-bit `cell_t`,
|
||||
`0xFFFFFFFFFFFFFFFB`) down to 32 bits (`0xFFFFFFFB`) before the addition. The addition
|
||||
(`0xFFFFFFFB + 3 = 0xFFFFFFFE`) happens entirely within that 32-bit width, and
|
||||
`cell_t result_low = (cell_t) uresult_low;` then *widens* the 32-bit unsigned value back to a
|
||||
64-bit signed `cell_t` via ordinary value-preserving conversion -- not the sign-extension the
|
||||
code actually needs. Confirmed by splitting `result_low`'s real 64-bit value into hi/lo 32-bit
|
||||
halves in the probe: `low_hi32=0 low_lo32=4294967294`, i.e. the true pushed value is
|
||||
`+4294967294` (a large **positive** number), not `-2`. `carry` stays `0` (no overflow at the
|
||||
truncated 32-bit width), so `result_high = d1high + d2high + carry = -1` -- correct on its
|
||||
own, but paired with a `result_low` that is silently wrong. `D.`'s check
|
||||
(`dhigh == -1 && dlow < 0`) then correctly, faithfully reports `DOUBLE-OVERFLOW` for this
|
||||
malformed double -- `D.` is not the bug, `D+` is.
|
||||
|
||||
`vm.h` already defines `ucell_t` (`uint64_t` on amd64/aarch64, `unsigned long` on riscv64 --
|
||||
the same conditional as `cell_t`, guaranteed to match its width on every target) for exactly
|
||||
this class of problem; `print_number_formatted()` (`format_words.c`) already uses it
|
||||
correctly. `D+`'s carry-detection using bare `unsigned long` instead is the actual defect --
|
||||
a one-word class of bug (`unsigned long` should be `ucell_t`), not a logic error.
|
||||
|
||||
**Wider scope, not yet empirically exercised:** `double_word_d_minus()` (`D-`),
|
||||
`double_word_dnegate()` (`DNEGATE`), and the `d_compare()` helper (used by `DMAX`/`DMIN`/
|
||||
`D<`/`D=`) all use the identical bare-`unsigned long` pattern (`double_words.c` lines
|
||||
~111-113, ~136, ~170-171) and are equally vulnerable in principle on any platform where
|
||||
`long` is narrower than `cell_t`. `DNEGATE` was exercised by the campaign (`T20: -5 S>D
|
||||
DNEGATE SWAP D. CR`) and happened to print the correct `5` on all three architectures --
|
||||
traced why: for this specific small input, the 32-bit-truncated magnitude computation still
|
||||
produces the mathematically correct positive result even without proper 64-bit sign
|
||||
extension, so the bug is latent, not absent, for `DNEGATE` here. `D-`/`DMAX`/`DMIN`/`D</D=`
|
||||
were never exercised by `std79-exerciser.fth` at all. `double_word_d_two_star()`/
|
||||
`double_word_d_two_slash()` (`D2*`/`D2/`) use `unsigned long long` instead, which the C
|
||||
standard guarantees is at least 64-bit everywhere -- not part of this bug class.
|
||||
|
||||
`M*`'s universal failure (bug 1) is a separate, unrelated defect (present on every architecture,
|
||||
so not a `long`-width issue) and would need its own trace starting from `M*`'s own double-cell
|
||||
multiply, not `D+`'s.
|
||||
|
||||
## XIII. Heap corruption under repeated WIREBIND attach/detach cycling — root-caused and CLOSED
|
||||
2026-09-10
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-11T03:08:21Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-11T10:40:38Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user