ACPI Crate 深度安全审计报告
Crate: acpi v6.1.1
仓库: rust-osdev/acpi
审计日期: 2026-05-25
审计者: 探微 (Tanwei)
类型: ACPI表格/AML解析器 | BIOS级固件解析 | no_std | 纯 Rust
关注点: 原始指针解析、表格边界、整数溢出
摘要
acpi 是 Rust 生态中最成熟的 ACPI 库之一,为操作系统内核提供 ACPI 表枚举、Generic Address Structure (GAS) 访问以及完整 AML 字节码解释器。整体代码质量较高,unsafe 使用有合理注释,但以下区域存在需注意的安全问题。
| 严重度 |
数量 |
说明 |
| 🔴 高危 |
1 |
表格长度减法整数下溢 → OOB 迭代 |
| 🟠 中危 |
3 |
缓冲区分配不足/无边界检查、OOB 读 |
| 🟡 低危 |
5 |
panic 点、unwrap 风险、隐式转换 |
| ℹ️ 信息 |
3 |
结构对齐、未完结 TODO、潜在 UB |
🔴 高危 (1)
ACPI-01: MADT/SRAT 条目迭代器 remaining_length 整数下溢 → OOB 读
文件: src/sdt/madt.rs:112 和 src/sdt/srat.rs:61
代码 (MADT):
pub fn entries(self: Pin<&Self>) -> MadtEntryIter<'_> {
let ptr = unsafe { Pin::into_inner_unchecked(self) as *const Madt as *const u8 };
MadtEntryIter {
pointer: unsafe { ptr.add(mem::size_of::<Madt>()) },
remaining_length: self.header.length - mem::size_of::<Madt>() as u32, // ← 高危
_phantom: PhantomData,
}
}
同样 (SRAT):
remaining_length: self.header.length - mem::size_of::<Srat>() as u32,
问题: self.header.length 是 u32,如果被篡改为小于 size_of::<Madt>()(~44 字节),减法会下溢:
- Debug 模式:直接 panic(整数溢出检查)
- Release 模式:无检查,下溢为
0xFFFFFFxx(~4GB)
remaining_length 从 u32 变为约 0xFFFFFFE1,随后迭代器会从栈上或映射内存区之外读取 EntryHeader(2字节),在实机(bare metal/x86)上产生页错误 → 内核崩溃 / DoS。
利用条件: 攻击者能篡改 ACPI 表(如通过 DMA、UEFI 固件漏洞、VMM 注入恶意 ACPI)。
影响: 操作系统引导时内核崩溃或越界内存访问。
修复建议:
remaining_length: self.header.length.saturating_sub(mem::size_of::<Madt>() as u32)
🟠 中危 (3)
ACPI-02: Buffer AML op 分配不足 → 越界 slice copy
文件: src/aml/mod.rs:666-674
代码:
Opcode::Buffer => {
// ...
let buffer_size = buffer_size.clone().unwrap_transparent_reference().as_integer()?;
let buffer_len = pkg_length - (context.current_block.pc - start_pc);
let mut buffer = vec![0; buffer_size as usize]; // ← 分配 buffer_size 字节
buffer[0..buffer_len].copy_from_slice( // ← buffer_len 可 > buffer_size
&context.current_block.stream()
[context.current_block.pc..(context.current_block.pc + buffer_len)],
);
}
问题: buffer_size 和 buffer_len 是两个独立来源。如果恶意 AML 设置 buffer_size = 1 但 buffer_len = 1000,buffer[0..1000].copy_from_slice(...) 会在 vec![0; 1] 上 panic(Bounds check failure)。在 no_std panic=abort 环境下触发 panic → DoS。
利用条件: 系统加载恶意 AML 表。
修复建议:
let copy_len = usize::min(buffer_len, buffer_size as usize);
buffer[0..copy_len].copy_from_slice(
&context.current_block.stream()
[context.current_block.pc..(context.current_block.pc + copy_len)],
);
ACPI-03: write_buffer_field 无边界检查 – 越界写可能导致 panic
文件: src/aml/object.rs:281-291
代码:
pub fn write_buffer_field(&mut self, value: &[u8], token: &ObjectToken) -> Result<(), AmlError> {
// TODO: bounds check the buffer first to avoid panicking ← 开发者已意识到
if let Self::BufferField { buffer, offset, length } = self {
let buffer = match unsafe { buffer.gain_mut(token) } {
Object::Buffer(buffer) => buffer.as_mut_slice(),
Object::String(string) => unsafe { string.as_bytes_mut() },
_ => panic!(),
};
copy_bits(value, 0, buffer, *offset, *length);
Ok(())
}
}
问题: BufferField { offset, length } 创建时没有验证 offset + length 不超过 source buffer 的总字节数。copy_bits 会写 buffer[*offset/8],如果 *offset 对应超出 buffer 长度的字节索引,Rust slice 检查会 panic。此外,copy_bits 的 src.get(src_index / 8 + 1) 在 src_index / 8 + 1 >= src.len() 时返回 &0x00(零扩展),这可能导致非预期的数据行为。
利用条件: 恶意 AML 创建 CreateField/CreateBitField 带巨大 bit_index 或 num_bits。
影响: 内核 panic(DoS)或读取/写入未初始化的数据。
修复建议: 在 create_* 操作(mod.rs:752-798)之后添加边界验证。
ACPI-04: SPCR namespace_string() OOB 读
文件: src/sdt/spcr.rs:155-162
代码:
pub fn namespace_string(&self) -> Result<&str, Utf8Error> {
let start = ptr::from_ref(self).cast::<u8>();
let bytes = unsafe {
let str_start = start.add(self.namespace_string_offset as usize);
slice::from_raw_parts(str_start, self.namespace_string_length as usize)
};
str::from_utf8(bytes)
}
问题: namespace_string_offset 和 namespace_string_length 来自 SPCR 表,没有任何方式验证 offset + length 是否在 SdtHeader.length 范围内。如果表被篡改,可以从任意物理地址读取内存。注意:这个函数标注了 pub 无文档化 Safety 要求。
利用条件: OEM 固件提供了格式错误的 SPCR 表,或攻击者篡改了 ACPI 镜像。
影响: 内核读取意外内存区域的信息泄露。
修复建议: 添加偏移量 + 长度 ≤ header.length() 的检查。
🟡 低危 (5)
ACPI-05: Mcfg::entries() 整数下溢
文件: src/sdt/mcfg.rs:24
代码:
let length = self.header.length as usize - mem::size_of::<Mcfg>();
问题: 如果 header.length < size_of::<Mcfg>(),结果下溢。Mcfg 结构体大(至少 60 字节),而最小的 SDT header 只有 36 字节。Debug 模式下 panic。
严重性: 低(需要严重格式错误的表来触发)。
修复: 改用 saturating_sub。
ACPI-06: Rsdp::search_for_on_bios EBDA 段读取
文件: src/rsdp.rs:112-114
代码:
let ebda_start_mapping =
unsafe { handler.map_physical_region::<u16>(EBDA_START_SEGMENT_PTR, mem::size_of::<u16>()) };
let ebda_start = (*ebda_start_mapping as usize) << 4;
问题: 从 BIOS 数据区 (0x40e) 读取 EBDA 段地址,不加验证。如果 BDA 数据损坏,ebda_start 可能指向非法范围,随后 find_search_areas 会映射一个大范围并读取。这是 ACPI 规范的标准行为,但若实现在没有 BIOS 的 UEFI 环境调用此函数则有风险。
注意: 函数标注为 unsafe 且仅适用于 BIOS 扫描。
影响: 信息泄露或映射非法区域。
修复: 对 ebda_start 添加范围检查。
ACPI-07: PhysicalMapping 中对齐假设
文件: src/lib.rs:150-152
代码:
let mut table_entries_ptr =
unsafe { self.rsdt_mapping.virtual_start.as_ptr().byte_add(mem::size_of::<SdtHeader>()) }.cast::<u8>();
let mut num_entries = (self.rsdt_mapping.region_length - mem::size_of::<SdtHeader>()) / entry_size;
问题: 如果 region_length < size_of::<SdtHeader>()(例如映射错误或长度 0),减法会下溢。RSDT/XSDT 长度最小应为 36。
影响: Debug 模式 panic。
修复: 使用 saturating_sub 并在长度不足时返回空迭代器。
ACPI-08: table_entries() 中的非安全指针算术
文件: src/lib.rs:150-164
代码:
let entry_size = if self.rsdp_revision == 0 { 4 } else { 8 };
let mut table_entries_ptr =
unsafe { self.rsdt_mapping.virtual_start.as_ptr().byte_add(mem::size_of::<SdtHeader>()) }.cast::<u8>();
问题: 以 RSDT 物理地址为基础,在 mapped region 内进行指针算术以读取条目。如果 region_length 小于 sizeof(SdtHeader) 或条目数量多于实际可用空间,迭代器将越界读取。所有读取在 unsafe 块内进行。
风险: 低(region_length 受 Handler::map_physical_region 验证)。
ACPI-09: parse_field_list 中大量 panic!() / unwrap()
多文件: src/aml/mod.rs:2360-2371, src/aml/object.rs:202, 以及约 30 个 panic!() / unwrap() 点
代码示例 (do_store):
Object::FieldUnit(field_unit) => self.do_field_write(field_unit, object.clone())?,
_ => {
return Err(AmlError::InvalidOperationOnObject {
op: Operation::Store, typ: target_object.typ(),
});
}
但 do_field_write 内部:
let Object::FieldUnit(ref bank) = **bank else { panic!() };
类似: Object::OpRegion(ref read_region) = **read_region else { panic!() };
问题: AML 执行路径预期字段操作数始终具有正确类型。如果命名空间对象被变异或不当处理,以下位置会引发 panic(no_std 下 = abort = DoS):
do_field_write / do_field_read:对 bank、data、region、read_region 的 unwrap
object.rs:202:read_buffer_field 中 Object::Buffer(buffer) => buffer(当 buffer 不是 Buffer/String 时 panic)
修复建议: 将 panic!() 替换为 Err(AmlError::...)?,以便 AML 错误优雅传播而非中止。
ℹ️ 信息项 (3)
ACPI-10: #[repr(C, packed)] 结构体可能存在未对齐访问 UB
文件: 多个 SDT 结构体(SdtHeader、Madt、Fadt、HpetTable、Rsdp 等)
问题: 所有 ACPI 表结构都标记为 #[repr(C, packed)]。在 AArch64 等严格对齐的架构上,直接引用打包结构体中的 u32/u64 字段可能引起对齐故障(Alignment fault)。该 crate 主要面向 x86(允许未对齐访问),且 SdtHeader 通常位于对齐地址。未对齐访问本身不会引起安全风险,但移植到新架构时会有问题。
当前处理: 所有字段访问通过 Deref → PhysicalMapping → 指针解引用来处理,实际访问在调用方完成。一些值通过 copy_from_slice 获取,这可以处理未对齐数据。
ACPI-11: 缺少 AML 执行时间限制(Loop Timeout)
文件: src/aml/mod.rs:14 (TODO comment)
问题: While 循环(Opcode::While)没有时间或指令数限制。恶意 AML 可以制造无限循环,冻结整个内核。该问题在此 crate 的 TODO 中有记录。
影响: DoS – 恶意 AML 可永久阻塞 AML 解释器。
修复: 实现指令计数 / 时间限制。
ACPI-12: SdtHeader::validate 信任 self.length 进行校验和计算
文件: src/sdt/mod.rs:146
代码:
let table_bytes = unsafe {
core::slice::from_raw_parts((self as *const SdtHeader).cast::<u8>(), self.length as usize)
};
let sum = table_bytes.iter().fold(0u8, |sum, &byte| sum.wrapping_add(byte));
问题: 校验和通过读取 self.length 字节来计算。如果 length 大于实际物理映射,这将变成越界读。该函数已标注 # Safety,要求调用者确保映射完整表格,因此这属于调用方责任。
总结
| ID |
严重度 |
位置 |
类型 |
描述 |
| ACPI-01 |
🔴 |
sdt/madt.rs:112, srat.rs:61 |
整数下溢 → OOB 迭代 |
header.length - sizeof(Madt) 无 saturating_sub |
| ACPI-02 |
🟠 |
aml/mod.rs:669 |
缓冲区不足 |
buffer_size < buffer_len 时 panic |
| ACPI-03 |
🟠 |
aml/object.rs:281 |
越界写 |
write_buffer_field 无边界检查 |
| ACPI-04 |
🟠 |
sdt/spcr.rs:155 |
OOB 读 |
namespace_string() 无偏移验证 |
| ACPI-05 |
🟡 |
sdt/mcfg.rs:24 |
整数下溢 |
header.length - sizeof(Mcfg) |
| ACPI-06 |
🟡 |
rsdp.rs:112 |
读取未验证 |
EBDA 段无范围检查 |
| ACPI-07 |
🟡 |
lib.rs:152 |
整数下溢 |
region_length - sizeof(SdtHeader) |
| ACPI-08 |
🟡 |
lib.rs:150 |
OOB 读 |
table_entries 指针算术 |
| ACPI-09 |
🟡 |
aml/ + object.rs |
panic 向量 |
~30 个 panic!() 在 AML 执行路径中 |
| ACPI-10 |
ℹ️ |
各处 |
结构对齐 |
#[repr(packed)] 在严格对齐架构上的风险 |
| ACPI-11 |
ℹ️ |
aml/mod.rs |
无执行限制 |
无循环超时机制 |
| ACPI-12 |
ℹ️ |
sdt/mod.rs:146 |
信任数据 |
validate() 信任 self.length |
可提交的 CVE 建议
建议 1: ACPI-01 – MADT/SRAT 迭代器中 length 减法无饱和处理导致的整数下溢。这使得攻击者可以通过提供 length 小于预期值的 ACPI 表来触发越界内存读取。在 no_std 裸机环境中,这构成严重 DoS / 信息泄露漏洞。
建议 2: ACPI-03 – write_buffer_field 缺少边界检查。恶意 AML 可以使用 CreateField 创建带越界偏移/长度的 BufferField,导致在后续 Index/Store 操作中写入堆分配缓冲区之外的字节。Rust 的边界检查会防止堆破坏,但会造成 panic(panic=abort 下为 DoS)。
建议修复优先级
- 立即: ACPI-01(saturating_sub 修复,零成本)
- 立即: ACPI-02(
min(buffer_len, buffer_size) 检查)
- 高: ACPI-03(为
write_buffer_field 和 read_buffer_field 添加边界验证)
- 高: ACPI-04(SPCR 字符串偏移验证)
- 中: ACPI-09(将
panic!() 替换为 AmlError 返回)
- 中: ACPI-11(实现循环/时间限制)
审计结束。全部 12 项问题记录于本报告。
ACPI Crate 深度安全审计报告
Crate:
acpiv6.1.1仓库:
rust-osdev/acpi审计日期: 2026-05-25
审计者: 探微 (Tanwei)
类型: ACPI表格/AML解析器 | BIOS级固件解析 |
no_std| 纯 Rust关注点: 原始指针解析、表格边界、整数溢出
摘要
acpi 是 Rust 生态中最成熟的 ACPI 库之一,为操作系统内核提供 ACPI 表枚举、Generic Address Structure (GAS) 访问以及完整 AML 字节码解释器。整体代码质量较高,unsafe 使用有合理注释,但以下区域存在需注意的安全问题。
🔴 高危 (1)
ACPI-01: MADT/SRAT 条目迭代器
remaining_length整数下溢 → OOB 读文件:
src/sdt/madt.rs:112和src/sdt/srat.rs:61代码 (MADT):
同样 (SRAT):
问题:
self.header.length是u32,如果被篡改为小于size_of::<Madt>()(~44 字节),减法会下溢:0xFFFFFFxx(~4GB)remaining_length从u32变为约0xFFFFFFE1,随后迭代器会从栈上或映射内存区之外读取EntryHeader(2字节),在实机(bare metal/x86)上产生页错误 → 内核崩溃 / DoS。利用条件: 攻击者能篡改 ACPI 表(如通过 DMA、UEFI 固件漏洞、VMM 注入恶意 ACPI)。
影响: 操作系统引导时内核崩溃或越界内存访问。
修复建议:
🟠 中危 (3)
ACPI-02:
BufferAML op 分配不足 → 越界 slice copy文件:
src/aml/mod.rs:666-674代码:
问题:
buffer_size和buffer_len是两个独立来源。如果恶意 AML 设置buffer_size = 1但buffer_len = 1000,buffer[0..1000].copy_from_slice(...)会在vec![0; 1]上 panic(Bounds check failure)。在no_stdpanic=abort 环境下触发 panic → DoS。利用条件: 系统加载恶意 AML 表。
修复建议:
ACPI-03:
write_buffer_field无边界检查 – 越界写可能导致 panic文件:
src/aml/object.rs:281-291代码:
问题:
BufferField { offset, length }创建时没有验证offset + length不超过 source buffer 的总字节数。copy_bits会写buffer[*offset/8],如果*offset对应超出 buffer 长度的字节索引,Rust slice 检查会 panic。此外,copy_bits的src.get(src_index / 8 + 1)在src_index / 8 + 1 >= src.len()时返回&0x00(零扩展),这可能导致非预期的数据行为。利用条件: 恶意 AML 创建
CreateField/CreateBitField带巨大bit_index或num_bits。影响: 内核 panic(DoS)或读取/写入未初始化的数据。
修复建议: 在
create_*操作(mod.rs:752-798)之后添加边界验证。ACPI-04: SPCR
namespace_string()OOB 读文件:
src/sdt/spcr.rs:155-162代码:
问题:
namespace_string_offset和namespace_string_length来自 SPCR 表,没有任何方式验证offset + length是否在SdtHeader.length范围内。如果表被篡改,可以从任意物理地址读取内存。注意:这个函数标注了pub无文档化 Safety 要求。利用条件: OEM 固件提供了格式错误的 SPCR 表,或攻击者篡改了 ACPI 镜像。
影响: 内核读取意外内存区域的信息泄露。
修复建议: 添加偏移量 + 长度 ≤
header.length()的检查。🟡 低危 (5)
ACPI-05:
Mcfg::entries()整数下溢文件:
src/sdt/mcfg.rs:24代码:
问题: 如果
header.length < size_of::<Mcfg>(),结果下溢。Mcfg结构体大(至少 60 字节),而最小的 SDT header 只有 36 字节。Debug 模式下 panic。严重性: 低(需要严重格式错误的表来触发)。
修复: 改用
saturating_sub。ACPI-06:
Rsdp::search_for_on_biosEBDA 段读取文件:
src/rsdp.rs:112-114代码:
问题: 从 BIOS 数据区 (
0x40e) 读取 EBDA 段地址,不加验证。如果 BDA 数据损坏,ebda_start可能指向非法范围,随后find_search_areas会映射一个大范围并读取。这是 ACPI 规范的标准行为,但若实现在没有 BIOS 的 UEFI 环境调用此函数则有风险。注意: 函数标注为
unsafe且仅适用于 BIOS 扫描。影响: 信息泄露或映射非法区域。
修复: 对
ebda_start添加范围检查。ACPI-07:
PhysicalMapping中对齐假设文件:
src/lib.rs:150-152代码:
问题: 如果
region_length < size_of::<SdtHeader>()(例如映射错误或长度 0),减法会下溢。RSDT/XSDT 长度最小应为 36。影响: Debug 模式 panic。
修复: 使用
saturating_sub并在长度不足时返回空迭代器。ACPI-08:
table_entries()中的非安全指针算术文件:
src/lib.rs:150-164代码:
问题: 以 RSDT 物理地址为基础,在 mapped region 内进行指针算术以读取条目。如果
region_length小于sizeof(SdtHeader)或条目数量多于实际可用空间,迭代器将越界读取。所有读取在 unsafe 块内进行。风险: 低(
region_length受Handler::map_physical_region验证)。ACPI-09:
parse_field_list中大量panic!()/unwrap()多文件:
src/aml/mod.rs:2360-2371,src/aml/object.rs:202, 以及约 30 个panic!()/unwrap()点代码示例 (do_store):
但
do_field_write内部:类似:
Object::OpRegion(ref read_region) = **read_region else { panic!() };问题: AML 执行路径预期字段操作数始终具有正确类型。如果命名空间对象被变异或不当处理,以下位置会引发 panic(
no_std下 = abort = DoS):do_field_write/do_field_read:对bank、data、region、read_region的 unwrapobject.rs:202:read_buffer_field中Object::Buffer(buffer) => buffer(当 buffer 不是 Buffer/String 时 panic)修复建议: 将
panic!()替换为Err(AmlError::...)?,以便 AML 错误优雅传播而非中止。ℹ️ 信息项 (3)
ACPI-10:
#[repr(C, packed)]结构体可能存在未对齐访问 UB文件: 多个 SDT 结构体(
SdtHeader、Madt、Fadt、HpetTable、Rsdp等)问题: 所有 ACPI 表结构都标记为
#[repr(C, packed)]。在 AArch64 等严格对齐的架构上,直接引用打包结构体中的u32/u64字段可能引起对齐故障(Alignment fault)。该 crate 主要面向 x86(允许未对齐访问),且SdtHeader通常位于对齐地址。未对齐访问本身不会引起安全风险,但移植到新架构时会有问题。当前处理: 所有字段访问通过
Deref→PhysicalMapping→ 指针解引用来处理,实际访问在调用方完成。一些值通过copy_from_slice获取,这可以处理未对齐数据。ACPI-11: 缺少 AML 执行时间限制(Loop Timeout)
文件:
src/aml/mod.rs:14(TODO comment)问题:
While循环(Opcode::While)没有时间或指令数限制。恶意 AML 可以制造无限循环,冻结整个内核。该问题在此 crate 的 TODO 中有记录。影响: DoS – 恶意 AML 可永久阻塞 AML 解释器。
修复: 实现指令计数 / 时间限制。
ACPI-12:
SdtHeader::validate信任self.length进行校验和计算文件:
src/sdt/mod.rs:146代码:
问题: 校验和通过读取
self.length字节来计算。如果length大于实际物理映射,这将变成越界读。该函数已标注# Safety,要求调用者确保映射完整表格,因此这属于调用方责任。总结
header.length - sizeof(Madt)无 saturating_subbuffer_size<buffer_len时 panicwrite_buffer_field无边界检查namespace_string()无偏移验证header.length - sizeof(Mcfg)region_length - sizeof(SdtHeader)table_entries指针算术panic!()在 AML 执行路径中#[repr(packed)]在严格对齐架构上的风险validate()信任self.length可提交的 CVE 建议
建议 1: ACPI-01 – MADT/SRAT 迭代器中
length减法无饱和处理导致的整数下溢。这使得攻击者可以通过提供length小于预期值的 ACPI 表来触发越界内存读取。在no_std裸机环境中,这构成严重 DoS / 信息泄露漏洞。建议 2: ACPI-03 –
write_buffer_field缺少边界检查。恶意 AML 可以使用CreateField创建带越界偏移/长度的BufferField,导致在后续Index/Store操作中写入堆分配缓冲区之外的字节。Rust 的边界检查会防止堆破坏,但会造成 panic(panic=abort 下为 DoS)。建议修复优先级
min(buffer_len, buffer_size)检查)write_buffer_field和read_buffer_field添加边界验证)panic!()替换为AmlError返回)审计结束。全部 12 项问题记录于本报告。