Open Bug 2043645 Opened 4 months ago

Address Mac voice-list fingerprint surface

Categories

(Core :: Privacy: Anti-Tracking, enhancement)

enhancement

Tracking

()

People

(Reporter: tjr, Unassigned)

References

(Blocks 1 open bug)

Details

Summary

speechSynthesis.getVoices() on macOS exposes the user's installed-voice configuration to the web. The data carried in voices_sha1, voices_count, and voices_default together produces a per-user signal whose dominant axis is macOS major version (which might also leak through fonts) but with a residual that uniquely identifies subsets of users — those who downloaded optional voices, those running specific macOS point releases, those whose default voice is in a different locale than their system locale, and (on older macOS versions) those who downloaded the legacy "Premium" voice variants.

This bug is about characterising that residual and proposing mitigations.

Background

speechSynthesis.getVoices() is the Web Speech API call web pages use to enumerate text-to-speech voices on the user's machine. Each returned SpeechSynthesisVoice has a name, voiceURI identifier, a lang property, and a default boolean. Firefox-on-macOS populates the list via the legacy NSSpeechSynthesizer API. Apple ships a default voice catalogue with each macOS release; users can download additional voices through System Settings → Accessibility → Spoken Content.

Current situation

Axis 1: macOS major version determines the voice count

Each macOS major version ships a different default voice catalogue. The voice count Firefox reports for a user with no extra downloads is essentially f(macOS major):

macOS Darwin dominant voices_count share of that macOS version on the dominant count
10.15 Catalina 19 48 86.97%
11 Big Sur 20 48 94.01%
12 Monterey 21 48 93.41%
13 Ventura 22 143 92.34%
14 Sonoma 23 143 95.11%
15 Sequoia 24 177 82.89%
26 Tahoe 25 184 83.41%

This axis exposes the macOS major version - the UA reports Mac OS X 10.15 on every macOS version due to UA freezing, but the voice count differentiates 11/12/13/14/15/26 cleanly. Knowing the user's voices_count tells you their macOS major version.

Axis 2: variation around the dominant voice count — within-version structure

For each macOS version, the share of users who don't sit on the dominant voice count splits into three buckets: users with more voices (downloaded extras), users with fewer non-zero voices, and users with zero voices (Web Speech disabled or unavailable for them).

For macOS 10.15 through 14, the "fewer" share is negligible — the variation is essentially just "did the user download any extras":

macOS dominant share more voices fewer (non-zero) zero
10.15 Catalina 86.97% 2.22% 0.03% 10.78%
11 Big Sur 94.01% 2.27% 0.01% 3.71%
12 Monterey 93.41% 2.02% 0.01% 4.56%
13 Ventura 92.34% 4.04% 0.02% 3.59%
14 Sonoma 95.11% 3.30% 0.02% 1.57%

For macOS 15 (Sequoia) and macOS 26 (Tahoe), the "fewer" share is significant — but most of it tracks Apple point-release variation, not user-installed voices. Broken out by point release:

Sequoia / Darwin 24.x:

os_version share of Sequoia dominant voices_count dominant % zero % more % fewer (non-zero) % top non-dominant cohorts
24.0.0 1.45% 176 100.00% 0.00% 0.00% 0.00% —
24.1.0 2.56% 176 100.00% 0.00% 0.00% 0.00% —
24.2.0 1.58% 176 100.00% 0.00% 0.00% 0.00% —
24.3.0 4.82% 176 98.62% 0.63% 0.74% 0.00% 177 (0.74%)
24.4.0 4.09% 176 97.86% 1.34% 0.80% 0.00% 177 (0.80%)
24.5.0 9.15% 177 98.77% 0.54% 0.69% 0.00% 178 (0.69%)
24.6.0 76.35% 177 96.85% 1.42% 1.69% 0.04% 178 (1.27%), 179 (0.20%)

Apple bumped the voice count from 176 to 177 at the 24.5.0 point release. Users who haven't updated past 24.4.0 sit on the older 176-voice catalogue; users on 24.5.0+ sit on 177. This isn't "fewer voices because of user behaviour"; it's "older Sequoia point release ships fewer voices."

Tahoe / Darwin 25.x:

os_version share of Tahoe dominant voices_count dominant % zero % more % fewer (non-zero) % top non-dominant cohorts
25.0.0 2.04% 184 92.51% 2.47% 0.85% 4.17% 180 (4.17%)
25.1.0 2.34% 184 91.00% 2.58% 0.92% 5.50% 180 (5.50%)
25.2.0 10.16% 184 90.90% 2.76% 1.12% 5.22% 180 (5.22%)
25.3.0 52.54% 184 88.36% 2.68% 1.78% 7.17% 180 (7.03%), 185 (1.30%)
25.4.0 32.15% 184 72.14% 4.06% 1.70% 22.10% 72 (17.40%), 180 (3.49%)
25.5.0 0.77% 184 90.49% 3.71% 2.14% 3.65% 180 (3.65%)

Tahoe shows an architecture-bifurcated voice catalogue, not two independent cohorts. On Apple Silicon the baseline is 184 voices (~97% of aarch64 users on 25.0–25.3 and 25.5); on Intel the baseline is 180 voices (~72–75% of
x86-64 users across all Tahoe releases). The 4-voice gap between arches is stable but undocumented by Apple. The 4–7% aggregate 180-share is simply the Intel-Mac share of Tahoe users, not a meaningful fingerprintable cohort. A plausible (but unconfirmed) explanation is that a small number of Tahoe voices are gated on Apple Silicon — for example, on-device neural variants that depend on the Neural Engine, or Personal Voice template/placeholder
slots, since Personal Voice creation is Apple-Silicon-only — which would naturally produce a fixed 4-voice surplus on aarch64 without Apple documenting it as a catalogue diff.

The real anomaly is a 25.4.0-specific paired regression on both arches: aarch64 drops from 184 to 72
voices for 18.22% of aarch64 users (17.40% of all 25.4.0), and Intel mirrors it with 68 voices for 16.79% of x86-64 users. Every member of each reduced cohort shares one identical voices_all_ssdeep value, so the missing 112
voices are exactly the same set for every affected user — not a partial download. voices_sample snapshots show the absent voices are the #ap: "Personality" family (Shelley, Grandpa, Rocko, Eddy, etc.), likely bundled with their
Enhanced/Siri-quality variants. The cohort holds at full size across the entire data window with no decay, ruling out "still downloading". The
most plausible cause is a Speech.framework cache invalidation tied to the Personal Voice 2026 / SpeechAnalyzer migration shipped in macOS 26.4; the same shape of regression hit macOS 12.3 (WebKit bug 237584), where
speechSynthesis.getVoices() silently lost entries until the next major OS release reset the state.

Because no public macOS API exists for an app to request the missing packs, affected users can't be auto-remediated from within Firefox — they would need to either reinstall the voices manually via System Settings →
Accessibility → Spoken Content, or install a macOS update to reset the Speech.framework state the way Ventura did after the 12.3 regression. The pragmatic Firefox-side options are a SUMO article documenting
the workaround, and/or a one-time info banner triggered when getVoices() returns the 72/68-voice signature on Darwin 25.4.0, deep-linking the user to the Accessibility settings pane via
x-apple.systempreferences:com.apple.Accessibility-Settings.extension.

Axis 3: default voice — locale-specific names and Premium voices

voices_default carries the identifier of the voice the user has selected as their macOS system default. The distribution for users with a populated default is dominated by the OS's shipped default for the user's locale, but a long tail exists.

Pre-Ventura (10.15 / 11 / 12) — uses the legacy #m:#as: URI scheme. Top defaults on macOS 12 Monterey (representative; the other pre-Ventura versions have nearly the same shape):

% of populated cohort voices_default locale
35.99% #m:#as:Alex en-US
11.97% #m:#as:thomas fr-FR
9.77% #m:#as:anna de-DE
6.07% #m:#as:daniel en-GB
5.48% #m:#as:monica es-ES
3.54% #m:#as:alice it-IT
1.83% #m:#as:thomas.premium fr-FR (Premium download)
1.61% #m:#as:karen en-AU
1.38% #m:#as:anna.premium de-DE (Premium)
1.37% #m:#as:luciana pt-BR
1.00% #m:#as:monica.premium es-ES (Premium)
0.96% #m:#as:daniel.premium en-GB (Premium)

The bolded .premium entries are users who downloaded the higher-quality "Premium" version of a voice through System Settings and made it their default. In aggregate, ~7-10% of pre-Ventura users with a populated default expose a .premium-suffixed voiceURI, which uniquely identifies them as a Premium-voice downloader plus their locale.

Ventura and later — Apple migrated voice identifiers to the new #m:#ac:lang-REGION.Name URI scheme at macOS 13. The dominant en-US default changed from Alex to Samantha. Premium voices were renamed under a separate com.apple.voice.premium.* scheme that Firefox's URI compression doesn't shorten, so they no longer appear with a .premium suffix in the high-share rows. On Sequoia (15) — typical of Ventura/Sonoma/Sequoia:

% of populated cohort voices_default
45.16% #m:#ac:en-US.Samantha
13.49% #m:#ac:en-GB.Daniel
9.58% #m:#ac:de-DE.Anna
7.56% #m:#ac:fr-FR.Thomas
2.59% #m:#ac:en-IN.Rishi
2.32% #m:#ac:en-AU.Karen
2.23% #m:#ac:it-IT.Alice
2.17% #m:#ac:es-MX.Paulina
(remainder spread across 295 more values, each < 2%)

Axis 4: users whose default voice isn't the modal default for their system locale

We computed the modal (most common) voices_default for each system locale. A user "matches" if their voice equals the modal for users in their (macOS version, locale) cell; they "mismatch" if they picked a non-modal value.

macOS analyzable users match modal mismatch modal
10.15 Catalina 108,252 86.54% 13.46%
11 Big Sur 76,385 87.91% 12.09%
12 Monterey 185,755 88.17% 11.83%
13 Ventura 140,823 98.78% 1.22%
14 Sonoma 180,317 98.80% 1.20%
15 Sequoia 214,765 97.71% 2.29%
26 Tahoe 116,356 91.44% 8.56%

("analyzable users" = users with a populated default whose system_locale parsed cleanly and whose locale had ≥ 100 users in this dataset — small enough to have a stable mode. Roughly 5-15% of users per version are excluded for these reasons.)

Two regimes are visible:

  1. Pre-Ventura (10.15-12): ~12-13% mismatch. This is dominated by the Premium-voice population from Axis 3. Users who downloaded the .premium version of their locale's voice and selected it as their default are mismatches under this measure — the non-Premium voice is the modal for every locale, so the Premium downloader counts as deviating. The 11-12% mismatch share lines up well with the ~7-10% aggregate Premium share noted in Axis 3, plus a residual of users who picked a non-default-region voice (en-GB Daniel on a US system, etc.).

  2. Ventura and Sonoma: ~1.2% mismatch. Apple's Ventura rename of Premium voices (out of the .premium suffix scheme) moved them out of the "compressed prefix" rows Firefox tracks, and most users sit on their locale's stock voice. This is the tightest period — the locale modal genuinely is the modal for almost everyone.

  3. Sequoia: 2.29% mismatch. Slight uptick over Sonoma; likely a mix of users who manually selected an alternate voice and users on intermediate macOS-version states.

  4. Tahoe: 8.56% mismatch — qualitatively different from the others. The populated-default cohort on Tahoe is the self-selected ~14% of Tahoe users who explicitly opted out of the Siri system default. That cohort is over-represented in users with non-modal preferences (notably, en-US system locale users who picked en-GB.Daniel for accent reasons — Daniel jumps to 39% of populated defaults across all Tahoe users with a populated default, and that population is concentrated within en-US system locale users who chose to opt out of Siri).

Proposed mitigations

Option 1 — Static voice allowlist

Define a small, fixed set of voices Firefox exposes via speechSynthesis.getVoices() regardless of what macOS actually has installed, and regardless of macOS version. The allowlist would be the same across every Firefox-on-macOS user.

What this eliminates:

  • The per-user installed-voice signal. Users who downloaded extra voices (Axes 2's "more voices" cohort, and the long-tail values in voices_sha1 within each macOS version) become indistinguishable from users who didn't.
  • The .premium-suffix signal on pre-Ventura macOS.
  • The macOS major version signal carried by voice count — every Firefox-on-macOS user reports the same count.
  • The macOS point-release signal on Sequoia/Tahoe (24.5.0 vs 24.6.0 / 25.4.0 anomaly) — collapsed.

Drawbacks:

  • Web content can no longer choose voices that exist on the user's system but aren't on the allowlist. Read-aloud features that depend on a specific voice (often a localized voice the page expects to be present) may degrade.
  • The allowlist has to be carefully chosen so that the voices it lists are actually present across all macOS versions and locales Firefox supports.

Option 2 — OS-version-adjusted allowlist (potentially as a tiered RFPTarget)

A relaxation of Option 1: maintain a per-macOS-version allowlist instead of a single static one. The allowlist on Ventura might include the new compact-scheme voices that don't exist pre-Ventura; the allowlist on Sequoia might include the additional voices that shipped at 15.

Rationale: The macOS major version is already partially leaked to the web through other channels — most notably the installed font set, which differs across macOS versions. If macOS major version is recoverable from fonts anyway, holding the line on "voices must not reveal macOS major" doesn't buy as much as it would otherwise.

Drawbacks:

  • The macOS major version exposed via this path is more granular than what UA-frozen Firefox currently exposes (10.15 in the UA vs. real version). Even if fonts leak it indirectly, this path leaks it directly with no inference step.
  • Maintenance: the allowlist needs to be updated as Apple ships new voices.

A tiered approach as two separate RFPTargets could split the trade-off explicitly:

  • SpeechSynthesisVoices (strict) — static allowlist, hides macOS version. The default behaviour for the strongest fingerprinting-protection settings.
  • SpeechSynthesisVoicesByOSVersion (relaxed) — per-macOS-version allowlist, eliminates per-user installed-voice signal but allows macOS-version inference. The default behaviour for less aggressive protection levels where consistency with Accept-Language / font / UA-recoverable information is acceptable.

The tiered design lets the strict-protection users get the maximum reduction while preserving usability for the looser-protection majority.

Option 3 — Constrain voices_default regardless of allowlist choice

Independent of the enumeration treatment, voices_default could be replaced with the modal default voice for the user's system locale (and macOS major version). Under this fix, every user with a populated default reports the value most commonly selected by other users in their (macOS version × system_locale) cohort. This collapses, in one move:

  • The Premium-voice signal (the modal default for any locale is the non-Premium voice; Premium downloaders are unified with the basic-voice population).
  • The locale-region-mismatch signal (en-US users with en-GB.Daniel get reset to the en-US modal, whether or not their system locale's typical voice is in a different region).
  • The "manually selected an alternate same-locale voice" signal (e.g., en-US users who picked Alex on Sequoia/Tahoe get reset to Samantha).

Per Axis 4, this would collapse the 1.2-13.5% of users per macOS version whose default isn't already the modal for their locale back into the modal cohort. Option 3 is compatible with Option 1 or Option 2 and can be implemented independently.

Expected impact

We simulated Options 1 and 2 against the full Firefox-on-macOS population and scored each user against FingerprintPro. Each filter is applied on top of the Phase 0-3 cumulative protection set (font protections, fdlibm, processor-count cap, screen-avail clamp, touch-point clamp, font list updates, WebGL vendor/renderer normalization, storage-quota normalization, etc.). The metric is "% of Darwin users uniquely identified by the fingerprinter" — lower is better.

Configuration FPJSv2 (Realistic)
Baseline (no protections) 82.63%
Phase 0-3 protections only 41.11%
Phase 0-3 + Option 2 (OS-version-adjusted allowlist) 39.52%
Phase 0-3 + Option 1 (Static allowlist) 36.18%
You need to log in before you can comment on or make changes to this bug.