From 9ba485290b2337804ac8eb81afdbb18245bd7acb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 25 Feb 2026 00:41:08 +0900 Subject: [PATCH] Fix data race --- src/core.go | 7 +++++++ src/matcher.go | 23 +++++++++++++++++++++-- src/terminal.go | 11 +++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/core.go b/src/core.go index 7ad0cdb1..20f41def 100644 --- a/src/core.go +++ b/src/core.go @@ -460,6 +460,11 @@ func Run(opts *Options) (int, error) { } if val.withNth != nil { newTransformer := val.withNth.fn + // Cancel any in-flight scan and block the terminal from reading + // items before mutating them in-place. Snapshot shares middle + // chunk pointers, so the matcher and terminal can race with us. + matcher.CancelScan() + terminal.PauseRendering() // Reset cross-line ANSI state before re-processing all items lineAnsiState = nil prevLineAnsiState = nil @@ -476,6 +481,8 @@ func Run(opts *Options) (int, error) { }, func() { nthTransformer = newTransformer }) + terminal.ResumeRendering() + matcher.ResumeScan() withNthChanged = true bump = true } diff --git a/src/matcher.go b/src/matcher.go index eb22abd8..189ef599 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -45,6 +45,8 @@ type Matcher struct { slab []*util.Slab mergerCache map[string]MatchResult revision revision + scanMutex sync.Mutex + cancelScan *util.AtomicBool } const ( @@ -66,7 +68,8 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern, partitions: partitions, slab: make([]*util.Slab, partitions), mergerCache: make(map[string]MatchResult), - revision: revision} + revision: revision, + cancelScan: util.NewAtomicBool(false)} } // Loop puts Matcher in action @@ -126,7 +129,9 @@ func (m *Matcher) Loop() { } if result.merger == nil { + m.scanMutex.Lock() result = m.scan(request) + m.scanMutex.Unlock() } if !result.cancelled { @@ -238,7 +243,7 @@ func (m *Matcher) scan(request MatchRequest) MatchResult { break } - if m.reqBox.Peek(reqReset) { + if m.cancelScan.Get() || m.reqBox.Peek(reqReset) { return MatchResult{nil, nil, wait()} } @@ -269,6 +274,20 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision}) } +// CancelScan cancels any in-flight scan, waits for it to finish, +// and prevents new scans from starting until ResumeScan is called. +// This is used to safely mutate shared items (e.g., during with-nth changes). +func (m *Matcher) CancelScan() { + m.cancelScan.Set(true) + m.scanMutex.Lock() + m.cancelScan.Set(false) +} + +// ResumeScan allows scans to proceed again after CancelScan. +func (m *Matcher) ResumeScan() { + m.scanMutex.Unlock() +} + func (m *Matcher) Stop() { m.reqBox.Set(reqQuit, nil) } diff --git a/src/terminal.go b/src/terminal.go index dcd9b01d..a87064e4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1738,6 +1738,17 @@ func (t *Terminal) Input() (bool, []rune) { return paused, copySlice(src) } +// PauseRendering blocks the terminal from reading items. +// Must be paired with ResumeRendering. +func (t *Terminal) PauseRendering() { + t.mutex.Lock() +} + +// ResumeRendering releases the lock acquired by PauseRendering. +func (t *Terminal) ResumeRendering() { + t.mutex.Unlock() +} + // UpdateCount updates the count information func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) { t.mutex.Lock()