keccak: cover runtime fallback paths - #29
Conversation
| func forceRuntimeFallback(t *testing.T) { | ||
| t.Helper() | ||
| old := useASM | ||
| useASM = false |
There was a problem hiding this comment.
The tests never observe the fallback, so they cannot fail when it breaks.
With useASM = false the fallback delegates to x/crypto/sha3, and the reference the tests compare against is also x/crypto/sha3. Every assertion here is "Keccak-256 is correct", which the native sponge path satisfies too. Nothing asserts that the !useASM branch actually ran.
Verified by mutation on this branch (amd64 + BMI2 machine):
- Delete the guard in
Sum256:
func Sum256(data []byte) [32]byte {
return sum256Sponge(data) // fallback branch removed
}→ all three new tests still pass.
- Flip every
if !useASMinHasher.Write/Sum256/Sum/Readtoif falseandHasher.Reset'sif useASMtoif true(i.e. delete the whole runtime fallback)
→ all three new tests still pass.
Second scenario, same root cause: on the hardware this PR targets (AMD64 without BMI1/BMI2, ARM64 without SHA3) useASM is already false, so forceRuntimeFallback changes nothing and the three tests become duplicates of the existing keccak_test.go cases — silently, with no signal.
To make the coverage real, the test has to pin something only the fallback produces. Two options that work together:
- assert the fallback state is materialized, e.g.
h.xc != nilafter aWritewithuseASM=false(andh.xc == nilafter the sameWritewith the platform value); - run the whole scenario body twice — once with the platform
useASM, once forced tofalse— and require the two result sets to be byte-identical. That compares the two implementations against each other instead of comparingx/cryptoto itself.
| func TestRuntimeFallbackWriteAfterReadPanics(t *testing.T) { | ||
| forceRuntimeFallback(t) | ||
|
|
||
| defer func() { | ||
| if recover() == nil { | ||
| t.Fatal("expected panic on Write after Read") | ||
| } | ||
| }() | ||
|
|
||
| var h Hasher | ||
| h.Read(make([]byte, 1)) | ||
| h.Write([]byte("data")) | ||
| } |
There was a problem hiding this comment.
Any panic satisfies this test, including one that means the guard is gone.
recover() == nil is the only check, and the hasher here is a zero value: h.Read must first lazily build h.xc. If that lazy init is lost, h.Read panics on the nil KeccakState and this test still reports success — it would claim the Write-after-Read guard works while Write is never even reached.
Verified by mutation on this branch:
func (h *Hasher) Read(out []byte) (int, error) {
if !useASM {
return h.xc.Read(out) // lazy init dropped
}
return h.sponge.Read(out)
}→ TestRuntimeFallbackWriteAfterReadPanics still PASSes (the recovered value is now a nil-pointer dereference from line 103, not "sha3: Write after Read").
Assert the recovered value, and write before reading so h.xc is established by a real absorb:
| func TestRuntimeFallbackWriteAfterReadPanics(t *testing.T) { | |
| forceRuntimeFallback(t) | |
| defer func() { | |
| if recover() == nil { | |
| t.Fatal("expected panic on Write after Read") | |
| } | |
| }() | |
| var h Hasher | |
| h.Read(make([]byte, 1)) | |
| h.Write([]byte("data")) | |
| } | |
| func TestRuntimeFallbackWriteAfterReadPanics(t *testing.T) { | |
| forceRuntimeFallback(t) | |
| defer func() { | |
| r := recover() | |
| if r == nil { | |
| t.Fatal("expected panic on Write after Read") | |
| } | |
| if s, ok := r.(string); !ok || !strings.Contains(s, "Write after Read") { | |
| t.Fatalf("unexpected panic value %v, want a Write-after-Read panic", r) | |
| } | |
| }() | |
| var h Hasher | |
| h.Write([]byte("seed")) | |
| h.Read(make([]byte, 1)) | |
| h.Write([]byte("data")) | |
| } |
(needs strings in the import block)
| t.Fatal("Hasher.Read differs from x/crypto") | ||
| } | ||
|
|
||
| h.Reset() |
There was a problem hiding this comment.
The one fallback branch left uncovered is Reset's nil-xc path.
Reset is always reached here with h.xc already non-nil (the preceding h.Read allocated it), and the same holds in the other two tests. So this branch never runs:
func (h *Hasher) Reset() {
...
if h.xc == nil {
h.xc = sha3.NewLegacyKeccak256().(KeccakState) // never executed
} else {
h.xc.Reset()
}
}go test -run RuntimeFallback -coverprofile on this branch confirms it: keccak_asm.go:164.18,166.4 has 0 hits, and Hasher.Reset sits at 60%.
Concrete miss: if that branch were collapsed to an unconditional h.xc.Reset(), the documented NewFastKeccak() / zero-value flow — h := keccak.NewFastKeccak(); h.Reset(); h.Write(p); h.Sum256() — would nil-panic on every non-BMI2 AMD64 and non-SHA3 ARM64 box, and all three tests in this file would stay green.
One extra case closes it:
var fresh Hasher
fresh.Reset()
fresh.Write(data)
if got := fresh.Sum256(); got != want {
t.Fatalf("Reset on zero value = %x, want %x", got, want)
}
Summary
x/crypto/sha3Why
go test -tags puregocompileskeccak_default.go. It does not execute the fallback branches insidekeccak_asm.go, which are used at runtime on AMD64 CPUs without BMI1/BMI2 and ARM64 CPUs without SHA3 support.This is an independent, test-only split of the fallback coverage in #13. It does not add cloning, marshaling, or production changes.
The change adds coverage for existing behavior, so TDD is not applicable. No tests are skipped when native assembly is unavailable.
Verification
go test ./...go test -race ./...go test -tags purego ./...go vet ./...go mod tidy -diffgolangci-lint run(twice)