From a150407a14e33489dd9be84af4f9eacc14fe9add Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Fri, 17 Jul 2026 22:07:40 -0700 Subject: [PATCH] =?UTF-8?q?feat(unity):=20UnityFS=20=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=E8=B6=8A=E7=95=8C=E6=A0=A1=E9=AA=8C=EF=BC=88?= =?UTF-8?q?issue=20#3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解析阶段已读取 directory 的 offset/size,但从不校验其是否落在解压 数据区(各 block uncompressed_size 之和)内;截断或损坏的 bundle 会给出越界目录条目并被静默接受。新增 validate_directory_bounds: 累加数据区大小、逐条校验 offset+size 不越界且不溢出 u64,越界即 报错并带上下文,接在 parse_blocks_info 之后。 验证:新增 4 个测试(合法覆盖尾部/越界拒绝/offset+size 溢出/ 端到端越界 bundle 被拒);bat-adapters 73 测试全绿,fmt + clippy -p bat-adapters --all-targets -D warnings 通过。 Co-Authored-By: Claude Fable 5 --- adapters/src/unity/unity_2021_3.rs | 122 +++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/adapters/src/unity/unity_2021_3.rs b/adapters/src/unity/unity_2021_3.rs index 70e23b2..1b87b77 100644 --- a/adapters/src/unity/unity_2021_3.rs +++ b/adapters/src/unity/unity_2021_3.rs @@ -86,6 +86,7 @@ impl Unity2021_3Adapter { flags, )?; let (blocks, directories) = Self::parse_blocks_info(&block_info)?; + validate_directory_bounds(&blocks, &directories)?; Ok(ParsedAssetBundle { unity_version: header.unity_version.clone(), @@ -145,6 +146,50 @@ impl Unity2021_3Adapter { } } +/// 校验目录条目落在解压数据区内。 +/// +/// UnityFS 的 directory 是解压后(所有 block 的 uncompressed 数据依次拼接而成的) +/// 连续数据区上的 `[offset, offset + size)` 切片。解析阶段只按结构读取这些数值, +/// 并不保证它们不越界;截断或损坏的 bundle 会给出指向数据区之外的目录条目, +/// 若不校验就静默接受,后续按 offset/size 取数据时才会出错或读到错误内容。 +/// 这里把每个目录条目与「各 block 解压大小之和」比对,越界即报错并带上下文。 +fn validate_directory_bounds( + blocks: &[UnityFsBlockInfo], + directories: &[UnityFsDirectoryInfo], +) -> Result<(), String> { + let mut data_region_size: u64 = 0; + for (index, block) in blocks.iter().enumerate() { + data_region_size = data_region_size + .checked_add(u64::from(block.uncompressed_size)) + .ok_or_else(|| { + format!( + "UnityFS 解压数据区大小溢出:累加到第 {index} 个 block(uncompressed_size={})时超过 u64", + block.uncompressed_size + ) + })?; + } + + for (index, directory) in directories.iter().enumerate() { + let end = directory + .offset + .checked_add(directory.size) + .ok_or_else(|| { + format!( + "UnityFS 目录条目 {} 的 offset({}) + size({}) 溢出 u64", + directory.path, directory.offset, directory.size + ) + })?; + if end > data_region_size { + return Err(format!( + "UnityFS 目录条目 {}(第 {index} 项)越界:offset({}) + size({}) = {} 超过解压数据区大小 {}", + directory.path, directory.offset, directory.size, end, data_region_size + )); + } + } + + Ok(()) +} + fn read_blocks_info_bytes<'a>( data: &'a [u8], reader: &mut UnityFsReader<'a>, @@ -497,4 +542,81 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().contains("signature")); } + + fn block(uncompressed_size: u32) -> UnityFsBlockInfo { + UnityFsBlockInfo { + uncompressed_size, + compressed_size: uncompressed_size, + flags: 0, + compression: UnityFsCompression::None, + } + } + + fn directory(offset: u64, size: u64) -> UnityFsDirectoryInfo { + UnityFsDirectoryInfo { + offset, + size, + flags: 0, + path: "CAB-test".to_string(), + } + } + + #[test] + fn directory_within_data_region_is_accepted() { + // 两个 block 共 12 字节解压数据区;目录条目正好覆盖尾部,合法。 + let result = + validate_directory_bounds(&[block(8), block(4)], &[directory(0, 8), directory(8, 4)]); + assert!(result.is_ok(), "{result:?}"); + } + + #[test] + fn directory_past_data_region_is_rejected() { + // 解压数据区仅 4 字节,目录声称 [0, 8) 越界,应被拒绝并带上下文。 + let error = validate_directory_bounds(&[block(4)], &[directory(0, 8)]).unwrap_err(); + assert!(error.contains("越界"), "{error}"); + assert!(error.contains("解压数据区大小 4"), "{error}"); + } + + #[test] + fn directory_offset_size_overflow_is_rejected() { + let error = validate_directory_bounds(&[block(4)], &[directory(u64::MAX, 1)]).unwrap_err(); + assert!(error.contains("溢出"), "{error}"); + } + + #[tokio::test] + async fn test_parse_rejects_out_of_bounds_directory() { + // 构造一个 directory.size 超过 block 解压大小的 bundle,端到端验证被拒。 + let mut blocks_info = Vec::new(); + blocks_info.extend_from_slice(&[0; 16]); + push_i32(&mut blocks_info, 1); + push_u32(&mut blocks_info, 4); // block uncompressed_size = 4 + push_u32(&mut blocks_info, 4); + push_u16(&mut blocks_info, 0); + push_i32(&mut blocks_info, 1); + push_u64(&mut blocks_info, 0); + push_u64(&mut blocks_info, 99); // directory size 99 远超数据区 + push_u32(&mut blocks_info, 0); + push_c_string(&mut blocks_info, "CAB-test"); + + let mut data = Vec::new(); + push_c_string(&mut data, "UnityFS"); + push_u32(&mut data, 8); + push_c_string(&mut data, "5.x.x"); + push_c_string(&mut data, "2021.3.56f2"); + push_u64(&mut data, 0); + push_u32(&mut data, blocks_info.len() as u32); + push_u32(&mut data, blocks_info.len() as u32); + push_u32(&mut data, 0); + align(&mut data, UNITYFS_ALIGNMENT); + data.extend_from_slice(&blocks_info); + data.extend_from_slice(b"data"); + + let adapter = Unity2021_3Adapter::new(); + let bundle = RawAssetBundle { + data, + path: Some("out-of-bounds.bundle".to_string()), + }; + let error = adapter.parse(&bundle).await.unwrap_err(); + assert!(error.contains("越界"), "{error}"); + } }