Skip to content

Advisory: Integer Underflow in MADT/SRAT Entry Iterator → OOB Read (CWE-193) #315

Description

@baima365-web

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:112src/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.lengthu32,如果被篡改为小于 size_of::<Madt>()(~44 字节),减法会下溢:

  • Debug 模式:直接 panic(整数溢出检查)
  • Release 模式:无检查,下溢为 0xFFFFFFxx(~4GB)

remaining_lengthu32 变为约 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_sizebuffer_len 是两个独立来源。如果恶意 AML 设置 buffer_size = 1buffer_len = 1000buffer[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_bitssrc.get(src_index / 8 + 1)src_index / 8 + 1 >= src.len() 时返回 &0x00(零扩展),这可能导致非预期的数据行为。

利用条件: 恶意 AML 创建 CreateField/CreateBitField 带巨大 bit_indexnum_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_offsetnamespace_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_lengthHandler::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:对 bankdataregionread_region 的 unwrap
  • object.rs:202read_buffer_fieldObject::Buffer(buffer) => buffer(当 buffer 不是 Buffer/String 时 panic)

修复建议: 将 panic!() 替换为 Err(AmlError::...)?,以便 AML 错误优雅传播而非中止。


ℹ️ 信息项 (3)

ACPI-10: #[repr(C, packed)] 结构体可能存在未对齐访问 UB

文件: 多个 SDT 结构体(SdtHeaderMadtFadtHpetTableRsdp 等)

问题: 所有 ACPI 表结构都标记为 #[repr(C, packed)]。在 AArch64 等严格对齐的架构上,直接引用打包结构体中的 u32/u64 字段可能引起对齐故障(Alignment fault)。该 crate 主要面向 x86(允许未对齐访问),且 SdtHeader 通常位于对齐地址。未对齐访问本身不会引起安全风险,但移植到新架构时会有问题。

当前处理: 所有字段访问通过 DerefPhysicalMapping → 指针解引用来处理,实际访问在调用方完成。一些值通过 copy_from_slice 获取,这可以处理未对齐数据。


ACPI-11: 缺少 AML 执行时间限制(Loop Timeout)

文件: src/aml/mod.rs:14 (TODO comment)

 *  - Loop timeouts

问题: 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-03write_buffer_field 缺少边界检查。恶意 AML 可以使用 CreateField 创建带越界偏移/长度的 BufferField,导致在后续 Index/Store 操作中写入堆分配缓冲区之外的字节。Rust 的边界检查会防止堆破坏,但会造成 panic(panic=abort 下为 DoS)。

建议修复优先级

  1. 立即: ACPI-01(saturating_sub 修复,零成本)
  2. 立即: ACPI-02(min(buffer_len, buffer_size) 检查)
  3. : ACPI-03(为 write_buffer_fieldread_buffer_field 添加边界验证)
  4. : ACPI-04(SPCR 字符串偏移验证)
  5. : ACPI-09(将 panic!() 替换为 AmlError 返回)
  6. : ACPI-11(实现循环/时间限制)

审计结束。全部 12 项问题记录于本报告。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions