English | 中文版
附录 B:CVE 代码分析——漏洞 C++ 代码 vs 安全 Rust 缓解方案
本附录展示附录 A 中记录的 CVE 的实际(或重建的)漏洞 C/C++ 代码,配以 ascend-rs 风格的 Rust 代码,从结构上防止每类漏洞。
B.1 引用计数释放后 Use-After-Free(CVE-2023-51042,AMDGPU)
Linux AMDGPU 驱动在释放 fence 引用计数后仍解引用其指针。
漏洞 C 代码(来自 amdgpu_cs.c,修复前 2e54154):
r = dma_fence_wait_timeout(fence, true, timeout);
dma_fence_put(fence); // 引用释放——fence 可能已被释放
if (r < 0)
return r;
if (r == 0)
break;
if (fence->error) // USE-AFTER-FREE:fence 已被释放
return fence->error;
ascend-rs 缓解方案——Rust 所有权确保值被消费而非悬垂:
#![allow(unused)]
fn main() {
fn wait_all_fences(fences: &[Arc<Fence>], timeout: Duration) -> Result<()> {
for fence in fences {
let status = fence.wait_timeout(timeout)?;
// 在仍持有 Arc 引用时检查 error
if let Some(err) = fence.error() {
return Err(err);
}
// Arc 引用在循环迭代结束前一直有效
// Rust 编译器拒绝在 drop 后使用 fence 的任何代码
}
Ok(())
}
}
Rust 如何防止此漏洞:Arc<Fence> 是引用计数的。编译器确保你无法在 Arc 被释放后访问 fence.error()——借用检查器在编译期拒绝对已移动/释放值的任何引用。
B.2 未检查用户索引导致越界写入(CVE-2024-0090,NVIDIA)
NVIDIA GPU 驱动通过 ioctl 接受用户提供的索引,未进行边界检查。
漏洞 C 代码(根据 CVE 描述重建):
struct gpu_resource_table {
uint32_t entries[MAX_GPU_RESOURCES];
uint32_t count;
};
static int nvidia_ioctl_set_resource(struct gpu_resource_table *table,
struct user_resource_request *req)
{
// 错误:未检查用户提供的索引
table->entries[req->index] = req->value; // 越界写入
return 0;
}
ascend-rs 缓解方案——Rust 切片在类型层面强制边界检查:
#![allow(unused)]
fn main() {
struct GpuResourceTable {
entries: Vec<u32>,
}
impl GpuResourceTable {
fn set_resource(&mut self, index: usize, value: u32) -> Result<()> {
*self.entries.get_mut(index)
.ok_or(Error::IndexOutOfBounds)? = value;
Ok(())
}
}
}
Rust 如何防止此漏洞:Vec<u32> 跟踪自身长度。.get_mut() 对越界访问返回 None。在安全 Rust 中无法静默地写入缓冲区之外。
B.3 整数溢出导致堆缓冲区溢出(CVE-2024-53873,NVIDIA CUDA Toolkit)
CUDA cuobjdump 从伪造的 .cubin 文件读取 2 字节有符号值,符号扩展后用于 memcpy 大小。
漏洞 C 代码(来自 Talos 反汇编分析):
int16_t name_len_raw = *(int16_t*)(section_data); // 0xFFFF = -1
int32_t name_len = (int32_t)name_len_raw; // 符号扩展为 -1
int32_t alloc_size = name_len + 1; // -1 + 1 = 0
memcpy(dest_buf, src, (size_t)alloc_size); // 堆缓冲区溢出
ascend-rs 缓解方案——Rust 的检查算术捕获溢出:
#![allow(unused)]
fn main() {
fn parse_debug_section(section: &[u8], dest: &mut [u8]) -> Result<()> {
let name_len_raw = i16::from_le_bytes(
section.get(0..2).ok_or(Error::TruncatedInput)?.try_into()?
);
let alloc_size: usize = (name_len_raw as i32)
.checked_add(1)
.and_then(|n| usize::try_from(n).ok())
.ok_or(Error::IntegerOverflow)?;
let src = section.get(offset..offset + alloc_size)
.ok_or(Error::BufferOverflow)?;
dest.get_mut(..alloc_size)
.ok_or(Error::BufferOverflow)?
.copy_from_slice(src);
Ok(())
}
}
Rust 如何防止此漏洞:checked_add() 在溢出时返回 None。usize::try_from() 拒绝负值。切片 .get() 对越界范围返回 None。
B.4 空容器越界读取(PyTorch Issue #37153)
PyTorch 的 CUDA 归约内核对标量张量的空 shape() 数组进行索引。
漏洞 C++ 代码(来自 Reduce.cuh):
// iter.shape() 对标量输入返回空 IntArrayRef
int64_t dim0;
if (reduction_on_fastest_striding_dimension) {
dim0 = iter.shape()[0]; // 越界:shape() 为空
// dim0 = 垃圾值(如 94599111233572)
}
ascend-rs 缓解方案——Rust 的 Option 类型使空值显式化:
#![allow(unused)]
fn main() {
fn configure_reduce_kernel(shape: &[usize]) -> Result<KernelConfig> {
let dim0 = shape.first()
.copied()
.ok_or(Error::ScalarTensorNotSupported)?;
let (dim0, dim1) = match shape {
[d0, d1, ..] => (*d0, *d1),
[d0] => (*d0, 1),
[] => return Err(Error::EmptyShape),
};
Ok(KernelConfig { dim0, dim1 })
}
}
Rust 如何防止此漏洞:shape.first() 返回 Option,强制调用者处理空值情况。match 对切片模式是穷举的——编译器要求 [](空)分支。
B.5 整数截断绕过边界检查(CVE-2019-16778,TensorFlow)
TensorFlow 的 UnsortedSegmentSum 内核将 int64 张量大小隐式截断为 int32。
漏洞 C++ 代码(来自 segment_reduction_ops.h):
template <typename T, typename Index> // Index = int32
struct UnsortedSegmentFunctor {
void operator()(OpKernelContext* ctx,
const Index num_segments, // 截断:int64 -> int32
const Index data_size, // 截断:int64 -> int32
const T* data, /* ... */)
{
if (data_size == 0) return; // 被绕过:截断值 != 0
// data_size = 1(从 4294967297 截断)
}
};
ascend-rs 缓解方案——Rust 类型系统拒绝隐式窄化:
#![allow(unused)]
fn main() {
fn unsorted_segment_sum(
data: &DeviceBuffer<f32>,
segment_ids: &DeviceBuffer<i32>,
num_segments: usize,
) -> Result<DeviceBuffer<f32>> {
let data_size: usize = data.len();
let data_size_i32: i32 = i32::try_from(data_size)
.map_err(|_| Error::TensorTooLarge {
size: data_size,
max: i32::MAX as usize,
})?;
// Rust 拒绝:let x: i32 = some_i64; // 错误:类型不匹配
Ok(output)
}
}
Rust 如何防止此漏洞:Rust 没有隐式整数窄化。let x: i32 = some_i64; 是编译错误。TryFrom/try_into() 在值不匹配时返回 Err。
B.6 锁释放后原始指针 Use-After-Free(CVE-2023-4211,ARM Mali)
ARM Mali GPU 驱动从共享状态复制原始指针,释放锁,休眠,然后解引用已悬垂的指针。
漏洞 C 代码(来自 mali_kbase_mem_linux.c,Project Zero 确认):
static void kbasep_os_process_page_usage_drain(struct kbase_context *kctx)
{
struct mm_struct *mm;
spin_lock(&kctx->mm_update_lock);
mm = rcu_dereference_protected(kctx->process_mm, /*...*/);
rcu_assign_pointer(kctx->process_mm, NULL);
spin_unlock(&kctx->mm_update_lock); // 锁释放
synchronize_rcu(); // 休眠——mm 可能被其他线程释放
add_mm_counter(mm, MM_FILEPAGES, -pages); // USE-AFTER-FREE
}
ascend-rs 缓解方案——Rust 的 Arc + Mutex 防止悬垂引用:
#![allow(unused)]
fn main() {
struct DeviceContext {
process_mm: Mutex<Option<Arc<MmStruct>>>,
}
impl DeviceContext {
fn drain_page_usage(&self) {
let mm = {
let mut guard = self.process_mm.lock().unwrap();
guard.take() // 设为 None,返回 Option<Arc<MmStruct>>
};
// 锁在此处释放(guard 被 drop)
if let Some(mm) = mm {
synchronize_rcu();
// mm 仍然存活——Arc 保证了这一点
mm.add_counter(MmCounter::FilePages, -pages);
}
// mm 在此处释放——Arc 引用计数递减
// 仅在最后一个 Arc 引用被 drop 时才释放底层内存
}
}
}
Rust 如何防止此漏洞:Arc<MmStruct> 是引用计数智能指针。从 Option 中取出后我们拥有一个强引用。即使锁释放后其他线程运行,我们的 Arc 保持 MmStruct 存活。在安全 Rust 中无法从 Arc 获得悬垂原始指针。