Skip to content

Android to Firefox 153.4.0esr (security), Mullvad DoH migration, uBO startup fixes - #2

Merged
CPlusPlus17 merged 33 commits into
mainfrom
android/esr-153.4
Oct 2, 2026
Merged

CPlusPlus17 merged 33 commits into
mainfrom
android/esr-153.4

Conversation

@CPlusPlus17

Copy link
Copy Markdown
Owner

Stacked on #1 (base desktop/firefox-157). Merge #1 first, then retarget this PR to main.

What changes for Android

  • Firefox 153.0esr → 153.4.0esr. This brings in the ESR security advisories MFSA 2026-77, -85, -93 and -100. Three patches were fixed, and one was dropped because 153.4 ships the same fix (fix-canvas-extraction-permission).
  • Mullvad DNS-over-HTTPS: Mullvad is shutting down its DoH service. It is removed from the picker, and on upgrade, users who had picked it move to the default provider (Quad9) with their mode unchanged. They see a one-time notice if DoH was active.
  • Settings (owner-approved): the telemetry client-ID canary, disk-cache encryption, the uBO bootstrap URL on librewolf.dev, and release notes pointing at this repo's Releases page.
  • Fixed (also in published Beta 2):
    • "uBlock Origin setup failed" on every fresh install. AMO auto-updates uBO during setup. The pin is now 1.75.0, and the readiness check survives an AMO update. It still requires a Mozilla-signed copy at or above the pinned version.
    • The same dialog 30 s after every launcher cold start. uBO's background page waited for a Gecko window that a launcher start never opens.
  • Fixed (new in 153.4): about:config showed only a spinner. 153.4's baseline CSP for system pages blocked the page's inline handlers, so this backports upstream's own fix, which moves them into the script. The CSP is not weakened.
  • Fixed (latent, shipped since Sep 14): firefox-suggest-data.patch had a @@ hunk header with no line numbers, so GNU patch skipped it without a word. A new lint rejects such headers.

Verified

  • Gates: check-patchfail (android on 153.4.0esr, desktop unchanged), lint-patch-scope, check-patch-order (CI's failing step, now 30/30), board.py checks. The full Fenix unit suite gives 0 unexpected failures (E5), and three Android Components tests from our patches that had never compiled were fixed.
  • Release APKs are built (R8, unsigned, 3 ABIs plus universal), with versionCode above Beta 2. Hashes are in the evidence README.
  • On device, rc2 x86_64, fresh profile per check, AMO reachable: the full smoke suite passes, except the two checks E12 expects to be red. That includes the complete graphics acceptance (161 checks), launcher start, about:config with 0 CSP errors, the uBO checks, and no-suggest (3/3, with a suggestions-ON negative control that catches ac.duckduckgo.com). The pref audit exits 0.
  • A real upgrade test, Beta 2 with Mullvad in Max mode → rc2: Mullvad is replaced by Quad9 with mode 3 kept, the dialog shows exactly once, and a fresh install shows no dialog.
  • Evidence: docs/android/evidence/lw-m7-01/esr-153.4.0/ and docs/android/REBASE.md Appendix D.

Owner decisions still open

  • 9 Firefox-for-Android CVEs have no ESR counterpart. CVE-2026-18809, -84117 and -92033 are high. Each needs a backport or a published known gap.
  • network.lna.block_insecure_contexts: upstream changed it to false in 153.4. Should Redoubt set it?
  • Mullvad notice wording (English only, names dns10.quad9.net).
  • expected-prefs.txt still says librewolf.webgl.prompt=false, while it has shipped true since LW-M7-14. It was left unregenerated on purpose.
  • Whether to tell Beta 2 testers about the uBO dialogs, and when to release rc2. Signing and publishing stay with the key holder; nothing here was release-signed.

🤖 Generated with Claude Code

CPlusPlus17 and others added 30 commits October 2, 2026 01:12
Moves the settings submodule 6b41a31 -> 0cae8fb (branch
android-esr-153.4). Android only; common.cfg, desktop.cfg and librewolf.cfg
are byte-identical, so desktop is unaffected.

Android now takes upstream's value for doh-rollout.provider-list (Mullvad
removed), toolkit.telemetry.cachedClientID (locked canary UUID instead of
""), browser.cache.disk.encryption.enabled (true) and
librewolf.uBO.assetsBootstrapLocation (librewolf.dev; verified HTTP 200 and
byte-identical to the codeberg body). app.releaseNotesURL points at
https://github.com/CPlusPlus17/Redoubt/releases.

board.py CFG_EXPECTED is unchanged: it counts common.cfg + desktop.cfg only,
and neither changed. android.cfg goes from 24 to 20 calls.

  python3 docs/android/board.py --check-cfg-split
    ok: 181 common / 200 desktop / 20 android calls; librewolf.cfg
    regenerates exactly
  python3 docs/android/board.py --check            ok
  scripts/tests/test-android-parity-gates.py       29 tests OK
  android-network-check.sh / android-rfp-check.sh --self-test   passed

Not changed here: patches/android/privacy-defaults.patch still carries
Mullvad in its hand-kept Kotlin copy of the DoH list (the Fenix picker reads
that, not the pref); that and the user migration belong to a separate change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app/build.gradle section had a bare '@@' hunk header. GNU patch skips
such a hunk silently - exit 0, no "patching file" line, no .rej - so
check-patchfail called the patch clean while the
`implementation ComponentsDependencies.mozilla_appservices_suggest` line
never reached the tree on any base.

The section is now `@@ -545,6 +545,13 @@ dependencies {`, generated with
git diff against the tree as patched by every entry before it (no-adjust,
no-glean, no-gms, no-crashreporter, fenix-abi-split, branding,
update-check). Verified:

- check-patchfail --targets=android on 153.0esr: exit 0, build.gradle is
  now patched, no fuzz;
- full common+android replay on 153.4.0esr: line lands (build.gradle:554),
  fuzz 0, final sha256 33826457..., the value b567bb3 predicted;
- build.gradle-only replay of the 8 editors with suggest-data before each
  of the other 7 and last: 33826457... in all 8 orders, so the seven
  REVIEWED_ORDER_FREE rows still hold, now on bytes that land.

check-patch-order.py: notes updated; exit 0, 29/29 constraints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A line starting with '@@' that is not '@@ -N[,M] +N[,M] @@[ heading]'
is a hunk GNU patch 2.8 skips silently (exit 0, no .rej), so
check-patchfail - which trusts patch's exit status - reports the patch
clean while part of it never lands. That is how firefox-suggest-data's
build.gradle dependency went missing from every build.

The check is textual and runs over every patch in every list, whatever its
scope; a hit is reported with file:line and exits 1 like a scope violation.

  $ python3 scripts/lint-patch-scope.py
  lint-patch-scope: OK - 120 patch file(s), no scope violations, no hunk header without line numbers

Negative check (scratch copy with the pre-fix firefox-suggest-data.patch
and a '@@ ' line appended to bootstrap-esr.patch): both reported, exit 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
version.android 153.0esr -> 153.4.0esr (archive.mozilla.org's spelling;
153.4esr is a 404), release.android stays 1. `make help TARGETS=android`:
"in effect for this invocation  : 153.4.0esr-1". Carries MFSA 2026-77,
-85, -93 and -100 (ESR 153.1-153.4, 201 CVEs).

check-patchfail --targets=android against the signed 153.4.0esr tarball
first exited 1 on three patches; each is fixed:

- android/ubo-readiness (ext-webRequest.js, 3/3 hunks): upstream removed
  remoteTab/registerTraceableChannel from registerEvent and its
  registrar. Ported: isLiveListener is now the 6th parameter,
  convert(_fire) still marks the listener live, the registrar takes
  { fire, context } and passes !!context. test-ubo-readiness.js runs
  18/18 against the patched tree once its call drops the old remoteTab
  slot (follow-up; the unmodified test silently stops after 2).
- android/extension-update-controls (Preferences.cpp hunks 2 and 6):
  upstream bug 2053962 added a backup-only standalone PWRunnable and
  DispatchWriteComplete to fix the same coalescing bug our patch fixes
  for every write. Our PWRunnable (each request owns its snapshot and
  settles its own promise) replaces it; the change-line diff against
  the old patch is exactly upstream's new backup code being removed.
- android/fix-canvas-extraction-permission-esr: 153.4.0esr carries the
  fix (CaptureStream passes GetCurrentJSContext()). Deleted and dropped
  from android.txt because Gecko fixed it; PATCH-SCOPE.md now 15/51/53/0
  = 119.

Result: exit 0; 15 fuzzy hunks across 13 patches (14 across 12 on
153.0esr; the new one is session-cleanup's CookiePersistentStorage.cpp
include block, fuzz 2, left alone because its bytes are hash-pinned by
test-session-cleanup.py and evidence receipts); --fuzz=0 rejects 15
patches (14 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Appendix D records what §10 asks for, stage A only (no make dir, no
Gecko build): the four ESR MFSAs and the nine Firefox-for-Android CVEs
with no ESR counterpart (all open), the bump, the before/after
check-patchfail numbers and every hand fix, which §6 pref checks ran
(6a-6d: no pref of ours orphaned, drifted or newly mirror-once; 6e
needs a built APK) and the eight gate outputs, all green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mullvad is discontinuing its DNS over HTTPS service. The 153 ESR beta
listed "Mullvad (No Filtering)" in the Fenix DoH picker
(privacy-defaults.patch), so a profile may still hold
https://dns.mullvad.net/dns-query in pref_key_doh_provider_uri. When the
service stops, Increased Protection loses DoH and Max Protection loses
name resolution.

New patches/android/doh-mullvad-migration.patch:

- Core.engine calls DohProviderMigration.migrate before DefaultSettings
  reads dohProviderUrl. Once per profile, a stored provider on
  dns.mullvad.net, *.dns.mullvad.net or doh.mullvad.net becomes
  Settings.LIBREWOLF_DOH_PROVIDER_URI (dns10.quad9.net, also common.cfg's
  network.trr.uri). The mode is kept. The done flag sits in
  fenix_preferences next to the provider, so a Mullvad URI typed in
  later is left alone.
- If the mode was Increased or Max, HomeActivity.onResume shows a
  one-time dialog naming the new provider. It uses the same AlertDialog
  mechanism as update-check. Off/Default: no dialog, the change is
  logged.
- Mullvad is removed from the built-in catalog, which now matches
  common.cfg's doh-rollout.provider-list (settings 0cae8fb).
  DefaultDohSettingsProviderTest is updated to match.
  DohProviderMigrationTest adds 6 Robolectric cases.

check-patch-order: privacy-defaults -> doh-mullvad-migration is
declared; the reversed order rejects every DoH hunk. The seven
HomeActivity.kt/Core.kt partners are recorded order-free: swapped
full-tree replays give byte-identical files.

On 153.4.0esr the patch applies at fuzz 0, offset 0. check-patchfail
--targets=android passes, and the fuzz-0 run lists the same 15 rejects
as before. lint-patch-scope passes (120), check-patch-order passes
(30/30) and board --check-scope passes (15/53/52/0 = 120).

Not compiled: there is no Gecko AAR here, so stage B builds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mment

doh-mullvad-migration.patch: DohProviderMigration.migrate now writes the
replacement provider under pref_key_doh_provider_uri (the key
Settings.dohProviderUrl reads) in the same SharedPreferences edit as the done
and notice flags, so they really are committed together; the header said so
before but the provider went through a separate apply(). migrate takes the
Context to resolve that key. The notice no longer relies on a process-global
noticeShowing flag that only OnDismissListener reset: the "already showing"
check is a weak reference to the dialog compared with its owner activity, so
a HomeActivity recreated under the dialog shows it again, and the pending flag
is cleared only by the user (OK or cancel), not by a teardown dismissal. The
about:config user-branch network.trr.uri case is documented as a known
limitation. Three new Robolectric tests (one-edit atomicity, show once per
activity / cleared by the user, recreation). The two lw/ files compile with
kotlinc 2.3.20 -Werror against stubs of the Fenix types; the Fenix build and
the test run remain stage B.

test-ubo-readiness.js: drop the 153.0 remoteTab slot from registerEvent and
the remoteTab context from convert(), matching the rebased ubo-readiness patch,
and exit 1 unless all 18 tests finished. 18/18 on the patched 153.4.0esr tree;
a test made to hang now fails with "7 of 18 ... a test never settled", and the
stale null argument fails with "2 of 18".

settings: Redoubt-settings 3db3803 rewords the app.releaseNotesURL comment,
which cited support-destination.patch (not on this branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pass

The 153.4.0esr unit-test run was the first to build these module test
source sets. Each failed:

- firefox-suggest-data: PinnedSuggestIngestionTest used mockk, which
  android-components excludes from every subproject, so
  :components:feature-fxsuggest test compilation failed. It now uses
  Mockito spy/doReturn and an argument captor. The assertions are the same.
- extension-update-controls: AddonUpdaterWorkerTest used Mockito's
  ArgumentMatchers.eq on a Kotlin non-null String parameter. That eq
  returns null, which threw an NPE and leaked an unfinished verification
  into the next test. It now uses mozilla.components.support.test.eq.
- global-privacy-controls: GeckoGlobalPrivacyControllerTest had a
  redundant `!!`, a warning that fails the build under -Werror.

Targeted results: GeckoEngineTest 183/183, GeckoGlobalPrivacyControllerTest
5/5, AddonUpdaterWorkerTest 6/6, AddonManagerTest 30/30,
PinnedSuggestIngestionTest 3/3, FxSuggestAdmissionTest 8/8 and
OnlineSuggestionAdmissionTest 6/6. REBASE.md records these results, the
Fenix suite result and the remaining failures in the Android Components
modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Smoke suite, runtime pref audit, an E11 first-run capture with DNS
9.9.9.9 and a real Beta 2 -> 153.4 Mullvad DoH upgrade test, all run
against the stage B x86_64 APK (unsigned sha256 59666c2a...). Beta 2
(rc-unsigned, 503bfd79...) is rerun in the same environment as a control.
Both installed copies were signed with one throwaway key; the release key
was not used.

The Mullvad migration works: the dialog appears once, network.trr.uri
becomes dns10.quad9.net, mode 3 is kept, and a fresh install shows no
dialog. All five approved prefs read back at runtime. The pref audit
exits 0. expected-prefs.txt is not regenerated, because its one stale row
(librewolf.webgl.prompt) comes from LW-M7-14, not from this rebase.

The smoke suite is not green. One defect is a 153.4 regression:
about:config renders no rows, because the new baseline system CSP blocks
config.xhtml's inline onload. Three defects also reproduce on Beta 2:
uBO first-run readiness fails while AMO serves 1.75.0, a cold start to
the home screen times out the uBO readiness gate, and the webgl harness
expects EXPIRE_TIME where the patch writes EXPIRE_SESSION.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two Android Components test classes still asserted upstream behaviour
that our patches change on purpose. The first 153.4.0esr run of their
modules found them.

- firefox-suggest-policy: in FxSuggestSuggestionProviderTest,
  includeSponsoredSuggestionsOnlyWhenAmpUnavailable and
  includeNonSponsoredSuggestionsOnlyWhenWikipediaUnavailable expected a
  storage query with providers = []. The patched provider admits no
  provider and so never queries the store. Both now verify
  storage.query is never called.
- l10n-strings: AddonsManagerAdapterTest "bind blocklisted add-on"
  expected "violating Mozilla's policies". lw-brand rewrites that
  resource, so the test now reads it from
  R.string.mozac_feature_addons_status_blocklisted_1 and asserts that it
  no longer names Mozilla. This matches the existing Fenix test fixes.

The hunks were generated against the pristine 153.4.0esr files, which
no other patch touches. Run in the stage-B container:
:components:feature-fxsuggest 73/73 and :components:feature-addons
218/218 pass. Before the change, 2 and 1 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
board.py --check-fenix-tests warned on the full 153.4.0esr suite (615
classes / 5,562 tests):
- SearchSuggestionsProvidersBuilderTest ran all 70 and passed. Its
  entry is removed.
- AutofillSettingsMiddlewareTest failed 2 of its 7 tests, not 3. Its
  ceiling goes from 3 to 2.

The gate's semantics allow both changes. A listed class that passes or
fails below its ceiling is a warning meaning "trim". A class that is no
longer listed and still passes cannot fail the gate. Re-run against the
archived JUnit XML of that suite:

  # failing 22 = 19 environmental + 3 known-real + 0 unexpected
  ok: no failures beyond the documented allowlist      (exit 0, no warnings)

BUILD.md now states the current baseline: 19 environmental failures in
3 classes. The 2026-09-08 153.0esr figures are kept as history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
§6e told the rebaser to confirm that librewolf.webgl.prompt compiled to
false and treated true as the landmine. That has been wrong since
canvas-webgl-permissions.patch (LW-M7-14, shipped in Beta 2). That
patch removes the Android false override from webgl-prompt-default.patch
because it ships the GeckoView/Fenix bridge that answers the prompt. A
rebaser following the old text would "fix" a correct build.

§6e now gives the expected value per patch list: true with LW-M7-14
listed, false without it. It treats a mismatch either way as a failure,
because false with the bridge present leaves the bridge as dead code.

Appendix D records the stage-B header: RelaxedAtomicBool true for
.prompt and .hide in all three objdirs, which is the expected value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 153.4.0esr this patch applied with offsets in nsFrameLoader.cpp and
with fuzz 2 in the CookiePersistentStorage.cpp include block, where
upstream added #include "mozilla/AppShutdown.h". b458dbc left it fuzzy
because test-session-cleanup.py pinned its bytes. Those pins are now
being re-captured for 153.4.0esr, and an exact --fuzz=0 replay is what
the receipt checks.

- nsFrameLoader.cpp: the two moved hunk headers shift by the reported
  offsets (+6, +8).
- CookiePersistentStorage.cpp: the section is regenerated against the
  tree the patch meets on the Android stack (tarball plus every earlier
  common.txt/android.txt patch).

Every +/- line is byte-identical to before. On the stack the refreshed
patch applies with --fuzz=0 and no offsets. All 15 resulting files are
byte-identical to the stage-B tree, which was patched with the old,
fuzzy version and then built and run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test-extension-update-controls.py, test-global-privacy-controls.py and
test-session-cleanup.py pinned the 153.0esr source and 153.0esr patch
bytes. The rebase changed extension-update-controls.patch and
global-privacy-controls.patch, so all three failed on a digest check
before they reached any source check.

evidence/lw-m7-01/esr-153.4.0/receipts/recapture.py rebuilds each
patch's before tree from the signed 153.4.0esr tarball plus every
earlier common.txt/android.txt patch, in check-patchfail order. That is
the real tree state the patch meets, which the 153.0esr guest and
scoped-pristine captures stood in for. It then requires an exact
--fuzz=0 replay and compares the whole-stack result with the stage-B
build tree:

  extension-update-controls  19/27 before files  exact  27/27 = stage B
  global-privacy-controls     9/21 before files  exact  21/21 = stage B
  session-cleanup            13/15 before files  exact  15/15 = stage B

The present/absent counts and predecessor sets match the 153.0esr
receipts. Running it twice gives byte-identical output.

replay.py verifies a receipt from repository files alone. Each test now
uses it when evidence/lw-m7-01/esr-<version.android>/receipts exists and
otherwise falls back to its original 153.0esr chain, which is left
untouched. Every downstream source check still runs on the replayed
tree: Preferences.cpp markers, JS/JSON/TOML/XML, the 18 extension-update
and 18 global-privacy JS host tests, the XPIDL/WebIDL parse, and the
test inventories.

Not re-captured: there is no guest capture for 153.4.0esr, and the
pinned XPIDL/WebIDL parser sources are still 153.0esr's. The
task-local tools lw-m7-35/36/37 check-ordering.py and lw-m7-36
check-source.py remain 153.0esr-only and fail on the new patch bytes,
as they already did at d2d0840. README.md in the receipts directory
says what each file proves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cute

-n never enters the source tree. Every other existence check prints
"# skipped under --no-execute: ..." for that reason. The brand-image
step added in 19dbe98 did not. It looked up its 82 destinations in
whatever directory -n was run from, so a dry run exited 1 on
"LW-M4-07 expects 82 Firefox brand file(s) that are not in the tree".
With a tree in the cwd it would have copied for real.

Under --no-execute it now prints what it skipped and returns before the
check and the copy. A real run is unchanged: it checks every destination
first and writes nothing if any is missing (landmine L4).

scripts/tests/test-ubo-extension.py: 22/22 (was 21/22, the android
dry-run subtest exiting 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default --apk/--apksigner directory was hardcoded to
librewolf-android-apk-153.0esr-1-unsigned/apk. It is now the Makefile's
ANDROID_APK_OUTDIR for the current track,
librewolf-android-apk-<version.android>-<release.android>/apk, so a
rebase moves it too. An absent input is still a usage error, not a pass.

The android-apk.sh output does not contain apksigner.jar; the 153.0esr
signing handoff bundle did. So with no arguments, --apksigner usually
needs to be given. With the stage-B
153.4.0esr x86_64 APK and the apksigner.jar from the 153.0esr signing
handoff bundle: 29/29 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes the "Left open" items that this pass resolved:
- receipts re-captured for all three pinned tests;
- stage B done, L1 header true as §6e expects;
- Fenix allowlist trimmed;
- the two AC test classes now assert Redoubt's behaviour.

Adds the post-refresh fuzz row (14 hunks / 12 patches, 14 rejects at
--fuzz=0) and the gate results from re-running everything on the
153.4.0esr tree. scripts/tests (15 .py + 8 .js) all exit 0, and every
board.py gate, lint-patch-scope, check-patch-order and check-patchfail
--targets=android exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not 2

The patch writes EXPIRE_NEVER for Remember in normal browsing and
EXPIRE_SESSION otherwise, matching LW-M7-14 ("real engine session expiry",
"expire on normal process restart") and desktop webgl-permission-desktop.
nsIPermissionManager.idl has EXPIRE_SESSION=1; 2 is EXPIRE_TIME. The 2
came from the LW-M7-14 node mock, which defined EXPIRE_SESSION: 2, and was
copied into the smoke's wait_record and CanvasPermissionTest's private
assertion. Fix both expectations; the patch's runtime code is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…baseline CSP

153.4.0esr's Document::InitCSP appends the baseline system CSP
(script-src chrome: resource: moz-src:) to system-principal documents.
GeckoView's about:config (config.xhtml) still wires itself up with inline
on* attributes on ESR, so script-src-attr blocks all 13 of them -- including
body onload, so the page never initialises (runtime diag
new-153.4-aboutconfig-csp.json).

Mozilla already fixed this: config.xhtml/config.js in firefox-153.0.4 and
firefox-157.0 are byte-identical and move every handler into config.js via
addEventListener. The new hunks are exactly diff -u 153.4.0esr -> 157.0 of
those two files; the patched result matches upstream's md5s. The page is not
exempted from the CSP.

Applies at --fuzz=0 on pristine 153.4.0esr. Needs a Gecko rebuild (omni.ja).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ld starts

Two "uBlock Origin setup failed" defects, both in published Beta 2.

Launcher cold start: on APP_STARTUP Gecko delays persistent background pages
until the first GeckoView window sends extensions-late-startup. The session
middleware holds every tab until uBO is ready, and a launcher start (home
screen, no URL) opens no window, so the blocking listener can never register
and the 30 s timeout always fires. A VIEW intent worked only because
IntentReceiverActivity opens a speculative session. The preinstaller now
does the same before it waits: it opens a window, loads nothing, and the
first tab adopts it.

AMO update during first run: AddonUpdaterWorker runs as soon as uBO is
installed and replaces it with a newer signed AMO release. That shutdown
rejects the version-bound wait. After a failed wait the preinstaller re-reads
the registry for up to 5 s. Only a same-ID copy that is enabled and strictly
newer than the awaited version restarts the attempt, with a fresh timeout and
at most twice. It takes the existing-installation path, so there is no
reinstall. Absence, disablement, the same version, a downgrade or an
unparseable version keeps the failure. Gecko still enforces the signature, and
the readiness signal is still the exact version's live blocking listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AMO serves 1.75.0, so on first run every 1.74.0 install was immediately
auto-updated under the readiness wait. Checks on the new pin (AMO file
5034826, 4650100 bytes, sha256 5b744158...):

- fetch-ubo-extension.py validation;
- three independent downloads with the same digest;
- PKCS#7 chain to the 153.4 tree's AMO production root;
- JAR digests over every member;
- the same permissions and Android minimum as 1.74.0.

js/start.js and js/traffic.js are byte-identical to 1.74.0, so the
listener-after-filters property in ubo-readiness.patch still holds. Only that
patch's header comment changes; its diff and Gecko are untouched.

The LW-M7-34 update-fixture pair moves to 1.74.0 -> 1.75.0, because its
verifier binds the newer fixture to the packaged pin. The 1.75.0 original and
its receipts were fetched with fixtures.py --fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pped

Every device check opens a URL first, because Marionette needs a Gecko window,
and that VIEW intent's speculative session is exactly what released Gecko's
delayed extension startup. So the harness could never see the 30 s uBO
failure dialog that Beta 2 shows on every launcher cold start.

The new check resolves the launcher activity through the package manager. It
cold-starts it on a fresh profile and again on a restart, and waits past the
30 s timeout. It fails on the dialog, on a logged failure, or on a missing
positive readiness line, because silence is not readiness.

Device evidence for both uBO fixes is in
docs/android/evidence/lw-m3-07/esr-153.4-ubo-startup-20261002/:

- the pre-fix APK fails the check;
- the fixed APK passes it, plus first-navigation blocking with a signed 1.75.0;
- a variant pinned to 1.74.0 survives a real AMO update during first run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rofile

Rebuild from the committed tree: a fresh `make dir TARGETS=android` at
0a13444, all three ABIs from new objdirs with MOZ_BUILD_DATE pinned to
20261002044800, the fat AAR, and the R8 release APKs (unsigned). The
x86_64 APK (1ac3b01d..., throwaway-signed 77348e7d...) ran on a fresh
emulator with AMO reachable, no priming and no iptables.

The stage C product defects are fixed on device:
- about:config renders and keeps an edit across a restart
- launcher cold starts reach uBO readiness, with no dialog
- uBO 1.75.0 is pinned and AMO has nothing newer, so there is no update
  race

The pref audit exits 0 and the generator differs only in the
librewolf.webgl.prompt row. The Beta 2 -> rc2 Mullvad upgrade migrates
once to Quad9 with mode 3 kept, and no uBO dialog stacks on the DoH
dialog.

The smoke suite is not green. webgl hits a harness selector that clicks
by text where the Compose tab menu has only content-desc. search and
no-suggest are blocked by the first-run uBO sheet. no-suggest also
counts uBO's 37 s post-launch list updater as typing traffic. With the
selector worked around in an uncommitted diagnostic copy, the quiet
"Review" action in a private tab opened nothing in 3 of 3 runs, and the
cause is not established. The README records each of these.

Stage C README: cite run2/16 for the second-launch logcat (25-* is
empty), and note that its APK predates later patch-byte changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uggest

Harness defect A (graphics): Fenix's Compose tab-counter menu exposes
"New private tab" only as content-desc (PopupToMenuItemsMapper.kt clears
semantics), so tab_menu() clicking text="New private tab" never matched.
menu_item() now selects by text or content-desc, counts one node carrying
both once, and refuses two different matching nodes.

Harness defect B (check-search, check-no-suggest): on a fresh profile the
"uBlock Origin was added" sheet covers the toolbar. Both checks now go
through toolbar_ready(), which acknowledges the sheet with the graphics
harness's own ubo_added_notice() (loaded from scripts/, not copied) and
accepts the address bar only after two matching dumps with no sheet.
ubo_added_notice_present() is shared for the read-only close wait.

check-no-suggest typing window: the check restarts the app, and uBO's
asset updater runs autoUpdateDelayAfterLaunch (37 s) after every launch,
fetching one stale list per autoUpdateAssetFetchPeriod (5 s). Both values
are now read from the APK's bundled xpi, and typing starts only once the
launch timer must have fired and the capture has carried no non-background
outbound payload for max(20 s, 3 fetch periods). No host is exempted: the
pre-typing traffic is recorded as evidence and the typing window is judged
exactly as before. A capture that never goes quiet (300 s) is reported in
the failure detail rather than hidden.

Offline tests only; no emulator run in this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keyboard

The quiet "Review" action in a private tab opened nothing in 3 of 3 rc2
runs. It is not a product defect. A New (private) tab opened from the
tab-counter menu leaves the soft keyboard on screen after the URL is
submitted. The keyboard window stays visible and touchable over
[0,1145][1080,1920] in the input dispatcher, while InputMethodManager
reports mInputShown=false. uiautomator dumps only the app window, so the
harness saw the snackbar and tapped Review at 946,1805. The tap landed on
a key (VibratorService logged a TOUCH vibration from the IME's uid), so
Fenix never received it. A scratch build with logging confirmed that the
snackbar's action listener never ran. Review opened the dialog in private
tabs once the keyboard was out of the way, and so did the site-information
path.

- tap() reads `dumpsys input` and refuses to tap through any foreign
  window. A soft keyboard is closed first; anything else is a Failure.
- type_url() closes the keyboard after submitting. It allows 1.5 s for
  the normal hide, then force-stops the default IME (never the app) and
  records soft-keyboard-closed in evidence.
- The quiet snackbar is LENGTH_LONG and can expire between the dump and
  the tap: rc2 had one normal-tab miss, and the first diagnostic run here
  had another. A miss now falls back to the site controls only if the
  notice is gone and no other permission UI opened. run() still requires
  Review to open the list in a normal tab and in a private tab.
- After "Close tab" closes the last private tab, Fenix shows the empty
  private home, whose tab-counter menu has no "New private tab". The
  second private page is typed into that home's address bar, which is
  what a user would do there. The harness had never reached this step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yboard

The quiet "Review" action works in private tabs. The rc2 harness tapped
it through a soft keyboard that stayed over the bottom of the screen
after the private URL was typed. uiautomator cannot see that window.

- Diagnosis: a scratch build with logging only shows that the action
  listener never runs for that tap. dumpsys input shows the IME window
  visible and touchable while IMMS reports it hidden, and the IME's uid
  takes the touch.
- The keyboard behaviour does not depend on private mode or on
  LW-M7-14. A normal tab from the same menu, a plain page and Beta 2
  all show it. A URL typed into the home screen's address bar does not.
- With the keyboard closed, Review opens the dialog in private tabs:
  rc2 3/3 and Beta 2 1/1 in manual reproduction, and the harness runs
  below 6/6.
- With the committed harness (f7f519f) on the rc2 APK and a fresh
  profile per run, the graphics acceptance passes 3/3 (161 checks each).
  Every run records the harness hashes, HEAD and a clean tree.
- Gates on this tree pass: check-patchfail android, lint-patch-scope,
  check-patch-order, board.py --check and --check-scope.

There is no rc3 because the product is unchanged. The stuck keyboard
after the tab-counter menu's New (private) tab is recorded for separate
triage, starting with upstream Fenix 153.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…M7-14 mock

Reran every device check on the rc2 APK (77348e7d...af83, throwaway-signed),
one fresh profile per check, with the harness committed at 11fd562
(android-smoke.sh aabbb7a7..., android-graphics-smoke.py ce0072ea...).
There is no rc3, because the product did not change after rc2.

- 14 checks PASS. These include the full graphics acceptance (161 checks,
  private grants, frames, lifetimes), which passed twice, plus check-search,
  launcher-start, aboutconfig, the three uBO checks, self-test and strings.
- On about:config, the console service shows 0 CSP / script-src-attr
  errors, and a positive control proves the query sees them. Logcat does
  not carry console errors on this release build, as the probe shows.
- The negative control behaves as expected: Beta 2 fails
  --check-launcher-start.
- first-run-capture and check-no-remote-settings are red, as expected (E12).
- check-no-suggest FAILs. The red packets are 39/46-byte, keep-alive-sized
  records on connections opened before typing. No new connection, DNS
  query or QUIC packet was sent before Enter. The harness was not changed.

LW-M7-14 evidence: the Services.perms double now has EXPIRE_SESSION 1, the
real nsIPermissionManager value, and the literal asserts and fixture seeds
now match it. The .tap was regenerated against the rc2 tree and is still
37/37. No shipped byte changes.

The stale "pinned uBO 1.74.0" comment in ubo-readiness.patch is left
alone. It is packed into omni.ja, so fixing it needs a Gecko rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2's final acceptance failed check-no-suggest on 13 non-background
packets (504 B) that were all HTTP/2 keep-alive or close records on TLS
connections opened before typing (AMO, uBO list hosts; ~59 s ping
period). The rule counted every outbound payload byte in the window.

attribute_typing_flows() now reads the capture from its header to the
end of the window, in both directions, and gives every flow with an
outbound SYN, datagram or payload in the window a verdict. It is typing
traffic when any of these hold: a DNS query (53/853), a SYN inside the
window, a flow first seen inside the window, any non-background UDP
(QUIC included), an address resolved by a DNS answer inside the window,
an SNI that is a configured search/suggestion host, or an outbound
segment that is not ONE TLS application-data/alert record of at most
46 B. 46 B is one 17-byte HTTP/2 control frame (9-byte header + 8-byte
PING/GOAWAY payload) in the larger of the TLS envelopes: 1.3 adds 22 B
(39 B on the wire), 1.2 AES-GCM adds 29 B (46 B). rc2's records were
exactly 39/46 B pings and 24/31 B alerts. Background exemptions are the
existing SNI/DNS-name ones, unchanged. Every non-background payload
pcap_payloads sees must also sit on a passing flow.

The per-flow attribution is recorded in the check JSON
(typing_flow_attribution). Re-graded offline against rc2's own capture,
the 10 flows of that window are 7 keepalive + 3 background, and the
post-Enter window flags the noai.duckduckgo.com DNS query and
connections.

--no-suggest-negative-control turns "Show search suggestions" ON through
Settings before typing; the check must then FAIL, the detail names the
suggestion host, and the switch is restored OFF.

16 new offline tests on synthetic captures (81 total).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PASS detail still described the old byte rule ("no outbound
transport payload outside background flows"). It now states the
per-connection rule and counts the keep-alive and background flows it
let through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hics UI guards

Documents what c35662c, f7f519f and the connection attribution
changed and never wrote down: the uBO installed-notice handling shared
with check-search, the pre-typing quiet wait derived from the bundled
uBO's updater schedule, per-connection attribution with the derivation
of the 46 B keep-alive bound, the --no-suggest-negative-control flag,
and, for graphics acceptance, the input-dispatcher tap guard, the
default-IME force-close, and the Review-expiry fallback together with
the requirement that Review opened the list in both a normal and a
private tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CPlusPlus17 and others added 3 commits October 2, 2026 13:27
…trol fails on ac.duckduckgo.com

Device runs of --check-no-suggest on the rc2 APK (installed 77348e7d…af83,
throwaway key) with the committed harness dba0f279 (86a8fd0), on one
emulator, DNS 9.9.9.9, fresh profile each run, AMO reachable, no
priming or iptables:

- negative control (--no-suggest-negative-control, suggestions ON
  before typing): FAIL as required. 3 new TLS connections to
  ac.duckduckgo.com plus its udp/53 query, and 2 DoT lookups. In the
  same window the 7 pre-existing AMO/uBO flows were graded keepalive.
- three normal runs: PASS, each with 7 keepalive + 3 background flows
  and 0 typing flows. Under the old byte rule each would have failed on
  13 packets / 504 B, the same as rc2's row 8.
- row 8's own capture, re-graded offline with the new attribution:
  0 typing flows.

The final-acceptance README now says that row 8 had 16 payload packets,
3 of them background and 13 failing (it had said 13 in one place and 16
in another). It adds rows 8a-8d with the new harness hash, and the unit
test results (test-android-smoke.py: 81 OK).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner signed rc2's four APKs on the Fedora host under a dated,
candidate-specific exception. Each signed APK is byte-identical,
entry for entry, to its tested unsigned input, and all four pass the
published-fingerprint v2+v3 check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every asset re-downloaded from the public prerelease matches
SHA256SUMS.signed and passes the published-fingerprint v2+v3 check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CPlusPlus17
CPlusPlus17 changed the base branch from desktop/firefox-157 to main October 2, 2026 13:50
@CPlusPlus17
CPlusPlus17 merged commit b70020d into main Oct 2, 2026
0 of 2 checks passed
@CPlusPlus17
CPlusPlus17 deleted the android/esr-153.4 branch October 14, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant