Repository navigation
pub fn copy_from_iter_raw incorrectly elided output lifetime #1265
Description
Hi,
There is an unsound problem in rust/kernel/iov.rs:164.
The pub function called copy_from_iter_raw returns &mut [u8] outlives out buffer (elision binds to &mut self).
Config
-
kernel tag: latest
-
kbuild:
./scripts/config --enable RUST --enable SAMPLES --enable SAMPLES_RUST
--enable SAMPLE_RUST_POC_IOV --enable KASAN
Root Cause
Based on lifetime elision rules, the signature pub fn copy_from_iter_raw(&mut self, out: &mut [MaybeUninit<u8>]) -> &mut [u8] (in rust/kernel/iov.rs) elides the output lifetime to that of &mut self , not out.
Lines 164 to 179 in 7c306f0
| pub fn copy_from_iter_raw(&mut self, out: &mut [MaybeUninit<u8>]) -> &mut [u8] { | |
| let capacity = out.len(); | |
| let out = out.as_mut_ptr().cast::<u8>(); | |
| // GUARANTEES: The C API guarantees that it does not write uninitialized bytes to the | |
| // provided buffer. | |
| // SAFETY: | |
| // * By the type invariants, it is still valid to read from this IO vector. | |
| // * `out` is valid for writing for `capacity` bytes because it comes from a slice of | |
| // that length. | |
| let len = unsafe { bindings::_copy_from_iter(out.cast(), capacity, self.as_raw()) }; | |
| // SAFETY: The underlying C api guarantees that initialized bytes have been written to the | |
| // first `len` bytes of the spare capacity. | |
| unsafe { slice::from_raw_parts_mut(out, len) } | |
| } |
If I understand correctly, the returned slice is built via slice::from_raw_parts_mut from a raw pointer derived from out, so it aliases/points into out's memory, yet the borrow checker only ties it to self.
Safe code can drop or reuse out while the returned &mut [u8] is still live, giving a dangling reference (UAF) and overlapping &mut aliasing.
The function is a pub one on a kernel crate in linux, so any safe caller can trigger it. In-tree callers only use .len() so are not exploitable for now, but the API is unsound.
POC
The poc is built as kernel module. I have (dirty-)patched the following things in tested kernel source code
config SAMPLE_RUST_POC_IOVinsamples/rust/Kconfigobj-$(CONFIG_SAMPLE_RUST_POC_IOV) += rust_poc_iov.oinsamples/rust/Makefile- a poc module below located at
samples/rust/rust_poc_iov.rs
// SPDX-License-Identifier: GPL-2.0
//! PoC for the lifetime-elision hole in `IovIterSource::copy_from_iter_raw`
//! (`rust/kernel/iov.rs`).
//!
//! Build with `CONFIG_KASAN=y`;
use core::mem::MaybeUninit;
use kernel::{bindings, iov, prelude::*};
module! {
type: PocIov,
name: "rfl_poc_iov",
authors: ["spidermana"],
description: "PoC: copy_from_iter_raw() returns a slice tied to &mut self, not to `out`",
license: "GPL",
}
struct PocIov;
impl kernel::Module for PocIov {
fn init(_module: &'static ThisModule) -> Result<Self> {
match poc() {
Ok(()) => pr_info!("POC: DONE\n"),
Err(e) => pr_info!("POC: SKIPPED (setup failed: {:?})\n", e),
}
// Never fail init: the verdict is in the log, and a failed built-in initcall only adds
// noise.
Ok(PocIov)
}
}
fn poc() -> Result {
// Setup (C API): an ITER_SOURCE `iov_iter` reading from a buffer that outlives `it`.
let src = [0x41u8; 32];
let kv = bindings::kvec {
iov_base: src.as_ptr().cast_mut().cast(),
iov_len: src.len(),
};
let mut raw = MaybeUninit::<bindings::iov_iter>::zeroed();
// SAFETY: `raw` is writable and `kv` describes one valid segment covering `src`.
unsafe { bindings::iov_iter_kvec(raw.as_mut_ptr(), bindings::ITER_SOURCE, &kv, 1, src.len()) };
// SAFETY: `raw` now holds a valid `ITER_SOURCE` iterator over `src`, which outlives `it`,
// and nothing else touches `raw`.
let it: &mut iov::IovIterSource<'_> = unsafe { iov::IovIterSource::from_raw(raw.as_mut_ptr()) };
// =========================== SAFE RUST ===========================
let dangling: &mut [u8] = {
let mut buf = KVec::<u8>::with_capacity(32, GFP_KERNEL)?;
it.copy_from_iter_raw(buf.spare_capacity_mut()) // <-- the hole: result is tied to `it`
// `buf` (a 32-byte slab object) is kfree'd right here
};
pr_info!(
"POC: holding &mut [u8] of len {} whose backing allocation is already freed\n",
dangling.len()
);
pr_info!("POC: touching the dangling slice (write)\n");
dangling.fill(0x42); // use-after-free write
// =================================================================
Ok(())
}Here is a part of logs from KASAN:
[ 2.370972] BUG: KASAN: slab-use-after-free in _RNvXCs62u9c2E79YJ_12rust_poc_iovNtB2_6PocIovNtNtCs1EKtwoKEMO2_6kernel6module6Module4init+0x174/0x258
[ 2.371734] Write of size 32 at addr ffff00000c1e1a80 by task swapper/0/1
[ 2.371802]
[ 2.372500] CPU: 0 UID: 0 PID: 1 Comm: swapper/0 Not tainted 7.3.0-rc4-00001-g9a879651bb7f-dirty #90 PREEMPT
[ 2.372658] Hardware name: linux,dummy-virt (DT)
[ 2.372890] Call trace:
[ 2.373068] show_stack+0x18/0x24 (C)
[ 2.375312] __dump_stack+0x28/0x38
[ 2.375412] dump_stack_lvl+0x54/0x6c
[ 2.375448] print_address_description+0x98/0x218
[ 2.375484] print_report+0x68/0x84
[ 2.375529] kasan_report+0xb0/0x10c
[ 2.375566] kasan_check_range+0x264/0x2a4
[ 2.375596] __asan_memset+0x34/0x64
[ 2.375629] _RNvXCs62u9c2E79YJ_12rust_poc_iovNtB2_6PocIovNtNtCs1EKtwoKEMO2_6kernel6module6Module4init+0x174/0x258
Fix
pub fn copy_from_iter_raw<'a>(&mut self, out: &'a mut [MaybeUninit<u8>]) -> &'a mut [u8]Others
If this fix is confirmed to be necessary, I can send a patch email to the LKML.
Always open to suggestions!
Activity
ojeda commentedon Oct 16, 2026
Cc @Darksonn since she originally wrote the code