7.2 KiB
tags, created, status
| tags | created | status | |||||
|---|---|---|---|---|---|---|---|
|
2026-06-23 | in-progress |
absoluteString O(n) scan — main-thread stall bugs
Root cause
String.firstIndex(of: "#") decodes grapheme clusters across every UTF-8 byte, making it O(n) in string length. URL.absoluteString materialises the full raw URL — including multi-megabyte data: URIs (e.g. content-scope privacy config JSON). Calling any character-scanning API on this on the main thread causes visible stalls and, for large enough payloads, a SIGKILL from the iOS watchdog.
Measured baseline (Apple Silicon M-series, optimised utf8 byte scan, no guard):
| Payload | Time |
|---|---|
1 MB, no # |
~15 ms |
1 MB, # at end |
~15 ms |
15 MB, no # |
~232 ms |
On older iPhone A-series (3–5× slower): 15 MB → ~700 ms–1.1 s per call. Stacked across multiple nav actions per load = watchdog territory.
Foundation.URL.fragment, by contrast, parses all URL components eagerly at URL(string:) init time — accessing .fragment is O(1) regardless of payload size (confirmed with 1 MB data: URI test, both with and without # at the end: < 1 ms).
Known fixed
hashedSuffixRange — StringExtension.swift
File: SharedPackages/Infrastructure/SystemFrameworksExtensions/Sources/FoundationExtensions/StringExtension.swift
Fix: data: guard at top of hashedSuffixRange short-circuits before any scan. Also replaced String.firstIndex(of: "#") with utf8.firstIndex(of: 0x23) (byte-level, avoids grapheme decoding).
Note: about: + %23 special case intentionally preserved — Foundation percent-encodes # to %23 in opaque about: URL absoluteStrings in some construction paths (e.g. URL(trimmedAddressBarString:)). Introduced in commit e880d888b3 ("fix about: scheme fragments dropping", Mar 2023). URL.fragment returns nil for these, so the string scan branch is genuinely needed.
Asana task: https://app.asana.com/1/137249556945/project/1202406491309510/task/1215403820548743
Known bugs — not yet fixed
HIGH — navigation hot path
isSameDocument / droppingHashedSuffix — URLExtension.swift:387
// URLExtension.swift:387
public func isSameDocument(_ other: URL) -> Bool {
self.absoluteString.droppingHashedSuffix() == other.absoluteString.droppingHashedSuffix()
}
Called from WKNavigationActionExtension.isSameDocumentNavigation (main thread, every nav action). Same O(n) scan via droppingHashedSuffix → hashedSuffixRange. Will be covered once the data: guard lands in hashedSuffixRange.
Fix suggestion: The data: guard in hashedSuffixRange propagates here automatically since droppingHashedSuffix delegates to it. No separate fix needed — covered by the StringExtension fix.
matches(_ other: URL) — URLExtension.swift:32–36
public func matches(_ other: URL) -> Bool {
let string1 = self.absoluteString
let string2 = other.absoluteString
return string1.droppingHashedSuffix().dropping(suffix: "/").appending(string1.hashedSuffix ?? "")
== string2.droppingHashedSuffix().dropping(suffix: "/").appending(string2.hashedSuffix ?? "")
}
Used in FrameInfo.Equatable, ContextMenuManager, PopupHandlingTabExtension — all main thread. Also covered by hashedSuffixRange data: guard.
DistributedNavigationDelegate.swift:673
hashedSuffix + droppingHashedSuffix in willPerformClientRedirect. Same chain. Covered by guard.
HIGH — JS injection with full absoluteString
TabViewController.swift:4719
"window.location.href='" + url.absoluteString + "'"
Entire URL embedded in a JS string for evaluateJavaScript. If url is a large data: URI, the JS string is megabytes long.
Fix suggestion: Check scheme before embedding. data: URIs should never be navigated to via window.location.href injection — bail early if url.scheme == "data".
Tab.swift / WKWebViewExtension.swift — escapedJavaScriptString()
Same pattern — absoluteString → escapedJavaScriptString() → evaluateJavaScript(...). Used in error-page recovery (location.replace(...)) and window.open.
Fix suggestion: Same — guard on scheme before serialising into JS.
HIGH — blob: credential stripping
macOS/DuckDuckGo/Common/Extensions/URLExtension.swift:843–849
// strippingUnsupportedCredentials() for blob: URLs
firstIndex(of: "@") + replacingOccurrences(..., .regularExpression)
blob: URLs can embed large payloads (base64-encoded media). Called from TabViewModel.addressBarString on @MainActor.
Fix suggestion: blob: URLs never have @-credentials in practice. Add a scheme guard:
guard scheme == "http" || scheme == "https" else { return self }
MEDIUM — address bar rendering
macOS/DuckDuckGo/Tab/ViewModel/TabViewModel.swift:403
addressBarString = url.absoluteString // non-blob path
TabViewModel.passiveAddressBarString already guards data: (returns "data:"), but addressBarString does not.
Fix suggestion: Apply same guard — if url.scheme == "data", set addressBarString = "data:".
iOS/DuckDuckGo/AddressDisplayHelper.swift:55–74
NSMutableAttributedString(string: absoluteString) + NSRange deemphasis. Omnibar rendering on main thread.
Fix suggestion: Same — cap or replace data: URLs before constructing attributed string.
iOS/DuckDuckGo/BackForwardMenuHistoryItem.swift:41–58
URL(trimmedAddressBarString: url.absoluteString), dropping, .count, dropLast on main thread for back/forward menu construction.
Fix suggestion: Guard on url.scheme != "data" before passing to trimmedAddressBarString initialiser.
MEDIUM — navigation pipeline regex/prefix checks
LinkCleaner.swift:36,61–65
NSRegularExpression.matches(in: url.absoluteString) — AMP format matching. Guarded to http/https by the call site but still scans full string length.
Fix suggestion: AMPCanonicalExtractor already has a maxURLLength guard — ensure LinkCleaner applies the same.
HistoryViewDataProvider.swift:478
absoluteString.localizedCaseInsensitiveContains(searchTerm) — history search. Runs on main thread. History URLs are rarely data: but theoretically possible.
AIChatMentionPickerFilter.swift:57
tab.url.absoluteString.lowercased().contains(query) — AI Chat tab picker filter, main thread.
Fix suggestion: Both — early-return false if url.scheme == "data" before scanning.
Test coverage added (branch fix-fragment-crash)
HashedSuffixDataURIPerformanceTestsinStringExtensionTests.swift— assertshashedSuffixcompletes in < 1 ms on 1 MB and 15 MBdata:URIs (with and without#at end). Fails without thedata:guard (15 MB → ~232 ms).URLFragmentDataURIPerformanceTestsinWKNavigationActionExtensionTests.swift— assertsURL.fragmentcompletes in < 10 ms on 1 MBdata:URI. Confirms Foundation parses eagerly (O(1) access).WKNavigationActionExtensionTests— full unit test suite forisSameDocumentNavigationcovering all nav types,data:URLs,about:%23edge case.