From 5465e2b4a90cf65c1048bc7e782a73513901aa5c Mon Sep 17 00:00:00 2001 From: zz_y Date: Fri, 4 Sep 2026 05:52:54 -0600 Subject: [PATCH 1/2] fix(precompute): scope compiled matchers to config --- asap-precompute-go/matcher_lifecycle_test.go | 18 ++++++++ asap-precompute-go/matchers.go | 44 +++++++++++--------- asap-precompute-go/precompute.go | 8 ++++ 3 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 asap-precompute-go/matcher_lifecycle_test.go diff --git a/asap-precompute-go/matcher_lifecycle_test.go b/asap-precompute-go/matcher_lifecycle_test.go new file mode 100644 index 00000000..5bd0d03b --- /dev/null +++ b/asap-precompute-go/matcher_lifecycle_test.go @@ -0,0 +1,18 @@ +package precompute + +import "testing" + +func TestRuntimeConfigOwnsCompiledMatchers(t *testing.T) { + source := &PrecomputeConfig{AggID: 1, Matchers: []LabelMatcher{{Name: "zone", Op: MatchRegex, Value: "a|b"}}} + p := New(source, newFakeFactory(), &fakeObserver{}).(*precompute) + if p.activeConfig().Matchers[0].compiled == nil { + t.Fatal("runtime matcher was not compiled at installation") + } + source.Matchers[0].Value = "never" + if !p.activeConfig().Matches(&Observation{Labels: []KeyValue{{Key: "zone", Value: "a"}}}) { + t.Fatal("caller mutation changed installed matcher") + } + if err := ValidateMatchers([]LabelMatcher{{Op: MatchRegex, Value: "["}}); err == nil { + t.Fatal("invalid regexp accepted") + } +} diff --git a/asap-precompute-go/matchers.go b/asap-precompute-go/matchers.go index 6c70a883..dad8df10 100644 --- a/asap-precompute-go/matchers.go +++ b/asap-precompute-go/matchers.go @@ -1,6 +1,7 @@ package precompute import ( + "fmt" "regexp" "sort" "strconv" @@ -54,40 +55,45 @@ type LabelMatcher struct { // pattern (Regex/NotRegex). Value string // Op picks Equal / NotEqual / Regex / NotRegex. - Op MatchOp + Op MatchOp + compiled *regexp.Regexp + prepared bool } -// regexCache memoizes compiled, fully-anchored regexps keyed by the raw -// pattern string. LabelMatcher is a value type embedded in -// PrecomputeConfig, so the matcher can't hold a *regexp.Regexp without -// breaking config copies / comparisons; instead Matches() looks the -// compiled form up here. A failed compile is cached as a nil regexp so -// repeated bad patterns don't recompile (and deterministically fail to -// match). -var regexCache sync.Map // map[string]*regexp.Regexp (nil ⇒ compile failed) +// ValidateMatchers rejects malformed regex configuration before activation. +func ValidateMatchers(matchers []LabelMatcher) error { + for i, matcher := range matchers { + if matcher.Op != MatchRegex && matcher.Op != MatchNotRegex { + continue + } + if _, err := regexp.Compile("^(?:" + matcher.Value + ")$"); err != nil { + return fmt.Errorf("matcher %d: %w", i, err) + } + } + return nil +} // compileAnchored returns the compiled, Prometheus-anchored regexp for // pattern (matching the full string via ^(?:...)$), using a process-wide // cache. Returns nil if the pattern fails to compile. func compileAnchored(pattern string) *regexp.Regexp { - if v, ok := regexCache.Load(pattern); ok { - if v == nil { - return nil - } - return v.(*regexp.Regexp) - } // Anchor like Prometheus: the pattern must match the entire value. // Wrap in a non-capturing group so top-level alternation (a|b) // anchors as a whole rather than ^a|b$. re, err := regexp.Compile("^(?:" + pattern + ")$") if err != nil { - regexCache.Store(pattern, (*regexp.Regexp)(nil)) return nil } - regexCache.Store(pattern, re) return re } +func (m LabelMatcher) regexp() *regexp.Regexp { + if m.prepared { + return m.compiled + } + return compileAnchored(m.Value) +} + // Matches returns true iff the observation satisfies all matchers. // // Semantics replicate today's per-processor matchesMatchers: @@ -130,7 +136,7 @@ func (cfg *PrecomputeConfig) Matches(obs *Observation) bool { if !present { return false } - re := compileAnchored(m.Value) + re := m.regexp() if re == nil || !re.MatchString(v) { return false } @@ -139,7 +145,7 @@ func (cfg *PrecomputeConfig) Matches(obs *Observation) bool { // Present-and-matching fails. A bad pattern (nil re) can't // match anything, so the negation passes. if present { - if re := compileAnchored(m.Value); re != nil && re.MatchString(v) { + if re := m.regexp(); re != nil && re.MatchString(v) { return false } } diff --git a/asap-precompute-go/precompute.go b/asap-precompute-go/precompute.go index ca864203..75401523 100644 --- a/asap-precompute-go/precompute.go +++ b/asap-precompute-go/precompute.go @@ -1070,6 +1070,14 @@ func clonePrecomputeConfig(source *PrecomputeConfig) *PrecomputeConfig { } cloned := *source cloned.Matchers = append([]LabelMatcher(nil), source.Matchers...) + for i := range cloned.Matchers { + matcher := &cloned.Matchers[i] + matcher.prepared = true + matcher.compiled = nil + if matcher.Op == MatchRegex || matcher.Op == MatchNotRegex { + matcher.compiled = compileAnchored(matcher.Value) + } + } cloned.AggregateBy = append([]string(nil), source.AggregateBy...) cloned.Quantiles = append([]float64(nil), source.Quantiles...) if source.SketchParams != nil { From 7c14a12fae968a9a116e2fa1fe83435f17e0b2d3 Mon Sep 17 00:00:00 2001 From: zz_y Date: Fri, 4 Sep 2026 06:03:33 -0600 Subject: [PATCH 2/2] fix(precompute): reject invalid matcher configs --- asap-precompute-go/matcher_lifecycle_test.go | 14 ++++++++++++++ asap-precompute-go/precompute.go | 11 ++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/asap-precompute-go/matcher_lifecycle_test.go b/asap-precompute-go/matcher_lifecycle_test.go index 5bd0d03b..ff911c56 100644 --- a/asap-precompute-go/matcher_lifecycle_test.go +++ b/asap-precompute-go/matcher_lifecycle_test.go @@ -16,3 +16,17 @@ func TestRuntimeConfigOwnsCompiledMatchers(t *testing.T) { t.Fatal("invalid regexp accepted") } } + +func TestInvalidMatcherConfigFailsClosed(t *testing.T) { + invalid := &PrecomputeConfig{AggID: 1, Matchers: []LabelMatcher{{Op: MatchRegex, Value: "["}}} + p := New(invalid, newFakeFactory(), &fakeObserver{}).(*precompute) + if p.activeConfig() != nil { + t.Fatal("invalid initial config became active") + } + valid := &PrecomputeConfig{AggID: 1, Matchers: []LabelMatcher{{Op: MatchEqual, Value: "m"}}} + p = New(valid, newFakeFactory(), &fakeObserver{}).(*precompute) + p.UpdateConfig(&PrecomputeConfigSet{Configs: []PrecomputeConfig{*invalid}}) + if p.activeConfig().Matchers[0].Op != MatchEqual { + t.Fatal("invalid update replaced active config") + } +} diff --git a/asap-precompute-go/precompute.go b/asap-precompute-go/precompute.go index 75401523..e7f56b19 100644 --- a/asap-precompute-go/precompute.go +++ b/asap-precompute-go/precompute.go @@ -323,9 +323,11 @@ func New(initialCfg *PrecomputeConfig, sketchFactory SketchFactory, observer Ske p.window.snapshotCache = p.snapshotCache p.window.sketchSink = &p.sketchSink if initialCfg != nil { - cfgCopy := clonePrecomputeConfig(initialCfg) - p.cfg.Store(cfgCopy) - p.sketchType = initialCfg.SketchType + if ValidateMatchers(initialCfg.Matchers) == nil { + cfgCopy := clonePrecomputeConfig(initialCfg) + p.cfg.Store(cfgCopy) + p.sketchType = initialCfg.SketchType + } } return p } @@ -1047,6 +1049,9 @@ func (p *precompute) UpdateConfig(cs *PrecomputeConfigSet) { if chosen == nil { chosen = &cs.Configs[0] } + if ValidateMatchers(chosen.Matchers) != nil { + return + } cfgCopy := clonePrecomputeConfig(chosen) // An in-flight window is owned by its current immutable config. Stage the // replacement until Tick/Drain closes that generation; otherwise old sketch