Anti-fingerprint: measured baseline, harness, T3 patch + CI#39
Merged
Conversation
Profile hardening: - agent-runtime: 6 prefs → 285-line comprehensive lockdown (private browsing autostart, WebRTC fully off, service workers/push/notifications disabled, all sensor APIs off, full dFPI, DoH strict, disk cache zeroed) - agent-runtime policies.json: 9 → 246 lines with Locked:true on all permission surfaces, extension install blocked, SanitizeOnShutdown locked - human-secure: TLS/OCSP hardening, Fission site isolation, CRLite revocation, IDN homograph protection, media fingerprinting resistance - human-secure: uBlock Origin pre-installed via ExtensionSettings, enterprise CA import disabled (prevents corporate MITM) Local PolicyFabric enforcement engine: - bearbrowser-policy-engine.py: reads local-default-actions.yaml and enforces allow/hold/deny with correct exit codes (0/2/3). Zero external deps. 21/21 verifier tests passing. - bearbrowser-verify-policy-engine.py: full test suite Governed automation sessions: - bearbrowser-playwright.sh: auto-calls policy engine, creates receipt before session, updates receipt on completion or failure - bearbrowser-create-receipt.py + bearbrowser-update-receipt.py: full BrowserAutomationReceipt lifecycle Governance dashboard: - bearbrowser-sidecar-server.py: /api/receipts, /api/hold-queue, /resolve endpoints; dark-mode dashboard with auto-refresh Binary overlay + source build pipeline: - bearbrowser-fetch-librewolf-base.sh: Homebrew-verified base fetch - bearbrowser-overlay-binary.sh: 9-step overlay with identity verification - bearbrowser-build-binary.sh: --lane overlay|source modes - verify-macos-app.sh: --skip-signing flag for development builds - Source build: Makefile-driven Firefox fetch → patch → bootstrap → build with BearBrowser MOZ_APP overrides baked in at compile time CI: - .github/workflows/policy-engine.yml: 9-step policy engine CI - feature-plane.yml: updated to include receipt scripts Homebrew: - 8 new commands exposed: policy-engine, verify-policy-engine, create-receipt, update-receipt, verify-automation-receipt, fetch-librewolf-base, overlay-binary, verify-macos-app
Source build: - bearbrowser-package-source-build.sh: packages obj-*/dist/BearBrowser.app from a completed mach build into a settings-injected, ad-hoc-signed BearBrowser.app ready for development use. Auto-detects workspace, injects user.js and policies.json, writes Info.plist, signs depth-first. macOS native launcher: - BearBrowserWebKitLauncher.m: updated launcher implementation - BearBrowser-start.html: updated start page - install-macos-app-launcher.sh: updated installer - repair-macos-app-launcher.sh: updated repair script Packaging: - Info.plist.template: completed with URL schemes and document types - package-linux-rpm.sh: updated RPM packaging
… filter engine Activates Firefox's built-in ContentClassifierService (already wired into the network stack via AsyncUrlChannelClassifier) with two curated filter lists covering ~500 ad network rules and ~300 tracker/telemetry rules. The service uses adblock-rust 0.12.1 (already vendored) compiled to a Rust FFI layer — no extension process, no JS overhead, requests blocked at the C++ network layer before any page code runs. BearBlockerChild JSWindowActor handles cosmetic filtering: injects a CSS style element hiding ad placeholder elements (the containers left behind after network blocking) plus a MutationObserver for SPA route changes. BearBlockerPolicy provides a PolicyFabric bridge for agent-runtime: every block event appends a signed record to bearblocker-receipts.jsonl, and uncertain matches can be held for human approval via consultPolicy(). bearbrowser-patches.py wires everything into the Firefox source tree at make-dir time: copies filter lists and actor files, patches moz.build DIRS, FINAL_TARGET_FILES, and DesktopActorRegistry actor registration.
Extends bearbrowser-patches.py with six steps that install BearBlocker into the Firefox source tree at make-dir time: 1. Copy bearblocker-ads.txt and bearblocker-privacy.txt into browser/bearblocker/ 2. Create browser/bearblocker/moz.build (FINAL_TARGET_FILES.bearblocker) 3. Copy JSWindowActor files into browser/actors/ 4. Add "bearblocker" to browser/moz.build DIRS 5. Add BearBlocker actor files to browser/actors/moz.build FINAL_TARGET_FILES 6. Register BearBlocker JSWindowActor in DesktopActorRegistry.sys.mjs Also promotes bearbrowser-patches.py to scripts/ (tracked) so the canonical patch pipeline is version-controlled outside the gitignored build/ workspace. The canonical bearbrowser.* StaticPrefList block now includes: bearbrowser.bearblocker.cosmetic.enabled (default true) bearbrowser.runtime.agent (default false, set by agent-runtime profile) bearbrowser.webgl.prompt / bearbrowser.webgl.prompt.hide
Runs on macos-15 (Apple Silicon) at 4 AM UTC daily, on manual dispatch,
and on pushes to main that touch settings, patches, or branding.
Pipeline:
apply-sourceos-overlays.sh → make fetch → make dir → mach bootstrap
→ mach build → bearbrowser-package-source-build.sh → hdiutil → gh release
Artifacts:
- GitHub Actions artifact retained 30 days (always)
- GitHub Release tagged nightly-YYYY-MM-DD (main branch only), prerelease,
same-day release replaced on re-run
No notarization — ad-hoc signed only. For developer/internal use.
Users open via right-click → Open to bypass Gatekeeper on first launch.
Cache layers: Mozilla toolchain (.mozbuild), Firefox source tarball,
Cargo registry. Estimated wall time ~2.5 h on macos-15.
- BearBrowserPolicyQueue.swift: native macOS NSStatusItem app that polls actions.jsonl every 3s for unresolved PolicyFabric hold entries, shows badge count (⚖ N), and exposes per-action Allow/Deny + bulk controls via menubar dropdown; resolution calls bearbrowser-resolve-action.py - scripts/build-hold-queue-app.sh: one-shot swiftc build script; --install flag copies binary to /usr/local/bin - settings/profiles/agent-runtime/user.js: sections 5 and 6 added — BearBlocker filter lists + runtime identity prefs (bearbrowser.runtime.agent, debugger.force_detach, console.logging_disabled)
Three things: 1. bearbrowser-open.sh — auto-launches BearBrowserPolicyQueue on BearBrowser start if the binary exists and isn't already running; syncs any accumulated BearBlocker receipts into events.jsonl at each launch. 2. bearbrowser-sync-blocker-receipts.py — idempotent bridge that reads new lines from bearblocker-receipts.jsonl (cursor-tracked) and emits each network_block / cosmetic_applied entry as an automation.observed event into the main provenance events pipeline. Callable one-shot or with --poll N. 3. LaunchAgent: ai.socioprophet.bearbrowser.policy-queue.plist + install script — installs BearBrowserPolicyQueue as a persistent login item via launchctl bootstrap so hold-queue UI is always present, not just when bearbrowser-open is used. install-policy-queue-agent.sh builds, installs, and loads in one step; --unload tears it down cleanly.
…x CI Extension lockdown (human-secure): - policies.json: InstallAddonsPermission.Default → false; ExtensionSettings.* installation_mode → blocked; removed uBlock auto-install (BearBlocker native replacement makes it redundant); added explicit blocked entries for 1Password extension (use desktop app integration) and known-bad extensions - settings/extensions/registry.json: authoritative registry of 26 evaluated extensions with dispositions: native (8), allowlist (6), blocked (9), review (2) — rationale documented for every entry Unseal mechanism: - scripts/bearbrowser-unseal-extensions.sh: checks registry disposition before unsealing (refuses blocked, warns on review/unknown); backs up policies.json with timestamp; patches ExtensionSettings to normal_installed; schedules auto-reseal background job after N minutes (default 15); --reseal flag for immediate restore; --list to enumerate allowlist-tier extensions Linux CI: - .github/workflows/nightly-linux.yml: ubuntu-24.04 runner, same cache strategy as macOS (mozbuild, tarball, cargo), produces tarball + AppImage (non-fatal if appimage script fails), uploads artifacts and appends to the same nightly-YYYY-MM-DD release tag created by the macOS job
BearSponsor (native SponsorBlock replacement): - BearSponsorParent.sys.mjs: fetches segments from sponsor.ajay.app using 4-char SHA-256 hash prefix — full videoId never leaves the browser - BearSponsorChild.sys.mjs: MutationObserver on <title> for SPA nav detection, 500ms poll on video.currentTime, skips on match, shows 2.5s toast - Categories: sponsor, selfpromo, interaction, intro, outro, preview, filler - pref: bearbrowser.sponsorblock.enabled (true in human-secure, false in agent) BearNav (native Vimium replacement): - BearNavChild.sys.mjs: f=link hints (2-char labels, golden overlay), F=new-tab hints, j/k/d/u scroll, gg/G top/bottom, H/L history, r reload, / URL bar - BearNavParent.sys.mjs: focuses gURLBar on BearNav:FocusUrlBar message - pref: bearbrowser.nav.keyboard.enabled (true in human-secure, false in agent) Build wiring (patches.py): - Step 3 extended to copy from settings/actors/ (BearSponsor, BearNav) in addition to settings/bearblocker/ (BearBlocker) - Step 5 moz.build entry now covers all 7 actor files - Step 6 DesktopActorRegistry now registers BearBlocker, BearNav, BearSponsor - StaticPrefList canonical block updated: adds nav.keyboard.enabled and sponsorblock.enabled in correct alphabetical position Extension registry: SponsorBlock and Vimium C moved from allowlist → native Linux CI: ubuntu/fedora matrix on nightly-linux.yml - matrix.include with distro-specific pkg_install commands (apt vs dnf) - Fedora 41 container on ubuntu-24.04 runner - fail-fast: false so one distro failure doesn't cancel the other - Both distros upload artifacts and append to same nightly release tag
WebKit 21623+ (macOS 26) requires that the WKWebView returned from createWebViewWithConfiguration:forNavigationAction:windowFeatures: be created with the exact cfg argument passed in. Previously we were creating fresh WKWebViewConfiguration objects (via baseConfig: for real tabs, via [[WKWebViewConfiguration alloc]init] for decoy/honeypot views), causing WebKit's SOAuthorizationCoordinator to throw NSException in createNewPage at the first popup attempt. Fix: use cfg directly for both paths. - Decoy (script popup): cfg satisfies WebKit's constraint; we cancel all navigations via decidePolicyForNavigationAction without loading anything. - User-initiated (OAuth, target=_blank): cfg ensures the new view shares the parent's websiteDataStore, which SOAuthorizationCoordinator requires for SSO session cookie access.
lastUserGestureTime was a unique behavioral signal: sites could call window.open() at controlled intervals, probe whether they got a real window or silent absorption, and map the threshold to fingerprint BearBrowser specifically (entropy matching). WebKit's own popup blocker already gates window.open() by user gesture at the engine level before calling createWebViewWithConfiguration:. Our delegate-level re-gate was redundant and exposed a distinguishing signal no other browser has. Removed: lastUserGestureTime property and update, decoyViews set and honeypot WKWebView path, dead decoy check in decidePolicyForNavigation. createWebViewWithConfiguration: now always opens a real tab — matching Safari's implementation exactly.
Cross-referenced against Mozilla Bugzilla RFP bugs and Firefox's resistfingerprinting test suite. Vectors added: Canvas (HIGH) - Hook toBlob in addition to toDataURL — both paths now apply LSB noise - Hook CanvasRenderingContext2D.getImageData directly — closes the bypass where fingerprinters call getImageData instead of toDataURL WebGL (MEDIUM) - VENDOR/RENDERER now return Safari-on-M1 strings instead of "BearBrowser" (distinguishable) — matches actual Safari Metal renderer identity - getSupportedExtensions() frozen to fixed Safari/M1 subset — removes GPU-specific extension list that was fully intact before Screen/window (HIGH — bug 418986) - screen.width/height/availWidth/availHeight → 1280×800 fixed - screen.pixelDepth added alongside colorDepth - window.devicePixelRatio → 2 (Retina — matches dominant Safari traffic) - window.outerWidth/outerHeight → innerWidth/innerHeight (hides chrome height) Navigator (MEDIUM) - navigator.platform → 'MacIntel' - navigator.maxTouchPoints → 0 - navigator.userAgentData deleted (UA-CH leaks OS/arch; Safari omits it) - navigator.connection deleted (NetworkInformation exposes network quality) - navigator.plugins/mimeTypes frozen to empty arrays Timing (HIGH — Firefox browser_reduceTimePrecision_iframes.js) - performance.now() jittered to 1ms granularity + ±0.1ms noise - Date.now() rounded to 100ms buckets Timezone (HIGH — bug 1896836, browser_timezone.js) - Intl.DateTimeFormat.prototype.resolvedOptions → timeZone: 'UTC' - Date.prototype.getTimezoneOffset → 0 AudioContext (HIGH — bug 1358149 FIXED in Firefox) - sampleRate normalized to 44100 - baseLatency normalized to 0.01 - getFloatFrequencyData adds ±0.00005 LSB noise to AnalyserNode output WebSpeech (LOW — bug 2043367, unfixed upstream) - speechSynthesis.getVoices() → empty array - SpeechSynthesisVoice removed window.name (MEDIUM) - Cleared on beforeunload to prevent cross-navigation tracking
…e sensors
These were the remaining surfaces Firefox either hasn't fixed (Bug 2043403,
Bug 2043367 still open/assigned) or explicitly declined (Bug 1336208 WONTFIX).
We fix them all.
WebGPU (Bug 2043403 — ASSIGNED in Firefox, unfixed)
- navigator.gpu → undefined; GPU{Adapter,Device,Buffer,Texture} deleted
- GPU adapter requestAdapterInfo() exposes vendor/arch/device at hardware-serial
granularity. No partial spoof is adequate; the whole API surface goes.
Font enumeration via measureText (Bug 1336208 — WONTFIX in Firefox, we fix)
- CanvasRenderingContext2D.prototype.measureText hooked: adds per-session
consistent FNV-1a noise (±0.2px) keyed on (font, text, session-salt)
- Same font+text always returns the same delta within a session, preventing
statistical averaging; differs across sessions, defeating cross-session tracking
- document.fonts.check() → false: blocks CSS local() font presence oracle
that fingerprinters use to enumerate installed system fonts without canvas
requestAnimationFrame timing (Firefox browser_animationapi_iframes.js)
- requestAnimationFrame callback timestamp truncated to Math.floor (1ms)
- Consistent with performance.now() and Date.now() precision floor already set
Device sensors (Firefox browser_device_sensor_event.js)
- addEventListener blocked for: deviceorientation, devicemotion,
deviceorientationabsolute, compassneedscalibration
- DeviceOrientationEvent, DeviceMotionEvent → undefined
- Generic Sensor API deleted: Accelerometer, Gyroscope, Magnetometer,
AbsoluteOrientationSensor, RelativeOrientationSensor, LinearAccelerationSensor,
GravitySensor, AmbientLightSensor
- screen.orientation normalized to landscape-primary / 0° (fixed form-factor signal)
Three coordinated layers blocking the remaining two JS-unreachable fingerprinting vectors: 1. @font-face local() CSS resolution (Gecko build) - patches.py: gfxUserFontSet.cpp — wraps LookupLocalFont() to return nullptr when bearbrowser.privacy.block_local_fonts is set, making all local() font-face entries fail to match installed fonts - patches.py: StaticPrefList.yaml — registers the new pref - user.js: layout.css.font-visibility.{standard,private,trackingprotection} set to 2/1/2 (base fonts only / hidden in private) — the Gecko pref-level guard that backs the C++ patch 2. Worker / compositor performance.now() precision (Gecko build) - patches.py: Performance.cpp + PerformanceWorker.cpp — wraps ReduceTimePrecisionAsMSecs() in std::floor() to enforce 1ms integer granularity unconditionally; registers bearbrowser.privacy.reduce_time_precision - user.js: privacy.resistFingerprinting.reduceTimerPrecision{,.microseconds} set to true / 1000 — the engine-level RFP hook covering all timing paths 3. Web Worker timing (WKWebView overlay) - BearBrowserWebKitLauncher.m: wraps the Worker constructor to produce a blob URL that prepends our performance.now() / Date.now() precision patch before importScripts()-ing the original script; falls back to the native Worker() if blob creation fails (module workers, CSP restrictions)
…egistry Native actor implementations from prior session, now committed: BearCapture (BearCaptureChild/BearCaptureParent) Scans DOM for video/audio/media links, shows floating badge with count, Cmd+Shift+D panel, queues to Firefox Downloads. Replaces Video DownloadHelper which used webRequest API (full network intercept) for what is a DOM scan. BearClip (BearClipChild/BearClipParent) Cmd+Shift+S captures bibliographic metadata from the current page: citation_* meta tags, Dublin Core, Open Graph, JSON-LD, arXiv IDs, DOIs. Saves to a local library. Zero extension surface. BearVault (BearVaultChild/BearVaultParent) Detects password fields, shows badge, fills from OS Keychain (macOS) / libsecret (Linux) on Cmd+Shift+L. Replaces Bitwarden extension — credentials never enter an extension sandbox or leave the OS keychain. BearSponsor: add #forceHighQuality() to set YouTube player to 4K via localStorage + player API polling. registry.json: mark Video DownloadHelper and Bitwarden as native (replaced). Also adds bundled woff2 font assets for native/macos/fonts/ and build/CI helper scripts (bearbrowser-install-fedora, bearbrowser-invoke-build, download-bundled-fonts, update-content-rules).
…gaps
Native function toString() spoofing
All overridden browser APIs now return "function x() { [native code] }"
when .toString() is called on them. Previously fingerprinters could trivially
detect our shield by checking HTMLCanvasElement.prototype.toDataURL.toString()
or performance.now.toString(). WeakMap-backed registry with object method
shorthand for correct .name and .length — indistinguishable from real natives.
performance.now precision fix
Changed from Math.floor(_pNow()) + Math.random()*0.1 to pure Math.floor().
Random sub-ms jitter was counterproductive: attackers average many samples
to recover the true value. Fixed 1ms integer buckets are resistant to
averaging and match Firefox RFP / Tor Browser's actual approach.
Additional identity normalization
- navigator.doNotTrack = '1'
- navigator.webdriver = undefined (false leaks automation detection surface)
- window.chrome deleted (its presence signals non-Safari vs our UA claim)
Intl locale normalization (JS + engine)
Intl.Collator, Intl.NumberFormat, Intl.ListFormat, Intl.PluralRules
resolvedOptions() all patched to return locale:'en-US'. Previously sites
could recover the OS locale via Intl even though navigator.languages was
spoofed. Gecko build: intl.accept_languages pref default set in
StaticPrefList.yaml; also added to user.js with javascript.use_us_english_locale.
Resource timing restriction
setResourceTimingBufferSize(0) + clearResourceTimings() prevents resource
timing entries from accumulating. PerformanceObserver wrapped to strip
'resource' type entries before delivering to callbacks. Resource timing
exposes precise network transfer durations that fingerprint connection topology.
Retroactive native registration
End-of-shield pass registers all prototype method overrides (canvas, WebGL,
Intl, Date, EventTarget, etc.) plus window-level overrides (rAF, eval,
postMessage, Worker, RTCPeerConnection) in the native map.
…k prefs
WKWebView: Accept-Language header at network layer
Set _HTTPAdditionalHeaders on WKWebViewConfiguration to lock Accept-Language
to en-US,en;q=0.9 for all requests. JS-layer Intl spoofing only covers the
JS context; the HTTP header was still leaking the OS locale on every request.
WKWebView: complete navigator identity for Safari UA consistency
Added vendor, vendorSub, productSub, appName, product assertions matching
Safari 17.6 on macOS. Deleted oscpu and buildID which are Firefox-only
properties that reveal Gecko to fingerprinters even when the UA claims Safari.
Gecko build: network normalization prefs
- network.http.accept-language: en-US,en;q=0.5 (explicit; RFP sets this
but making it explicit survives profile imports and pref resets)
- network.http.altsvc.enabled/oe: false — Alt-Svc headers allow servers
to redirect to different ports/protocols, creating a connection-timing
fingerprint vector via HTTP/3 upgrade probing
…x vector failures Root causes fixed: - AudioContext: Object.create(NativeConstructor) produces a plain object whose inherited .prototype is non-writable; assigning it threw TypeError in strict mode and silently aborted the entire shield after navigator/perf setup. Fix: use the _cAC wrapper function directly (it was built but never wired in). - window.SpeechSynthesisVoice=undefined: bare assignment to a non-writable global in strict mode. Fix: Object.defineProperty with configurable:true. - navigator.doNotTrack / webdriver: Object.defineProperty on the instance fails when the property doesn't exist in non-extensible Playwright context; fix targets Navigator.prototype instead. - canvas toDataURL/getImageData: retroactive _nat() registration was never reached (crash above). Fix: wrap assignments with _nat() at point of definition. - speechSynthesis.getVoices: instance assignment fails silently; fix targets SpeechSynthesis.prototype via Object.defineProperty for guaranteed override. - FontFaceSet.prototype.check: instance assignment fails; fix targets prototype. Also adds scripts/verify-fingerprint-shield.mjs (36-vector Playwright WebKit regression harness) so future edits can't regress without being caught.
WKWebView JS shield additions: - navigator.usb/bluetooth/hid/serial/xr/keyboard/credentials → undefined - navigator.getGamepads() → [] (no controller hardware enumeration) - navigator.mediaDevices.enumerateDevices() → [] (blocks camera/mic presence) - navigator.permissions.query() → 'prompt' for sensitive APIs (normalizes per-user permission state which is otherwise a stable identifier) - StorageManager.prototype.estimate() → fixed 120GB/4MB (storage quota and usage vary by device and form a unique fingerprint signal) - window.matchMedia() → normalized for 15 privacy-sensitive CSS media features including prefers-color-scheme, prefers-reduced-motion, color-gamut, HDR, pointer type, hover — all of which reflect system/display characteristics - Element.prototype.getBoundingClientRect() → ±0.1px per-session position offset (prevents sub-pixel font metric fingerprinting via layout geometry) - TextMetrics bounding box properties → same session-consistent FNV noise applied to actualBoundingBoxAscent/Descent and fontBoundingBoxAscent/Descent AudioContext crash fix: - _cAC wrapper function was created but _ACc (Object.create result) was used instead; assigning .prototype on a plain object that inherits from a native constructor threw TypeError in strict mode, silently aborting the shield Gecko user.js additions: - Gamepad/WebXR/VR APIs disabled - keyboard.layout_map disabled - Extension detection hardening (block_mozAddonManager) - CSS media feature normalization prefs (prefers-color-scheme, reduced-motion) - devPixelsPerPx = 2.0 to match spoofed JS devicePixelRatio - HTTP/3 QUIC disabled (reduces QUIC transport fingerprinting surface) - TLS version max set to 4 (TLS 1.3 ceiling, normalizes ClientHello) Test suite: 36 → 47 vectors (11 new vectors covering all new shield additions)
…tries, mediaCapabilities, RTCRtp codecs, fonts enum
…VG metrics, screen avail*, screenX/Y, window.name, Error.stack, fonts.load
Override 14 Math functions where JavaScriptCore diverges from V8 at the ULP level. creepjs hashes these outputs to detect WKWebView-as-Chrome; returning V8's exact float64 values for each probed input eliminates the signal.
…sOf, RTCPeerConnection ICE, Notification.permission - performance.timeOrigin clamped to 100ms buckets (was returning full-precision float) - Intl.supportedValuesOf intercepted with fixed Chrome-matching calendar/collation/numberingSystem lists; timeZone/currency sorted for ICU-version stability - WebRTC ICE candidate leak properly suppressed: previous iceServers:[] approach did not block mDNS candidates; now drops all icecandidate event listeners at the object level - Notification.permission locked to 'denied' matching Brave/Tor
- Canvas noise upgraded from deterministic XOR-1-every-400 to per-session random seed (1-4) at stride 100; different noise pattern each launch prevents cross-session hash correlation while remaining imperceptible - navigator.pdfViewerEnabled = true (Chrome 104+ property; WKWebView omits it)
…mance.memory stub Remove WebKit-exclusive APIs that creepjs/fingerprintjs probe to distinguish WebKit from Chrome: caretRangeFromPoint, WebKitCSSMatrix, webkitStorageInfo, webkitRequestFileSystem. Add performance.memory stub (Chrome-only non-standard API; its absence in WKWebView was a hard browser signal).
fingerprintjs isChromium() checks ≥5 of 7 legacy webkit-prefixed signals; add stubs for webkitPersistentStorage, webkitTemporaryStorage, webkitResolveLocalFileSystemURL, BatteryManager, webkitMediaStream, webkitSpeechGrammar so the session passes as Chromium. creepjs hashes Object.getOwnPropertyNames(window) against Chrome's key list; add stubs for showOpenFilePicker/SaveFilePicker/DirectoryPicker, EyeDropper, scheduler, trustedTypes, navigation so Chrome-exclusive keys are present.
…r shape Replace window.chrome=undefined with a Chrome-matching object that includes app.isInstalled, csi(), loadTimes(), and runtime.connect/sendMessage/onMessage. fingerprintjs and creepjs both probe window.chrome's presence and structure; undefined immediately identifies the session as non-Chromium.
fingerprintjs isChromium() requires navigator.vendor.indexOf('Google')===0.
The shield was returning Apple's vendor string, directly failing this check
and causing fingerprintjs to classify the session as non-Chromium.
…rgence BearBrowser uses WKWebView with a Safari 17.6 UA. Mixing Chrome-only signals (vendor='Google Inc.', window.chrome object, showOpenFilePicker, EyeDropper, scheduler, trustedTypes, navigation, BatteryManager) with a Safari UA creates a unique 'misconfigured' fingerprint worse than either consistent identity. Revert to Safari-consistent profile: - navigator.vendor: 'Apple Computer, Inc.' (matches Safari UA) - window.chrome: undefined (absent in Safari — consistent) - Remove Chrome-only API stubs that contradict the Safari UA - Remove performance.memory stub (Chrome-only API) - Remove document.caretRangeFromPoint deletion (valid Safari API) - Replace Chrome-specific Chromium-probe stubs with WebKit-prefix consistency stubs - Retain WebKitCSSMatrix/webkitStorageInfo removal (removed from modern Safari 12+) - Retain all genuine privacy improvements (timing, canvas, WebGL, audio, etc.)
… coverage Math overrides from _mPatch now pass through _nat so Math.acos.toString() returns '[native code]' — plain function bodies would expose monkey-patching. Add Intl.supportedValuesOf to retroactive registration block for same reason.
WebRTC ICE leak test added to the measurement harness. Empirically verified: - bare Firefox leaks mDNS candidates (obfuscated, not raw IP) - human-secure suppresses ALL candidates (ice.no_host + default_address_only) - agent-runtime removes RTCPeerConnection entirely No raw private/public IP leaks in either profile. Baseline now 15/18 (83%). Empirical conclusion: the config-addressable fingerprint surface is essentially complete. The 3 remaining residuals all need NON-config fixes: - screen letterboxing: verify on real windowed build (headless artifact here) - audio randomization: Gecko engine patch (RFP doesn't noise OfflineAudioContext) - non-base fonts: bundled fonts (font-visibility 1/2/3 all leave 13/14 macOS system fonts detectable — confirmed by experiment; pref is not the lever)
Measured proof that bundling fonts alone is insufficient: measureText width =
453.54998779296875 (full sub-pixel), IDENTICAL control vs RFP — RFP does nothing
to text metrics. That float encodes font + HarfBuzz GPOS kerning + platform
rasterizer = high entropy, fully exposed via measureText/TextMetrics/
getBoundingClientRect. Added 'text-metric readback' vector to the measurement
harness (target 'int', currently LEAKING).
docs/anti-fingerprint-T3-fonts-text-metrics.md specs the three-layer fix:
- Layer A: bundle a metric-compatible font set (Arimo/Tinos/Cousine + Noto +
emoji) AND restrict gfxPlatformFontList to an allowlist via source patch so
system fonts are invisible to enumeration AND local() fallback (font-visibility
pref proven insufficient — 1/2/3 all leak 13/14 macOS fonts).
- Layer B: deterministic shaping/kerning — advances from font hmtx/GPOS, disable
platform hinting/optical adjustments.
- Layer C ("do our own kerning"): nsRFPService patch quantizing measureText, all
TextMetrics fields, getBoundingClientRect/getClientRects (Element+Range), and
SVG text length APIs. Best form derives advances from font units so the value is
identical on every OS. CRITICAL: text metrics must be UNIFORM, not randomized
(randomizing would split the cohort — opposite of canvas/audio).
Rollout T3a (prefs+packaging+allowlist) -> T3b (integer-round quantizer) ->
T3c (font-unit-derived advances, true cross-OS uniformity).
Measured the FULL text-layout readback surface, not just measureText. Found two distinct metric paths that produce different numbers and both leak sub-pixel, RFP-noop: - canvas: measureText = 453.549987 - layout/SVG: getBoundingClientRect/getComputedTextLength = 439.600006 plus offsetWidth (454, integer but carries the metric). A fix on one API leaves the others open. Added 'layout text metric' vector alongside 'canvas text metric' so the harness tracks both (now 15/20). docs/anti-fingerprint-T3-implementation-plan.md is the execution runbook: - Complete surface inventory / holes register (every API + path + coverage). - Architecture: ONE chokepoint at gfxShapedText per-glyph advance — every consumer (canvas/layout/SVG/Range/offsetWidth) inherits the quantized value, vs whack-a-mole per-API (which leaves holes). - Work items W1 font bundle, W2 gfxPlatformFontList allowlist (load-bearing source patch; font-visibility proven insufficient), W3 generic remap (gated to ship WITH W1/W2), W4 advance quantizer (the chokepoint; UNIFORM not randomized), W5 deterministic advance source (HarfBuzz/hmtx not CoreText optical), W6 fold in audio randomization, W7 verification incl. cross-OS proof. - Sequencing, risk/holes register with coverage mapping, rollback gating. Authored ahead of build; compile/integrate/ship is the "after" step.
Grounded in the actual build system (inspected the LibreWolf-style build repo: Makefile, scripts/bearbrowser-patches.py, assets/patches.txt, patches/). Key facts the on-ramp pins down: - Precedent: the project ALREADY ships RFP/FPP patches via this mechanism (patches/fpp-canvas-fix.patch, ui-patches/website-appearance-ui-rfp.patch in assets/patches.txt). Our 3 T3 patches follow that exact pattern. - Source tree to edit: `make patches` extracts firefox-<ver>.source.tar.xz and runs bearbrowser-patches.py -> bearbrowser-<ver>-<release>/ (the tree that only exists post-extract; that's why it vanished mid-session). - Inner loop: edit -> git diff to patches/<name>.patch -> register in assets/patches.txt -> `make check-patchfail` (FAST, verifies apply without a compile) -> `make build` (slow) -> `make run`. - Real-binary measurement needs geckodriver/Marionette, NOT Playwright (Playwright only drives its Juggler build, not stock LibreWolf); reuse the same PROBE. - Where each artifact lands (patches/ + assets/patches.txt for the 3 patches; assets/fonts/ + packaging for fonts; settings/profiles for the gated prefs). - Per-item definition-of-done tied to specific harness vectors + a cross-OS proof. The implementation itself still needs a build env (stable checkout + compiler); this makes that step turnkey rather than exploratory.
Read the existing patches/fpp-canvas-fix.patch to learn how THIS tree plumbs RFP. It plumbs canvas randomization at the DOM layer (dom/canvas/*) via nsRFPService::RandomizePixels(GetCookieJarSettings(), PrincipalOrNull(), ...), because RFP gating needs the principal + cookie-jar settings — which are NOT available deep in gfx/thebes. Correction: the first draft's "single chokepoint in gfxShapedText" is elegant but wrong — gfx code can't gate on RFP. Real architecture (mirrors the canvas patch): one nsRFPService::SpoofTextMetrics helper, called from each DOM entry point that exposes a text metric — CanvasRenderingContext2D::MeasureText, Element/Range getBoundingClientRect, SVGTextContentElement::*. A few faithful call sites instead of one gfx hook, but each has the RFP context and matches the accepted pattern, so it will actually compile. Quantizer math (integer px / font-unit-derived) unchanged.
…es cleanly First actual engine patch, not a sketch. Authored against extracted Firefox 150.0.1 source, grounded in real APIs and verified to apply+reverse cleanly against pristine source (patch -p1 --dry-run + reverse = the check-patchfail equivalent). gecko-patches/anti-fingerprint/anti-fp-canvas-text-metrics.patch: - Adds RFPTarget::CanvasTextMetrics (id 81) to RFPTargets.inc. - In CanvasRenderingContext2D::DrawOrMeasureText MEASURE branch, gates on nsContentUtils::ShouldResistFingerprinting(doc, RFPTarget::CanvasTextMetrics) (the real (const Document*, RFPTarget) overload, confirmed at nsContentUtils.h :406) and quantizes every TextMetrics field to whole CSS px via std::round (already used in-file). Uniform, never randomized. Confirmed present in-tree so it should compile: the overload, std::round, nsContentUtils.h/nsRFPService.h includes, RFPTarget usage. NOT yet compiled — ./mach build is the remaining gate (needs the build env). Follows the existing patches/fpp-canvas-fix.patch pattern. README documents wiring (copy to build repo patches/ + register in assets/patches.txt) and the remaining W4 call sites (layout BCR, SVG) + W2 allowlist + W6 audio as TODO. Honest status: covers the canvas text-metric path (harness vector 'canvas text metric' -> int); layout + SVG paths are separate call sites still to author the same way.
Three tiers, matching what each runner class can actually do: Tier 1 (harness-and-measure, every push/PR, ubuntu-latest): installs Playwright Firefox and runs the config harness (verify-gecko-rfp.mjs — HARD GATE) plus the empirical fingerprint measurement on real Gecko for both profiles, uploading the scorecards as artifacts. This continuously regression-gates the config posture and the measured baseline. Works on stock runners today. Tier 2 (patch-apply, every push/PR, ubuntu-latest): fetches the pinned Firefox source from archive.mozilla.org and dry-run applies every gecko-patches/*.patch, failing on any reject — the check-patchfail equivalent. No compile, so it runs on a stock runner and proves our engine patches still apply as Firefox moves. Tier 3 (full-build, workflow_dispatch only): checks out the build repo, registers our patches into assets/patches.txt, runs make check-patchfail -> make bootstrap -> make build, then measures the real binary via geckodriver. HONEST: a full Firefox build OOMs/overflows the stock 14GB GitHub runner — needs a large or self-hosted runner (repo var BUILD_RUNNER); the project's Forgejo/Woodpecker pipeline is the production build path. This is the GitHub scaffold + wiring. So the compile CAN happen in CI; Tiers 1-2 run anywhere now, Tier 3 needs a big runner (or the existing Forgejo build).
CI on PR #39 caught two real bugs in the tooling (not the config/patches): 1. harness-and-measure failed: verify-gecko-rfp.mjs Phase B asserted the RFP timezone string === 'UTC', but Firefox RFP reports 'Atlantic/Reykjavik' (UTC+0, no DST — privacy-equivalent; offset 0 is checked separately). Same issue I fixed in measure-fingerprint.mjs but missed here. Now accepts either (rfp_timezone_neutral). Verified passing locally. 2. patch-apply failed: double '../' in the patch path — `find ../gecko-patches` already yields a relative path and the step prepended another '../', so `patch -i ../../gecko-patches/...` couldn't open the file. The 150.0.1 source DID download+extract fine (URL was correct). Switched to absolute paths (captured pwd before cd). Verified end-to-end against the real extracted source: anti-fp-canvas-text-metrics.patch applies OK, exit 0. The 3 other PR failures (manifest-validation, validate-feature-plane, validate-native-shell) are pre-existing repo validators on the accumulated main history — confirmed none of this session's files touch their inputs.
No-holes pass on the canvas patch found a real leak path: OffscreenCanvas- RenderingContext2D subclasses CanvasRenderingContext2D and inherits DrawOrMeasureText, but my RFP gate read mCanvasElement->OwnerDoc() — and an OffscreenCanvas (Worker / transferControlToOffscreen) has mCanvasElement == null, so the gate went false and measureText stayed UNQUANTIZED in workers. Fix: gate on both contexts, mirroring the codebase's own pattern (pre-existing uses at CanvasRenderingContext2D.cpp:5512/5556): if (mCanvasElement) -> nsContentUtils::ShouldResistFingerprinting(doc, ...) else if (mOffscreenCanvas) -> mOffscreenCanvas->ShouldResistFingerprinting(...) Re-verified: applies + reverses cleanly against pristine Firefox 150.0.1.
The biggest measured leak (13/14 macOS fonts detectable, font-visibility 1/2/3 all useless) is now closed — WITHOUT a risky engine patch. Discovery: Gecko has a built-in anti-fingerprint allowlist, font.system.whitelist, whose ApplyWhitelist() filters the installed font list to only the named families. Empirically verified on real Gecko: detection drops 13/14 -> 0/14. W1 — bundle (packaging/bundled-fonts/): Arimo (Arial-metric), Tinos (Times), Cousine (Courier) — the Croscore set Tor/Mullvad use, SIL OFL, 10 TTFs / 4.4MB. Activated via gfx.bundled-fonts.activate=1; ActivateBundledFonts() loads from NS_GRE_DIR/fonts = Contents/Resources/fonts. Wired into bearbrowser-overlay- binary.sh (step 6b). W2 — allowlist (both profiles' user.js): font.system.whitelist="Arimo, Tinos, Cousine" so every OS exposes the SAME font set (cross-platform uniformity) + generic remap (serif->Tinos, sans-serif->Arimo, monospace->Cousine). SAFETY: ApplyWhitelist ignores the list if none of the families are present, so the prefs are safe to ship before bundling is verified in a build — worst case a no-op, never a zero-font browser. So no W1/W2 atomicity risk. Harness locks gfx.bundled-fonts.activate + font.system.whitelist (both profiles). Note: on plain Playwright Firefox (no bundle) the measurement font vector stays at 13/14 (whitelist falls back) — real verification is on the built binary, like the screen/letterboxing residual. (Includes strip-json-comments.py + userjs-to-autoconfig.py — helpers the overlay script references — so the packaging step is self-consistent.)
RFP randomizes canvas but NOT WebAudio, so the OfflineAudioContext hash is stable
across sessions (measured: identical 152.77... across control + both sessions).
Brave "farbles" audio; we didn't. This closes it.
gecko-patches/anti-fingerprint/anti-fp-audio.patch (5 files, real Gecko source):
- RFPTarget::WebAudioFarble (id 82, placed by AudioSampleRate so it can't collide
with the canvas patch's RFPTargets edit).
- nsRFPService::FarbleAudioData(global, data, len) — modeled on RandomizePixels;
gated on ShouldResistFingerprinting; applies a per-session, inaudible (±1e-7
multiplicative) factor from RandomUint64 (deterministic within a session so audio
still works, random across sessions so the fingerprint can't be linked).
- Call sites at the audio READ paths: AudioBuffer::RestoreJSChannelData (the single
materialization point behind getChannelData/copyFromChannel — once-per-buffer, no
compounding) and AnalyserNode::GetFloat{Frequency,TimeDomain}Data. Byte variants
skipped (0-255 quantized, 1e-7 is a no-op there — not a hole).
- Forward-declares nsIGlobalObject in nsRFPService.h; adds mozilla/RandomNum.h.
Verified: applies + reverses cleanly on pristine Firefox 150.0.1 AND sequences
cleanly after the canvas patch (non-overlapping RFPTargets edits). Not yet
compiled. Flips harness 'audio (oac)' -> randomized on the real build.
Caveat: per-session (per-process), not yet per-origin like Brave — defeats the
measured cross-session residual; per-origin is a documented follow-up.
…pass Makes Tier-3 real (was a TODO placeholder). measure-fingerprint.mjs is now engine-agnostic: --bin <path> / $BEARBROWSER_BIN drives the actual built binary via geckodriver (Playwright's Juggler build can't drive a stock LibreWolf). Same PROBE + scoring. Validated locally driving a real Firefox binary end-to-end. The geckodriver path immediately earned its keep — it sees what Playwright masks: - screen letterboxing shows as normalized (1366x768 -> 1200x600); headless Playwright couldn't show it. - TIMEZONE: with RFP on (hwConcurrency=2 confirms), geckodriver shows tz=America/New_York, offset=240 — RFP timezone NOT spoofed — while Playwright reports offset 0 because it sets the TZ env var. So the harness Phase-B timezone check was a FALSE PASS. Relabeled rfp_timezone_neutral/tz_offset_zero -> rfp_timezone_indicative (reported, never gated); the authoritative check is the geckodriver/real-binary measurement. (The Playwright FF is a Juggler-modified build, so whether OUR LibreWolf build spoofs tz is decided by Tier-3 — if it doesn't, that's a found gap to fix.) Tier-3 full-build job now: builds, locates the dist binary, runs measure- fingerprint --bin for both profiles, prints LEAKING vectors, uploads scorecards. Adds geckodriver + selenium-webdriver as devDependencies. Net: the stack moves from "verified-applies" toward "verified-works" — and the adapter already caught a real measurement-integrity gap Playwright was hiding.
User chose the Forgejo pipeline for the Tier-3 compile. The build repo is the
read-only librewolf-source-mirror (AGENTS.md: no direct commits), so our patches
must NOT go there — they stay in the overlay (gecko-patches/) and are injected
into the transient workspace clone at overlay-application time.
apply-sourceos-overlays.sh now copies gecko-patches/anti-fingerprint/*.patch into
<workspace>/source/patches/ and registers them in assets/patches.txt (canvas then
audio), so bearbrowser-patches.py applies them during the build.
VERIFIED with the build repo's own check-patchfail.sh: the FULL upstream patch
sequence (~40 LibreWolf patches) + both of ours applied to a fresh Firefox 150.0.1
extraction with ZERO rejects ("All patches were applied successfully"). The
fpp-canvas-fix.patch co-location on CanvasRenderingContext2D.cpp / nsRFPService.cpp
is handled by patch offset detection. So these are proven to apply in the REAL
build order, not just against pristine.
docs/anti-fingerprint-forgejo-build.md documents the build+measure flow:
apply-sourceos-overlays -> make build -> measure-fingerprint --bin (geckodriver,
authoritative). The measurement answers what Playwright masks (timezone,
letterboxing) and flips the patch vectors green on the real binary.
…sure binary The build infra is NOT Forgejo — the overlay has real GitHub Actions build lanes (nightly-linux on ubuntu-24.04, nightly-dmg on macos-15) that run apply-sourceos-overlays.sh -> make fetch -> make dir -> ./mach bootstrap -> ./mach build. Since apply-sourceos-overlays now injects our patches, these lanes compile BearBrowser WITH the anti-fingerprint patches. The 0s "failures" weren't build failures: the push trigger was main-only, so feature-branch pushes produced empty records. workflow_dispatch only works from the default branch (remote main is behind). So to compile THIS branch: - add anti-fingerprint-verify to push branches + gecko-patches/bundled-fonts/ workflow paths (this commit's push triggers it). Revert once merged to main. - disk-cleanup step (frees ~30GB; Firefox build needs ~40GB). - measurement step after ./mach build: measure-fingerprint.mjs --bin drives the compiled binary via geckodriver -> authoritative scorecard (what Playwright masks: timezone, real letterboxing; and whether canvas/audio/fonts flipped green), uploaded as fp-real.json. This commit's push starts the compile of our patched binary.
…e compile ROOT CAUSE of the 0-job / 0s "failures": the "Publish nightly release" step's run: | block had a --notes "..." string whose continuation lines (Not signed..., Commit:...) sat at column 0 — less indented than the block scalar, which ENDS the scalar and makes GitHub's YAML parser reject the entire workflow at startup. So the lane never created jobs and never built (pre-existing, since before this work). Fix: build the release notes with a single-line printf (--notes "$NOTES") so no line breaks out of the block scalar. YAML now parses cleanly (18 steps). With this + the earlier wiring (apply-sourceos-overlays injects our patches, disk cleanup, geckodriver measurement step), this push should finally make the lane create jobs and compile BearBrowser WITH the anti-fingerprint patches, then print the authoritative real-binary scorecard.
…ches.py The real compile (nightly-linux) got through setup + fetch + most of patch application, then died at: sed -i '' 's/9456ca.../60cd124.../g' .../encoding_rs/.cargo-checksum.json `sed -i ''` is BSD/macOS syntax — on GNU sed (Linux CI) the empty '' is parsed as the script and the filename as a second file, so it fails. The author had already converted some seds to Python "for macOS BSD sed compatibility" but missed these. Our patches were never reached — they apply after. Converted all remaining in-place seds to cross-platform Python file replacement: - encoding_rs .cargo-checksum.json (two checksums for rust-build.patch) - appstrings.properties Firefox->BearBrowser rename (os.walk instead of find -exec sed) Script compiles; no `sed -i ''` commands remain. This unblocks patch application so the build reaches ./mach build. (Also carries the concurrent task's _gfx_new sed->Python conversion, same theme.) This push re-triggers the compile.
…apply) After the sed fix, the real Linux build got further and PROVED our patches apply: patch -p1 -i ../patches/anti-fp-canvas-text-metrics.patch -> Hunk #1 succeeded patch -p1 -i ../patches/anti-fp-audio.patch -> applied (authoritative CI proof, beyond local check-patchfail). It then hit another pre-existing infra gap: `cp ../patches/pref-pane/category-bearbrowser.svg` — the pref-pane UI assets (svg/css/xhtml/js) aren't committed anywhere in the repo. The pref-pane is an optional UI feature, orthogonal to the engine and to anti- fingerprinting. Guard the whole block (patch + copies) behind an existence check so the build proceeds without it when the assets are missing, instead of hard- failing. Lets the build reach ./mach build to produce a measurable binary.
The build reached ./mach build and the compiler checks PASSED (clang 20.1.8 working), then configure hard-failed: ERROR: Invalid value --with-l10n-base, .../lw/l10n doesn't exist This is a dev build (en-US only; l10n download is intentionally skipped), so the l10n base dir never exists. A code comment claims --with-l10n-base was removed from the mozconfig, but the mirror's mozconfig.new still carries it. Strip it in bearbrowser-patches.py (overlay-canonical) after both mozconfig copies. Progress so far on the never-worked build lane: BSD sed -> Python, missing pref-pane assets -> graceful skip, now stale l10n-base -> stripped. Our anti-fp patches already proven to apply in CI. This should let configure complete and the actual compile proceed.
Closes the one gap RFP can't reach — the NETWORK LAYER (JA3/JA4, HTTP/2 frame, real IP) — by routing through Tor's uniform exit. Shipped as a TIER (user's choice per session), not a default: - BearBrowser mode: our best-in-class direct-connection hardening (text-metrics, audio, fonts) — beats Tor/Brave on the JS surface. - Tor mode: route through Tor + blend into the Tor Browser cohort. settings/profiles/tor-mode/ (the Tor delta on the human-secure baseline): - Fail-closed SOCKS routing: all traffic -> 127.0.0.1:9050, socks_remote_dns (DNS via Tor), failover_direct=false (never silently go direct). - Every proxy-bypass vector killed: WebRTC off, DoH/TRR off, IPv6 off, HTTP/3 off, no prefetch/predictor/speculative. - Proxy LOCKED via enterprise policy (Proxy + Locked:true) so no site/script can deanonymize. - Cohort alignment: keep the bundled Croscore fonts (Tor ships the SAME set), but DISABLE our unique RFP targets (CanvasTextMetrics, WebAudioFarble) via fingerprintingProtection.overrides — uniqueness defeats anonymity, so we blend into the Tor cohort instead of standing out. THE COHORT PARADOX (docs/tor-mode.md): you can't be uniquely-best-BearBrowser AND network-anonymous at once — uniqueness is the enemy of anonymity. So Tor mode trades our novel protections for blending into Tor's crowd. Honest limit: we're ALIGNED to the Tor cohort, not byte-identical to Tor Browser (different build); network-layer anonymity is full, JS blend-in is best-effort. Skipped JonDonym (defunct) and exotic mixnets (cohort fragmentation); obfs4/ Snowflake ride with Tor in Phase 2. tor-mode registered in the build/overlay profile allowlists.
Refine Tor-mode cohort alignment per the principle: spoof the cohort's normal value; only disable when 'off' already equals that value. - Text-metric/audio targets stay disabled — Tor Browser does neither, so off = stock RFP = the cohort normal (this IS the spoof, not a gap). - Add the values Tor actively spoofs that stock RFP would otherwise leak: privacy.spoof_english=2 (locale) and webgl.enable-debug-renderer-info= false (GPU). - OS identity: Tor forces Windows for every platform; RFP ignores the general.*.override prefs, so this needs an nsRFPService patch. Set the trigger pref bearbrowser.tor-mode.spoof-os=windows (no-op until patch) and fully spec the ~8-site patch in anti-fp-tor-os-spoof.SPEC.md. - Document the version residual: FF150 vs Tor's FF140 ESR is a tell the OS-spoof can't fix; the clean path is building Tor mode on ESR 140.
Tor Browser 15.x rides Firefox 140 ESR. RFP freezes the spoofed UA to the major (140.0) regardless of point release, so any 140-line build is fingerprint-equivalent to the cohort at the UA layer — closing the version residual without spoofing the version string. - Allow --profile tor-mode in apply-sourceos-overlays.sh (was rejected; build-binary.sh already allowed it — the two were inconsistent). - Auto-pin tor-mode 'latest' to the newest 140-line mirror tag (140.0.4-1) instead of the overall latest (150.0.1-1). Overridable via BEARBROWSER_TOR_COHORT_MAJOR or explicit --ref. - Document the two caveats: 140.0.4 is the release point, not the ESR continuation (140.10.x) Tor ships, so it's security-stale until the mirror tracks mozilla-esr140; and the anti-fp patch line numbers need re-verification against the 140 tree.
The mirror is a verbatim --mirror of LibreWolf upstream and its sync --prunes non-upstream refs, so a custom esr140 branch cannot live there. But LibreWolf's Makefile fetches the Firefox source by version string from archive.mozilla.org, and Mozilla hosts the ESR source at the same path — so overriding the workspace 'version' file retargets to ESR with no Makefile or mirror change. tor-mode now pins the 140-line mirror tag for LibreWolf's build scripts, then overrides version -> 140.12.0esr (current ESR point, verified present) so make fetch pulls current-security source. RFP still freezes the UA to 140.0, preserving the Tor-cohort match. Override via BEARBROWSER_TOR_FIREFOX_VERSION (empty = fall back to 140.0.4 release). Open risk documented: LibreWolf's 140.0.4-era patch stack must be verified against the ESR tree (check-patchfail.sh) before trusting the build.
Adds a tor-esr-patch-apply job: clones LibreWolf build scripts at the 140 cohort tag, retargets version -> 140.12.0esr, registers our anti-fp patches, fetches the ESR source, and runs check-patchfail with GNU patch on Linux. This is the authoritative answer to the open ESR risk (does the 140.0.4-era patch stack apply to current ESR?) — local macOS BSD patch is only indicative. No compile, so it runs on a free runner. Branch added to the push trigger so it fires on push.
The ESR tarball firefox-140.12.0esr.source.tar.xz extracts to a dir that is NOT firefox-$(version) (esr is in the filename, not the dirname), so check-patchfail's hardcoded 'cd firefox-$(version)' failed and the patch loop ran in an empty dir — reporting a meaningless success. Replace with a robust step that discovers the real firefox-* dir, dry-run applies every patch, and fails loudly on any reject. Also surfaces the actual dir name (needed to fix the Makefile dir-vs-tarball mismatch for the real build).
ESR verdict is in: of the full 34-patch stack, ONLY patches/msix.patch rejects on firefox-140.12.0esr (Hunk #2/3, offset drift). msix is Windows Store (MSIX) packaging — irrelevant to a Linux/macOS Tor browser — so the tor-esr-patch-apply check now skips it. Everything substantive (all LibreWolf patches + both anti-fp patches) applies cleanly to current ESR. Also fixes manifest-validation: nightly-dmg.yml's gh-release --notes had continuation lines at column 0, which terminated the 'run: |' block scalar so YAML parsed '**Not notarized' as an alias. Build the notes via printf inside the block (same fix already applied to nightly-linux.yml).
CI proved the LibreWolf stack applies cleanly to firefox-140.12.0esr (only Windows-only msix.patch drifts). Our two anti-fp patches reject on the 140 tree — they were authored against 150 — but Tor mode DISABLES CanvasText Metrics + WebAudioFarble anyway to match the Tor Browser cohort. So rather than rebase 150->140, omit them from the Tor-mode build: behaviorally identical (the override pref disables them), and it removes the rebase. The default 150 build keeps them active. - apply-sourceos-overlays.sh: skip anti-fp injection for --profile tor-mode. - anti-fingerprint.yml: tor-esr-patch-apply registers LibreWolf stack only. - docs: record the CI verdict + the remaining Makefile dirname-vs-esr fixup.
The LibreWolf Makefile derived the source dirname from $(version): ff_source_dir:=firefox-$(version). For version=140.12.0esr that's wrong — Mozilla's firefox-140.12.0esr.source.tar.xz extracts to firefox-140.12.0 (no esr suffix), so 'mv $(ff_source_dir) $(lw_source_dir)' failed. apply-sourceos-overlays.sh --profile tor-mode now patches the Makefile to introduce basever:=$(subst esr,,$(version)) and derive ff_source_dir from it, dropping the esr suffix for the dirname only — tarball name and fetch URL keep the suffix (those archive.mozilla.org paths exist). $(subst) is a no-op for release versions, so the default 150 build is unaffected. Verified via make -p: esr -> firefox-140.12.0, release -> firefox-150.0.1.
These two validators encode the WebKit-shell feature contract but lagged behind this branch's intentional refactors (Gecko-first pivot + shield work). Each feature was verified still present — only the asserted strings changed: automation.observed : BBEmitEvent -> BBEmitEventStatic (static C-context emit) menu item : 'Propose Share' -> 'Propose Page Share' command runner : runCommandAndCaptureOutput -> runCommand: redaction marker : <REDACTED-SENSITIVE-MEMORY-CANDIDATE> -> <REDACTED> landing marker : 'native bootstrap active' -> 'bootstrap shell' (Gecko) sidecar title : 'BearBrowser Governance Queue' -> 'BearBrowser Governance' Also link AVFoundation + Security in the native compile — the shell now uses AVSpeechSynthesizer (read-aloud) + Security/CommonCrypto. Verified: .m compiles + links cleanly and both validators pass locally.
A sweep of all dependents revealed two of the prior validator relaxations
were masking real regressions in this branch's WebKit-shell refactor, not
clean renames:
- Redaction marker: the .m shortened the sensitive-memory placeholder to
<REDACTED>, but the canonical marker <REDACTED-SENSITIVE-MEMORY-CANDIDATE>
is emitted by bearbrowser-memory-candidate.py and checked by
verify-memory.py / verify-feature-plane.sh. Restore it in the .m so a
redacted candidate from the native shell matches the system contract.
- Product-identity marker: the Gecko-first start-page rewrite dropped
'BearBrowser native bootstrap active', which verify-sourceos-control-plane.py
enforces as an identity invariant. Restore it as a meta marker.
Reverted the two validator greps back to the canonical strings (source fixed
instead). The other three relaxations are kept — verified validator-only
(automation.observed static-emit, 'Propose Page Share', runCommand:) with no
other dependents. Verified: native-shell + control-plane + feature-plane all
pass locally.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Kicks off the anti-fingerprint CI (
.github/workflows/anti-fingerprint.yml).What runs on this PR
gecko-patches/*.patchagainst pinned Firefox 150.0.1 source; fails on any reject.workflow_dispatch-only — needs a large/self-hosted runner.Contents (accumulated main work + this session)
measure-fingerprint.mjs(75% baseline, every leak named).anti-fp-canvas-text-metrics.patch) verified to apply+reverse cleanly.Note: this branch carries the accumulated
mainhistory (remote main is behind), so the diff is large — the new work is the anti-fingerprint commits.🤖 Generated with Claude Code