Skip to main content

native_ipc_platform/
linux.rs

1//! Linux sealed-memfd mappings and authenticated descriptor transfer.
2
3use std::ffi::OsStr;
4use std::fmt;
5use std::io;
6use std::mem::{size_of, zeroed};
7use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
8use std::os::unix::fs::PermissionsExt;
9use std::os::unix::net::UnixListener;
10use std::os::unix::net::UnixStream;
11use std::path::PathBuf;
12use std::process::{Child, Command};
13use std::ptr::NonNull;
14use std::time::{Duration, Instant};
15
16use native_ipc_core::layout::{RegionSetLayout, ValidatedRegionLayout, ValidationExpectations};
17use native_ipc_core::mapping::{
18    BindingError, ReadOnlyMapping, ReaderRegion, SoleWriterMapping, WriterRegion,
19};
20
21use crate::BackendStatus;
22
23const REQUIRED_SEALS: libc::c_int =
24    libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_FUTURE_WRITE | libc::F_SEAL_SEAL;
25const FRAME_MAGIC: [u8; 8] = *b"NIPCFD\0\0";
26const FRAME_LEN: usize = 48;
27const TMPFS_MAGIC: libc::c_long = 0x0102_1994;
28const ENV_SOCKET: &str = "NATIVE_IPC_LINUX_SOCKET";
29const ENV_NONCE: &str = "NATIVE_IPC_SESSION_NONCE";
30const ENV_PARENT_PID: &str = "NATIVE_IPC_PARENT_PID";
31
32/// Linux mapping, descriptor-transfer, or peer-authentication failure.
33#[derive(Debug)]
34pub enum LinuxError {
35    /// A native syscall failed with the captured errno.
36    Os {
37        /// Bounded syscall name.
38        operation: &'static str,
39        /// Captured platform errno.
40        code: i32,
41    },
42    /// Requested mapping size is zero or cannot be page-rounded.
43    InvalidSize(usize),
44    /// Received peer credentials differ from the expected live process.
45    WrongPeer,
46    /// Descriptor transfer frame was truncated, malformed, or stale.
47    InvalidFrame,
48    /// Received descriptor count or ancillary type was not exactly one SCM_RIGHTS fd.
49    InvalidAncillaryData,
50    /// Received capability lacks the exact anonymous sealed-shmem policy.
51    InvalidCapability,
52    /// Private bootstrap path, environment, or child process setup failed.
53    Bootstrap,
54    /// Quiescent core layout validation failed.
55    Layout(native_ipc_core::layout::LayoutError),
56    /// Audited core binding failed.
57    Binding(BindingError),
58}
59
60/// Owned exact child process, authenticated control channel, and cleanup ledger.
61pub struct ChildSession {
62    child: Child,
63    channel: AuthenticatedChannel,
64    bootstrap_dir: PathBuf,
65}
66
67impl ChildSession {
68    /// Spawns an exact helper executable and authenticates its post-exec connection.
69    pub fn spawn(program: &OsStr, arguments: &[&OsStr]) -> Result<Self, LinuxError> {
70        let mut nonce = [0_u8; 32];
71        // SAFETY: output buffer is valid and flags zero request blocking system RNG.
72        if unsafe { libc::getrandom(nonce.as_mut_ptr().cast(), nonce.len(), 0) }
73            != nonce.len() as isize
74        {
75            return Err(last_os("getrandom"));
76        }
77        let suffix = hex(&nonce[..16]);
78        let bootstrap_dir = std::env::temp_dir().join(format!("native-ipc-{suffix}"));
79        std::fs::create_dir(&bootstrap_dir).map_err(|_| LinuxError::Bootstrap)?;
80        std::fs::set_permissions(&bootstrap_dir, std::fs::Permissions::from_mode(0o700))
81            .map_err(|_| LinuxError::Bootstrap)?;
82        let socket_path = bootstrap_dir.join("control.sock");
83        let listener = UnixListener::bind(&socket_path).map_err(|_| LinuxError::Bootstrap)?;
84        std::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(0o600))
85            .map_err(|_| LinuxError::Bootstrap)?;
86        listener
87            .set_nonblocking(true)
88            .map_err(|_| LinuxError::Bootstrap)?;
89
90        let mut command = Command::new(program);
91        command
92            .args(arguments)
93            .env(ENV_SOCKET, &socket_path)
94            .env(ENV_NONCE, hex(&nonce))
95            .env(ENV_PARENT_PID, std::process::id().to_string());
96        let mut child = command.spawn().map_err(|_| LinuxError::Bootstrap)?;
97        let expected = PeerCredentials {
98            pid: child.id(),
99            // SAFETY: scalar identity syscalls have no preconditions.
100            uid: unsafe { libc::geteuid() },
101            // SAFETY: scalar identity syscalls have no preconditions.
102            gid: unsafe { libc::getegid() },
103        };
104        let deadline = Instant::now() + Duration::from_secs(10);
105        let stream = loop {
106            match listener.accept() {
107                Ok((stream, _)) => {
108                    if peer_credentials(&stream)? == expected {
109                        break stream;
110                    }
111                }
112                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
113                    if Instant::now() >= deadline {
114                        let _ = child.kill();
115                        let _ = child.wait();
116                        return Err(LinuxError::Bootstrap);
117                    }
118                    std::thread::sleep(Duration::from_millis(5));
119                }
120                Err(_) => return Err(LinuxError::Bootstrap),
121            }
122        };
123        let channel = AuthenticatedChannel::new(stream, expected, nonce)?;
124        Ok(Self {
125            child,
126            channel,
127            bootstrap_dir,
128        })
129    }
130
131    /// Authenticated capability-transfer channel.
132    pub const fn channel(&self) -> &AuthenticatedChannel {
133        &self.channel
134    }
135
136    /// Child process identifier held live and unreaped by this session.
137    pub fn child_id(&self) -> u32 {
138        self.child.id()
139    }
140
141    /// Requests termination and reaps the owned child.
142    pub fn terminate(mut self) -> Result<(), LinuxError> {
143        self.child.kill().map_err(|_| LinuxError::Bootstrap)?;
144        self.child.wait().map_err(|_| LinuxError::Bootstrap)?;
145        Ok(())
146    }
147}
148
149impl Drop for ChildSession {
150    fn drop(&mut self) {
151        let _ = self.child.kill();
152        let _ = self.child.wait();
153        let _ = std::fs::remove_dir_all(&self.bootstrap_dir);
154    }
155}
156
157/// Connects the spawned helper using inherited freshness and expected-parent data.
158pub fn connect_spawned_helper() -> Result<AuthenticatedChannel, LinuxError> {
159    let path = std::env::var_os(ENV_SOCKET).ok_or(LinuxError::Bootstrap)?;
160    let nonce = parse_nonce(&std::env::var(ENV_NONCE).map_err(|_| LinuxError::Bootstrap)?)?;
161    let parent_pid = std::env::var(ENV_PARENT_PID)
162        .map_err(|_| LinuxError::Bootstrap)?
163        .parse()
164        .map_err(|_| LinuxError::Bootstrap)?;
165    let stream = UnixStream::connect(path).map_err(|_| LinuxError::Bootstrap)?;
166    let expected = PeerCredentials {
167        pid: parent_pid,
168        // SAFETY: scalar identity syscalls have no preconditions.
169        uid: unsafe { libc::geteuid() },
170        // SAFETY: scalar identity syscalls have no preconditions.
171        gid: unsafe { libc::getegid() },
172    };
173    AuthenticatedChannel::new(stream, expected, nonce)
174}
175
176impl fmt::Display for LinuxError {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(formatter, "Linux transport failed: {self:?}")
179    }
180}
181
182impl std::error::Error for LinuxError {}
183impl From<native_ipc_core::layout::LayoutError> for LinuxError {
184    fn from(value: native_ipc_core::layout::LayoutError) -> Self {
185        Self::Layout(value)
186    }
187}
188impl From<BindingError> for LinuxError {
189    fn from(value: BindingError) -> Self {
190        Self::Binding(value)
191    }
192}
193
194/// Kernel-authenticated Unix peer identity.
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub struct PeerCredentials {
197    /// Process ID captured by the kernel at connection time.
198    pub pid: u32,
199    /// Effective user identity.
200    pub uid: u32,
201    /// Effective group identity.
202    pub gid: u32,
203}
204
205/// Authenticated private control channel with a pollable peer-lifecycle handle.
206pub struct AuthenticatedChannel {
207    stream: UnixStream,
208    nonce: [u8; 32],
209    peer: PeerCredentials,
210    pidfd: OwnedFd,
211}
212
213impl AuthenticatedChannel {
214    /// Authenticates an already-private post-exec Unix connection.
215    pub fn new(
216        stream: UnixStream,
217        expected: PeerCredentials,
218        nonce: [u8; 32],
219    ) -> Result<Self, LinuxError> {
220        let peer = peer_credentials(&stream)?;
221        if peer != expected || nonce == [0; 32] {
222            return Err(LinuxError::WrongPeer);
223        }
224        let pidfd = pidfd_open(peer.pid)?;
225        Ok(Self {
226            stream,
227            nonce,
228            peer,
229            pidfd,
230        })
231    }
232
233    /// Returns the kernel-authenticated peer credentials.
234    pub const fn peer(&self) -> PeerCredentials {
235        self.peer
236    }
237
238    /// Returns whether the peer pidfd currently reports exit without blocking.
239    pub fn peer_exited(&self) -> Result<bool, LinuxError> {
240        let mut poll = libc::pollfd {
241            fd: self.pidfd.as_raw_fd(),
242            events: libc::POLLIN,
243            revents: 0,
244        };
245        // SAFETY: `poll` points to one initialized entry for the call.
246        let result = unsafe { libc::poll(&mut poll, 1, 0) };
247        if result < 0 {
248            return Err(last_os("poll(pidfd)"));
249        }
250        Ok(result == 1 && poll.revents != 0)
251    }
252
253    /// Sends one sealed reader capability with the session nonce and exact size.
254    pub fn send(&self, capability: &ExportedReaderCapability) -> Result<(), LinuxError> {
255        send_fd(
256            &self.stream,
257            &self.nonce,
258            capability.len,
259            capability.fd.as_raw_fd(),
260        )
261    }
262
263    /// Receives, validates, maps, and binds one read-only capability.
264    pub fn receive_reader(
265        &self,
266        expected_len: usize,
267        expected: ValidationExpectations,
268        topology: RegionSetLayout,
269    ) -> Result<ReaderRegion<LinuxReaderMapping>, LinuxError> {
270        let fd = receive_fd(&self.stream, &self.nonce, expected_len)?;
271        LinuxReaderMapping::import(fd, expected_len, expected, topology)
272    }
273}
274
275/// Quiescent owner of an anonymous, page-rounded, writable memfd mapping.
276pub struct QuiescentRegion {
277    fd: OwnedFd,
278    mapping: Mapping,
279    logical_len: usize,
280}
281
282impl QuiescentRegion {
283    /// Allocates and zeroes an anonymous sealable memfd mapping.
284    pub fn new(logical_len: usize) -> Result<Self, LinuxError> {
285        let len = page_align(logical_len)?;
286        let name = c"native-ipc";
287        // SAFETY: static name is NUL-terminated and flags are valid.
288        let raw = unsafe {
289            libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING)
290        };
291        if raw < 0 {
292            return Err(last_os("memfd_create"));
293        }
294        // SAFETY: successful syscall returned a new owned descriptor.
295        let fd = unsafe { OwnedFd::from_raw_fd(raw) };
296        // SAFETY: descriptor is live and length was checked.
297        if unsafe { libc::ftruncate(fd.as_raw_fd(), len as libc::off_t) } != 0 {
298            return Err(last_os("ftruncate"));
299        }
300        let mapping = Mapping::map(fd.as_raw_fd(), len, libc::PROT_READ | libc::PROT_WRITE)?;
301        // SAFETY: new mapping is exclusive and completely initialized by zero fill.
302        unsafe { std::slice::from_raw_parts_mut(mapping.base.as_ptr(), len) }.fill(0);
303        mapping.advise()?;
304        Ok(Self {
305            fd,
306            mapping,
307            logical_len,
308        })
309    }
310
311    /// Exact page-rounded capability length.
312    pub const fn len(&self) -> usize {
313        self.mapping.len
314    }
315    /// Returns whether the capability is empty (always false for valid values).
316    pub const fn is_empty(&self) -> bool {
317        false
318    }
319    /// Requested logical layout length.
320    pub const fn logical_len(&self) -> usize {
321        self.logical_len
322    }
323    /// Quiescent initialization bytes covering the full capability.
324    pub fn as_bytes(&self) -> &[u8] {
325        // SAFETY: this typestate has no exported fd or peer mapping.
326        unsafe { std::slice::from_raw_parts(self.mapping.base.as_ptr(), self.mapping.len) }
327    }
328    /// Mutable quiescent initialization bytes covering the full capability.
329    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
330        // SAFETY: `&mut self` and quiescent typestate provide exclusivity.
331        unsafe { std::slice::from_raw_parts_mut(self.mapping.base.as_ptr(), self.mapping.len) }
332    }
333
334    /// Validates, seals, and prepares the sole writer plus export capability.
335    pub fn prepare_writer(
336        self,
337        expected: ValidationExpectations,
338        topology: RegionSetLayout,
339    ) -> Result<PreparedWriter, LinuxError> {
340        // SAFETY: no descriptor or mapping has escaped this quiescent owner.
341        let layout =
342            unsafe { ValidatedRegionLayout::validate(self.as_bytes(), expected, &topology) }?;
343        // SAFETY: descriptor is live and seal mask is valid.
344        if unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_ADD_SEALS, REQUIRED_SEALS) } < 0 {
345            return Err(last_os("fcntl(F_ADD_SEALS)"));
346        }
347        validate_fd(self.fd.as_raw_fd(), self.mapping.len)?;
348        Ok(PreparedWriter {
349            mapping: LinuxWriterMapping {
350                fd: self.fd,
351                mapping: self.mapping,
352            },
353            layout,
354            topology,
355        })
356    }
357}
358
359/// Sealed sole-writer mapping awaiting export/commit.
360pub struct PreparedWriter {
361    mapping: LinuxWriterMapping,
362    layout: ValidatedRegionLayout,
363    topology: RegionSetLayout,
364}
365
366impl PreparedWriter {
367    /// Duplicates a sealed descriptor that can create only future read-only mappings.
368    pub fn export_reader(&self) -> Result<ExportedReaderCapability, LinuxError> {
369        // SAFETY: fcntl duplicates the live descriptor with close-on-exec.
370        let raw = unsafe { libc::fcntl(self.mapping.fd.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) };
371        if raw < 0 {
372            return Err(last_os("fcntl(F_DUPFD_CLOEXEC)"));
373        }
374        Ok(ExportedReaderCapability {
375            // SAFETY: successful fcntl returned a new owned descriptor.
376            fd: unsafe { OwnedFd::from_raw_fd(raw) },
377            len: self.mapping.mapping.len,
378        })
379    }
380
381    /// Commits the unique local writer into the audited core bridge.
382    pub fn bind(self) -> Result<WriterRegion<LinuxWriterMapping>, LinuxError> {
383        Ok(WriterRegion::new(self.mapping, self.layout, self.topology)?)
384    }
385}
386
387/// Sealed descriptor intended for one authenticated reader transfer.
388pub struct ExportedReaderCapability {
389    fd: OwnedFd,
390    len: usize,
391}
392
393/// Platform-minted sole-writer witness retaining the memfd and mapping lifetime.
394pub struct LinuxWriterMapping {
395    fd: OwnedFd,
396    mapping: Mapping,
397}
398// SAFETY: construction requires an exclusive pre-seal mapping; FUTURE_WRITE
399// prevents all later writable mappings and write-like fd operations.
400unsafe impl SoleWriterMapping for LinuxWriterMapping {
401    fn base(&self) -> NonNull<u8> {
402        self.mapping.base
403    }
404    fn len(&self) -> usize {
405        self.mapping.len
406    }
407}
408
409/// Platform-minted read-only witness retaining the received fd and mapping.
410pub struct LinuxReaderMapping {
411    _fd: OwnedFd,
412    mapping: Mapping,
413}
414// SAFETY: import maps only PROT_READ after exact anonymous-seal validation.
415unsafe impl ReadOnlyMapping for LinuxReaderMapping {
416    fn base(&self) -> NonNull<u8> {
417        self.mapping.base
418    }
419    fn len(&self) -> usize {
420        self.mapping.len
421    }
422}
423
424impl LinuxReaderMapping {
425    fn import(
426        fd: OwnedFd,
427        len: usize,
428        expected: ValidationExpectations,
429        topology: RegionSetLayout,
430    ) -> Result<ReaderRegion<Self>, LinuxError> {
431        validate_fd(fd.as_raw_fd(), len)?;
432        let mapping = Mapping::map(fd.as_raw_fd(), len, libc::PROT_READ)?;
433        mapping.advise()?;
434        // SAFETY: READY/COMMIT protocol keeps the writer quiescent during import.
435        let bytes = unsafe { std::slice::from_raw_parts(mapping.base.as_ptr(), len) };
436        let layout = unsafe { ValidatedRegionLayout::validate(bytes, expected, &topology) }?;
437        Ok(ReaderRegion::new(
438            Self { _fd: fd, mapping },
439            layout,
440            topology,
441        )?)
442    }
443}
444
445struct Mapping {
446    base: NonNull<u8>,
447    len: usize,
448}
449impl Mapping {
450    fn map(fd: RawFd, len: usize, protection: libc::c_int) -> Result<Self, LinuxError> {
451        // SAFETY: arguments describe a checked file-backed shared mapping.
452        let pointer = unsafe {
453            libc::mmap(
454                std::ptr::null_mut(),
455                len,
456                protection,
457                libc::MAP_SHARED,
458                fd,
459                0,
460            )
461        };
462        if pointer == libc::MAP_FAILED {
463            return Err(last_os("mmap"));
464        }
465        let base = NonNull::new(pointer.cast()).ok_or(LinuxError::InvalidCapability)?;
466        Ok(Self { base, len })
467    }
468    fn advise(&self) -> Result<(), LinuxError> {
469        for advice in [libc::MADV_DONTDUMP, libc::MADV_DONTFORK] {
470            // SAFETY: mapping range is live for the complete call.
471            if unsafe { libc::madvise(self.base.as_ptr().cast(), self.len, advice) } != 0 {
472                return Err(last_os("madvise"));
473            }
474        }
475        Ok(())
476    }
477}
478impl Drop for Mapping {
479    fn drop(&mut self) {
480        // SAFETY: this object uniquely owns the live mapping.
481        let _ = unsafe { libc::munmap(self.base.as_ptr().cast(), self.len) };
482    }
483}
484
485fn validate_fd(fd: RawFd, expected_len: usize) -> Result<(), LinuxError> {
486    // SAFETY: output structures are valid for the live descriptor.
487    let mut stat: libc::stat = unsafe { zeroed() };
488    if unsafe { libc::fstat(fd, &mut stat) } != 0 {
489        return Err(last_os("fstat"));
490    }
491    let mut statfs: libc::statfs = unsafe { zeroed() };
492    if unsafe { libc::fstatfs(fd, &mut statfs) } != 0 {
493        return Err(last_os("fstatfs"));
494    }
495    // SAFETY: descriptor is live and command takes no pointer argument.
496    let seals = unsafe { libc::fcntl(fd, libc::F_GET_SEALS) };
497    if seals < 0
498        || seals & REQUIRED_SEALS != REQUIRED_SEALS
499        || stat.st_size != expected_len as libc::off_t
500        || stat.st_nlink != 0
501        || stat.st_mode & libc::S_IFMT != libc::S_IFREG
502        || statfs.f_type != TMPFS_MAGIC
503    {
504        return Err(LinuxError::InvalidCapability);
505    }
506    Ok(())
507}
508
509fn peer_credentials(stream: &UnixStream) -> Result<PeerCredentials, LinuxError> {
510    let mut credentials: libc::ucred = unsafe { zeroed() };
511    let mut length = size_of::<libc::ucred>() as libc::socklen_t;
512    // SAFETY: output buffer and length pointer are valid.
513    if unsafe {
514        libc::getsockopt(
515            stream.as_raw_fd(),
516            libc::SOL_SOCKET,
517            libc::SO_PEERCRED,
518            (&mut credentials as *mut libc::ucred).cast(),
519            &mut length,
520        )
521    } != 0
522        || length as usize != size_of::<libc::ucred>()
523    {
524        return Err(last_os("getsockopt(SO_PEERCRED)"));
525    }
526    Ok(PeerCredentials {
527        pid: credentials.pid as u32,
528        uid: credentials.uid,
529        gid: credentials.gid,
530    })
531}
532
533fn pidfd_open(pid: u32) -> Result<OwnedFd, LinuxError> {
534    // SAFETY: syscall has scalar arguments and returns a new fd on success.
535    let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0) } as libc::c_int;
536    if raw < 0 {
537        return Err(last_os("pidfd_open"));
538    }
539    // SAFETY: successful syscall returned an owned descriptor.
540    Ok(unsafe { OwnedFd::from_raw_fd(raw) })
541}
542
543fn send_fd(stream: &UnixStream, nonce: &[u8; 32], len: usize, fd: RawFd) -> Result<(), LinuxError> {
544    let mut frame = [0_u8; FRAME_LEN];
545    frame[..8].copy_from_slice(&FRAME_MAGIC);
546    frame[8..16].copy_from_slice(&(len as u64).to_le_bytes());
547    frame[16..48].copy_from_slice(nonce);
548    let mut iovec = libc::iovec {
549        iov_base: frame.as_mut_ptr().cast(),
550        iov_len: frame.len(),
551    };
552    let control_len = unsafe { libc::CMSG_SPACE(size_of::<RawFd>() as u32) } as usize;
553    let mut control = vec![0_u8; control_len];
554    let mut message: libc::msghdr = unsafe { zeroed() };
555    message.msg_iov = &mut iovec;
556    message.msg_iovlen = 1;
557    message.msg_control = control.as_mut_ptr().cast();
558    message.msg_controllen = control.len();
559    // SAFETY: message owns a suitably sized control buffer.
560    unsafe {
561        let header = libc::CMSG_FIRSTHDR(&message);
562        (*header).cmsg_level = libc::SOL_SOCKET;
563        (*header).cmsg_type = libc::SCM_RIGHTS;
564        (*header).cmsg_len = libc::CMSG_LEN(size_of::<RawFd>() as u32) as usize;
565        std::ptr::write(libc::CMSG_DATA(header).cast::<RawFd>(), fd);
566    }
567    // SAFETY: iovec/control buffers remain live for the call.
568    let sent = unsafe { libc::sendmsg(stream.as_raw_fd(), &message, libc::MSG_NOSIGNAL) };
569    if sent != FRAME_LEN as isize {
570        return Err(if sent < 0 {
571            last_os("sendmsg")
572        } else {
573            LinuxError::InvalidFrame
574        });
575    }
576    Ok(())
577}
578
579fn receive_fd(
580    stream: &UnixStream,
581    nonce: &[u8; 32],
582    expected_len: usize,
583) -> Result<OwnedFd, LinuxError> {
584    let mut frame = [0_u8; FRAME_LEN];
585    let mut iovec = libc::iovec {
586        iov_base: frame.as_mut_ptr().cast(),
587        iov_len: frame.len(),
588    };
589    let control_len = unsafe { libc::CMSG_SPACE(size_of::<RawFd>() as u32) } as usize;
590    let mut control = vec![0_u8; control_len];
591    let mut message: libc::msghdr = unsafe { zeroed() };
592    message.msg_iov = &mut iovec;
593    message.msg_iovlen = 1;
594    message.msg_control = control.as_mut_ptr().cast();
595    message.msg_controllen = control.len();
596    // SAFETY: iovec/control buffers remain valid for the call.
597    let received =
598        unsafe { libc::recvmsg(stream.as_raw_fd(), &mut message, libc::MSG_CMSG_CLOEXEC) };
599    if received != FRAME_LEN as isize
600        || message.msg_flags & (libc::MSG_TRUNC | libc::MSG_CTRUNC) != 0
601        || frame[..8] != FRAME_MAGIC
602        || frame[16..48] != *nonce
603        || u64::from_le_bytes(frame[8..16].try_into().expect("fixed range")) != expected_len as u64
604    {
605        return Err(if received < 0 {
606            last_os("recvmsg")
607        } else {
608            LinuxError::InvalidFrame
609        });
610    }
611    // SAFETY: control buffer contains the kernel-produced cmsghdr chain.
612    let header = unsafe { libc::CMSG_FIRSTHDR(&message) };
613    if header.is_null()
614        || unsafe { (*header).cmsg_level } != libc::SOL_SOCKET
615        || unsafe { (*header).cmsg_type } != libc::SCM_RIGHTS
616        || unsafe { (*header).cmsg_len }
617            != unsafe { libc::CMSG_LEN(size_of::<RawFd>() as u32) } as usize
618        || !unsafe { libc::CMSG_NXTHDR(&message, header) }.is_null()
619    {
620        return Err(LinuxError::InvalidAncillaryData);
621    }
622    // SAFETY: exact cmsg length proves one aligned fd payload.
623    let raw = unsafe { std::ptr::read(libc::CMSG_DATA(header).cast::<RawFd>()) };
624    if raw < 0 {
625        return Err(LinuxError::InvalidAncillaryData);
626    }
627    // SAFETY: SCM_RIGHTS installed one new descriptor in this process.
628    Ok(unsafe { OwnedFd::from_raw_fd(raw) })
629}
630
631fn page_align(size: usize) -> Result<usize, LinuxError> {
632    if size == 0 {
633        return Err(LinuxError::InvalidSize(size));
634    }
635    // SAFETY: sysconf has no pointer arguments.
636    let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
637    if page <= 0 {
638        return Err(last_os("sysconf(_SC_PAGESIZE)"));
639    }
640    let page = page as usize;
641    size.checked_add(page - 1)
642        .map(|value| value & !(page - 1))
643        .filter(|value| *value <= isize::MAX as usize)
644        .ok_or(LinuxError::InvalidSize(size))
645}
646
647fn hex(bytes: &[u8]) -> String {
648    const DIGITS: &[u8; 16] = b"0123456789abcdef";
649    let mut encoded = String::with_capacity(bytes.len() * 2);
650    for byte in bytes {
651        encoded.push(DIGITS[(byte >> 4) as usize] as char);
652        encoded.push(DIGITS[(byte & 0x0f) as usize] as char);
653    }
654    encoded
655}
656
657fn parse_nonce(encoded: &str) -> Result<[u8; 32], LinuxError> {
658    if encoded.len() != 64 {
659        return Err(LinuxError::Bootstrap);
660    }
661    let mut nonce = [0_u8; 32];
662    for (output, pair) in nonce.iter_mut().zip(encoded.as_bytes().chunks_exact(2)) {
663        let high = (pair[0] as char)
664            .to_digit(16)
665            .ok_or(LinuxError::Bootstrap)?;
666        let low = (pair[1] as char)
667            .to_digit(16)
668            .ok_or(LinuxError::Bootstrap)?;
669        *output = ((high << 4) | low) as u8;
670    }
671    Ok(nonce)
672}
673
674fn last_os(operation: &'static str) -> LinuxError {
675    LinuxError::Os {
676        operation,
677        code: io::Error::last_os_error().raw_os_error().unwrap_or(-1),
678    }
679}
680
681/// Reports an enforced sealed-memfd backend on supported Linux kernels.
682pub const fn status() -> BackendStatus {
683    BackendStatus::Available
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689    use native_ipc_core::layout::{
690        AcknowledgementRouteSpec, Endpoint, LayoutError, LayoutLimits, RegionSpec, RoleId,
691    };
692
693    #[test]
694    fn backend_reports_available() {
695        assert_eq!(status(), BackendStatus::Available);
696    }
697
698    #[test]
699    fn peer_credentials_are_kernel_derived() {
700        let (left, right) = UnixStream::pair().unwrap();
701        let expected = peer_credentials(&right).unwrap();
702        let channel = AuthenticatedChannel::new(left, expected, [7; 32]).unwrap();
703        assert_eq!(channel.peer(), expected);
704    }
705
706    #[test]
707    fn invalid_sizes_capabilities_and_peer_identity_fail_exactly() {
708        assert!(matches!(
709            QuiescentRegion::new(0),
710            Err(LinuxError::InvalidSize(0))
711        ));
712        assert!(matches!(
713            QuiescentRegion::new(usize::MAX),
714            Err(LinuxError::InvalidSize(usize::MAX))
715        ));
716
717        let unsealed = QuiescentRegion::new(1).unwrap();
718        assert!(matches!(
719            validate_fd(unsealed.fd.as_raw_fd(), unsealed.len()),
720            Err(LinuxError::InvalidCapability)
721        ));
722
723        let (left, right) = UnixStream::pair().unwrap();
724        let actual = peer_credentials(&right).unwrap();
725        let wrong = PeerCredentials {
726            pid: actual.pid.wrapping_add(1),
727            ..actual
728        };
729        assert!(matches!(
730            AuthenticatedChannel::new(left, wrong, [9; 32]),
731            Err(LinuxError::WrongPeer)
732        ));
733
734        let (left, right) = UnixStream::pair().unwrap();
735        let actual = peer_credentials(&right).unwrap();
736        assert!(matches!(
737            AuthenticatedChannel::new(left, actual, [0; 32]),
738            Err(LinuxError::WrongPeer)
739        ));
740    }
741
742    #[test]
743    fn sealed_capability_transfers_and_binds_payload_path() {
744        let producer = RoleId::new(1).unwrap();
745        let peer = RoleId::new(2).unwrap();
746        let specs = [
747            RegionSpec {
748                role: producer,
749                writer: Endpoint::Initiator,
750                slot_count: 1,
751                payload_bytes: 16,
752                acknowledgement_count: 1,
753            },
754            RegionSpec {
755                role: peer,
756                writer: Endpoint::Responder,
757                slot_count: 1,
758                payload_bytes: 16,
759                acknowledgement_count: 1,
760            },
761        ];
762        let routes = [
763            AcknowledgementRouteSpec {
764                owner: peer,
765                target: producer,
766                slot_index: 0,
767                cell_index: 0,
768            },
769            AcknowledgementRouteSpec {
770                owner: producer,
771                target: peer,
772                slot_index: 0,
773                cell_index: 0,
774            },
775        ];
776        let topology = RegionSetLayout::calculate(
777            [5; 32],
778            13,
779            &specs,
780            &routes,
781            LayoutLimits {
782                maximum_mapping_size: 1 << 20,
783                maximum_slot_count: 2,
784                maximum_acknowledgement_count: 2,
785                maximum_payload_bytes: 64,
786            },
787        )
788        .unwrap();
789        let layout = topology.region(producer).unwrap();
790        let mut owner = QuiescentRegion::new(layout.total_size() as usize).unwrap();
791        layout.encode_into(owner.as_bytes_mut()).unwrap();
792        let expected = ValidationExpectations {
793            schema_id: [5; 32],
794            generation: 13,
795            role: producer,
796            writer: Endpoint::Initiator,
797            maximum_mapping_size: owner.len() as u64,
798        };
799        let prepared = owner.prepare_writer(expected, topology.clone()).unwrap();
800        let capability = prepared.export_reader().unwrap();
801
802        // A sealed exported fd cannot create another writable mapping.
803        let denied = unsafe {
804            libc::mmap(
805                std::ptr::null_mut(),
806                capability.len,
807                libc::PROT_READ | libc::PROT_WRITE,
808                libc::MAP_SHARED,
809                capability.fd.as_raw_fd(),
810                0,
811            )
812        };
813        assert_eq!(denied, libc::MAP_FAILED);
814        assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
815
816        // Seals also deny descriptor writes and any size change with exact EPERM.
817        let byte = 0xff_u8;
818        let written =
819            unsafe { libc::pwrite(capability.fd.as_raw_fd(), (&raw const byte).cast(), 1, 0) };
820        assert_eq!(written, -1);
821        assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
822        assert_eq!(
823            unsafe {
824                libc::ftruncate(
825                    capability.fd.as_raw_fd(),
826                    capability.len.saturating_add(4096) as libc::off_t,
827                )
828            },
829            -1
830        );
831        assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
832        assert_eq!(unsafe { libc::ftruncate(capability.fd.as_raw_fd(), 1) }, -1);
833        assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
834
835        let (left, right) = UnixStream::pair().unwrap();
836        let credentials = peer_credentials(&left).unwrap();
837        let sender = AuthenticatedChannel::new(left, credentials, [8; 32]).unwrap();
838        let receiver = AuthenticatedChannel::new(right, credentials, [8; 32]).unwrap();
839        let reader = std::thread::scope(|scope| {
840            let sent = scope.spawn(|| sender.send(&capability));
841            let received =
842                receiver.receive_reader(prepared.mapping.mapping.len, expected, topology.clone());
843            sent.join().unwrap().unwrap();
844            received.unwrap()
845        });
846        let mut writer = prepared.bind().unwrap();
847        assert_eq!(
848            writer.publish(0, 1, None, &[0xaa; 17]).unwrap_err(),
849            BindingError::Layout(LayoutError::PayloadOutOfBounds {
850                length: 17,
851                capacity: 16,
852            })
853        );
854        writer.publish(0, 1, None, b"linux").unwrap();
855        assert_eq!(reader.copy_payload(0, 1).unwrap(), b"linux");
856    }
857
858    #[test]
859    fn spawned_helper_is_pid_authenticated_and_owned() {
860        let executable = std::env::current_exe().unwrap();
861        let arguments = [
862            OsStr::new("--exact"),
863            OsStr::new("linux::tests::spawned_helper_entry"),
864            OsStr::new("--ignored"),
865            OsStr::new("--nocapture"),
866        ];
867        let session = ChildSession::spawn(executable.as_os_str(), &arguments).unwrap();
868        assert_eq!(session.channel().peer().pid, session.child_id());
869        session.terminate().unwrap();
870    }
871
872    #[test]
873    #[ignore = "spawned only by the owned-child integration test"]
874    fn spawned_helper_entry() {
875        let channel = connect_spawned_helper().unwrap();
876        assert_ne!(channel.peer().pid, 0);
877        std::thread::sleep(Duration::from_secs(30));
878    }
879}