[2026-06-30] eagle: work/tech-design/opaque-url-fragment-detection.md

This commit is contained in:
Alexey Martemyanov
2026-06-30 18:32:31 +06:00
parent a7e7cff8c8
commit 2368a5f814
@@ -81,26 +81,34 @@ Every downstream Foundation API (`URL.fragment`, `URLComponents`, `NSURLComponen
Since NSURL encodes `#` as `%23`, scanning for it recovers the fragment in many cases. Partly working, but fundamentally ambiguous: `%23` is also valid payload in `data:` URLs (SVG fill colors, anchor hrefs, CSS). The two cases produce identical `absoluteString` and cannot be distinguished from the string alone. A size guard avoids scanning large payloads but causes incorrect equality results.
### 2. `CFURLCreateAbsoluteURLWithBytes` hook
### 2. `CFURLCreateAbsoluteURLWithBytes` GOT-patch hook
WebKit calls `CFURLCreateAbsoluteURLWithBytes()` at the exact moment it converts a `WTF::URL` to a `CFURL`. The raw byte buffer at that call site still contains the literal `#`. Intercepting this C function via the Mach-O `__DATA,__interpose` section (without method swizzling) gives unambiguous access to the original bytes — scan for `0x23` in a simple C loop, store the byte range as an associated object on the bridged `NSURL`.
WebKit calls `CFURLCreateAbsoluteURLWithBytes()` at the exact moment it converts a `WTF::URL` to a `CFURL`. The raw byte buffer at that point still contains the literal `#`. The hook intercepts this call using **runtime GOT patching** (same technique as `CxaThrowSwapper` / fishhook) — not a link-time `__DATA,__interpose` section, which would not reach WebKit's internal call from `WebKit.framework` into `CoreFoundation.framework`.
```c
#define DYLD_INTERPOSE(_replacement, _replacee) \
__attribute__((used)) \
static struct { const void *replacement; const void *replacee; } \
_interpose_##_replacee \
__attribute__((section("__DATA,__interpose"))) = \
{ (const void *)&(_replacement), (const void *)&(_replacee) }
Mechanism:
1. `_dyld_register_func_for_add_image` registers a callback that fires for every Mach-O image loaded into the process (including already-loaded images at registration time).
2. The callback walks the image's **indirect symbol table** (`ImageMap.rebindSymbol`) targeting only the `WebKit` dylib, finds its GOT entry for `CFURLCreateAbsoluteURLWithBytes`, and replaces it with the hook pointer. ARM64e pointer authentication is handled via `BSKStripFunctionPointer` / `BSKSignFunctionPointer`.
3. The hook calls the original via the saved pointer, then scans the raw byte buffer with `memchr` (SIMD-vectorised on ARM64/x86) for `':'` (opaque check) then `'#'`, and stores the resulting `NSRange` as an associated object on the `CFURL` result.
4. `URL.opaqueFragmentAnnotation` reads/writes that associated object; `URL.isOpaqueFragmentScanned` guards against double-scanning the same instance.
5. `uninstallCFURLSwapper()` walks all loaded images and restores the saved original pointer.
```swift
// install: register dyld callback patch WebKit's GOT entry at runtime
_dyld_register_func_for_add_image(_cfURLProcessMachHeader)
// hook: memchr scan + associated-object annotation
if let hashRaw = memchr(searchBase, Int32(UInt8(ascii: "#")), searchLen) {
url.opaqueFragmentAnnotation = NSRange(location: hashOff, length: len - hashOff)
}
```
Callers read `NSURL.ddg_fragmentByteRange` for the unambiguous `#` position. Could be limited to `#ifdef DEBUG` so there is zero production overhead.
**Downside:** couples to WebKit's dylib name and a private CFURL symbol; requires bypassing page protection to write into a read-only GOT page; depends on dyld image-load ordering; massive and fragile.
**Downside:** adds Objective-C interpose files (`CFURLCreateAbsoluteURLWithBytesInterpose.m`, `NSURL+FragmentByteRange.h/.m`), couples to a private CFURL entry point, and requires the associated-object write to happen before any Swift code reads the URL — fragile under future WebKit changes.
### 2a. `__DATA,__interpose` (static link-time interpose) ⚠️ (option, not chosen)
### 2a. `dyld_interpose` at the dyld level ⚠️ (option, not chosen)
A related but distinct mechanism: a `__DATA,__interpose` section in the binary redirects calls to the named C function to a replacement at load time, before any code runs. Simpler than GOT patching (no dyld callback, no image walk), and dyld applies it process-wide so it does intercept calls from other frameworks.
Same concept as above but applied via `DYLD_INSERT_LIBRARIES` + `DYLD_INTERPOSE` at the dynamic linker level, outside the process binary entirely. More powerful (intercepts calls from any dylib), but more invasive, not available in App Store binaries, and harder to confine to DEBUG.
**Downside:** the `__DATA,__interpose` section is a well-known jailbreak / hooking fingerprint; App Store review tooling flags it, making approval unlikely.
---