From 8dae61aa661858d616310c3415f35cd7843f1afa Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Wed, 10 Jun 2026 17:18:29 +0200 Subject: [PATCH] fix(race): read result validity before Merge redeems it to the pool validateRequiredDefinitions read red.IsValid() *after* res.Merge(red). Merge redeems a result whose wantsRedeemOnMerge is set (true for any pool-borrowed result) back into the process-global sync.Pool. Reading red after that point races with a concurrent Spec() goroutine borrowing the same *Result and calling cleared() on it. This surfaced as a rare data race under -race in Test_ParallelPool: Write at ... Result.cleared() <- resultsPool.BorrowResult() Previous read ... Result.IsValid() <- validateRequiredDefinitions Capture validity into a local before merging, matching the read-before- merge pattern used everywhere else (e.g. validateRequiredProperties, default/example validators). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Frederic BIDON --- spec.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec.go b/spec.go index 21b5ec8..0849e47 100644 --- a/spec.go +++ b/spec.go @@ -554,8 +554,12 @@ DEFINITIONS: if schema.Required != nil { // Safeguard for _, pn := range schema.Required { red := s.validateRequiredProperties(pn, d, &schema) //#nosec + // NOTE: capture validity before merging: Merge may redeem `red` to the + // pool (wantsRedeemOnMerge), after which reading it races with a concurrent + // BorrowResult().cleared() in another goroutine sharing the global pool. + isValid := red.IsValid() res.Merge(red) - if !red.IsValid() && !s.Options.ContinueOnErrors { + if !isValid && !s.Options.ContinueOnErrors { break DEFINITIONS // there is an error, let's stop that bleeding } }