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, 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/// Owned `eventfd(2)` used to wake the driver loop (backlog#1102): one is
86/// registered with the ring so the kernel signals it on every CQE, the other is
87/// signaled by `submit`/shutdown so a new message wakes the loop immediately —
88/// together they replace the spike's 200 µs busy-poll.
89struct EventFd {
90 fd: std::os::fd::RawFd,
91}
92
93impl EventFd {
94 fn new() -> io::Result<Self> {
95 // SAFETY: eventfd returns a fresh owned fd or -1; the flags are valid.
96 let fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
97 if fd < 0 {
98 return Err(io::Error::last_os_error());
99 }
100 Ok(Self { fd })
101 }
102
103 fn as_raw(&self) -> std::os::fd::RawFd {
104 self.fd
105 }
106
107 /// Make the fd readable. A saturated counter (EAGAIN) is fine — it is
108 /// already readable, which is all a wakeup needs.
109 fn signal(&self) {
110 let v: u64 = 1;
111 // SAFETY: writing 8 bytes from a valid u64 to an eventfd we own.
112 unsafe {
113 libc::write(self.fd, (&v as *const u64).cast(), 8);
114 }
115 }
116
117 /// Reset the counter. EFD_NONBLOCK guarantees this never blocks; a single
118 /// successful read drains the whole counter, the next returns EAGAIN.
119 fn drain(&self) {
120 let mut v: u64 = 0;
121 // SAFETY: reading 8 bytes into a valid u64 from an eventfd we own.
122 while unsafe { libc::read(self.fd, (&mut v as *mut u64).cast(), 8) } == 8 {}
123 }
124}
125
126impl Drop for EventFd {
127 fn drop(&mut self) {
128 // SAFETY: we own this fd and drop it exactly once.
129 unsafe {
130 libc::close(self.fd);
131 }
132 }
133}
134
135/// Block until a CQE is ready (`cq`), a new message arrives (`wake`), or the
136/// heartbeat elapses. The return value is ignored: a spurious wakeup, timeout,
137/// or EINTR just runs one loop turn (intake + reap), which is always safe.
138fn wait_for_events(cq: &EventFd, wake: &EventFd, timeout: Duration) {
139 let mut fds = [
140 libc::pollfd {
141 fd: cq.as_raw(),
142 events: libc::POLLIN,
143 revents: 0,
144 },
145 libc::pollfd {
146 fd: wake.as_raw(),
147 events: libc::POLLIN,
148 revents: 0,
149 },
150 ];
151 let ms = timeout.as_millis().min(i32::MAX as u128) as libc::c_int;
152 // SAFETY: `fds` is a valid, initialized array of two pollfds.
153 unsafe {
154 libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, ms);
155 }
156}
157
158/// Why the probe refused to start the io_uring driver.
159///
160/// Mirrors the P2 degradation contract (backlog#894): a restricted
161/// environment must be recognized and answered with a silent fallback to the
162/// std backend, never surfaced to callers.
163#[derive(Debug)]
164pub enum ProbeFailure {
165 /// `io_uring_setup` itself failed (seccomp/gVisor/old kernel).
166 Setup(io::Error),
167 /// The ring was created but a real `IORING_OP_READ` did not complete
168 /// correctly (gVisor accepts setup but fails ops; also covers silent
169 /// data corruption, which we treat as "unusable").
170 ReadOp(io::Error),
171}
172
173impl ProbeFailure {
174 /// True when the **probe-time** errno belongs to the "expected
175 /// restriction" class that P2 maps to permanent per-disk fallback:
176 /// EACCES/EPERM/ENOSYS/EINVAL/EOPNOTSUPP. Anything else is a genuine bug
177 /// worth surfacing.
178 ///
179 /// IMPORTANT (C7, rustfs/backlog#1059): this classification is valid ONLY
180 /// for a one-shot startup probe, where these errnos unambiguously mean
181 /// "io_uring is unusable here" (gVisor/seccomp/old kernel). Runtime
182 /// per-op errnos have different semantics and MUST NOT reuse this class.
183 /// In particular EINVAL is triple-meaning at runtime — offset > i64::MAX
184 /// (signed loff_t), O_DIRECT buffer/offset/len misalignment (P2 will use
185 /// O_DIRECT), and setup `entries` over the cap — none of which imply the
186 /// disk should be permanently degraded off io_uring. P2's degradation
187 /// contract must split errnos into three classes:
188 ///
189 /// * probe-time restriction -> degrade this disk to the std backend;
190 /// * runtime parameter error -> return the error to the caller (and,
191 /// for a suspected bug, re-verify once via std pread) — never latch;
192 /// * transient (EINTR/EAGAIN) -> retry, never surface.
193 ///
194 /// See `submit` for the offset guard that keeps a caller arithmetic bug
195 /// from ever reaching the kernel as a runtime EINVAL.
196 pub fn is_expected_restriction(&self) -> bool {
197 let err = match self {
198 ProbeFailure::Setup(e) | ProbeFailure::ReadOp(e) => e,
199 };
200 matches!(
201 err.raw_os_error(),
202 Some(libc::EACCES) | Some(libc::EPERM) | Some(libc::ENOSYS) | Some(libc::EINVAL) | Some(libc::EOPNOTSUPP)
203 )
204 }
205}
206
207// Submission-side backpressure (C10, rustfs/backlog#1060; async in #1102).
208//
209// A `tokio::sync::Semaphore` with `entries` permits bounds in-flight ops below
210// CQ capacity. The load-bearing rule is the RELEASE POINT: a permit is released
211// at the CQE (when the pending-table entry is removed), NOT at future drop.
212// Tying a permit to the future (the natural RAII shape) would let a quorum
213// dropping many futures return permits while their orphan buffers still sit in
214// the pending table awaiting slow-disk CQEs, decoupling the permit count from
215// resident memory and reopening the memory-DoS surface.
216//
217// That rule is now enforced by the type system rather than by a manual
218// `release()` call: the `OwnedSemaphorePermit` travels with `Msg::Read` into the
219// `Pending` entry and is dropped exactly when the entry is removed at the final
220// CQE. A short-read resubmit keeps the entry — and thus the permit.
221//
222// Acquisition never blocks the caller's thread: `submit` takes the permit with
223// `try_acquire_owned()` on the common unsaturated path (no allocation, no await,
224// submission stays eager), and when saturated it hands the acquire future to the
225// returned `ReadHandle`, which awaits it on its first poll and submits then.
226
227/// Boxed `Semaphore::acquire_owned` future held by a saturated `ReadHandle`.
228type AcquireFut = Pin<Box<dyn Future<Output = Result<OwnedSemaphorePermit, tokio::sync::AcquireError>> + Send>>;
229
230#[derive(Default)]
231struct DriverStats {
232 submitted: AtomicU64,
233 delivered: AtomicU64,
234 orphan_reclaimed: AtomicU64,
235 in_flight: AtomicU64,
236 cancel_succeeded: AtomicU64,
237 cancel_not_found: AtomicU64,
238 cancel_already: AtomicU64,
239 cq_overflow: AtomicU64,
240}
241
242/// Point-in-time copy of the driver counters.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
244pub struct StatsSnapshot {
245 /// Read ops handed to the kernel.
246 pub submitted: u64,
247 /// CQEs whose result was received by a live caller.
248 pub delivered: u64,
249 /// CQEs whose caller had dropped the future: the buffer stayed in the
250 /// pending table the whole time and was reclaimed here, at the CQE.
251 pub orphan_reclaimed: u64,
252 /// Ops submitted but not yet completed. The kernel may still write into
253 /// their buffers.
254 pub in_flight: u64,
255 /// ASYNC_CANCEL CQEs that reported the target op was canceled (res == 0).
256 pub cancel_succeeded: u64,
257 /// ASYNC_CANCEL CQEs that reported the target was not found (-ENOENT):
258 /// the op had already completed.
259 pub cancel_not_found: u64,
260 /// ASYNC_CANCEL CQEs that reported the target was already executing and
261 /// could not be interrupted (-EALREADY). A rising count is the hung-disk
262 /// signal that makes drain-to-zero non-terminating (C4,
263 /// rustfs/backlog#1055).
264 pub cancel_already: u64,
265 /// Kernel CQ-ring overflow counter. MUST stay 0: a non-zero value means
266 /// CQEs were lost, so their pending entries are never reclaimed and drain
267 /// never completes. Treated as fatal (C5, rustfs/backlog#1056).
268 pub cq_overflow: u64,
269}
270
271enum Msg {
272 Read {
273 id: u64,
274 file: Arc<File>,
275 offset: u64,
276 len: usize,
277 done: oneshot::Sender<io::Result<Vec<u8>>>,
278 /// Backpressure permit, acquired before the op reaches the driver and
279 /// released only when the pending entry is dropped at the final CQE
280 /// (rustfs/backlog#1060/#1102). If the driver rejects the op (shutting
281 /// down) the permit is dropped with the message — released immediately.
282 permit: OwnedSemaphorePermit,
283 /// Block size the read must be aligned to. `1` means a normal buffered
284 /// read; `> 1` means the file was opened `O_DIRECT` and the driver must
285 /// read the block-aligned superset range into a block-aligned buffer
286 /// (rustfs/backlog#1102).
287 align: usize,
288 },
289 Cancel {
290 id: u64,
291 },
292 Shutdown,
293}
294
295/// One in-flight LOGICAL read. This struct — not the caller — owns everything
296/// the kernel touches:
297///
298/// - `buf`: the destination buffer. Its heap allocation must stay put until
299/// the final CQE; the `Vec` itself may move (HashMap rehash) since that
300/// never relocates the heap block. It is never resized or dropped before
301/// the CQE handler removes this entry.
302/// - `file`: keeps the fd open even if every caller-side clone is dropped, and
303/// supplies the fd for short-read resubmission. Without it, dropping the
304/// future could close the fd while an SQE built from that fd still sits in
305/// the backlog (SQE construction → io_uring_enter window), and a recycled
306/// fd number would make the kernel read the WRONG file (spike finding, with
307/// the corrected mechanism per rustfs/backlog#1063).
308/// - `offset`/`nread`: track a short-read resubmit loop (C9,
309/// rustfs/backlog#1058). io_uring may legally short-read a regular file;
310/// the driver resubmits the remainder into `buf[nread..]` until the request
311/// is fully satisfied or a real EOF (res == 0) is seen, so reclamation
312/// happens only at the FINAL CQE of the logical read.
313/// - `_permit`: the backpressure permit. Holding it here makes the
314/// "release at the CQE, never at future drop" rule (rustfs/backlog#1060) a
315/// property of the type: the permit is dropped exactly when this entry is
316/// removed at the final CQE. A short-read resubmit keeps the entry, and thus
317/// the permit, so in-flight memory stays bounded.
318/// - Alignment geometry (rustfs/backlog#1102). For a buffered read these are
319/// `pad = head = 0`, `align = 1`, `region_len = want`, so every rule below
320/// collapses to the plain case. For an `O_DIRECT` read the driver reads the
321/// block-aligned superset `[offset, offset + region_len)` into
322/// `buf[pad .. pad + region_len]` (both block-aligned) and hands the caller
323/// only `buf[pad + head .. pad + head + want]` — alignment padding never
324/// escapes.
325struct Pending {
326 buf: Vec<u8>,
327 file: Arc<File>,
328 done: Option<oneshot::Sender<io::Result<Vec<u8>>>>,
329 /// Kernel read offset: the block-aligned offset for a direct read, the
330 /// logical offset for a buffered one, `CURRENT_POSITION` for a stream.
331 offset: u64,
332 /// Bytes already read into the read region (`buf[pad..]`).
333 nread: usize,
334 _permit: OwnedSemaphorePermit,
335 /// Offset inside `buf` where the block-aligned read region starts.
336 pad: usize,
337 /// Bytes of the read region that precede the caller's logical range.
338 head: usize,
339 /// Logical length the caller asked for.
340 want: usize,
341 /// Bytes the kernel is asked to read (block-aligned for a direct read).
342 region_len: usize,
343 /// `1` for buffered, the block size for `O_DIRECT`.
344 align: usize,
345}
346
347/// Where a [`ReadHandle`] is in its lifecycle (rustfs/backlog#1102).
348enum HandleState {
349 /// Nothing was ever handed to the driver (a rejected parameter, or the
350 /// driver was already gone). The result is already sitting in `rx`, and
351 /// there is no buffer, permit, or SQE to reclaim.
352 Inert,
353 /// Backpressure was saturated at `submit` time, so the permit — and with it
354 /// the submission — is deferred to the first poll. The caller's thread is
355 /// never blocked. Dropping the handle in this state submitted nothing.
356 WaitingPermit {
357 acquire: AcquireFut,
358 file: Arc<File>,
359 offset: u64,
360 len: usize,
361 align: usize,
362 done: oneshot::Sender<io::Result<Vec<u8>>>,
363 wake: Arc<EventFd>,
364 },
365 /// The op is with the driver: its buffer lives in the pending table and is
366 /// reclaimed only at the CQE.
367 Submitted,
368}
369
370/// Handle to a read. Await it for the result.
371///
372/// Dropping it before completion abandons the result only; if the op was
373/// already submitted it also sends `IORING_OP_ASYNC_CANCEL` (best effort) so the
374/// CQE — and with it the buffer reclamation — arrives sooner.
375/// `without_cancel_on_drop` disables that to model the bare "quorum drops the
376/// future" case.
377///
378/// Submission is eager whenever a backpressure permit is immediately available
379/// (the common case, unchanged from the blocking implementation). Only when the
380/// semaphore is saturated does the handle acquire the permit and submit on its
381/// first poll, so `submit` never blocks a runtime worker.
382pub struct ReadHandle {
383 id: u64,
384 rx: oneshot::Receiver<io::Result<Vec<u8>>>,
385 tx: mpsc::Sender<Msg>,
386 finished: bool,
387 cancel_on_drop: bool,
388 state: HandleState,
389}
390
391impl ReadHandle {
392 pub fn without_cancel_on_drop(mut self) -> Self {
393 self.cancel_on_drop = false;
394 self
395 }
396}
397
398impl Future for ReadHandle {
399 type Output = io::Result<Vec<u8>>;
400
401 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
402 let this = &mut *self;
403
404 // Saturated at submit time: take the permit, then hand the op to the
405 // driver. The permit rides along in the message and is released only
406 // when the pending entry is dropped at the CQE.
407 let acquired = match &mut this.state {
408 HandleState::WaitingPermit { acquire, .. } => match acquire.as_mut().poll(cx) {
409 Poll::Pending => return Poll::Pending,
410 Poll::Ready(res) => Some(res),
411 },
412 _ => None,
413 };
414 if let Some(res) = acquired {
415 let Ok(permit) = res else {
416 // The semaphore was closed: the driver is gone.
417 this.finished = true;
418 return Poll::Ready(Err(io::Error::other("uring driver shut down")));
419 };
420 let HandleState::WaitingPermit {
421 file,
422 offset,
423 len,
424 align,
425 done,
426 wake,
427 ..
428 } = std::mem::replace(&mut this.state, HandleState::Submitted)
429 else {
430 unreachable!("state was WaitingPermit")
431 };
432 if this
433 .tx
434 .send(Msg::Read {
435 id: this.id,
436 file,
437 offset,
438 len,
439 done,
440 permit,
441 align,
442 })
443 .is_err()
444 {
445 // Driver gone between the acquire and the send; the message
446 // (with its permit) is dropped, releasing it.
447 this.finished = true;
448 return Poll::Ready(Err(io::Error::other("uring driver shut down")));
449 }
450 wake.signal();
451 }
452
453 match Pin::new(&mut this.rx).poll(cx) {
454 Poll::Ready(res) => {
455 this.finished = true;
456 Poll::Ready(match res {
457 Ok(inner) => inner,
458 Err(_) => Err(io::Error::other("uring driver shut down before completion")),
459 })
460 }
461 Poll::Pending => Poll::Pending,
462 }
463 }
464}
465
466impl Drop for ReadHandle {
467 fn drop(&mut self) {
468 // The buffer is deliberately NOT touched here: the driver owns it
469 // until the CQE. All we may do is ask the kernel to hurry up. A handle
470 // dropped before it was submitted (Inert / WaitingPermit) has no buffer,
471 // no permit and no SQE, so there is nothing to cancel.
472 if matches!(self.state, HandleState::Submitted) && !self.finished && self.cancel_on_drop {
473 let _ = self.tx.send(Msg::Cancel { id: self.id });
474 }
475 }
476}
477
478/// Process-level io_uring driver: one ring, one driver thread.
479/// One io_uring ring plus the thread that drives it.
480///
481/// Every cancel-safety invariant holds *per shard*, exactly as it did when a
482/// driver owned a single ring: this shard's pending table owns its buffers and
483/// fds until their CQEs, its permits are released only when a pending entry is
484/// dropped, and its bounded drain is what shutdown joins on. A `ReadHandle`
485/// carries the `tx` and `wake` of the shard that accepted it, so a cancel or a
486/// deferred submission always routes back to that same shard.
487struct Shard {
488 tx: mpsc::Sender<Msg>,
489 handle: Option<JoinHandle<()>>,
490 stats: Arc<DriverStats>,
491 /// Backpressure permits (one per allowed in-flight op on this ring). Closed
492 /// when the driver thread exits so any waiting `ReadHandle` resolves with a
493 /// driver-gone error instead of hanging (rustfs/backlog#1102).
494 sem: Arc<Semaphore>,
495 /// Signaled after every message send so this shard's loop wakes immediately
496 /// instead of waiting out the heartbeat (backlog#1102).
497 wake_efd: Arc<EventFd>,
498}
499
500impl Shard {
501 /// Ask the shard's thread to drain and exit, then join it. Idempotent: the
502 /// `JoinHandle` is taken, so a later `Drop` is a no-op.
503 fn join(&mut self) {
504 if let Some(h) = self.handle.take() {
505 let _ = self.tx.send(Msg::Shutdown);
506 self.wake_efd.signal();
507 let _ = h.join();
508 }
509 }
510}
511
512impl Drop for Shard {
513 fn drop(&mut self) {
514 self.join();
515 }
516}
517
518pub struct UringDriver {
519 /// One or more independent rings. A cache-hit buffered read completes inline
520 /// inside `io_uring_enter`, so the thread driving a ring performs that
521 /// read's memcpy — which caps a single-ring driver at one core's memory
522 /// bandwidth (~5 GB/s measured, rustfs/backlog#1145). Sharding lifts that
523 /// ceiling roughly linearly while keeping the ring set per-disk, so a stalled
524 /// disk still cannot starve another disk's rings (rustfs/backlog#1055).
525 shards: Vec<Shard>,
526 next_id: AtomicU64,
527 /// Round-robin cursor for shard selection. Relaxed: it only has to spread
528 /// ops, never to order them.
529 rr: AtomicUsize,
530}
531
532impl UringDriver {
533 /// Create the ring AND verify a real `IORING_OP_READ` round-trip on a
534 /// temp file before accepting work. `io_uring_setup` succeeding is not
535 /// enough: gVisor/seccomp environments can create a ring whose ops then
536 /// fail with ENOSYS/EINVAL (backlog#894 probe design).
537 /// Start a single-ring driver. Identical to `probe_and_start_sharded(entries, 1)`.
538 pub fn probe_and_start(entries: u32) -> Result<Self, ProbeFailure> {
539 Self::probe_and_start_sharded(entries, 1)
540 }
541
542 /// Start a driver backed by `shards` independent rings, each with `entries`
543 /// SQ slots and its own driver thread.
544 ///
545 /// Use more than one shard when the workload hits the page cache: such reads
546 /// complete inline in `io_uring_enter`, so a single driver thread performs
547 /// every one of their memcpys and caps the driver at one core's memory
548 /// bandwidth. Measured on a 16-core host (rustfs/backlog#1145): 1 ring →
549 /// 4890 MB/s, 2 → 8969 MB/s, 4 → 15806 MB/s, with per-ring throughput flat.
550 /// Reads that miss the cache are device-bound and do not need sharding.
551 ///
552 /// In-flight ops are capped at `entries` *per shard* (the invariant that
553 /// makes CQ overflow structurally unreachable holds per ring), so the whole
554 /// driver admits up to `shards * entries` concurrent reads.
555 ///
556 /// `shards` is clamped to at least 1. Probing happens on the first shard, so
557 /// a restricted environment fails exactly as it does for a single ring; if a
558 /// later shard fails to start, the ones already running are shut down and
559 /// joined before the error is returned.
560 pub fn probe_and_start_sharded(entries: u32, shards: usize) -> Result<Self, ProbeFailure> {
561 let mut started = Vec::with_capacity(shards.max(1));
562 for _ in 0..shards.max(1) {
563 // `?` drops `started`, whose `Shard::drop` joins each running thread.
564 started.push(Self::start_shard(entries)?);
565 }
566 Ok(Self {
567 shards: started,
568 next_id: AtomicU64::new(1),
569 rr: AtomicUsize::new(0),
570 })
571 }
572
573 /// Pick the shard for the next op. Round-robin spreads the inline-completion
574 /// memcpy across driver threads; correctness does not depend on the choice,
575 /// because the handle remembers which shard took the op.
576 fn shard(&self) -> &Shard {
577 let n = self.shards.len();
578 &self.shards[self.rr.fetch_add(1, Ordering::Relaxed) % n]
579 }
580
581 fn start_shard(entries: u32) -> Result<Shard, ProbeFailure> {
582 let mut ring = IoUring::new(entries).map_err(ProbeFailure::Setup)?;
583 // Require the NODROP feature (kernel >= 5.5). Without it, CQ overflow
584 // silently drops CQEs, stranding pending entries forever and hanging
585 // shutdown (C5, rustfs/backlog#1056). ENOSYS is in the expected-
586 // restriction class, so this degrades to the std backend cleanly.
587 if !ring.params().is_feature_nodrop() {
588 return Err(ProbeFailure::Setup(io::Error::from_raw_os_error(libc::ENOSYS)));
589 }
590 probe_real_read(&mut ring).map_err(ProbeFailure::ReadOp)?;
591
592 // Wake the driver loop on CQEs (kernel-signaled via a registered
593 // eventfd) and on new messages (submit-signaled), replacing the 200 µs
594 // busy-poll (backlog#1102). Registration needs the ring, which the
595 // driver thread then owns; `cq_efd` is moved in alongside so it outlives
596 // the ring (dropped after it, unregistering cleanly).
597 let cq_efd = EventFd::new().map_err(ProbeFailure::Setup)?;
598 ring.submitter()
599 .register_eventfd(cq_efd.as_raw())
600 .map_err(ProbeFailure::Setup)?;
601 let wake_efd = Arc::new(EventFd::new().map_err(ProbeFailure::Setup)?);
602 let thread_wake = Arc::clone(&wake_efd);
603
604 let (tx, rx) = mpsc::channel();
605 let stats = Arc::new(DriverStats::default());
606 let thread_stats = Arc::clone(&stats);
607 // Cap in-flight at the SQ depth (entries), which is < CQ capacity
608 // (2*entries), so CQ overflow is structurally unreachable (C5/C10).
609 let sem = Arc::new(Semaphore::new(entries as usize));
610 let thread_sem = Arc::clone(&sem);
611 let handle = std::thread::Builder::new()
612 .name("uring-spike-driver".into())
613 .spawn(move || drive(ring, rx, thread_stats, thread_sem, cq_efd, thread_wake))
614 .expect("spawn driver thread");
615
616 Ok(Shard {
617 tx,
618 handle: Some(handle),
619 stats,
620 sem,
621 wake_efd,
622 })
623 }
624
625 /// Positioned read (pread semantics) — regular files, buffered.
626 pub fn read_at(&self, file: Arc<File>, offset: u64, len: usize) -> ReadHandle {
627 assert_ne!(offset, CURRENT_POSITION, "offset u64::MAX is reserved");
628 self.submit(file, offset, len, 1)
629 }
630
631 /// Read at the file's current position (read(2) semantics) — pipes.
632 pub fn read_current(&self, file: Arc<File>, len: usize) -> ReadHandle {
633 self.submit(file, CURRENT_POSITION, len, 1)
634 }
635
636 /// Positioned read from a file opened with `O_DIRECT` (rustfs/backlog#1102).
637 ///
638 /// `align` is the device's logical block size — a power of two, typically
639 /// 512 or 4096. `offset` and `len` are the caller's *logical* range and need
640 /// no alignment: the driver reads the block-aligned superset range into a
641 /// block-aligned buffer and returns exactly `[offset, offset + len)`.
642 /// Alignment padding never reaches the caller, so a `BitrotReader` expecting
643 /// an exact shard length never sees padded output.
644 ///
645 /// The caller must have opened `file` with `O_DIRECT`; otherwise this is
646 /// just a (correct but pointless) buffered read of the superset range.
647 pub fn read_at_direct(&self, file: Arc<File>, offset: u64, len: usize, align: usize) -> ReadHandle {
648 assert_ne!(offset, CURRENT_POSITION, "offset u64::MAX is reserved");
649 self.submit(file, offset, len, align)
650 }
651
652 fn submit(&self, file: Arc<File>, offset: u64, len: usize, align: usize) -> ReadHandle {
653 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
654 assert_eq!(id & CANCEL_BIT, 0, "op id overflowed into the cancel bit");
655 let (done, rx) = oneshot::channel();
656
657 // Bind the op to one shard for its whole life: the permit, the message,
658 // the wake, and any later cancel all go to this ring. The handle holds
659 // clones of that shard's `tx`/`wake_efd`, so nothing can route a cancel
660 // to a ring whose pending table does not hold the op. The rejection paths
661 // below return an `Inert` handle that never sends, but still need a `tx`.
662 let shard = self.shard();
663
664 // Reject an offset the kernel would answer with a runtime EINVAL that
665 // must NOT be mistaken for an environment restriction (C7,
666 // rustfs/backlog#1059). The kernel reads `off` as a signed loff_t, so
667 // offset > i64::MAX becomes a negative ki_pos → EINVAL. A caller
668 // offset-arithmetic bug has to surface as an error here, never as a
669 // permanent per-disk fallback. CURRENT_POSITION is the reserved
670 // read(2) sentinel and bypasses this check.
671 if offset != CURRENT_POSITION && offset > i64::MAX as u64 {
672 let _ = done.send(Err(io::Error::new(
673 io::ErrorKind::InvalidInput,
674 "offset exceeds i64::MAX (kernel loff_t is signed)",
675 )));
676 return ReadHandle {
677 id,
678 rx,
679 tx: shard.tx.clone(),
680 finished: false,
681 cancel_on_drop: false,
682 state: HandleState::Inert,
683 };
684 }
685
686 // Reject a length the kernel would short-read past MAX_RW_COUNT and
687 // that the SQE's u32 `len` field would silently truncate: len == 2^32
688 // becomes a 0-byte read the caller decodes as a false EOF (C6,
689 // rustfs/backlog#1057). Failing fast here also removes the caller-
690 // controlled `vec![0u8; len]` capacity-overflow panic that made the
691 // unwind-UAF (rustfs/backlog#1054) reachable. P2 must chunk instead.
692 if len > MAX_READ_LEN {
693 let _ = done.send(Err(io::Error::new(
694 io::ErrorKind::InvalidInput,
695 "read length exceeds MAX_RW_COUNT (2 GiB - 4 KiB); caller must chunk",
696 )));
697 return ReadHandle {
698 id,
699 rx,
700 tx: shard.tx.clone(),
701 finished: false,
702 cancel_on_drop: false,
703 state: HandleState::Inert,
704 };
705 }
706
707 // Reject a bad O_DIRECT alignment, and a request whose block-aligned
708 // superset range would exceed the kernel's single-read cap
709 // (rustfs/backlog#1102). `align == 1` (buffered) always passes.
710 match aligned_geometry(offset, len, align) {
711 Some((_, _, region_len)) if region_len <= MAX_READ_LEN => {}
712 _ => {
713 let _ = done.send(Err(io::Error::new(
714 io::ErrorKind::InvalidInput,
715 "alignment must be a power of two and the block-aligned range must fit MAX_RW_COUNT",
716 )));
717 return ReadHandle {
718 id,
719 rx,
720 tx: shard.tx.clone(),
721 finished: false,
722 cancel_on_drop: false,
723 state: HandleState::Inert,
724 };
725 }
726 }
727
728 // Take a backpressure permit BEFORE the op reaches the driver; it is
729 // released only when the pending entry is dropped at the CQE (C10,
730 // rustfs/backlog#1060). Acquisition never blocks the caller's thread
731 // (rustfs/backlog#1102).
732 match Arc::clone(&shard.sem).try_acquire_owned() {
733 // Fast path: a permit was free, so submit eagerly — no allocation,
734 // no await, and the op is in flight the moment `submit` returns,
735 // exactly as with the previous blocking implementation.
736 Ok(permit) => {
737 if let Err(mpsc::SendError(msg)) = shard.tx.send(Msg::Read {
738 id,
739 file,
740 offset,
741 len,
742 done,
743 permit,
744 align,
745 }) {
746 // Driver gone: the op never reached it. Surface an explicit
747 // driver-gone error through `done` instead of letting the
748 // caller infer one from the dropped oneshot, matching the
749 // `Closed` arm below. The permit rides back in `msg` and is
750 // released when it drops here.
751 if let Msg::Read { done, .. } = msg {
752 let _ = done.send(Err(io::Error::other("uring driver shut down")));
753 }
754 return ReadHandle {
755 id,
756 rx,
757 tx: shard.tx.clone(),
758 finished: false,
759 cancel_on_drop: false,
760 state: HandleState::Inert,
761 };
762 }
763 // Wake the driver loop so the read starts immediately.
764 shard.wake_efd.signal();
765 ReadHandle {
766 id,
767 rx,
768 tx: shard.tx.clone(),
769 finished: false,
770 cancel_on_drop: true,
771 state: HandleState::Submitted,
772 }
773 }
774 // Saturated: `entries` ops are already in flight. Do NOT block the
775 // calling (runtime worker) thread — hand the acquire future to the
776 // handle, which awaits it on its first poll and submits then.
777 Err(TryAcquireError::NoPermits) => ReadHandle {
778 id,
779 rx,
780 tx: shard.tx.clone(),
781 finished: false,
782 cancel_on_drop: true,
783 state: HandleState::WaitingPermit {
784 acquire: Box::pin(Arc::clone(&shard.sem).acquire_owned()),
785 file,
786 offset,
787 len,
788 align,
789 done,
790 wake: Arc::clone(&shard.wake_efd),
791 },
792 },
793 // The driver has exited and closed the semaphore.
794 Err(TryAcquireError::Closed) => {
795 let _ = done.send(Err(io::Error::other("uring driver shut down")));
796 ReadHandle {
797 id,
798 rx,
799 tx: shard.tx.clone(),
800 finished: false,
801 cancel_on_drop: false,
802 state: HandleState::Inert,
803 }
804 }
805 }
806 }
807
808 /// Counters summed across every shard. The conservation identities the
809 /// cancel-safety tests assert (`submitted == delivered + orphan_reclaimed`,
810 /// `in_flight == 0` after a clean drain) hold per shard, so they hold for
811 /// the sum.
812 pub fn stats(&self) -> StatsSnapshot {
813 let mut snap = StatsSnapshot::default();
814 for shard in &self.shards {
815 let s = &shard.stats;
816 snap.submitted += s.submitted.load(Ordering::SeqCst);
817 snap.delivered += s.delivered.load(Ordering::SeqCst);
818 snap.orphan_reclaimed += s.orphan_reclaimed.load(Ordering::SeqCst);
819 snap.in_flight += s.in_flight.load(Ordering::SeqCst);
820 snap.cancel_succeeded += s.cancel_succeeded.load(Ordering::SeqCst);
821 snap.cancel_not_found += s.cancel_not_found.load(Ordering::SeqCst);
822 snap.cancel_already += s.cancel_already.load(Ordering::SeqCst);
823 snap.cq_overflow += s.cq_overflow.load(Ordering::SeqCst);
824 }
825 snap
826 }
827
828 /// Stop accepting work, cancel all in-flight ops, drain every ring to
829 /// `in_flight == 0`, then join each driver thread. Only after that is a ring
830 /// dropped/unmapped — the shutdown ordering P2 requires, per shard.
831 ///
832 /// Shards are asked to stop first and joined afterwards, so their bounded
833 /// drains overlap instead of serializing `shards * DRAIN_TIMEOUT`.
834 pub fn shutdown(mut self) -> StatsSnapshot {
835 for shard in &self.shards {
836 let _ = shard.tx.send(Msg::Shutdown);
837 shard.wake_efd.signal();
838 }
839 for shard in &mut self.shards {
840 shard.join();
841 }
842 let snap = self.stats();
843 // A clean drain leaves in_flight == 0. A non-zero count here means some
844 // shard's bounded drain bailed out on a hung device and leaked its
845 // ring+buffers to stay memory-safe (C4, rustfs/backlog#1055) — a degraded
846 // but safe outcome, not a panic. Callers/tests that require a clean drain
847 // assert on the returned snapshot themselves.
848 if snap.in_flight != 0 {
849 eprintln!(
850 "uring-spike shutdown: {} ops still in flight (bounded-drain bailout on a hung device)",
851 snap.in_flight
852 );
853 }
854 snap
855 }
856}
857
858impl Drop for UringDriver {
859 fn drop(&mut self) {
860 // Ask every shard to stop before joining any of them, so their bounded
861 // drains overlap. Dropping the `Vec<Shard>` would instead run each
862 // `Shard::drop` in turn, serializing up to `shards * DRAIN_TIMEOUT` on a
863 // hung device. `Shard::join` is idempotent, so the later drops are no-ops.
864 for shard in &self.shards {
865 let _ = shard.tx.send(Msg::Shutdown);
866 shard.wake_efd.signal();
867 }
868 for shard in &mut self.shards {
869 shard.join();
870 }
871 }
872}
873
874fn probe_real_read(ring: &mut IoUring) -> io::Result<()> {
875 let pattern: Vec<u8> = (0..512u32).map(|i| (i * 7 + 13) as u8).collect();
876
877 // Open an anonymous probe file seeded with the pattern. File setup runs
878 // BEFORE any SQE, so its errors early-return safely — nothing is in flight.
879 let file = open_probe_file(&pattern)?;
880
881 let mut buf = vec![0u8; pattern.len()];
882 let sqe = opcode::Read::new(types::Fd(file.as_raw_fd()), buf.as_mut_ptr(), buf.len() as u32)
883 .offset(0)
884 .build()
885 .user_data(0xB0BE);
886
887 // SAFETY: a push failure means the kernel never accepted the SQE, so
888 // `buf`/`file` may be dropped safely on this early return.
889 if unsafe { ring.submission().push(&sqe) }.is_err() {
890 return Err(io::Error::other("probe: submission queue full"));
891 }
892
893 // C1 (rustfs/backlog#1053): once the SQE is handed to the kernel, the read
894 // may be punted to io-wq and write into `buf` at ANY later point. Until its
895 // CQE arrives, `buf`/`file` must NOT be dropped and the ring must NOT be
896 // unmapped — otherwise the kernel writes into freed memory (UAF). The probe
897 // path has no pending-table backstop, so we must drain to the CQE here, and
898 // any early exit first leaks the buffer ("leak over UAF").
899 let res = match drain_probe_cqe(ring) {
900 Ok(res) => res,
901 Err(e) => {
902 // Could not confirm the op terminated: leak `buf` (the real UAF
903 // hazard — the kernel may still write 512 bytes into it) and,
904 // defensively, `file`. Leaking one 512-byte startup-probe buffer is
905 // trivially cheaper than a silent heap corruption.
906 std::mem::forget(buf);
907 std::mem::forget(file);
908 return Err(e);
909 }
910 };
911
912 // The CQE has arrived: the kernel is done with `buf`, so dropping it and
913 // `file` below is now safe.
914 if res < 0 {
915 Err(io::Error::from_raw_os_error(-res))
916 } else if res as usize != pattern.len() || buf != pattern {
917 Err(io::Error::other("probe: read completed but data mismatched"))
918 } else {
919 Ok(())
920 }
921}
922
923/// Open a probe file seeded with `pattern`, avoiding the symlink/TOCTOU/
924/// leftover hazards of a predictable temp path (C3, rustfs/backlog#1061).
925///
926/// Primary: `O_TMPFILE` — an anonymous inode with no name at all, so there is
927/// nothing for an attacker to pre-plant a symlink at, no TOCTOU window, and no
928/// leftover file. Fallback (filesystems without O_TMPFILE): create in the temp
929/// dir with `O_CREAT|O_EXCL|O_NOFOLLOW` + 0600 + a per-process nonce, then
930/// unlink immediately so no attacker-planted symlink is followed and no named
931/// file survives.
932fn open_probe_file(pattern: &[u8]) -> io::Result<File> {
933 let dir = std::env::temp_dir();
934 let c_dir = std::ffi::CString::new(dir.as_os_str().as_bytes()).map_err(|_| io::Error::other("probe dir path has NUL"))?;
935 // SAFETY: `c_dir` is a valid NUL-terminated path; O_TMPFILE requires a
936 // directory and O_RDWR/O_WRONLY. On success we own the returned fd.
937 let fd = unsafe { libc::open(c_dir.as_ptr(), libc::O_TMPFILE | libc::O_RDWR | libc::O_CLOEXEC, 0o600) };
938 if fd >= 0 {
939 let mut file = unsafe { File::from_raw_fd(fd) };
940 file.write_all(pattern)?;
941 return Ok(file);
942 }
943 open_probe_file_exclusive(&dir, pattern)
944}
945
946fn open_probe_file_exclusive(dir: &std::path::Path, pattern: &[u8]) -> io::Result<File> {
947 static SEQ: AtomicU64 = AtomicU64::new(0);
948 let nonce = SEQ.fetch_add(1, Ordering::Relaxed);
949 let path = dir.join(format!("uring-spike-probe-{}-{}", std::process::id(), nonce));
950 let c_path = std::ffi::CString::new(path.as_os_str().as_bytes()).map_err(|_| io::Error::other("probe path has NUL"))?;
951 // O_EXCL refuses a pre-existing file; O_NOFOLLOW refuses a symlink; 0600 is
952 // owner-only. SAFETY: `c_path` is a valid NUL-terminated path; on success
953 // we own the fd.
954 let fd = unsafe {
955 libc::open(
956 c_path.as_ptr(),
957 libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_RDWR | libc::O_CLOEXEC,
958 0o600,
959 )
960 };
961 if fd < 0 {
962 return Err(io::Error::last_os_error());
963 }
964 let mut file = unsafe { File::from_raw_fd(fd) };
965 file.write_all(pattern)?;
966 // Unlink now: the fd stays valid, no named leftover remains.
967 // SAFETY: `c_path` is still a valid NUL-terminated path.
968 unsafe {
969 libc::unlink(c_path.as_ptr());
970 }
971 Ok(file)
972}
973
974/// Wait for the probe SQE's CQE and return its raw result.
975///
976/// The SQE has already been pushed; this only drains it. `submit_and_wait`
977/// interrupted by a signal returns EINTR — since the kernel consumed the SQE
978/// atomically before the wait phase, we retry the WAIT only and never re-push
979/// (C8, backlog#1059). A bounded attempt count keeps a probe that hit a hung
980/// device from blocking forever; exhausting it returns an error that drives
981/// the caller's leak-over-UAF fallback.
982fn drain_probe_cqe(ring: &mut IoUring) -> io::Result<i32> {
983 const MAX_WAIT_ATTEMPTS: u32 = 4096;
984 for _ in 0..MAX_WAIT_ATTEMPTS {
985 match ring.submit_and_wait(1) {
986 Ok(_) => {}
987 // Signal interrupted the wait; the SQE is already in flight, so
988 // just wait again (do NOT re-push).
989 Err(e) if e.raw_os_error() == Some(libc::EINTR) => {}
990 Err(e) => return Err(e),
991 }
992 if let Some(cqe) = ring.completion().next() {
993 return Ok(cqe.result());
994 }
995 }
996 Err(io::Error::other("probe: no CQE after bounded wait"))
997}
998
999/// Owns everything the kernel can still be writing into: the ring, the
1000/// pending (orphan) table of in-flight buffers, and the SQE backlog.
1001///
1002/// C2 (rustfs/backlog#1054): the "CQE is the only reclamation point"
1003/// invariant holds only while the driver thread does NOT unwind. On a panic,
1004/// Rust would drop the pending table (freeing every in-flight buffer) while
1005/// the kernel may still write into them → mass UAF; reversing drop order does
1006/// not help because io_uring teardown on ring drop is asynchronous and does
1007/// not wait for in-flight ops. So this type's `Drop` refuses to run field
1008/// destructors during an unwind: it aborts the process first, leaving the
1009/// ring mapped and the buffers allocated (leak over UAF). A storage read path
1010/// silently corrupting memory is worse than a crash.
1011struct DriverState {
1012 ring: IoUring,
1013 pending: HashMap<u64, Pending>,
1014 backlog: VecDeque<io_uring::squeue::Entry>,
1015}
1016
1017impl Drop for DriverState {
1018 fn drop(&mut self) {
1019 if std::thread::panicking() {
1020 // Abort BEFORE any field destructor runs: the ring stays mapped
1021 // and the in-flight buffers stay allocated, so the kernel can
1022 // never write into freed memory.
1023 eprintln!(
1024 "uring-spike driver thread panicked with {} ops in flight; \
1025 aborting to avoid UAF of in-flight buffers",
1026 self.pending.len()
1027 );
1028 std::process::abort();
1029 }
1030 // Normal drop: the shutdown invariant guarantees pending/backlog are
1031 // empty and in_flight == 0, so unmapping the ring here is safe.
1032 }
1033}
1034
1035/// Hand the caller exactly the logical range `[head, head + want)` of the read
1036/// region, truncated to what was actually read (rustfs/backlog#1102).
1037///
1038/// Alignment padding (`buf[..pad]`), the bytes before the logical range
1039/// (`head`), and the block-aligned tail after it never reach the caller — a
1040/// `BitrotReader` expecting an exact shard length would flag padded output as
1041/// corruption. Only bytes the kernel actually wrote are exposed: `avail` is
1042/// clamped to `nread`, so the zero-filled remainder of the buffer stays hidden
1043/// (content hygiene, C12 / rustfs/backlog#1062).
1044fn deliver(p: &mut Pending) -> Vec<u8> {
1045 let avail = p.nread.saturating_sub(p.head).min(p.want);
1046 let start = p.pad + p.head;
1047 // The buffered path (`align == 1`) has `pad == 0` and `head == 0`, so the
1048 // logical range already starts at byte 0 — skip the full-buffer memmove and
1049 // just truncate. Only the O_DIRECT path (nonzero start) needs the shift.
1050 if start != 0 && avail != 0 {
1051 p.buf.copy_within(start..start + avail, 0);
1052 }
1053 p.buf.truncate(avail);
1054 std::mem::take(&mut p.buf)
1055}
1056
1057/// What to do with a pending entry after its CQE (C9, rustfs/backlog#1058).
1058enum ReapStep {
1059 /// The logical read is done: remove the entry and deliver this result.
1060 Finish(io::Result<Vec<u8>>),
1061 /// Short read, not EOF: re-queue this SQE for the remainder; keep the entry.
1062 Resubmit(io_uring::squeue::Entry),
1063}
1064
1065fn drive(
1066 ring: IoUring,
1067 rx: mpsc::Receiver<Msg>,
1068 stats: Arc<DriverStats>,
1069 sem: Arc<Semaphore>,
1070 cq_efd: EventFd,
1071 wake_efd: Arc<EventFd>,
1072) {
1073 let mut state = DriverState {
1074 ring,
1075 pending: HashMap::new(),
1076 backlog: VecDeque::new(),
1077 };
1078 let mut shutting_down = false;
1079 let mut drain_deadline: Option<Instant> = None;
1080
1081 loop {
1082 // Block until a CQE is ready (the ring's registered eventfd), a new
1083 // message arrives (the wakeup eventfd), or the heartbeat elapses —
1084 // this replaces the spike's 200 µs busy-poll (backlog#1102). Draining
1085 // both eventfds after waking keeps them from staying spuriously
1086 // readable; a missed edge is harmless because the CQ/mpsc are re-checked
1087 // unconditionally below.
1088 wait_for_events(&cq_efd, &wake_efd, LOOP_HEARTBEAT);
1089 cq_efd.drain();
1090 wake_efd.drain();
1091
1092 // 1. Intake: drain all queued messages (the wait above did the blocking,
1093 // so this is purely non-blocking).
1094 loop {
1095 let msg = match rx.try_recv() {
1096 Ok(m) => m,
1097 Err(TryRecvError::Empty) => break,
1098 Err(TryRecvError::Disconnected) => {
1099 shutting_down = true;
1100 break;
1101 }
1102 };
1103 match msg {
1104 Msg::Read {
1105 id,
1106 file,
1107 offset,
1108 len,
1109 done,
1110 permit,
1111 align,
1112 } => {
1113 if shutting_down {
1114 let _ = done.send(Err(io::Error::other("uring driver shutting down")));
1115 // The op never became in-flight; dropping `permit` here
1116 // returns it immediately.
1117 drop(permit);
1118 continue;
1119 }
1120 // `submit` already validated this geometry.
1121 let (kernel_offset, head, region_len) =
1122 aligned_geometry(offset, len, align).expect("submit validated the geometry");
1123 // For an O_DIRECT read the kernel needs a block-aligned
1124 // buffer, so over-allocate by `align - 1` and start the read
1125 // region at the first aligned byte inside the allocation.
1126 // For a buffered read this degenerates to `vec![0u8; len]`.
1127 // `submit` already capped `align <= MAX_READ_LEN` and
1128 // `region_len <= MAX_READ_LEN`, so this add cannot overflow;
1129 // the checked form keeps the invariant explicit rather than
1130 // relying on it silently.
1131 let cap = match region_len.checked_add(align - 1) {
1132 Some(cap) => cap,
1133 None => {
1134 let _ = done.send(Err(io::Error::other("aligned O_DIRECT allocation size overflow")));
1135 drop(permit);
1136 continue;
1137 }
1138 };
1139 let mut buf = vec![0u8; cap];
1140 let pad = buf.as_ptr().align_offset(align);
1141 // Runtime guard (not a debug-only assert): if the allocator
1142 // ever returned a block `align_offset` cannot satisfy, refuse
1143 // the read instead of doing UB pointer arithmetic below.
1144 if pad == usize::MAX || pad.checked_add(region_len).is_none_or(|end| end > buf.len()) {
1145 let _ = done.send(Err(io::Error::other("could not align O_DIRECT read buffer")));
1146 drop(permit);
1147 continue;
1148 }
1149
1150 // The raw pointer is captured before `buf` moves into the
1151 // table; moving the Vec never relocates its heap block, and
1152 // the entry is only removed at the CQE. `region_len as u32`
1153 // is lossless: `submit` rejected anything > MAX_READ_LEN.
1154 //
1155 // SAFETY: `pad <= align - 1` and `pad + region_len <=
1156 // buf.len()`, so the pointer stays inside the allocation.
1157 let region_ptr = unsafe { buf.as_mut_ptr().add(pad) };
1158 let sqe = opcode::Read::new(types::Fd(file.as_raw_fd()), region_ptr, region_len as u32)
1159 .offset(kernel_offset)
1160 .build()
1161 .user_data(id);
1162 state.pending.insert(
1163 id,
1164 Pending {
1165 buf,
1166 file,
1167 done: Some(done),
1168 offset: kernel_offset,
1169 nread: 0,
1170 // Released exactly when this entry is removed at the
1171 // final CQE — never at future drop (backlog#1060).
1172 _permit: permit,
1173 pad,
1174 head,
1175 want: len,
1176 region_len,
1177 align,
1178 },
1179 );
1180 stats.submitted.fetch_add(1, Ordering::SeqCst);
1181 stats.in_flight.fetch_add(1, Ordering::SeqCst);
1182 state.backlog.push_back(sqe);
1183 }
1184 Msg::Cancel { id } => {
1185 if state.pending.contains_key(&id) {
1186 state
1187 .backlog
1188 .push_back(opcode::AsyncCancel::new(id).build().user_data(id | CANCEL_BIT));
1189 }
1190 }
1191 Msg::Shutdown => {
1192 shutting_down = true;
1193 for id in state.pending.keys() {
1194 state
1195 .backlog
1196 .push_back(opcode::AsyncCancel::new(*id).build().user_data(*id | CANCEL_BIT));
1197 }
1198 }
1199 }
1200 }
1201
1202 // 2. Push backlog into the SQ (stop when full; retry next turn).
1203 {
1204 let mut sq = state.ring.submission();
1205 while let Some(sqe) = state.backlog.pop_front() {
1206 // SAFETY: read SQEs point into `pending`-owned buffers that
1207 // live until their CQE; cancel SQEs carry no pointers.
1208 if unsafe { sq.push(&sqe) }.is_err() {
1209 state.backlog.push_front(sqe);
1210 break;
1211 }
1212 }
1213 }
1214 match state.ring.submit() {
1215 Ok(_) => {}
1216 Err(e) if e.raw_os_error() == Some(libc::EBUSY) => {
1217 // CQ-overflow backpressure on pre-5.19 NODROP kernels: the
1218 // kernel refuses new submissions until we reap. Keep the
1219 // backlog and reap this turn instead of spinning (C5,
1220 // rustfs/backlog#1056).
1221 }
1222 Err(_) => {
1223 // EINTR and friends: retry on the next loop turn.
1224 }
1225 }
1226
1227 // 3. Reap. A Pending entry (and thus its buffer) is dropped ONLY when
1228 // the logical read finishes; a short read is resubmitted for the
1229 // remainder and the entry stays put (C9, rustfs/backlog#1058).
1230 while let Some(cqe) = state.ring.completion().next() {
1231 let ud = cqe.user_data();
1232 if ud & CANCEL_BIT != 0 {
1233 // Result of the AsyncCancel op itself; the read's own CQE
1234 // (ECANCELED or success) still arrives separately. Record the
1235 // three-state outcome for diagnosability (C4,
1236 // rustfs/backlog#1055): EALREADY means the read is executing
1237 // and cannot be interrupted, i.e. its CQE may never come on a
1238 // hung device — the signal the bounded drain below relies on.
1239 match cqe.result() {
1240 0 => stats.cancel_succeeded.fetch_add(1, Ordering::SeqCst),
1241 r if r == -libc::ENOENT => stats.cancel_not_found.fetch_add(1, Ordering::SeqCst),
1242 r if r == -libc::EALREADY => stats.cancel_already.fetch_add(1, Ordering::SeqCst),
1243 _ => 0,
1244 };
1245 continue;
1246 }
1247 let res = cqe.result();
1248 if !state.pending.contains_key(&ud) {
1249 continue;
1250 }
1251
1252 // Decide the next step while borrowing the entry, then act after
1253 // the borrow ends (finish removes it; resubmit re-queues an SQE).
1254 let step = {
1255 let p = state.pending.get_mut(&ud).expect("checked above");
1256 if res < 0 {
1257 // Error (incl. ECANCELED) terminates the logical read.
1258 ReapStep::Finish(Err(io::Error::from_raw_os_error(-res)))
1259 } else if res == 0 {
1260 // Real EOF: deliver whatever of the logical range was read.
1261 ReapStep::Finish(Ok(deliver(p)))
1262 } else {
1263 p.nread += res as usize;
1264 // Only POSITIONED reads (read_at / read_at_direct, whole-range
1265 // pread contract) resubmit a short read. CURRENT_POSITION
1266 // reads (read_current on pipes/streams) follow read(2)
1267 // semantics: a short read is a valid final result and must be
1268 // delivered as-is — resubmitting would block forever waiting
1269 // for stream data that may never come.
1270 let is_stream = p.offset == CURRENT_POSITION;
1271 let covered = p.nread >= p.head + p.want;
1272 // An O_DIRECT resubmit must stay block-aligned. The kernel
1273 // returns block multiples except at the file tail, so a
1274 // non-multiple means we reached EOF: stop and deliver.
1275 let unaligned_tail = p.align > 1 && !p.nread.is_multiple_of(p.align);
1276 if is_stream || covered || unaligned_tail || p.nread >= p.region_len {
1277 ReapStep::Finish(Ok(deliver(p)))
1278 } else {
1279 // Positioned short read, not EOF: resubmit the remainder
1280 // into the read region. The buffer stays owned by the
1281 // driver and in_flight is unchanged — one logical op.
1282 // For a direct read, `pad + nread` and `offset + nread`
1283 // are both block-aligned, as is `remaining`.
1284 let remaining = p.region_len - p.nread;
1285 // SAFETY: `pad + nread < pad + region_len <= buf.len()`,
1286 // and the buffer lives in the pending table until the CQE.
1287 let ptr = unsafe { p.buf.as_mut_ptr().add(p.pad + p.nread) };
1288 let next_off = p.offset + p.nread as u64;
1289 let sqe = opcode::Read::new(types::Fd(p.file.as_raw_fd()), ptr, remaining as u32)
1290 .offset(next_off)
1291 .build()
1292 .user_data(ud);
1293 ReapStep::Resubmit(sqe)
1294 }
1295 }
1296 };
1297
1298 match step {
1299 ReapStep::Finish(outcome) => {
1300 // Content hygiene (C12, rustfs/backlog#1062): the delivered
1301 // bytes are ⊆ [0, res) — buf was freshly zeroed per op and
1302 // truncated to res. When P3 reuses a driver-owned slab
1303 // across requests, this ⊆ [0, res) property MUST be
1304 // preserved or a previous tenant's object bytes leak.
1305 let mut p = state.pending.remove(&ud).expect("checked above");
1306 match p.done.take().expect("done sender set at submit").send(outcome) {
1307 Ok(()) => stats.delivered.fetch_add(1, Ordering::SeqCst),
1308 // Caller dropped the future: the buffer survived in
1309 // the table until this final CQE and is reclaimed here.
1310 Err(_) => stats.orphan_reclaimed.fetch_add(1, Ordering::SeqCst),
1311 };
1312 stats.in_flight.fetch_sub(1, Ordering::SeqCst);
1313 // `p` (and with it `_permit`) is dropped here, at the CQE
1314 // and pending-table removal — never at future drop (C10,
1315 // rustfs/backlog#1060). No manual release to forget.
1316 }
1317 ReapStep::Resubmit(sqe) => state.backlog.push_back(sqe),
1318 }
1319 }
1320
1321 // Monitor CQ overflow. With NODROP (asserted at probe) the crate's
1322 // submit() auto-flushes the kernel overflow list, so this should stay
1323 // 0; any non-zero value means CQEs were lost — pending entries would
1324 // never be reclaimed. Record it as a fatal signal (C5,
1325 // rustfs/backlog#1056).
1326 let overflow = state.ring.completion().overflow();
1327 if overflow != 0 {
1328 stats.cq_overflow.store(overflow as u64, Ordering::SeqCst);
1329 eprintln!("uring-spike driver: CQ overflow = {overflow}; CQEs lost — treat as fatal in P2");
1330 }
1331
1332 // 4. Exit when drained: the kernel no longer references any buffer, so
1333 // dropping the ring (unmap) is safe. If a hung device keeps a CQE
1334 // from ever arriving, bail out under a bounded deadline instead of
1335 // blocking forever (C4, rustfs/backlog#1055).
1336 if shutting_down {
1337 if state.pending.is_empty() && state.backlog.is_empty() {
1338 // Close the semaphore so any handle still awaiting a permit
1339 // resolves with a driver-gone error instead of hanging.
1340 sem.close();
1341 return; // clean drain: DriverState drops normally, ring unmaps.
1342 }
1343 let deadline = *drain_deadline.get_or_insert_with(|| Instant::now() + DRAIN_TIMEOUT);
1344 if Instant::now() >= deadline {
1345 // A CQE may never arrive (ASYNC_CANCEL cannot interrupt an
1346 // in-execution regular-file read on a hung disk). We must NOT
1347 // unmap the ring or free the still-in-flight buffers — leak the
1348 // whole state (leak over UAF) and exit so shutdown() returns.
1349 eprintln!(
1350 "uring-spike driver: bounded drain timed out with {} ops still in flight; \
1351 leaking ring + buffers to stay memory-safe",
1352 state.pending.len()
1353 );
1354 // Close the semaphore so any handle awaiting a permit resolves
1355 // with a driver-gone error. The leaked pending entries keep
1356 // their permits, which is fine: nothing will wait on them.
1357 sem.close();
1358 std::mem::forget(state);
1359 return;
1360 }
1361 }
1362 // No pacing sleep: `wait_for_events` at the top of the loop blocks until
1363 // the next CQE, message, or heartbeat (backlog#1102).
1364 }
1365}