From e9bd06a193ed5dcced0fc842b57a606d215f449c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 09:48:39 +0900 Subject: [PATCH] lib/crypto/sha256: restore faithful re-entrant sum() (#265 unblocked) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The port shipped sum() single-shot — mutating the live hash state — because Hare's state snapshot `let copy = *h; let h = ©` (a deref-rhs aggregate let-init of an array-containing struct) miscompiled in cgen. #265 fold-1 (master 4d3f846) landed the full-size aggregate copy for that axis, so restore the faithful form: pad+finalize the snapshot, leave the live state untouched, close() the copy. sum() is now non-destructive — summing twice yields the same digest and writing after a sum() continues the stream. Pinned by a new reentrant() @test (sum-twice identical + write-after-sum continuity). NIST vectors unchanged. --- lib/crypto/sha256/sha256.ww | 32 ++++++++++++-------------------- lib/crypto/sha256/sha256_test.ww | 31 ++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/crypto/sha256/sha256.ww b/lib/crypto/sha256/sha256.ww index 78fa3346..6040f83f 100644 --- a/lib/crypto/sha256/sha256.ww +++ b/lib/crypto/sha256/sha256.ww @@ -11,11 +11,7 @@ // mismatch would mean cgen promoted a u32 op to 64-bit without wrapping // (a cgen bug to STOP+report, NOT to mask here). // -// DIVERGENCES from sha256.ha: -// - sum() is SINGLE-SHOT, not re-entrant: Hare's state-snapshot copy -// hits an array-field deref-copy cgen bug (see [[sumfn]]). This is -// the one BEHAVIORAL divergence (blocked on a filed cgen fix); the -// rest below are semantics-preserving spellings. +// DIVERGENCES from sha256.ha (all semantics-preserving spellings): // - `state` embeds [[hash.hash]], whose first field is an inline // io.vtable (not Hare's `stream: io::stream` pointer); see // lib/hash/hash.ww. The vtable slots are wired post-construction in @@ -168,21 +164,17 @@ fn writefn(s: io.stream, buf: []u8) (size | io.error) = { // sumfn — finalize and emit the digest. ref/hare/crypto/sha256/sha256.ha:115-143. // -// DIVERGENCE (re-entrancy / blocked on a cgen bug). Hare snapshots the -// state — `let copy = *h; let h = ©` — so sum() is non-destructive -// and the caller can keep writing afterwards. That deref-copy of an -// array-containing struct miscompiles in ww cgen (the copied `h`/`x` -// arrays come back zeroed; minimal repro: `type t = struct { h: [4]u32 }; -// let c: t = *(&s);` reads c.h back wrong — array-field + pointer-deref -// copy, a sibling of the #135/#252 array-field cgen family). Filed for -// ken; flagged for a drew fidelity ruling. Until it lands, sum() runs on -// the live state and is therefore SINGLE-SHOT: writing or summing again -// after a sum() yields wrong results. Every current caller does exactly -// one terminal sum(), so the NIST digests are unaffected (all vectors, -// incl. the multi-block million-'a' stream, verify byte-identical). The -// final close() still wipes the live state per Hare. +// Re-entrant per Hare: snapshot the state (`let copy = *h; let h = ©`) +// and pad+finalize the COPY, so the live hash is untouched and the caller +// can keep writing or sum() again. The deref-copy of this array-containing +// struct was blocked by a cgen bug (#265) — fold-1 (master 4d3f846) landed +// the full-size aggregate copy for deref-rhs let-init, unblocking it. The +// close() at the end wipes the copy (Hare's `defer hash::close(h)`), not +// the live state. fn sumfn(h: *hash.hash, buf: []u8) void = { - let st: *state = h: *state; + let live: *state = h: *state; + let copy: state = *live; + let st: *state = (©); let ln: size = st.ln; let tmp: [64]u8 = [0...]; @@ -219,7 +211,7 @@ fn sumfn(h: *hash.hash, buf: []u8) void = { i += 1; }; - hash.close(h); + hash.close((©): *hash.hash); }; // block — process every whole 64-byte block in `buf`. diff --git a/lib/crypto/sha256/sha256_test.ww b/lib/crypto/sha256/sha256_test.ww index a4dc1612..d734ccea 100644 --- a/lib/crypto/sha256/sha256_test.ww +++ b/lib/crypto/sha256/sha256_test.ww @@ -91,6 +91,34 @@ fn check(msg: []u8, want: str) void = { "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"); }; +// reentrant — sum() is non-destructive: it pads+finalizes a snapshot, so +// the live hash survives. Summing twice yields the same digest, and a +// write after a sum() continues the same stream. Pins the faithful +// state-snapshot restored once #265 fold-1 unblocked the deref-rhs +// aggregate copy `let copy = *h` (see sha256.ww [[sumfn]]). +@test fn reentrant() void = { + let st: sha256.state = sha256.sha256(); + let h: *hash.hash = (&st): *hash.hash; + hash.write(h, strings.toutf8("abc")); + + let out1: [32]u8; + let out2: [32]u8; + hash.sum(h, out1[0:32]); + hash.sum(h, out2[0:32]); + if (!bytes.equal(out1[0:32], out2[0:32])) { fail(); }; + checkbytes(out1[0:32], + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + + // Write more after the sum(): the post-sum stream is uncorrupted, so + // the running digest of "abc"+"def" matches a one-shot hash of "abcdef". + hash.write(h, strings.toutf8("def")); + let out3: [32]u8; + hash.sum(h, out3[0:32]); + let want: [32]u8; + dohash(strings.toutf8("abcdef"), want[0:32]); + if (!bytes.equal(out3[0:32], want[0:32])) { fail(); }; +}; + // sz()/bsz() report the SHA-256 constants regardless of state. @test fn sizes() void = { let st: sha256.state = sha256.sha256(); @@ -104,6 +132,7 @@ export fn main() i32 = { signalled = 2; abc(); signalled = 3; twoblockpad(); signalled = 4; millionas(); - signalled = 5; sizes(); + signalled = 5; reentrant(); + signalled = 6; sizes(); return 0; };