mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-21 22:11:26 +08:00
feat(unity): UnityFS 目录条目越界校验(issue #3)
解析阶段已读取 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user