fix: harden ghost clear race and aggressive webview reuse safety
- commit
- 8e7fa596047db9da67f3b4b154296baf72762d99
- author
- brice · · 2026-02-07 08:41 UTC
- parent
- 162a4b9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +14 -2
M internal/infrastructure/webkit/pool.go +3 -1
| @@ -519,7 +519,9 @@ func (p *WebViewPool) shouldReuseOnRelease(wv *WebView) bool { | |||
| 519 | 519 | case reusePolicySafe: | |
| 520 | 520 | return !wv.HasNavigationActivity() | |
| 521 | 521 | case reusePolicyAggressive: | |
| 522 | - | return true | |
| 522 | + | // Even in aggressive mode, avoid reusing views that already navigated. | |
| 523 | + | // Reusing navigated views can leak back/forward/page context across tabs. | |
| 524 | + | return !wv.HasNavigationActivity() | |
| 523 | 525 | default: | |
| 524 | 526 | return false | |
| 525 | 527 | } | |
| @@ -519,7 +519,9 @@ func (p *WebViewPool) shouldReuseOnRelease(wv *WebView) bool { | |||||
| 519 | case reusePolicySafe: | 519 | case reusePolicySafe: | ||
| 520 | return !wv.HasNavigationActivity() | 520 | return !wv.HasNavigationActivity() | ||
| 521 | case reusePolicyAggressive: | 521 | case reusePolicyAggressive: | ||
| 522 | - | return true | 522 | + | // Even in aggressive mode, avoid reusing views that already navigated. |
| 523 | + | // Reusing navigated views can leak back/forward/page context across tabs. | |||
| 524 | + | return !wv.HasNavigationActivity() | |||
| 523 | default: | 525 | default: | ||
| 524 | return false | 526 | return false | ||
| 525 | } | 527 | } | ||
M internal/ui/component/omnibox.go +11 -1
| @@ -914,8 +914,10 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) { | |||
| 914 | 914 | func (o *Omnibox) clearGhostText() { | |
| 915 | 915 | o.mu.Lock() | |
| 916 | 916 | realInput := o.realInput | |
| 917 | + | ghostFullText := o.ghostFullText | |
| 917 | 918 | hadGhost := o.hasGhostText || o.ghostSuffix != "" || o.ghostFullText != "" | |
| 918 | 919 | o.ghostToken++ | |
| 920 | + | capturedToken := o.ghostToken | |
| 919 | 921 | o.ghostSuffix = "" | |
| 920 | 922 | o.ghostFullText = "" | |
| 921 | 923 | o.hasGhostText = false | |
| @@ -929,10 +931,18 @@ func (o *Omnibox) clearGhostText() { | |||
| 929 | 931 | if o.entry == nil { | |
| 930 | 932 | return false | |
| 931 | 933 | } | |
| 934 | + | o.mu.RLock() | |
| 935 | + | currentToken := o.ghostToken | |
| 936 | + | o.mu.RUnlock() | |
| 937 | + | if capturedToken != currentToken { | |
| 938 | + | return false | |
| 939 | + | } | |
| 932 | 940 | o.mu.Lock() | |
| 933 | 941 | o.isAcceptingGhost = true | |
| 934 | 942 | o.mu.Unlock() | |
| 935 | - | if o.entry.GetText() != realInput { | |
| 943 | + | // Only revert text when the entry still shows the previous ghost completion. | |
| 944 | + | // If the user typed since scheduling this idle callback, never overwrite it. | |
| 945 | + | if o.entry.GetText() == ghostFullText && o.entry.GetText() != realInput { | |
| 936 | 946 | o.entry.SetText(realInput) | |
| 937 | 947 | } | |
| 938 | 948 | o.entry.SelectRegion(-1, -1) | |
| @@ -914,8 +914,10 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) { | |||||
| 914 | func (o *Omnibox) clearGhostText() { | 914 | func (o *Omnibox) clearGhostText() { | ||
| 915 | o.mu.Lock() | 915 | o.mu.Lock() | ||
| 916 | realInput := o.realInput | 916 | realInput := o.realInput | ||
| 917 | + | ghostFullText := o.ghostFullText | |||
| 917 | hadGhost := o.hasGhostText || o.ghostSuffix != "" || o.ghostFullText != "" | 918 | hadGhost := o.hasGhostText || o.ghostSuffix != "" || o.ghostFullText != "" | ||
| 918 | o.ghostToken++ | 919 | o.ghostToken++ | ||
| 920 | + | capturedToken := o.ghostToken | |||
| 919 | o.ghostSuffix = "" | 921 | o.ghostSuffix = "" | ||
| 920 | o.ghostFullText = "" | 922 | o.ghostFullText = "" | ||
| 921 | o.hasGhostText = false | 923 | o.hasGhostText = false | ||
| @@ -929,10 +931,18 @@ func (o *Omnibox) clearGhostText() { | |||||
| 929 | if o.entry == nil { | 931 | if o.entry == nil { | ||
| 930 | return false | 932 | return false | ||
| 931 | } | 933 | } | ||
| 934 | + | o.mu.RLock() | |||
| 935 | + | currentToken := o.ghostToken | |||
| 936 | + | o.mu.RUnlock() | |||
| 937 | + | if capturedToken != currentToken { | |||
| 938 | + | return false | |||
| 939 | + | } | |||
| 932 | o.mu.Lock() | 940 | o.mu.Lock() | ||
| 933 | o.isAcceptingGhost = true | 941 | o.isAcceptingGhost = true | ||
| 934 | o.mu.Unlock() | 942 | o.mu.Unlock() | ||
| 935 | - | if o.entry.GetText() != realInput { | 943 | + | // Only revert text when the entry still shows the previous ghost completion. |
| 944 | + | // If the user typed since scheduling this idle callback, never overwrite it. | |||
| 945 | + | if o.entry.GetText() == ghostFullText && o.entry.GetText() != realInput { | |||
| 936 | o.entry.SetText(realInput) | 946 | o.entry.SetText(realInput) | ||
| 937 | } | 947 | } | ||
| 938 | o.entry.SelectRegion(-1, -1) | 948 | o.entry.SelectRegion(-1, -1) | ||