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
40 changes: 29 additions & 11 deletions glidefs/src/block/ublk/buffer_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,11 @@ pub struct PoolSlot {

impl PoolSlot {
#[inline]
pub fn as_mut_slice(&mut self, len: usize) -> &mut [u8] {
debug_assert!(len <= SLOT_SIZE, "slot len {len} exceeds slot size {SLOT_SIZE}");
unsafe { std::slice::from_raw_parts_mut(self.ptr, len) }
pub fn as_mut_slice(&mut self, len: usize) -> Option<&mut [u8]> {
if len > SLOT_SIZE {
return None;
}
Some(unsafe { std::slice::from_raw_parts_mut(self.ptr, len) })
}

#[inline]
Expand Down Expand Up @@ -343,10 +345,10 @@ impl IoBuf {
/// The first `len` bytes of the buffer. `len` is bounded by `SLOT_SIZE`
/// upstream (a single ublk I/O never exceeds `IO_BUF_BYTES`).
#[inline]
pub fn as_mut_slice(&mut self, len: usize) -> &mut [u8] {
pub fn as_mut_slice(&mut self, len: usize) -> Option<&mut [u8]> {
match self {
IoBuf::Pooled(slot) => slot.as_mut_slice(len),
IoBuf::Heap(v) => &mut v[..len],
IoBuf::Heap(v) => v.get_mut(..len),
}
}
}
Expand All @@ -373,12 +375,15 @@ fn try_alloc_zeroed(len: usize) -> Option<Vec<u8>> {
/// (init OOM, see [`worker_pool`]), a fallibly-allocated heap buffer — counted
/// in `GLOBAL_HEAP_FALLBACKS` so sustained degradation is alertable.
///
/// Returns `None` only in the doubly-degraded case where the pool is absent
/// *and* even the heap fallback can't be committed (host critically OOM). The
/// caller must fail that single I/O with `EIO` — never abort. This is the
/// per-I/O sibling of the init-path fix: neither `mmap` nor `vec` failure may
/// take down storage for every VM on the host.
/// Returns `None` for an invalid oversized request or in the doubly-degraded
/// case where the pool is absent and even the heap fallback can't be committed
/// (host critically OOM). The caller must fail that single I/O with `EIO` —
/// never abort. This is the per-I/O sibling of the init-path fix: neither
/// `mmap` nor `vec` failure may take down storage for every VM on the host.
pub async fn acquire_io_buf(len: usize) -> Option<IoBuf> {
if len > SLOT_SIZE {
return None;
}
match worker_pool() {
Some(pool) => Some(IoBuf::Pooled(pool.acquire().await)),
None => match try_alloc_zeroed(len) {
Expand Down Expand Up @@ -426,14 +431,27 @@ mod tests {
fn try_acquire_round_trip() {
let pool = Rc::new(WorkerBufferPool::new().unwrap());
let mut slot = pool.try_acquire().unwrap();
let buf = slot.as_mut_slice(4096);
let buf = slot.as_mut_slice(4096).unwrap();
buf[0] = 0x42;
buf[4095] = 0x42;
assert_eq!(pool.acquires.load(Ordering::Relaxed), 1);
drop(slot);
assert_eq!(pool.free.borrow().len(), POOL_SLOTS);
}

#[test]
fn pool_slot_rejects_oversized_slice() {
let pool = Rc::new(WorkerBufferPool::new().unwrap());
let mut slot = pool.try_acquire().unwrap();
assert!(slot.as_mut_slice(SLOT_SIZE).is_some());
assert!(slot.as_mut_slice(SLOT_SIZE + 1).is_none());
}

#[tokio::test]
async fn acquire_io_buf_rejects_oversized_len() {
assert!(acquire_io_buf(SLOT_SIZE + 1).await.is_none());
}

#[test]
fn try_acquire_returns_none_on_exhaustion() {
let pool = Rc::new(WorkerBufferPool::new().unwrap());
Expand Down
23 changes: 15 additions & 8 deletions glidefs/src/block/ublk/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2125,22 +2125,29 @@ async fn io_task_user_copy(
// the daemon keeps serving instead of aborting; the worker
// upgrades back to the pool once memory recovers.
//
// `None` is the doubly-degraded case: no pool *and* the heap
// fallback couldn't be committed (host critically OOM). Fail
// just this one I/O with EIO — the daemon stays up serving
// every other tag and VM; the kernel retries the I/O. The
// alternative, an infallible alloc, would SIGABRT the whole
// daemon and take down storage host-wide.
// `None` means either an invalid oversized request or the
// doubly-degraded case: no pool *and* the heap fallback couldn't
// be committed (host critically OOM). Fail just this one I/O
// with EIO — the daemon stays up serving every other tag and VM.
let Some(mut iobuf) = super::buffer_pool::acquire_io_buf(length as usize).await
else {
tracing::error!(
qid, tag, length,
"bounce buffer alloc failed (host OOM) — failing this I/O with EIO",
max_len = super::buffer_pool::SLOT_SIZE,
"bounce buffer unavailable or length invalid — failing this I/O with EIO",
);
q.submit_io_commit_cmd(tag, BufDesc::Slice(&[]), -libc::EIO).await?;
continue;
};
let Some(buf) = iobuf.as_mut_slice(length as usize) else {
tracing::error!(
qid, tag, length,
max_len = super::buffer_pool::SLOT_SIZE,
"bounce buffer length exceeds slot size — failing this I/O with EIO",
);
q.submit_io_commit_cmd(tag, BufDesc::Slice(&[]), -libc::EIO).await?;
continue;
};
let buf: &mut [u8] = iobuf.as_mut_slice(length as usize);

if op == sys::UBLK_IO_OP_WRITE {
// Copy WRITE data out of the kernel cmd buffer into ours.
Expand Down
Loading