ITADN

[Unsoundness] Out-of-bounds read in public API parse_unk_size_null_utf16_string due to unsafe loop condition order

#719Openlwz23 创建于 2025-11-24
L
lwz23commented
Hello, While working on a static analysis tool designed to detect unsoundness and safety violations in Rust projects, we identified a potential vulnerability in the etw-reader crate within this repository. Location: The issue is located in the function parse_unk_size_null_utf16_string in etw-reader/src/utils.rs. https://github.com/mstange/samply/blob/c947d6339a9bea6c5659c6317796c5061d0fd7e6/etw-reader/src/utils.rs#L8 Since utils is a public module and parse_unk_size_null_utf16_string is a public function, this unsafe API is accessible to external users, allowing Safe Rust code to trigger Undefined Behavior (UB). Description: The function accepts a safe slice &[u8] and attempts to interpret it as a null-terminated UTF-16 string. However, the while loop used to find the null terminator has a flawed condition order: ``` // Current implementation while unsafe { *ptr } != 0 && ptr < end { len += 1; ptr = unsafe { ptr.offset(1) }; } ``` The logical AND (&&) operator evaluates from left to right. This means unsafe { *ptr } is executed before checking if ptr < end. If the input slice contains valid aligned data but does not contain a null terminator (e.g., [0xFF, 0xFF, 0xFF, 0xFF]), the pointer ptr will eventually advance to equal end. In the final iteration check, the code will attempt to read *end (which is out of bounds) before the ptr < end check can stop the loop. This results in an out-of-bounds read. Proof of Concept (PoC): We constructed a reproduction case where a safe call with a specifically crafted slice triggers UB. ``` use std::alloc::{alloc, Layout}; // Code extracted from etw-reader/src/utils.rs for reproduction fn is_aligned<T>(ptr: *const T) -> bool where T: Sized, { ptr as usize & (std::mem::align_of::<T>() - 1) == 0 } pub fn parse_unk_size_null_utf16_string(v: &[u8]) -> String { let start: *const u16 = v.as_ptr().cast(); if !is_aligned(start) { panic!("Not aligned"); } // safe because we not going past the end of the slice let end: *const u16 = unsafe { v.as_ptr().add(v.len()) }.cast(); // find the null termination let mut len = 0; let mut ptr = start; // BUG HERE: *ptr is dereferenced before checking ptr < end while unsafe { *ptr } != 0 && ptr < end { len += 1; ptr = unsafe { ptr.offset(1) }; } let slice = unsafe { std::slice::from_raw_parts(start, len) }; String::from_utf16_lossy(slice) } #[repr(align(2))] struct AlignedData { // 4 bytes, corresponding to 2 u16s. // Intentionally no 0x0000 (null terminator). bytes: [u8; 4], } fn main() { // 1. Construct data: [0xFF, 0xFF, 0xFF, 0xFF] // Interpreted as two u16::MAX. let v = AlignedData { bytes: [0xFF, 0xFF, 0xFF, 0xFF], }; // 2. Call the vulnerable function with a safe slice. // If the function were sound, this should panic or return a string, but not UB. let _ = parse_unk_size_null_utf16_string(&v.bytes); } ``` Miri Output: Running this with Miri (cargo miri run) confirms the out-of-bounds memory access: ``` error: Undefined Behavior: memory access failed: expected a pointer to 2 bytes of memory, but got alloc472+0x4 which is at or beyond the end of the allocation of size 4 bytes --> src\main.rs:25:20 | 25 | while unsafe { *ptr } != 0 && ptr < end { | ^^^^ memory access failed: expected a pointer to 2 bytes of memory, but got alloc472+0x4 which is at or beyond the end of the allocation of size 4 bytes ``` Recommendation: To fix this, the order of the checks in the while loop condition must be reversed. You must verify that the pointer is within bounds before dereferencing it. ``` // Check bounds first, then dereference while ptr < end && unsafe { *ptr } != 0 { len += 1; ptr = unsafe { ptr.offset(1) }; } ``` Alternatively, utilizing safer iterators or cast_slice from crates like bytemuck (as hinted in the code comments) would avoid manual pointer arithmetic entirely.
0 条评论