96 lines
14 KiB
Markdown
96 lines
14 KiB
Markdown
---
|
||
tags:
|
||
- cpm
|
||
- ios
|
||
- investigation
|
||
updated: '2026-10-30'
|
||
status: verified-control-flow-findings
|
||
---
|
||
# CPM extension: проверка JavaScript — 30 октября 2026
|
||
|
||
Да: отдельно проверены исходники самого extension и CPM bundle, извлечённый из iOS 7.243.0. Дополнение к [[cpm-ios-idle-wake-investigation-2026-10-30]].
|
||
|
||
Проверенный source checkout: `b00cc227112d4548755af9d3c4d93029c6e07066`, 15 октября, с init progress instrumentation. Это локальный checkout, не утверждение об актуальном upstream HEAD. Shipped app tag: `7.243.0+ios`, embedded manifest `2026.10.14`. Код приложения и extension не менялся.
|
||
|
||
## 1. Запоздалый initResp не привязан к исходному документу
|
||
|
||
Shipped handler сохраняет tabId/frameId и URL, затем ждёт config/settings/site policy. Ответ отправляется через `tabs.sendMessage(tabId, ..., {frameId})`; идентификатора документа или init request в payload нет. Content-script callback принимает любой `initResp` и вызывает initialize без проверки, какому init он принадлежал.
|
||
|
||
Если пока handler A ждёт ответ на policy check, вкладка переходит на B, её main-frame по-прежнему адресуется как frameId=0. Старый handler не отменяется. WebKit tabsSendMessage выбирает текущее tab webView и процессы слушателей; исходный документ A не является целью вызова без document targeting.
|
||
|
||
Контролируемая проверка:
|
||
- A отправляет init; её site check намеренно задержан.
|
||
- В той же вкладке создан B; для B site check возвращает false, dashboard получает site_disabled.
|
||
- После отпускания A старый initResp попадает в modeled current frame B.
|
||
- Реальный shipped receiveMessageCallback вызывает initialize(enabled=true) у B.
|
||
- При контрольном порядке A response → navigation B initialize происходит только у A.
|
||
|
||
Это дефект корреляции протокола. В проверке используется модель navigation/frame routing и spy вместо DOM initialize; физический переход на iPhone в этом проходе не запускался. Из кода подтверждено отсутствие document guard. Доля production эпизодов и фактические действия CPM на странице B не измерены.
|
||
|
||
Исправление: локальный per-document/per-init nonce, echo в ответе и проверка на стороне content script; отмена/игнорирование устаревших background задач. Сравнения только URL недостаточно при reload того же адреса. Поддерживаемый documentId может быть дополнительной защитой, но его совместимость следует проверить отдельно.
|
||
|
||
Sources:
|
||
- [Shipped sendMessage](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-extension/public/js/background-embedded.js:2715).
|
||
- [Shipped receiveMessageCallback](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-extension/public/js/content-scripts/cpm.js:3333).
|
||
- [Локальный extension source](/Users/admin/DuckDuckGo/duckduckgo-privacy-extension/shared/js/background/components/cookie-prompt-management.js:640).
|
||
|
||
## 2. Восстановление config не восстанавливает init текущего документа
|
||
|
||
При remoteConfigJson=null handler вызывает scheduleConfigRefresh, отправляет dashboard config_unavailable и возвращается без initResp. Новый refresh не сохраняет/не повторяет исходный init. Shipped content script отправляет init в конструкторе один раз; recovery-retry этой инициализации не найден.
|
||
|
||
Контролируемая проверка:
|
||
- config для первого init отсутствует;
|
||
- новый refresh сразу успешно восстанавливает config;
|
||
- после его завершения initResp по-прежнему не отправлен;
|
||
- явно повторённый init получает initResp.
|
||
|
||
Это конкретный пробел восстановления после временной ошибки. Dashboard при этом может сообщить config_unavailable и закрыть health episode, хотя исходный документ остаётся без инициализации. Сам по себе этот сценарий не объясняет browser-wide пяти минутный no-response: новые navigation после восстановления config способны работать.
|
||
|
||
В существующем unit-test проверен dashboard config_unavailable; восстановление исходного init этот тест не утверждает.
|
||
|
||
Исправление: ограниченный retry pending init для ещё живого того же документа; обязательны document guard, отмена на navigation и idempotency, чтобы не повторять действия CPM. Нельзя сохранять pending init только по tabId/frameId.
|
||
|
||
Source: [Shipped config-unavailable branch](/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/shipped-extension/public/js/background-embedded.js:2588).
|
||
|
||
## 3. Storage ожидания могут глобально удерживать dashboard
|
||
|
||
Подтверждённое ранее свойство: config get/set без собственного deadline и общая _stateQueue, которая ждёт session storage. Одна never-settled запись cpmState удерживает dashboard разных вкладок. Shipped probes дали две попытки отправить initResp и ноль dashboard-вызовов, пока запись не отпущена.
|
||
|
||
Это не доказательство естественного зависания WebKit storage. Но это конкретная зависимость критичного наблюдения от persistence. Состояние JS event loop/runtime может оставаться работоспособным. Отдельный app↔page ACK нужен, чтобы отличить реальную потерю CPM от зависания dashboard pipeline.
|
||
|
||
Исправление требует отделить оперативное in-memory состояние и delivery evidence от persistence, сохранив порядок/revision записей. Простой timeout не отменяет старую запись и может создать stale-write race.
|
||
|
||
## Проверки и границы
|
||
|
||
`extension-flow-probes.cjs` выполняет три сценария: config recovery без replay, ordered-navigation control, overlapping navigation. Они повторены на:
|
||
- shipped CookiePromptManagement + shipped receiveMessageCallback;
|
||
- классе CookiePromptManagement из локального extension source + том же shipped receiveMessageCallback.
|
||
|
||
Оба набора дали одинаковые результаты. Source I/O замокан; реальные методы обработки сообщений и приёма initResp используются без изменения. Constructor/alarms/pixel emission не являются предметом этих проверок. Config/state probes предыдущего прохода сохранены отдельно.
|
||
|
||
Результаты: `extension-flow-probe-results.json`, `extension-flow-checkout-results.json` в `/Users/admin/DuckDuckGo/apple-browsers.git/cpm-ios-idle-wake-investigation-2026-10-30/`.
|
||
|
||
Приоритет для узкого extension fix: document-bound init/response и cancellation; затем bounded recovery исходного init после временной config ошибки. Параллельно измерить state queue, чтобы проверить её вклад в wake dashboard failures. Ни один из механизмов пока нельзя объявить единственной причиной всей production idle-wake группы.
|
||
|
||
## Длительная поломка во всех вкладках — уточнение
|
||
|
||
Общий pending remoteConfigJson способен без собственного дедлайна остановить init всех новых документов текущего worker. Контролируемая проверка shipped методов с удерживаемым config promise: три новые вкладки — 0 initResp и 0 dashboard; отдельный свежий экземпляр с доступным config работает, пока старый остаётся заблокирован. После освобождения старого promise завершаются все три init. Это доказательство зависимости/liveness при заданном stall, а не естественного WebKit trigger.
|
||
|
||
Pending state write отличается: он блокирует общий dashboard pipeline, но initResp могут продолжать отправляться. Его нельзя автоматически называть фактическим отказом CPM на страницах.
|
||
|
||
Оба дефекта per-document из предыдущих разделов способны оставлять конкретный документ без корректной инициализации до reload. Они сами по себе не доказывают поломку всех будущих документов.
|
||
|
||
Проверен shipped M4 путь: reloadExtension успешно выгружает старый context и вызывает loader.loadWebExtension; makeContext создаёт новый WKWebExtensionContext. Чистая in-memory JS очередь/promise не должны пережить действительную замену worker. В проверенном upstream WebKit context unload вызывает invalidateStorage и сбрасывает sessionStorageStore; обычный idle unload background view этого не делает. Эти детали upstream не являются верификацией каждого shipped iOS engine.
|
||
|
||
Поэтому для отказа свежих navigation после успешно выполненного M4 нужны либо повторение trigger в новом worker, либо общий нижележащий сбой, либо ошибка самого recovery/измерения. Подтверждённого чисто extension-JS состояния, которое навсегда ломает новые документы и гарантированно переживает успешную полную замену контекста, в этом проходе не найдено. session storage здесь не следует путать с постоянной дисковой записью поломки.
|
||
|
||
Артефакты: global-config-probe.cjs / global-config-probe-results.json. Проверка не запускала реальный M4 на iPhone; fresh-instance control проверяет границу JS состояния.
|
||
|
||
## Общая очередь: связь между вкладками и лишние записи report
|
||
|
||
cpmState содержит dashboardStates по tabId, detectionCache, summaryEvents и globalErrors. Один экземпляр CookiePromptManagement обслуживает все вкладки. modifyCpmState добавляет операцию в один _stateQueue: getCpmState → callback изменения → await updateCpmState → await session.set. Ошибку/rejection цепочка ловит, после чего следующие операции могут идти; never-settled promise удерживает хвост. Это последовательная зависимость dashboard pipeline, не обязательная блокировка доставки initResp.
|
||
|
||
Дополнительная проверка shipped handler: 64 сообщения report от subframes с пустым state вызвали 64 записи полного cpmState при неизменившемся состоянии. Причина: case report всегда вызывает modifyCpmState, хотя все изменения в callback защищены isMainFrame; завершающая запись выполняется и для no-op. Это проверенное усиление нагрузки и связь iframe-событий одной страницы с задержкой dashboard других вкладок. Задержка на реальном storage и production outage этой проверкой не измерены. Результат сохранён в subframe-report-probe-results.json.
|
||
|
||
Другие общие места, совместимые с нормальной lifecycle-историей: pending remoteConfigJson; отсутствие RuntimeOnMessage route или отказ shared MessageRouter до autoconsent-handler; недоступный общий native bridge; повторяющийся JS exception на общем config/input; занятый SW JS thread; принятый native ответ, не прошедший current-document attribution. Для них нужны отдельные ingress/wait/rejection/ACK свидетельства. Нормальные bg_events описывают создание/удаление view и некоторые native/WebKit события, а не прохождение каждого из этих этапов. Их естественные production triggers пока не установлены.
|