Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions examples/loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,14 @@ fn __lo_make_io_sqe(op: u32, off: u64, bytes: u32, buf_addr: *mut u8) -> io_urin
}
}

async fn lo_handle_io_cmd_async(q: &UblkQueue<'_>, tag: u16, buf_addr: *mut u8) -> i32 {
/// Handle I/O operations asynchronously using slice-based buffer management.
///
/// This function demonstrates slice-based async I/O patterns for educational purposes:
/// - Uses safe slice access instead of raw pointer manipulation
/// - Leverages IoBuf's slice methods for memory safety
/// - Shows when slice-to-pointer conversion is necessary for libublk API calls
/// - Maintains async/await patterns for high-performance I/O
async fn lo_handle_io_cmd_async(q: &UblkQueue<'_>, tag: u16, io_slice: &mut [u8]) -> i32 {
let iod = q.get_iod(tag);
let res = __lo_prep_submit_io_cmd(iod);
if res < 0 {
Expand All @@ -160,6 +167,10 @@ async fn lo_handle_io_cmd_async(q: &UblkQueue<'_>, tag: u16, buf_addr: *mut u8)
let off = (iod.start_sector << 9) as u64;
let bytes = (iod.nr_sectors << 9) as u32;

// Convert slice to pointer only when required by libublk API
// This conversion is necessary because io_uring operations require raw pointers
// for kernel interface compatibility. The slice ensures we have valid bounds.
let buf_addr = io_slice.as_mut_ptr();
let sqe = __lo_make_io_sqe(op, off, bytes, buf_addr);
let res = q.ublk_submit_sqe(sqe).await;
if res != -(libc::EAGAIN) {
Expand All @@ -170,7 +181,12 @@ async fn lo_handle_io_cmd_async(q: &UblkQueue<'_>, tag: u16, buf_addr: *mut u8)
return -libc::EAGAIN;
}

fn lo_handle_io_cmd_sync(q: &UblkQueue<'_>, tag: u16, i: &UblkIOCtx, buf_addr: *mut u8) {
/// Handle I/O operations synchronously (for comparison with async slice patterns).
///
/// Note: This sync handler still uses raw pointers as it follows the traditional
/// synchronous I/O pattern. The async handler above demonstrates the preferred
/// slice-based approach for new implementations.
fn lo_handle_io_cmd_sync(q: &UblkQueue<'_>, tag: u16, i: &UblkIOCtx, io_slice: &[u8]) {
let iod = q.get_iod(tag);
let op = iod.op_flags & 0xff;
let data = UblkIOCtx::build_user_data(tag as u16, op, 0, true);
Expand All @@ -182,31 +198,48 @@ fn lo_handle_io_cmd_sync(q: &UblkQueue<'_>, tag: u16, i: &UblkIOCtx, buf_addr: *
assert!(cqe_tag == tag as u32);

if res != -(libc::EAGAIN) {
q.complete_io_cmd(tag, buf_addr, Ok(UblkIORes::Result(res)));
q.complete_io_cmd(
tag,
io_slice.as_ptr() as *mut u8,
Ok(UblkIORes::Result(res)),
);
return;
}
}

let res = __lo_prep_submit_io_cmd(iod);
if res < 0 {
q.complete_io_cmd(tag, buf_addr, Ok(UblkIORes::Result(res)));
q.complete_io_cmd(
tag,
io_slice.as_ptr() as *mut u8,
Ok(UblkIORes::Result(res)),
);
} else {
let op = iod.op_flags & 0xff;
// either start to handle or retry
let off = (iod.start_sector << 9) as u64;
let bytes = (iod.nr_sectors << 9) as u32;
let sqe = __lo_make_io_sqe(op, off, bytes, buf_addr).user_data(data);
let sqe = __lo_make_io_sqe(op, off, bytes, io_slice.as_ptr() as *mut u8).user_data(data);
q.ublk_submit_sqe_sync(sqe).unwrap();
}
}

fn q_fn(qid: u16, dev: &UblkDev) {
let bufs_rc = Rc::new(dev.alloc_queue_io_bufs());
let bufs = bufs_rc.clone();

// Synchronous I/O handler demonstrating slice access patterns
let lo_io_handler = move |q: &UblkQueue, tag: u16, io: &UblkIOCtx| {
let bufs = bufs_rc.clone();

lo_handle_io_cmd_sync(q, tag, io, bufs[tag as usize].as_mut_ptr());
// Note: For educational purposes, this shows how slice access can be used
// even in sync handlers. The slice provides safe bounds-checked access.
let io_slice = bufs[tag as usize].as_slice();

// Convert to raw pointer only when required by legacy sync handler API
// This demonstrates the pattern: use slices for safety, convert to pointers
// only when absolutely necessary for API compatibility
lo_handle_io_cmd_sync(q, tag, io, &io_slice);
};

UblkQueue::new(qid, dev)
Expand All @@ -225,7 +258,11 @@ fn q_a_fn(qid: u16, dev: &UblkDev, depth: u16) {
let q = q_rc.clone();

f_vec.push(exe.spawn(async move {
let buf = IoBuf::<u8>::new(q.dev.dev_info.max_io_buf_bytes as usize);
// Use IoBuf for safe I/O buffer management with automatic memory alignment
let mut buf = IoBuf::<u8>::new(q.dev.dev_info.max_io_buf_bytes as usize);

// Extract raw pointer only when required by libublk APIs for buffer registration
// The raw pointer is needed for the libublk buffer registration system
let buf_addr = buf.as_mut_ptr();
let mut cmd_op = sys::UBLK_U_IO_FETCH_REQ;
let mut res = 0;
Expand All @@ -237,7 +274,11 @@ fn q_a_fn(qid: u16, dev: &UblkDev, depth: u16) {
break;
}

res = lo_handle_io_cmd_async(&q, tag, buf_addr).await;
// Use safe slice access for I/O operations
// IoBuf's as_mut_slice() provides bounds-checked access eliminating
// the need for unsafe pointer operations in the I/O handler
let io_slice = buf.as_mut_slice();
res = lo_handle_io_cmd_async(&q, tag, io_slice).await;
cmd_op = sys::UBLK_U_IO_COMMIT_AND_FETCH_REQ;
}
}));
Expand Down
22 changes: 15 additions & 7 deletions examples/null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,29 @@ fn get_io_cmd_result(q: &UblkQueue, tag: u16) -> i32 {
}

#[inline]
fn handle_io_cmd(q: &UblkQueue, tag: u16, buf_addr: *mut u8) {
fn handle_io_cmd(q: &UblkQueue, tag: u16, io_slice: Option<&[u8]>) {
let bytes = get_io_cmd_result(q, tag);

// Convert back to raw pointer only when required by the libublk API
let buf_addr = io_slice.map_or(std::ptr::null_mut(), |s| s.as_ptr() as *mut u8);
q.complete_io_cmd(tag, buf_addr, Ok(UblkIORes::Result(bytes)));
}

fn q_sync_fn(qid: u16, dev: &UblkDev, user_copy: bool) {
let bufs_rc = Rc::new(dev.alloc_queue_io_bufs());
let bufs = bufs_rc.clone();

// logic for io handling
// logic for io handling using safe slice-based operations
let io_handler = move |q: &UblkQueue, tag: u16, _io: &UblkIOCtx| {
let buf_addr = if user_copy {
std::ptr::null_mut()
// Use safe slice access instead of raw pointer manipulation
// This leverages IoBuf's Deref/DerefMut traits for memory safety
let io_slice = if user_copy {
None // No buffer slice for user_copy mode
} else {
bufs[tag as usize].as_mut_ptr()
// Safe slice creation using IoBuf's as_mut_slice() method
// Benefits: bounds checking, lifetime verification, no unsafe code
Some(bufs[tag as usize].as_slice())
};
handle_io_cmd(q, tag, buf_addr);
handle_io_cmd(q, tag, io_slice);
};

UblkQueue::new(qid, dev)
Expand All @@ -62,12 +67,15 @@ fn q_async_fn(qid: u16, dev: &UblkDev, user_copy: bool) {
f_vec.push(exe.spawn(async move {
let mut cmd_op = libublk::sys::UBLK_U_IO_FETCH_REQ;
let mut res = 0;
// Use IoBuf with slice-based access for memory safety
let (_buf, buf_addr) = if user_copy {
(None, std::ptr::null_mut())
} else {
let buf = IoBuf::<u8>::new(q.dev.dev_info.max_io_buf_bytes as usize);

q.register_io_buf(tag, &buf);
// Extract raw pointer only when required by libublk APIs
// IoBuf provides safe slice access via Deref/DerefMut traits
let addr = buf.as_mut_ptr();
(Some(buf), addr)
};
Expand Down
110 changes: 86 additions & 24 deletions examples/ramdisk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,74 @@ use std::io::{Error, ErrorKind};
use std::rc::Rc;
use std::sync::Arc;

fn handle_io(q: &UblkQueue, tag: u16, buf_addr: *mut u8, start: *mut u8) -> i32 {
/// Handle I/O operations using safe slice-based memory operations.
///
/// This function demonstrates how slice operations provide memory safety
/// benefits over raw pointer manipulation:
/// - Automatic bounds checking prevents buffer overflows
/// - Compile-time lifetime verification ensures memory safety
/// - No unsafe pointer arithmetic required
/// - Rust's ownership system prevents use-after-free errors
fn handle_io(q: &UblkQueue, tag: u16, io_buf: &mut [u8], ramdisk_storage: &mut [u8]) -> i32 {
let iod = q.get_iod(tag);
let off = (iod.start_sector << 9) as u64;
let bytes = (iod.nr_sectors << 9) as i32;
let off = (iod.start_sector << 9) as usize; // Convert to usize for slice indexing
let bytes = (iod.nr_sectors << 9) as usize; // Convert to usize for slice operations
let op = iod.op_flags & 0xff;

// Bounds checking: Ensure the operation doesn't exceed storage bounds
// This slice-based approach automatically prevents buffer overflows
// that could occur with raw pointer arithmetic and libc::memcpy
if off.saturating_add(bytes) > ramdisk_storage.len() {
return -libc::EINVAL;
}

// Ensure I/O buffer has sufficient capacity for the operation
// Slice bounds checking prevents reading/writing beyond buffer limits
if bytes > io_buf.len() {
return -libc::EINVAL;
}

match op {
libublk::sys::UBLK_IO_OP_READ => unsafe {
libc::memcpy(
buf_addr as *mut libc::c_void,
start.wrapping_add(off.try_into().unwrap()) as *mut libc::c_void,
bytes as usize,
);
libublk::sys::UBLK_IO_OP_READ => {
// Safe slice-to-slice copy operation replaces unsafe libc::memcpy
// copy_from_slice() automatically:
// - Verifies source and destination have compatible lengths
// - Performs bounds checking on both slices
// - Prevents buffer overflows through compile-time guarantees
let src = &ramdisk_storage[off..off + bytes];
let dst = &mut io_buf[..bytes];
dst.copy_from_slice(src);
},
libublk::sys::UBLK_IO_OP_WRITE => unsafe {
libc::memcpy(
start.wrapping_add(off.try_into().unwrap()) as *mut libc::c_void,
buf_addr as *mut libc::c_void,
bytes as usize,
);
libublk::sys::UBLK_IO_OP_WRITE => {
// Safe slice-to-slice copy operation replaces unsafe libc::memcpy
// This approach eliminates common memory safety issues:
// - No risk of writing beyond storage boundaries
// - Automatic length verification prevents partial writes to invalid memory
// - Slice bounds are verified at compile-time and runtime
let src = &io_buf[..bytes];
let dst = &mut ramdisk_storage[off..off + bytes];
dst.copy_from_slice(src);
},
libublk::sys::UBLK_IO_OP_FLUSH => {}
libublk::sys::UBLK_IO_OP_FLUSH => {
// Flush operation requires no memory copying
}
_ => {
return -libc::EINVAL;
}
}

bytes
bytes as i32
}

async fn io_task(q: &UblkQueue<'_>, tag: u16, dev_buf_addr: *mut u8) {
async fn io_task(q: &UblkQueue<'_>, tag: u16, ramdisk_storage: &mut [u8]) {
let buf_size = q.dev.dev_info.max_io_buf_bytes as usize;
let buffer = IoBuf::<u8>::new(buf_size);

// Use IoBuf for safe I/O buffer management with automatic memory alignment
// IoBuf provides slice-based access through Deref/DerefMut traits
let mut buffer = IoBuf::<u8>::new(buf_size);

// Extract raw pointer only when required by libublk APIs
// For actual memory operations, we'll use safe slice access
let addr = buffer.as_mut_ptr();
let mut cmd_op = libublk::sys::UBLK_U_IO_FETCH_REQ;
let mut res = 0;
Expand All @@ -57,7 +92,11 @@ async fn io_task(q: &UblkQueue<'_>, tag: u16, dev_buf_addr: *mut u8) {
break;
}

res = handle_io(&q, tag, addr, dev_buf_addr);
// Use safe slice access for memory operations
// IoBuf's as_mut_slice() provides bounds-checked access
// This eliminates the need for unsafe pointer operations
let io_slice = buffer.as_mut_slice();
res = handle_io(&q, tag, io_slice, ramdisk_storage);
cmd_op = libublk::sys::UBLK_U_IO_COMMIT_AND_FETCH_REQ;
}
}
Expand Down Expand Up @@ -108,7 +147,7 @@ fn read_dev_id(efd: i32) -> Result<i32, Error> {

///run this ramdisk ublk daemon completely in single context with
///async control command, no need Rust async any more
fn rd_add_dev(dev_id: i32, buf_addr: *mut u8, size: u64, for_add: bool, efd: i32) {
fn rd_add_dev(dev_id: i32, ramdisk_storage: &mut [u8], size: u64, for_add: bool, efd: i32) {
let dev_flags = if for_add {
UblkFlags::UBLK_DEV_F_ADD_DEV
} else {
Expand Down Expand Up @@ -137,11 +176,25 @@ fn rd_add_dev(dev_id: i32, buf_addr: *mut u8, size: u64, for_add: bool, efd: i32

// spawn async io tasks
let mut f_vec = Vec::new();

// Extract raw pointer and length for sharing across async tasks
// This is the minimal unsafe code needed for async context sharing
let storage_ptr = ramdisk_storage.as_mut_ptr();
let storage_len = ramdisk_storage.len();

for tag in 0..ctrl.dev_info().queue_depth as u16 {
let q_clone = q_rc.clone();

f_vec.push(exec.spawn(async move {
io_task(&q_clone, tag, buf_addr).await;
// Reconstruct slice from raw pointer for each async task
// This is safe because:
// 1. The original ramdisk_storage buffer outlives all async tasks
// 2. Each task operates on different regions controlled by I/O offset bounds
// 3. The slice provides bounds checking for all operations within io_task
let storage_slice = unsafe {
std::slice::from_raw_parts_mut(storage_ptr, storage_len)
};
io_task(&q_clone, tag, storage_slice).await;
}));
}

Expand Down Expand Up @@ -191,8 +244,17 @@ fn test_add(recover: usize) {
ctrl.start_user_recover().unwrap();
}

let buf = libublk::helpers::IoBuf::<u8>::new(size as usize);
rd_add_dev(dev_id, buf.as_mut_ptr(), size, recover == 0, efd);
// Create ramdisk storage using IoBuf for proper alignment and memory management
// IoBuf provides safe slice access while maintaining required memory alignment
let mut ramdisk_buf = libublk::helpers::IoBuf::<u8>::new(size as usize);

// Zero-initialize the ramdisk storage for consistent behavior
// Using safe slice operations instead of unsafe memory manipulation
ramdisk_buf.zero_buf();

// Get mutable slice for safe operations within rd_add_dev
let storage_slice = ramdisk_buf.as_mut_slice();
rd_add_dev(dev_id, storage_slice, size, recover == 0, efd);
}
daemonize::Outcome::Parent(Ok(_)) => match read_dev_id(efd) {
Ok(id) => UblkCtrl::new_simple(id).unwrap().dump(),
Expand Down
Loading