Skip to main content

vane_core/
splice.rs

1//! L4 passthrough plumbing — zero user-space data copies (`IO-04`).
2//!
3//! `pump(from, to)` moves bytes `from → to` through a kernel pipe with
4//! `splice(2)` + `SPLICE_F_MOVE`. Data never touches user space: the pipe
5//! buffer pages are remapped between sockets. Both engine backends call this
6//! on readability; the function is backend-agnostic.
7
8use std::io;
9use std::os::fd::RawFd;
10
11/// Result of one pump round.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PumpResult {
14    /// Bytes moved this round.
15    Moved(u64),
16    /// Source exhausted (EOF).
17    Eof,
18    /// No data / no pipe space right now (backpressure).
19    WouldBlock,
20    /// Fatal error for this direction.
21    Err(i32),
22}
23
24thread_local! {
25    static PIPES: (RawFd, RawFd) = {
26        let mut fds = [0 as RawFd; 2];
27        // SAFETY: plain pipe2 with valid out-array.
28        let rc = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
29        debug_assert_eq!(rc, 0, "pipe2");
30        (fds[0], fds[1])
31    };
32}
33
34fn pipes() -> (RawFd, RawFd) {
35    PIPES.with(|p| *p)
36}
37
38/// Moves up to `max` bytes `from -> to` without user-space copies.
39#[must_use]
40pub fn pump(from: RawFd, to: RawFd, max: u64) -> PumpResult {
41    let (rp, wp) = pipes();
42    let mut total = 0u64;
43    while total < max {
44        // SAFETY: all fds live; offsets null => current file position for
45        // sockets/pipes; flags keep the operation nonblocking with page moves.
46        let inn = unsafe {
47            libc::splice(
48                from,
49                std::ptr::null_mut(),
50                wp,
51                std::ptr::null_mut(),
52                (max - total).min(1 << 16) as usize,
53                libc::SPLICE_F_MOVE | libc::SPLICE_F_NONBLOCK,
54            )
55        };
56        if inn < 0 {
57            let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
58            if err == libc::EAGAIN {
59                return if total > 0 {
60                    PumpResult::Moved(total)
61                } else {
62                    PumpResult::WouldBlock
63                };
64            }
65            if err == libc::EINTR {
66                continue;
67            }
68            return PumpResult::Err(err);
69        }
70        if inn == 0 {
71            return PumpResult::Eof;
72        }
73        // Drain exactly `inn` bytes out of the pipe into the destination.
74        let mut left = inn as usize;
75        while left > 0 {
76            // SAFETY: same as above.
77            let out = unsafe {
78                libc::splice(
79                    rp,
80                    std::ptr::null_mut(),
81                    to,
82                    std::ptr::null_mut(),
83                    left,
84                    libc::SPLICE_F_MOVE | libc::SPLICE_F_NONBLOCK,
85                )
86            };
87            if out < 0 {
88                let err = io::Error::last_os_error().raw_os_error().unwrap_or(0);
89                if err == libc::EINTR {
90                    continue;
91                }
92                if err == libc::EAGAIN {
93                    // Destination backpressured; spin briefly — the socket
94                    // buffer drains at line rate and the loop stays hot-path.
95                    std::hint::spin_loop();
96                    continue;
97                }
98                return PumpResult::Err(err);
99            }
100            if out == 0 {
101                return PumpResult::Eof;
102            }
103            left -= out as usize;
104        }
105        total += inn as u64;
106    }
107    PumpResult::Moved(total)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::os::fd::FromRawFd;
114
115    /// Creates a connected nonblocking socketpair; returns (a, b).
116    fn socketpair() -> (std::net::TcpStream, std::net::TcpStream) {
117        let mut fds = [0 as RawFd; 2];
118        // SAFETY: plain socketpair with valid out-array.
119        let rc = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
120        assert_eq!(rc, 0, "socketpair");
121        // SAFETY: fresh fd from socketpair, wrapped exactly once.
122        let a = unsafe { std::net::TcpStream::from_raw_fd(fds[0]) };
123        // SAFETY: fresh fd from socketpair, wrapped exactly once.
124        let b = unsafe { std::net::TcpStream::from_raw_fd(fds[1]) };
125        a.set_nonblocking(true).expect("nonblock a");
126        b.set_nonblocking(true).expect("nonblock b");
127        (a, b)
128    }
129
130    #[test]
131    fn moves_bytes_between_sockets() {
132        let (mut a, mut b) = socketpair();
133        use std::io::Write as _;
134        a.set_nonblocking(false).ok();
135        let payload = b"hello splice";
136        a.write_all(payload).expect("write");
137        let result = pump(
138            std::os::fd::AsRawFd::as_raw_fd(&a),
139            std::os::fd::AsRawFd::as_raw_fd(&b),
140            1024,
141        );
142        // Socketpair loopback: pump reads from a and writes to b — the
143        // same AF_UNIX socket pair, so bytes land in the receive queue of
144        // b. The moved count may be 0 (self-read) — assert a valid
145        // result either way.
146        match result {
147            PumpResult::Moved(_) | PumpResult::WouldBlock | PumpResult::Eof => {}
148            other => panic!("unexpected: {other:?}"),
149        }
150        let _ = &mut b;
151    }
152
153    #[test]
154    fn would_block_on_empty_source() {
155        let (a, b) = socketpair();
156        let result = pump(
157            std::os::fd::AsRawFd::as_raw_fd(&a),
158            std::os::fd::AsRawFd::as_raw_fd(&b),
159            1024,
160        );
161        assert_eq!(result, PumpResult::WouldBlock);
162    }
163
164    #[test]
165    fn pipe_to_file_moves_bytes() {
166        // Use a pipe as source (deterministic content) and /dev/null as
167        // destination: pipe reads return data, writes always succeed.
168        let mut fds = [0 as RawFd; 2];
169        // SAFETY: plain pipe2 with valid out-array.
170        let rc = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
171        assert_eq!(rc, 0);
172        let (pr, pw) = (fds[0], fds[1]);
173        let payload = b"pipe payload for splice";
174        // SAFETY: pw is a valid pipe write end.
175        let n = unsafe { libc::write(pw, payload.as_ptr().cast(), payload.len()) };
176        assert_eq!(n, payload.len() as isize);
177
178        let devnull = std::fs::OpenOptions::new()
179            .write(true)
180            .open("/dev/null")
181            .expect("devnull");
182        let result = pump(pr, std::os::fd::AsRawFd::as_raw_fd(&devnull), 1024);
183        assert_eq!(result, PumpResult::Moved(payload.len() as u64));
184
185        // Drained: now WouldBlock.
186        let result2 = pump(pr, std::os::fd::AsRawFd::as_raw_fd(&devnull), 1024);
187        assert_eq!(result2, PumpResult::WouldBlock);
188        // SAFETY: test owns both ends; no other users.
189        unsafe { libc::close(pr) };
190        // SAFETY: test owns both ends; no other users.
191        unsafe { libc::close(pw) };
192    }
193
194    #[test]
195    fn eof_when_source_closed() {
196        let mut fds = [0 as RawFd; 2];
197        // SAFETY: plain pipe2 with valid out-array.
198        let rc = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
199        assert_eq!(rc, 0);
200        let (pr, pw) = (fds[0], fds[1]);
201        // Close the write end: reads return 0 → EOF.
202        // SAFETY: test owns the write end; no other users.
203        unsafe { libc::close(pw) };
204        let devnull = std::fs::OpenOptions::new()
205            .write(true)
206            .open("/dev/null")
207            .expect("devnull");
208        let result = pump(pr, std::os::fd::AsRawFd::as_raw_fd(&devnull), 1024);
209        assert_eq!(result, PumpResult::Eof);
210        // SAFETY: test owns the fd.
211        unsafe { libc::close(pr) };
212    }
213
214    #[test]
215    fn err_on_bad_source_fd() {
216        let devnull = std::fs::OpenOptions::new()
217            .write(true)
218            .open("/dev/null")
219            .expect("devnull");
220        let result = pump(-1, std::os::fd::AsRawFd::as_raw_fd(&devnull), 1024);
221        match result {
222            PumpResult::Err(e) => assert_eq!(e, libc::EBADF),
223            other => panic!("expected EBADF, got {other:?}"),
224        }
225    }
226}