Skip to content

Service tester updates#24

Open
icepaq wants to merge 10 commits into
mainfrom
service-tester-updates
Open

Service tester updates#24
icepaq wants to merge 10 commits into
mainfrom
service-tester-updates

Conversation

@icepaq

@icepaq icepaq commented May 13, 2026

Copy link
Copy Markdown

Related Issue

Closes #

Description

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Other

Testing

Fingerprint Report

Please submit a report from both the service tester and build tester.

Fingerprint report

Checklist

  • I have linked a related issue above
  • My changes are focused on a single logical change
  • I have added testing instructions which include the desired result
  • Service tests pass (for python library changes) -./service-tester/run_tests.sh --browser-version official/prerelease/146.0.1-alpha.25 (attach screenshot) temporarily out of service lol
  • Build test passes (for patch changes) - ./build-tester/run_tests.sh A score of at least 1000 must be achieved (attach screenshot)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for real fingerprint presets for Firefox v149+ binaries via fingerprint_preset=True configuration
    • Introduced per-context audio fingerprint seeding and transformation via new setAudioFingerprintSeed() API
  • Bug Fixes

    • Fixed Firefox chrome window initialization timing issues for tab creation
    • Improved screen dimension spoofing accuracy and timezone override handling
    • Enhanced WebGL parameter masking and extension filtering
  • Documentation

    • Updated guides for real fingerprint preset usage and per-context configuration
  • Chores

    • Updated Firefox base version to 150.0.2
    • Improved test infrastructure and build configuration

Review Change Stack

icepaq and others added 10 commits May 10, 2026 01:45
improved build tester scoring
- scripts/_mixin.py: switch moz_target from x86_64-pc-mingw32 (no longer
  supported in FF150) to x86_64-pc-windows-msvc
- additions/juggler/screencast/HeadlessWindowCapturer.h: typedef pid_t
  on XP_WIN; libwebrtc headers (video_capture.h, desktop_capturer.h)
  reference pid_t which is POSIX-only
- patches/anti-font-fingerprinting.patch: include mozilla/dom/Document.h
  in gfxTextRun.cpp; on Windows it is not transitively included so
  doc->GetInnerWindow() failed with "incomplete type 'Document'"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@icepaq icepaq changed the base branch from main to 150 May 13, 2026 22:29
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR upgrades the Camoufox Firefox fork from version 146.0.1 to 150.0.2 while introducing six per-user-context fingerprinting managers, real preset support by Firefox version, a complete Juggler target registry, and refactored test infrastructure using local executables.

Changes

Firefox 150 upgrade and build infrastructure

Layer / File(s) Summary
Version update and SDK compatibility
upstream.sh, patches/macos-sdk-26.1-allow.patch, scripts/_mixin.py
Bump Firefox version from 146.0.1 to 150.0.2, lower macOS SDK requirement from 26.2 to 26.1, and change Windows build target from MinGW triple to MSVC triple for better compatibility.
Juggler Firefox compatibility improvements
additions/juggler/TargetRegistry.js, additions/juggler/screencast/HeadlessWindowCapturer.h, additions/juggler/screencast/HeadlessWindowCapturer.cpp, additions/juggler/screencast/ScreencastEncoder.h, additions/juggler/screencast/nsScreencastService.cpp
Add polling loop in window initialization to wait for domWindow.gBrowser when unavailable on startup, adjust download warning messaging, introduce rtc namespace alias for libwebrtc compatibility, and switch screencast widget acquisition from ViewManager to direct GetRootWidget() call.

Per-user-context spoofing and fingerprinting framework

Layer / File(s) Summary
Font spacing seed and Roverfox storage managers
patches/anti-font-fingerprinting.patch
Introduce FontSpacingSeedManager for per-context font-spacing seed persistence with disable tracking, RoverfoxStorageManager as cross-process key-value store backed by Firefox Preferences, and WebIDL integration for Window.setFontSpacingSeed() that self-destructs after first use. Thread userContextId through text shaping to deterministically perturb glyph advances based on resolved seed.
Audio and screen dimension managers
patches/audio-fingerprint-manager.patch, patches/screen-spoofing.patch
Add AudioFingerprintManager for per-context audio fingerprint seed transformation in Web Audio buffers and AnalyserNode output, and ScreenDimensionManager for per-context screen width/height/color-depth spoofing. Both persist to RoverfoxStorageManager and expose WebIDL setters that disable after first invocation.
Timezone and WebGL parameter managers
patches/timezone-spoofing.patch, patches/webgl-spoofing.patch
Implement TimezoneManager for per-realm timezone overrides applied to JS engine and worker globals via new JS::SetRealmTimeZoneOverride API, and WebGLParamsManager for per-context WebGL vendor/renderer strings. Integrate both into ClientWebGLContext to spoof capabilities, parameters, and precision formats via MaskConfig and stored per-context values.

Juggler target registry and page/browser control

Layer / File(s) Summary
TargetRegistry and PageTarget implementation
additions/juggler/TargetRegistry.js.bak
Add 1300+ line TargetRegistry module implementing browser target lifecycle (creation/destruction), page target management per tab, per-context BrowserContext for permission and proxy management, download interception with UUID mapping, video/screencast recording start/stop/ack flows, and per-browsing-context override plumbing (UA/platform/DPPX/viewport/zoom/media/color-scheme/reduced-motion/contrast/forced-colors/offline/cache-disabled). Serialize initial page creation to work around Playwright timing issue.

Real fingerprint presets and test infrastructure refactoring

Layer / File(s) Summary
Preset selection by Firefox version
README.md, docs/per-context-patches.md, pythonlib/camoufox/fingerprints.py, pythonlib/camoufox/utils.py
Add v150 preset bundle for Firefox 149–152, implement version-aware preset selection via _select_presets_file() cutoff at Firefox v149, and update documentation to recommend opt-in real fingerprints via fingerprint_preset=True. Pass ff_version through load_presets() and get_random_preset() for version-specific bundle loading.
Service tester executable-path refactoring
service-tester/README.md, service-tester/run_tests.py, service-tester/run_tests.sh, service-tester/_proxies.py
Refactor test runner from browser-version-based remote selection to local executable path, add wheel building and OS-specific binary auto-detection, copy properties.json on macOS, implement proxy bypass for localhost, and update both Python and shell scripts to require/pass --executable-path.

Configuration, branding, and masking integration

Layer / File(s) Summary
Branding and distribution configuration
browser/app/Makefile.in, browser/app/firefox.exe.manifest, patches/config.patch, patches/windows-theming-bug-modified.patch, patches/librewolf/mozilla_dirs.patch
Rename Windows manifest from Firefox to Camoufox, add distribution assets to installer (defaults/pref/local-settings.js, distribution/policies.json, camoufox.cfg), introduce LibreWolf-specific native manifest directory properties (XRE_MOZ_SYS_NATIVE_MANIFESTS, XRE_MOZ_USER_NATIVE_MANIFESTS), and update filesystem paths from .mozilla to .librewolf and Mozilla to LibreWolf across system/user extension and profile locations.
UI defaults and LibreWolf customization
patches/librewolf/ui-patches/hide-default-browser.patch, patches/locale-spoofing.patch, intl/components/moz.build, intl/components/src/Locale.cpp, intl/components/src/Locale.h, intl/locale/OSPreferences.cpp
Remove Firefox default-browser polling and UI (disable "set as my default browser" features, remove alwaysCheckDefault preference, unregister default-browser settings groups), and implement locale spoofing via ChromeUtils.camouGetString() to set intl.accept_languages from locale:all or fallback locale:language+locale:region during browser startup. Override OS locale enumeration to prefer Camoufox-provided locale values.
MaskConfig spoofing across fonts, geolocation, and graphics
patches/font-hijacker.patch, patches/system-ui-font-spoofing.patch, patches/geolocation-spoofing.patch, build-tester/scripts/grading.py
Integrate MaskConfig font whitelisting into gfxPlatformFontList and FontFace status/load logic, spoof system-ui font by platform ("MacIntel" → Helvetica, "Win32" → Segoe UI), override geolocation coordinates/accuracy when configured before network requests, and update CSS media feature device-width/height queries to check per-context ScreenDimensionManager values. Also update test grading from fail-count to pass-percentage thresholds.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • CloverLabsAI/camoufox#11: Introduces per-userContextId font spacing seed storage and WebIDL setFontSpacingSeed() self-destruct, extended in this PR to integrate with full text-shaping pipeline threading.
  • CloverLabsAI/camoufox#13: Implements per-context fingerprint spoofing initialization and preset routing, which this PR enhances with version-aware real preset selection (v150 bundle for Firefox 149+).

Poem

🐰 From Firefox 146 to shiny 150,
Six managers bloom, per-context and true—
Fonts dance with seeds, screens spoof their view,
Audio masks, timezone tricks, WebGL's debut! ✨
Real presets shine when the version is right,
Juggler orchestrates pages with all of its might.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch service-tester-updates

@icepaq icepaq changed the base branch from 150 to main May 13, 2026 22:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (17)
patches/locale-spoofing.patch (2)

55-70: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't return c_str() from temporary MaskConfig strings.

lang.value(), region.value(), and script.value() are destroyed at the end of each if, so these helpers return dangling pointers. Language(), Script(), and Region() then immediately read invalid memory via strlen(...) / Set(...). Please return an owning string type here or cache the MaskConfig values in storage with a stable lifetime before exposing raw pointers.

Also applies to: 122-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/locale-spoofing.patch` around lines 55 - 70,
Locale::GetCamouLanguage, GetCamouRegion, and GetCamouScript currently return
pointers to temporaries (lang.value().c_str()) which dangle; change them to
return an owning type or provide a stable-cached storage: either return
std::string (or std::optional<std::string>) instead of const char*, or store the
MaskConfig values in persistent members (e.g., Locale::camouLanguage,
camouRegion, camouScript) that are populated from MaskConfig::GetString and
return pointers to those members; update callers Language(), Script(), and
Region() to accept the new return type or use the cached members so strlen/Set
read valid memory.

9-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

locale:all never reaches the C++ locale path.

Startup writes intl.accept_languages from locale:all, but Locale::GetCamouLocale() only consults locale:language, locale:region, and navigator.language. If the caller provides only locale:all, Locale::GetDefaultLocale() and OSPreferences still fall back to the real system locale, which reintroduces a locale fingerprint mismatch. Please normalize locale:all in GetCamouLocale() too, or centralize the locale selection logic so both paths consume the same value.

Also applies to: 75-93, 172-197

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/locale-spoofing.patch` around lines 9 - 23, The C++ path
(Locale::GetCamouLocale / Locale::GetDefaultLocale / OSPreferences) doesn't
consult the JS-only "locale:all" value, so normalize or centralize the
selection: update Locale::GetCamouLocale to check for the "locale:all"
camouflaged string (and parse its comma-separated list into the same canonical
"language[-region], language" form the JS code produces) or refactor so both JS
(ChromeUtils.camouGetString usage) and C++ share a single helper that resolves
"locale:all", "locale:language", "locale:region", and "navigator.language";
ensure the resulting string is what
Services.prefs.setCharPref("intl.accept_languages", ...) would set so both
startup and OSPreferences consume the identical locale value.
patches/geolocation-spoofing.patch (1)

17-22: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the geolocation permission gate even for spoofed coordinates.

This short-circuits to Allow before CheckPromptPrefs(), so any site gets geolocation access whenever spoofed lat/lon are configured, even if the site would normally prompt or has already been denied. The spoof should replace the coordinates, not bypass the permission decision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/geolocation-spoofing.patch` around lines 17 - 22, The added block
short-circuits permission handling by calling
request->RequestDelayedTask(nsGeolocationRequest::DelayedTaskType::Allow) and
returning true when MaskConfig::GetDouble("geolocation:latitude") &&
MaskConfig::GetDouble("geolocation:longitude") are present; instead, remove the
early Allow/return and only record the spoofed latitude/longitude (read via
MaskConfig::GetDouble) on the request or a small request-local field so the
normal permission path (CheckPromptPrefs(), existing prompt/deny logic) still
runs; after the permission is granted by the existing flow, inject or replace
the coordinates on the response using the stored spoof values (refer to
nsGeolocationRequest and CheckPromptPrefs() to locate where to apply the spoofed
coords).
patches/font-hijacker.patch (1)

50-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't replace the font loading state machine with the allowlist result.

FontFace::Status() now reports every allowed face as Loaded, FontFace::Load() no longer calls mImpl->Load(aRv), and FontFaceImpl::SetStatus() ignores the actual loader status. That breaks @font-face behavior: allowed fonts can resolve before any resource fetch completes, and loading/failed states are no longer observable. Keep the existing status transitions intact and apply the allowlist at the family-resolution/gating layer instead.

Also applies to: 88-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/font-hijacker.patch` around lines 50 - 80, Revert the logic that
substitutes the loader state with the allowlist: restore FontFace::Status() to
return mImpl->Status() and make FontFace::Load() call mImpl->Load(aRv) (keeping
mImpl->UpdateOwnerKeepAlive()), rather than resolving/rejecting immediately
based on GetFamily()/mozilla::dom::IsFontAllowed(). Instead, implement the
allowlist check at the family-resolution/gating layer (where the
network/resource fetch or loader completion is observed) so you only gate final
promise resolution/rejection there—leave FontFaceImpl::SetStatus() to drive the
normal Loaded/Loading/Error transitions and only consult IsFontAllowed when
deciding whether to allow resolution after the loader reports success or to
convert a successful load into a blocked/failure outcome.
patches/librewolf/mozilla_dirs.patch (1)

5-10: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the new XREMoz*NativeManifests lookups on unsupported platforms.

NativeManifests.sys.mjs now resolves these two directory-service properties unconditionally, but the provider code in this patch only handles them inside the XP_UNIX || XP_MACOSX branch. On Windows, Services.dirsvc.get(...) will throw before the native-manifest fallback can run. Please skip these entries on unsupported platforms or handle missing properties defensively.

Also applies to: 50-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/librewolf/mozilla_dirs.patch` around lines 5 - 10, The new
unconditional calls to Services.dirsvc.get for XREMozUserNativeManifests and
XREMozSysNativeManifests in NativeManifests (NativeManifests.sys.mjs) cause
throws on unsupported platforms; update the code that builds dirs (where
Services.dirsvc.get("XREMozUserNativeManifests", ...) and
Services.dirsvc.get("XREMozSysNativeManifests", ... ) are invoked) to only
attempt those lookups when the platform check matches (the same XP_UNIX ||
XP_MACOSX branch used by the provider) or wrap each lookup in a defensive
try/catch that skips missing properties, so Windows won't call
Services.dirsvc.get for those keys and the native-manifest fallback can run.
additions/juggler/TargetRegistry.js.bak (1)

1-1307: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Delete the TargetRegistry.js.bak file entirely.

This file is dead code. Firefox loads only chrome://juggler/content/TargetRegistry.js (the non-.bak version); nothing in the codebase imports or uses the .bak variant. Worse, the .bak file is an older intermediate checkpoint missing critical fixes: it contains seven [TR-DEBUG] debug console.error statements (lines 323, 357, 359, 361, 366, 370, 373) that were cleaned from the production version, and it lacks a 14-line Firefox 150+ workaround for gBrowser population that exists in the main file. Shipping both creates silent drift and maintenance confusion. Remove it in the same commit that adds or modifies the actual TargetRegistry.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@additions/juggler/TargetRegistry.js.bak` around lines 1 - 1307, Remove the
dead backup file TargetRegistry.js.bak entirely; it contains stale code
(including console.error “[TR-DEBUG]” statements in _newPageInternal) and
diverges from the canonical TargetRegistry implementation (see TargetRegistry
class, PageTarget, and waitForWindowReady), so delete this file and include that
deletion in the same commit that adds/modifies the real TargetRegistry.js to
avoid drift and confusion.
patches/timezone-spoofing.patch (3)

27-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GetTimezone has a write side effect — surprising for a getter.

When the per-context key is missing, the MaskConfig fallback branch writes the value back into RoverfoxStorageManager before returning. That:

  • promotes a read into a cross-process IPC write (SendRoverfoxStoragePut on the parent path), which is expensive on a hot path;
  • changes observable persistent state from what looks like a pure query;
  • can race with concurrent SetTimezone calls on other threads/contexts.

Either rename to make the mutation explicit, or just return the MaskConfig value without persisting it (let the next SetTimezone call own the write).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/timezone-spoofing.patch` around lines 27 - 44,
TimezoneManager::GetTimezone currently writes to RoverfoxStorageManager when
falling back to MaskConfig, which is a surprising mutation for a getter; change
the fallback branch in TimezoneManager::GetTimezone to avoid the write side
effect by removing the RoverfoxStorageManager::PutString call and simply convert
MaskConfig::GetString("timezone") into outTimezone (NS_ConvertASCIItoUTF16) and
return true, leaving persistence to SetTimezone (or alternatively rename the
method if you intentionally want it to persist, but do not perform the PutString
inside this getter).

148-188: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Per-context timezone setter has two cross-context isolation breaks.

Two distinct problems in SetTimezone:

  1. SetTimezone(0, timezone) poisons context 0. TimezoneManager::SetTimezone always calls DisableFunction(userContextId) (line 24). So when a window in context N ≠ 0 calls setTimezone, you also write timezone_0 and set tz_disabled_0 = true, which permanently hides setTimezone from every context-0 window even though context 0 never called it. Combined with GetTimezone not falling back from timezone_<N> to timezone_0, the fallback write to 0 has no read-side benefit and only causes the lockout.

  2. JS::SetTimeZoneOverride(timezoneASCII.get()) is process-wide. This mutates a global SpiderMonkey state shared by every realm in the process. The ai-summary describes this as “ultimate fallback” for shared/service workers, but per the patch's design every worker realm already gets a per-realm override via GetOrCreateGlobalScope (WorkerPrivate.cpp changes) and getDateTimeInfo() reads behaviors_.timeZoneOverride(). The last context to call setTimezone therefore wins globally, breaking the per-context promise the rest of the patch is built on.

Recommend dropping both the SetTimezone(0, ...) block and the JS::SetTimeZoneOverride(...) call, and instead extending the fallback read path in TimezoneManager::GetTimezone (or in worker startup) to read per-realm storage only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/timezone-spoofing.patch` around lines 148 - 188, The SetTimezone
implementation introduces two cross-context breaks: remove the fallback write to
context 0 and the process-wide SpiderMonkey override; specifically, delete the
branch that calls TimezoneManager::SetTimezone(0, timezone) (the block guarded
by if (userContextId != 0)) and remove the
JS::SetTimeZoneOverride(timezoneASCII.get()) call, and instead update
TimezoneManager::GetTimezone (or the worker startup read path used by
WorkerPrivate/getDateTimeInfo) to consult per-realm storage and perform the
intended fallback there so shared/service workers can read an appropriate
per-realm value without poisoning userContextId 0 or mutating global
SpiderMonkey state.

368-383: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cache DateTimeInfo and invalidate only on timezone override changes.

getDateTimeInfo() is called for every Date construction, locale-sensitive operation, and Intl.DateTimeFormat operation. The current implementation unconditionally allocates a new DateTimeInfo with js::MakeUnique<js::DateTimeInfo>(tzOverride->chars()) on every call. Instead, cache the constructed DateTimeInfo and only rebuild it when behaviors_.timeZoneOverride() actually changes. Timezone overrides are set infrequently (via setTimeZoneOverride, document navigation, or worker setup)—the "always create fresh" approach incurs unnecessary heap allocations on a genuinely hot code path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/timezone-spoofing.patch` around lines 368 - 383,
Realm::getDateTimeInfo currently recreates a js::DateTimeInfo on every call;
change it to reuse the cached dateTimeInfo_ and only rebuild when
behaviors_.timeZoneOverride() actually changes. In getDateTimeInfo(), fetch
RefPtr<JS::TimeZoneString> tzOverride = behaviors_.timeZoneOverride(), compare
tzOverride->chars() (or the RefPtr identity) against the last-used value (store
the last timezone string or the RefPtr in the Realm instance), and only call
js::MakeUnique<js::DateTimeInfo>(...) to replace dateTimeInfo_ when the timezone
differs; otherwise return the existing dateTimeInfo_.get(); ensure this logic is
inside the JS_HAS_INTL_API block and preserves existing behavior when tzOverride
is null (return nullptr).
patches/anti-font-fingerprinting.patch (3)

1324-1342: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Case-transformed text runs always get userContextId = 0.

nsCaseTransformTextRunFactory::RebuildTextRun hardcodes privateBrowsingId = 0, so any text passing through CSS text-transform: uppercase/lowercase/capitalize (and inner factories that wrap it) will bypass per-context font spacing perturbation entirely, leaving a fingerprintable hole that distinguishes transformed vs. untransformed runs.

Consider plumbing aUserContextId through nsTransformingTextRunFactory::MakeTextRun (or via the parent text-run/gfxFontGroup::mUserContextId) so the transformed children inherit the right ID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/anti-font-fingerprinting.patch` around lines 1324 - 1342, The code
hardcodes privateBrowsingId = 0 in nsCaseTransformTextRunFactory::RebuildTextRun
causing transformed text runs to lose the real user/context id; modify the
plumbing so the proper userContextId is propagated — either add a parameter
aUserContextId to nsTransformingTextRunFactory::MakeTextRun (and update callers
like mInnerTransformingTextRunFactory->MakeTextRun) or read and forward
gfxFontGroup::mUserContextId when constructing child runs; ensure
nsCaseTransformTextRunFactory::RebuildTextRun passes that real id into the
fontGroup->MakeTextRun/inner MakeTextRun calls so transformed children inherit
the correct context instead of 0.

97-98: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing newline at end of FontSpacingSeedManager.cpp.

The patch ends without a trailing newline (\ No newline at end of file). Add one to satisfy POSIX text-file conventions and avoid noisy diffs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/anti-font-fingerprinting.patch` around lines 97 - 98, The file ends
with the line "} // namespace mozilla" but lacks a trailing newline; open
FontSpacingSeedManager.cpp, add a single POSIX newline character at EOF so the
file ends with a blank line after the closing " } // namespace mozilla" to
satisfy text-file conventions and remove the "\ No newline at end of file"
marker.

864-896: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Letter-spacing perturbation compounds glyph drift (double-offset).

In gfxHarfBuzzShaper::ShapeText, every glyph receives both:

  • x_advance += spacing (moves the pen by an extra spacing per glyph), and
  • x_offset += cumulativeOffset (renders glyph i shifted by i * spacing).

Combined with the advance accumulation, the visual position of glyph i ends up offset by roughly 2 * i * spacing from the unspoofed layout, while clusters/pen positions only advance by i * spacing. This makes glyphs drift further than the pen tracks, which can cause overlap with surrounding runs, hit-testing/selection mismatches, and stronger-than-intended visual perturbation. The same applies in the vertical branch.

If the intent is normal letter-spacing perturbation, drop the *_offset mutation and only adjust *_advance. If the cumulative offset is deliberate, please document the rationale and verify downstream selection/measurement code tolerates the resulting advance/offset mismatch.

🐛 Proposed fix: spacing via advance only
     hb_position_t spacing = FloatToFixed(randomFloat);
 
     uint32_t glyphCount;
     hb_glyph_position_t* glyphPositions =
         hb_buffer_get_glyph_positions(mBuffer, &glyphCount);
 
-    hb_position_t cumulativeOffset = 0;
-
     // Apply custom letter spacing
     for (uint32_t i = 0; i < glyphCount; ++i) {
       if (aVertical) {
         glyphPositions[i].y_advance -= spacing;
-        glyphPositions[i].y_offset -= cumulativeOffset;
       } else {
         glyphPositions[i].x_advance += spacing;
-        glyphPositions[i].x_offset += cumulativeOffset;
       }
-      cumulativeOffset += spacing;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/anti-font-fingerprinting.patch` around lines 864 - 896, The
letter-spacing perturbation doubles per-glyph displacement because ShapeText in
gfxHarfBuzzShaper modifies both advances and offsets
(glyphPositions[i].x_advance/x_offset and y_advance/y_offset) and also
accumulates cumulativeOffset; to fix, remove the offset mutations and the
cumulativeOffset accumulation and only adjust x_advance (or y_advance when
aVertical) by spacing (i.e., drop changes to x_offset and y_offset and the
cumulativeOffset logic) so the perturbation is applied via advances alone;
update the code around glyphPositions, spacing, and the aVertical branch in
gfxHarfBuzzShaper::ShapeText accordingly.
patches/audio-fingerprint-manager.patch (2)

246-345: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

AnalyserNode transforms mOutputBuffer in place — repeated reads compound the distortion.

In GetFloatFrequencyData and GetByteFrequencyData, AudioFingerprintManager::ApplyTransformation(mOutputBuffer.Elements(), …) mutates the cached buffer directly. mOutputBuffer is reused across calls (with smoothing applied against the previous frame in standard AnalyserNode behavior), so:

  • Two reads on the same frame multiply samples by the LCG-derived multiplier twice, producing drift proportional to call count.
  • Smoothing feeds transformed values back into the next FFT result, so even single reads per frame accumulate distortion over time.

Apply the transformation on a copy destined for the JS array instead (the ProcessData callback already has the output Span — transform there), so mOutputBuffer remains the canonical untouched FFT state. GetByteTimeDomainData already does this correctly via tmpBuffer; the float-frequency / byte-frequency paths should follow the same pattern.

🐛 Sketch of fix for the float-frequency path
-  // Apply audio fingerprint transformation to frequency data
-  {
-    ...
-    uint32_t seed = AudioFingerprintManager::GetSeed(userContextId);
-    if (seed != 0) {
-      AudioFingerprintManager::ApplyTransformation(mOutputBuffer.Elements(), mOutputBuffer.Length(), seed);
-    }
-  }
   aArray.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) {
     size_t length = std::min(size_t(aData.Length()), mOutputBuffer.Length());
+    // ... existing copy into aData ...
+    uint32_t userContextId = /* derive as before */;
+    uint32_t seed = AudioFingerprintManager::GetSeed(userContextId);
+    if (seed != 0) {
+      AudioFingerprintManager::ApplyTransformation(aData.Elements(), length, seed);
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/audio-fingerprint-manager.patch` around lines 246 - 345,
GetFloatFrequencyData and GetByteFrequencyData currently call
AudioFingerprintManager::ApplyTransformation on mOutputBuffer (mutating the
cached FFT state), which causes compounded distortion across reads; instead,
remove the in-place transformation and apply
AudioFingerprintManager::ApplyTransformation to the per-call output span or a
local copy inside the aArray.ProcessData callback so mOutputBuffer remains
untouched. Concretely: in GetFloatFrequencyData and GetByteFrequencyData delete
the pre-ProcessData block that mutates mOutputBuffer, and inside the ProcessData
lambda (where you already compute length and have the destination Span), compute
the seed via AudioFingerprintManager::GetSeed(...) the same way and call
AudioFingerprintManager::ApplyTransformation on the callback span (or on a local
copy created from mOutputBuffer) before copying into aData; keep
GetByteTimeDomainData behavior as-is for reference.

191-211: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Cross-context disable: writing to userContextId=0 locks context 0 out of its own setter.

AudioFingerprintManager::SetSeed unconditionally calls DisableFunction(aUserContextId) (see lines 33–39 of AudioFingerprintManager.cpp). In this method you then call SetSeed(0, seed) whenever userContextId != 0, which:

  1. Overwrites context 0's audioFingerprintSeed_0 with whatever value context N supplied (information leak across user contexts).
  2. Sets audio_disabled_0 = true, so setAudioFingerprintSeed is now hidden on context 0 windows even though context 0 never called it.

Additionally, GetSeed doesn't fall back from audioFingerprintSeed_<N> to audioFingerprintSeed_0, so the cross-write doesn't even buy you a fallback — it only causes the disable side-effect.

🐛 Proposed fix
   AudioFingerprintManager::SetSeed(userContextId, seed);
-  if (userContextId != 0) {
-    AudioFingerprintManager::SetSeed(0, seed);
-  }
-  AudioFingerprintManager::DisableFunction(userContextId);
+  // SetSeed already disables for `userContextId`; no need to touch context 0.

If a cross-context fallback is actually desired, add a write path that stores the value without calling DisableFunction, and have GetSeed look up audioFingerprintSeed_0 as a fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/audio-fingerprint-manager.patch` around lines 191 - 211,
nsGlobalWindowInner::SetAudioFingerprintSeed currently calls
AudioFingerprintManager::SetSeed(0, seed) which triggers DisableFunction(0) and
overwrites context 0 state (audioFingerprintSeed_0 and audio_disabled_0),
causing context 0 to be locked out; instead, stop calling SetSeed(0, seed) from
nsGlobalWindowInner when userContextId != 0 and either (a) add a new
AudioFingerprintManager API (e.g. SetSeedFallback or SetSeedNoDisable) that
writes audioFingerprintSeed_<id> without calling DisableFunction, or (b) modify
AudioFingerprintManager::SetSeed to accept a flag to avoid DisableFunction for
cross-context writes; also update AudioFingerprintManager::GetSeed to fallback
to audioFingerprintSeed_0 when the requested context seed is absent so
cross-context fallback works correctly.
patches/screen-spoofing.patch (2)

78-107: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Shared screen_disabled_%u flag silently disables the other setter.

DisabledKeyForUserContext returns one key (screen_disabled_<ucid>), and the WebIDL Func guard IsFunctionEnabledForWebIDL is bound to both setScreenDimensions and setScreenColorDepth in Window.webidl (lines 338–347). Because nsGlobalWindowInner::SetScreenDimensions and SetScreenColorDepth both call ScreenDimensionManager::DisableFunction(userContextId), calling either method on a fresh context immediately hides both methods on subsequent window instances in that context.

A page that needs to set width/height and color depth can never invoke the second call from a new window. Use a per-setter key.

🐛 Proposed fix
-  static void DisableFunction(uint32_t userContextId);
-  static bool IsFunctionEnabledForWebIDL(JSContext* aCx, JSObject* aObj);
+  static void DisableDimensions(uint32_t userContextId);
+  static void DisableColorDepth(uint32_t userContextId);
+  static bool IsDimensionsEnabledForWebIDL(JSContext* aCx, JSObject* aObj);
+  static bool IsColorDepthEnabledForWebIDL(JSContext* aCx, JSObject* aObj);

Then Window.webidl should reference the matching Func= for each method, and the setters should call their dedicated disable helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/screen-spoofing.patch` around lines 78 - 107, The shared key
"screen_disabled_%u" causes calling one setter to disable the other; change the
code to use a distinct per-setter key and helpers: replace
DisabledKeyForUserContext with two specific key generators (e.g.,
DisabledKeyForDimensions(uint32_t) and DisabledKeyForColorDepth(uint32_t)) or
add a suffix parameter, implement matching disable helpers (e.g.,
DisableDimensions/DisableColorDepth) instead of the single DisableFunction, and
update IsFunctionEnabledForWebIDL to check the correct per-setter key; also
update Window.webidl to reference the correct Func= guard for each of
SetScreenDimensions and SetScreenColorDepth so each setter only disables its own
flag.

307-324: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hardcoded availTop = 25 magic number.

GetAvailRect uses 25 as the default top inset and reduces available height by it. This silently bakes a platform assumption (macOS-style menu bar height) into every spoofed value. At minimum, name the constant; ideally, default to 0 and rely on the existing screen.availTop MaskConfig, or derive it from the real platform chrome height.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/screen-spoofing.patch` around lines 307 - 324, In
nsScreen::GetAvailRect, remove the hardcoded magic number 25 for availTop and
instead use a named default or zero: call
MaskConfig::GetInt32("screen.availTop") and if it has a value use that,
otherwise set availTop to 0 (or to a well-named constant like DEFAULT_AVAIL_TOP
= 0); update the code paths around nsGlobalWindowInner::Cast(inner),
mozilla::dom::ScreenDimensionManager::GetUserContextIdFromWindow(win) and
ScreenDimensionManager::GetDimensions(userContextId, &w, &h) to compute and
return {0, availTop, w, h - availTop} using the new default variable rather than
25 so platform assumptions aren’t baked in.
patches/webgl-spoofing.patch (1)

162-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cross-write to userContextId = 0 leaks vendor/renderer between contexts.

SetWebGLVendor and SetWebGLRenderer both do:

WebGLParamsManager::SetVendor(userContextId, vendor);
if (userContextId != 0) {
  WebGLParamsManager::SetVendor(0, vendor);
}

But ClientWebGLContext::GetParameter only reads WebGLParamsManager::GetVendor(GetUserContextId(), stored) — there's no fallback to context 0. So the write to webgl_vendor_0 never affects reads in any other context; its only observable effect is that a default-context window now reads back whatever the most recent non-default context wrote (information leak across user contexts, and inconsistent with what context 0 itself would have observed before).

Drop the if (userContextId != 0) { … (0, vendor) } block in both setters, or — if a cross-context default is actually desired — make GetVendor/GetRenderer fall back to the _0 key explicitly so the semantics are documented and symmetric.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/webgl-spoofing.patch` around lines 162 - 205, The setters
nsGlobalWindowInner::SetWebGLVendor and ::SetWebGLRenderer should not write to
the default context key (userContextId 0) because
ClientWebGLContext::GetParameter only reads
WebGLParamsManager::GetVendor/GetRenderer for the current GetUserContextId(),
causing a cross-context leak; remove the conditional block that calls
WebGLParamsManager::SetVendor(0, ...) and WebGLParamsManager::SetRenderer(0,
...), leaving the per-userContextId writes, leaving
DisableVendorFunction/DisableRendererFunction and the JS self-destruct code
unchanged; if you instead want symmetric fallback semantics, update
WebGLParamsManager::GetVendor/GetRenderer to fall back to the 0 key explicitly
and document the behavior, but do not perform the unconditional write from the
setters.
🧹 Nitpick comments (7)
additions/juggler/TargetRegistry.js (1)

205-220: 💤 Low value

Redundant null check after polling loop with rejection.

The polling loop (lines 205-217) rejects the promise with an error if gBrowser is not populated within 15 seconds. The subsequent check at line 218 (if (!domWindow.gBrowser)) is redundant because:

  • If the polling loop times out, it rejects and the execution never reaches line 218
  • If the polling loop succeeds, gBrowser is guaranteed to be non-null

Consider removing lines 218-219 to eliminate dead code.

♻️ Simplify by removing redundant check
         });
       }
-      if (!domWindow.gBrowser)
-        return;
       const tabContainer = domWindow.gBrowser.tabContainer;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@additions/juggler/TargetRegistry.js` around lines 205 - 220, The redundant
null check after the polling loop should be removed: the Promise used to await
domWindow.gBrowser (created inside the if (!domWindow.gBrowser) block using
start, tick, setTimeout and rejecting on timeout) either resolves when gBrowser
is populated or rejects on timeout, so the subsequent if (!domWindow.gBrowser)
return; is dead code; delete that check and directly use domWindow.gBrowser
(e.g., proceed to const tabContainer = domWindow.gBrowser.tabContainer) to
simplify the flow.
additions/juggler/screencast/HeadlessWindowCapturer.h (1)

9-11: ⚡ Quick win

Risk of pid_t redefinition conflict on Windows.

The typedef int pid_t; workaround for Windows may cause redefinition errors if pid_t is already defined by another included header (e.g., <process.h> or other system headers). Consider guarding the typedef with a more robust check:

`#ifdef` XP_WIN
`#ifndef` _PID_T_
`#define` _PID_T_
typedef int pid_t;
`#endif`
`#endif`

Or use a namespace-scoped type to avoid global namespace pollution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@additions/juggler/screencast/HeadlessWindowCapturer.h` around lines 9 - 11,
The current Windows-only typedef int pid_t in HeadlessWindowCapturer.h can clash
with other headers; update the fix by guarding the typedef so it only defines
pid_t when not already defined (e.g., wrap the typedef in an `#ifndef` check) or
replace the global typedef with a namespaced alias (avoid polluting global
namespace) and update usages in the file (references to pid_t) to use the new
safe symbol; ensure the change compiles on XP_WIN and preserves behavior for
code expecting pid_t.
patches/anti-font-fingerprinting.patch (1)

656-674: ⚡ Quick win

Avoid exposing mPresContext as a public field.

Moving mPresContext from private to public in nsFontMetrics just to let AutoTextRun access it breaks encapsulation and lets any caller mutate the raw pointer. Prefer a small public accessor and keep the member private.

♻️ Proposed refactor
   bool AllowForceGDIClassic() const { return mAllowForceGDIClassic; }
 `#endif`
 
-  // Pointer to the pres context for which this fontMetrics object was
-  // created.
-  nsPresContext* MOZ_NON_OWNING_REF mPresContext;
+  nsPresContext* PresContext() const { return mPresContext; }
 
  private:
   // Private destructor, to discourage deletion outside of Release():
   ~nsFontMetrics();
@@
   const nsFont mFont;
   RefPtr<gfxFontGroup> mFontGroup;
   RefPtr<nsAtom> const mLanguage;
+  // Pointer to the pres context for which this fontMetrics object was
+  // created.
+  nsPresContext* MOZ_NON_OWNING_REF mPresContext;
   const int32_t mP2A;

And update AutoTextRun to use aMetrics->PresContext() instead of aMetrics->mPresContext.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/anti-font-fingerprinting.patch` around lines 656 - 674, nsFontMetrics
exposes mPresContext as a public field which breaks encapsulation and allows
callers (like AutoTextRun) to mutate the raw pointer; revert mPresContext to
private, add a small public accessor on nsFontMetrics named PresContext() that
returns the nsPresContext* (or MOZ_NON_OWNING_REF nsPresContext*), and update
any callers (e.g. AutoTextRun) to call aMetrics->PresContext() instead of
accessing aMetrics->mPresContext.
patches/audio-fingerprint-manager.patch (1)

246-345: ⚡ Quick win

Duplicated user-context-id resolution block.

The same ~10-line block resolving userContextId from AudioContext → GlobalObject → Window/Worker is copy-pasted four times across AnalyserNode getters. Extract a small static or member helper (e.g. uint32_t ResolveUserContextId(AudioContext*)) to keep the call sites readable and prevent the four copies from drifting independently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/audio-fingerprint-manager.patch` around lines 246 - 345, The
duplicated block that resolves userContextId appears in
AnalyserNode::GetFloatFrequencyData, GetByteFrequencyData,
GetFloatTimeDomainData, and GetByteTimeDomainData; extract it into a single
helper (e.g. static uint32_t ResolveUserContextId(AudioContext* aContext) or a
private AnalyserNode::ResolveUserContextId() member) that encapsulates: getting
the AudioContext via GetParentObject(), obtaining nsIGlobalObject and
nsPIDOMWindowInner, falling back to GetCurrentThreadWorkerPrivate(), and
returning the mUserContextId (or 0). Replace each pasted block with a call to
that helper, then call
AudioFingerprintManager::GetSeed(ResolveUserContextId(...)) and
AudioFingerprintManager::ApplyTransformation as before.
patches/webgl-spoofing.patch (3)

510-523: ⚡ Quick win

Variable declaration inside a case label without braces.

The new case LOCAL_GL_COMPRESSED_TEXTURE_FORMATS: declares std::vector<uint32_t> compressedTextureUint32 directly under the label without an enclosing block. This works only because it's the last case before }, but it's brittle (a later case/default added below would make this ill-formed because the jump would skip a non-trivial initialization) and several compilers (-Wswitch-unreachable, MSVC C2360) flag it. Wrap the case body in braces.

♻️ Proposed fix
-    case LOCAL_GL_COMPRESSED_TEXTURE_FORMATS:
+    case LOCAL_GL_COMPRESSED_TEXTURE_FORMATS: {
       std::vector<uint32_t> compressedTextureUint32(
           state.mCompressedTextureFormats.begin(),
           state.mCompressedTextureFormats.end());
       retval.set(Create<dom::Uint32Array>(
           cx, this,
           MaskConfig::MParamGLVector<uint32_t>(pname, compressedTextureUint32,
                                                mIsWebGL2),
           rv));
       return;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/webgl-spoofing.patch` around lines 510 - 523, The case for
LOCAL_GL_COMPRESSED_TEXTURE_FORMATS declares a local std::vector
(compressedTextureUint32) directly under the case label which can cause
ill-formed code when other cases are added; fix it by enclosing the case body in
braces so the vector and the subsequent call to
retval.set(Create<dom::Uint32Array>(...,
MaskConfig::MParamGLVector<uint32_t>(pname, compressedTextureUint32, mIsWebGL2),
rv)) have their own scope, leaving the existing symbols
(compressedTextureUint32, MaskConfig::MParamGLVector, retval.set,
Create<dom::Uint32Array>, mIsWebGL2) unchanged.

255-275: 💤 Low value

Worker path falls through to userContextId = 0 when window lookup fails.

In GetUserContextId, the mOffscreenCanvas branch only consults GetCurrentThreadWorkerPrivate() when do_QueryInterface(global) succeeds in giving a non-null nsPIDOMWindowInner is false and we're inside that if (win) else-branch — but win is checked inside if (global), so if global is null we never reach the worker path. For OffscreenCanvas transferred to a worker, GetOwnerGlobal() should return the worker's global, and do_QueryInterface(global) to nsPIDOMWindowInner returns null, so the current shape does fall through to the worker check — but only because of an else if chain that's easy to break. Add a unit test or at minimum a brief comment so a future reorder doesn't silently regress worker spoofing to userContextId = 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/webgl-spoofing.patch` around lines 255 - 275, GetUserContextId's
mOffscreenCanvas branch currently only tries the worker path inside the else of
the "if (win)" that itself is nested under "if (global)", so a null global can
skip the WorkerPrivate check and fall through to returning 0; refactor the
mOffscreenCanvas handling in GetUserContextId so you always attempt to resolve a
WorkerPrivate via GetCurrentThreadWorkerPrivate() if do_QueryInterface(global)
returns nullptr (i.e., try nsPIDOMWindowInner* win = do_QueryInterface(global);
if win use win->GetBrowsingContext(), else independently call
GetCurrentThreadWorkerPrivate() and return
wp->GetOriginAttributes().mUserContextId), and add a short comment by the
WorkerPrivate check (or a unit test) noting why the worker-path must be
evaluated even when global is null to prevent future regressions.

369-423: ⚖️ Poor tradeoff

Optimize parameter masking checks to avoid repeated JSON lookups on every getParameter call.

GLParam() performs nested JSON lookups via GetNested() on every invocation, even when no WebGL masking is configured. While config parsing is cached via std::call_once, the .contains() checks and JSON access still run per-call. Since getParameter is invoked extremely frequently by WebGL apps (per-frame, sometimes per-draw), consider early-exiting if no mask configuration keys are set, or caching a flag indicating whether any WebGL parameter masking is configured.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/webgl-spoofing.patch` around lines 369 - 423, The current
GetParameter path calls MaskConfig::GLParam and does nested JSON lookups on
every invocation; add a cached boolean (e.g. MaskConfig::HasWebGLParameterMasks
or similar) computed once (using the existing call_once/config init) that
indicates whether any WebGL parameter masks or related keys exist, and
early-return/skip calling MaskConfig::GLParam and MaskConfig::CheckBool when
that flag is false; update MaskConfig to compute and expose this flag during its
initialization and change ClientWebGLContext::GetParameter to check the cached
flag before invoking MaskConfig::GLParam or doing per-call contains/lookups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f05c3982-bec1-47ea-ad67-a038421377b7

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9ff07 and cdddf69.

📒 Files selected for processing (33)
  • README.md
  • additions/juggler/TargetRegistry.js
  • additions/juggler/TargetRegistry.js.bak
  • additions/juggler/screencast/HeadlessWindowCapturer.cpp
  • additions/juggler/screencast/HeadlessWindowCapturer.h
  • additions/juggler/screencast/ScreencastEncoder.h
  • additions/juggler/screencast/nsScreencastService.cpp
  • build-tester/scripts/grading.py
  • docs/per-context-patches.md
  • patches/anti-font-fingerprinting.patch
  • patches/audio-fingerprint-manager.patch
  • patches/config.patch
  • patches/font-hijacker.patch
  • patches/geolocation-spoofing.patch
  • patches/librewolf/mozilla_dirs.patch
  • patches/librewolf/ui-patches/hide-default-browser.patch
  • patches/locale-spoofing.patch
  • patches/macos-sdk-26.1-allow.patch
  • patches/playwright/0-playwright.patch
  • patches/screen-spoofing.patch
  • patches/system-ui-font-spoofing.patch
  • patches/timezone-spoofing.patch
  • patches/webgl-spoofing.patch
  • patches/windows-theming-bug-modified.patch
  • pythonlib/camoufox/fingerprint-presets-v150.json
  • pythonlib/camoufox/fingerprints.py
  • pythonlib/camoufox/utils.py
  • scripts/_mixin.py
  • service-tester/README.md
  • service-tester/_proxies.py
  • service-tester/run_tests.py
  • service-tester/run_tests.sh
  • upstream.sh

Comment on lines +277 to +279
async cancelDownload(options) {
this._downloadInterceptor.cancelDownload(options.uuid);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing await causes cancelDownload to resolve before the underlying cancel completes.

DownloadInterceptor.cancelDownload (line 96) is async and awaits externalAppHandler.cancel(...). Here the outer TargetRegistry.cancelDownload does not await it, so:

  • the returned promise resolves immediately, before the cancellation actually finishes, breaking callers that rely on the operation being complete (e.g. test/CDP teardown sequences); and
  • any rejection from externalAppHandler.cancel becomes an unhandled promise rejection rather than propagating to the caller.
🛠 Suggested fix
   async cancelDownload(options) {
-    this._downloadInterceptor.cancelDownload(options.uuid);
+    await this._downloadInterceptor.cancelDownload(options.uuid);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@additions/juggler/TargetRegistry.js.bak` around lines 277 - 279,
TargetRegistry.cancelDownload currently calls
this._downloadInterceptor.cancelDownload(options.uuid) without awaiting it,
causing the outer promise to resolve prematurely and swallow rejections; update
TargetRegistry.cancelDownload to await (or return) the promise from
DownloadInterceptor.cancelDownload so callers get completion and any errors
propagate (refer to TargetRegistry.cancelDownload and
DownloadInterceptor.cancelDownload/externalAppHandler.cancel).

Comment on lines +322 to +374
async _newPageInternal({browserContextId}) {
console.error(`[TR-DEBUG] _newPageInternal start ctx=${browserContextId}`);
const browserContext = this.browserContextForId(browserContextId);
const features = "chrome,dialog=no,all";
// See _callWithURIToLoad in browser.js for the structure of window.arguments
// window.arguments[1]: unused (bug 871161)
// [2]: referrerInfo (nsIReferrerInfo)
// [3]: postData (nsIInputStream)
// [4]: allowThirdPartyFixup (bool)
// [5]: userContextId (int)
// [6]: originPrincipal (nsIPrincipal)
// [7]: originStoragePrincipal (nsIPrincipal)
// [8]: triggeringPrincipal (nsIPrincipal)
// [9]: allowInheritPrincipal (bool)
// [10]: csp (nsIContentSecurityPolicy)
// [11]: nsOpenWindowInfo
const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
urlSupports.data = 'about:blank';
args.appendElement(urlSupports); // 0
args.appendElement(undefined); // 1
args.appendElement(undefined); // 2
args.appendElement(undefined); // 3
args.appendElement(undefined); // 4
const userContextIdSupports = Cc[
"@mozilla.org/supports-PRUint32;1"
].createInstance(Ci.nsISupportsPRUint32);
userContextIdSupports.data = browserContext.userContextId;
args.appendElement(userContextIdSupports); // 5
args.appendElement(undefined); // 6
args.appendElement(undefined); // 7
args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8

console.error(`[TR-DEBUG] opening window with url=${AppConstants.BROWSER_CHROME_URL}`);
const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
console.error(`[TR-DEBUG] openWindow returned, awaiting ready`);
await waitForWindowReady(window);
console.error(`[TR-DEBUG] window ready, browsers=${window.gBrowser ? window.gBrowser.browsers.length : 'no gBrowser'}`);
if (window.gBrowser.browsers.length !== 1)
throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`);
const browser = window.gBrowser.browsers[0];
let target = this._browserToTarget.get(browser);
console.error(`[TR-DEBUG] initial target lookup: ${target ? 'found' : 'not found'}`);
let attempts = 0;
while (!target) {
attempts++;
console.error(`[TR-DEBUG] awaiting TargetCreated event (attempt ${attempts})`);
await helper.awaitEvent(this, TargetRegistry.Events.TargetCreated);
target = this._browserToTarget.get(browser);
console.error(`[TR-DEBUG] after TargetCreated: ${target ? 'matched' : 'not matched'}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove [TR-DEBUG] console.error statements before merge.

_newPageInternal is sprinkled with seven console.error('[TR-DEBUG] …') calls (lines 323, 357, 359, 361, 366, 370, 373). These look like ad-hoc instrumentation left over from chasing the Playwright issue 34586 timing problem. They will spam stderr on every page creation in production builds and leak internal state (context id, attempt counters, chrome URL). Drop them, or replace with a single gated dump() behind an env/pref check if you want a knob for future debugging.

🧹 Proposed cleanup
   async _newPageInternal({browserContextId}) {
-    console.error(`[TR-DEBUG] _newPageInternal start ctx=${browserContextId}`);
     const browserContext = this.browserContextForId(browserContextId);
@@
-    console.error(`[TR-DEBUG] opening window with url=${AppConstants.BROWSER_CHROME_URL}`);
     const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
-    console.error(`[TR-DEBUG] openWindow returned, awaiting ready`);
     await waitForWindowReady(window);
-    console.error(`[TR-DEBUG] window ready, browsers=${window.gBrowser ? window.gBrowser.browsers.length : 'no gBrowser'}`);
     if (window.gBrowser.browsers.length !== 1)
       throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`);
     const browser = window.gBrowser.browsers[0];
     let target = this._browserToTarget.get(browser);
-    console.error(`[TR-DEBUG] initial target lookup: ${target ? 'found' : 'not found'}`);
-    let attempts = 0;
     while (!target) {
-      attempts++;
-      console.error(`[TR-DEBUG] awaiting TargetCreated event (attempt ${attempts})`);
       await helper.awaitEvent(this, TargetRegistry.Events.TargetCreated);
       target = this._browserToTarget.get(browser);
-      console.error(`[TR-DEBUG] after TargetCreated: ${target ? 'matched' : 'not matched'}`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@additions/juggler/TargetRegistry.js.bak` around lines 322 - 374, The
_newPageInternal function contains multiple console.error('[TR-DEBUG] ...')
debug prints that must be removed before merge; locate the debug calls inside
_newPageInternal (references: browserContextId, AppConstants.BROWSER_CHROME_URL,
waitForWindowReady, window, this._browserToTarget, and the loop awaiting
TargetRegistry.Events.TargetCreated) and delete those console.error lines, or
replace them with a single conditional debug output (e.g., use dump() or a
logger) gated by a runtime env/pref check so it does not spam stderr in
production.

Comment on lines +218 to +236
@@ -2507,6 +2509,22 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument,
MOZ_ALWAYS_SUCCEEDS(bc->SetCurrentInnerWindowId(mInnerWindow->WindowID()));
}

+ // Camoufox: Auto-apply stored timezone when navigating to new document
+ // This ensures timezone persists across page navigations within same context
+ {
+ uint32_t userContextId = 0;
+ if (bc) {
+ userContextId = bc->OriginAttributesRef().mUserContextId;
+ }
+ nsAutoString storedTimezone;
+ if (mozilla::dom::TimezoneManager::GetTimezone(userContextId, storedTimezone)) {
+ if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) {
+ NS_LossyConvertUTF16toASCII timezoneASCII(storedTimezone);
+ JS::SetRealmTimeZoneOverride(cx, timezoneASCII.get());
+ }
+ }
+ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

SetNewDocument ignores JS::SetRealmTimeZoneOverride failures.

The auto-apply-on-navigate path discards the return value of JS::SetRealmTimeZoneOverride. If the stored timezone is corrupt or no longer a valid TZ id (e.g. after an ICU update), the new realm silently runs with no override, while TimezoneManager still reports it as set. At least log a warning, or Remove() the stored value so the next call recomputes from MaskConfig.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@patches/timezone-spoofing.patch` around lines 218 - 236, In SetNewDocument
after obtaining storedTimezone via mozilla::dom::TimezoneManager::GetTimezone
and before/after calling JS::SetRealmTimeZoneOverride, check its boolean return
and handle failures: if JS::SetRealmTimeZoneOverride(cx, timezoneASCII.get())
returns false, log a warning (include storedTimezone and userContextId) and call
the TimezoneManager removal API (e.g.
mozilla::dom::TimezoneManager::Remove(userContextId) or the equivalent method)
so the invalid stored value is cleared; keep using
nsContentUtils::GetCurrentJSContext and the storedTimezone/ timezoneASCII
variables to locate the code to change. Ensure logging uses the existing logging
facility (NS_WARNING/ProcessLogger/MOZ_LOG as appropriate) rather than silently
discarding the failure.

Comment thread scripts/_mixin.py
return "aarch64-unknown-linux-gnu" if arch == "arm64" else f"{arch}-pc-linux-gnu"
if target == "windows":
return f"{arch}-pc-mingw32"
return f"{arch}-pc-windows-msvc"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Firefox 150 require MSVC instead of MinGW for Windows builds?

💡 Result:

No. Firefox 150 does not require MSVC instead of MinGW for Windows builds. What the Firefox build docs and build-system metadata indicate is that MSVC is no longer the Tier-1 Windows build compiler; instead, Windows/x86 and Windows/x86-64 are Tier-1 targets that use clang-cl, while MinGW is a supported Tier-2 option (e.g., mingw-clang on Windows/x86, and mingw-gcc was listed as unsupported due to clang-bindgen requirements). The Mozilla build setup for Windows also centers on the MozillaBuild environment plus the ability to pass the desired target/ toolchain via mozconfig (for example, you can pass a MinGW target like x86_64-pc-windows-gnu/mingw-w64). Evidence: - Mozilla’s supported build targets list Tier-1 Windows/x86 and Windows/x86-64 as using clang-cl, and explicitly states “Prior to Firefox 63, Windows/x86 and Windows/x86-64 relied on the MSVC compiler; from Firefox 63 onward MSVC is not supported.” It then lists Tier-2 Windows/x86 (mingw-clang) and Windows/x86-64 (mingw-gcc, “Unsupported due to requirements for clang-bindgen”).[1] - A Firefox build job/mozconfig example for MinGW clang shows setting “--target=x86_64-pc-windows-gnu” and using an x86_64-w64-mingw32-clang toolchain, with MinGW-specific notes in the file.[2] - Mozilla’s Windows build instructions describe using the MozillaBuild shell and running./mach build, rather than mandating MSVC specifically.[3][4] So, for Windows builds of Firefox 150, MSVC is not a requirement; MinGW-based builds are possible (with caveats per the supported-targets matrix), while the primary supported Windows compiler is clang-cl.

Citations:


Remove or reject the change from MinGW to MSVC build target.

Firefox 150 does not support MSVC for Windows builds. Mozilla's official documentation explicitly states that MSVC support was removed starting with Firefox 63. The current supported Windows compilers are clang-cl (Tier-1) and mingw-clang (Tier-2). Changing from MinGW to MSVC will break Windows builds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/_mixin.py` at line 61, Revert the Windows target change in
scripts/_mixin.py: the return currently using the MSVC target must be changed
back to the supported MinGW/clang target so Windows builds remain compatible;
locate the return statement that constructs the target triple (the line
returning the "{arch}-pc-windows-msvc" string) and restore it to the previous
Windows target triple (e.g., the MinGW/clang variant such as
"{arch}-pc-windows-gnu"/mingw target) or otherwise reject the msVC change so
builds keep using clang-cl or mingw-clang as documented by Mozilla.

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