diff --git a/work/projects/cpm-web-extension-breakage-findings.md b/work/projects/cpm-web-extension-breakage-findings.md index db096407..be2e3c45 100644 --- a/work/projects/cpm-web-extension-breakage-findings.md +++ b/work/projects/cpm-web-extension-breakage-findings.md @@ -783,3 +783,34 @@ Keys read by the recorder: `lifecycle` (string), `receivedInitResponse` (bool), - OSLogStore-based counters (per request). - iOS wiring (factory passes no recorder; behaviour unchanged there). - Breakage-report fields — `CPMMessagingDiagnosticsRecorder.snapshot().pixelParameters` is ready to be merged into `CookieConsentInfo` when wanted. + + +## New failure class: background → app native messaging (2026-09-09, late) + +Found by reading the actual `init` handler in `background-embedded.js` instead of WebKit. This class needs **no WebKit anomaly** and produces the exact all-tabs symptom and the exact health-monitor "stuck" signature. + +### Mechanism (extension side, `background-embedded.js`) + +`handleAutoConsentMessage` (2537) awaits three native requests before it ever sends `initResp`: `remoteConfigJson` (`getResourceIfNew`, 2555), `checkAutoconsentSetting` (`isAutoconsentSettingEnabled`, 2590), `checkAutoconsentEnabledForSite` (`isFeatureEnabled`, 2613). `_request` (2954) swallows any error or 20 s timeout and returns `undefined`; `checkAutoconsentSetting` turns that into `{ enabled: false }` (3040) → heuristic mode `""` → **"autoconsent setting not enabled" → `return` without `initResp`** (2592-2606). The follow-up `refreshDashboardState(setting_disabled)` is itself a native call, so when native messaging is broken nothing reaches the app → the health monitor sees no dashboard response → `cpmInitializationFailed` → `cpmMessagingStuck`. Every tab, every navigation, until the extension is reloaded. Content↔background works fine the whole time, so all WebKit-side probes read healthy. + +### App side: how native messaging breaks + +`WebExtensionContext::sendNativeMessage` calls the controller delegate directly (`WebExtensionContextAPIRuntimeCocoa.mm:338`), no permission or app-id check. The app (`WebExtensionManager+NativeMessaging.swift:122-151`) routes by `(context.uniqueIdentifier, featureName)` and returns **`nil`** on `.noHandler` and `.failure`; the extension turns `nil` into `throw "unexpected response type"` → `undefined` → disabled. Handlers exist only if `willLoad` registered them for the live context's identifier. + +Paths that remove handlers while a context stays loaded: +- `reloadExtension(identifier:)` (`WebExtensionManager.swift:414-449`): unload → `unregisterHandlers` → `loadWebExtension`. `WebExtensionLoader.loadWebExtension` has an idempotent branch (`WebExtensionLoader.swift:70-84`): if a context with that identifier is already in the controller it returns **without calling `willLoad`** → handlers stay unregistered. Reachable when another load of the same identifier interleaves between the unload and the load (e.g. scriptlet-triggered `reloadExtension(for:)` from `WebExtensionScriptletCoordinator.swift:165` racing the Fire `reloadInstalledExtensions`; the coordinator serializes only its own ops). Pinned by `testWhenExtensionIsAlreadyLoaded_ThenSecondLoadDoesNotCallDelegate`. +- Any future code path calling `unregisterHandlers` without a matching `willLoad`. + +Also possible without unregistering: a handler that never replies (delegate is `async`; a hang on the main actor) → 20 s timeout per request → three requests per `init` → same "disabled" outcome ~60 s later. + +### Deterministic reproduction (added) + +Debug → Web Extensions → **Break CPM Native Messaging (unregister handlers)** (`WebExtensionManager.unregisterNativeMessageHandlersForDebugging`). Then load a CMP page in any tab/window: banner not handled everywhere; Console shows `⚠️ No handler for extension '' feature 'autoconsent'`; snapshot shows `native_handler_registered=false` with everything else green; health monitor fires `initialization_failed` then `stuck`. Reload the extension from the debug menu → recovers. This is the first reproduction that matches the production signature end to end. + +### Diagnostics added + +`native_handler_registered` (router lookup identical to `routeMessage`) in snapshot/pixels. In production stuck pixels, `native_handler_registered=false` = this class; `=true` with all probes green points at the timeout variant (handler hang) or at the content-script marker being absent. + +### What this does and does not prove + +Proves the class produces the symptom and that the code has a path (idempotent load after unregister) that leaves the live context without handlers. Does **not** yet prove that path fires in production — that is what `native_handler_registered` in the stuck pixels and Console `No handler for extension` lines on internal machines will tell.