Skip to main content

vane_core/engine/
uring.rs

1//! io_uring backend (`IO-01`..`03`): one ring per worker, fixed registered
2//! buffers, SQPOLL optional.
3//!
4//! Reads use `IORING_OP_READ_FIXED` (`opcode::ReadFixed`) with
5//! `buf_index = slot`, so the kernel writes directly into the worker's
6//! pre-allocated pool — zero per-op buffer mapping. Writes use
7//! `WRITE_FIXED` symmetrically. L4 splice pumping arms multishot `PollAdd`
8//! readiness and runs kernel-only `splice(2)` loops inline — request bytes
9//! never enter user space (`IO-04`).
10
11use std::collections::HashMap;
12use std::io;
13use std::net::SocketAddr;
14use std::os::fd::{IntoRawFd, RawFd};
15use std::path::Path;
16use std::time::Duration;
17
18use io_uring::types::{Fd, SubmitArgs, Timespec};
19
20use super::{Cqe, Engine, Poll};
21use crate::buffer::BufferPool;
22use crate::splice;
23use crate::token::Token;
24
25/// Listener state (accept re-arms after each connection).
26struct Listener {
27    fd: RawFd,
28    #[allow(dead_code)] // re-arm identity (kept for symmetry with mio)
29    token: Token,
30    /// sockaddr output buffer the kernel fills for each accepted connection.
31    sa: Box<[u8; 128]>,
32    sa_len: Box<libc::socklen_t>,
33}
34
35/// io_uring-backed [`Engine`].
36pub struct UringEngine {
37    ring: io_uring::IoUring,
38    /// Slot base pointers (registered as kernel fixed buffers).
39    slot_bases: Vec<*mut u8>,
40    buf_size: usize,
41    listeners: HashMap<u64, Listener>,
42    /// Splice direction per token: bits -> (from_fd, to_fd).
43    splice_dirs: HashMap<u64, (RawFd, RawFd)>,
44    /// Completed accepts awaiting pickup: fd -> peer.
45    accepted: HashMap<RawFd, SocketAddr>,
46    /// Owned sockaddr storage per in-flight connect (token bits -> (addr, len)).
47    /// io_uring copies the sockaddr at SUBMISSION time, not at SQE build
48    /// time — a stack-local sockaddr would dangle between `push` and
49    /// `submit` (use-after-free manifesting as EAFNOSUPPORT under load).
50    connect_addrs: HashMap<u64, Box<ConnectAddr>>,
51}
52
53/// Owned connect address for one in-flight `Connect` SQE.
54struct ConnectAddr {
55    storage: libc::sockaddr_storage,
56    len: libc::socklen_t,
57}
58
59/// Serializes a `SocketAddr` into owned storage, returning the box plus a
60/// pointer/len pair valid for as long as the box lives.
61///
62/// # Safety
63/// The caller must keep the returned box alive (in `connect_addrs`) until
64/// the op completes. The heap address is stable across moves.
65unsafe fn connect_addr_boxed(
66    addr: SocketAddr,
67) -> (Box<ConnectAddr>, *const libc::sockaddr, libc::socklen_t) {
68    // SAFETY: fully initialized for the active family below.
69    let mut storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
70    let len = match addr {
71        SocketAddr::V4(v4) => {
72            // SAFETY: family matches the written layout.
73            let sa: &mut libc::sockaddr_in =
74                unsafe { &mut *std::ptr::addr_of_mut!(storage).cast::<libc::sockaddr_in>() };
75            sa.sin_family = libc::AF_INET as _;
76            sa.sin_port = v4.port().to_be();
77            sa.sin_addr.s_addr = u32::from_ne_bytes(v4.ip().octets());
78            std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t
79        }
80        SocketAddr::V6(v6) => {
81            // SAFETY: family matches the written layout.
82            let sa: &mut libc::sockaddr_in6 =
83                unsafe { &mut *std::ptr::addr_of_mut!(storage).cast::<libc::sockaddr_in6>() };
84            sa.sin6_family = libc::AF_INET6 as _;
85            sa.sin6_port = v6.port().to_be();
86            sa.sin6_addr.s6_addr = v6.ip().octets();
87            std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t
88        }
89    };
90    let boxed = Box::new(ConnectAddr { storage, len });
91    // The heap address is stable for the box's lifetime (per the function
92    // contract the caller stores it in `connect_addrs`); `addr_of!` on a
93    // place expression needs no unsafe block.
94    let ptr = std::ptr::addr_of!(boxed.storage).cast::<libc::sockaddr>();
95    let out_len = boxed.len;
96    (boxed, ptr, out_len)
97}
98
99// SAFETY: slot pointers live in the worker-owned pool; single-thread use.
100unsafe impl Send for UringEngine {}
101
102impl UringEngine {
103    /// Builds the ring and registers the buffer pool as kernel fixed buffers.
104    ///
105    /// # Errors
106    /// Ring creation or registration failure (e.g., SQPOLL denied for the
107    /// current user — the runtime falls back per `IO-05`).
108    pub fn new(entries: u32, pool: Option<&BufferPool>, sqpoll: bool) -> io::Result<Self> {
109        let ring = if sqpoll {
110            io_uring::IoUring::builder()
111                .setup_sqpoll(2_000)
112                .build(entries)?
113        } else {
114            io_uring::IoUring::new(entries)?
115        };
116        let (slot_bases, buf_size) = match pool {
117            Some(pool) => {
118                // Base addresses only (no dereference); slots outlive the ring.
119                let bases = (0..pool.capacity() as u32)
120                    .map(|i| pool.slot(i).as_ptr() as *mut u8)
121                    .collect::<Vec<_>>();
122                let iovecs: Vec<libc::iovec> = bases
123                    .iter()
124                    .map(|p| libc::iovec {
125                        // SAFETY: pointer valid for buf_size bytes.
126                        iov_base: (*p).cast(),
127                        iov_len: pool.buf_size(),
128                    })
129                    .collect();
130                // SAFETY: iovecs reference stable slot storage.
131                unsafe {
132                    ring.submitter().register_buffers(&iovecs)?;
133                }
134                (bases, pool.buf_size())
135            }
136            None => (Vec::new(), 0),
137        };
138        Ok(Self {
139            ring,
140            slot_bases,
141            buf_size,
142            listeners: HashMap::new(),
143            splice_dirs: HashMap::new(),
144            accepted: HashMap::new(),
145            connect_addrs: HashMap::new(),
146        })
147    }
148
149    /// Queues an SQE (no syscall); `poll` batches the submit. Under SQPOLL
150    /// the kernel thread picks entries up without any syscall at all.
151    ///
152    /// Caller contract (checked at each call site): the entry's buffers and
153    /// fds must remain valid until its CQE is consumed on this thread.
154    fn push(&mut self, entry: io_uring::squeue::Entry, token: Token) {
155        let entry = entry.user_data(token.bits());
156        loop {
157            // SAFETY: entry pushed exactly once; completion consumed here.
158            unsafe {
159                if self.ring.submission().push(&entry).is_ok() {
160                    return;
161                }
162            }
163            // SQ full: flush to the kernel and retry.
164            let _ = self.ring.submit();
165            std::hint::spin_loop();
166        }
167    }
168
169    fn slot_ptr(&self, slot: u32) -> *mut u8 {
170        self.slot_bases[slot as usize]
171    }
172
173    fn arm_accept(&mut self, bits: u64) {
174        let Some(l) = self.listeners.get_mut(&bits) else {
175            return;
176        };
177        let fd = l.fd;
178        let sa_ptr = l.sa.as_mut_ptr();
179        let len_ptr: *mut libc::socklen_t = &mut *l.sa_len;
180        // SAFETY contract for `push`: sa/sa_len are stable worker-owned
181        // buffers, valid until the CQE is consumed on this thread.
182        let entry = io_uring::opcode::Accept::new(Fd(fd), sa_ptr.cast(), len_ptr)
183            .flags(libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC)
184            .build();
185        self.push(entry, Token::from_bits(bits));
186    }
187
188    fn arm_readiness(&mut self, fd: RawFd, token: Token) {
189        // SAFETY contract for `push`: fd live until session close; multishot
190        // poll re-arms itself.
191        let entry = io_uring::opcode::PollAdd::new(Fd(fd), libc::POLLIN as u32)
192            .multi(true)
193            .build();
194        self.push(entry, token);
195    }
196}
197
198impl Engine for UringEngine {
199    fn kind(&self) -> &'static str {
200        "io_uring"
201    }
202
203    fn add_listener(&mut self, fd: RawFd, token: Token) -> io::Result<()> {
204        let bits = token.bits();
205        self.listeners.insert(
206            bits,
207            Listener {
208                fd,
209                token,
210                sa: Box::new([0u8; 128]),
211                sa_len: Box::new(128),
212            },
213        );
214        self.arm_accept(bits);
215        Ok(())
216    }
217
218    fn add_stream(&mut self, _fd: RawFd, _token: Token) -> io::Result<()> {
219        // Connected sockets pass per-op; IORING_REGISTER_FILES is a
220        // follow-up optimization needing stable fd slots per session.
221        Ok(())
222    }
223
224    fn read(&mut self, token: Token, fd: RawFd, slot: u32) -> io::Result<Poll> {
225        let ptr = self.slot_ptr(slot);
226        // SAFETY contract for `push`: the fixed-buffer slot is exclusively
227        // owned while the op is in flight; the kernel writes the registered
228        // buffer directly.
229        let entry =
230            io_uring::opcode::ReadFixed::new(Fd(fd), ptr, self.buf_size as u32, slot as u16)
231                .offset(0)
232                .build();
233        self.push(entry, token);
234        Ok(Poll::Pending)
235    }
236
237    fn write(
238        &mut self,
239        token: Token,
240        fd: RawFd,
241        slot: u32,
242        len: usize,
243        offset: usize,
244    ) -> io::Result<Poll> {
245        let ptr = self.slot_ptr(slot);
246        // SAFETY: fixed buffer (bytes serialized by the session pre-submit);
247        // pointer arithmetic stays within the registered slot.
248        let entry = unsafe {
249            io_uring::opcode::WriteFixed::new(
250                Fd(fd),
251                ptr.add(offset),
252                (len - offset) as u32,
253                slot as u16,
254            )
255            .offset(0)
256            .build()
257        };
258        self.push(entry, token);
259        Ok(Poll::Pending)
260    }
261
262    fn connect(&mut self, token: Token, addr: SocketAddr) -> io::Result<(RawFd, Poll)> {
263        let domain = if addr.is_ipv4() {
264            socket2::Domain::IPV4
265        } else {
266            socket2::Domain::IPV6
267        };
268        let sock =
269            socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP))?;
270        sock.set_nonblocking(true)?;
271        sock.set_tcp_nodelay(true)?;
272        let fd = sock.into_raw_fd();
273        // The sockaddr must live in owned storage until the op completes:
274        // io_uring copies it at submit time, not when the SQE is built.
275        // SAFETY: the box is stored in `connect_addrs` for the op lifetime.
276        let (owned, ptr, len) = unsafe { connect_addr_boxed(addr) };
277        let entry = io_uring::opcode::Connect::new(Fd(fd), ptr, len).build();
278        self.connect_addrs.insert(token.bits(), owned);
279        self.push(entry, token);
280        Ok((fd, Poll::Pending))
281    }
282
283    fn connect_unix(&mut self, token: Token, path: &Path) -> io::Result<(RawFd, Poll)> {
284        let sock = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)?;
285        sock.set_nonblocking(true)?;
286        let fd = sock.into_raw_fd();
287        let sa = socket2::SockAddr::unix(path)?;
288        // SAFETY: raw sockaddr bytes are copied into owned storage, kept in
289        // `connect_addrs` for the op lifetime.
290        let mut storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
291        let bytes = sa.as_ptr().cast::<u8>();
292        let copy_len = (sa.len() as usize).min(std::mem::size_of::<libc::sockaddr_storage>());
293        // SAFETY: sa is a valid sockaddr of `sa.len()` bytes.
294        unsafe {
295            std::ptr::copy_nonoverlapping(bytes, std::ptr::addr_of_mut!(storage).cast(), copy_len)
296        };
297        let len = sa.len();
298        let owned = Box::new(ConnectAddr { storage, len });
299        // Heap address is stable while `owned` lives in `connect_addrs`.
300        let ptr = std::ptr::addr_of!(owned.storage).cast::<libc::sockaddr>();
301        let entry = io_uring::opcode::Connect::new(Fd(fd), ptr, len).build();
302        self.connect_addrs.insert(token.bits(), owned);
303        self.push(entry, token);
304        Ok((fd, Poll::Pending))
305    }
306
307    fn accept(&mut self, _lfd: RawFd, ltoken: Token) -> io::Result<Option<(RawFd, SocketAddr)>> {
308        // Return one completed accept if the poll loop queued it.
309        let keys: Vec<RawFd> = self.accepted.keys().copied().collect();
310        if let Some(fd) = keys.into_iter().next() {
311            let addr = self.accepted.remove(&fd).expect("just listed");
312            return Ok(Some((fd, addr)));
313        }
314        // Not ready yet. Do NOT re-arm here: exactly one accept SQE is
315        // outstanding per listener at all times (armed in `add_listener`,
316        // re-armed on every completion in `poll`). Re-arming per call would
317        // accumulate unbounded SQEs and exhaust the submission queue.
318        let _ = ltoken;
319        Ok(None)
320    }
321
322    fn splice_pump(&mut self, a: Token, afd: i32, b: Token, bfd: i32) -> io::Result<()> {
323        // Directions are keyed by token bits: identical tokens would
324        // silently overwrite one direction.
325        debug_assert_ne!(a.bits(), b.bits(), "splice directions need distinct tokens");
326        self.splice_dirs.insert(a.bits(), (afd, bfd));
327        self.splice_dirs.insert(b.bits(), (bfd, afd));
328        self.arm_readiness(afd, a);
329        self.arm_readiness(bfd, b);
330        Ok(())
331    }
332
333    fn remove(&mut self, fd: RawFd) {
334        self.accepted.remove(&fd);
335    }
336
337    fn poll(&mut self, timeout: Option<Duration>, out: &mut Vec<Cqe>) -> io::Result<()> {
338        // Flush submissions (no-op under SQPOLL — kernel thread drains).
339        self.ring.submit()?;
340
341        match timeout {
342            None => match self.ring.submit_and_wait(1) {
343                Ok(_) => {}
344                Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
345                Err(e) => return Err(e),
346            },
347            Some(d) => {
348                // Bounded wait via IORING_ENTER_EXT_ARG; ETIME = clean timeout.
349                let ms = d.as_millis().min(60_000) as u64;
350                let ts = Timespec::new()
351                    .sec(ms / 1_000)
352                    .nsec((ms % 1_000) as u32 * 1_000_000);
353                let args = SubmitArgs::new().timespec(&ts);
354                match self.ring.submitter().submit_with_args(1, &args) {
355                    Ok(_) => {}
356                    Err(e) if e.raw_os_error() == Some(libc::ETIME) => return Ok(()),
357                    Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
358                    Err(_) => {
359                        // Fallback for kernels without EXT_ARG: plain wait.
360                        self.ring.submit_and_wait(0)?;
361                    }
362                }
363            }
364        }
365
366        // Drain completions.
367        let mut accept_hits: Vec<(Token, RawFd)> = Vec::new();
368        for cqe in self.ring.completion() {
369            let token = Token::from_bits(cqe.user_data());
370            let raw = cqe.result();
371            let result = if raw >= 0 {
372                Ok(raw as u32)
373            } else {
374                Err(io::Error::from_raw_os_error(-raw))
375            };
376            // In-flight connect storage is safe to release once its CQE
377            // has been observed.
378            if token.op() == crate::token::Op::Connect {
379                self.connect_addrs.remove(&token.bits());
380            }
381            match token.op() {
382                crate::token::Op::Accept => {
383                    if raw >= 0 {
384                        accept_hits.push((token, raw as RawFd));
385                    } else if -raw != libc::ECANCELED {
386                        out.push(Cqe { token, result });
387                    }
388                }
389                crate::token::Op::Splice => {
390                    if raw < 0 {
391                        if -raw != libc::ECANCELED {
392                            out.push(Cqe { token, result });
393                        }
394                    } else if let Some(&(from, to)) = self.splice_dirs.get(&token.bits()) {
395                        match splice::pump(from, to, 1 << 20) {
396                            splice::PumpResult::Moved(n) => {
397                                out.push(Cqe {
398                                    token,
399                                    result: Ok(n as u32),
400                                });
401                            }
402                            splice::PumpResult::Eof => {
403                                out.push(Cqe {
404                                    token,
405                                    result: Ok(0),
406                                });
407                            }
408                            splice::PumpResult::WouldBlock => {}
409                            splice::PumpResult::Err(code) => out.push(Cqe {
410                                token,
411                                result: Err(io::Error::from_raw_os_error(code)),
412                            }),
413                        }
414                    }
415                }
416                _ => out.push(Cqe { token, result }),
417            }
418        }
419
420        // Materialize accepted connections and re-arm listeners.
421        for (token, fd) in accept_hits {
422            let bits = token.bits();
423            let addr = self.listeners.get(&bits).map_or_else(
424                || SocketAddr::from(([0, 0, 0, 0], 0)),
425                |l| parse_sockaddr(&l.sa),
426            );
427            self.accepted.insert(fd, addr);
428            self.arm_accept(bits);
429        }
430        Ok(())
431    }
432
433    fn take_accepted(&mut self, fd: RawFd) -> Option<SocketAddr> {
434        self.accepted.remove(&fd)
435    }
436}
437
438fn parse_sockaddr(buf: &[u8; 128]) -> SocketAddr {
439    // SAFETY: buffer is sockaddr_storage sized.
440    let sa: &libc::sockaddr_storage = unsafe { &*buf.as_ptr().cast() };
441    match sa.ss_family as i32 {
442        libc::AF_INET => {
443            // SAFETY: AF_INET guarantees sockaddr_in layout.
444            let a: &libc::sockaddr_in =
445                unsafe { &*(sa as *const libc::sockaddr_storage).cast::<libc::sockaddr_in>() };
446            SocketAddr::from((
447                std::net::Ipv4Addr::from(u32::from_be(a.sin_addr.s_addr)),
448                u16::from_be(a.sin_port),
449            ))
450        }
451        _ => {
452            // SAFETY: AF_INET6 guarantees sockaddr_in6 layout.
453            let a: &libc::sockaddr_in6 =
454                unsafe { &*(sa as *const libc::sockaddr_storage).cast::<libc::sockaddr_in6>() };
455            SocketAddr::from((
456                std::net::Ipv6Addr::from(a.sin6_addr.s6_addr),
457                u16::from_be(a.sin6_port),
458            ))
459        }
460    }
461}
462
463#[cfg(test)]
464mod fault_tests {
465    use super::*;
466    use crate::buffer::DEFAULT_BUF_SIZE;
467    use crate::token::Op;
468
469    fn test_engine() -> (BufferPool, UringEngine) {
470        let pool = BufferPool::new(8, DEFAULT_BUF_SIZE).expect("pool");
471        let engine = UringEngine::new(64, Some(&pool), false).expect("uring available");
472        (pool, engine)
473    }
474
475    fn tok(op: Op) -> Token {
476        Token::new(op, 0, 0, 0)
477    }
478
479    fn sockpair() -> (RawFd, RawFd) {
480        let mut fds = [0 as RawFd; 2];
481        // SAFETY: plain socketpair with valid out-array.
482        let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
483        assert_eq!(rc, 0);
484        for fd in fds {
485            // SAFETY: fcntl on a live fd.
486            let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
487            // SAFETY: same live fd; only adds O_NONBLOCK.
488            unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
489        }
490        (fds[0], fds[1])
491    }
492
493    fn close(fd: RawFd) {
494        // SAFETY: test owns the fd.
495        unsafe { libc::close(fd) };
496    }
497
498    fn drain(engine: &mut UringEngine, secs: u64) -> Vec<Cqe> {
499        let mut out = Vec::new();
500        engine
501            .poll(Some(std::time::Duration::from_secs(secs)), &mut out)
502            .expect("poll");
503        out
504    }
505
506    /// Polls until `pred` matches a CQE or the attempt budget runs out
507    /// (kernels race completion delivery against `submit_and_wait`).
508    fn drain_until(engine: &mut UringEngine, mut pred: impl FnMut(&Cqe) -> bool) -> Vec<Cqe> {
509        let mut all = Vec::new();
510        for _ in 0..60 {
511            let mut out = Vec::new();
512            engine
513                .poll(Some(std::time::Duration::from_millis(100)), &mut out)
514                .expect("poll");
515            if out.iter().any(&mut pred) {
516                all.extend(out);
517                return all;
518            }
519            all.extend(out);
520        }
521        all
522    }
523
524    #[test]
525    fn write_then_read_roundtrip() {
526        let (_pool, mut engine) = test_engine();
527        let (a, b) = sockpair();
528        let wt = tok(Op::DownstreamWrite);
529        assert!(matches!(
530            engine.write(wt, a, 0, 32, 0).expect("write"),
531            Poll::Pending
532        ));
533        let cqes = drain(&mut engine, 2);
534        assert!(
535            cqes.iter().any(|c| c.token == wt && c.result.is_ok()),
536            "write CQE missing: {cqes:?}"
537        );
538        // Read the bytes back through the ring into slot 1.
539        let rt = tok(Op::DownstreamRead);
540        assert!(matches!(
541            engine.read(rt, b, 1).expect("read"),
542            Poll::Pending
543        ));
544        let cqes = drain(&mut engine, 2);
545        let got = cqes.iter().find(|c| c.token == rt).expect("read CQE");
546        assert!(matches!(got.result, Ok(32)));
547        close(a);
548        close(b);
549    }
550
551    #[test]
552    fn connect_refused_completes_with_error() {
553        let (_pool, mut engine) = test_engine();
554        let addr: SocketAddr = "127.0.0.1:1".parse().expect("addr");
555        let t = tok(Op::Connect);
556        let (fd, poll) = engine.connect(t, addr).expect("connect issued");
557        match poll {
558            Poll::Done(_) => {}
559            Poll::Pending => {
560                let cqes = drain_until(&mut engine, |c| c.token == t);
561                assert!(
562                    cqes.iter().any(|c| c.token == t && c.result.is_err()),
563                    "refused connect must error: {cqes:?}"
564                );
565            }
566        }
567        close(fd);
568    }
569
570    #[test]
571    fn connect_unix_missing_path_errors() {
572        let (_pool, mut engine) = test_engine();
573        let t = tok(Op::Connect);
574        let dir = tempfile::tempdir().expect("dir");
575        let missing = dir.path().join("no.sock");
576        let res = engine.connect_unix(t, &missing);
577        assert!(res.is_err() || matches!(res, Ok((_, Poll::Pending))));
578    }
579
580    #[test]
581    fn accept_flow_materializes_connection() {
582        let (_pool, mut engine) = test_engine();
583        let listener =
584            crate::tcp_listener("127.0.0.1:0".parse().expect("addr"), true, 64).expect("bind");
585        let lfd = std::os::fd::AsRawFd::as_raw_fd(&listener);
586        engine.add_listener(lfd, Token::accept(0)).expect("add");
587        // No client yet: accept reports None (single outstanding SQE).
588        assert!(
589            engine
590                .accept(lfd, Token::accept(0))
591                .expect("accept")
592                .is_none()
593        );
594        let addr = listener.local_addr().expect("addr");
595        let _client = std::net::TcpStream::connect(addr).expect("connect");
596        // Accept completions land in the engine's accepted map (not the
597        // CQE out-vec): poll until the retrieval API reports the peer.
598        let mut got = None;
599        for _ in 0..60 {
600            let mut out = Vec::new();
601            engine
602                .poll(Some(std::time::Duration::from_millis(100)), &mut out)
603                .expect("poll");
604            got = engine.accept(lfd, Token::accept(0)).expect("accept2");
605            if got.is_some() {
606                break;
607            }
608        }
609        // The completed accept is retrievable through the engine API.
610        assert!(got.is_some(), "materialized connection expected");
611        let (fd, peer) = got.expect("some");
612        assert!(peer.port() != 0);
613        close(fd);
614    }
615
616    #[test]
617    fn splice_moved_and_eof_paths() {
618        let (_pool, mut engine) = test_engine();
619        let mut fds = [0 as RawFd; 2];
620        // SAFETY: plain pipe2 with valid out-array.
621        assert_eq!(
622            // SAFETY: out-array is a valid 2-element fd buffer.
623            unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) },
624            0
625        );
626        let (pr, pw) = (fds[0], fds[1]);
627        let payload = b"uring-splice";
628        // SAFETY: write to a live pipe.
629        unsafe { libc::write(pw, payload.as_ptr().cast(), payload.len()) };
630        let (sa, sb) = sockpair();
631        let t = tok(Op::Splice);
632        let t_rev = Token::new(Op::Splice, 0, 0, 1);
633        engine.splice_pump(t, pr, t_rev, sb).expect("splice_pump");
634        let cqes = drain_until(&mut engine, |c| c.token == t);
635        assert!(
636            cqes.iter()
637                .any(|c| c.token == t && matches!(c.result, Ok(n) if n as usize == payload.len())),
638            "splice moved CQE: {cqes:?}"
639        );
640        // Drain the pipe, then the pump must report EOF.
641        let mut buf = [0u8; 64];
642        // SAFETY: read into a live buffer.
643        let n = unsafe { libc::read(sa, buf.as_mut_ptr().cast(), 64) };
644        assert_eq!(&buf[..n as usize], payload);
645        close(pw); // writer gone: next pump sees EOF
646        let t2 = Token::new(Op::Splice, 1, 0, 0);
647        let t2_rev = Token::new(Op::Splice, 1, 0, 1);
648        engine
649            .splice_pump(t2, pr, t2_rev, sb)
650            .expect("splice_pump2");
651        let cqes = drain_until(&mut engine, |c| c.token == t2);
652        assert!(
653            cqes.iter()
654                .any(|c| c.token == t2 && matches!(c.result, Ok(0))),
655            "splice EOF CQE: {cqes:?}"
656        );
657        close(pr);
658        close(sa);
659        close(sb);
660    }
661
662    #[test]
663    fn remove_clears_tracked_fd() {
664        let (_pool, mut engine) = test_engine();
665        let (a, b) = sockpair();
666        engine
667            .add_stream(a, tok(Op::DownstreamRead))
668            .expect("add_stream");
669        engine.remove(a);
670        // No panic; op state dropped.
671        close(a);
672        close(b);
673    }
674}