Skip to content

Commit 52d367e

Browse files
fix: reject self-referential GSUB/GPOS extension lookups (#235)
An extension lookup pointing at the extension type recursed unboundedly: a stack-overflow abort in debug and, via tail-call optimisation, an infinite loop in release. Guards on the lookup type, which also kills non-self-referential extension chains. Closes #192.
1 parent 8940e34 commit 52d367e

3 files changed

Lines changed: 69 additions & 2 deletions

File tree

src/ggg/lookup.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,83 @@ impl FromData for LookupFlags {
150150

151151
pub(crate) fn parse_extension_lookup<'a, T: 'a>(
152152
data: &'a [u8],
153+
ext_kind: u16,
153154
parse: impl FnOnce(&'a [u8], u16) -> Option<T>,
154155
) -> Option<T> {
155156
let mut s = Stream::new(data);
156157
let format = s.read::<u16>()?;
157158
match format {
158159
1 => {
159160
let kind = s.read::<u16>()?;
161+
// An Extension subtable must reference a lookup type other than the
162+
// extension type itself. The OpenType spec requires extensionLookupType
163+
// to "be set to any lookup type other than the extension lookup type".
164+
// Without this check, a self-referential extension (e.g. kind == ext_kind
165+
// with extensionOffset == 0) makes `parse` re-enter this function forever
166+
// and overflow the stack on malformed fonts.
167+
if kind == ext_kind {
168+
return None;
169+
}
160170
let offset = s.read::<Offset32>()?.to_usize();
161171
parse(data.get(offset..)?, kind)
162172
}
163173
_ => None,
164174
}
165175
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use super::LookupSubtable;
180+
use crate::gpos::PositioningSubtable;
181+
use crate::gsub::SubstitutionSubtable;
182+
183+
// A GSUB Extension subtable (type 7) whose inner extensionLookupType is
184+
// itself 7 and whose extensionOffset is 0 references its own bytes. Before
185+
// the recursion guard this made the parser re-enter itself forever and
186+
// overflow the stack. It must now be rejected without recursing.
187+
#[test]
188+
fn gsub_self_referential_extension_is_rejected() {
189+
let data: &[u8] = &[
190+
0x00, 0x01, // format = 1
191+
0x00, 0x07, // extensionLookupType = 7 (nested extension, invalid)
192+
0x00, 0x00, 0x00, 0x00, // extensionOffset = 0 (points at self)
193+
];
194+
assert!(SubstitutionSubtable::parse(data, 7).is_none());
195+
}
196+
197+
// Same self-reference for a GPOS Extension subtable (type 9).
198+
#[test]
199+
fn gpos_self_referential_extension_is_rejected() {
200+
let data: &[u8] = &[
201+
0x00, 0x01, // format = 1
202+
0x00, 0x09, // extensionLookupType = 9 (nested extension, invalid)
203+
0x00, 0x00, 0x00, 0x00, // extensionOffset = 0 (points at self)
204+
];
205+
assert!(PositioningSubtable::parse(data, 9).is_none());
206+
}
207+
208+
// A well-formed GSUB Extension subtable wrapping a real Single Substitution
209+
// must still resolve to its inner subtable: the guard rejects only the
210+
// extension-of-extension case, not legitimate extensions.
211+
#[test]
212+
fn gsub_valid_extension_still_resolves() {
213+
let data: &[u8] = &[
214+
// Extension subtable
215+
0x00, 0x01, // format = 1
216+
0x00, 0x01, // extensionLookupType = 1 (SingleSubst)
217+
0x00, 0x00, 0x00, 0x08, // extensionOffset = 8
218+
// Inner SingleSubstitution, format 1 (at offset 8)
219+
0x00, 0x01, // substFormat = 1
220+
0x00, 0x06, // coverageOffset = 6 (relative to inner)
221+
0x00, 0x00, // deltaGlyphID = 0
222+
// Coverage table, format 1 (at inner offset 6)
223+
0x00, 0x01, // coverageFormat = 1
224+
0x00, 0x01, // glyphCount = 1
225+
0x00, 0x41, // glyphArray[0] = 65
226+
];
227+
assert!(matches!(
228+
SubstitutionSubtable::parse(data, 7),
229+
Some(SubstitutionSubtable::Single(_))
230+
));
231+
}
232+
}

src/tables/gpos.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ impl<'a> LookupSubtable<'a> for PositioningSubtable<'a> {
980980
6 => MarkToMarkAdjustment::parse(data).map(Self::MarkToMark),
981981
7 => ContextLookup::parse(data).map(Self::Context),
982982
8 => ChainedContextLookup::parse(data).map(Self::ChainContext),
983-
9 => crate::ggg::parse_extension_lookup(data, Self::parse),
983+
9 => crate::ggg::parse_extension_lookup(data, 9, Self::parse),
984984
_ => None,
985985
}
986986
}

src/tables/gsub.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ impl<'a> LookupSubtable<'a> for SubstitutionSubtable<'a> {
262262
4 => LigatureSubstitution::parse(data).map(Self::Ligature),
263263
5 => ContextLookup::parse(data).map(Self::Context),
264264
6 => ChainedContextLookup::parse(data).map(Self::ChainContext),
265-
7 => crate::ggg::parse_extension_lookup(data, Self::parse),
265+
7 => crate::ggg::parse_extension_lookup(data, 7, Self::parse),
266266
8 => ReverseChainSingleSubstitution::parse(data).map(Self::ReverseChainSingle),
267267
_ => None,
268268
}

0 commit comments

Comments
 (0)