rustfs_uring/driver.rs
1// Copyright 2024 RustFS Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{HashMap, HashSet, VecDeque};
16use std::fs::File;
17use std::io;
18use std::io::Write as _;
19use std::os::fd::{AsRawFd, FromRawFd};
20use std::os::unix::ffi::OsStrExt;
21use std::pin::Pin;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
24use std::sync::mpsc::{self, TryRecvError};
25use std::task::{Context, Poll};
26use std::thread::JoinHandle;
27use std::time::{Duration, Instant};
28
29use io_uring::{IoUring, opcode, types};
30
31/// Upper bound on how long shutdown waits for in-flight ops to drain before
32/// leaking the ring+buffers and exiting (C4, rustfs/backlog#1055). ASYNC_CANCEL
33/// cannot interrupt an in-execution regular-file read on a D-state/NFS-hung
34/// disk, so drain-to-zero can be non-terminating; this bounds it.
35const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
36use tokio::sync::{OwnedSemaphorePermit, Semaphore, TryAcquireError, oneshot};
37
38/// user_data bit marking the CQE of an `AsyncCancel` SQE itself (as opposed
39/// to the CQE of the read op it targets).
40const CANCEL_BIT: u64 = 1 << 63;
41
42/// `offset` value meaning "use the file's current position" (read(2)
43/// semantics); required for pipes/sockets where pread returns ESPIPE.
44const CURRENT_POSITION: u64 = u64::MAX;
45
46/// Kernel single-read cap: `MAX_RW_COUNT = INT_MAX & PAGE_MASK` (2 GiB − 4 KiB
47/// on 4 KiB pages). io_uring's READ length field is a u32, and any request
48/// above this short-reads. We reject beyond it in `submit` so a `len as u32`
49/// truncation can never silently turn a huge read into a 0-byte "EOF" (C6,
50/// rustfs/backlog#1057); P2 must chunk reads larger than this.
51const MAX_READ_LEN: usize = 0x7fff_f000;
52
53/// Block-aligned superset geometry for a read (rustfs/backlog#1102).
54///
55/// Returns `(kernel_offset, head, region_len)`: the offset handed to the kernel,
56/// how many bytes of the read region precede the caller's logical range, and how
57/// many bytes the kernel is asked to read. `align == 1` is the buffered case and
58/// passes `offset` (which may be `CURRENT_POSITION`) straight through.
59///
60/// `None` when `align` is not a power of two or the aligned range would overflow.
61fn aligned_geometry(offset: u64, len: usize, align: usize) -> Option<(u64, usize, usize)> {
62 // A real device block is tiny (512..=4096). Capping alignment at the read
63 // cap keeps `align_offset(align)` always satisfiable (so it never returns
64 // `usize::MAX`, which would make the driver's later `ptr::add(pad)` UB) and
65 // keeps the `region_len + align - 1` allocation from overflowing `usize`.
66 if align == 0 || !align.is_power_of_two() || align > MAX_READ_LEN {
67 return None;
68 }
69 if align == 1 {
70 return Some((offset, 0, len));
71 }
72 let mask = align as u64 - 1;
73 let kernel_offset = offset & !mask;
74 let head = usize::try_from(offset - kernel_offset).ok()?;
75 let region_len = head.checked_add(len)?.checked_next_multiple_of(align)?;
76 Some((kernel_offset, head, region_len))
77}
78
79/// Heartbeat bound on the driver loop's blocking wait (backlog#1102). The loop
80/// normally wakes on a CQE (the ring's registered eventfd) or a new message
81/// (the wakeup eventfd); this timeout only bounds the wait so the bounded-drain
82/// deadline is still checked and any queued cancel is picked up promptly.
83const LOOP_HEARTBEAT: Duration = Duration::from_millis(50);
84
85/// Heartbeat used when the shard is fully idle — no in-flight ops and not
86/// shutting down (rustfs/backlog#1169). New work still wakes the loop instantly
87/// via `wake_efd` and completions via the registered `cq_efd`; this only bounds
88/// the fallback wait, so a much longer value cuts idle timer/syscall churn
89/// across many per-disk shards without affecting latency.
90const IDLE_HEARTBEAT: Duration = Duration::from_secs(1);
91
92/// Per-ring cap on io-wq BOUNDED workers (rustfs/backlog#1169). Cold buffered
93/// and O_DIRECT reads punted to io-wq each spawn a bounded worker, and the
94/// kernel default is min(sq_entries, 4*nCPU) PER ring — one ring per shard per
95/// disk can otherwise materialize thousands of PF_IO_WORKER threads under a
96/// cold-read burst. Best-effort (needs kernel >= 5.15); older kernels keep the
97/// default.
98const IOWQ_MAX_BOUNDED_WORKERS: u32 = 16;
99
100/// Consecutive non-transient `ring.submit()` failures the driver tolerates
101/// before it stops retrying silently and shuts the shard down, so callers get a
102/// driver-gone error and fall back to the std backend instead of stalling
103/// forever on ops the kernel will never accept (rustfs/backlog#1162). With the
104/// 50 ms heartbeat this bounds the silent-retry window to a few seconds.
105const MAX_CONSECUTIVE_SUBMIT_ERRORS: u32 = 128;
106
107/// How many times a single logical read retries a transient CQE errno
108/// (EINTR/EAGAIN) without making progress before it surfaces the error, so a
109/// pathological storm cannot spin the driver thread (rustfs/backlog#1166).
110const MAX_TRANSIENT_RETRIES: u32 = 16;
111
112/// Owned `eventfd(2)` used to wake the driver loop (backlog#1102): one is
113/// registered with the ring so the kernel signals it on every CQE, the other is
114/// signaled by `submit`/shutdown so a new message wakes the loop immediately —
115/// together they replace the spike's 200 µs busy-poll.
116struct EventFd {
117 fd: std::os::fd::RawFd,
118}
119
120impl EventFd {
121 fn new() -> io::Result<Self> {
122 // SAFETY: eventfd returns a fresh owned fd or -1; the flags are valid.
123 let fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
124 if fd < 0 {
125 return Err(io::Error::last_os_error());
126 }
127 Ok(Self { fd })
128 }
129
130 fn as_raw(&self) -> std::os::fd::RawFd {
131 self.fd
132 }
133
134 /// Make the fd readable. A saturated counter (EAGAIN) is fine — it is
135 /// already readable, which is all a wakeup needs.
136 fn signal(&self) {
137 let v: u64 = 1;
138 // SAFETY: writing 8 bytes from a valid u64 to an eventfd we own.
139 unsafe {
140 libc::write(self.fd, (&v as *const u64).cast(), 8);
141 }
142 }
143
144 /// Reset the counter. EFD_NONBLOCK guarantees this never blocks; a single
145 /// successful read drains the whole counter, the next returns EAGAIN.
146 fn drain(&self) {
147 let mut v: u64 = 0;
148 // SAFETY: reading 8 bytes into a valid u64 from an eventfd we own.
149 while unsafe { libc::read(self.fd, (&mut v as *mut u64).cast(), 8) } == 8 {}
150 }
151}
152
153impl Drop for EventFd {
154 fn drop(&mut self) {
155 // SAFETY: we own this fd and drop it exactly once.
156 unsafe {
157 libc::close(self.fd);
158 }
159 }
160}
161
162/// Block until a CQE is ready (`cq`), a new message arrives (`wake`), or the
163/// heartbeat elapses. The return value is ignored: a spurious wakeup, timeout,
164/// or EINTR just runs one loop turn (intake + reap), which is always safe.
165fn wait_for_events(cq: &EventFd, wake: &EventFd, timeout: Duration) {
166 let mut fds = [
167 libc::pollfd {
168 fd: cq.as_raw(),
169 events: libc::POLLIN,
170 revents: 0,
171 },
172 libc::pollfd {
173 fd: wake.as_raw(),
174 events: libc::POLLIN,
175 revents: 0,
176 },
177 ];
178 let ms = timeout.as_millis().min(i32::MAX as u128) as libc::c_int;
179 // SAFETY: `fds` is a valid, initialized array of two pollfds.
180 unsafe {
181 libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, ms);
182 }
183}
184
185/// Why the probe refused to start the io_uring driver.
186///
187/// Mirrors the P2 degradation contract (backlog#894): a restricted
188/// environment must be recognized and answered with a silent fallback to the
189/// std backend, never surfaced to callers.
190#[derive(Debug)]
191pub enum ProbeFailure {
192 /// `io_uring_setup` itself failed (seccomp/gVisor/old kernel).
193 Setup(io::Error),
194 /// The ring was created but a real `IORING_OP_READ` did not complete
195 /// correctly (gVisor accepts setup but fails ops; also covers silent
196 /// data corruption, which we treat as "unusable").
197 ReadOp(io::Error),
198}
199
200impl ProbeFailure {
201 /// True when the **probe-time** errno belongs to the "expected
202 /// restriction" class that P2 maps to permanent per-disk fallback:
203 /// EACCES/EPERM/ENOSYS/EINVAL/EOPNOTSUPP. Anything else is a genuine bug
204 /// worth surfacing.
205 ///
206 /// IMPORTANT (C7, rustfs/backlog#1059): this classification is valid ONLY
207 /// for a one-shot startup probe, where these errnos unambiguously mean
208 /// "io_uring is unusable here" (gVisor/seccomp/old kernel). Runtime
209 /// per-op errnos have different semantics and MUST NOT reuse this class.
210 /// In particular EINVAL is triple-meaning at runtime — offset > i64::MAX
211 /// (signed loff_t), O_DIRECT buffer/offset/len misalignment (P2 will use
212 /// O_DIRECT), and setup `entries` over the cap — none of which imply the
213 /// disk should be permanently degraded off io_uring. P2's degradation
214 /// contract must split errnos into three classes:
215 ///
216 /// * probe-time restriction -> degrade this disk to the std backend;
217 /// * runtime parameter error -> return the error to the caller (and,
218 /// for a suspected bug, re-verify once via std pread) — never latch;
219 /// * transient (EINTR/EAGAIN) -> retry, never surface.
220 ///
221 /// See `submit` for the offset guard that keeps a caller arithmetic bug
222 /// from ever reaching the kernel as a runtime EINVAL.
223 pub fn is_expected_restriction(&self) -> bool {
224 let err = match self {
225 ProbeFailure::Setup(e) | ProbeFailure::ReadOp(e) => e,
226 };
227 matches!(
228 err.raw_os_error(),
229 Some(libc::EACCES) | Some(libc::EPERM) | Some(libc::ENOSYS) | Some(libc::EINVAL) | Some(libc::EOPNOTSUPP)
230 )
231 }
232}
233
234// Submission-side backpressure (C10, rustfs/backlog#1060; async in #1102).
235//
236// A `tokio::sync::Semaphore` with `entries` permits bounds in-flight ops below
237// CQ capacity. The load-bearing rule is the RELEASE POINT: a permit is released
238// at the CQE (when the pending-table entry is removed), NOT at future drop.
239// Tying a permit to the future (the natural RAII shape) would let a quorum
240// dropping many futures return permits while their orphan buffers still sit in
241// the pending table awaiting slow-disk CQEs, decoupling the permit count from
242// resident memory and reopening the memory-DoS surface.
243//
244// That rule is now enforced by the type system rather than by a manual
245// `release()` call: the `OwnedSemaphorePermit` travels with `Msg::Read` into the
246// `Pending` entry and is dropped exactly when the entry is removed at the final
247// CQE. A short-read resubmit keeps the entry — and thus the permit.
248//
249// Acquisition never blocks the caller's thread: `submit` takes the permit with
250// `try_acquire_owned()` on the common unsaturated path (no allocation, no await,
251// submission stays eager), and when saturated it hands the acquire future to the
252// returned `ReadHandle`, which awaits it on its first poll and submits then.
253
254/// Boxed `Semaphore::acquire_owned` future held by a saturated `ReadHandle`.
255type AcquireFut = Pin<Box<dyn Future<Output = Result<OwnedSemaphorePermit, tokio::sync::AcquireError>> + Send>>;
256
257#[derive(Default)]
258struct DriverStats {
259 submitted: AtomicU64,
260 delivered: AtomicU64,
261 orphan_reclaimed: AtomicU64,
262 in_flight: AtomicU64,
263 cancel_succeeded: AtomicU64,
264 cancel_not_found: AtomicU64,
265 cancel_already: AtomicU64,
266 cq_overflow: AtomicU64,
267 submit_errors: AtomicU64,
268}
269
270/// Point-in-time copy of the driver counters.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
272pub struct StatsSnapshot {
273 /// Read ops handed to the kernel.
274 pub submitted: u64,
275 /// CQEs whose result was received by a live caller.
276 pub delivered: u64,
277 /// CQEs whose caller had dropped the future: the buffer stayed in the
278 /// pending table the whole time and was reclaimed here, at the CQE.
279 pub orphan_reclaimed: u64,
280 /// Ops submitted but not yet completed. The kernel may still write into
281 /// their buffers.
282 pub in_flight: u64,
283 /// ASYNC_CANCEL CQEs that reported the target op was canceled (res == 0).
284 pub cancel_succeeded: u64,
285 /// ASYNC_CANCEL CQEs that reported the target was not found (-ENOENT):
286 /// the op had already completed.
287 pub cancel_not_found: u64,
288 /// ASYNC_CANCEL CQEs that reported the target was already executing and
289 /// could not be interrupted (-EALREADY). A rising count is the hung-disk
290 /// signal that makes drain-to-zero non-terminating (C4,
291 /// rustfs/backlog#1055).
292 pub cancel_already: u64,
293 /// Kernel CQ-ring overflow counter. With NODROP (asserted at probe) overflow
294 /// CQEs are buffered in the kernel overflow list and flushed on the next
295 /// enter, NOT lost — so a non-zero value is a backpressure warning, not fatal
296 /// loss (C5, rustfs/backlog#1056, #1167). In-flight is capped at `entries`
297 /// and cancels are deduped, keeping completions <= 2*entries, so it should
298 /// stay 0 in practice.
299 pub cq_overflow: u64,
300 /// `ring.submit()` calls that returned a non-transient error. A rising count
301 /// means `io_uring_enter` is persistently failing (e.g. a seccomp/LSM policy
302 /// applied after startup); the driver shuts the shard down after a bounded
303 /// run of consecutive failures so callers fall back instead of stalling
304 /// (rustfs/backlog#1162).
305 pub submit_errors: u64,
306}
307
308enum Msg {
309 Read {
310 id: u64,
311 file: Arc<File>,
312 offset: u64,
313 len: usize,
314 done: oneshot::Sender<io::Result<Vec<u8>>>,
315 /// Backpressure permit, acquired before the op reaches the driver and
316 /// released only when the pending entry is dropped at the final CQE
317 /// (rustfs/backlog#1060/#1102). If the driver rejects the op (shutting
318 /// down) the permit is dropped with the message — released immediately.
319 permit: OwnedSemaphorePermit,
320 /// Block size the read must be aligned to. `1` means a normal buffered
321 /// read; `> 1` means the file was opened `O_DIRECT` and the driver must
322 /// read the block-aligned superset range into a block-aligned buffer
323 /// (rustfs/backlog#1102).
324 align: usize,
325 },
326 Cancel {
327 id: u64,
328 },
329 Shutdown,
330 /// Test-only fault injection (rustfs/backlog#1103): unwind the driver thread
331 /// with ops in flight so the `DriverState::Drop` abort barrier (C2/#1054) is
332 /// exercised. Never present in a default build.
333 #[cfg(feature = "fault-injection")]
334 TestPanic,
335}
336
337/// One in-flight LOGICAL read. This struct — not the caller — owns everything
338/// the kernel touches:
339///
340/// - `buf`: the destination buffer. Its heap allocation must stay put until
341/// the final CQE; the `Vec` itself may move (HashMap rehash) since that
342/// never relocates the heap block. It is never resized or dropped before
343/// the CQE handler removes this entry.
344/// - `file`: keeps the fd open even if every caller-side clone is dropped, and
345/// supplies the fd for short-read resubmission. Without it, dropping the
346/// future could close the fd while an SQE built from that fd still sits in
347/// the backlog (SQE construction → io_uring_enter window), and a recycled
348/// fd number would make the kernel read the WRONG file (spike finding, with
349/// the corrected mechanism per rustfs/backlog#1063).
350/// - `offset`/`nread`: track a short-read resubmit loop (C9,
351/// rustfs/backlog#1058). io_uring may legally short-read a regular file;
352/// the driver resubmits the remainder into `buf[nread..]` until the request
353/// is fully satisfied or a real EOF (res == 0) is seen, so reclamation
354/// happens only at the FINAL CQE of the logical read.
355/// - `_permit`: the backpressure permit. Holding it here makes the
356/// "release at the CQE, never at future drop" rule (rustfs/backlog#1060) a
357/// property of the type: the permit is dropped exactly when this entry is
358/// removed at the final CQE. A short-read resubmit keeps the entry, and thus
359/// the permit, so in-flight memory stays bounded.
360/// - Alignment geometry (rustfs/backlog#1102). For a buffered read these are
361/// `pad = head = 0`, `align = 1`, `region_len = want`, so every rule below
362/// collapses to the plain case. For an `O_DIRECT` read the driver reads the
363/// block-aligned superset `[offset, offset + region_len)` into
364/// `buf[pad .. pad + region_len]` (both block-aligned) and hands the caller
365/// only `buf[pad + head .. pad + head + want]` — alignment padding never
366/// escapes.
367struct Pending {
368 buf: Vec<u8>,
369 file: Arc<File>,
370 done: Option<oneshot::Sender<io::Result<Vec<u8>>>>,
371 /// Kernel read offset: the block-aligned offset for a direct read, the
372 /// logical offset for a buffered one, `CURRENT_POSITION` for a stream.
373 offset: u64,
374 /// Bytes already read into the read region (`buf[pad..]`).
375 nread: usize,
376 _permit: OwnedSemaphorePermit,
377 /// Offset inside `buf` where the block-aligned read region starts.
378 pad: usize,
379 /// Bytes of the read region that precede the caller's logical range.
380 head: usize,
381 /// Logical length the caller asked for.
382 want: usize,
383 /// Bytes the kernel is asked to read (block-aligned for a direct read).
384 region_len: usize,
385 /// `1` for buffered, the block size for `O_DIRECT`.
386 align: usize,
387 /// Consecutive transient-errno (EINTR/EAGAIN) retries since the last byte of
388 /// progress, bounded by `MAX_TRANSIENT_RETRIES` so a storm cannot spin the
389 /// driver thread (rustfs/backlog#1166). Reset whenever a read makes progress.
390 transient_retries: u32,
391}
392
393impl Pending {
394 /// Build the read SQE for the not-yet-read remainder
395 /// `[pad + nread, pad + region_len)` at file offset `offset + nread`. This is
396 /// the single place a read SQE is constructed: the initial submit calls it
397 /// with `nread == 0` (the whole region), and a short-read or transient-errno
398 /// resubmit calls it after `nread` has advanced (rustfs/backlog#1058/#1166).
399 /// For an `O_DIRECT` read `pad + nread`, `offset + nread`, and the remaining
400 /// length are all block-aligned.
401 fn read_sqe(&self, ud: u64) -> io_uring::squeue::Entry {
402 let remaining = self.region_len - self.nread;
403 // SAFETY: `pad + nread < pad + region_len <= buf.len()`, and the buffer
404 // lives in the pending table until the CQE, so the kernel may write here.
405 // The read region is exclusively owned by this entry (no live aliases),
406 // so deriving a `*mut` from the shared `as_ptr` is sound.
407 let ptr = unsafe { self.buf.as_ptr().add(self.pad + self.nread).cast_mut() };
408 let next_off = self.offset + self.nread as u64;
409 opcode::Read::new(types::Fd(self.file.as_raw_fd()), ptr, remaining as u32)
410 .offset(next_off)
411 .build()
412 .user_data(ud)
413 }
414}
415
416/// Where a [`ReadHandle`] is in its lifecycle (rustfs/backlog#1102).
417enum HandleState {
418 /// Nothing was ever handed to the driver (a rejected parameter, or the
419 /// driver was already gone). The result is already sitting in `rx`, and
420 /// there is no buffer, permit, or SQE to reclaim.
421 Inert,
422 /// Backpressure was saturated at `submit` time, so the permit — and with it
423 /// the submission — is deferred to the first poll. The caller's thread is
424 /// never blocked. Dropping the handle in this state submitted nothing.
425 WaitingPermit {
426 acquire: AcquireFut,
427 file: Arc<File>,
428 offset: u64,
429 len: usize,
430 align: usize,
431 done: oneshot::Sender<io::Result<Vec<u8>>>,
432 wake: Arc<EventFd>,
433 },
434 /// The op is with the driver: its buffer lives in the pending table and is
435 /// reclaimed only at the CQE.
436 Submitted {
437 /// The accepting shard's wakeup eventfd, so a cancel sent on drop wakes
438 /// the driver loop now instead of after the heartbeat
439 /// (rustfs/backlog#1163).
440 wake: Arc<EventFd>,
441 },
442}
443
444/// Handle to a read. Await it for the result.
445///
446/// Dropping it before completion abandons the result only; if the op was
447/// already submitted it also sends `IORING_OP_ASYNC_CANCEL` (best effort) so the
448/// CQE — and with it the buffer reclamation — arrives sooner.
449/// `without_cancel_on_drop` disables that to model the bare "quorum drops the
450/// future" case.
451///
452/// Submission is eager whenever a backpressure permit is immediately available
453/// (the common case, unchanged from the blocking implementation). Only when the
454/// semaphore is saturated does the handle acquire the permit and submit on its
455/// first poll, so `submit` never blocks a runtime worker.
456pub struct ReadHandle {
457 id: u64,
458 rx: oneshot::Receiver<io::Result<Vec<u8>>>,
459 tx: mpsc::Sender<Msg>,
460 finished: bool,
461 cancel_on_drop: bool,
462 state: HandleState,
463}
464
465impl ReadHandle {
466 pub fn without_cancel_on_drop(mut self) -> Self {
467 self.cancel_on_drop = false;
468 self
469 }
470}
471
472impl Future for ReadHandle {
473 type Output = io::Result<Vec<u8>>;
474
475 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
476 let this = &mut *self;
477
478 // Saturated at submit time: take the permit, then hand the op to the
479 // driver. The permit rides along in the message and is released only
480 // when the pending entry is dropped at the CQE.
481 let acquired = match &mut this.state {
482 HandleState::WaitingPermit { acquire, .. } => match acquire.as_mut().poll(cx) {
483 Poll::Pending => return Poll::Pending,
484 Poll::Ready(res) => Some(res),
485 },
486 _ => None,
487 };
488 if let Some(res) = acquired {
489 let Ok(permit) = res else {
490 // The semaphore was closed: the driver is gone.
491 this.finished = true;
492 return Poll::Ready(Err(io::Error::other("uring driver shut down")));
493 };
494 // Clone the wake before moving the WaitingPermit out, so the new
495 // Submitted state carries it for the drop-cancel path
496 // (rustfs/backlog#1163).
497 let submitted_wake = match &this.state {
498 HandleState::WaitingPermit { wake, .. } => Arc::clone(wake),
499 _ => unreachable!("state was WaitingPermit"),
500 };
501 let HandleState::WaitingPermit {
502 file,
503 offset,
504 len,
505 align,
506 done,
507 wake,
508 ..
509 } = std::mem::replace(&mut this.state, HandleState::Submitted { wake: submitted_wake })
510 else {
511 unreachable!("state was WaitingPermit")
512 };
513 if this
514 .tx
515 .send(Msg::Read {
516 id: this.id,
517 file,
518 offset,
519 len,
520 done,
521 permit,
522 align,
523 })
524 .is_err()
525 {
526 // Driver gone between the acquire and the send; the message
527 // (with its permit) is dropped, releasing it.
528 this.finished = true;
529 return Poll::Ready(Err(io::Error::other("uring driver shut down")));
530 }
531 wake.signal();
532 }
533
534 match Pin::new(&mut this.rx).poll(cx) {
535 Poll::Ready(res) => {
536 this.finished = true;
537 Poll::Ready(match res {
538 Ok(inner) => inner,
539 Err(_) => Err(io::Error::other("uring driver shut down before completion")),
540 })
541 }
542 Poll::Pending => Poll::Pending,
543 }
544 }
545}
546
547impl Drop for ReadHandle {
548 fn drop(&mut self) {
549 // The buffer is deliberately NOT touched here: the driver owns it
550 // until the CQE. All we may do is ask the kernel to hurry up. A handle
551 // dropped before it was submitted (Inert / WaitingPermit) has no buffer,
552 // no permit and no SQE, so there is nothing to cancel.
553 if let HandleState::Submitted { wake } = &self.state
554 && !self.finished
555 && self.cancel_on_drop
556 {
557 // Close the receiver BEFORE waking the driver. The wake below
558 // makes the driver process the cancel immediately, possibly while
559 // this drop is still running — before the `rx` field is
560 // destroyed. Closing it first guarantees the cancel-induced
561 // completion the driver reaps is counted as an orphan reclaim, not
562 // delivered to a receiver that is about to drop anyway
563 // (rustfs/backlog#1163).
564 self.rx.close();
565 let _ = self.tx.send(Msg::Cancel { id: self.id });
566 // Wake the loop so the cancel is queued now, not after the
567 // heartbeat. On an idle ring (the hung-disk case cancel-on-drop
568 // exists for) this keeps orphan reclamation prompt.
569 wake.signal();
570 }
571 }
572}
573
574/// Process-level io_uring driver: one ring, one driver thread.
575/// One io_uring ring plus the thread that drives it.
576///
577/// Every cancel-safety invariant holds *per shard*, exactly as it did when a
578/// driver owned a single ring: this shard's pending table owns its buffers and
579/// fds until their CQEs, its permits are released only when a pending entry is
580/// dropped, and its bounded drain is what shutdown joins on. A `ReadHandle`
581/// carries the `tx` and `wake` of the shard that accepted it, so a cancel or a
582/// deferred submission always routes back to that same shard.
583struct Shard {
584 tx: mpsc::Sender<Msg>,
585 handle: Option<JoinHandle<()>>,
586 stats: Arc<DriverStats>,
587 /// Backpressure permits (one per allowed in-flight op on this ring). Closed
588 /// when the driver thread exits so any waiting `ReadHandle` resolves with a
589 /// driver-gone error instead of hanging (rustfs/backlog#1102).
590 sem: Arc<Semaphore>,
591 /// Signaled after every message send so this shard's loop wakes immediately
592 /// instead of waiting out the heartbeat (backlog#1102).
593 wake_efd: Arc<EventFd>,
594}
595
596impl Shard {
597 /// Ask the shard's thread to drain and exit, then join it. Idempotent: the
598 /// `JoinHandle` is taken, so a later `Drop` is a no-op.
599 fn join(&mut self) {
600 if let Some(h) = self.handle.take() {
601 let _ = self.tx.send(Msg::Shutdown);
602 self.wake_efd.signal();
603 let _ = h.join();
604 }
605 }
606}
607
608impl Drop for Shard {
609 fn drop(&mut self) {
610 self.join();
611 }
612}
613
614pub struct UringDriver {
615 /// One or more independent rings. A cache-hit buffered read completes inline
616 /// inside `io_uring_enter`, so the thread driving a ring performs that
617 /// read's memcpy — which caps a single-ring driver at one core's memory
618 /// bandwidth (~5 GB/s measured, rustfs/backlog#1145). Sharding lifts that
619 /// ceiling roughly linearly while keeping the ring set per-disk, so a stalled
620 /// disk still cannot starve another disk's rings (rustfs/backlog#1055).
621 shards: Vec<Shard>,
622 next_id: AtomicU64,
623 /// Round-robin cursor for shard selection. Relaxed: it only has to spread
624 /// ops, never to order them.
625 rr: AtomicUsize,
626}
627
628impl UringDriver {
629 /// Create the ring AND verify a real `IORING_OP_READ` round-trip on a
630 /// temp file before accepting work. `io_uring_setup` succeeding is not
631 /// enough: gVisor/seccomp environments can create a ring whose ops then
632 /// fail with ENOSYS/EINVAL (backlog#894 probe design).
633 /// Start a single-ring driver. Identical to `probe_and_start_sharded(entries, 1)`.
634 pub fn probe_and_start(entries: u32) -> Result<Self, ProbeFailure> {
635 Self::probe_and_start_sharded(entries, 1)
636 }
637
638 /// Start a driver backed by `shards` independent rings, each with `entries`
639 /// SQ slots and its own driver thread.
640 ///
641 /// Use more than one shard when the workload hits the page cache: such reads
642 /// complete inline in `io_uring_enter`, so a single driver thread performs
643 /// every one of their memcpys and caps the driver at one core's memory
644 /// bandwidth. Measured on a 16-core host (rustfs/backlog#1145): 1 ring →
645 /// 4890 MB/s, 2 → 8969 MB/s, 4 → 15806 MB/s, with per-ring throughput flat.
646 /// Reads that miss the cache are device-bound and do not need sharding.
647 ///
648 /// In-flight ops are capped at `entries` *per shard* (the invariant that
649 /// makes CQ overflow structurally unreachable holds per ring), so the whole
650 /// driver admits up to `shards * entries` concurrent reads.
651 ///
652 /// `shards` is clamped to at least 1. Probing happens on the first shard, so
653 /// a restricted environment fails exactly as it does for a single ring; if a
654 /// later shard fails to start, the ones already running are shut down and
655 /// joined before the error is returned.
656 pub fn probe_and_start_sharded(entries: u32, shards: usize) -> Result<Self, ProbeFailure> {
657 let mut started = Vec::with_capacity(shards.max(1));
658 for i in 0..shards.max(1) {
659 // Probe only the first shard (rustfs/backlog#1165): the probe read
660 // exercises io_uring against the environment-global temp_dir, so one
661 // confirmation is representative. Shards 2..n only create a ring and
662 // verify NODROP — this avoids `shards - 1` extra O_TMPFILE
663 // create+write+read round-trips per disk on every start and renew.
664 // `?` drops `started`, whose `Shard::drop` joins each running thread.
665 started.push(Self::start_shard(entries, i == 0)?);
666 }
667 Ok(Self {
668 shards: started,
669 next_id: AtomicU64::new(1),
670 rr: AtomicUsize::new(0),
671 })
672 }
673
674 /// Pick the shard for the next op. Round-robin spreads the inline-completion
675 /// memcpy across driver threads; correctness does not depend on the choice,
676 /// because the handle remembers which shard took the op.
677 fn shard(&self) -> &Shard {
678 let n = self.shards.len();
679 &self.shards[self.rr.fetch_add(1, Ordering::Relaxed) % n]
680 }
681
682 fn start_shard(entries: u32, probe: bool) -> Result<Shard, ProbeFailure> {
683 let mut ring = IoUring::new(entries).map_err(ProbeFailure::Setup)?;
684 // Require the NODROP feature (kernel >= 5.5). Without it, CQ overflow
685 // silently drops CQEs, stranding pending entries forever and hanging
686 // shutdown (C5, rustfs/backlog#1056). ENOSYS is in the expected-
687 // restriction class, so this degrades to the std backend cleanly.
688 if !ring.params().is_feature_nodrop() {
689 return Err(ProbeFailure::Setup(io::Error::from_raw_os_error(libc::ENOSYS)));
690 }
691 // Only the first shard runs the real-read probe (rustfs/backlog#1165); the
692 // rest still create a ring and check NODROP above, which is what makes
693 // io_uring usable, but skip the redundant temp_dir round-trip.
694 if probe {
695 probe_real_read(&mut ring).map_err(ProbeFailure::ReadOp)?;
696 }
697
698 // Wake the driver loop on CQEs (kernel-signaled via a registered
699 // eventfd) and on new messages (submit-signaled), replacing the 200 µs
700 // busy-poll (backlog#1102). Registration needs the ring, which the
701 // driver thread then owns; `cq_efd` is moved in alongside so it outlives
702 // the ring (dropped after it, unregistering cleanly).
703 let cq_efd = EventFd::new().map_err(ProbeFailure::Setup)?;
704 ring.submitter()
705 .register_eventfd(cq_efd.as_raw())
706 .map_err(ProbeFailure::Setup)?;
707
708 // Cap the ring's io-wq bounded worker pool so a cold-read burst cannot
709 // materialize thousands of PF_IO_WORKER threads against the process's
710 // TasksMax/RLIMIT_NPROC (rustfs/backlog#1169). Best-effort: 0 leaves the
711 // unbounded pool unchanged, and a kernel without this op (< 5.15) keeps
712 // the default — neither is fatal to a working ring.
713 let mut iowq_max = [IOWQ_MAX_BOUNDED_WORKERS, 0u32];
714 let _ = ring.submitter().register_iowq_max_workers(&mut iowq_max);
715
716 let wake_efd = Arc::new(EventFd::new().map_err(ProbeFailure::Setup)?);
717 let thread_wake = Arc::clone(&wake_efd);
718
719 let (tx, rx) = mpsc::channel();
720 let stats = Arc::new(DriverStats::default());
721 let thread_stats = Arc::clone(&stats);
722 // Cap in-flight at the SQ depth (entries), which is < CQ capacity
723 // (2*entries), so CQ overflow is structurally unreachable (C5/C10).
724 let sem = Arc::new(Semaphore::new(entries as usize));
725 let thread_sem = Arc::clone(&sem);
726 // Deterministic spawn-failure seam (rustfs/backlog#1164): exercise the
727 // degrade-not-panic path without a real cgroup pids-limit. Never present
728 // in a default build.
729 #[cfg(feature = "fault-injection")]
730 if std::env::var_os("RUSTFS_URING_FAULT_SPAWN").is_some() {
731 return Err(ProbeFailure::Setup(io::Error::from_raw_os_error(libc::EAGAIN)));
732 }
733
734 // Thread creation fails with EAGAIN under a cgroup pids-limit or
735 // RLIMIT_NPROC — exactly the constrained environments the probe/degrade
736 // design exists for. Degrade to the std backend instead of panicking out
737 // of async disk init/reconnect (rustfs/backlog#1164). The spawn happens
738 // after the probe read already drained, so on failure `ring`/`cq_efd`
739 // (moved into the closure) drop cleanly with no SQE in flight.
740 let handle = std::thread::Builder::new()
741 .name("uring-spike-driver".into())
742 .spawn(move || drive(ring, rx, thread_stats, thread_sem, cq_efd, thread_wake))
743 .map_err(ProbeFailure::Setup)?;
744
745 Ok(Shard {
746 tx,
747 handle: Some(handle),
748 stats,
749 sem,
750 wake_efd,
751 })
752 }
753
754 /// Positioned read (pread semantics) — regular files, buffered.
755 pub fn read_at(&self, file: Arc<File>, offset: u64, len: usize) -> ReadHandle {
756 assert_ne!(offset, CURRENT_POSITION, "offset u64::MAX is reserved");
757 self.submit(file, offset, len, 1)
758 }
759
760 /// Read at the file's current position (read(2) semantics) — pipes.
761 pub fn read_current(&self, file: Arc<File>, len: usize) -> ReadHandle {
762 self.submit(file, CURRENT_POSITION, len, 1)
763 }
764
765 /// Positioned read from a file opened with `O_DIRECT` (rustfs/backlog#1102).
766 ///
767 /// `align` is the device's logical block size — a power of two, typically
768 /// 512 or 4096. `offset` and `len` are the caller's *logical* range and need
769 /// no alignment: the driver reads the block-aligned superset range into a
770 /// block-aligned buffer and returns exactly `[offset, offset + len)`.
771 /// Alignment padding never reaches the caller, so a `BitrotReader` expecting
772 /// an exact shard length never sees padded output.
773 ///
774 /// The caller must have opened `file` with `O_DIRECT`; otherwise this is
775 /// just a (correct but pointless) buffered read of the superset range.
776 pub fn read_at_direct(&self, file: Arc<File>, offset: u64, len: usize, align: usize) -> ReadHandle {
777 assert_ne!(offset, CURRENT_POSITION, "offset u64::MAX is reserved");
778 self.submit(file, offset, len, align)
779 }
780
781 fn submit(&self, file: Arc<File>, offset: u64, len: usize, align: usize) -> ReadHandle {
782 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
783 assert_eq!(id & CANCEL_BIT, 0, "op id overflowed into the cancel bit");
784 let (done, rx) = oneshot::channel();
785
786 // Bind the op to one shard for its whole life: the permit, the message,
787 // the wake, and any later cancel all go to this ring. The handle holds
788 // clones of that shard's `tx`/`wake_efd`, so nothing can route a cancel
789 // to a ring whose pending table does not hold the op. The rejection paths
790 // below return an `Inert` handle that never sends, but still need a `tx`.
791 let shard = self.shard();
792
793 // Reject an offset the kernel would answer with a runtime EINVAL that
794 // must NOT be mistaken for an environment restriction (C7,
795 // rustfs/backlog#1059). The kernel reads `off` as a signed loff_t, so
796 // offset > i64::MAX becomes a negative ki_pos → EINVAL. A caller
797 // offset-arithmetic bug has to surface as an error here, never as a
798 // permanent per-disk fallback. CURRENT_POSITION is the reserved
799 // read(2) sentinel and bypasses this check.
800 if offset != CURRENT_POSITION && offset > i64::MAX as u64 {
801 let _ = done.send(Err(io::Error::new(
802 io::ErrorKind::InvalidInput,
803 "offset exceeds i64::MAX (kernel loff_t is signed)",
804 )));
805 return ReadHandle {
806 id,
807 rx,
808 tx: shard.tx.clone(),
809 finished: false,
810 cancel_on_drop: false,
811 state: HandleState::Inert,
812 };
813 }
814
815 // Reject a length the kernel would short-read past MAX_RW_COUNT and
816 // that the SQE's u32 `len` field would silently truncate: len == 2^32
817 // becomes a 0-byte read the caller decodes as a false EOF (C6,
818 // rustfs/backlog#1057). Failing fast here also removes the caller-
819 // controlled `vec![0u8; len]` capacity-overflow panic that made the
820 // unwind-UAF (rustfs/backlog#1054) reachable. P2 must chunk instead.
821 if len > MAX_READ_LEN {
822 let _ = done.send(Err(io::Error::new(
823 io::ErrorKind::InvalidInput,
824 "read length exceeds MAX_RW_COUNT (2 GiB - 4 KiB); caller must chunk",
825 )));
826 return ReadHandle {
827 id,
828 rx,
829 tx: shard.tx.clone(),
830 finished: false,
831 cancel_on_drop: false,
832 state: HandleState::Inert,
833 };
834 }
835
836 // Reject a bad O_DIRECT alignment, a request whose block-aligned superset
837 // range would exceed the kernel's single-read cap, and one whose aligned
838 // END crosses i64::MAX — the kernel reads pos as a signed loff_t, so
839 // `kernel_offset + region_len > i64::MAX` fails at runtime with
840 // EINVAL/EOVERFLOW, exactly the errno class the C7 guard must pre-empt at
841 // submit (rustfs/backlog#1102, #1166). Pre-empting it here also makes
842 // every resubmit's `next_off < kernel_offset + region_len` provably
843 // <= i64::MAX. `align == 1` (buffered) always passes the alignment part.
844 match aligned_geometry(offset, len, align) {
845 // CURRENT_POSITION (stream) reads use no positional offset — the
846 // kernel reads from the current file position — so the i64::MAX end
847 // check does not apply to them (their sentinel offset would overflow
848 // it). Exempt them exactly as the offset guard above does.
849 Some((kernel_offset, _, region_len))
850 if region_len <= MAX_READ_LEN
851 && (offset == CURRENT_POSITION
852 || kernel_offset
853 .checked_add(region_len as u64)
854 .is_some_and(|end| end <= i64::MAX as u64)) => {}
855 _ => {
856 let _ = done.send(Err(io::Error::new(
857 io::ErrorKind::InvalidInput,
858 "alignment must be a power of two, and the block-aligned range must fit MAX_RW_COUNT and end within i64::MAX",
859 )));
860 return ReadHandle {
861 id,
862 rx,
863 tx: shard.tx.clone(),
864 finished: false,
865 cancel_on_drop: false,
866 state: HandleState::Inert,
867 };
868 }
869 }
870
871 // Take a backpressure permit BEFORE the op reaches the driver; it is
872 // released only when the pending entry is dropped at the CQE (C10,
873 // rustfs/backlog#1060). Acquisition never blocks the caller's thread
874 // (rustfs/backlog#1102).
875 match Arc::clone(&shard.sem).try_acquire_owned() {
876 // Fast path: a permit was free, so submit eagerly — no allocation,
877 // no await, and the op is in flight the moment `submit` returns,
878 // exactly as with the previous blocking implementation.
879 Ok(permit) => {
880 if let Err(mpsc::SendError(msg)) = shard.tx.send(Msg::Read {
881 id,
882 file,
883 offset,
884 len,
885 done,
886 permit,
887 align,
888 }) {
889 // Driver gone: the op never reached it. Surface an explicit
890 // driver-gone error through `done` instead of letting the
891 // caller infer one from the dropped oneshot, matching the
892 // `Closed` arm below. The permit rides back in `msg` and is
893 // released when it drops here.
894 if let Msg::Read { done, .. } = msg {
895 let _ = done.send(Err(io::Error::other("uring driver shut down")));
896 }
897 return ReadHandle {
898 id,
899 rx,
900 tx: shard.tx.clone(),
901 finished: false,
902 cancel_on_drop: false,
903 state: HandleState::Inert,
904 };
905 }
906 // Wake the driver loop so the read starts immediately.
907 shard.wake_efd.signal();
908 ReadHandle {
909 id,
910 rx,
911 tx: shard.tx.clone(),
912 finished: false,
913 cancel_on_drop: true,
914 state: HandleState::Submitted {
915 wake: Arc::clone(&shard.wake_efd),
916 },
917 }
918 }
919 // Saturated: `entries` ops are already in flight. Do NOT block the
920 // calling (runtime worker) thread — hand the acquire future to the
921 // handle, which awaits it on its first poll and submits then.
922 Err(TryAcquireError::NoPermits) => ReadHandle {
923 id,
924 rx,
925 tx: shard.tx.clone(),
926 finished: false,
927 cancel_on_drop: true,
928 state: HandleState::WaitingPermit {
929 acquire: Box::pin(Arc::clone(&shard.sem).acquire_owned()),
930 file,
931 offset,
932 len,
933 align,
934 done,
935 wake: Arc::clone(&shard.wake_efd),
936 },
937 },
938 // The driver has exited and closed the semaphore.
939 Err(TryAcquireError::Closed) => {
940 let _ = done.send(Err(io::Error::other("uring driver shut down")));
941 ReadHandle {
942 id,
943 rx,
944 tx: shard.tx.clone(),
945 finished: false,
946 cancel_on_drop: false,
947 state: HandleState::Inert,
948 }
949 }
950 }
951 }
952
953 /// Counters summed across every shard. The conservation identities the
954 /// cancel-safety tests assert (`submitted == delivered + orphan_reclaimed`,
955 /// `in_flight == 0` after a clean drain) hold per shard, so they hold for
956 /// the sum.
957 pub fn stats(&self) -> StatsSnapshot {
958 let mut snap = StatsSnapshot::default();
959 for shard in &self.shards {
960 let s = &shard.stats;
961 snap.submitted += s.submitted.load(Ordering::SeqCst);
962 snap.delivered += s.delivered.load(Ordering::SeqCst);
963 snap.orphan_reclaimed += s.orphan_reclaimed.load(Ordering::SeqCst);
964 snap.in_flight += s.in_flight.load(Ordering::SeqCst);
965 snap.cancel_succeeded += s.cancel_succeeded.load(Ordering::SeqCst);
966 snap.cancel_not_found += s.cancel_not_found.load(Ordering::SeqCst);
967 snap.cancel_already += s.cancel_already.load(Ordering::SeqCst);
968 snap.cq_overflow += s.cq_overflow.load(Ordering::SeqCst);
969 snap.submit_errors += s.submit_errors.load(Ordering::SeqCst);
970 }
971 snap
972 }
973
974 /// Test-only fault injection (rustfs/backlog#1103): poison one driver thread
975 /// so it panics with ops in flight, exercising the `DriverState::Drop` abort
976 /// barrier (C2/#1054). Compiled out entirely unless the `fault-injection`
977 /// feature is on — never in a default/production build.
978 #[cfg(feature = "fault-injection")]
979 pub fn test_inject_panic(&self) {
980 let shard = self.shard();
981 let _ = shard.tx.send(Msg::TestPanic);
982 shard.wake_efd.signal();
983 }
984
985 /// Stop accepting work, cancel all in-flight ops, drain every ring to
986 /// `in_flight == 0`, then join each driver thread. Only after that is a ring
987 /// dropped/unmapped — the shutdown ordering P2 requires, per shard.
988 ///
989 /// Shards are asked to stop first and joined afterwards, so their bounded
990 /// drains overlap instead of serializing `shards * DRAIN_TIMEOUT`.
991 pub fn shutdown(mut self) -> StatsSnapshot {
992 for shard in &self.shards {
993 let _ = shard.tx.send(Msg::Shutdown);
994 shard.wake_efd.signal();
995 }
996 for shard in &mut self.shards {
997 shard.join();
998 }
999 let snap = self.stats();
1000 // A clean drain leaves in_flight == 0. A non-zero count here means some
1001 // shard's bounded drain bailed out on a hung device and leaked its
1002 // ring+buffers to stay memory-safe (C4, rustfs/backlog#1055) — a degraded
1003 // but safe outcome, not a panic. Callers/tests that require a clean drain
1004 // assert on the returned snapshot themselves.
1005 if snap.in_flight != 0 {
1006 tracing::warn!(
1007 in_flight = snap.in_flight,
1008 "uring shutdown: ops still in flight (bounded-drain bailout on a hung device)"
1009 );
1010 }
1011 snap
1012 }
1013}
1014
1015impl Drop for UringDriver {
1016 fn drop(&mut self) {
1017 // Ask every shard to stop before joining any of them, so their bounded
1018 // drains overlap. Dropping the `Vec<Shard>` would instead run each
1019 // `Shard::drop` in turn, serializing up to `shards * DRAIN_TIMEOUT` on a
1020 // hung device. `Shard::join` is idempotent, so the later drops are no-ops.
1021 for shard in &self.shards {
1022 let _ = shard.tx.send(Msg::Shutdown);
1023 shard.wake_efd.signal();
1024 }
1025 for shard in &mut self.shards {
1026 shard.join();
1027 }
1028 }
1029}
1030
1031fn probe_real_read(ring: &mut IoUring) -> io::Result<()> {
1032 let pattern: Vec<u8> = (0..512u32).map(|i| (i * 7 + 13) as u8).collect();
1033
1034 // Open an anonymous probe file seeded with the pattern. File setup runs
1035 // BEFORE any SQE, so its errors early-return safely — nothing is in flight.
1036 let file = open_probe_file(&pattern)?;
1037
1038 let mut buf = vec![0u8; pattern.len()];
1039 let sqe = opcode::Read::new(types::Fd(file.as_raw_fd()), buf.as_mut_ptr(), buf.len() as u32)
1040 .offset(0)
1041 .build()
1042 .user_data(0xB0BE);
1043
1044 // SAFETY: a push failure means the kernel never accepted the SQE, so
1045 // `buf`/`file` may be dropped safely on this early return.
1046 if unsafe { ring.submission().push(&sqe) }.is_err() {
1047 return Err(io::Error::other("probe: submission queue full"));
1048 }
1049
1050 // C1 (rustfs/backlog#1053): once the SQE is handed to the kernel, the read
1051 // may be punted to io-wq and write into `buf` at ANY later point. Until its
1052 // CQE arrives, `buf`/`file` must NOT be dropped and the ring must NOT be
1053 // unmapped — otherwise the kernel writes into freed memory (UAF). The probe
1054 // path has no pending-table backstop, so we must drain to the CQE here, and
1055 // any early exit first leaks the buffer ("leak over UAF").
1056 let res = match drain_probe_cqe(ring) {
1057 Ok(res) => res,
1058 Err(e) => {
1059 // Could not confirm the op terminated: leak `buf` (the real UAF
1060 // hazard — the kernel may still write 512 bytes into it) and,
1061 // defensively, `file`. Leaking one 512-byte startup-probe buffer is
1062 // trivially cheaper than a silent heap corruption.
1063 std::mem::forget(buf);
1064 std::mem::forget(file);
1065 return Err(e);
1066 }
1067 };
1068
1069 // The CQE has arrived: the kernel is done with `buf`, so dropping it and
1070 // `file` below is now safe.
1071 if res < 0 {
1072 Err(io::Error::from_raw_os_error(-res))
1073 } else if res as usize != pattern.len() || buf != pattern {
1074 Err(io::Error::other("probe: read completed but data mismatched"))
1075 } else {
1076 Ok(())
1077 }
1078}
1079
1080/// Open a probe file seeded with `pattern`, avoiding the symlink/TOCTOU/
1081/// leftover hazards of a predictable temp path (C3, rustfs/backlog#1061).
1082///
1083/// Primary: `O_TMPFILE` — an anonymous inode with no name at all, so there is
1084/// nothing for an attacker to pre-plant a symlink at, no TOCTOU window, and no
1085/// leftover file. Fallback (filesystems without O_TMPFILE): create in the temp
1086/// dir with `O_CREAT|O_EXCL|O_NOFOLLOW` + 0600 + a per-process nonce, then
1087/// unlink immediately so no attacker-planted symlink is followed and no named
1088/// file survives.
1089fn open_probe_file(pattern: &[u8]) -> io::Result<File> {
1090 let dir = std::env::temp_dir();
1091 let c_dir = std::ffi::CString::new(dir.as_os_str().as_bytes()).map_err(|_| io::Error::other("probe dir path has NUL"))?;
1092 // SAFETY: `c_dir` is a valid NUL-terminated path; O_TMPFILE requires a
1093 // directory and O_RDWR/O_WRONLY. On success we own the returned fd.
1094 let fd = unsafe { libc::open(c_dir.as_ptr(), libc::O_TMPFILE | libc::O_RDWR | libc::O_CLOEXEC, 0o600) };
1095 if fd >= 0 {
1096 let mut file = unsafe { File::from_raw_fd(fd) };
1097 file.write_all(pattern)?;
1098 return Ok(file);
1099 }
1100 open_probe_file_exclusive(&dir, pattern)
1101}
1102
1103fn open_probe_file_exclusive(dir: &std::path::Path, pattern: &[u8]) -> io::Result<File> {
1104 static SEQ: AtomicU64 = AtomicU64::new(0);
1105 let nonce = SEQ.fetch_add(1, Ordering::Relaxed);
1106 let path = dir.join(format!("uring-spike-probe-{}-{}", std::process::id(), nonce));
1107 let c_path = std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| io::Error::other("probe path has NUL"))?;
1108 // O_EXCL refuses a pre-existing file; O_NOFOLLOW refuses a symlink; 0600 is
1109 // owner-only. SAFETY: `c_path` is a valid NUL-terminated path; on success
1110 // we own the fd.
1111 let fd = unsafe {
1112 libc::open(
1113 c_path.as_ptr(),
1114 libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_RDWR | libc::O_CLOEXEC,
1115 0o600,
1116 )
1117 };
1118 if fd < 0 {
1119 return Err(io::Error::last_os_error());
1120 }
1121 let mut file = unsafe { File::from_raw_fd(fd) };
1122 file.write_all(pattern)?;
1123 // Unlink now: the fd stays valid, no named leftover remains.
1124 // SAFETY: `c_path` is still a valid NUL-terminated path.
1125 unsafe {
1126 libc::unlink(c_path.as_ptr());
1127 }
1128 Ok(file)
1129}
1130
1131/// Wait for the probe SQE's CQE and return its raw result.
1132///
1133/// The SQE has already been pushed; this only drains it. `submit_and_wait`
1134/// interrupted by a signal returns EINTR — since the kernel consumed the SQE
1135/// atomically before the wait phase, we retry the WAIT only and never re-push
1136/// (C8, backlog#1059). A bounded attempt count keeps a probe that hit a hung
1137/// device from blocking forever; exhausting it returns an error that drives
1138/// the caller's leak-over-UAF fallback.
1139fn drain_probe_cqe(ring: &mut IoUring) -> io::Result<i32> {
1140 // Bound the wait by WALL-CLOCK, not by an attempt count. `submit_and_wait(1)`
1141 // parks in the kernel's io_cqring_wait until a CQE or a signal, so a single
1142 // call can block forever when the probe read never completes — e.g. a
1143 // temp_dir backed by a hung/D-state or NFS device. Since this runs on the
1144 // caller's (async disk-init) thread, an unbounded block hangs startup. On
1145 // kernels with EXT_ARG (>= 5.11) pass a timeout to the enter; on older
1146 // kernels fall back to the blocking wait, whose only real risk is a hung
1147 // temp_dir (rare) and which the deadline still re-checks between returns
1148 // (rustfs/backlog#1165). On expiry, error out so the caller's leak-over-UAF
1149 // fallback degrades the disk to the std backend instead of hanging.
1150 const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
1151 let deadline = Instant::now() + PROBE_TIMEOUT;
1152 let ext_arg = ring.params().is_feature_ext_arg();
1153 loop {
1154 let remaining = deadline.saturating_duration_since(Instant::now());
1155 if remaining.is_zero() {
1156 return Err(io::Error::other("probe: no CQE within the bounded wait"));
1157 }
1158 let waited = if ext_arg {
1159 let ts = types::Timespec::new().sec(remaining.as_secs()).nsec(remaining.subsec_nanos());
1160 let args = types::SubmitArgs::new().timespec(&ts);
1161 ring.submitter().submit_with_args(1, &args)
1162 } else {
1163 ring.submit_and_wait(1)
1164 };
1165 match waited {
1166 Ok(_) => {}
1167 // Signal interrupted the wait; the SQE is already in flight, so wait
1168 // again (do NOT re-push). The deadline still bounds the total time.
1169 Err(e) if e.raw_os_error() == Some(libc::EINTR) => {}
1170 // EXT_ARG timeout elapsed with no CQE: loop to re-check the deadline.
1171 Err(e) if e.raw_os_error() == Some(libc::ETIME) => {}
1172 Err(e) => return Err(e),
1173 }
1174 if let Some(cqe) = ring.completion().next() {
1175 // fault-injection (backlog#1103 → C1/#1053): the real CQE has arrived,
1176 // so the kernel is finished with the probe buffer. Forcing the error
1177 // path here exercises probe_real_read's leak-over-UAF fallback with no
1178 // live in-flight write to race.
1179 #[cfg(feature = "fault-injection")]
1180 if std::env::var_os("RUSTFS_URING_FAULT_PROBE_DRAIN").is_some() {
1181 return Err(io::Error::other("fault-injection: forced probe drain failure"));
1182 }
1183 return Ok(cqe.result());
1184 }
1185 }
1186}
1187
1188/// Owns everything the kernel can still be writing into: the ring, the
1189/// pending (orphan) table of in-flight buffers, and the SQE backlog.
1190///
1191/// C2 (rustfs/backlog#1054): the "CQE is the only reclamation point"
1192/// invariant holds only while the driver thread does NOT unwind. On a panic,
1193/// Rust would drop the pending table (freeing every in-flight buffer) while
1194/// the kernel may still write into them → mass UAF; reversing drop order does
1195/// not help because io_uring teardown on ring drop is asynchronous and does
1196/// not wait for in-flight ops. So this type's `Drop` refuses to run field
1197/// destructors during an unwind: it aborts the process first, leaving the
1198/// ring mapped and the buffers allocated (leak over UAF). A storage read path
1199/// silently corrupting memory is worse than a crash.
1200struct DriverState {
1201 ring: IoUring,
1202 pending: HashMap<u64, Pending>,
1203 backlog: VecDeque<io_uring::squeue::Entry>,
1204}
1205
1206impl Drop for DriverState {
1207 fn drop(&mut self) {
1208 if std::thread::panicking() {
1209 // Abort BEFORE any field destructor runs: the ring stays mapped
1210 // and the in-flight buffers stay allocated, so the kernel can
1211 // never write into freed memory.
1212 eprintln!(
1213 "uring-spike driver thread panicked with {} ops in flight; \
1214 aborting to avoid UAF of in-flight buffers",
1215 self.pending.len()
1216 );
1217 std::process::abort();
1218 }
1219 // Normal drop: the shutdown invariant guarantees pending/backlog are
1220 // empty and in_flight == 0, so unmapping the ring here is safe.
1221 }
1222}
1223
1224/// Best-effort file length via `fstat` on the driver thread, used to tell a
1225/// genuine O_DIRECT tail short read from a non-block-multiple short read that
1226/// happened mid-file on a stacked filesystem (rustfs/backlog#1168). `None` when
1227/// the stat fails, in which case the caller keeps the conservative EOF
1228/// assumption rather than risk a wrong error or an unbounded resubmit loop.
1229fn file_len(file: &File) -> Option<u64> {
1230 file.metadata().ok().map(|m| m.len())
1231}
1232
1233/// Hand the caller exactly the logical range `[head, head + want)` of the read
1234/// region, truncated to what was actually read (rustfs/backlog#1102).
1235///
1236/// Alignment padding (`buf[..pad]`), the bytes before the logical range
1237/// (`head`), and the block-aligned tail after it never reach the caller — a
1238/// `BitrotReader` expecting an exact shard length would flag padded output as
1239/// corruption. Only bytes the kernel actually wrote are exposed: `avail` is
1240/// clamped to `nread`, so the zero-filled remainder of the buffer stays hidden
1241/// (content hygiene, C12 / rustfs/backlog#1062).
1242fn deliver(p: &mut Pending) -> Vec<u8> {
1243 let avail = p.nread.saturating_sub(p.head).min(p.want);
1244 let start = p.pad + p.head;
1245 // The buffered path (`align == 1`) has `pad == 0` and `head == 0`, so the
1246 // logical range already starts at byte 0 — skip the full-buffer memmove and
1247 // just truncate. Only the O_DIRECT path (nonzero start) needs the shift.
1248 if start != 0 && avail != 0 {
1249 p.buf.copy_within(start..start + avail, 0);
1250 }
1251 p.buf.truncate(avail);
1252 std::mem::take(&mut p.buf)
1253}
1254
1255/// What to do with a pending entry after its CQE (C9, rustfs/backlog#1058).
1256enum ReapStep {
1257 /// The logical read is done: remove the entry and deliver this result.
1258 Finish(io::Result<Vec<u8>>),
1259 /// Short read, not EOF: re-queue this SQE for the remainder; keep the entry.
1260 Resubmit(io_uring::squeue::Entry),
1261}
1262
1263/// Queue at most one `AsyncCancel` per op (rustfs/backlog#1167): a drop-cancel
1264/// followed by a shutdown, or the submit-error shutdown, must not enqueue a
1265/// second cancel for the same id. The set is bounded by the pending table
1266/// because ids are monotonic and an entry is removed when its op is reaped.
1267fn queue_cancel(backlog: &mut VecDeque<io_uring::squeue::Entry>, queued_cancels: &mut HashSet<u64>, id: u64) {
1268 if queued_cancels.insert(id) {
1269 backlog.push_back(opcode::AsyncCancel::new(id).build().user_data(id | CANCEL_BIT));
1270 }
1271}
1272
1273/// Push as much of the backlog into the SQ as fits, stopping when the ring is
1274/// full (the remainder retries next turn).
1275fn flush_backlog(ring: &mut IoUring, backlog: &mut VecDeque<io_uring::squeue::Entry>) {
1276 let mut sq = ring.submission();
1277 while let Some(sqe) = backlog.pop_front() {
1278 // SAFETY: read SQEs point into `pending`-owned buffers that live until
1279 // their CQE; cancel SQEs carry no pointers.
1280 if unsafe { sq.push(&sqe) }.is_err() {
1281 backlog.push_front(sqe);
1282 break;
1283 }
1284 }
1285}
1286
1287/// Flush the backlog into the SQ and submit it, with submit-error classification
1288/// (rustfs/backlog#1162). The single submit path for the whole loop: called once
1289/// after intake and once more after reap when resubmits were queued. Skips the
1290/// `io_uring_enter` syscall on an empty SQ (rustfs/backlog#1169). EINTR/EBUSY are
1291/// transient; any other errno is counted and, after a bounded run, transitions
1292/// the shard to shutdown so callers fall back to the std backend.
1293fn submit_ring(
1294 state: &mut DriverState,
1295 stats: &DriverStats,
1296 consecutive_submit_errors: &mut u32,
1297 submit_error_logged: &mut bool,
1298 shutting_down: &mut bool,
1299 queued_cancels: &mut HashSet<u64>,
1300) {
1301 flush_backlog(&mut state.ring, &mut state.backlog);
1302 if state.ring.submission().is_empty() {
1303 return;
1304 }
1305 match state.ring.submit() {
1306 Ok(_) => *consecutive_submit_errors = 0,
1307 // CQ-overflow backpressure (EBUSY) and signal interruption (EINTR) are
1308 // transient — retry next turn without counting them (C5, backlog#1056).
1309 Err(e) if matches!(e.raw_os_error(), Some(libc::EBUSY) | Some(libc::EINTR)) => *consecutive_submit_errors = 0,
1310 Err(e) => {
1311 // The queued SQEs were not accepted, so their CQEs never arrive. A
1312 // brief run may be transient (EAGAIN); a persistent one (e.g. EPERM
1313 // from a seccomp/LSM policy applied after startup) must not be retried
1314 // forever in silence.
1315 stats.submit_errors.fetch_add(1, Ordering::SeqCst);
1316 *consecutive_submit_errors += 1;
1317 if !*submit_error_logged {
1318 *submit_error_logged = true;
1319 tracing::warn!(error = %e, "uring driver: ring.submit() failed; retrying, will shut down if persistent");
1320 }
1321 if !*shutting_down && *consecutive_submit_errors >= MAX_CONSECUTIVE_SUBMIT_ERRORS {
1322 tracing::warn!(
1323 consecutive_errors = *consecutive_submit_errors,
1324 "uring driver: consecutive submit failures; shutting down so callers fall back to the std backend"
1325 );
1326 *shutting_down = true;
1327 let ids: Vec<u64> = state.pending.keys().copied().collect();
1328 for id in ids {
1329 queue_cancel(&mut state.backlog, queued_cancels, id);
1330 }
1331 }
1332 }
1333 }
1334}
1335
1336fn drive(
1337 ring: IoUring,
1338 rx: mpsc::Receiver<Msg>,
1339 stats: Arc<DriverStats>,
1340 sem: Arc<Semaphore>,
1341 cq_efd: EventFd,
1342 wake_efd: Arc<EventFd>,
1343) {
1344 let mut state = DriverState {
1345 ring,
1346 pending: HashMap::new(),
1347 backlog: VecDeque::new(),
1348 };
1349 let mut shutting_down = false;
1350 let mut drain_deadline: Option<Instant> = None;
1351 // Consecutive non-transient submit failures, and a once-only log latch, for
1352 // the persistent-submit-failure escape hatch (rustfs/backlog#1162).
1353 let mut consecutive_submit_errors: u32 = 0;
1354 let mut submit_error_logged = false;
1355 // Ids with an AsyncCancel already queued, so a drop-cancel followed by a
1356 // shutdown (or vice versa) does not enqueue a second cancel for the same op —
1357 // keeping total completions <= 2*entries and CQ overflow unreachable
1358 // (rustfs/backlog#1167). Ids are monotonic, so an entry is removed only when
1359 // its pending op is reaped; the set stays bounded by the pending table.
1360 let mut queued_cancels: HashSet<u64> = HashSet::new();
1361
1362 // Bounded-drain deadline (C4, rustfs/backlog#1055). Production always uses the
1363 // fixed DRAIN_TIMEOUT; a fault-injection build may shorten it via env so the
1364 // leak-over-UAF escape hatch is testable without a 5 s wait (backlog#1103).
1365 // Read once here (not per turn) so a `--test-threads=1` env toggle in one
1366 // test never leaks into another's already-running driver thread.
1367 #[cfg(not(feature = "fault-injection"))]
1368 let drain_timeout = DRAIN_TIMEOUT;
1369 #[cfg(feature = "fault-injection")]
1370 let drain_timeout = std::env::var("RUSTFS_URING_FAULT_DRAIN_TIMEOUT_MS")
1371 .ok()
1372 .and_then(|ms| ms.parse().ok())
1373 .map(Duration::from_millis)
1374 .unwrap_or(DRAIN_TIMEOUT);
1375 // When set, drop an op's real completion on the floor so it stays pending and
1376 // the bounded drain is forced onto its timeout path (backlog#1103 → C4/#1055).
1377 #[cfg(feature = "fault-injection")]
1378 let fault_stuck_drain = std::env::var_os("RUSTFS_URING_FAULT_STUCK_DRAIN").is_some();
1379
1380 loop {
1381 // Block until a CQE is ready (the ring's registered eventfd), a new
1382 // message arrives (the wakeup eventfd), or the heartbeat elapses —
1383 // this replaces the spike's 200 µs busy-poll (backlog#1102). Draining
1384 // both eventfds after waking keeps them from staying spuriously
1385 // readable; a missed edge is harmless because the CQ/mpsc are re-checked
1386 // unconditionally below.
1387 // Adaptive heartbeat (rustfs/backlog#1169): poll at 50 ms only while
1388 // there is in-flight work to reap or a drain deadline to honor; when the
1389 // shard is fully idle, wait up to IDLE_HEARTBEAT. New work still wakes us
1390 // immediately via wake_efd and completions via cq_efd, so the longer idle
1391 // wait only cuts timer/syscall churn.
1392 let heartbeat = if shutting_down || !state.pending.is_empty() {
1393 LOOP_HEARTBEAT
1394 } else {
1395 IDLE_HEARTBEAT
1396 };
1397 wait_for_events(&cq_efd, &wake_efd, heartbeat);
1398 cq_efd.drain();
1399 wake_efd.drain();
1400
1401 // 1. Intake: drain all queued messages (the wait above did the blocking,
1402 // so this is purely non-blocking).
1403 loop {
1404 let msg = match rx.try_recv() {
1405 Ok(m) => m,
1406 Err(TryRecvError::Empty) => break,
1407 Err(TryRecvError::Disconnected) => {
1408 shutting_down = true;
1409 break;
1410 }
1411 };
1412 match msg {
1413 Msg::Read {
1414 id,
1415 file,
1416 offset,
1417 len,
1418 done,
1419 permit,
1420 align,
1421 } => {
1422 if shutting_down {
1423 let _ = done.send(Err(io::Error::other("uring driver shutting down")));
1424 // The op never became in-flight; dropping `permit` here
1425 // returns it immediately.
1426 drop(permit);
1427 continue;
1428 }
1429 // `submit` already validated this geometry.
1430 let (kernel_offset, head, region_len) =
1431 aligned_geometry(offset, len, align).expect("submit validated the geometry");
1432 // For an O_DIRECT read the kernel needs a block-aligned
1433 // buffer, so over-allocate by `align - 1` and start the read
1434 // region at the first aligned byte inside the allocation.
1435 // For a buffered read this degenerates to `vec![0u8; len]`.
1436 // `submit` already capped `align <= MAX_READ_LEN` and
1437 // `region_len <= MAX_READ_LEN`, so this add cannot overflow;
1438 // the checked form keeps the invariant explicit rather than
1439 // relying on it silently.
1440 let cap = match region_len.checked_add(align - 1) {
1441 Some(cap) => cap,
1442 None => {
1443 let _ = done.send(Err(io::Error::other("aligned O_DIRECT allocation size overflow")));
1444 drop(permit);
1445 continue;
1446 }
1447 };
1448 let buf = vec![0u8; cap];
1449 let pad = buf.as_ptr().align_offset(align);
1450 // Runtime guard (not a debug-only assert): if the allocator
1451 // ever returned a block `align_offset` cannot satisfy, refuse
1452 // the read instead of doing UB pointer arithmetic below.
1453 if pad == usize::MAX || pad.checked_add(region_len).is_none_or(|end| end > buf.len()) {
1454 let _ = done.send(Err(io::Error::other("could not align O_DIRECT read buffer")));
1455 drop(permit);
1456 continue;
1457 }
1458
1459 // Move the buffer into the pending table (which owns it until
1460 // the CQE), THEN build the SQE from the entry: the initial read
1461 // is `read_sqe` with `nread == 0`, so the read-region pointer
1462 // math and the `Read` builder live in exactly one place.
1463 // Moving the Vec never relocates its heap block, so the pointer
1464 // the SQE captures stays valid.
1465 state.pending.insert(
1466 id,
1467 Pending {
1468 buf,
1469 file,
1470 done: Some(done),
1471 offset: kernel_offset,
1472 nread: 0,
1473 // Released exactly when this entry is removed at the
1474 // final CQE — never at future drop (backlog#1060).
1475 _permit: permit,
1476 pad,
1477 head,
1478 want: len,
1479 region_len,
1480 align,
1481 transient_retries: 0,
1482 },
1483 );
1484 let sqe = state.pending.get(&id).expect("just inserted").read_sqe(id);
1485 stats.submitted.fetch_add(1, Ordering::SeqCst);
1486 stats.in_flight.fetch_add(1, Ordering::SeqCst);
1487 state.backlog.push_back(sqe);
1488 }
1489 Msg::Cancel { id } => {
1490 if state.pending.contains_key(&id) {
1491 queue_cancel(&mut state.backlog, &mut queued_cancels, id);
1492 }
1493 }
1494 Msg::Shutdown => {
1495 shutting_down = true;
1496 let ids: Vec<u64> = state.pending.keys().copied().collect();
1497 for id in ids {
1498 queue_cancel(&mut state.backlog, &mut queued_cancels, id);
1499 }
1500 }
1501 #[cfg(feature = "fault-injection")]
1502 Msg::TestPanic => {
1503 // Panic WITH buffers still in flight: the abort barrier in
1504 // `DriverState::Drop` must fire rather than let the unwind
1505 // free them under the kernel (rustfs/backlog#1103 → C2/#1054).
1506 panic!(
1507 "fault-injection: driver thread panic requested with {} ops in flight",
1508 state.pending.len()
1509 );
1510 }
1511 }
1512 }
1513
1514 // 2. Flush the backlog into the SQ and submit it (the single submit path;
1515 // see `submit_ring`).
1516 submit_ring(
1517 &mut state,
1518 &stats,
1519 &mut consecutive_submit_errors,
1520 &mut submit_error_logged,
1521 &mut shutting_down,
1522 &mut queued_cancels,
1523 );
1524
1525 // 3. Reap. A Pending entry (and thus its buffer) is dropped ONLY when
1526 // the logical read finishes; a short read is resubmitted for the
1527 // remainder and the entry stays put (C9, rustfs/backlog#1058).
1528 while let Some(cqe) = state.ring.completion().next() {
1529 let ud = cqe.user_data();
1530 if ud & CANCEL_BIT != 0 {
1531 // Result of the AsyncCancel op itself; the read's own CQE
1532 // (ECANCELED or success) still arrives separately. Record the
1533 // three-state outcome for diagnosability (C4,
1534 // rustfs/backlog#1055): EALREADY means the read is executing
1535 // and cannot be interrupted, i.e. its CQE may never come on a
1536 // hung device — the signal the bounded drain below relies on.
1537 match cqe.result() {
1538 0 => stats.cancel_succeeded.fetch_add(1, Ordering::SeqCst),
1539 r if r == -libc::ENOENT => stats.cancel_not_found.fetch_add(1, Ordering::SeqCst),
1540 r if r == -libc::EALREADY => stats.cancel_already.fetch_add(1, Ordering::SeqCst),
1541 _ => 0,
1542 };
1543 continue;
1544 }
1545 // fault-injection (backlog#1103 → C4/#1055): drop this real completion
1546 // so the op stays pending and the bounded drain must take its
1547 // DRAIN_TIMEOUT leak path. The CQE has already arrived, so the kernel
1548 // is done with the buffer — the eventual `forget` leaks a completed
1549 // allocation, never live memory.
1550 #[cfg(feature = "fault-injection")]
1551 if fault_stuck_drain && state.pending.contains_key(&ud) {
1552 continue;
1553 }
1554 let res = cqe.result();
1555 if !state.pending.contains_key(&ud) {
1556 continue;
1557 }
1558
1559 // Decide the next step while borrowing the entry, then act after
1560 // the borrow ends (finish removes it; resubmit re-queues an SQE).
1561 let step = {
1562 let p = state.pending.get_mut(&ud).expect("checked above");
1563 if res < 0 {
1564 let err = -res;
1565 // C7 three-class contract (rustfs/backlog#1166): a transient
1566 // errno (EINTR/EAGAIN) must be retried, not surfaced as the
1567 // read's final result — surfacing it would also discard the
1568 // already-read prefix of a resubmit. Bounded per logical read
1569 // so a storm cannot spin the driver thread. Streams
1570 // (CURRENT_POSITION) cannot resubmit positionally; ECANCELED
1571 // and every other errno terminate the logical read.
1572 let transient = err == libc::EINTR || err == libc::EAGAIN;
1573 if transient
1574 && p.offset != CURRENT_POSITION
1575 && p.nread < p.region_len
1576 && p.transient_retries < MAX_TRANSIENT_RETRIES
1577 {
1578 p.transient_retries += 1;
1579 ReapStep::Resubmit(p.read_sqe(ud))
1580 } else {
1581 // Error (incl. ECANCELED, or a transient errno past its
1582 // retry budget) terminates the logical read.
1583 ReapStep::Finish(Err(io::Error::from_raw_os_error(err)))
1584 }
1585 } else if res == 0 {
1586 // Real EOF: deliver whatever of the logical range was read.
1587 ReapStep::Finish(Ok(deliver(p)))
1588 } else {
1589 p.nread += res as usize;
1590 // Progress resets the transient-retry budget (rustfs/backlog#1166).
1591 p.transient_retries = 0;
1592 // Only POSITIONED reads (read_at / read_at_direct, whole-range
1593 // pread contract) resubmit a short read. CURRENT_POSITION
1594 // reads (read_current on pipes/streams) follow read(2)
1595 // semantics: a short read is a valid final result and must be
1596 // delivered as-is — resubmitting would block forever waiting
1597 // for stream data that may never come.
1598 let is_stream = p.offset == CURRENT_POSITION;
1599 let covered = p.nread >= p.head + p.want;
1600 if is_stream || covered || p.nread >= p.region_len {
1601 ReapStep::Finish(Ok(deliver(p)))
1602 } else if p.align > 1 && !p.nread.is_multiple_of(p.align) {
1603 // O_DIRECT non-block-multiple short read below the covered
1604 // range. The kernel returns block multiples EXCEPT at the
1605 // file tail — but a stacked filesystem (NFS/FUSE, or a
1606 // signal-split direct I/O) can legally return a non-multiple
1607 // mid-file, and assuming EOF there would silently truncate
1608 // the delivered range. Disambiguate with the actual file
1609 // length instead of inferring it (rustfs/backlog#1168).
1610 match file_len(&p.file) {
1611 // Genuine tail: at or past EOF — deliver what we read.
1612 Some(len) if p.offset + p.nread as u64 >= len => ReapStep::Finish(Ok(deliver(p))),
1613 // Mid-file non-multiple: an O_DIRECT read cannot resubmit
1614 // from a non-block-aligned offset, so surface an error
1615 // rather than truncate. The integration falls back to
1616 // the std backend for this read, preserving correctness.
1617 Some(_) => ReapStep::Finish(Err(io::Error::other(
1618 "io_uring O_DIRECT: non-block-aligned short read before EOF",
1619 ))),
1620 // fstat failed: keep the conservative EOF assumption
1621 // rather than risk a wrong error or an infinite loop.
1622 None => ReapStep::Finish(Ok(deliver(p))),
1623 }
1624 } else {
1625 // Positioned short read, not EOF, block-aligned: resubmit
1626 // the remainder into the read region. The buffer stays
1627 // owned by the driver and in_flight is unchanged — one
1628 // logical op.
1629 ReapStep::Resubmit(p.read_sqe(ud))
1630 }
1631 }
1632 };
1633
1634 match step {
1635 ReapStep::Finish(outcome) => {
1636 // Content hygiene (C12, rustfs/backlog#1062): the delivered
1637 // bytes are ⊆ [0, res) — buf was freshly zeroed per op and
1638 // truncated to res. When P3 reuses a driver-owned slab
1639 // across requests, this ⊆ [0, res) property MUST be
1640 // preserved or a previous tenant's object bytes leak.
1641 let mut p = state.pending.remove(&ud).expect("checked above");
1642 match p.done.take().expect("done sender set at submit").send(outcome) {
1643 Ok(()) => stats.delivered.fetch_add(1, Ordering::SeqCst),
1644 // Caller dropped the future: the buffer survived in
1645 // the table until this final CQE and is reclaimed here.
1646 Err(_) => stats.orphan_reclaimed.fetch_add(1, Ordering::SeqCst),
1647 };
1648 stats.in_flight.fetch_sub(1, Ordering::SeqCst);
1649 // Drop any queued-cancel bookkeeping for this now-gone op so
1650 // the dedup set stays bounded by the pending table
1651 // (rustfs/backlog#1167).
1652 queued_cancels.remove(&ud);
1653 // `p` (and with it `_permit`) is dropped here, at the CQE
1654 // and pending-table removal — never at future drop (C10,
1655 // rustfs/backlog#1060). No manual release to forget.
1656 }
1657 ReapStep::Resubmit(sqe) => state.backlog.push_back(sqe),
1658 }
1659 }
1660
1661 // A short-read resubmit queued during reap must reach the kernel in THIS
1662 // turn, not wait out the next heartbeat (rustfs/backlog#1163). Reap runs
1663 // after the submit above, so re-run the single submit path when reap left
1664 // work in the backlog; an idle turn leaves it empty and skips the call.
1665 if !state.backlog.is_empty() {
1666 submit_ring(
1667 &mut state,
1668 &stats,
1669 &mut consecutive_submit_errors,
1670 &mut submit_error_logged,
1671 &mut shutting_down,
1672 &mut queued_cancels,
1673 );
1674 }
1675
1676 // Monitor CQ overflow. With NODROP (asserted at probe) overflowed CQEs
1677 // are BUFFERED in the kernel overflow list and flushed on the next enter,
1678 // never lost — so a non-zero value is a backpressure warning, not fatal
1679 // loss (rustfs/backlog#1056, #1167). In-flight reads are capped at
1680 // `entries` and cancels are deduped (at most one per op), keeping total
1681 // completions <= 2*entries, so this should stay 0 in practice.
1682 let overflow = state.ring.completion().overflow();
1683 if overflow != 0 {
1684 stats.cq_overflow.store(overflow as u64, Ordering::SeqCst);
1685 tracing::warn!(
1686 overflow,
1687 "uring driver: CQ overflow; CQEs buffered (NODROP), not lost — backpressure warning"
1688 );
1689 }
1690
1691 // 4. Exit when drained: the kernel no longer references any buffer, so
1692 // dropping the ring (unmap) is safe. If a hung device keeps a CQE
1693 // from ever arriving, bail out under a bounded deadline instead of
1694 // blocking forever (C4, rustfs/backlog#1055).
1695 if shutting_down {
1696 if state.pending.is_empty() && state.backlog.is_empty() {
1697 // Close the semaphore so any handle still awaiting a permit
1698 // resolves with a driver-gone error instead of hanging.
1699 sem.close();
1700 return; // clean drain: DriverState drops normally, ring unmaps.
1701 }
1702 let deadline = *drain_deadline.get_or_insert_with(|| Instant::now() + drain_timeout);
1703 if Instant::now() >= deadline {
1704 // A CQE may never arrive (ASYNC_CANCEL cannot interrupt an
1705 // in-execution regular-file read on a hung disk). We must NOT
1706 // unmap the ring or free the still-in-flight buffers — leak the
1707 // whole state (leak over UAF) and exit so shutdown() returns.
1708 tracing::warn!(
1709 in_flight = state.pending.len(),
1710 "uring driver: bounded drain timed out with ops still in flight; leaking ring + buffers to stay memory-safe"
1711 );
1712 // Fail every stranded caller BEFORE leaking the pending table.
1713 // `oneshot::Sender::send` consumes the sender and never touches
1714 // `p.buf`, so the kernel-owned buffer stays allocated (leak over
1715 // UAF preserved) while an awaited `ReadHandle` resolves with an
1716 // error instead of pending forever — every other driver-gone path
1717 // already delivers an error, and this one must too
1718 // (rustfs/backlog#1161).
1719 for p in state.pending.values_mut() {
1720 if let Some(tx) = p.done.take() {
1721 let _ = tx.send(Err(io::Error::other("uring driver leaked op on bounded-drain timeout")));
1722 }
1723 }
1724 // Close the semaphore so any handle still awaiting a permit
1725 // resolves with a driver-gone error too. The leaked pending
1726 // entries keep their permits, which is fine: nothing waits on
1727 // them any more.
1728 sem.close();
1729 // The leaked ring still has `cq_efd` registered via
1730 // IORING_REGISTER_EVENTFD and in-flight ops that may post CQEs, so
1731 // the eventfd must outlive it. Leak it alongside the ring instead
1732 // of letting the returning `drive` drop (close) it out from under
1733 // the still-mapped ring, honoring start_shard's documented "cq_efd
1734 // outlives the ring" invariant on this exit too (rustfs/backlog#1167).
1735 std::mem::forget(cq_efd);
1736 std::mem::forget(state);
1737 return;
1738 }
1739 }
1740 // No pacing sleep: `wait_for_events` at the top of the loop blocks until
1741 // the next CQE, message, or heartbeat (backlog#1102).
1742 }
1743}