Skip to content

fix(build): enable bundled fonts on macOS - #708

Open
wnxd wants to merge 1 commit into
daijro:mainfrom
wnxd:upstream-bundled-fonts
Open

fix(build): enable bundled fonts on macOS#708
wnxd wants to merge 1 commit into
daijro:mainfrom
wnxd:upstream-bundled-fonts

Conversation

@wnxd

@wnxd wnxd commented Jul 31, 2026

Copy link
Copy Markdown

Fixes the macOS side of #706.

What's wrong

Camoufox ships 287 font files under Contents/Resources/fonts on macOS
(Makefile passes --fonts windows linux to package.py) and already sets
defaultPref("gfx.bundled-fonts.activate", 1) in settings/camoufox.cfg — but
none of them are ever loaded.

Firefox does have the macOS code for this:
CoreTextFontList::ActivateBundledFonts() reads <GRE>/fonts and hands it to
ActivateFontsFromDir(), which registers the files with
CTFontManagerRegisterFontURLs(urls, kCTFontManagerScopeProcess, ...). That
function is guarded by #ifdef MOZ_BUNDLED_FONTS, and upstream defaults the
option to:

@depends(target)
def bundled_fonts_default(target):
    return target.os == "WINNT" or target.kernel == "Linux"

macOS is not in that set, so the whole path is compiled out. Linux and Windows
get it implicitly and never needed the flag written down; macOS does. The option
itself is gated only on project == "browser" — there is no platform
restriction.

Note that ActivateFontsFromDir() is not inside the #ifdef, which is why
CTFontManagerRegisterFontURLs still shows up in XUL's symbol table on macOS
even though nothing calls it for the bundled set. That misleads binary
inspection.

Why it matters

On a macOS host the spoofed font list currently resolves against the host's own
fonts, so the measurable set is host fonts ∩ claimed list. Font metrics are a
cross-checked surface — a UA claiming Windows next to macOS font metrics is a
contradiction detectors specifically look for — so this weakens the spoof rather
than merely narrowing it.

Verification

Built this branch on CI (macos/arm64) and measured both builds with the same
frozen config
, so the claimed font list is identical on both sides and the only
variable is the flag. Measurement is the usual metrics probe (render a string in
"<family>", monospace|serif|sans-serif and compare against each generic's
baseline), not an enumeration API.

before after
measurable, of 51 claimed 12 (23%) 35 (68%)
macOS-native fonts leaking 0/8 0/8

Calibri, Consolas, Segoe UI, Cambria Math and Nirmala UI all go from
unmeasurable to measurable.

The 16 that remain

Not failures of this change:

  • 4 are weight variants whose base family does resolve (Segoe UI Semilight
    Segoe UI ✓, Yu Gothic UI LightYu Gothic UI ✓). CSS collapses these
    onto one family, so a metrics probe cannot distinguish them. Measurement-method
    floor, not a spoofing gap.

  • Most of the rest are localized family names. Eight pairs, tested with every
    name added to the whitelist so filtering is not a factor:

    English Localized
    SimSun / NSimSun 宋体 / 新宋体
    Microsoft YaHei 微软雅黑
    Microsoft JhengHei 微軟正黑體
    Yu Gothic 游ゴシック
    MS PGothic / MS Gothic MS Pゴシック / MS ゴシック
    MingLiU-ExtB 細明體-ExtB

    English 8/8 resolve, localized 8/8 do not. The fonts are loaded — the English
    names prove that — but CoreText registration only exposes the en-US family name
    (nameID 1), not the localized aliases. DirectWrite on real Windows honours
    both, so this is a separate and much narrower gap, worth its own issue.

I also briefly suspected .ttc collections were the problem, since most of the
stragglers are .ttc. Cambria Math comes from cambria.ttc and resolves
fine, so that theory is wrong.

-apple-system still resolves on both builds. That is the
font-system-fonts-css2.patch path, unrelated to this change.

Open question

Do you know why Mozilla defaults this off on macOS? I could not find a rationale.
If it is just that nobody needed it, this is the whole fix. If there is a known
problem with process-scoped CoreText registration (startup cost, sandboxing),
that would be worth knowing before this ships.

Untested here: whether the metrics of the now-loaded fonts match real Windows,
and whether the Windows build's implicit MOZ_BUNDLED_FONTS actually works in
practice — I only have a macOS host.

camoufox ships 287 font files under Contents/Resources/fonts on macOS
(Makefile passes `--fonts windows linux` to package.py) and already sets
`defaultPref("gfx.bundled-fonts.activate", 1)` in settings/camoufox.cfg,
but none of them are ever loaded: the macOS build does not define
MOZ_BUNDLED_FONTS, so CoreTextFontList::ActivateBundledFonts() is
compiled out. That function is the only caller of ActivateFontsFromDir(),
which is what registers <GRE>/fonts with CoreText via
CTFontManagerRegisterFontURLs().

Upstream defaults the option to `target.os == "WINNT" or target.kernel ==
"Linux"` (toolkit/moz.configure), so Linux and Windows enable it
implicitly and macOS silently does not. The configure option itself is
gated only on `project == "browser"`, which camoufox is; there is no
platform restriction.

Effect on a macOS host: a spoofed font list currently resolves against
the host's own fonts, so the measurable set is `host fonts INTERSECT
claimed list` -- 13 of 60 claimed families measurable in local testing,
and those 13 are exactly the macOS-native ones. Font metrics are a
cross-checked fingerprinting surface (a UA claiming Windows alongside
macOS font metrics is a contradiction detectors look for), so this
undercuts the spoofing rather than merely reducing it.

Refs daijro#706

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JWriter20

Copy link
Copy Markdown
Collaborator

Independent confirmation on a different build path, plus a data point for your open question.

I cross-compiled this from Linux (multibuild.py --target macos --arch arm64 on Ubuntu, i.e. the CI path, not native macOS CI) and tested on an M4 / macOS 15.3.1. The flag survives the cross path: MOZ_BUNDLED_FONTS: '1' lands in config.status, and the 287 files package to Contents/Resources/fonts/.

A warning for anyone verifying this: gfx.bundled-fonts.activate is not usable as an A/B switch. I first tried toggling it 0/1 on a single binary, expecting it to gate ActivateBundledFonts() per CoreTextFontList.cpp:1007. It doesn't — both arms behaved identically as "on". The pref mechanism itself works (toggling dom.webnotifications.enabled and javascript.options.wasm via firefox_user_prefs does take effect), so this is specific to that pref. Two separate builds are required, which cost me a round.

With a proper control build — identical tree, only --enable-bundled-fonts removed:

Gecko level (no CAMOU_CONFIG), families that ship in bundle/fonts/ and are absent from stock macOS:

control with flag
Candara, Calibri, Consolas, Segoe UI, Gabriola, Corbel, Cambria Math, Ink Free, Leelawadee UI, Nirmala UI, Yu Gothic 0/11 11/11
Arimo, Cousine, Tinos 0/3 3/3
Caladea, Carlito (not actually in the bundle) no no
Helvetica Neue, Menlo, Monaco (macOS native) yes yes
gibberish controls no no

Sitka Text was the one Windows family unresolved in both — SitkaVF.ttf is a variable font and presumably registers under a different family name.

With the spoofed Windows list active (all 107 from fonts.json['win'], pinned fonts:spacing_seed, ASCII + CJK samples, per the measurement caveats in #706):

control with flag
claimed families measurable 14/107 (13%) 103/107 (96%)
macOS-native fonts leaking 0/8 0/8

build-tester shows no regression: 1038–1039/1048 with the flag vs 1037/1048 on the control, Grade A both, Font Platform 16/16 and Font Environment 20/20 passing in both. The spread is a flaky cross-profile uniqueness collision that reproduces on the control too.

On your open question — why Mozilla defaults this off on macOS. There is a measurable startup cost. Launch-to-first-page, headless, 7 runs each, same machine, alternating builds:

median trimmed mean min max
control 1.018s 1.022s 1.007s 1.070s
with flag 1.232s 1.234s 1.224s 1.471s

+214 ms, roughly 21%. That is 287 files going through CTFontManagerRegisterFontURLs at font-list init. I can't claim it's Mozilla's actual reason, but it is a concrete cost of the kind that keeps a platform default off, and it's worth weighing against the fingerprinting gain rather than assuming the flag is free. For camoufox the trade looks clearly worth it — 13% → 96% of the claimed list becoming real is a much bigger effect than 200 ms of startup.

Verified on 152.0.4-beta.28, cross-compiled from Linux, macOS 15.3.1 arm64 (M4).

@wnxd

wnxd commented Jul 31, 2026

Copy link
Copy Markdown
Author

Correction: the "remaining gaps" section of this PR description is wrong about localized family names. Details and the measurements are in #706 (comment)

Short version: bundled fonts load lazily (the first measurement is what triggers loading), and the whitelist is keyed on canonical en-US family names while fonts.json mixes canonical and localized forms. With the full 107-family Windows list whitelisted and a warm-up pass, this build measures 107/107, not the partial figure quoted above. There is no missing alias registration.

The before/after comparison in the PR is unaffected — both sides used one frozen config, so seed and whitelist were identical.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants