From 069234157eb3efad619d175917996e5258d81035 Mon Sep 17 00:00:00 2001 From: sanghnkim-max <267927865+sanghnkim-max@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:06:44 +0900 Subject: [PATCH] fix(doc): bound the LFO reservation by the records the table holds parse_plf_lfo reserved Vec capacity straight from the PlfLfo's u32 count. A short table declaring 0xFFFF_FFFF asked for ~64 GB up front, which aborts the process wherever the allocator refuses it; the loop below already stops at the first missing record, so the reservation was the only unbounded step. Co-Authored-By: Claude Opus 5.5 --- src/formats/doc/lists.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/formats/doc/lists.rs b/src/formats/doc/lists.rs index d0648216..ef668780 100644 --- a/src/formats/doc/lists.rs +++ b/src/formats/doc/lists.rs @@ -190,11 +190,19 @@ fn parse_lvl(plf: &[u8], pos: usize) -> Option<(LevelDef, usize)> { const LFO_SIZE: usize = 16; +/// How many LFO records to reserve for. `count` is an untrusted u32, so reserve +/// only for records the table can actually hold: a short table declaring +/// 0xFFFF_FFFF otherwise asks for ~64 GB up front, which aborts the process +/// wherever the allocator refuses it. +fn lfo_capacity(plf: &[u8], count: usize) -> usize { + count.min(plf.len().saturating_sub(4) / LFO_SIZE) +} + fn parse_plf_lfo(plf: &[u8], by_lsid: &HashMap, lists: &mut Lists) { let Some(count) = get_u32(plf, 0).map(|v| v as usize) else { return; }; - let mut lfos: Vec<(u32, usize)> = Vec::with_capacity(count); + let mut lfos: Vec<(u32, usize)> = Vec::with_capacity(lfo_capacity(plf, count)); let mut pos = 4; for _ in 0..count { let Some(record) = plf.get(pos..).and_then(|rest| rest.get(..LFO_SIZE)) else { @@ -316,4 +324,20 @@ mod tests { let (def, _) = parse_lvl(&lvl, 0).unwrap(); assert_eq!(def.restart, None); } + + /// A count far beyond the records present reserves only for those + /// records, and the short table resolves nothing. (Asserted on the + /// capacity, not by allocating: an overcommitting allocator would let + /// the unbounded reservation "succeed" and hide the bug.) + #[test] + fn oversized_lfo_count_reserves_only_the_records_present() { + let mut plf = u32::MAX.to_le_bytes().to_vec(); + plf.extend_from_slice(&[0u8; LFO_SIZE]); + assert_eq!(lfo_capacity(&plf, u32::MAX as usize), 1); + assert_eq!(lfo_capacity(&plf[..3], u32::MAX as usize), 0); + assert_eq!(lfo_capacity(&plf, 0), 0); + let mut lists = Lists::default(); + parse_plf_lfo(&plf, &HashMap::new(), &mut lists); + assert!(lists.get(1).is_none()); + } }