Repository navigation
keccakf_amd64: drop NOSPLIT so the runtime can preemp assembly call #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7ebf06f
8fce91b
e95505c
dcb6bbf
507d551
5f0b3f8
2eb827b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,14 @@ package keccak | |
|
|
||
| import ( | ||
| "math/rand" | ||
| "os" | ||
| "path/filepath" | ||
| "regexp" | ||
| "runtime" | ||
| "strconv" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
||
| // TestXorAndPermute verifies the fused XOR+permute assembly entry point | ||
|
|
@@ -34,3 +41,111 @@ func TestXorAndPermute(t *testing.T) { | |
| } | ||
| } | ||
| } | ||
|
|
||
| // The permutation kernel's stack-growth check is the only preemption point in | ||
| // the absorb and squeeze loops: those loops call nothing else, and assembly is | ||
| // never async-preemptible (runtime/preempt.go, isAsyncSafePoint rejects any | ||
| // FuncFlagAsm frame). Two ways to lose it: | ||
| // | ||
| // NOSPLIT drops the check whatever the frame size | ||
| // a frame under 128 bytes makes the assembler mark a leaf NOSPLIT itself | ||
| // (abi.StackSmall, obj6.go/obj7.go) | ||
| // | ||
| // Either one lets a single large Sum256 or Write hold every P in | ||
| // stop-the-world for the length of the hash. | ||
| var kernelTEXT = regexp.MustCompile(`^TEXT\s+·(\w+)\(SB\),\s*(?:([A-Z0-9|+]+),\s*)?\$(\d+)-\d+`) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stack-check guard silently skips any TEXT line its strict regex can't parse, as long as another TEXT line in the same file matches
vet's asmTEXT regex (asmdecl.go:143) accepts all of the first three. A TEXT line in any of these forms is never checked. If it is the only TEXT in its file, the Concrete case: someone adds Fix: Count every line matching a loose |
||
|
|
||
| func TestKernelKeepsStackCheck(t *testing.T) { | ||
| files, err := filepath.Glob("*.s") | ||
| if err != nil || len(files) == 0 { | ||
| t.Fatalf("no assembly files found: %v", err) | ||
| } | ||
| for _, f := range files { | ||
| src, err := os.ReadFile(f) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| found := 0 | ||
| for i, line := range strings.Split(string(src), "\n") { | ||
| m := kernelTEXT.FindStringSubmatch(line) | ||
| if m == nil { | ||
| continue | ||
| } | ||
| found++ | ||
| name, flags, frame := m[1], m[2], m[3] | ||
| where := f + ":" + strconv.Itoa(i+1) | ||
| // Reject the whole flag field, not the spelling: textflag.h | ||
| // defines NOSPLIT as 4 and the assembler takes the number, so | ||
| // matching the name alone lets the regression back in. | ||
| if flags != "" { | ||
| t.Errorf("%s: %s carries assembler flags %q. NOSPLIT (spelled, or as its "+ | ||
| "numeric value 4) drops the stack-growth check, which is the only "+ | ||
| "preemption point in the block loops calling it; without it a large "+ | ||
| "Sum256 blocks every stop-the-world for the whole hash.", | ||
| where, name, flags) | ||
| } | ||
| if n, _ := strconv.Atoi(frame); n < 128 { | ||
| t.Errorf("%s: %s has a %d-byte frame, under abi.StackSmall (128). The "+ | ||
| "assembler marks a leaf that small NOSPLIT itself, which drops the "+ | ||
| "stack-growth check. Keep the frame at 128 bytes or more.", where, name, n) | ||
| } | ||
| } | ||
| if found == 0 { | ||
| t.Errorf("%s: no TEXT directive matched, so nothing in this file is "+ | ||
| "guarded; was the frame size turned into a #define?", f) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // gcWait reports the longest a GC had to wait while work ran in another | ||
| // goroutine. Work the runtime cannot preempt shows up here directly. | ||
| func gcWait(work func()) time.Duration { | ||
| done := make(chan struct{}) | ||
| go func() { defer close(done); work() }() | ||
| var worst time.Duration | ||
| for { | ||
| select { | ||
| case <-done: | ||
| return worst | ||
| default: | ||
| } | ||
| start := time.Now() | ||
| runtime.GC() | ||
| worst = max(worst, time.Since(start)) | ||
| } | ||
| } | ||
|
|
||
| // TestSTWPauseWhileHashing measures the property the TEXT flags only imply: how | ||
| // long the runtime waits for a hashing goroutine to yield. | ||
| // | ||
| // The budget is relative to the same hash computed through the pure-Go fallback, | ||
| // which always has a preemption point, so a slow or loaded runner moves both | ||
| // numbers together and only a real regression separates them. | ||
| func TestSTWPauseWhileHashing(t *testing.T) { | ||
| if testing.Short() || runtime.GOMAXPROCS(0) < 2 { | ||
| t.Skip("hashes 32 MiB, and needs GOMAXPROCS >= 2 to see a stalled GC") | ||
| } | ||
| if !useASM { | ||
| t.Skip("hardware acceleration unavailable on this CPU") | ||
| } | ||
| buf := make([]byte, 32*1024*1024) | ||
|
|
||
| useASM = false // same call, preemptible implementation | ||
| floor := gcWait(func() { | ||
| for range 4 { | ||
| Sum256(buf) | ||
| } | ||
| }) | ||
| useASM = true | ||
| got := gcWait(func() { | ||
| for range 4 { | ||
| Sum256(buf) | ||
| } | ||
| }) | ||
|
|
||
| t.Logf("worst GC wait: assembly %v, pure Go %v", got, floor) | ||
| if got > 5*time.Millisecond && got > 4*floor { | ||
| t.Fatalf("a GC waited %v on the assembly path against %v on the pure-Go path: "+ | ||
| "the block loop has no preemption point", got, floor) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generator comment makes 128 bytes the frame floor, but the kernel writes 200 bytes of scratch
The new comment says "do not shrink the frame below 128 bytes. The frame holds the round temp state". The test repeats it ("Keep the frame at 128 bytes or more", keccak_asm_test.go:90) and accepts any frame of 128 or more. But 128 is only the preemption floor. The real floor for this kernel is 200:
off()(gen_keccakf_bmi2.go:175-180) emitsidx*8(SP)for lanes 0-24, so the generated code writes up to192(SP)whateverfsizeis. The committed .s matches: its highest stack offset is192(SP).A maintainer who follows the comment and sets
fsize = 128gets a kernel that still passesTestKernelKeepsStackCheck. At run time the round code overwrites the saved BP at 128(SP), the return PC at 136(SP) and the caller's argument slots, and the program crashes at RET. The digest tests would crash right away, so the damage is contained, but the comment gives the wrong safe floor.Fix: Say the frame must stay at 200 bytes (25 × 8 bytes of round scratch, which
off()addresses up to 192(SP)). Mention abi.StackSmall (128) only as the separate reason a much smaller frame would also lose the stack check. Better, derive the offset limit fromfsizeso the two can't drift apart.