Files
obsidian-vault/work/wiki/apple-browsers/bugs-absolutestring-scan.md
T

7.2 KiB
Raw Blame History

tags, created, status
tags created status
ddg-browser
bugs
performance
ios
macos
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 (35× slower): 15 MB → ~700 ms1.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

hashedSuffixRangeStringExtension.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 / droppingHashedSuffixURLExtension.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 droppingHashedSuffixhashedSuffixRange. 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:3236

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.swiftescapedJavaScriptString()

Same pattern — absoluteStringescapedJavaScriptString()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:843849

// 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:5574

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:4158

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,6165

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)

  • HashedSuffixDataURIPerformanceTests in StringExtensionTests.swift — asserts hashedSuffix completes in < 1 ms on 1 MB and 15 MB data: URIs (with and without # at end). Fails without the data: guard (15 MB → ~232 ms).
  • URLFragmentDataURIPerformanceTests in WKNavigationActionExtensionTests.swift — asserts URL.fragment completes in < 10 ms on 1 MB data: URI. Confirms Foundation parses eagerly (O(1) access).
  • WKNavigationActionExtensionTests — full unit test suite for isSameDocumentNavigation covering all nav types, data: URLs, about:%23 edge case.