Repository navigation
feat(browser)!: stealth Chromium 154 with coherent personas - #124
Merged
Merged
Conversation
…the build Rebase 0037 (upstream now enables kIncognitoStaticStorageQuota) and regenerate 0010/0011 with real context. build-linux.sh: split into BROWSER_STAGE=prep|build so both targets build concurrently from one tree; fetch every non_git_source CIPD/GCS dep from DEPS and its recursedeps generically; fail fast on missing source inputs; ninja -k 0 with one retry for upstream ordering races; drop the gclient --jobs=2 throttle; match depot_tools' reformatted GSUTIL_DEFAULT_PATH. run-build.sh: bake install-build-deps into a per-version image.
… improvements plan
…e persona's audio device Rewrite 0026 to scale OfflineAudioContext output once, at the render-thread copy, instead of offsetting every AudioBuffer read: silence stays zero, both read paths agree and script-built buffers are untouched. New 0061 reports 48 kHz with the persona's measured device buffer (Windows 480, macOS 256 frames) when a persona is active. Smoke gains an audio section covering both.
Chrome builds navigator.keyboard.getLayoutMap() from the server's XKB keymap. Set it explicitly after the X-ready poll in both the Xvfb and the Xvnc branch, so the layout is a deliberate choice lane M can tune.
… IDs Drop the RTX 3060 and RX 7600 rows and move every Intel and AMD row to a device ID with captured WebGL capability tables (9A49, 46A6, A7A0, 9B41, 3EA0, AMD 1638), re-pairing thread counts with parts that carry each die. No row claims less than 16 GB, which the host-derived jsHeapSizeLimit would contradict.
Write payments.can_make_payment_enabled, credentials_enable_service, credentials_enable_autosignin, autofill.credit_card_enabled (true) and bookmark_bar.show_on_all_tabs (false) when absent, on fresh and existing profiles. A value already set in the browser is left alone.
CreepJS catches --fingerprinting-client-rects-noise with a known-geometry check however it is seeded (Element.getClientRects "unknown rotate dimensions"). Rects still differ per seed through each seed's own screen and window size, and getClientRects and getBBox now agree.
With the referrer hunks gone from 0040 and the kRemoveClientHints flip gone from 0019, the binary ships real Chrome's referrer and client-hint defaults itself. The flag was also appended after the caller's args, so it silently overrode any --disable-features a caller passed. Ships with the binary pin.
Drop the MinimalReferrers/NoCrossOriginReferrers flips from 0040 (keeping SetIpv6ProbeFalse) and the RemoveClientHints flip from 0019, so the binary matches stock Chrome without --disable-features. Smoke asserts the Referer and Sec-CH-UA headers on the wire from a two-origin local server.
…ample-rate switches Nothing in the series, the 154 tree or the Go daemon reads either switch, so drop them from the shared header and from 0050's renderer forwarding list. 0050 is regenerated against 154 and loses two comment-only hunks that rewrote upstream URLs to their domain-substituted form.
Both patched headless/lib/{renderer,browser}, which only the headless_shell
executable links; the chrome target never compiles them. The README records
the drop, the new patch count and the current Widevine state.
Bromite's canvas readback noise and measureText factor were re-rolled per call and per Document, so an unchanged canvas read back differently every time (#80). Patch 0055 keys the image-data noise on the seed plus the pixels read, skips transparent and flat pixels, noises a private copy on both the one-shot and streaming encoders so toDataURL, toBlob and convertToBlob agree, takes the measureText factor from the seed and scales only the glyph-dependent metrics. smoke.py gains the (a)-(e) canvas assertions.
…and hardware hints
…hanged patches from records Stage 4 records each applied patch in .browser-applied/<name>.<hash>.patch and reverses a changed or removed patch from that copy, so a warm prep rewrites only the files it must and out/ stays warm. A one-time BROWSER_APPLIED_SEED converts the legacy .done markers, checked against their hashes. Zero-context insertions, which git apply lands at end of file, are rejected by stage 4 and by the new patch-lint recipe (just check and CI). regen-patch.sh regenerates a patch and proves the round trip plus the identifier diff. 000-shared is copied only on a content change. Also corrects the args.gn Widevine comment: nothing fetches a CDM.
…gamut/availTop, heap limit 0062 maps the Windows persona's CSS system colours to Chrome on Windows' light-mode values (ActiveText no longer the stock red that fires CreepJS hasKnownBgColor). 0011 reports colorDepth 30 and the menu bar as availTop on the macOS persona; 0054 adds color-gamut p3, dynamic-range high and (color: 10) there. 0064 pins jsHeapSizeLimit to V8 154's 8 GiB+ value when a persona is active. smoke.py gains a display-persona section.
probes.py holds every probe from the 154 improvements plan's validation matrix as one dict entry of JS, served from a two-origin 127.0.0.1 page, and both scripts emit it as metrics["probes"]. detect.py's worker check now reads the shared worker probe, which adds languages, deviceMemory, userAgentData and timezone to the comparison. realref.py pins the window to the primary display.
…d version Patch 0006 hardcoded Chrome/151.0.0.0, so on 154 navigator.userAgent (page and workers) contradicted the HTTP header, userAgentData.brands and Sec-CH-UA. The major now comes from --fingerprint-brand-version, falling back to the version the browser fills into the UA metadata from version_info. smoke.py asserts the page, a worker, the header and the brands agree on one major.
Chrome 154 ships navigator.cpuPerformance, a tier computed from the host's core count and CPU model, so the container leaked the build host's CPU beside a persona describing another machine. New patch 0065 tiers the persona instead: its hardwareConcurrency, plus the Apple chip its Metal WebGL renderer names, through upstream's own GetTierFromCpuInfo.
… driver (#50) New patch 0056 in v8/src/inspector: getErrorProperty walks the chain by descriptor and never runs page getters, error stacks are formatted only when formatting is inert, the property walk skips RegExp.prototype.flags, and the NodeList length and console.table column reads go through descriptors.
The default us/pc105 keymap already matches real Chrome's getLayoutMap in 47 of 48 entries on both personas. The outlier is IntlBackslash (keycode 94, <LSGT>), which X maps to '<'. Override that key alone via xkbcomp: the section sign on the arm64 macOS persona, backslash on the amd64 Windows one.
…drivers run smoke.py and detect.py gain a driver-shaped section: the issue #50 probe and its siblings with Runtime off, then on for the page and an auto-attached worker; the counts must not move. cdp-getter-probe.sh runs the same probe through the bundled playwright-cli for release step 9.
Ungoogled registers enable_a_ping false, so <a ping> is never sent. Write Chrome's stock true when absent, like the other stock prefs.
…, Windows system fonts 0013 pins window.screenX/screenY to the maximized work-area origin (availTop is the macOS menu bar) and shifts mouse/pointer event screen coordinates by the same offset. MenuBarHeight() moves into cuttle_seed so 0011 and 0013 share it; the notchless value is 30 (macOS 26, measured). 0062 adds the macOS Highlight/HighlightText/SelectedItem values, the unblended Windows dark Highlight, and the Windows menu/small-caption/status-bar fonts. smoke.py checks all of it.
…WebGPU adapters Patch 0058 adds a header-only capability table keyed on --fingerprint-gpu-renderer (D3D11 rows ported from fingerprint-chromium, BSD-3; Metal rows measured on a real Apple M1 Max under Chrome 154). Patch 0016 now serves numeric getParameter caps, getShaderPrecisionFormat and a hide-only extension filter from it, and drops the WebGL1-only allowlist that also hid WebGL2 extensions. Patch 0060 resolves requestAdapter to null for a CPU adapter under a persona and reports bgra8unorm as the preferred canvas format.
…lect Patch 0059 returns real Chrome 154's MSL or HLSL translation for the shaders detectors are known to compile (deviceandbrowserinfo's hasInconsistentWebGLShaderLang vertex shader and our posture probe pair), so a Metal or D3D11 renderer string no longer sits over SwiftShader's SPIR-V notes. MSL is captured on a real Apple M1 Max; HLSL is derived from the 154 ANGLE translator and matches a real Windows capture's head and length. Other shaders keep the backend translation.
…h stock Chrome also denies there
…es and record their posture
tenequm
marked this pull request as ready for review
September 29, 2026 21:43
…ed IP A proxied seed with no forced WebRTC IP runs under disable_non_proxied_udp, which still resolves a page's TURN hostname through the system resolver, outside the proxy. The new --fingerprint-webrtc-no-local-dns switch (forwarded to renderers by 0050) makes 0057's lookup failure apply without a forced IP, and cuttle passes it alongside the policy flags. Without the switch the binary behaves as before.
…trics Windows sizes a font without USE_TYPO_METRICS from OS/2 winAscent/winDescent, while the Linux build reads hhea. The stamped Consolas faces kept Consolas's hhea (1521/-527/350), so monospace fontBoundingBoxAscent read 36 at 48px and 7 at 10px against real Windows Chrome's 44 and 9. The Windows metrics extractor now writes the win metrics into hhea (line height kept through the gap), and the table carries 1884/-514/0 for both Consolas faces. Every other stamped Windows face already had hhea equal to its win metrics.
…e-a871b1/1/cuttle into release/154
…lt with the WebRTC DNS switch
A direct seed with a pinned timezone ran the full geo lookup on the launch path, which may download GeoLite2 under the shared lock, and a lookup that found the IP but no timezone was cached for the full 10 minutes, leaving unpinned direct seeds on UTC. A pinned timezone now resolves only the exit IP, that IP-only entry never answers a seed that needs the timezone, and an IP without a timezone is retried after the short failure TTL.
An explicit --fingerprint-webrtc-ip=auto on a direct seed with a pinned timezone skipped the egress lookup and was then dropped, so the seed got real ICE where the same seed without a timezone got the forced egress IP. And a zoned IPv6 value (fe80::1%eth0) passed netip.ParseAddr, so a proxied seed skipped the fail-closed policy although the binary's IPFromString rejects it and forces nothing.
A connection that pins --fingerprint-screen-* skips ScreenArgs, but AppleSiliconArgs still drew the Mac against the operator screen, so a 1440x900 pin could land on an M2/M3 15" GPU, breaking the one-machine invariant. The Mac is now drawn among the models with the pinned screen.
…the refusals BROWSER_STAGE=all also runs prep_tree, which reverses and re-applies patches under any running STAGE=build container, but only STAGE=prep was refused. The install-build-deps bake also ran before both refusals, so a rejected invocation still paid 1-2 minutes and read the script from under a running prep.
…o diff --git line Patches 0026, 0055, 0057 and 0061 start their diffs at '--- a/', so the header cut kept the whole file and a regeneration put the old diff in front of the new one.
…ical smoke.py and benches/probes.py each carry CDP_GETTER_PROBE, and neither can load the other: the build container mounts only validate/, and probes.py is copied alone to the reference box. Assert the two copies match, alongside the other smoke.py cross-file checks.
10 tasks
cuttle_webgl_caps.h, which carries the BSD-3 D3D11 rows, is created by 0016-webgl-vendor-renderer-from-cli; 0058 only extends it with the WebGL2 int64 caps. THIRD-PARTY.md and the browser README both named 0058.
Calibri, Segoe UI Italic/Bold Italic and Cambria are built only into the Windows (amd64) font pack, so listing them as shared overstated what the macOS image ships.
…ding The series is 37 patches and 0036-storage-estimate-from-cli was dropped. Only image-data noise stays on, so the section no longer says to keep 'the flags'. The sizing paragraph described the retired 100 GB cache volume rather than the snapshot of the server's root disk.
The image no longer ships ungoogled-chromium 148 with clark's stealth5 series as-is, and Segoe UI Regular and Bold now come from Selawik, not Carlito.
…stScript names
The Windows metrics table was last regenerated before the extractor learned to
record each face's names, so rename-fonts.py gave every Windows stand-in the
bare family as its full name: local("Segoe UI Bold"), local("Tahoma-Bold") and
every other Bold/Italic face failed to load where real Windows Chrome finds
them. The table is regenerated from a Windows 11 install; the only change is
the added names for all 19 entries - advances, vertical metrics and kerning
are byte-identical, so the Consolas win-metrics sizing is kept.
…nes, so local() finds them
tenequm
added a commit
that referenced
this pull request
Sep 30, 2026
🤖 I have created a release *beep* *boop* --- ## [0.16.0](v0.15.1...v0.16.0) (2026-09-30) ### ⚠ BREAKING CHANGES * **browser:** stealth Chromium 154 with coherent personas ([#124](#124)) ### Features * **browser:** stealth Chromium 154 with coherent personas ([#124](#124)) ([6eacdd7](6eacdd7)) ### Bug Fixes * **serve:** keep proxy credentials out of the ignored-params warning ([#125](#125)) ([2cf0023](2cf0023)) * **skill:** ship the skill as a real file under skills/cuttle ([#122](#122)) ([e04bd29](e04bd29)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
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.
Stealth Chromium moves from 151.0.7922.137 to 154.0.8037.57 (ungoogled-chromium 154.0.8037.57-1), and the same build carries the fingerprint-coherence work from the improvements plan plus two rounds of fixes from the final gates and review. Nothing ships as a pure rebase: both targets are built from one commit,
69789c0, published asbrowser-v154.0.8037.57-1, and one pin commit here movesversions.env, the Dockerfile,chromiumVersion, the golden andposture.jsonto it.Addresses #24: the binary now sends real Chrome's
Sec-CH-UAheaders itself, so cuttle no longer passes--disable-features. Also addresses #45, #50 and #80, and part of #53.Chromium 151 -> 154 rebase
kIncognitoStaticStorageQuotaitself.Chrome/151.0.0.0innavigator.userAgent; the major now comes from the validated--fingerprint-brand-version, so page, worker, header and brands agree on 154.headless_browser_implhunk: they patchedheadless/lib, which thechrometarget never compiles.usageDetails, before the opaque-origin TypeError).fingerprint-location,audio-sample-rateandfingerprint-fonts-dirswitches.The series goes from 27 to 37 patches: 0055-0067 are new; 0002, 0036 and 0045 are gone.
Stealth improvements, by what a page now sees
toDataURL,toBlobandconvertToBlobagree, flat or cleared canvases read back exact, and--fingerprint-noise=falseturns it off. macOS readbacks unpremultiply on the CPU as real Mac Chrome does, so the first read no longer rounds apart from later ones. measureText noise is off (it moved widths off Chrome's fixed-point grid) and the client-rects noise switch is dropped.WEBGL_debug_shadersanswers known detector shaders in MSL or HLSL.readPixelsnoise now covers only the RGBA bytes validation accepted, is keyed on seed and framebuffer coordinates, and never touches alpha. A software WebGPU adapter resolvesnull; adapter info keeps device and description empty as stock does, with Dawn architecture names; the Intel-Mac GPU is out of the macOS pools.--fingerprint-webrtc-ip, one.localhost candidate and one srflx at the seed's exit IP; no UDP leaves the real interface. TURN over TCP or TLS connects through the proxy; every WebRTC DNS lookup fails in forced mode, so a TURN or candidate hostname never reaches the system resolver. A proxied seed without a forced IP gets the same through the new opt-in--fingerprint-webrtc-no-local-dns, which cuttle passes next to thedisable_non_proxied_udppolicy; stock Chrome and 151 resolved a page's TURN hostname through the system resolver there. Direct seeds get their egress IP, cached 10 minutes (failures 1 minute).Runtime.enable(error stack,RegExp.prototype.flags,NodeList.length,console.tablecolumns), and each realm's ownErroris checked before a stack is formatted. 0066 keepsRuntime.enablefrom raising the stack capture depth to 200 frames, which are_you_a_bot'shasInconsistentTimingResolutiontimed.AudioContextreports 48 kHz with the persona's device buffer, and Windowsplaybackrounds to 960 frames as real Windows does. Analyser noise off now means off, and byte data derives from the noised floats.colorDepth30,color-gamut: p3,dynamic-range: highandavailTop/screenYat the menu bar.outerWidth/outerHeightare the real window's again, so popups and resized windows keep a real frame. Screen, device-memory and core-count switches are bounded to values Chrome can report.--fingerprintseed is drawn once in the browser and passed to every renderer, so OOPIFs and workers match their page;--fingerprint-brand-versionis validated once for every UA-CH surface; hardwareConcurrency shares one range between 0006 and 0065;navigator.cpuPerformanceandjsHeapSizeLimitfollow the persona, not the host;navigator.connectionrounds rtt and downlink like stock Chrome; macOS UA-CHplatformVersiondefaults to a real release instead of 10.15.7; persona voice names decode as UTF-8.permissions.query.strict-origin-when-cross-originreferrers andSec-CH-UAship in the binary; stock profile prefs ungoogled re-defaults are written only when absent.system-uiandBlinkMacSystemFontresolve to Inter re-widthed to SF Pro Text, hidden by name; Windowssystem-uiresolves to a Selawik-based "Segoe UI" with real Segoe UI advances and kerning. Both packs add stock families with real metrics and kerning (macOS: Avenir, Avenir Next, Futura, Verdana, Tahoma, Georgia, Trebuchet MS; Windows: Verdana, Tahoma, Trebuchet MS, Georgia, Consolas, Lucida Console, Impact, Comic Sans MS, Segoe UI Symbol), and macOSmonospaceis Menlo.local()by its real full and PostScript name (Arial Bold,Arial-BoldMT,Segoe UI Bold).-noresetso the persona keymap sticks (all 48getLayoutMap()keys match);--test-typeon every launch removes the--no-sandboxinfobar; the system window frame on both personas reads chromeWidth 0 / chromeHeight 87 like real maximized Chrome;--msaa_is_slowon Windows reproduces its canvas antialiasing; VNC mode renders on the same GL path as plain mode and maximizes the session window. Geo-derived locales map only to English variants Chrome offers.Build pipeline
git applywith applied records: stage 4 keeps each applied patch in.browser-applied/and reverses only changed or removed patches, so a warm prep rewrites only what it must andout/stays warm.regen-patch.shproves a patch round-trips before replacing it.git applylands a zero-context insertion at end of file and reports success, so stage 4,just patch-lint(part ofjust check) and CI reject@@ -N,0hunks.BROWSER_STAGE=prep|build: prep does every write to the shared tree once and refuses to run while a build is running; both targets then build concurrently. Non-git CIPD/GCS deps are fetched from DEPS and stamped only after a complete install, and ninja runs-k 0with one retry for upstream ordering races.install-build-deps.shand baked once per version..sha256file lists the tarball by basename sosha256sum -cworks on a downloaded asset.Validation
Gates run on the exact published artifacts, each persona on its own arch, inside a runtime image with the new binary and font pack.
validate/smoke.py), per personabrowser-v151.0.7922.137-1(validate/parity.py)navigator.connectionrounding (151 reported raw rtt/downlink, 154 rounds like stock)detect.py, macOS personadetect.py, Windows personaRuntime.enableon, both personascuttle serve): session, pool, VNCgo -C packages/cuttle run ./test/smoke)validate/cdp-getter-probe.sh)Every posture move has to head toward the real Chrome 154 values; the numbers land in
packages/browser/benches/posture.jsonwith the pin commit.Known limits
liga/calt/dlig; Light, Semibold and Semilight are not shipped.readPixelsis not noised, and WebGLreadPixelsnoise skips a read's last row and column, so a crop disagrees with the full read..localnames are never registered).devicePixelRatioignores browser zoom.speechSynthesis.speak()with a persona voice fails withsynthesis-failed;BarcodeDetector.detect()returns[].--disable-features, cuttle on an older self-built binary (CUTTLE_BROWSER_BINARY) would lose the referrer and client-hint fix.Follow-ups
packages/browser/hetzner/provision.sh,teardown.sh,cloud-init.yaml) in a separate PR; the README documents the snapshot flow.Release notes
Stealth Chromium moves to 154 with more coherent personas: seed-stable canvas, per-GPU WebGL, WebRTC at the exit IP, persona fonts, colours and window geometry, and stock client hints. Existing seeds may report a different GPU or screen; a pinned screen no longer offered is refused.