Skip to content

Latest commit

 

History

History
374 lines (296 loc) · 86.6 KB

File metadata and controls

374 lines (296 loc) · 86.6 KB

clearcote Stealth-Patch Manifest

Target: Chromium 149.0.7827.114 (ungoogled base) · Windows x64 · cross-compiled on Linux/Wine (SSH box) Base spoofing layer: adryfish/fingerprint-chromium 142→149 port Coherence model: Brave per-eTLD+1 farbling, ported onto clearcote's session-seed root Status: execution-ready · file paths are real Chromium 149 targets, grounded against the SSH-box tree


1. Overview — Stealth Thesis

clearcote's stealth posture rests on two coherence axes that most anti-detect forks get wrong:

  1. Engine-level coherence (vertical). Every fingerprint surface must describe one plausible real machine — and that machine must match the actual render backend the binary ships with. A WebGL renderer string cannot coexist with MAX_TEXTURE_SIZE=4096, a contradictory ANGLE/D3D string, off-distribution hardwareConcurrency + deviceMemory, a Berlin geolocation under an America/New_York timezone — or a readPixels hash that came from a different GPU than the renderer string claims. Surfaces are drawn tier-first from curated real-device distributions (Steam/StatCounter clusters), never independently randomized.

  2. Per-eTLD+1 coherence (horizontal). The single most important upgrade over the entire stealth niche: adryfish/ChromiumFish ship one global --fingerprint seed, so every site sees identical farbled values — a cross-site linking supercookie. clearcote keeps that int as a session token and layers Brave's per-eTLD+1 derivation on top: same site → stable all session (no flicker tell); different sites → unlinkable; new session → fresh persona.

The render-backend rule (new, governs all GPU surfaces). The cross-compiled binary renders WebGL/WebGPU via whatever backend is present at runtime on the deployment host, not the build host. clearcote therefore pins one persona GPU whose real pixel output is indistinguishable from that backend:

  • Default deployment = real Windows host with a real GPU → advertise an ANGLE-on-D3D11 string for a mainstream integrated GPU (Intel UHD 770), because ANGLE/D3D11 is exactly what a real consumer Chrome on that host renders with, and the readback hash falls in that GPU's real cluster.
  • Headless/SwiftShader runtime (CI, GPU-less VPS) → advertise an ANGLE-SwiftShader-consistent string, never a discrete-GPU name, so renderer string and pixel hash agree.
  • Never advertise a discrete "NVIDIA RTX 3060"-class renderer over a backend that does not produce that GPU's pixels. A renderer-string-vs-readPixels-hash mismatch is the single strongest GPU tell, and a string spoof that creates one makes the browser more detectable than shipping the backend's honest string.

The layering. Three strata, applied in order:

Layer Role Source model
L0 — farbling-core + persona engine One unified seed engine: --clearcote-seed root + per-eTLD+1 token derivation + SessionCache (the entropy reader every surface uses) and the FingerprintProfile persona struct (UA/screen/HW/GPU tuples). Two readers, one root. Brave BraveSessionCache + ChromiumFish persona-seed plumbing
L1 — adryfish base port adryfish 142→149 replayed; provides baseline spoofing for surfaces it already covers. Its noise is upgraded to per-eTLD+1, not double-patched. The audit gate (below) closes its global-seed reads. adryfish/fingerprint-chromium
L2 — per-surface patches Canvas/WebGL/WebGPU/audio/fonts/navigator/screen/TLS/WebRTC/geo-media/timing/de-Google/humanize. Each reads the L0 token (per-site noise) or persona (Current()). Brave + Camoufox + Cromite + BotBrowser + ChromiumFish

Three non-negotiable design rules thread every patch:

  • Never blank, never zero. Blank canvas, zeroed readPixels, empty WebGPU vendor, empty getVoices(), 100ms-clamped performance.now() — each is a harder tell than the real value. clearcote perturbs or substitutes coherently, never blocks.
  • Never read the global seed directly. Every surface resolves through SessionCache::From(context) / fingerprinting::Current(). A single bypass re-creates the linking supercookie — enforced by a build-time assertion (see §3), not a later-phase audit.
  • Never let a constant collapse the fleet. Any value pinned to one literal across all personas (font set, prefers-color-scheme, battery level) becomes a linkable cohort. Such values are drawn per-persona from a weighted distribution, not hard-pinned.

2. Patch Table by Domain

2.0 — farbling-core + persona engine (L0 infrastructure)

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
clearcote-seed-switch Session entropy root: --clearcote-seed=<uint64> (superset of adryfish int32 --fingerprint) New CONTENT_EXPORT kClearcoteSeed; browser auto-generates one base::RandUint64 at startup if absent, back-fills onto CommandLine; lazy std::call_once Current() reader content/public/common/content_switches.{h,cc}, chrome/browser/chrome_content_browser_client.cc ChromiumFish persona-seed.patch; adryfish --fingerprint P0 Zero web surface. Operational risk: if launcher omits flag and processes self-generate divergently → intra-session flicker. Browser generates once, force-forwards.
clearcote-seed-forward Seed coherence across all processes (renderer/GPU/utility/zygote/worker hosts) Add kClearcoteSeed (and kPersonaSeed/kPersonaOs) to kSwitchNames[] allow-list (PropagateBrowserCommandLineToRenderer) + mirror into AppendExtraCommandLineSwitches for GPU/utility. Subsumes persona-seed-plumbing — one forward patch, not two. content/browser/renderer_host/render_process_host_impl.cc, chrome/browser/chrome_content_browser_client.cc ChromiumFish persona-seed.patch P0 Flag visible only to local process-inspectors (not web). Inverse risk: forgetting one process type (shared-worker host) reintroduces incoherence. Must NOT collide with any automation-flag set that flips navigator.webdriver.
clearcote-persona-profile Single source of truth: FingerprintProfile (persona Chrome version, brand list, unified/navigator platform, UA-metadata, HC, device_memory, screen WxH/avail/colorDepth/DPR, the one persona GPU, WebGL limits) — tier-first draw New third_party/blink/public/common/fingerprinting/fingerprint_profile.{h,cc}; fingerprinting::Current() thread-local via DeriveFromSeed(seed, eTLD+1, os). SplitMix64-mix seed → draw HwTier FIRST (Steam-weighted) → HC+RAM+screen+GPU from tier sub-pools (no off-distribution pairs). Merges persona-fingerprint-profile + fingerprint-profile-engine persona-half into one struct. …/public/common/fingerprinting/fingerprint_profile.h, …/common/fingerprinting/fingerprint_profile.cc, …/common/BUILD.gn chromiumfish persona-seed + fingerprint-profile.patch, extended per-eTLD+1 P0 Low if per-OS block internally coherent and GPU pinned to the one render-backend-consistent value (Intel UHD 770 / ANGLE-D3D11 for real-Win deploy; ANGLE-SwiftShader for headless). WeightedPick keeps (cores, RAM, OS, Chrome version, screen, GPU) to real clusters.
clearcote-persona-version-freeze The frozen persona Chrome version consumed identically by UA/CH generator, TLS group table, prototype-coherence A/B diff, JA4 guards Expose FingerprintProfile::chrome_version as a single P0 artifact; all version-coherent consumers READ it, never re-derive. A real shipped Chrome build (full version quad + brand-list permutation order captured from that build) …/common/fingerprinting/fingerprint_profile.cc (kPersonaChromeBuilds[]) clearcote-native (closes the cross-phase version-drift gap) P0 Kill-tell if drift: UA says Chrome X, TLS/prototype/JA4 keyed to Chrome Y → matches no real Chrome. Freezing one value forbids drift across the 4 phases.
clearcote-etld-token Global seed → per-eTLD+1 128-bit base::Token DeriveSiteToken(seed, etld1) à la Brave CreateStableFarblingToken: high = PersistentHash(etld1) ^ seed; low = PersistentHash(high) ^ rotl(seed). eTLD+1 via GetDomainAndRegistry(origin, INCLUDE_PRIVATE_REGISTRIES). Pure function — no persisted per-site metadata net/base/registry_controlled_domains/registry_controlled_domain.h, components/content_settings/renderer/content_settings_agent_impl.h, components/content_settings/common/content_settings_agent.mojom, …/content_settings_manager.mojom brave-core brave_shields_utils.cc GetFarblingToken P0 Deterministic: if seed leaks to JS, all tokens predictable+linkable — keep seed browser/renderer-internal. Must split on eTLD+1 (co.uk handled, subdomains share = no tell).
clearcote-mojom-token Wire: carries per-eTLD+1 token to each renderer frame Add mojo_base.mojom.Token farbling_token to per-frame content-settings struct; populate in ContentSettingsManagerImpl browser-side; getter on ContentSettingsAgentImpl …/content_settings_agent.mojom, …/content_settings_manager.mojom, …/content_settings_agent_impl.h, third_party/blink/public/platform/web_content_settings_client.h brave-core shields_settings.mojom P0 IPC-internal. Correctness: default-zero token = farbling OFF (token.is_zero()) so chrome://, extensions, file://, opaque origins are never farbled — farbling WebUI is a hard tell and breaks browser UI.
clearcote-session-cache (== farbling-seed-core; build once) The renderer-side per-document seed holder + PRNG factory every surface calls Port BraveSessionCacheclearcote::SessionCache, a GarbageCollected Supplement<ExecutionContext>. Pulls token from ContentSettingsClient->farbling_token(), XORs StorageKey nonce hash for anon/credentialless iframes. MakePseudoRandomGenerator(FarbleKey)randen_engine<uint64> (verified present in abseil), seed = high^low^key. Per-surface helpers: PerturbPixels (HMAC-SHA256), audio fudge, FarbledInteger. New core/farbling/BUILD.gn third_party/blink/renderer/core/execution_context/execution_context.h, …/platform/supplementable.h, …/platform/storage/blink_storage_key.h, …/platform/network/network_utils.h brave-core brave_session_cache.{h,cc} P0 This file IS the engine — a bug taints every surface. (1) randen_engine+HMAC math must reproduce Brave bit-exactly; (2) created lazily per ExecutionContext, survives GC or values flicker; (3) short-circuit when token zero.
clearcote-worker-seed Extends token into Web/Shared/Service workers + Audio/Paint worklets Thread farbling_token through GlobalScopeCreationParams; inject at CreateWorkerContentSettingsClient for dedicated/shared, embedded_worker_instance_client_impl.cc for service. SessionCache::From(worker_scope) resolves same token content/renderer/render_frame_impl.cc, …/workers/global_scope_creation_params.h, …/workers/worker_or_worklet_global_scope.h, content/renderer/service_worker/embedded_worker_instance_client_impl.cc brave-core brave_session_cache.cc worker branches P0 High if omitted: detectors run canvas/audio probes inside a Worker to catch main-thread-only spoofing. Service-worker token pinned to registration's eTLD+1, not top frame.
clearcote-disable-global-fingerprint Audit gate (build-time): neutralizes adryfish's single-global-seed paths so two seed systems never coexist; the supercookie is impossible to ship After adryfish 142→149 lands, redirect adryfish FingerprintSeed() → per-document SessionCache seed; replace inline global-seed reads with SessionCache::From(context).MakePseudoRandomGenerator(key). Add a static_assert/compile-time guard that fails the build if any surface references the adryfish global-seed accessor symbol. …/platform/network/network_utils.cc, content/renderer/render_thread_impl.h, …/execution_context/execution_context.h adryfish global accessor (no RESEARCH.md — unverifiable; removed as a citation) P0 (promoted from P1) The gate must close before/with the first P0 surface, not a phase later — otherwise P0 ships the supercookie. Build fails rather than warns. Exact lines depend on which adryfish surfaces port — but the patch lands in P0a, immediately after the replay.

2.1 — canvas-webgl-gpu

GPU ownership (resolves the §2.1-vs-§2.5 contradiction): there is exactly ONE persona GPU, defined in clearcote-persona-profile, pinned to Intel UHD 770 / ANGLE-D3D11 for real-Windows deployment (or ANGLE-SwiftShader for headless runtime). webgl-strings-coherent is the SOLE owner of the UNMASKED string write; the screen domain's old webgl-gpu-coherence is folded into this row (it no longer writes the string, it only reads Current().gpu). No two patches write the same getter.

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
canvas2d-farble-perturb getImageData, toDataURL, toBlob/convertToBlob, measureText PerturbPixels at every readback exit: canvas_key = HMAC-SHA256(token, pixels); seed v from first 8 bytes; walk 32-byte key, 16 bits/byte, each bit picks channel=v%3, pixel=4*(v%count)+channel, flips LSB, advances via LFSR. Content+token dependent → invisible, stable per-site, unlinkable. Hook getImageDataInternal return, ToDataURLInternal pre-encode, CanvasAsyncBlobCreator ctor, OffscreenCanvas paths. measureText via FarbledInteger on advance width …/canvas2d/base_rendering_context_2d.cc, …/base_rendering_context_2d.idl, …/html/canvas/html_canvas_element.cc, …/html/canvas/canvas_async_blob_creator.cc, …/offscreencanvas/offscreen_canvas.cc, …/offscreencanvas2d/offscreen_canvas_rendering_context_2d.cc, …/imagebitmap/image_bitmap.cc brave-core PerturbPixelsInternal P0 LSB-only = below visual/statistical detection (Brave-deployed). Must cover OffscreenCanvas+worker. Pitfall: getImageData and toDataURL must derive from the SAME token. Never blank.
webgl-strings-coherent (SOLE UNMASKED owner; folds in screen webgl-gpu-coherence) WebGL/WebGL2 UNMASKED_VENDOR/RENDERER_WEBGL, and the inline GL_VERSION/GL_SHADING_LANGUAGE_VERSION strings, getSupportedExtensions/getExtension Re-point the existing kSpoofWebGLInfo hook (verified at webgl base 4104-4105) at Current().gpu — a real internally-consistent Intel UHD 770 / ANGLE-D3D11 vendor/renderer/driver tuple matching spoofed UA/OS and the render backend. Also override the WebGL2 inline GL_VERSION (webgl2_rendering_context_base.cc:4789) and GL_SHADING_LANGUAGE_VERSION cases, which currently read the REAL ContextGL()->GetString(GL_VERSION) and leak the SwiftShader/ANGLE backend version verbatim. Per-context WebGLFarbledExtensionHandler …/webgl/webgl_rendering_context_base.cc, …/webgl2_rendering_context_base.cc, …/webgl_rendering_context_base.h, …/bindings/core/webgl/webgl_farbled_extension_handler.cc (new) brave-core GetWebGLDebugInfoValue + camoufox MaskConfig webGl + adryfish/ungoogled kSpoofWebGLInfo hook P0 Most-fingerprinted surface. Brave's random 8-char string is Brave-identifiable — clearcote defaults to a COHERENT real Intel-UHD-770 tuple. Critique-correct: UNMASKED does fall through to base at 4987 (no leak there), but WebGL2 GL_VERSION reads the real backend inline — that is the genuine leak this patch now closes. Renderer MUST match readback hash (one GPU).
webgl-params-coherence Numeric/array getParameter: MAX_TEXTURE_SIZE, MAX_VIEWPORT_DIMS, ALIASED_*_RANGE, MAX_*_UNIFORM_VECTORS, getShaderPrecisionFormat, getContextAttributes, drawingBufferWidth/Height, antialias/MSAA sample counts Override from the same Intel UHD 770 profile that picks the renderer string (e.g. MAX_TEXTURE_SIZE=16384). Port Camoufox MaskConfig::GLParam/MParamGL. Pin shader-precision triples + context attributes (BotBrowser model). Add drawingBufferWidth/Height and getContextAttributes().antialias / sample-count to the enumerated pname set so they match the spoofed GPU rather than the real backend's MSAA capability …/webgl/webgl_rendering_context_base.cc, …/webgl2_rendering_context_base.cc, …/webgl_context_attribute_helpers.cc, …/webgl_shader_precision_format.cc camoufox webgl-spoofing.patch + BotBrowser webglAttrs.diff P1 CreepJS / fingerprinters GPU-coherence flags renderer + impossible MAX_TEXTURE_SIZE. Ship one curated (renderer → full-param-vector) tuple for the persona GPU. Over-spoofing (impossible values) worse than not spoofing.
webgl-readpixels-farble readPixels() into ArrayBufferView (canvas-equivalent FP bypassing canvas2d) After real ContextGL readPixels fills buffer in ReadPixelsHelper, apply same HMAC+LFSR LSB perturbation, keyed per-eTLD+1, gated WEBGL level. For FLOAT/HALF_FLOAT framebuffers: apply a tiny relative-epsilon perturbation (not skip) so a float-FBO request is not a perturbation-free readback path …/webgl/webgl_rendering_context_base.cc, …/core/farbling/brave_session_cache.cc brave-core BRAVE_WEBGL_…_RETURN (Brave nulls; clearcote perturbs) P1 Brave blocks at MAXIMUM → all-zero pixels = detectable. clearcote perturbs. Critique-fix: do not skip float — multiply each component by 1 ± epsilon (epsilon ≪ ULP-of-interest) keyed to the token, avoiding NaN/Inf while still perturbing. Pixel hash must come from the same GPU the string advertises.
webgpu-adapter-farble GPUAdapterInfo: vendor, architecture, device, description, driver Scrub/override in CreateAdapterInfoForAdapter (gpu_adapter.cc:125, verified), gated per-site WEBGPU level, rewrite to values from the same Intel UHD 770 profile feeding WebGL. base::AutoReset like Brave's BraveScrubWebGpuAdapterInfo but supply real strings, not empty …/webgpu/gpu_adapter.cc, …/gpu_adapter_info.cc, …/webgpu/gpu.cc brave-core BraveScrubWebGpuAdapterInfo P1 Brave empties strings = detectable + inconsistent with populated WebGL. clearcote makes WebGPU vendor/arch COHERENT with the one persona GPU. Mismatch (WebGL=Intel, WebGPU=NVIDIA) = strong signal.
gpu-strings-source-coherence GPU-process source of truth: GPUInfo.gl_vendor/gl_renderer, surfaced to chrome://gpu, GL_VERSION/VENDOR/RENDERER Deepest coherence: rewrite GPU strings at collection in gpu_info_collector.cc so WebGL/WebGPU/chrome://gpu/GL_VERSION all derive from one consistent record = the persona Intel UHD 770. Process-global (OS baseline); layer per-site noise in Blink patches gpu/config/gpu_info_collector.cc, gpu/config/gpu_info.h, gpu/command_buffer/client/gles2_implementation.cc, …/service/gles2_cmd_decoder.cc Novel clearcote (camoufox value-table + BotBrowser profile concept) P2 Protective when correct. Risk: gpu_info_collector feeds GPU blocklisting/driver-workaround selection — keep deviceID/vendorID/driver_version within real released Intel UHD 770 combos. P2 because Blink patches already cover web surfaces.

2.2 — audio

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
audio-buffer-farble (SOLE primary noise source for the sample path) AudioBuffer.getChannelData() / copyFromChannel() — primary AudioContext FP Add ScriptState to both (verified: getChannelData lacks ScriptState today); at read sites value *= fudge_factor where fudge = 0.999 + (token.high/UINT64_MAX)/1000, per-eTLD+1. Mutate the JS-visible copy only, not internal DSP …/webaudio/audio_buffer.cc, …/audio_buffer.h, …/audio_buffer.idl brave-core audio_buffer.*.patch P0 Low if fudge ~0.1% + per-eTLD+1 stable (CreepJS runs probe twice — drift detectable). Must NOT touch internal render path.
audio-analyser-farble getFloat/ByteFrequencyData, getFloat/ByteTimeDomainData std::optional<ClearcoteAudioFarblingHelper> member into RealtimeAnalyser, seeded in AnalyserHandler ctor from the same token/fudge as audio-buffer-farble; short-circuit each of 4 readers before dB/byte conversion …/webaudio/realtime_analyser.cc, …/realtime_analyser.h, …/analyser_handler.cc brave-core realtime_analyser.cc.patch P1 Single-source guarantee: analyser and buffer farble must read one shared (seed, fudge) from one token, or a CreepJS double-probe sees offline ≠ live = detectable inconsistency.
audio-farbling-helper Shared per-seed noise primitive (not a surface) New PLATFORM_EXPORT ClearcoteAudioFarblingHelper(fudge, seed, max): default = multiply; MAXIMUM = 63-bit Galois LFSR → v/UINT64_MAX/10. Seed/fudge from per-eTLD+1 token (high→fudge, low→seed). The one helper instance is shared by buffer + analyser + offline isolation …/platform/audio/clearcote_audio_farbling_helper.{cc,h}, …/platform/BUILD.gn brave-core brave_audio_farbling_helper.{cc,h} P0 Internal. LFSR/fudge math byte-identical to reference; one shared seed source prevents the triple-path divergence the critique flags.
audio-context-metadata-spoof sampleRate, baseLatency, outputLatency, maxChannelCount Replace permission-based latency quantization with persona values; ctor passes profile.audio_sample_rate into RealtimeAudioDestinationNode::Create; MaxChannelCount() returns persona. Assert OfflineAudioContext actually renders at the spoofed sampleRate (add a render-rate check in the offline destination) so sampleRate getter and real render rate agree. Fixed-per-persona …/webaudio/audio_context.cc, …/audio_context.h, …/base_audio_context.cc chromiumfish webaudio.patch + camoufox P1 baseLatency128/sampleRate multiples = tell. Critique-fix: the spoofed rate must be the rate OfflineAudioContext genuinely renders at, not just the getter value.
offline-audio-render-isolation OfflineAudioContext rendered output (DynamicsCompressor probe for non-getChannelData reads) In DoOfflineRendering, before copy into shared_render_target_, run IsolateAudioSamples(channel, frames_processed_, seed) using the same shared helper/seed as audio-buffer-farble — thread frames_processed_ as global offset. Win build: enabled unconditionally, gated behind audio level. Fallback only for reads not covered by buffer-farble; both share one seed so sums never drift out of band …/webaudio/offline_audio_destination_handler.cc, …/offline_audio_context.cc chromiumfish webaudio.patch (IsolateAudioSamples) P2 Double-noising risk eliminated by single shared seed (critique). Do NOT copy chromiumfish's brittle hard-coded Win-target-sum delta-absorption — uniform per-seed multiplicative only.

2.3 — fonts

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
font-allowlist-win-persona (distribution, not one canonical set) Installed-font list to CSS family-probing — realistic Win10/11 set, drawn per-persona from a weighted distribution Engine allowlist (modeled on Cromite bromite_allowed_fonts.h): kAllowedFontNames base + weighted overlay variants drawn per-persona from the seed: stock, stock+Office (~200 fonts), stock+Adobe, stock+OEM so the fleet shows realistic font-count entropy instead of one too-clean cluster. Locale-keyed script sub-lists off --lang + IsFontAllowed(). Gate inside FontCache::CreateTypeface (Win path); resolve TRUE GDI name via new skia::DWriteFontTypeface_GetGDIFamilyName. @font-face bypass …/fonts/skia/font_cache_skia.cc, …/fonts/win/font_cache_skia_win.cc, …/fonts/font_cache.h, …/fonts/skia/clearcote_allowed_fonts.h (new), skia/ext/skia_utils_win.{cc,h}, …/platform/BUILD.gn Cromite Fonts-fingerprinting-mitigation.patch + chromiumfish font-substitution.patch P0 Critique-fix: a single canonical stock-only set is itself a narrow fingerprintable cohort (CreepJS scores font-list entropy). Variant overlays per-persona restore real-world variance. Must still gate DWrite proxy count consistently with the CSS-probe count.
font-access-api-filter queryLocalFonts() enumeration + DWrite proxy family count/names Filter enumeration through the same per-persona allowlist variant browser-side; clamp GetFamilyCount/GetFamilyNames. Drop non-allowlisted FontMetadata; suppress non-persona PostScript/full names. Returned order matches real DWrite enumeration content/browser/font_access/font_access_manager.cc, …/modules/font_access/font_access.cc, …/font_access/font_metadata.cc, content/browser/renderer_host/media/dwrite_font_proxy_impl_win.cc Cromite gate logic extended (clearcote-native) P0 If queryLocalFonts/CSS-probe/DWrite count diverge by one family = strong signal. All three from the one chosen per-persona variant.
font-metrics-jitter-pertld measureText() width, bounding boxes, em-height, baselines, glyph advances Per-eTLD+1 deterministic sub-pixel offset into HarfBuzz pos->x_advance at single choke harfbuzz_shaper.cc:288-295, delta ≤0.1px from hash(eTLD+1 ⊕ session_token ⊕ glyph_id ⊕ family). Both measureText + layout consume advances → coherent …/fonts/shaping/harfbuzz_shaper.cc, …/html/canvas/text_metrics.cc, …/fonts/simple_font_data.cc, …/fonts/font_metrics.h camoufox anti-font-fingerprinting.patch re-keyed per-eTLD+1 P0 Jitter small + quantized. Shaping-only so measureText/getClientRects/scrollWidth agree. Seed NOT global. (See element-rect-jitter for non-text rects.)
element-rect-jitter-pertld (new — covers the non-text ClientRects surface) getBoundingClientRect/getClientRects sub-pixel values for non-text elements + CSS-transform precision Apply a per-eTLD+1 deterministic sub-pixel quantization at the layout→DOMRect boundary (layout_object.cc rect mapping / dom_rect_read_only.cc) with the same token as font-metrics-jitter so text and element rects stay mutually coherent. Delta ≤0.05px, stable per-site …/layout/layout_object.cc, …/geometry/dom_rect_read_only.cc, …/dom/element.cc camoufox rect-jitter concept, re-keyed per-eTLD+1 (clearcote-native extension) P2 Critique-fix: font jitter only covered text advances; fingerprinters also hash non-text element rects + transform precision. Too-large delta breaks layout assertions; keep ≤0.05px and shared-seed with text jitter.
font-fallback-mac-skip Removes macOS/Linux family names from CSS cascade (Helvetica/Menlo/Apple*/Hiragino*/PingFang*/DejaVu*) In FontFallbackList::GetFontData loop, continue past denylisted families. GetLastResortFallbackFont bypasses loop via direct FontCache::GetFontData() (no SIGSEGV). Aggressive denylist OUT of strict gate (chromiumfish crash lesson) …/fonts/font_fallback_list.cc, …/fonts/alternate_font_family.h, …/fonts/skia/clearcote_allowed_fonts.h chromiumfish font-substitution.patch P1 Incomplete denylist → host-OS name leaks; over-block → last-resort starve → crash. Verify last-resort truly bypasses loop on Win.
font-disable-local-src-match @font-face src:local() side-channel + Android downloadable-font matching Gate-by-allowlist (allow persona locals, reject rest) rather than wholesale-disable: route local() unique-face lookup (kLocalUniqueFace) through IsFontAllowed(); disable AndroidDownloadableFontsMatching …/fonts/win/font_cache_skia_win.cc, …/public/common/features.cc, …/runtime_enabled_features.json5 Cromite feature-disable + camoufox font-hijacker.patch P1 Wholesale-disable observable (known local won't resolve on Win UA). Gate-by-allowlist preferred.
bundle-windows-metric-fonts Backs allowlisted names with on-disk files for Win-correct metrics on non-Win build/test host Ship libre clones (Arimo→Arial, Tinos→Times, Cousine→Courier, Carlito→Calibri, Caladea→Cambria, ComicRelief→Comic Sans, OpenSans) under third_party; register so names resolve to distinct-metric files third_party/clearcote_windows_fonts/, …/fonts/alternate_font_family.h, chrome/browser/BUILD.gn chromiumfish lean-windows-fonts.patch P2 On a real Win host this must be inert (prefer OS fonts) or it replaces authentic metrics = tell. Test-parity/cross-compile safety net only.

2.4 — navigator-ua-automation

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
navigator-ua-clienthints navigator.userAgent, UA header, platform, appVersion, userAgentData (full set), all 11 Sec-CH-UA*, high-entropy getHighEntropyValues split + Sec-CH-UA-Full-Version-List brand-list permutation order Route every value through fingerprinting::Current() reading clearcote-persona-version-freeze: GetUserAgentBrandList/GetProductAndVersion/GetUnifiedPlatform/GetPlatformForUAMetadata/GetPlatformVersion/GetCpuArchitecture/GetCpuBitness/GetUserAgentMetadata → persona; GetReducedNavigatorPlatform() → persona. Pin the GREASE brand-list permutation to the exact order the frozen persona Chrome build ships (captured in kPersonaChromeBuilds), and pin the low-vs-high-entropy hint partition to that build components/embedder_support/user_agent_utils.cc, …/execution_context/navigator_base.cc, …/frame/navigator_id.cc, …/frame/navigator_ua_data.cc, services/network/public/cpp/client_hints.cc chromiumfish navigator.patch + camoufox navigator-spoofing.patch P0 Kill-tell = UA-vs-CH or UA-vs-platform mismatch — eliminated by single-sourcing the frozen version. Critique-fix: GREASE/full-version-list ordering and the high-entropy split are now concretely pinned to a real build's permutation, not left as a "risk".
webdriver-automation-scrub navigator.webdriver→false; removes AutomationControlled auto-enable, Headless UA token, automation infobar Remove 4 AutomationControlled rows in runtime_features.cc (kEnableAutomation/kHeadless/kRemoteDebuggingPipe/--remote-debugging-port==0); neutralize probe::ApplyAutomationOverride; delete product.insert(0,"Headless"); suppress automation_infobar_delegate; re-enable WebShare on Linux build content/child/runtime_features.cc, …/frame/navigator.cc, components/embedder_support/user_agent_utils.cc, chrome/browser/ui/startup/automation_infobar_delegate.cc, chrome/renderer/chrome_content_renderer_client.cc BotBrowser removeHeadless.diff + chromiumfish navigator.patch + rebrowser/patchright P0 webdriver===false mandatory. cdc_ is NOT in the engine (chromedriver-only) — nothing to scrub. Don't over-suppress in a way that diverges from clean consumer Chrome.
hardware-concurrency-coherent (merged: navigator HC == screen HC; single owner of the one call site) navigator.hardwareConcurrency (+ Worker) Override NavigatorConcurrentHardware::hardwareConcurrency()Current().hardware_concurrency. Single patch, single call site (replaces adryfish --fingerprint-hardware-concurrency leaf read on the same line — do not stack a second writer). Covers main+worker+SW …/frame/navigator_concurrent_hardware.cc chromiumfish fingerprint-profile.patch + camoufox P0 Critique-fix: navigator and screen specs both targeted this one site — now ONE owner. Tier-correlated with RAM/GPU via profile.
device-memory-coherent (merged: navigator == screen memory half; single source) navigator.deviceMemory (+ Worker) + Sec-CH-Device-Memory Set ApproximatedDeviceMemory::physical_memory_mb_ from persona before CalculateAndSet… so JS value AND CH header agree from one source (not two getters). Respect kUpdatedDeviceMemoryLimitsFor2026 (2.0–32.0 GB clamp); value must survive clamp …/frame/navigator_device_memory.cc, …/common/device_memory/approximated_device_memory.cc chromiumfish fingerprint-profile.patch + Brave deviceMemory farbling P0 Critique-fix: three potential writers (navigator, screen, adryfish flag) collapsed to one bucket-source patch. JS-vs-CH divergence eliminated.
navigator-prototype-coherence Navigator/Element/window prototype shape + closed Shadow DOM At LocalWindowProxy::Initialize() delete ONLY props present in 149 build but absent in the frozen persona Chrome (clearcote-persona-version-freeze) — A/B-diffed vs that exact build, re-verified every uprev. Restore closed-shadow semantics so attachShadow({mode:'closed'}).shadowRoot===null …/bindings/core/v8/local_window_proxy.cc, …/dom/events/event_target.idl, …/dom/element.cc, …/dom/element.idl chromiumfish navigator.patch (A/B-verified) P2 Critique-fix: the A/B diff target is now pinned to the same frozen version the UA advertises, so the trimmed shape can never match "no real Chrome" via cross-phase drift. Keep delete minimal + main-world-only.
permissions-api-state-coherence (new) navigator.permissions.query() state for camera/mic/geolocation/notifications must agree with spoofed media-devices + geolocation Override PermissionStatus resolution so queried states are coherent with actual permission posture: pre-grant → prompt (not granted) for camera/mic/geo; post-grant → granted. Ensures the Permissions API does not contradict the device-list/geo spoofs …/permissions/permission_status.cc, …/modules/permissions/permissions.cc, content/browser/permissions/permission_controller_impl.cc clearcote-native (closes a cross-surface coherence gap) P1 Critique-fix (missing surface): a site cross-checks permissions.query({name:'camera'}) against enumerateDevices labels and getUserMedia outcome. If devices show populated labels but permission reads prompt, that's a contradiction — this patch keeps them consistent.

2.5 — screen-hardware

GPU note: the screen domain no longer writes the WebGL string. The single persona GPU (Intel UHD 770 / ANGLE-D3D11) lives in clearcote-persona-profile; the WebGL string is owned solely by webgl-strings-coherent (§2.1). Screen patches READ Current().gpu for coherence checks only.

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
screen-display-spoof screen.{width,height,availWidth,availHeight,availLeft,availTop,colorDepth,pixelDepth} + Screen Details {left,top} In screen.cc replace GetRect(false/true) + colorDepth()/pixelDepth() with persona fields; screen_detailed.cc left()/top()=0 (single screen); isExtended()=false …/frame/screen.cc, …/frame/screen.h, …/screen_details/screen_detailed.cc chromiumfish display.patch + camoufox screen-spoofing.patch P0 Keep Brave browsertest invariants: screen.width≥outerWidth, availWidth≥innerWidth, width-availWidth plausible taskbar delta (~40px).
screeninfo-source-override (backbone — lands with/just before leaf accessors) display::ScreenInfo (rect, available_rect, depth, DSF, is_extended) at single upstream source → screen.*, CSS device-*, outerWidth/Height, Screen Details all coherent Override ScreenInfo returned to Blink (renderer copy, after IPC) keyed on Current(). One override propagates everywhere ui/display/screen_info.h, …/css/media_values.cc, …/frame/local_dom_window.cc, content/renderer/render_widget chromiumfish display.patch + camoufox GetDeviceSurfaceDimensions P0 (promoted from P1; ordered before leaves) Critique-fix: the backbone must precede/accompany the leaf patches (screen-display-spoof, media-query-device-coherence) so the leaves never double-apply or disagree across a phase gap. Done right = lowest detection (everything agrees).
media-query-device-coherence CSS media features: device-width/height, resolution/DPR, color, dynamic-range, color-gamut, pointer/hover, prefers-* CalculateDeviceWidth/HeightCurrent().screen_* (closes the verified leak at media_values.cc:165-184 where they read screen_info.rect directly); force Primary/AvailablePointerType=kPointerFineType, HoverType=kHoverHoverType; route DPR/ColorBits(8)/Gamut(sRGB) through persona. prefers-color-scheme and prefers-reduced-motion are drawn per-persona from a weighted distribution (≈45% dark, ≈8% reduced-motion), NOT pinned canonical …/css/media_values.cc, …/media_values_cached.cc, …/media_values_dynamic.cc chromiumfish display.patch; CalculateDeviceWidth/Height fix is clearcote-original P0 The CalculateDeviceWidth/Height leak is the manifest's single best finding (verified). Critique-fix: forcing every persona to light/no-preference creates a linkable zero-dark-mode cohort — now per-persona weighted so the fleet's media-feature vector matches population stats.
hardware-concurrency-memory-coherence (folded into navigator hardware-concurrency-coherent + device-memory-coherent — see §2.4; this row is the tier-correlation rule, not a second writer) The FingerprintProfile draws (cores, RAM) from one kHwTiers tier so the merged navigator patches emit tier-correlated values. No screen-side write to the navigator getters …/common/fingerprinting/fingerprint_profile.cc chromiumfish kHwTiers P0 Critique-fix: eliminates the duplicate-writer conflict — screen contributes the distribution constraint, navigator owns the getter. Workers report the same value (no flicker).

2.6 — tls-network

Reframed per critique. Verified in stock 149: kTlsMldsaSignatures and kTLSTrustAnchorIDs are already FEATURE_DISABLED_BY_DEFAULT; ssl_client_socket_impl.cc:794 already takes the classical kVerifyPrefs[] (no ML-DSA) branch, and the Trust Anchor IDs block at line 871 is already dead. So the stock-149 JA4 already matches a real Chrome 149. These are therefore regression guards keyed to the persona version, not active "flips" — and hardcoding the classical branch is explicitly avoided because it would desync from a server-triggered PQ handshake a real 149 would complete. The real engineering work is the version-selectable group table coordinated with the UA generator (clearcote-persona-version-freeze).

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
tls-sigalgs-version-guard signature_algorithms ext coherence with the persona version Regression assertion, not a flip: verify the adryfish port did NOT toggle kTlsMldsaSignatures on. If the persona advertises ≤148, the classical-only branch (already default) is correct; if the persona advertises 149+ where a real 149 would offer ML-DSA on a triggering handshake, keep the feature at its real-149 default rather than hardcoding classical. Driven by the frozen persona version net/base/features.cc, net/socket/ssl_client_socket_impl.cc (no RESEARCH.md — citation removed; verified against stock 149) P1 (de-escalated from P0 — no-op as a "flip") Critique-fix: framing that ML-DSA presence is a "1-bit not-real-148 tell" was backwards — stock 149 also omits it by default. The risk is introducing a desync by hardcoding; this guard prevents that.
tls-trust-anchor-ids-guard Trust Anchor IDs ext coherence Regression assertion: confirm kTLSTrustAnchorIDs stays DISABLED and the SSL_set1_requested_trust_anchors block at ssl_client_socket_impl.cc:871 stays dead (it is gated on ssl_config_.trust_anchor_ids.has_value(), only populated when the disabled feature is on). No deletion needed in stock; assert no port regression net/socket/ssl_client_socket_impl.cc, net/ssl/ssl_config_service.cc, net/base/features.cc (verified against stock 149) P1 (de-escalated from P0) Critique-fix: stock 149 already omits this ext → already matches a real 149. Deleting code is unnecessary; the only failure mode is the adryfish port enabling it, which this guard catches.
tls-supported-groups-keyshare-version-table (the real TLS work) supported_groups (0x000a) + key_share (0x0033): curve list + PQ group (X25519MLKEM768 vs Kyber768Draft00 0x6399) Make kDefaultSSLSupportedGroups[] version-selectable, driven by clearcote-persona-version-freeze so {group_id, send_key_share} matches the advertised Chrome version exactly. Recent persona: MLKEM768; older persona: Kyber768Draft00 or drop PQ net/ssl/ssl_config_service.cc, net/socket/ssl_client_socket_impl.cc adryfish network-consistency + version table from persona P0 Hard dependency on the frozen persona version (critique: the "independent subtree, no seed dependency" framing was wrong here). Mismatched PQ group vs UA = precise JA4 tell. One version table, shared with the UA generator.
tls-cipher-grease-version-guard cipher_suites order, GREASE placement, brotli-only compress_certificate (0x001b), ALPS codepoint Verification/guard that BoringSSL defaults still reproduce the frozen persona Chrome: disable-grease-tls never set, no zstd/zlib cert compression, SSL_set_alps_use_new_codepoint consistent with persona version. Moved earlier (P1) so version coherence is enforced before, not after, the rest of the stack net/socket/ssl_client_socket_impl.cc, net/ssl/cert_compression.cc, net/base/features.cc persona version table + 149 default verification P1 (promoted from P2) Critique-fix: a P2 guard that catches UA-vs-TLS version drift lands too late. Promoted so the JA4-vs-UA check runs in the same band as the UA generator.
http2-settings-frame-pin H2 SETTINGS (the H2 fingerprint): values + on-wire order, WINDOW_UPDATE delta, pseudo-header order Lock AddDefaultHttp2Settings (ENABLE_PUSH=0, HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144) + kSpdySessionMaxRecvWindowSize=15MB (delta=15663105). Keep enable_http2_settings_grease=false. Pseudo-header order :method,:authority,:scheme,:path untouched net/http/http_network_session.cc, …/http_network_session.h, net/spdy/spdy_session.cc, net/spdy/spdy_http_utils.cc BotBrowser ADVANCED_FEATURES + 149 verification P0 Vanilla = Chrome. Risk = downstream patch/proxy reordering SETTINGS or enabling H2 grease → an H2 fingerprint no real Chrome produces. Lock-and-guard.
udp-over-socks5-tunnel (THE single UDP-over-SOCKS5 implementation — see §4 unify note) Egress of ALL browser UDP (STUN/TURN/media + QUIC/H3) via SOCKS5 UDP ASSOCIATE — proxy exit IP observed, not VPS IP New net::Socks5UdpClientSocket (RFC 1928 UDP ASSOCIATE; verified absent from stock socks5_client_socket.cc). TCP control via SOCKSConnectJob, parse BND.ADDR/PORT, wrap datagrams. One agreed wiring point: CreateDatagramClientSocket() dispatch (socket_manager.cc:315) when proxy is_socks5() + UDP-capable; QUIC via quic_session_pool.cc (parallel to existing quic_proxy_datagram_client_socket.cc). Flag-gated + auto-detect with fallback (disable QUIC + force ICE relay-only when proxy lacks UDP) net/socket/socks5_client_socket.cc, net/socket/socks_connect_job.cc, net/socket/udp_client_socket.cc, services/network/p2p/socket_udp.cc, services/network/p2p/socket_manager.cc, net/quic/quic_session_pool.cc BotBrowser ADVANCED_FEATURES (reimplemented); mirrors Chromium's own quic_proxy_datagram_client_socket.cc P0 Highest-value differentiator. Critique-fix: the two former specs (udp-over-socks5-quic-stun-tunnel + udp-over-socks5-tunnel) disagreed on wiring point — unified to the CreateDatagramClientSocket dispatch. Half-implemented → QUIC leaks real egress (worse than disabling); needs RTT/jitter parity testing.

2.7 — webrtc-proxy

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
webrtc-ip-handling-default-deny ICE gathering policy → forced disable_non_proxied_udp; host+srflx candidates with real VPS/LAN IP never gathered Patch ToWebRTCIPHandlingPolicy() fall-through → kDisableNonProxiedUdp (Cromite change); clamp switch at peer_connection_dependency_factory.cc:1074-1095 so kDefault can never set enable_nonproxied_udp/multiple_routes=true. Engine-constant …/peerconnection/webrtc_ip_handling_policy.cc, …/peerconnection/peer_connection_dependency_factory.cc, chrome/browser/ui/browser_ui_prefs.cc, chrome/browser/renderer_preferences_util.cc Cromite webRTC-do-not-expose-local-IP-addresses.patch + chromiumfish P0 Must ship with mdns-local-ip-coherence in the SAME phase (critique) — default-deny with no TURN yields zero candidates ≠ real Chrome's .local host candidate, an abnormal empty set is itself detectable in the gap.
mdns-local-ip-coherence (promoted to P0 — co-ships with default-deny) Local-IP host candidate → obfuscated <uuid>.local (real desktop-Chrome default) instead of raw LAN IP or absent Force-enable kWebRtcHideLocalIpsWithMdns + MdnsResponderAdapter so allow_mdns_obfuscation unconditionally true; deterministic .local UUID per-eTLD+1 (stable per-session/site, not cross-linkable); coordinate with default-deny so exactly one mDNS host candidate surfaces …/peerconnection/peer_connection_dependency_factory.cc, …/platform/p2p/mdns_responder_adapter.cc, …/platform/p2p/filtering_network_manager.cc Upstream kWebRtcHideLocalIpsWithMdns hardened always-on + Brave-seeded UUID P0 (promoted from P2) Critique-fix: shipping mDNS two phases after default-deny leaves an abnormal empty candidate set in between. Co-shipped, the candidate set is never empty — one plausible .local host candidate always emits.
socks5-credentialed-and-dns-in-proxy DNS egress (socks5h: ATYP=domain in-tunnel) + SOCKS5 username/password (RFC 1929) (1) socks5h mode (hostname forwarded, local resolution suppressed); resolved-IP branch only for bypassed hosts. (2) Offer method 0x02 in greeting (verified currently hardcoded {0x05,0x01,0x00} at line 255) + RFC 1929 sub-negotiation, creds from --proxy-server. (3) explicit loopback/RFC1918 bypass so local services resolve locally net/socket/socks5_client_socket.cc, net/socket/socks_connect_job.cc, net/proxy_resolution/configured_proxy_resolution_service.cc, net/base/proxy_server.h, net/dns/host_resolver_manager.cc, net/dns/context_host_resolver.cc BotBrowser CLI_FLAGS + adryfish README gap P0 Low. socks5h + credentialed SOCKS5 is standard/undetectable page-side; removes ISP-side DNS leak. Keep explicit loopback bypass.
ice-candidate-sdp-stats-spoof Residual IP/host in SDP a=candidate, RTCIceCandidate.address, foundation/relatedAddress, RTCIceCandidateStats.address Intercept candidate emission at rtc_ice_candidate.cc + onicecandidate dispatch + SDP munging; replace IPs with per-eTLD+1 farbling-seed value; sanitize getStats() in report builders. Preserve loopback/mDNS (mirror camoufox isSpecialIP). All three surfaces (SDP, .address, getStats()) rewrite coherently with the same value …/peerconnection/rtc_ice_candidate.cc, …/rtc_peer_connection.cc, …/rtc_session_description.cc, …/rtc_ice_candidate.idl camoufox webrtc-ip-spoofing.patch ported Gecko→Blink P1 With default-deny + mDNS co-shipped, this mainly hardens relay address + stats. Constant/implausible spoof IP is itself a tell — same per-eTLD+1 value across all three surfaces.

2.8 — geo-media-sensors

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
timezone-native-pin navigator/Date/Intl timezone + ICU country-code — pinned natively (no CDP override flag) Replace GetCurrentTimezoneId()/host_timezone_id_ in timezone_controller.cc with launch-time --clearcote-timezone so SetIcuTimeZoneAndNotifyV8 adopts it as genuine ICU default; force ICU adoption in icu_util.cc:328 + host-side timezone.cc. No HasTimeZoneOverride()==true tell …/core/timezone/timezone_controller.cc, base/i18n/icu_util.cc, base/i18n/timezone.cc BotBrowser timezone.diff + camoufox + CloakBrowser P0 High value, low risk if coherent. TZ must match geo IP + navigator.languages region + Intl locale. Avoid CDP-override signature.
locale-native-pin navigator.language(s), Accept-Language, Intl/ICU locale, date/number formatting Set ICU default via SetICUDefaultLocale(); feed GetApplicationLocaleInternalNonMac()/GetAcceptLanguages() from --clearcote-locale; build navigator.languages as primary+base. Accept-Language header byte-matches navigator.languages order base/i18n/rtl.cc, ui/base/l10n/l10n_util.cc, …/frame/navigator_language.cc camoufox locale-spoofing.patch + CloakBrowser P0 Header-vs-JS mismatch = classic emulation tell. Coherent with timezone+geo.
geolocation-coherent-spoof (permission-gated — no auto-grant) geolocation lat/long/accuracy Inject at device-service GeolocationImpl::SetOverride/OnLocationUpdate (not CDP path); do NOT auto-resolve the permission prompt — leave the prompt to default (deny/ask) like a real desktop user; only when permission is genuinely granted, return a position within proxy-IP geo with per-eTLD+1 jitter and Wi-Fi/IP-class accuracy (~tens–hundreds m), never GPS-class 1m on a GPS-less desktop services/device/geolocation/geolocation_impl.cc, …/public/cpp/geolocation/geoposition.h, content/browser/geolocation/geolocation_service_impl.cc camoufox geolocation-spoofing.patch P1 Critique-fix: auto-granting geo to every site is itself a behavioral tell (real desktop users overwhelmingly deny/ignore). Removed auto-grant; coordinate with permissions-api-state-coherence so permissions.query reads prompt until a real grant.
media-devices-farble (permission-gated labels) enumerateDevices() count, kind mix, labels, deviceId, groupId At MediaDevices::DevicesEnumerated(), synthesize a coherent desktop profile (1 mic, 1 cam, 1–2 speakers). Labels are EMPTY pre-getUserMedia grant (matching real Chrome); populate Integrated Webcam (VID:PID)-style labels ONLY after a genuine permission grant. deviceId/groupId = stable HMAC(seed, kind, index) constant per-site, rotate per-session; group_id pairs mic+speaker like real HW …/mediastream/media_devices.cc, content/browser/renderer_host/media/media_devices_manager.cc, media/capture/video/video_capture_device_descriptor.cc camoufox media-device-spoofing.patch + BotBrowser video_capture_device_descriptor.cc.diff P1 Critique-fix: populating labels pre-permission IS the exact leak the patch claimed to fix — now gated on permission state. Device count non-zero (zero on a "normal user" is also suspicious), IDs stable within session.
speech-voices-spoof (async empty-then-populated race preserved) speechSynthesis.getVoices() — full voice list matching OS persona (Win SAPI + Edge voices, not Linux espeak) Intercept SpeechSynthesis::OnSetVoiceList() (cross-platform choke above tts_win.cc), replace mojom vector with persona table (Microsoft David/Zira/Mark Desktop + Edge natural). Reproduce real Chrome's async behavior: first synchronous getVoices() returns empty, then a voiceschanged event fires and populates — do not return a fully-populated list on the first synchronous call. Mark localService=true, correct default voice for locale …/speech/speech_synthesis.cc, …/speech/speech_synthesis_voice.cc camoufox speech-voices-spoofing.patch + Brave speech farbling P1 Critique-fix: a too-eager populated list on first sync call is itself a tell (real Chrome returns empty then fires voiceschanged). List must match the real Win build of the frozen persona Chrome (names/count/order/lang).
battery-keyboard-sensors-desktop (per-persona battery variance) getBattery() (AC desktop), keyboard.getLayoutMap(), generic-sensor absence Port chromiumfish: KeyboardLayout drops service IPC, fixed map per locale; Sensor/DeviceMotion/Orientation report no available sensor (events never fire / construction rejects) — not zeroed readings. BatteryManager: charging=true but level drawn per-persona from a weighted distribution (e.g. 0.78–1.0 with realistic spread), dischargingTime=Infinity, chargingTime plausible — NOT every persona pinned to exactly level=1.0/chargingTime=0 …/battery/battery_manager.cc, …/keyboard/keyboard_layout.cc, …/sensor/sensor.cc, …/device_orientation/device_orientation_controller.cc chromiumfish device-sensors.patch + BotBrowser sensor emulation P2 Critique-fix: a fleet where EVERY machine reports level=1.0/chargingTime=0 is a linkable cohort. Per-persona battery variance breaks the cohort. Keep getBattery present (Chrome desktop exposes it). Fake non-empty sensors on "desktop" = contradiction → sensor-absent.
network-info-coherence navigator.connection.{effectiveType,rtt,downlink,downlinkMax,saveData,type} + Sec-CH-* Override getters in network_information.cc to stable persona values (type='wifi', effectiveType='4g', rounded rtt, plausible downlink); emit matching CH headers (ECT/RTT/Downlink). rtt/downlink carry realistic per-session variance, not a constant int …/netinfo/network_information.cc BotBrowser --bot-network-info-override P2 Values plausible for claimed geo + match Sec-CH-RTT/Downlink/ECT headers (JS-vs-header mismatch = tell). Prevents datacenter RTT contradicting consumer-broadband persona.
math-libm-ulp-coherence (new) Math.sin/cos/tan/exp/log/atan2 ULP differences across libm implementations (classic OS/arch tell) Route a small set of transcendental ops through a single deterministic implementation bundled with the build (or a fixed correctly-rounded polynomial) so the ULP fingerprint matches the claimed OS regardless of the host libm the binary was cross-compiled against. Apply only to the JS-visible Math bindings, not internal layout math …/bindings/core/v8/… Math intrinsic shims, v8/src/base/ieee754.cc (bundled impl), …/platform/wtf/math_extras.h clearcote-native (closes a classic libm tell) P2 Critique-fix (missing surface): Linux-cross-compiled libm produces ULP patterns distinct from a real Windows Chrome — a per-op Math.* ULP probe flags it. Bundling one deterministic implementation makes the ULP signature OS-coherent. Scope tightly to avoid perf/correctness regressions.
timing-chrome-parity-assert (regression assertion, not an active patch) performance.now()/event timestamps + video-frame callback — exact Chrome defaults (100µs/5µs) Verify, don't pin: time_clamper.h:23-24 already defines kCoarseResolutionMicroseconds=100 / kFineResolutionMicroseconds=5 in stock 149. Add a regression assertion that the final binary still reports these (and 100µs in video RVFC); only patch if the adryfish port regressed them. Reject any 100ms/1ms RFP clamp …/core/timing/time_clamper.h, …/video_rvfc/video_frame_callback_requester_impl.cc chromiumfish timing-misc.patch (downgraded to assertion) P2 (de-escalated from P0) Critique-fix: stock 149 already has the correct values — shipping it as an active P0 patch over unchanged values risks future-uprev drift. It is a regression check on the adryfish port, not a P0 surface patch. interactive challenges / fingerprinters performance.now()%1 distribution still passes because the value is already correct.

2.9 — degoogle-humanize

Patch Spoofs Technique Real target files Source ref Pri Detection-risk note
degoogle-kill-component-registrations Residual startup connects to update.googleapis.com (Widevine, OptimizationHints, TrustTokenKeyCommitments, FirstPartySets) Stub RegisterComponentsForUpdate body (remove Register*Component(cus) calls) or guard behind always-false flag; point kUpdaterJSONDefault/FallbackUrl at localhost sink chrome/browser/component_updater/registration.cc, components/component_updater/component_updater_url_constants.cc ungoogled rewrites host but doesn't stop registration — clearcote closes it P0 Low. No JS surface. Widevine bundled at build (enable_widevine=true) — verify DRM video plays from bundled CDM.
degoogle-disable-optimization-guide Fetch to optimizationguide-pa.googleapis.com (GetHints/GetModels) Flip kOptimizationHintsFEATURE_DISABLED_BY_DEFAULT; short-circuit fetcher; ensure killed component can't re-arm components/optimization_guide/core/optimization_guide_features.cc, …/optimization_guide_constants.cc, chrome/browser/component_updater/registration.cc ungoogled disable-ai.patch extended P0 Low. Pure network-leak removal.
verify-degoogle-gcm-disabled Ensures ungoogled disable-gcm.patch present — no MCS to mtalk.google.com:5228 Confirm disable-gcm.patch in apply series (empties GCMClientImpl::Start, early-return InitializeMCSClient, strips mcs_client_->Login). Optionally don't instantiate GCMDriver/GCMProfileService components/gcm_driver/gcm_client_impl.cc, google_apis/gcm/engine/gservices_settings.cc ungoogled disable-gcm.patch P0 None — verification/inclusion of a battle-tested ungoogled patch.
degoogle-disable-network-time Background time-sync to clients2.google.com/time/1/current Flip non-Android kNetworkTimeServiceQueryingFEATURE_DISABLED_BY_DEFAULT. Reinforces TZ coherence (time from proxy-coherent system clock) components/network_time/network_time_tracker.cc clearcote-native (ungoogled residual) P1 Low.
degoogle-disable-captive-portal-and-variations connectivitycheck.gstatic.com/generate_204 probe + variations/Finch seed fetch Disable captive_portal feature / sink the URL; disable VariationsService so GetVariationsServerURL never invoked components/captive_portal/core/captive_portal_detector.cc, components/variations/service/variations_service.cc, components/variations/variations_switches.cc ungoogled flags.gn (partial) + clearcote-native P1 Low. Variations off = reproducible/deterministic build.
humanize-browser-level-click (primary human-click path) Browser.humanizedClick/humanizedClickSelector CDP methods — human click WITHOUT kFromDebugger + WITHOUT Page debugger attach; cross-origin OOPIF correct New browser-level CDP. HumanizedClickRunner state machine (base::OneShotTimer): bezier path, then kMouseDown / dwell 40–110ms / kMouseUp. Construct WebMouseEvent with kNoModifiers (no kFromDebugger), route via RenderWidgetHostInputEventRouter::RouteMouseEvent. Parameterize duration/σ/bow from the per-eTLD+1 seed (not fixed constants). Selector variant resolves center via isolated-world JS …/protocol/browser_handler.cc, …/browser_handler.h, content/browser/devtools/protocol_config.json, …/devtools_protocol/domains/Browser.pdl chromiumfish humanized-input.patch + CloakBrowser presets P1 kNoModifiers is the key win — isTrusted===true, no debugger marker. Drop macOS cursor-overlay files (Win target) + agentRunTask (out of scope). Seed-parameterized trajectory avoids deterministic profiling.
humanize-cdp-mouse-bezier (fallback for direct CDP mouse.click) Transparent humanizer for Playwright/Puppeteer mouse.click() — curved/jittered/eased trajectory In DispatchMouseEvent, on mousePressed read last_cursor_position_, build cubic bezier (control pts ±15% perpendicular), EaseInOut smoothstep, Box-Muller jitter (σ from seed, clamp 2.0px), ~120 samples/s for ~0.5s via ForwardMouseEvent. Duration/σ/bow seeded per-eTLD+1 content/browser/devtools/protocol/input_handler.cc, …/input_handler.h chromiumfish humanized-input.patch P1 This path still carries kFromDebugger via CreateWebMouseEventprefer humanize-browser-level-click; keep this only for clients that drive the standard Input.dispatchMouseEvent. Parameterized from seed.
humanize-cdp-keyboard-timing CDP keyboard cadence: per-key gaussian dwell+flight, word-boundary pauses Extend DispatchKeyEvent (or browser-level Input.humanizedType): schedule keyDown/char/keyUp via OneShotTimer, gaussian dwell (~60–120ms) + flight (~80–200ms), rarer long pauses. Reuse Box-Muller. Params seeded per-eTLD+1. Non-kFromDebugger path content/browser/devtools/protocol/input_handler.cc, …/input_handler.h chromiumfish humanized-input.patch + CloakBrowser/camoufox P2 Fixed distribution profilable over long sessions — seeded per-eTLD+1.
humanize-synthetic-gesture-unmark Removes kFromDebugger from SyntheticGesture path (scroll/touch/pointer-action) Drop modifiers |= kFromDebugger in synthetic_mouse_driver.cc (58,92,124) + synthetic_smooth_move_gesture.cc (309) (or flag-gate). No renderer-side kFromDebugger consumer; does NOT gate a CDP callback ack content/common/input/synthetic_mouse_driver.cc, …/synthetic_smooth_move_gesture.cc, content/browser/renderer_host/input/synthetic_gesture_target_aura.cc clearcote-native P2 Pure marker removal on a path with no renderer consumer. Verify scroll-gesture completion acks still resolve.

3. Build Order / Phasing

Phase P0 — Foundation (unified seed + persona + GPU resolution + coherence + version freeze + TLS group table). Nothing else compiles meaningfully without these.

P0a — Unified seed/persona engine + audit gate (must land first, strict order):

  1. clearcote-seed-switch → 2. clearcote-seed-forward (subsumes persona-seed-plumbing) → 3. clearcote-persona-profile (subsumes persona-fingerprint-profile + fingerprint-profile-engine, pins the one persona GPU) → 4. clearcote-persona-version-freeze → 5. clearcote-etld-token → 6. clearcote-mojom-token → 7. clearcote-session-cache (== farbling-seed-core) → 8. clearcote-worker-seed

This is a linear dependency chain, and it is one unified engine, not three: the session token (canvas/audio/font noise) and the persona profile (UA/screen/HW/GPU/version tuples) are two readers of the same --clearcote-seed root. The frozen persona Chrome version (#4) is a P0 artifact consumed identically by the UA/CH generator, the TLS group table, the prototype A/B diff, and the JA4 guards — frozen once, never re-derived.

P0a-gate — the audit gate runs immediately after the adryfish replay, BEFORE any surface:

  • Replay adryfish 142→149, then land clearcote-disable-global-fingerprint with a build-time static_assert that fails compilation if any surface still references the adryfish global-seed accessor. The cross-site supercookie must be impossible to ship in P0, not patched in P1.

P0b — P0 surfaces (depend on session-cache #7 and/or persona profile #3):

  • canvas2d-farble-perturb, webgl-strings-coherent (SOLE UNMASKED+GL_VERSION owner, reads the one persona GPU)depend on seed + persona
  • audio-buffer-farble, audio-farbling-helper (one shared seed for the whole audio path)depend on seed
  • font-allowlist-win-persona (per-persona variant), font-access-api-filter, font-metrics-jitter-pertldjitter depends on seed; allowlist depends on persona --lang
  • navigator-ua-clienthints (reads frozen version + pins GREASE/full-version-list order), webdriver-automation-scrub, hardware-concurrency-coherent (merged single owner), device-memory-coherent (merged single source)depend on persona
  • screeninfo-source-override (backbone — lands before/with the leaves), then screen-display-spoof, media-query-device-coherence (closes the CalculateDeviceWidth/Height leak; per-persona prefers-*)depend on persona
  • timezone-native-pin, locale-native-pinlaunch-config
  • TLS subtree (HARD dependency on the frozen persona version #4): tls-supported-groups-keyshare-version-table (the real work), http2-settings-frame-pin
  • WebRTC/proxy: webrtc-ip-handling-default-deny + mdns-local-ip-coherence (co-shipped, same phase), udp-over-socks5-tunnel (the ONE UDP-over-SOCKS5 impl), socks5-credentialed-and-dns-in-proxy
  • de-Google P0: verify-degoogle-gcm-disabled, degoogle-kill-component-registrations, degoogle-disable-optimization-guide

Phase P1 — Surfaces & hardening

webgl-params-coherence (+ drawingBuffer/MSAA) · webgl-readpixels-farble (+ FLOAT/HALF_FLOAT epsilon) · webgpu-adapter-farble · audio-analyser-farble (shares audio seed) · audio-context-metadata-spoof (asserts offline render rate) · font-fallback-mac-skip · font-disable-local-src-match · permissions-api-state-coherence · tls-sigalgs-version-guard (regression guard) · tls-trust-anchor-ids-guard (regression guard) · tls-cipher-grease-version-guard (promoted) · ice-candidate-sdp-stats-spoof · geolocation-coherent-spoof (permission-gated, no auto-grant) · media-devices-farble (permission-gated labels) · speech-voices-spoof (async empty-then-populated) · degoogle-disable-network-time · degoogle-disable-captive-portal-and-variations · humanize-browser-level-click (primary) · humanize-cdp-mouse-bezier (fallback)

Phase P2 — Defense-in-depth & polish

gpu-strings-source-coherence · offline-audio-render-isolation (shares audio seed; fallback only) · element-rect-jitter-pertld · bundle-windows-metric-fonts · navigator-prototype-coherence (A/B-diff vs frozen version) · battery-keyboard-sensors-desktop (per-persona battery variance) · network-info-coherence · math-libm-ulp-coherence · timing-chrome-parity-assert (regression assertion) · humanize-cdp-keyboard-timing · humanize-synthetic-gesture-unmark

Patches by dependency (explicit)

Hard seed dependency (read SessionCache/per-eTLD+1 token): canvas2d-farble-perturb, webgl-strings-coherent (noise-mode extensions), webgl-readpixels-farble, webgpu-adapter-farble (per-site axis), audio-buffer-farble, audio-analyser-farble, audio-farbling-helper, offline-audio-render-isolation, font-metrics-jitter-pertld, element-rect-jitter-pertld, media-devices-farble (IDs), geolocation-coherent-spoof (jitter), ice-candidate-sdp-stats-spoof, mdns-local-ip-coherence (UUID), humanize-* (param seeding). Persona-profile dependency (read fingerprinting::Current()): all of navigator-ua-automation, screen-hardware, webgl-strings-coherent (the one GPU), webgl-params-coherence, audio-context-metadata-spoof, font-allowlist-win-persona, speech-voices-spoof, network-info-coherence, battery-keyboard-sensors-desktop. Frozen-version dependency (read clearcote-persona-version-freeze #4): navigator-ua-clienthints (UA/CH + GREASE order), tls-supported-groups-keyshare-version-table, tls-cipher-grease-version-guard, tls-sigalgs/trust-anchor-ids-guard, navigator-prototype-coherence, http2-settings-frame-pin (verification). No seed/persona dependency: all de-Google patches, webdriver-automation-scrub, humanize-synthetic-gesture-unmark, socks5-credentialed-and-dns-in-proxy, timing-chrome-parity-assert, math-libm-ulp-coherence.


4. Composition with adryfish

clearcote ports adryfish/fingerprint-chromium 142→149 as the base, then layers the per-eTLD+1 engine and surface patches on top. The governing rule: where adryfish already touches a surface, we UPGRADE its noise to per-eTLD+1 rather than double-patch — two competing spoof paths on one getter = guaranteed incoherence. clearcote-disable-global-fingerprint is the build-time audit gate that enforces this (a compile error, not a later-phase review).

Surfaces adryfish ALREADY covers → UPGRADE (do not double-patch)

Surface adryfish coverage clearcote action
Global fingerprint seed --fingerprint=<int32> single global seed; --fingerprint-platform/-brand/-hardware-concurrency Keep the int as SESSION TOKEN. Redirect FingerprintSeed() → per-document SessionCache seed (clearcote-disable-global-fingerprint, build-asserted). Widen int32→uint64 (clearcote-seed-switch).
hardwareConcurrency --fingerprint-hardware-concurrency leaf override in navigator_concurrent_hardware.cc hardware-concurrency-coherent is the single owner of that one call site — replaces the flag-read with Current().hardware_concurrency. Navigator and screen specs are merged into this one patch (no second writer).
deviceMemory adryfish fingerprint flag path device-memory-coherent single-sources through ApproximatedDeviceMemory so JS value and CH header agree. Navigator + screen + adryfish flag collapse to one bucket-source patch.
navigator.platform / UA platform --fingerprint-platform Route through persona Current() (the frozen version) so UA/CH/platform single-sourced (navigator-ua-clienthints).
WebGL info 149 base ships kSpoofWebGLInfo + kSpoofWebGLRendererParam.Get() (verified at webgl base 4104-4105); GL_RENDERER"WebKit WebGL" Re-point the existing hook at the one persona GPU (webgl-strings-coherent — SOLE owner). Canvas-domain string patch and the former screen-domain webgl-gpu-coherence are one patch, one GPU, one owner — they no longer both write the getter.
audio toggle audio first-class --disable-spoofing toggle (Chrome 144+) Map clearcote's per-eTLD+1 audio level onto the toggle; replace global-seed fudge with per-token fudge. One shared seed across buffer/analyser/offline.
GPU vendor/renderer flags --fingerprint-gpu-vendor/-renderer REMOVED in Chrome 144 (now --disable-spoofing=gpu) Don't resurrect flags — go through kSpoofWebGLInfo.

Surfaces clearcote adds NET-NEW (adryfish has no coverage)

Surface Why net-new
Per-eTLD+1 derivation (clearcote-etld-token, -mojom-token, -session-cache, -worker-seed) adryfish has only a global seed — the per-site coherence engine is new (Brave-modeled).
Frozen persona Chrome version (clearcote-persona-version-freeze) adryfish has no single version artifact shared across UA/TLS/prototype/JA4.
Canvas LSB perturbation (canvas2d-farble-perturb, webgl-readpixels-farble) adryfish doesn't do Brave-style HMAC+LFSR content-dependent perturbation.
WebGL2 inline GL_VERSION/GL_SHADING_LANGUAGE_VERSION rewrite adryfish's WebGL hook only covers UNMASKED; the inline backend version string is uncovered (now in webgl-strings-coherent).
WebGPU adapter + GPU-process source strings Newer surface, no adryfish coverage.
Audio buffer/analyser/offline farbling adryfish only has the coarse toggle.
Font allowlist (distribution) + metrics/rect jitter + Font Access filter Entirely new (Cromite/Camoufox-modeled).
TLS group-table + H2 SETTINGS pinning adryfish notes network-consistency conceptually but ships no group/H2 pinning.
UDP-over-SOCKS5 + WebRTC leak shield + socks5h DNS + mDNS coherence adryfish README notes SOCKS lacks password auth + no UDP — all net-new.
Permissions-API coherence, math/libm ULP, geo/media permission-gating New coherence surfaces the critique identified.
Timezone/locale/geo native pin, media-devices, speech voices, sensors, network-info, prototype coherence, automation scrub, de-Google residual kills, humanize input New (BotBrowser/Camoufox/ungoogled-modeled).

Unify-don't-duplicate (build ONCE — resolved per critique)

These were listed redundantly across domain JSONs; each is one single-owner patch, finalized before build:

  • farbling-seed-core (canvas) == clearcote-session-cache (core) == fingerprint-profile-engine seed-half (screen) → one seed engine.
  • persona-fingerprint-profile (navigator) == fingerprint-profile-engine persona-half (screen) → one clearcote-persona-profile (which also pins the one persona GPU).
  • persona-seed-plumbing (screen) == clearcote-seed-forward (core) → one forward patch.
  • hardware-concurrency-coherent (navigator) == hardware-concurrency-memory-coherence HC-half (screen) → one owner of navigator_concurrent_hardware.cc.
  • device-memory-coherent (navigator) == memory-half (screen) → one bucket-source patch.
  • webgl-strings-farble/webgl-params-coherence (canvas) == webgl-gpu-coherence (screen) → webgl-strings-coherent is the SOLE writer of the UNMASKED/GL_VERSION getter, pinned to the one persona GPU. Screen reads Current().gpu; it does not write the getter. This resolves the NVIDIA-vs-Intel contradiction: there is exactly one GPU, Intel UHD 770 / ANGLE-D3D11 (or ANGLE-SwiftShader headless).
  • udp-over-socks5-quic-stun-tunnel (tls-network) == udp-over-socks5-tunnel (webrtc) → one RFC 1928 UDP-ASSOCIATE implementation, one wiring point: CreateDatagramClientSocket() dispatch at socket_manager.cc:315.

5. Coverage Matrix

Every detection surface × phase × fork modeled on. U = upgrade of adryfish base; N = net-new clearcote patch.

Detection surface P0 P1 P2 Fork modeled on adryfish
Per-eTLD+1 seed engine Brave (BraveSessionCache) U
Cross-process seed coherence ChromiumFish (persona-seed) U
Frozen persona Chrome version (1 artifact) clearcote-native N
Worker/worklet seed propagation Brave N
Canvas 2D readback (getImageData/toDataURL/toBlob) Brave (PerturbPixels) N
OffscreenCanvas / worker canvas Brave N
measureText / text metrics Brave + Camoufox N
getBoundingClientRect / non-text rects Camoufox (re-keyed) N
WebGL UNMASKED vendor/renderer (one GPU) Camoufox (coherent, Intel UHD 770) U
WebGL2 inline GL_VERSION/GLSL_VERSION leak clearcote-native (closes backend leak) U
WebGL numeric param limits Camoufox + BotBrowser N
WebGL drawingBuffer / MSAA samples Camoufox + BotBrowser N
WebGL extensions list Brave (WebGLFarbledExtensionHandler) N
WebGL readPixels (UNSIGNED_BYTE) Brave (perturb, not block) N
WebGL readPixels (FLOAT/HALF_FLOAT, epsilon) clearcote-native (no skip path) N
WebGL contextAttributes / shaderPrecision BotBrowser + Camoufox N
WebGPU GPUAdapterInfo (one GPU, real strings) Brave (BraveScrubWebGpuAdapterInfo) N
GPU-process strings (chrome://gpu, GL_VERSION) Novel (one persona GPU record) N
Renderer-string ↔ readPixels-hash coherence clearcote-native (render-backend rule) N
AudioBuffer getChannelData/copyFromChannel Brave (audio_buffer macros) U
AnalyserNode live readers (×4, shared seed) Brave N
AudioContext sampleRate (== real render rate) ChromiumFish + Camoufox N
OfflineAudioContext render isolation (shared seed) ChromiumFish (IsolateAudioSamples) N
Font list (CSS family-probing, distribution) Cromite + ChromiumFish N
Font Access API (queryLocalFonts) Cromite (extended) N
Font metrics jitter (HarfBuzz advances) Camoufox (re-keyed per-eTLD+1) N
Mac/Linux font-name cascade leak ChromiumFish N
@font-face src:local() side-channel Cromite + Camoufox N
Bundled metric fonts (test-parity) ChromiumFish (lean-windows-fonts) N
navigator.userAgent / UA header ChromiumFish + Camoufox U
userAgentData / Sec-CH-UA* (×11) ChromiumFish U
getHighEntropyValues split + full-version-list order clearcote-native (pinned to real build) N
navigator.platform / appVersion ChromiumFish + Camoufox U
navigator.webdriver / AutomationControlled BotBrowser + ChromiumFish + patchright N
Headless UA token BotBrowser (removeHeadless) N
Automation infobar / WebShare congruence BotBrowser + ChromiumFish N
cdc_ globals (not in engine — chromedriver-only)
navigator.hardwareConcurrency (+workers, 1 owner) ChromiumFish + Camoufox U
navigator.deviceMemory (+CH header, 1 source) ChromiumFish + Brave U
Permissions API state coherence clearcote-native N
Prototype shape / closed Shadow DOM (vs frozen ver) ChromiumFish (A/B-verified) N
screen.width/height/avail*/colorDepth ChromiumFish + Camoufox U
ScreenInfo source coherence (backbone, pre-leaves) ChromiumFish + Camoufox N
CSS @media device-width/height (leak — best finding) clearcote-original N
CSS pointer/hover/resolution/color-gamut ChromiumFish U
CSS prefers-* (per-persona weighted, not pinned) clearcote-native (cohort fix) N
outerWidth/Height / devicePixelRatio ChromiumFish (via ScreenInfo) U
TLS signature_algorithms (version guard) regression assertion (stock-149 verified) N
TLS Trust Anchor IDs (version guard) regression assertion (stock-149 verified) N
TLS supported_groups / key_share (version table) persona version table U
TLS cipher/GREASE/cert-compression/ALPS (guard) persona version + 149 verification U
HTTP/2 SETTINGS fingerprint + pseudo-header order BotBrowser + ChromiumFish N
QUIC/H3 version + transport params (149 default lock) N
WebRTC ICE IP-handling (host/srflx leak) Cromite + ChromiumFish N
mDNS .local host candidate (co-ships w/ deny) Upstream hardened + Brave seed N
UDP-over-SOCKS5 (QUIC/STUN tunnel, 1 impl) BotBrowser (reimplemented) N
SOCKS5 credentials (RFC 1929) BotBrowser + adryfish gap N
DNS-through-proxy (socks5h) BotBrowser N
ICE candidate / SDP / RTCStats IP (coherent) Camoufox (Gecko→Blink) N
Timezone (navigator/Date/Intl/country) BotBrowser + Camoufox + CloakBrowser N
Locale (languages/Accept-Language/Intl) Camoufox + CloakBrowser N
Geolocation (permission-gated, no auto-grant) Camoufox N
enumerateDevices (labels permission-gated) Camoufox + BotBrowser + Brave N
speechSynthesis.getVoices() (async race preserved) Camoufox + Brave N
getBattery (per-persona variance) / keyboard / sensors ChromiumFish + BotBrowser N
navigator.connection (network-info + CH) BotBrowser N
Math/libm transcendental ULP (OS/arch tell) clearcote-native N
performance.now() / timing resolution (assertion) ChromiumFish (regression assert, stock-149) U
GCM / mtalk.google.com:5228 ungoogled (disable-gcm) N
Component updater (update.googleapis.com) clearcote-native N
Optimization guide fetch ungoogled-adjacent extended N
Network-time probe clearcote-native N
Captive-portal / variations seed ungoogled partial + clearcote-native N
Browser-level human click (isTrusted, OOPIF, seeded) ChromiumFish + CloakBrowser N
CDP mouse input (kFromDebugger / trajectory, seeded) ChromiumFish (humanized-input) N
CDP keyboard cadence (seeded) ChromiumFish + CloakBrowser + Camoufox N
Synthetic-gesture kFromDebugger marker clearcote-native N

Phase totals: P0 = 34 surfaces (unified seed/persona/version + GPU-coherence + TLS group-table/H2 + WebRTC deny+mDNS + de-Google leak kills + automation scrub) · P1 = 24 surfaces · P2 = 13 surfaces.


Coherence invariants the build must satisfy (acceptance gates):

  • One GPU end to end: WebGL UNMASKED renderer == WebGL2 GL_VERSION == WebGL param limits == WebGPU adapter == chrome://gpu == the readPixels hash cluster == spoofed OS — all describe the single persona GPU (Intel UHD 770 / ANGLE-D3D11, or ANGLE-SwiftShader on a headless runtime). No renderer-string-vs-readback-hash mismatch.
  • screen.width ≥ outerWidth ≥ innerWidth; screen.width − availWidth = plausible taskbar delta; matchMedia('(device-width:${screen.width}px)') true (Brave screen_farbling_browsertest); ScreenInfo backbone agrees with every leaf accessor.
  • One frozen version: navigator.userAgent Chrome version == Sec-CH-UA == full-version-list/GREASE order == TLS group table == prototype shape's claimed version == JA4 guards — all read clearcote-persona-version-freeze.
  • Timezone == geo-IP city == navigator.languages region == Intl locale.
  • hardwareConcurrency == deviceMemory == GPU tier == screen tier, drawn from one kHwTiers tier; workers report the same value (no flicker).
  • Permission coherence: enumerateDevices labels empty pre-grant; permissions.query reads prompt until a genuine grant; geolocation never auto-granted and never GPS-class accuracy on desktop.
  • No collapsed cohorts: font set, prefers-color-scheme, and battery level are per-persona weighted, not single constants.
  • Same eTLD+1 → byte-stable values all session (no flicker); different eTLD+1 → unlinkable; audio offline == live == buffer (one shared seed).
  • performance.now() resolution byte-matches real Chrome 149 (100µs/5µs, regression-asserted); navigator.webdriver === false; no UDP egress outside the SOCKS5 tunnel.
  • Build-time: compilation fails if any surface references the adryfish global-seed accessor (supercookie impossible in P0).

Detector-critique adjustments

What changed from the draft, and why:

  1. Resolved the GPU identity contradiction (headline fix). The draft advertised "NVIDIA RTX 3060" in §2.1 but pinned "Intel UHD 770" in §2.5/§4, while admitting readPixels runs on the real SwiftShader/ANGLE backend — guaranteeing a renderer-string-vs-pixel-hash mismatch, the strongest GPU tell. Now there is exactly one persona GPU (Intel UHD 770 / ANGLE-D3D11 for real-Windows deploy, ANGLE-SwiftShader for headless runtime), defined once in clearcote-persona-profile, written by a single owner (webgl-strings-coherent), with an explicit render-backend rule and an acceptance gate that the renderer string and readback hash come from the same GPU.

  2. Reframed the TLS section from no-op "flips" to version-keyed guards. Verified that kTlsMldsaSignatures and kTLSTrustAnchorIDs are already FEATURE_DISABLED_BY_DEFAULT in stock 149, so the stock JA4 already matches a real 149 and the "1-bit not-real-148 tell" framing was backwards. The sigalgs/trust-anchor patches became P1 regression assertions (verify the port didn't toggle them on; do not hardcode the classical branch, which would desync from a server-triggered PQ handshake a real 149 completes). The genuine work — the version-selectable supported_groups/key_share table coordinated with the UA generator — stays P0, and tls-cipher-grease-version-guard was promoted P2→P1 so version coherence is enforced early.

  3. Demoted timing-chrome-parity to a regression assertion. time_clamper.h already defines 100µs/5µs in stock 149; shipping an active patch over unchanged values risks future-uprev drift. Now a P2 assertion that the binary still reports the stock values (patch only if the adryfish port regressed them).

  4. Promoted the audit gate into P0 with a build-time assertion. clearcote-disable-global-fingerprint was P1/deferred while P0 surfaces read the seed — a phase ordering that could ship the cross-site supercookie in P0. It now lands in P0a immediately after the adryfish replay, with a static_assert that fails compilation if any surface still references the global-seed accessor.

  5. Unified the duplicate patches before build (not as a coordination note). One seed engine, one persona profile (which now owns the single GPU), one navigator_concurrent_hardware.cc owner, one deviceMemory bucket-source, one WebGL UNMASKED writer, and one UDP-over-SOCKS5 implementation with a single agreed wiring point (CreateDatagramClientSocket at socket_manager.cc:315) — resolving the two conflicting file lists in the draft.

  6. Added the WebGL2 inline GL_VERSION/GL_SHADING_LANGUAGE_VERSION leak. Verified that webgl2_rendering_context_base.cc:4789 reads the real backend version string inline (UNMASKED correctly falls through to base at 4987, so the draft's fear there was misplaced). webgl-strings-coherent now also overrides the inline version strings.

  7. Gated permission-dependent surfaces to match real Chrome. media-devices-farble now returns empty labels pre-getUserMedia (populating them was the exact leak it claimed to fix); geolocation-coherent-spoof no longer auto-grants the prompt and never uses GPS-class accuracy on a desktop; added permissions-api-state-coherence so permissions.query agrees with the device/geo posture; and speech-voices-spoof preserves Chrome's async empty-then-voiceschanged race instead of returning a populated list on the first synchronous call.

  8. Broke linkable single-constant cohorts. Font allowlist became a per-persona weighted distribution (stock / +Office / +Adobe / +OEM) instead of one too-clean canonical set; prefers-color-scheme/prefers-reduced-motion are drawn per-persona (~45% dark) instead of pinned; battery level carries per-persona variance instead of a fleet-wide 1.0.

  9. Added missing surfaces. element-rect-jitter-pertld (non-text getBoundingClientRect/transform precision, shared seed with text jitter), webgl-readpixels-farble FLOAT/HALF_FLOAT epsilon path (no skip), WebGL drawingBuffer/MSAA params, math-libm-ulp-coherence (transcendental ULP OS tell), getHighEntropyValues split + full-version-list/GREASE ordering pinned to a real build, OfflineAudioContext render-rate assertion (not just the getter), and the permissions-api coherence above.

  10. Fixed cross-phase ordering. Pinned the persona Chrome version as a single frozen P0 artifact (clearcote-persona-version-freeze) consumed identically by UA/CH, TLS, prototype-diff, and JA4 guards — eliminating version drift across phases; made screeninfo-source-override (the backbone) land before/with the leaf screen accessors; and co-shipped mdns-local-ip-coherence with webrtc-ip-handling-default-deny in P0 so the candidate set is never abnormally empty between phases.

  11. Removed unverifiable citations. All RESEARCH.md §4.1 references were dropped (the file does not exist on the box); the affected claims are re-grounded against the stock-149 tree directly.