Address Mac voice-list fingerprint surface
Categories
(Core :: Privacy: Anti-Tracking, 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:
-
Pre-Ventura (10.15-12): ~12-13% mismatch. This is dominated by the Premium-voice population from Axis 3. Users who downloaded the
.premiumversion 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.). -
Ventura and Sonoma: ~1.2% mismatch. Apple's Ventura rename of Premium voices (out of the
.premiumsuffix 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. -
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.
-
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.Danielfor 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 withAccept-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.Danielget 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% |
Description
•