Skip to content

keccak: cover runtime fallback paths - #29

Draft
yperbasis wants to merge 1 commit into
masterfrom
review/runtime-fallback-tests
Draft

keccak: cover runtime fallback paths#29
yperbasis wants to merge 1 commit into
masterfrom
review/runtime-fallback-tests

Conversation

@yperbasis

Copy link
Copy Markdown
Member

Summary

  • force the compiled runtime fallback deterministically in tests
  • compare one-shot, streaming, reset, append, and extended-read behavior with x/crypto/sha3
  • cover zero-value handling and the write-after-read panic
  • exercise lengths around the 136-byte rate boundary

Why

go test -tags purego compiles keccak_default.go. It does not execute the fallback branches inside keccak_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 -diff
  • golangci-lint run (twice)

func forceRuntimeFallback(t *testing.T) {
t.Helper()
old := useASM
useASM = false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  1. Delete the guard in Sum256:
func Sum256(data []byte) [32]byte {
	return sum256Sponge(data)  // fallback branch removed
}

→ all three new tests still pass.

  1. Flip every if !useASM in Hasher.Write/Sum256/Sum/Read to if false and Hasher.Reset's if useASM to if 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 != nil after a Write with useASM=false (and h.xc == nil after the same Write with the platform value);
  • run the whole scenario body twice — once with the platform useASM, once forced to false — and require the two result sets to be byte-identical. That compares the two implementations against each other instead of comparing x/crypto to itself.

Comment on lines +93 to +105
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"))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants