Skip to content

Commit abd5c8a

Browse files
committed
fix(vfs): fence off the io_uring ring after an abandoned submission
An SQE names its transfer buffer by raw pointer, so once a submission reaches the kernel the buffer must stay valid until its completion is observed. A submit cycle that could not fully account for its completions used to let the caller free those buffers anyway. Track that case explicitly, poison the ring so later submissions fail instead of racing with stale completions, and require the caller to leak (never free) buffers behind an abandoned submission.
1 parent 9ee464c commit abd5c8a

3 files changed

Lines changed: 249 additions & 165 deletions

File tree

src/vfs/iouring/file.rs

Lines changed: 57 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,36 @@
2323
use std::os::unix::io::AsRawFd;
2424
use std::sync::Arc;
2525

26-
use io_uring::IoUring;
2726
use io_uring::opcode;
2827
use io_uring::types::Fd;
29-
use parking_lot::Mutex;
3028

3129
use crate::Result;
3230
use crate::errors::PagedbError;
3331
use crate::vfs::blocking::offload;
32+
use crate::vfs::iouring::ring::{SharedRing, SubmitError};
3433
use crate::vfs::traits::{
35-
VfsFile, checked_indexed_completion, checked_iouring_positioned_offset, checked_read_count,
36-
checked_signed_file_len, write_all_at,
34+
VfsFile, checked_iouring_positioned_offset, checked_read_count, checked_signed_file_len,
35+
write_all_at,
3736
};
3837
use crate::vfs::types::{ReadReq, WriteReq};
3938

4039
/// Per-file handle backed by an `std::fs::File` fd and the shared `io_uring`.
4140
///
4241
/// `Send` and `Sync` are both derived, not asserted: `Arc<std::fs::File>` is
43-
/// thread-safe, and `io_uring::IoUring` is `Send + Sync`, so `Arc<Mutex<_>>`
44-
/// around it is too. Nothing about this type needs a hand-written auto-trait
45-
/// impl, and it must not grow one — a manual impl would silently keep holding
46-
/// once a future field stopped qualifying.
42+
/// thread-safe, and `SharedRing` wraps the ring in a `Mutex`, so an `Arc` of
43+
/// it is too. Nothing about this type needs a hand-written auto-trait impl,
44+
/// and it must not grow one — a manual impl would silently keep holding once a
45+
/// future field stopped qualifying.
4746
pub struct IouringFile {
4847
/// Shared so a blocking-pool call can own a reference to the descriptor
4948
/// for its whole duration, independently of when this handle drops.
5049
file: Arc<std::fs::File>,
5150
writable: bool,
52-
ring: Arc<Mutex<IoUring>>,
51+
ring: Arc<SharedRing>,
5352
}
5453

5554
impl IouringFile {
56-
pub(crate) fn new(file: std::fs::File, writable: bool, ring: Arc<Mutex<IoUring>>) -> Self {
55+
pub(crate) fn new(file: std::fs::File, writable: bool, ring: Arc<SharedRing>) -> Self {
5756
Self {
5857
file: Arc::new(file),
5958
writable,
@@ -63,7 +62,7 @@ impl IouringFile {
6362

6463
/// The two owned handles a blocking-pool cycle needs. Both are `Arc`
6564
/// clones, and both keep their target alive for as long as the cycle runs.
66-
fn shared(&self) -> (Arc<std::fs::File>, Arc<Mutex<IoUring>>) {
65+
fn shared(&self) -> (Arc<std::fs::File>, Arc<SharedRing>) {
6766
(Arc::clone(&self.file), Arc::clone(&self.ring))
6867
}
6968

@@ -91,121 +90,32 @@ impl IouringFile {
9190
.map_err(|_| PagedbError::Io(std::io::Error::other("buffer too large for u32")))
9291
}
9392

94-
/// Submit a single SQE, wait for exactly one CQE with matching
95-
/// `user_data`, and return the CQE result.
93+
/// Run one submit + drain cycle, translating the ring's memory-safety
94+
/// verdict into a plain error while leaking whatever the kernel may still
95+
/// own.
9696
///
97-
/// # Safety
98-
///
99-
/// The caller must ensure that any buffers referenced by the SQE remain
100-
/// valid for the duration of this call (i.e. until `submit_and_wait`
101-
/// returns and the CQE is drained). Because the ring lock is held across
102-
/// the entire submit+drain sequence and we wait for the exact CQE before
103-
/// returning, this invariant is satisfied for any buffer whose lifetime
104-
/// outlasts this function.
105-
unsafe fn submit_one(
106-
ring: &mut IoUring,
107-
entry: &io_uring::squeue::Entry,
108-
user_data: u64,
109-
) -> std::io::Result<i32> {
110-
// SAFETY: caller guarantees the buffers referenced by `entry` are live.
111-
unsafe {
112-
ring.submission()
113-
.push(entry)
114-
.map_err(|_| std::io::Error::other("submission queue full"))?;
115-
}
116-
ring.submit_and_wait(1)?;
117-
let mut result = None;
118-
{
119-
let mut cq = ring.completion();
120-
cq.sync();
121-
for cqe in cq.by_ref() {
122-
if cqe.user_data() == user_data {
123-
result = Some(cqe.result());
124-
break;
125-
}
126-
// Stale CQEs from prior submissions are discarded.
127-
}
128-
}
129-
let res =
130-
result.ok_or_else(|| std::io::Error::other("io_uring: expected CQE not found"))?;
131-
if res < 0 {
132-
Err(std::io::Error::from_raw_os_error(-res))
133-
} else {
134-
Ok(res)
135-
}
136-
}
137-
138-
/// Submit a batch of SQEs and wait for all of them. Each SQE must carry
139-
/// its index (0..n) as `user_data`. Returns CQE results in submission order.
140-
///
141-
/// # Safety
142-
///
143-
/// All buffers referenced by every entry in `entries` must remain valid
144-
/// until this function returns (same contract as `submit_one`).
145-
unsafe fn submit_batch(
146-
ring: &mut IoUring,
97+
/// `buffers` is every allocation the SQEs point at. On the abandoned path
98+
/// it is forgotten rather than dropped: entries are still outstanding, and
99+
/// the kernel writing into reclaimed memory is the one outcome that must
100+
/// not be possible. Leaking a bounded amount on a failing device is the
101+
/// cheap side of that trade.
102+
fn run_cycle<B>(
103+
ring: &SharedRing,
147104
entries: &[io_uring::squeue::Entry],
148-
) -> std::io::Result<Vec<i32>> {
149-
let total = entries.len();
150-
if total == 0 {
151-
return Ok(Vec::new());
152-
}
153-
// Cap each submission at the ring's SQ depth. Larger callers
154-
// (a full B+ tree flush) get chunked across multiple ring round-trips.
155-
// Each chunk re-tags `user_data` with the index within the chunk so
156-
// the CQE drain can match results into the global results vector.
157-
let chunk_size = crate::vfs::iouring::ring::RING_DEPTH as usize;
158-
let mut results = vec![0i32; total];
159-
let mut base = 0usize;
160-
while base < total {
161-
let end = (base + chunk_size).min(total);
162-
let chunk_len = end - base;
163-
{
164-
let mut sq = ring.submission();
165-
for (i, entry) in entries[base..end].iter().enumerate() {
166-
// Re-tag with the in-chunk index. The caller-assigned
167-
// `user_data` is overwritten because the outer `for cqe`
168-
// loop needs a stable 0..chunk_len keyspace.
169-
let tagged = entry.clone().user_data(i as u64);
170-
// SAFETY: caller guarantees buffers are live for `entries`.
171-
unsafe {
172-
sq.push(&tagged)
173-
.map_err(|_| std::io::Error::other("submission queue full"))?;
174-
}
175-
}
105+
buffers: B,
106+
) -> Result<(Vec<i32>, B)> {
107+
// SAFETY: `buffers` owns every allocation the entries reference and is
108+
// moved into this function, so the buffers outlive the call; on the
109+
// `Abandoned` path it is leaked instead of dropped, which extends that
110+
// lifetime for the rest of the process as the contract requires.
111+
match unsafe { ring.submit_and_collect(entries) } {
112+
Ok(results) => Ok((results, buffers)),
113+
Err(SubmitError::Settled(error)) => Err(PagedbError::Io(error)),
114+
Err(abandoned @ SubmitError::Abandoned(_)) => {
115+
std::mem::forget(buffers);
116+
Err(PagedbError::Io(abandoned.into_io()))
176117
}
177-
ring.submit_and_wait(chunk_len)?;
178-
let mut chunk_results = vec![None; chunk_len];
179-
let mut found = 0usize;
180-
{
181-
let mut cq = ring.completion();
182-
cq.sync();
183-
for cqe in cq.by_ref() {
184-
if checked_indexed_completion(&mut chunk_results, cqe.user_data(), cqe.result())
185-
.map_err(|error| match error {
186-
PagedbError::Io(io) => io,
187-
other => std::io::Error::other(other.to_string()),
188-
})?
189-
{
190-
found += 1;
191-
}
192-
if found == chunk_len {
193-
break;
194-
}
195-
}
196-
}
197-
if found < chunk_len {
198-
return Err(std::io::Error::other(
199-
"io_uring: fewer CQEs returned than submitted",
200-
));
201-
}
202-
for (index, result) in chunk_results.into_iter().enumerate() {
203-
results[base + index] = result
204-
.ok_or_else(|| std::io::Error::other("io_uring: missing indexed CQE result"))?;
205-
}
206-
base = end;
207118
}
208-
Ok(results)
209119
}
210120
}
211121

@@ -227,15 +137,9 @@ impl VfsFile for IouringFile {
227137
.offset(offset)
228138
.build()
229139
.user_data(0);
230-
let n = {
231-
let mut guard = ring.lock();
232-
// SAFETY: `scratch` is owned by this closure and outlives the
233-
// submit+drain below, and the ring lock is held across both, so
234-
// the kernel is finished with the buffer before this returns.
235-
unsafe { IouringFile::submit_one(&mut guard, &entry, 0) }
236-
}
237-
.map_err(PagedbError::Io)?;
238-
// n >= 0 guaranteed by submit_one (negative becomes Err).
140+
let (results, scratch) = IouringFile::run_cycle(&ring, &[entry], scratch)?;
141+
let n = single_result(&results)?;
142+
// `single_result` rejects a negative CQE, so this is non-negative.
239143
#[allow(clippy::cast_sign_loss)]
240144
let read = checked_read_count(n as usize, len)?;
241145
Ok((scratch, read))
@@ -273,15 +177,7 @@ impl VfsFile for IouringFile {
273177
);
274178
}
275179

276-
let results = {
277-
let mut guard = ring.lock();
278-
// SAFETY: every buffer referenced by `entries` lives in
279-
// `buffers`, owned by this closure, and the ring lock is held
280-
// across submit+drain so the kernel cannot touch them after
281-
// `submit_batch` returns.
282-
unsafe { IouringFile::submit_batch(&mut guard, &entries) }
283-
}
284-
.map_err(PagedbError::Io)?;
180+
let (results, buffers) = IouringFile::run_cycle(&ring, &entries, buffers)?;
285181
drop(entries); // buf raw-ptrs no longer needed
286182

287183
let mut out: Vec<(Vec<u8>, usize)> = Vec::with_capacity(plan.len());
@@ -328,14 +224,10 @@ impl VfsFile for IouringFile {
328224
.offset(offset)
329225
.build()
330226
.user_data(0);
331-
let n = {
332-
let mut guard = ring.lock();
333-
// SAFETY: `data` is owned by this closure and outlives the
334-
// submit+drain below; the ring lock is held across both.
335-
unsafe { IouringFile::submit_one(&mut guard, &entry, 0) }
336-
}
337-
.map_err(PagedbError::Io)?;
338-
// n >= 0 guaranteed by submit_one.
227+
let (results, data) = IouringFile::run_cycle(&ring, &[entry], data)?;
228+
let n = single_result(&results)?;
229+
drop(data);
230+
// `single_result` rejects a negative CQE, so this is non-negative.
339231
#[allow(clippy::cast_sign_loss)]
340232
let written = n as usize;
341233
Ok(written)
@@ -388,14 +280,7 @@ impl VfsFile for IouringFile {
388280
);
389281
}
390282

391-
let results = {
392-
let mut guard = ring.lock();
393-
// SAFETY: every buffer referenced by `entries` lives in `plan`,
394-
// owned by this closure, and the ring lock is held across
395-
// submit+drain.
396-
unsafe { IouringFile::submit_batch(&mut guard, &entries) }
397-
}
398-
.map_err(PagedbError::Io)?;
283+
let (results, plan) = IouringFile::run_cycle(&ring, &entries, plan)?;
399284
drop(entries);
400285

401286
let mut short_writes = Vec::new();
@@ -448,13 +333,11 @@ impl VfsFile for IouringFile {
448333
offload(move || {
449334
let fd = Fd(file.as_raw_fd());
450335
let entry = opcode::Fsync::new(fd).build().user_data(0);
451-
let completed = {
452-
let mut guard = ring.lock();
453-
// SAFETY: `Fsync` carries no buffer pointer; there is nothing
454-
// to alias. The `Arc` keeps the fd open for the whole call.
455-
unsafe { IouringFile::submit_one(&mut guard, &entry, 0) }
456-
};
457-
completed.map_err(PagedbError::Io)?;
336+
// `Fsync` carries no buffer pointer, so there is nothing the
337+
// kernel could still be reading; the unit payload makes that
338+
// explicit rather than leaving an empty allocation to leak.
339+
let (results, ()) = IouringFile::run_cycle(&ring, &[entry], ())?;
340+
single_result(&results)?;
458341
Ok(())
459342
})
460343
.await
@@ -495,3 +378,14 @@ impl VfsFile for IouringFile {
495378
true
496379
}
497380
}
381+
382+
/// Unwrap a one-entry cycle's result, turning a negative CQE into its errno.
383+
fn single_result(results: &[i32]) -> Result<i32> {
384+
let result = *results
385+
.first()
386+
.ok_or_else(|| PagedbError::Io(std::io::Error::other("io_uring: no CQE for entry")))?;
387+
if result < 0 {
388+
return Err(PagedbError::Io(std::io::Error::from_raw_os_error(-result)));
389+
}
390+
Ok(result)
391+
}

0 commit comments

Comments
 (0)