diff --git a/pkg/parser/schedule_fuzzy_scatter.go b/pkg/parser/schedule_fuzzy_scatter.go index 14080e3e916..6471ee1473e 100644 --- a/pkg/parser/schedule_fuzzy_scatter.go +++ b/pkg/parser/schedule_fuzzy_scatter.go @@ -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 @@ -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 { + scheduleFuzzyScatterLog.Printf("Warning: stableHash called with non-positive modulo %d, returning 0", modulo) + return 0 + } + // 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)) } // ScatterSchedule takes a fuzzy cron expression and a workflow identifier diff --git a/pkg/parser/schedule_fuzzy_scatter_test.go b/pkg/parser/schedule_fuzzy_scatter_test.go index 73fd341ce25..10055c49325 100644 --- a/pkg/parser/schedule_fuzzy_scatter_test.go +++ b/pkg/parser/schedule_fuzzy_scatter_test.go @@ -4,6 +4,7 @@ package parser import ( "fmt" + "math" "strings" "testing" ) @@ -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) {