Files
obsidian-vault/work/projects/cpm-extension-js-review-2026-10-30.md
T

14 KiB
Raw Blame History

tags, updated, status
tags updated status
cpm
ios
investigation
2026-10-30 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:

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.

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 пока не установлены.