Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pkg/parser/schedule_fuzzy_scatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func avoidPeakMinutes(hour, minute int) int {

// stableHash returns a deterministic hash value in the range [0, modulo)
// using FNV-1a hash algorithm, which is stable across platforms and Go versions.
// If modulo is <= 0, stableHash returns 0 (the range is empty/invalid).
func stableHash(s string, modulo int) int {
h := fnv.New32a()
// hash.Hash.Write never returns an error in practice, but check to satisfy gosec G104
Expand All @@ -176,7 +177,13 @@ func stableHash(s string, modulo int) int {
scheduleFuzzyScatterLog.Printf("Warning: hash write failed: %v", err)
return 0
}
return int(h.Sum32() % uint32(modulo))
if modulo <= 0 {
Comment thread
pelikhan marked this conversation as resolved.
scheduleFuzzyScatterLog.Printf("Warning: stableHash called with non-positive modulo %d, returning 0", modulo)
return 0
Comment thread
pelikhan marked this conversation as resolved.
Comment thread
pelikhan marked this conversation as resolved.
}
// Use int64 arithmetic to avoid truncation when modulo exceeds math.MaxUint32
// on 64-bit platforms where int is 64 bits wide.
return int(int64(h.Sum32()) % int64(modulo))
Comment thread
pelikhan marked this conversation as resolved.
}

// ScatterSchedule takes a fuzzy cron expression and a workflow identifier
Expand Down
18 changes: 18 additions & 0 deletions pkg/parser/schedule_fuzzy_scatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package parser

import (
"fmt"
"math"
"strings"
"testing"
)
Expand Down Expand Up @@ -557,6 +558,23 @@ func TestStableHash(t *testing.T) {
if hash1 == hash3 {
t.Logf("Warning: different strings produced same hash (rare but possible)")
}

// Test zero modulo returns 0 (guard against divide-by-zero)
if got := stableHash("any", 0); got != 0 {
t.Errorf("stableHash(_, 0) = %d, want 0", got)
}

// Test negative modulo returns 0 safely
if got := stableHash("any", -1); got != 0 {
t.Errorf("stableHash(_, -1) = %d, want 0", got)
}

// Test modulo larger than math.MaxUint32 to guard against truncation regression
const largeModulo = math.MaxUint32 + 1 // 4_294_967_296
hashLarge := stableHash("test-workflow", largeModulo)
if hashLarge < 0 || hashLarge >= largeModulo {
t.Errorf("stableHash out of range for large modulo: got %d, want [0, %d)", hashLarge, largeModulo)
}
}

func TestScatterScheduleWeekdays(t *testing.T) {
Expand Down
Loading