Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e36e0fe
fix: Return None if headless
datawookie May 29, 2025
6b8b086
fix(addons): re-download addons with a missing manifest
codechrl Aug 11, 2026
b2d8421
Verify sha256 of downloaded release assets before extracting
luandhgt Aug 6, 2026
1934b35
Enable TLS verification for public IP lookups
luandhgt Aug 6, 2026
9519d2d
wip: add Windows service test script (run_tests.ps1)
May 25, 2026
1ffa32d
feat(windows): add run_tests.ps1 and fix Windows compat in service-te…
May 25, 2026
fff2c73
fix(pythonlib): derive navigator.appVersion from the preset's user agent
polyackiy Sep 4, 2026
5361844
Spoof navigator.maxTouchPoints from config
nxbdev Jul 22, 2026
03ac03c
Add media:spoof_codecs config option to prevent codec fingerprinting
rubenvereecken Apr 3, 2026
0969a94
camoucfg: add cached font allowlist helpers
pratyush618 Jul 28, 2026
70a22d8
fix(fonts): apply font allowlist at lookup time, not via whitelist pref
pratyush618 Jul 28, 2026
55b056f
fix(fonts): exempt chrome docs from CSS2 system-font spoof
pratyush618 Jul 28, 2026
97fcc48
fix(fonts): exempt chrome docs from system-ui spoof
pratyush618 Jul 28, 2026
6e994b9
fix(juggler): don't dispatch mouse events on the chrome/content bound…
JWriter20 Sep 4, 2026
4132dd5
refactor(juggler): make the input-dispatch deadlock structurally unre…
JWriter20 Sep 4, 2026
17fe73a
fix(patches): retarget the media-codec include hunk to the FF152 order
JWriter20 Sep 5, 2026
0169975
fix(config): declare media:spoof_codecs, and guard the whole class
JWriter20 Sep 5, 2026
0e1f9a8
fix(python): bound headful geometry again after the get_screen_cons flip
JWriter20 Sep 5, 2026
a80abb4
feat(patches): report a touchscreen digitizer, not a phone
JWriter20 Sep 5, 2026
e459dc5
fix(patches): express the no-search-engines stub in search-config v2 …
JWriter20 Sep 5, 2026
92d79a8
chore: drop three stale .bak files
JWriter20 Sep 5, 2026
1ae5bae
test(touchscreen): replace the reconstructed reference with the real …
JWriter20 Sep 6, 2026
35b45c1
Merge pull request #756 from JWriter20/integration/triage-2026-09-05
JWriter20 Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Lint

# Static checks only -- no browser build, so these can gate every pull request.
# The build workflow runs on tags and takes ~40 minutes; nothing was checking
# pull requests before this.
on:
pull_request:
push:
branches: [main]
workflow_dispatch:

jobs:
input-dispatch:
name: Synthesized input goes through one chokepoint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python3 scripts/check-input-dispatch.py
26 changes: 26 additions & 0 deletions additions/camoucfg/MaskConfig.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Written by daijro.
#include "json.hpp"
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <optional>
#include <codecvt>
Expand Down Expand Up @@ -116,6 +117,31 @@ inline std::vector<std::string> GetStringListLower(const std::string& key) {
return result;
}

/**
* The spoofed font family allowlist ("fonts"), lowercased and cached for the
* lifetime of the process. CAMOU_CONFIG is read once at startup and never
* changes, and the gfx font lookup paths consult this on every family
* resolution, so re-parsing the JSON per call is not an option.
* An empty list means no font spoofing is configured.
*/
inline const std::vector<std::string>& FontAllowlist() {
static const std::vector<std::string> fonts = GetStringListLower("fonts");
return fonts;
}

inline bool HasFontAllowlist() { return !FontAllowlist().empty(); }

/**
* Whether a font family may be used. `family` must already be lowercased
* (gfxPlatformFontList::GenerateFontListKey output is). Always true when no
* allowlist is configured.
*/
inline bool IsFontAllowed(std::string_view family) {
const auto& fonts = FontAllowlist();
if (fonts.empty()) return true;
return std::find(fonts.begin(), fonts.end(), family) != fonts.end();
}

template <typename T>
inline std::optional<T> GetUintImpl(const std::string& key) {
const auto& data = GetJson();
Expand Down
32 changes: 32 additions & 0 deletions additions/juggler/Helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
const {setTimeout, clearTimeout} = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");

export class Helper {
decorateAsEventEmitter(objectToDecorate) {
Expand Down Expand Up @@ -172,6 +173,8 @@ export class Helper {

const helper = new Helper();

const kEventTimedOut = Symbol('event-timed-out');

export class EventWatcher {
constructor(receiver, eventNames, pendingEventWatchers = new Set()) {
this._pendingEventWatchers = pendingEventWatchers;
Expand Down Expand Up @@ -202,6 +205,35 @@ export class EventWatcher {
}
}

/**
* Like ensureEvent, but gives up after timeoutMs and resolves null instead of
* waiting forever.
*
* Callers awaiting an ack for synthesized input must use this. Input dispatch
* is serialized on activateAndRun()'s process-global promise chain, so an ack
* that never arrives does not merely lose one event -- it wedges every later
* input event in the process, in every tab, permanently, at 0% CPU with no
* diagnostic. Four shipped deadlocks (#225, #677, #751, #752) were all that
* failure. Bounding the wait is what makes the fifth one a log line.
*/
async ensureEventWithin(aEventName, timeoutMs, predicate) {
const pending = this.ensureEvent(aEventName, predicate);
// Whichever promise loses the race stays pending until dispose() rejects
// it; swallow that so a timed-out wait never surfaces as an unhandled
// rejection in chrome JS.
pending.catch(() => {});
let timer;
const timedOut = new Promise(resolve => {
timer = setTimeout(() => resolve(kEventTimedOut), timeoutMs);
});
try {
const result = await Promise.race([pending, timedOut]);
return result === kEventTimedOut ? null : result;
} finally {
clearTimeout(timer);
}
}

async ensureEvents(eventNames, predicate) {
if (!Array.isArray(eventNames))
throw new Error('ERROR: ensureEvents expects an array of event names as its first argument');
Expand Down
30 changes: 29 additions & 1 deletion additions/juggler/TargetRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ const {AppConstants} = ChromeUtils.importESModule("resource://gre/modules/AppCon
// scripts), so the screencast tick has to import them explicitly.
const {setTimeout, clearTimeout} = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs");

// Last-resort bound on how long one callback may occupy the process-global
// activation chain below. The chain must always advance: a callback that never
// returns wedges every later input event in every tab, permanently.
//
// The per-ack deadline in MouseDispatch.js covers the await that has actually
// caused all four shipped deadlocks, but it is one of several unbounded waits
// reachable from a single slot -- apz-repaints-flushed, TabSwitchDone below,
// the drag path's juggler-drag-finalized and dragover waits, and the
// cross-process dispatchDragEvent sends all have the same shape. None of them
// has failed yet. Bounding only the wait that has already bitten us is the
// posture that produced those four fixes, so bound the slot itself too.
//
// Sized as a backstop, not a tuning knob: with a 5s ack deadline a legitimate
// worst-case input slot approaches 10s, so this must sit well clear of that.
const kActivationSlotBudgetMs = 30000;
const kSlotExpired = Symbol('activation-slot-expired');

const Cr = Components.results;

const helper = new Helper();
Expand Down Expand Up @@ -512,7 +529,18 @@ export class PageTarget {
const notificationsPopup = muteNotificationsPopup ? this._linkedBrowser?.ownerDocument.getElementById('notification-popup') : null;
notificationsPopup?.style.setProperty('pointer-events', 'none');
try {
await callback();
let timer;
const expired = new Promise(resolve => {
timer = setTimeout(() => resolve(kSlotExpired), kActivationSlotBudgetMs);
});
try {
if (await Promise.race([callback(), expired]) === kSlotExpired) {
dump(`[juggler] WARN activation-chain slot exceeded ` +
`${kActivationSlotBudgetMs}ms; advancing the chain without it\n`);
}
} finally {
clearTimeout(timer);
}
} finally {
notificationsPopup?.style.removeProperty('pointer-events');
}
Expand Down
Loading
Loading