From 23a63f5c769fe51f2e5f6c3d217cfbfd5c8ea713 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 19:20:26 -0500 Subject: [PATCH] font-catalog: cap the font file read where the file has grown since its size was checked (#873) --- crates/font-catalog/src/catalog.rs | 15 ++++++++++++++- crates/font-catalog/src/tests.rs | 12 ++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/font-catalog/src/catalog.rs b/crates/font-catalog/src/catalog.rs index c20753fb..10663e6a 100644 --- a/crates/font-catalog/src/catalog.rs +++ b/crates/font-catalog/src/catalog.rs @@ -22,6 +22,19 @@ const MAX_SCAN_READ: u64 = 256 << 20; /// The largest font file loaded. const MAX_LOAD: u64 = 512 << 20; +/// The bytes of the file at `path`, at most `max`: the size was checked before, but a file can +/// grow between that check and the read, so the read itself stops one byte past the limit. +pub(crate) fn read_capped(path: &Path, max: u64) -> Result, FontFileError> { + use std::io::Read as _; + let mut bytes = Vec::new(); + std::fs::File::open(path)?.take(max.saturating_add(1)).read_to_end(&mut bytes)?; + let size = bytes.len() as u64; + if size > max { + return Err(FontFileError::TooLarge { size, max }); + } + Ok(bytes) +} + /// Why a font file or font data wasn't added or loaded. #[derive(Debug, thiserror::Error)] pub enum FontFileError { @@ -544,7 +557,7 @@ impl Catalog { if size > MAX_LOAD { return Err(FontFileError::TooLarge { size, max: MAX_LOAD }); } - let bytes: Arc<[u8]> = std::fs::read(path)?.into(); + let bytes: Arc<[u8]> = read_capped(path, MAX_LOAD)?.into(); let mut files = lock(&self.files); files.retain(|_, w| w.strong_count() > 0); files.insert(path.to_path_buf(), Arc::downgrade(&bytes)); diff --git a/crates/font-catalog/src/tests.rs b/crates/font-catalog/src/tests.rs index c061ca2a..5137a429 100644 --- a/crates/font-catalog/src/tests.rs +++ b/crates/font-catalog/src/tests.rs @@ -556,3 +556,15 @@ fn repeated_name_records_are_bounded_before_pairing() { assert!(!face.keys.legacy.is_empty()); assert_eq!(face.keys.legacy.len(), 256, "the pair budget counts distinct pairs"); } + +/// A read stops at its cap, whatever the file has grown to since its size was checked. +#[test] +fn a_read_is_capped_where_the_file_has_grown() { + use crate::catalog::read_capped; + let path = std::env::temp_dir().join(format!("craft-font-catalog-cap-{}", std::process::id())); + std::fs::write(&path, b"0123456789").unwrap(); + assert_eq!(read_capped(&path, 10).unwrap(), b"0123456789"); + assert!(matches!(read_capped(&path, 9), Err(FontFileError::TooLarge { size: 10, max: 9 }))); + assert!(matches!(read_capped(&path.with_extension("missing"), 9), Err(FontFileError::Io(_)))); + let _ = std::fs::remove_file(&path); +}