Commit Graph
14 Commits
Author SHA1 Message Date
Jake WriterandD Yiapanis d6a806e2b5 fix(fingerprint): keep the WebGL renderer coherent with the screen (#729)
BrowserForge picks navigator/screen; the GPU is drawn separately from
webgl_data.db weighted only by OS. Nothing ties the two together, so the
synthetic path emits pairs no real machine ships -- a discrete desktop GPU
behind a 1024x600 panel. Consistency checks (Pixelscan, Fingerprint.com) read
that as masking even though every individual value is plausible on its own.

Builds on @dyiapanis's #730, which identified the problem and the GPU-class
thresholds, with three changes:

  * Constrain the GPU to the screen rather than the screen to the GPU.
    sample_webgl_for_screen does rejection sampling, so the GPU keeps
    webgl_data.db's real OS-weighted distribution and the geometry -- already
    reconciled against the real display and the window box by
    clamp_screen_to_display / fix_screen_no_taskbar / clamp_window_dimensions
    / clamp_window_position -- is left alone.
  * Where no coherent GPU exists at all (BrowserForge still carries
    netbook-era geometry, and nothing in the pool drives a sub-1366x768
    panel), raise_screen_to_gpu_floor lifts the screen instead. It measures
    the screen-to-avail gap BEFORE mutating -- #730 computed it after
    overwriting screen.height, which turned a 1024x600 -> 1080 bump into a
    520px "taskbar", a fresh impossible-geometry tell -- and it runs BEFORE
    clamp_screen_to_display so a genuinely small monitor still wins and a
    headful window cannot be pushed back off its own display (#499).
  * No Apple-M Retina floor. Apple silicon also ships in the Mac mini and Mac
    Studio, which drive whatever external monitor is attached, so pinning it
    to 2560x1600 would reject real hardware and shrink the pool for nothing.

Measured over 300 synthetic fingerprints, incoherent GPU/screen pairs fall
from 54.3% to 0%, with avail <= screen and availHeight < height holding in
every trial. The screen floor is a no-op for the Linux and Windows pools
(0/400 draws below it) and fires on 3.5% of macOS draws, so the entropy cost
is confined to the implausible tail it exists to remove.

Co-authored-by: D Yiapanis <d@yiapanis.co>
2026-08-29 13:05:31 -06:00
Jake Writer 160c806ad1 fix(stealth): make spoofed speech voices fail closed (#731)
Firefox registers the host's speech-dispatcher / SAPI / NSSpeech voices
unless something stops it, and nsSynthVoiceRegistry only stopped it when the
explicit `voices:blockIfNotDefined` flag was set. Nothing set that flag, so
the host was suppressed only as a side effect of a non-empty spoofed list --
and the Python layer built that list inside a bare `except Exception: pass`.

Any path that left the list empty or unset therefore fell through to the host
backend. On a stock Linux box that exposes 14805 espeak-ng voices to the page
under a fingerprint claiming macOS or Windows, which both leaks the real host
OS and contradicts the rest of the profile. Reproduced on 152.0.4-beta.29:

    config                     voices exposed
    generation raises          14805  (all host speechd)
    {"voices": []}             14805  (all host speechd)
    valid list                   115  (correct)

Three changes, so the failure is closed at both layers:

  * nsSynthVoiceRegistry::AddVoice now also blocks when MaskConfig carries a
    `voices` array at all -- including an empty one, or one whose entries were
    all rejected as malformed. An empty spoofed list must mean "no voices",
    never "all of the host's". With no `voices` key the browser still behaves
    like stock Firefox, so a bare binary is unaffected.
  * launch_options pins `voices:blockIfNotDefined` (via set_into, so an
    explicit caller value still wins) and degrades a generation failure to an
    empty list rather than leaving the key unset. It also passes the spoofed
    navigator.language through, so the default voice matches the locale.
  * validate_voices rejects the shapes MaskConfig::MVoices() silently drops --
    bare "Name:lang:type" strings and half-filled objects -- before launch
    instead of letting them degrade into a host-voice leak.

Both failure paths now expose 0 voices; the normal path still exposes 115.
2026-08-28 13:21:32 -06:00
Jake WriterandClaude Opus 5 2834a463d1 test(virtdisplay): assert the real post-condition of kill()
`VirtualDisplay.kill()` reaps the Xvfb child and then clears `self.proc`, so
asserting `vd.proc.poll() is not None` afterwards raises AttributeError on
None. Two tests failed this way on main, unrelated to any of the merged PRs.

Assert `proc is None or proc.poll() is not None` -- reaped-and-cleared is the
success path, and a surviving handle must still report an exit code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:33:55 -06:00
Cloudymap1e e61642aaf5 fix(server): close browser when launcher exits 2026-07-28 00:29:11 -06:00
Pratyush Sharma a5afa46cfa Apply screen constraints on Windows and macOS
get_screen_cons() was gated on DISPLAY being set, which only ever happens on
Linux, so headful runs on Windows and macOS generated fingerprints with no
monitor bound at all.

Fixes #425
2026-07-28 00:29:03 -06:00
Pratyush Sharma 22c6ffbdda Probe the host monitor in CSS pixels
screeninfo makes the process per-monitor DPI aware, so it reports physical
pixels, while Firefox lays windows out in CSS pixels. At 150% Windows scaling a
1920x1080 panel is 1280x720 CSS px, so bounding the fingerprint by the physical
size lets the window open 1.5x larger than the screen.

Refs #425
2026-07-28 00:29:03 -06:00
Pratyush Sharma fbafbcf9f0 Exclude virtual displays from the display clamp
headless='virtual' reaches launch_options as headless=False with
virtual_display set (async_api rewrites it), so the headful gate fired and
clamped the fingerprint to Xvfb's 1x1 stub. fix_screen_no_taskbar then drove
availHeight to -39 and validate_config rejected the launch outright.
2026-07-28 00:29:03 -06:00
Pratyush Sharma 2266f27501 Clamp headful window geometry to the real display
get_screen_cons() bounds the generated fingerprint to the monitor, but
BrowserForge honours a Screen constraint only when its pool has a match:
FingerprintGenerator.partial_csp catches the filtering failure and deletes the
constraint unless strict=True. So a 1366x768 laptop routinely gets a 2560x1440
fingerprint with window.outerWidth 1920, and browser-init resizes the real
chrome window to it -- rendering past the edge of the monitor.

Re-apply the bound after generation instead of trusting BrowserForge with it,
and pull screenX/screenY back inside the shrunken screen.

Headful only. headless has no window to overflow, and headless='virtual' runs a
1x1 Xvfb whose "monitor" would otherwise shrink the fingerprint to 1x1.

Fixes #499
2026-07-28 00:29:03 -06:00
Cloudymap1e a8ad6285d6 fix(linux): prepare required runtime directory 2026-07-28 00:26:37 -06:00
Cloudymap1e 9e74311572 fix(python): preserve humanize duration types 2026-07-28 00:26:37 -06:00
Cloudymap1e 3efa5e2182 fix(python): isolate per-launch environments 2026-07-28 00:26:36 -06:00
Jake Writer 0e4151f820 fix: page-recycle hang under spoofed window dims, and the unmerged halves of #637-#647
Fixes the new_page() hang from #666, and restores the pythonlib/ + settings/
halves of #637-#647 that were dropped when those PRs were consolidated into #666
(that PR only carried patches/ + additions/, so these never actually landed).

## new_page() hangs when window.outer* is spoofed (#666)

The outer-size hijack in browser-init.patch pinned the chrome documentElement to
the spoofed size. That caps .browserStack, which caps the content viewport, so
the content window can never reach the size Juggler asks for in
updateViewportSize() -- and awaitViewportDimensions awaits exact equality with
no timeout, so it deadlocks rather than erroring. The second new_page() hung
forever and took the context with it.

The pin was never load-bearing: GetOuterWidth/GetOuterHeight already consult
MaskConfig unconditionally (fingerprint-injection.patch), so window.outerWidth is
spoofed in C++ regardless of the real chrome window size. Resizing is enough.

Measured on the official v152.0.4-beta.26 build (headless):

    config      before        after
    none        pass          pass
    inner       pass          pass
    outer       HANG          pass
    both        HANG          pass  (iw:360 ih:740 ow:360 oh:800 -- exact)

This corrects the diagnosis in #666, which blamed the inner+outer combination and
the `!(outerWidth || outerHeight)` guard. outer* ALONE is sufficient to hang, and
dropping inner* does not help, so that guard is not the culprit.

Also fixed driver-side: Playwright's implicit 1280x720 viewport is what asks for
the impossible size, so the driver now defaults to no_viewport when the config
spoofs any window dimension. That fixes the hang on already-released builds
without a rebuild. An explicit viewport=/no_viewport= from the caller wins.

## WebRTC ICE prefs (#538)

#666 merged the C++ half of the WebRTC fix but not the prefs, so the shipped
build still has no_host=true and none of the proxy_only prefs.
proxy_only_if_behind_proxy is the pref that actually stops the real-IP leak: it
prevents a UDP STUN request routing around a TCP proxy. no_host=false keeps the
stock two-candidate shape, which obfuscate_host_addresses makes leak-free.

## Also restored from the consolidation

- fix(proxy): dom.security.https_first rewrote http:// before the launch-arg
  proxy filter saw it, breaking CONNECT-only proxies (#638).
- fix(stealth): speech-voice spoofing + stop leaking host voices (#646).
- fix(stealth): clamp inner <= outer <= avail <= screen; BrowserForge can emit
  impossible geometries that leak as tells (#647).

Refs: https://github.com/daijro/camoufox/pull/666
Refs: https://github.com/daijro/camoufox/issues/538
2026-07-16 14:38:39 -06:00
Pratyush Sharma ab20eca72d Add regression tests for camoufox server
Cover both failure modes from #656 and pin the driver entrypoint
contract, so a future Playwright reshuffle fails in CI rather than in a
user's terminal. No browser download or launch, so they run anywhere.

Refs #656
2026-07-15 18:32:05 +05:30
Jake Writer 8f9ff07b48 fix(virtdisplay): atomically claim X11 display via Xvfb -displayfd (#597)
Replace the userspace lock-file scan + random-jitter retry loop with
Xvfb's own -displayfd mechanism. Xvfb scans up from :0 and atomically
binds the first free X11 socket (kernel-mediated, no userspace race),
then writes the chosen display number back through an inherited pipe.

This eliminates the duplicate-display race that occurred when many
camoufox processes started concurrently and all observed the same set
of free display numbers before any of them bound.

Adds a 10s read timeout on the displayfd pipe so a hung Xvfb fails
fast instead of blocking forever, and adds tests covering single
launch, idempotent get(), 50 concurrent reservations with uniqueness,
and post-kill display reuse.
2026-05-08 19:12:09 -04:00