Skip to content

Commit 07b5235

Browse files
committed
Change GSUB limit to be based on input size
This helps mitigate the billion laughs attack where a font expands the text over and over again. This is demonstrated by the GSUB-3 test from the unicode text-rendering-tests, which I've imported as part of this change. See also #48 and #97
1 parent 15156d7 commit 07b5235

File tree

12 files changed

+188
-18
lines changed

12 files changed

+188
-18
lines changed

src/gsub.rs

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ use crate::unicode::VariationSelector;
2727
use crate::{tag, GlyphId};
2828

2929
const SUBST_RECURSION_LIMIT: usize = 2;
30-
const MAX_GLYPHS: usize = 10_000;
30+
// Matches Harfbuzz:
31+
// https://github.com/harfbuzz/harfbuzz/blob/8062c372590980d36d5b4cc720d33dca2662c56e/src/hb-limits.hh#L32
32+
pub(crate) const MAX_GLYPHS_FACTOR: usize = 256;
3133

3234
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
3335
pub struct FeatureInfo {
@@ -294,6 +296,7 @@ pub fn gsub_apply_lookup<T: GlyphData>(
294296
feature_tag: u32,
295297
opt_alternate: Option<usize>,
296298
glyphs: &mut Vec<RawGlyph<T>>,
299+
max_glyphs: usize,
297300
start: usize,
298301
mut length: usize,
299302
pred: impl Fn(&RawGlyph<T>) -> bool,
@@ -313,7 +316,7 @@ pub fn gsub_apply_lookup<T: GlyphData>(
313316
let mut i = start;
314317
while i < start + length {
315318
if match_type.match_glyph(opt_gdef_table, &glyphs[i]) && pred(&glyphs[i]) {
316-
match multiplesubst(subtables, i, glyphs)? {
319+
match multiplesubst(subtables, i, glyphs, max_glyphs)? {
317320
Some(replace_count) => {
318321
i += replace_count;
319322
length += replace_count;
@@ -364,6 +367,7 @@ pub fn gsub_apply_lookup<T: GlyphData>(
364367
match_type,
365368
i,
366369
glyphs,
370+
max_glyphs,
367371
)? {
368372
Some((input_length, changes)) => {
369373
i += input_length;
@@ -390,6 +394,7 @@ pub fn gsub_apply_lookup<T: GlyphData>(
390394
match_type,
391395
i,
392396
glyphs,
397+
max_glyphs,
393398
)? {
394399
Some((input_length, changes)) => {
395400
i += input_length;
@@ -460,11 +465,18 @@ fn multiplesubst<T: GlyphData>(
460465
subtables: &[MultipleSubst],
461466
i: usize,
462467
glyphs: &mut Vec<RawGlyph<T>>,
468+
max_glyphs: usize,
463469
) -> Result<Option<usize>, ParseError> {
464470
match multiplesubst_would_apply(subtables, i, glyphs)? {
465471
Some(sequence_table) => {
466-
if sequence_table.substitute_glyphs.len() + glyphs.len() >= MAX_GLYPHS {
467-
return Err(ParseError::LimitExceeded);
472+
if sequence_table.substitute_glyphs.len() + glyphs.len() >= max_glyphs {
473+
// The Unicode text rendering tests say that shaping should stop when the limit is
474+
// reached, but not fail:
475+
//
476+
// "If your implementation is immune to this attack, it should neither crash nor
477+
// hang when rendering lol with this font. Instead, your implementation should stop
478+
// executing once its internal buffer has reached a size limit."
479+
return Ok(Some(0));
468480
}
469481

470482
if !sequence_table.substitute_glyphs.is_empty() {
@@ -590,6 +602,7 @@ fn contextsubst<T: GlyphData>(
590602
match_type: MatchType,
591603
i: usize,
592604
glyphs: &mut Vec<RawGlyph<T>>,
605+
max_glyphs: usize,
593606
) -> Result<Option<(usize, isize)>, ParseError> {
594607
match contextsubst_would_apply(opt_gdef_table, subtables, match_type, i, glyphs)? {
595608
Some(subst) => apply_subst_context(
@@ -602,6 +615,7 @@ fn contextsubst<T: GlyphData>(
602615
&subst,
603616
i,
604617
glyphs,
618+
max_glyphs,
605619
),
606620
None => Ok(None),
607621
}
@@ -637,6 +651,7 @@ fn chaincontextsubst<T: GlyphData>(
637651
match_type: MatchType,
638652
i: usize,
639653
glyphs: &mut Vec<RawGlyph<T>>,
654+
max_glyphs: usize,
640655
) -> Result<Option<(usize, isize)>, ParseError> {
641656
match chaincontextsubst_would_apply(opt_gdef_table, subtables, match_type, i, glyphs)? {
642657
Some(subst) => apply_subst_context(
@@ -649,6 +664,7 @@ fn chaincontextsubst<T: GlyphData>(
649664
&subst,
650665
i,
651666
glyphs,
667+
max_glyphs,
652668
),
653669
None => Ok(None),
654670
}
@@ -702,6 +718,7 @@ fn apply_subst_context<T: GlyphData>(
702718
subst: &SubstContext<'_>,
703719
i: usize,
704720
glyphs: &mut Vec<RawGlyph<T>>,
721+
max_glyphs: usize,
705722
) -> Result<Option<(usize, isize)>, ParseError> {
706723
let mut changes = 0;
707724
let len = match match_type.find_nth(
@@ -724,6 +741,7 @@ fn apply_subst_context<T: GlyphData>(
724741
usize::from(*subst_lookup_index),
725742
feature_tag,
726743
glyphs,
744+
max_glyphs,
727745
i,
728746
)? {
729747
Some(changes0) => changes += changes0,
@@ -754,6 +772,7 @@ fn apply_subst<T: GlyphData>(
754772
lookup_index: usize,
755773
feature_tag: u32,
756774
glyphs: &mut Vec<RawGlyph<T>>,
775+
max_glyphs: usize,
757776
index: usize,
758777
) -> Result<Option<isize>, ParseError> {
759778
let lookup = lookup_list.lookup_cache_gsub(gsub_cache, lookup_index)?;
@@ -767,10 +786,12 @@ fn apply_subst<T: GlyphData>(
767786
singlesubst(subtables, feature_tag, &mut glyphs[i])?;
768787
Ok(Some(0))
769788
}
770-
SubstLookup::MultipleSubst(ref subtables) => match multiplesubst(subtables, i, glyphs)? {
771-
Some(replace_count) => Ok(Some((replace_count as isize) - 1)),
772-
None => Ok(None),
773-
},
789+
SubstLookup::MultipleSubst(ref subtables) => {
790+
match multiplesubst(subtables, i, glyphs, max_glyphs)? {
791+
Some(replace_count) => Ok(Some((replace_count as isize) - 1)),
792+
None => Ok(None),
793+
}
794+
}
774795
SubstLookup::AlternateSubst(ref subtables) => {
775796
alternatesubst(subtables, 0, &mut glyphs[i])?;
776797
Ok(Some(0))
@@ -793,6 +814,7 @@ fn apply_subst<T: GlyphData>(
793814
match_type,
794815
i,
795816
glyphs,
817+
max_glyphs,
796818
)? {
797819
Some((_length, change)) => Ok(Some(change)),
798820
None => Ok(None),
@@ -813,6 +835,7 @@ fn apply_subst<T: GlyphData>(
813835
match_type,
814836
i,
815837
glyphs,
838+
max_glyphs,
816839
)? {
817840
Some((_length, change)) => Ok(Some(change)),
818841
None => Ok(None),
@@ -1077,6 +1100,7 @@ pub fn apply(
10771100
num_glyphs: u16,
10781101
glyphs: &mut Vec<RawGlyph<()>>,
10791102
) -> Result<(), ShapingError> {
1103+
let max_glyphs = glyphs.len().saturating_mul(MAX_GLYPHS_FACTOR);
10801104
match features {
10811105
Features::Custom(features_list) => gsub_apply_custom(
10821106
gsub_cache,
@@ -1087,6 +1111,7 @@ pub fn apply(
10871111
tuple,
10881112
num_glyphs,
10891113
glyphs,
1114+
max_glyphs,
10901115
),
10911116
Features::Mask(feature_mask) => gsub_apply_default(
10921117
dotted_circle_index,
@@ -1098,6 +1123,7 @@ pub fn apply(
10981123
tuple,
10991124
num_glyphs,
11001125
glyphs,
1126+
max_glyphs,
11011127
),
11021128
}
11031129
}
@@ -1111,6 +1137,7 @@ fn gsub_apply_custom(
11111137
tuple: Option<Tuple<'_>>,
11121138
num_glyphs: u16,
11131139
glyphs: &mut Vec<RawGlyph<()>>,
1140+
max_glyphs: usize,
11141141
) -> Result<(), ShapingError> {
11151142
let gsub_table = &gsub_cache.layout_table;
11161143
if let Some(script) = gsub_table.find_script_or_default(script_tag)? {
@@ -1137,6 +1164,7 @@ fn gsub_apply_custom(
11371164
tag::RVRN,
11381165
None,
11391166
glyphs,
1167+
max_glyphs,
11401168
0,
11411169
glyphs.len(),
11421170
|_| true,
@@ -1156,6 +1184,7 @@ fn gsub_apply_custom(
11561184
feature_tag,
11571185
alternate,
11581186
glyphs,
1187+
max_glyphs,
11591188
glyphs.len() - 1,
11601189
1,
11611190
|_| true,
@@ -1169,6 +1198,7 @@ fn gsub_apply_custom(
11691198
feature_tag,
11701199
alternate,
11711200
glyphs,
1201+
max_glyphs,
11721202
0,
11731203
glyphs.len(),
11741204
|_| true,
@@ -1457,6 +1487,7 @@ fn gsub_apply_default(
14571487
tuple: Option<Tuple<'_>>,
14581488
num_glyphs: u16,
14591489
glyphs: &mut Vec<RawGlyph<()>>,
1490+
max_glyphs: usize,
14601491
) -> Result<(), ShapingError> {
14611492
let gsub_table = &gsub_cache.layout_table;
14621493
let feature_variations = gsub_table.feature_variations(tuple)?;
@@ -1480,6 +1511,7 @@ fn gsub_apply_default(
14801511
opt_lang_tag,
14811512
feature_variations,
14821513
glyphs,
1514+
max_glyphs,
14831515
)?;
14841516
}
14851517
feature_mask.remove(FeatureMask::RVRN);
@@ -1493,6 +1525,7 @@ fn gsub_apply_default(
14931525
opt_lang_tag,
14941526
feature_variations,
14951527
glyphs,
1528+
max_glyphs,
14961529
)?,
14971530
ScriptType::Indic => scripts::indic::gsub_apply_indic(
14981531
dotted_circle_index,
@@ -1531,6 +1564,7 @@ fn gsub_apply_default(
15311564
opt_lang_tag,
15321565
feature_variations,
15331566
glyphs,
1567+
max_glyphs,
15341568
)?,
15351569
ScriptType::ThaiLao => scripts::thai_lao::gsub_apply_thai_lao(
15361570
gsub_cache,
@@ -1540,6 +1574,7 @@ fn gsub_apply_default(
15401574
opt_lang_tag,
15411575
feature_variations,
15421576
glyphs,
1577+
max_glyphs,
15431578
)?,
15441579
ScriptType::Default => {
15451580
feature_mask &= get_supported_features(gsub_cache, script_tag, opt_lang_tag)?;
@@ -1569,6 +1604,7 @@ fn gsub_apply_default(
15691604
lookups,
15701605
lookups_frac,
15711606
glyphs,
1607+
max_glyphs,
15721608
)?;
15731609
} else {
15741610
let index = get_lookups_cache_index(
@@ -1579,7 +1615,14 @@ fn gsub_apply_default(
15791615
feature_mask,
15801616
)?;
15811617
let lookups = &gsub_cache.cached_lookups.lock().unwrap()[index];
1582-
gsub_apply_lookups(gsub_cache, gsub_table, opt_gdef_table, lookups, glyphs)?;
1618+
gsub_apply_lookups(
1619+
gsub_cache,
1620+
gsub_table,
1621+
opt_gdef_table,
1622+
lookups,
1623+
glyphs,
1624+
max_glyphs,
1625+
)?;
15831626
}
15841627
}
15851628
}
@@ -1595,13 +1638,15 @@ fn gsub_apply_lookups(
15951638
opt_gdef_table: Option<&GDEFTable>,
15961639
lookups: &[(usize, u32)],
15971640
glyphs: &mut Vec<RawGlyph<()>>,
1641+
max_glyphs: usize,
15981642
) -> Result<(), ShapingError> {
15991643
gsub_apply_lookups_impl(
16001644
gsub_cache,
16011645
gsub_table,
16021646
opt_gdef_table,
16031647
lookups,
16041648
glyphs,
1649+
max_glyphs,
16051650
0,
16061651
glyphs.len(),
16071652
)?;
@@ -1614,6 +1659,7 @@ fn gsub_apply_lookups_impl(
16141659
opt_gdef_table: Option<&GDEFTable>,
16151660
lookups: &[(usize, u32)],
16161661
glyphs: &mut Vec<RawGlyph<()>>,
1662+
max_glyphs: usize,
16171663
start: usize,
16181664
mut length: usize,
16191665
) -> Result<usize, ShapingError> {
@@ -1626,6 +1672,7 @@ fn gsub_apply_lookups_impl(
16261672
*feature_tag,
16271673
None,
16281674
glyphs,
1675+
max_glyphs,
16291676
start,
16301677
length,
16311678
|_| true,
@@ -1641,6 +1688,7 @@ fn gsub_apply_lookups_frac(
16411688
lookups: &[(usize, u32)],
16421689
lookups_frac: &[(usize, u32)],
16431690
glyphs: &mut Vec<RawGlyph<()>>,
1691+
max_glyphs: usize,
16441692
) -> Result<(), ShapingError> {
16451693
let mut i = 0;
16461694
while i < glyphs.len() {
@@ -1652,6 +1700,7 @@ fn gsub_apply_lookups_frac(
16521700
opt_gdef_table,
16531701
lookups,
16541702
glyphs,
1703+
max_glyphs,
16551704
i,
16561705
start_pos,
16571706
)?;
@@ -1662,6 +1711,7 @@ fn gsub_apply_lookups_frac(
16621711
opt_gdef_table,
16631712
lookups_frac,
16641713
glyphs,
1714+
max_glyphs,
16651715
i,
16661716
end_pos - start_pos + 1,
16671717
)?;
@@ -1672,6 +1722,7 @@ fn gsub_apply_lookups_frac(
16721722
opt_gdef_table,
16731723
lookups,
16741724
glyphs,
1725+
max_glyphs,
16751726
i,
16761727
glyphs.len() - i,
16771728
)?;
@@ -1717,6 +1768,7 @@ fn apply_rvrn(
17171768
opt_lang_tag: Option<u32>,
17181769
feature_variations: Option<&FeatureTableSubstitution<'_>>,
17191770
glyphs: &mut Vec<RawGlyph<()>>,
1771+
max_glyphs: usize,
17201772
) -> Result<(), ShapingError> {
17211773
let gsub_table = &gsub_cache.layout_table;
17221774
let index = get_lookups_cache_index(
@@ -1727,13 +1779,24 @@ fn apply_rvrn(
17271779
FeatureMask::RVRN,
17281780
)?;
17291781
let lookups = &gsub_cache.cached_lookups.lock().unwrap()[index];
1730-
gsub_apply_lookups(gsub_cache, gsub_table, opt_gdef_table, lookups, glyphs)?;
1782+
gsub_apply_lookups(
1783+
gsub_cache,
1784+
gsub_table,
1785+
opt_gdef_table,
1786+
lookups,
1787+
glyphs,
1788+
max_glyphs,
1789+
)?;
17311790
Ok(())
17321791
}
17331792

17341793
#[cfg(test)]
17351794
mod tests {
17361795
use super::*;
1796+
use crate::{
1797+
binary::read::ReadScope, font::MatchingPresentation, font_data::FontData,
1798+
tests::read_fixture, Font,
1799+
};
17371800

17381801
#[test]
17391802
fn feature_mask_iter() {
@@ -1769,4 +1832,26 @@ mod tests {
17691832
];
17701833
assert_eq!(&mask.features().collect::<Vec<_>>(), expected);
17711834
}
1835+
1836+
#[test]
1837+
fn billion_laughs() -> Result<(), Box<dyn std::error::Error>> {
1838+
let data = read_fixture("tests/fonts/opentype/TestGSUBThree.ttf");
1839+
let scope = ReadScope::new(&data);
1840+
let font_file = scope.read::<FontData<'_>>()?;
1841+
let provider = font_file.table_provider(0)?;
1842+
let mut font = Font::new(provider)?;
1843+
1844+
// Map text to glyphs and then apply font shaping
1845+
let script = tag::LATN;
1846+
let lang = tag!(b"ENG ");
1847+
let features = Features::default();
1848+
let glyphs = font.map_glyphs("lol", script, MatchingPresentation::NotRequired);
1849+
let infos = font
1850+
.shape(glyphs, script, Some(lang), &features, None, true)
1851+
.map_err(|(err, _info)| err)?;
1852+
1853+
assert_eq!(infos.len(), 759);
1854+
1855+
Ok(())
1856+
}
17721857
}

0 commit comments

Comments
 (0)