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