Skip to content

Fix web extension load on locked devices + lighter burn reload#5235

Open
miasma13 wants to merge 6 commits into
mainfrom
michal/fix-load-extensions
Open

Fix web extension load on locked devices + lighter burn reload#5235
miasma13 wants to merge 6 commits into
mainfrom
michal/fix-load-extensions

Conversation

@miasma13
Copy link
Copy Markdown
Contributor

@miasma13 miasma13 commented Jun 2, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/414709148257752/task/1215332334414209?focus=true
Tech Design URL: N/A
CC:

Description

Two related web-extension issues on iOS (both gated behind iOS 18.4 + the webExtensions feature flag).

1. Lightweight reload on data clear (fire). Every fire unloaded all web extensions and then ran the full loadInstalledExtensions(), which re-reads and re-parses each extension archive from disk (WKWebExtension(resourceBaseURL:)) plus install-store reads and orphaned-file cleanup — even though the installed set never changes during a burn. New reloadInstalledExtensions() reuses the already-parsed WKWebExtension captured at unload time, keeping only the controller.unload/load cycle that resets background-page in-memory state. Wired on both iOS and macOS; falls back to a full load if nothing is cached or a reload throws.

2. WKWebExtensionErrorInvalidArchive (domain code 9), ~400/day on iOS. The on-disk extension archive was being read by WKWebExtension(resourceBaseURL:) while protected data was unavailable (device locked / before first unlock), so the read failed. It surfaced on both load and install because both construct the extension from the same stored artifact (and a failed load auto-uninstalls, so the follow-up reinstall fails too — a cascade). Fix: gate both the load (scheduleExtensionLoad) and install/sync (syncEmbeddedExtensions) on UIApplication.shared.isProtectedDataAvailable; when unavailable, defer the work and replay it once on protectedDataDidBecomeAvailableNotification. macOS has no file data protection, so this gate is iOS-only.

Two temporary pixels are added to measure the frequency of this fallback:

  • m_web_extension_deferred_protected_data_unavailable — fires when web-extension load/install is postponed because protected data isn't readable yet (device locked / before first unlock), i.e. the situation that previously produced the WKWebExtensionErrorInvalidArchive (code 9) errors.
  • m_web_extension_resumed_protected_data_available — fires when protected data becomes available and that deferred load/install actually runs.

Testing Steps

Prerequisites

  • Use no feature-flag overrides (default/production flag state).
  • A device/simulator on an OS that supports web extensions: iOS 18.4+ / macOS 15.4+.
  • Cookie Pop-up Management (CPM) enabled.
  • YouTube ad blocking enabled.
  • Duck Player disabled
  • Run the full flow on both iOS and macOS.

Steps

  1. Navigate to YouTube and play a video.
  2. Confirm the "YouTube Ad block on" badge animates into the address bar.
  3. Navigate to https://privacy-test-pages.site/features/autoconsent/index.html.
  4. Confirm the "Cookies managed" badge animates into the address bar and the page's button shows the "I was clicked!" label.
  5. Tap the Fire button and clear everything.
  6. Repeat steps 1–5 2–3x times, confirming both features still work correctly after each Fire.

▎ Steps 5–6 are the core check: each Fire unloads the web extensions and re-runs the lightweight reloadInstalledExtensions(), so the ad-blocking and autoconsent (embedded) extensions must reload and function correctly after every burn — with no degradation across repeated Fires.

Impact and Risks

Medium — changes web-extension load/install timing on the fire and launch paths. Blast radius limited to users on iOS 18.4+ with the webExtensions feature flag enabled.

What could go wrong?

  • If protectedDataDidBecomeAvailableNotification never fired, deferred load/install could stay pending until the next launch. Mitigated: the gate only defers when protected data is genuinely unavailable, a normal unlock posts the notification, and teardown clears any pending work.
  • The lightweight reload could reload a stale set if the installed set changed while unloaded. Mitigated: it falls back to the full loadInstalledExtensions() on an empty cache or any reload failure, and feature-flag-driven installs run a full sync separately.

Quality Considerations

  • Edge cases: device locked at cold launch / auto-clear-on-launch; both the load path and the standalone install triggers (feature-flag handlers, YouTube ad-blocking notification) are gated.
  • Performance: removes per-burn disk re-parse, install-store reads, and orphan cleanup; reload is now just the WebKit unload/load cycle.
  • Privacy: no change to what extensions can access; the deferral only delays loading until the files are readable.
  • Monitoring: two new daily pixels — m_web_extension_deferred_protected_data_unavailable and m_web_extension_resumed_protected_data_available — to confirm code-9 errors drop and deferrals recover.
  • Tests: unit tests added for reloadInstalledExtensions (lightweight path, empty-cache fallback, failure fallback).

Notes to Reviewer

  • macOS receives the reload optimization (issue 1) but not the protected-data gate (issue 2), which is iOS-only by design (macOS has no file data protection).
  • Verified the WebExtensions package builds and its tests pass; the iOS app's Swift sources compile (MainCoordinator/PixelEvent build clean) — a full app build only trips on environmental headless-CLI script phases (codesign identity, simulator TARGET_DEVICE_OS_VERSION), unrelated to these changes.

Internal references:

Definition of Done | Engineering Expectations | Tech Design Template


Note

Medium Risk
Changes web extension load timing on fire and cold launch (iOS 18.4+ with web extensions enabled); mitigated by full-load fallback and unlock notification replay, but deferred work could linger if protected-data notifications never fire.

Overview
Adds a fire/data-clear reload path that reuses in-memory WKWebExtension instances captured during unloadAllExtensions() instead of re-reading archives from disk. New reloadWebExtension on the loader and reloadInstalledExtensions() on the manager wire this into iOS and macOS burn completion (replacing loadInstalledExtensions()), with fallback to a full load when the cache is empty or any lightweight reload fails.

On iOS only, extension startup load and embedded-extension sync now wait until UIApplication.shared.isProtectedDataAvailable, deferring work via coalesced callbacks on protectedDataDidBecomeAvailable to avoid WKWebExtensionErrorInvalidArchive when Application Support is unreadable while the device is locked.

Adds daily pixels for deferral/resume and unit tests for the reload fallbacks and lightweight path.

Reviewed by Cursor Bugbot for commit 61f4337. Bugbot is set up for automated code reviews on this repo. Configure here.

miasma13 added 2 commits June 2, 2026 13:30
…in-memory parsed extension instead of re-reading from disk on every burn
… avoid InvalidArchive errors on locked devices, with deferral/resume pixels
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Privacy Review task: https://app.asana.com/0/69071770703008/1215338943121945

Comment thread iOS/DuckDuckGo/UICoordination/MainCoordinator.swift
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 61f4337. Configure here.

@miasma13 miasma13 requested a review from tomasstrba June 3, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant