From ad697a88d97e28f63da253ea287982d185db7d90 Mon Sep 17 00:00:00 2001 From: Jake Writer Date: Fri, 28 Aug 2026 12:53:31 -0600 Subject: [PATCH] fix(stealth): perturb glyph advances only, not positioning offsets (#741) The font-spacing randomisation in anti-font-fingerprinting.patch fed a running total into each glyph's x_offset/y_offset as well as its advance: glyphPositions[i].x_advance += spacing; glyphPositions[i].x_offset += cumulativeOffset; cumulativeOffset += spacing; x_offset is the GPOS positioning offset -- the thing that parks a combining mark over its base. It does not contribute to the measured width, so it adds nothing to the metric the randomisation exists to perturb, but layout never sees it either: gfxTextRun stores advances, while painting honours the offsets. The running total therefore paints every glyph a further `spacing` past where layout placed it, and detaches every combining mark from its base. Latin hides this because Gecko's word cache shapes each space-delimited word separately, so the accumulator resets constantly. Thai has no inter-word spaces, so a whole paragraph is a single shaping run and the error grows without bound. Measured on 152.0.4-beta.29 at the worst-case seed, painted text overran its layout box by: Thai 1x run 4.90px Latin 1x run 0.00px Thai 2x run 9.80px Latin 2x run 0.00px Thai 4x run 20.60px Latin 4x run 0.00px which is the garbled Thai in #741 -- and, since the seed is randomised per launch, why it looks like an intermittent regression rather than a bug. Perturb advances only, and skip zero-advance glyphs: a combining mark is not a separate character, and widening it pushes the following base away and strands the mark. Every advancing glyph still gets +spacing, so the measured width -- the actual fingerprinting signal -- is perturbed exactly as before (verified: 4/8/16/32px by run length, unchanged). After the fix the overrun is 0.00px for Thai at every run length. --- patches/anti-font-fingerprinting.patch | 32 ++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/patches/anti-font-fingerprinting.patch b/patches/anti-font-fingerprinting.patch index 3dd48a6..a30c48a 100644 --- a/patches/anti-font-fingerprinting.patch +++ b/patches/anti-font-fingerprinting.patch @@ -851,7 +851,7 @@ index e41408f51c..ffa0bd4caf 100644 #include -@@ -1465,6 +1469,36 @@ bool gfxHarfBuzzShaper::ShapeText(const char16_t* aText, uint32_t aOffset, +@@ -1465,6 +1469,50 @@ bool gfxHarfBuzzShaper::ShapeText(const char16_t* aText, uint32_t aOffset, hb_shape(mHBFont, mBuffer, features.Elements(), features.Length()); @@ -870,18 +870,32 @@ index e41408f51c..ffa0bd4caf 100644 + hb_glyph_position_t* glyphPositions = + hb_buffer_get_glyph_positions(mBuffer, &glyphCount); + -+ hb_position_t cumulativeOffset = 0; -+ -+ // Apply custom letter spacing ++ // Perturb ADVANCES only. ++ // ++ // x_offset/y_offset are the positioning offsets GPOS uses to place a glyph ++ // relative to the pen -- most importantly to park a combining mark over ++ // its base. They do not contribute to the measured width, so they add ++ // nothing to the metric we are perturbing, but layout never sees them: ++ // gfxTextRun stores advances, while painting honours the offsets. Feeding ++ // a running total into them paints every glyph a further `spacing` past ++ // where layout placed it and detaches every combining mark from its base. ++ // Scripts whose marks carry a zero advance -- Thai, Lao, Arabic, ++ // Devanagari, Hebrew -- collapse into stacked glyphs, while Latin merely ++ // looks slightly loose (daijro/camoufox#741). ++ // ++ // Zero-advance glyphs are skipped for the same reason: a combining mark is ++ // not a separate character, and widening it pushes the following base away ++ // and strands the mark. Every advancing glyph still gets +spacing, so the ++ // measured width -- the actual fingerprinting signal -- is perturbed ++ // exactly as before. + for (uint32_t i = 0; i < glyphCount; ++i) { + if (aVertical) { -+ glyphPositions[i].y_advance -= spacing; -+ glyphPositions[i].y_offset -= cumulativeOffset; -+ } else { ++ if (glyphPositions[i].y_advance != 0) { ++ glyphPositions[i].y_advance -= spacing; ++ } ++ } else if (glyphPositions[i].x_advance != 0) { + glyphPositions[i].x_advance += spacing; -+ glyphPositions[i].x_offset += cumulativeOffset; + } -+ cumulativeOffset += spacing; + } + } +