1use std::ffi::OsStr;
4use std::fmt;
5use std::io::{self, Write};
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;
22use crate::protocol::{CONTROL_FRAME_LEN, ManifestEntry, PeerAccess, TransferManifest};
23
24const REQUIRED_SEALS: libc::c_int =
25 libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_FUTURE_WRITE | libc::F_SEAL_SEAL;
26const CAPABILITY_MAGIC: [u8; 8] = *b"NIPCFD\0\0";
27const READY_MAGIC: [u8; 8] = *b"NIPCRDY1";
28const COMMIT_MAGIC: [u8; 8] = *b"NIPCCMT1";
29const TMPFS_MAGIC: libc::c_long = 0x0102_1994;
30const ENV_SOCKET: &str = "NATIVE_IPC_LINUX_SOCKET";
31const ENV_NONCE: &str = "NATIVE_IPC_SESSION_NONCE";
32const ENV_PARENT_PID: &str = "NATIVE_IPC_PARENT_PID";
33
34#[derive(Debug)]
36pub enum LinuxError {
37 Os {
39 operation: &'static str,
41 code: i32,
43 },
44 InvalidSize(usize),
46 WrongPeer,
48 InvalidFrame,
50 InvalidAncillaryData,
52 InvalidCapability,
54 Bootstrap,
56 Layout(native_ipc_core::layout::LayoutError),
58 Binding(BindingError),
60}
61
62pub struct ChildSession {
64 child: Child,
65 channel: AuthenticatedChannel,
66 bootstrap_dir: PathBuf,
67}
68
69struct BootstrapGuard {
74 bootstrap_dir: Option<PathBuf>,
75 child: Option<Child>,
76}
77
78impl BootstrapGuard {
79 fn dir(&self) -> &std::path::Path {
80 self.bootstrap_dir
81 .as_deref()
82 .expect("directory is owned until disarm")
83 }
84
85 fn disarm(mut self) -> (PathBuf, Child) {
86 (
87 self.bootstrap_dir
88 .take()
89 .expect("directory is owned until disarm"),
90 self.child.take().expect("child is armed before disarm"),
91 )
92 }
93}
94
95impl Drop for BootstrapGuard {
96 fn drop(&mut self) {
97 if let Some(child) = self.child.as_mut() {
98 let _ = child.kill();
99 let _ = child.wait();
100 }
101 if let Some(dir) = self.bootstrap_dir.take() {
102 let _ = std::fs::remove_dir_all(dir);
103 }
104 }
105}
106
107impl ChildSession {
108 pub fn spawn(program: &OsStr, arguments: &[&OsStr]) -> Result<Self, LinuxError> {
110 let mut nonce = [0_u8; 32];
111 if unsafe { libc::getrandom(nonce.as_mut_ptr().cast(), nonce.len(), 0) }
113 != nonce.len() as isize
114 {
115 return Err(last_os("getrandom"));
116 }
117 let suffix = hex(&nonce[..16]);
118 let bootstrap_dir = std::env::temp_dir().join(format!("native-ipc-{suffix}"));
119 std::fs::create_dir(&bootstrap_dir).map_err(|_| LinuxError::Bootstrap)?;
120 let mut guard = BootstrapGuard {
124 bootstrap_dir: Some(bootstrap_dir),
125 child: None,
126 };
127 std::fs::set_permissions(guard.dir(), std::fs::Permissions::from_mode(0o700))
128 .map_err(|_| LinuxError::Bootstrap)?;
129 let socket_path = guard.dir().join("control.sock");
130 let listener = UnixListener::bind(&socket_path).map_err(|_| LinuxError::Bootstrap)?;
131 std::fs::set_permissions(&socket_path, std::fs::Permissions::from_mode(0o600))
132 .map_err(|_| LinuxError::Bootstrap)?;
133 listener
134 .set_nonblocking(true)
135 .map_err(|_| LinuxError::Bootstrap)?;
136
137 let mut command = Command::new(program);
138 command
139 .args(arguments)
140 .env(ENV_SOCKET, &socket_path)
141 .env(ENV_NONCE, hex(&nonce))
142 .env(ENV_PARENT_PID, std::process::id().to_string());
143 let child = command.spawn().map_err(|_| LinuxError::Bootstrap)?;
144 let expected = PeerCredentials {
145 pid: child.id(),
146 uid: unsafe { libc::geteuid() },
148 gid: unsafe { libc::getegid() },
150 };
151 guard.child = Some(child);
152 let deadline = Instant::now() + Duration::from_secs(10);
153 let stream = loop {
154 match listener.accept() {
155 Ok((stream, _)) => {
156 if peer_credentials(&stream)? == expected {
157 break stream;
158 }
159 }
160 Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
161 if Instant::now() >= deadline {
162 return Err(LinuxError::Bootstrap);
163 }
164 std::thread::sleep(Duration::from_millis(5));
165 }
166 Err(_) => return Err(LinuxError::Bootstrap),
167 }
168 };
169 let channel = AuthenticatedChannel::new(stream, expected, nonce)?;
170 let (bootstrap_dir, child) = guard.disarm();
171 Ok(Self {
172 child,
173 channel,
174 bootstrap_dir,
175 })
176 }
177
178 pub const fn channel(&self) -> &AuthenticatedChannel {
180 &self.channel
181 }
182
183 pub const fn channel_mut(&mut self) -> &mut AuthenticatedChannel {
185 &mut self.channel
186 }
187
188 pub fn child_id(&self) -> u32 {
190 self.child.id()
191 }
192
193 pub fn terminate(mut self) -> Result<(), LinuxError> {
195 self.child.kill().map_err(|_| LinuxError::Bootstrap)?;
196 self.child.wait().map_err(|_| LinuxError::Bootstrap)?;
197 Ok(())
198 }
199}
200
201impl Drop for ChildSession {
202 fn drop(&mut self) {
203 let _ = self.child.kill();
204 let _ = self.child.wait();
205 let _ = std::fs::remove_dir_all(&self.bootstrap_dir);
206 }
207}
208
209pub fn connect_spawned_helper() -> Result<AuthenticatedChannel, LinuxError> {
211 let path = std::env::var_os(ENV_SOCKET).ok_or(LinuxError::Bootstrap)?;
212 let nonce = parse_nonce(&std::env::var(ENV_NONCE).map_err(|_| LinuxError::Bootstrap)?)?;
213 let parent_pid = std::env::var(ENV_PARENT_PID)
214 .map_err(|_| LinuxError::Bootstrap)?
215 .parse()
216 .map_err(|_| LinuxError::Bootstrap)?;
217 let stream = UnixStream::connect(path).map_err(|_| LinuxError::Bootstrap)?;
218 let expected = PeerCredentials {
219 pid: parent_pid,
220 uid: unsafe { libc::geteuid() },
222 gid: unsafe { libc::getegid() },
224 };
225 AuthenticatedChannel::new_with_identity(stream, expected, nonce, parent_pid, std::process::id())
226}
227
228impl fmt::Display for LinuxError {
229 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230 write!(formatter, "Linux transport failed: {self:?}")
231 }
232}
233
234impl std::error::Error for LinuxError {}
235impl From<native_ipc_core::layout::LayoutError> for LinuxError {
236 fn from(value: native_ipc_core::layout::LayoutError) -> Self {
237 Self::Layout(value)
238 }
239}
240impl From<BindingError> for LinuxError {
241 fn from(value: BindingError) -> Self {
242 Self::Binding(value)
243 }
244}
245
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248pub struct PeerCredentials {
249 pub pid: u32,
251 pub uid: u32,
253 pub gid: u32,
255}
256
257pub struct AuthenticatedChannel {
259 stream: UnixStream,
260 nonce: [u8; 32],
261 peer: PeerCredentials,
262 pidfd: OwnedFd,
263 parent_pid: u32,
264 child_pid: u32,
265 next_transfer_id: u64,
266 poisoned: bool,
267}
268
269impl AuthenticatedChannel {
270 pub fn new(
272 stream: UnixStream,
273 expected: PeerCredentials,
274 nonce: [u8; 32],
275 ) -> Result<Self, LinuxError> {
276 Self::new_with_identity(stream, expected, nonce, std::process::id(), expected.pid)
277 }
278
279 fn new_with_identity(
280 stream: UnixStream,
281 expected: PeerCredentials,
282 nonce: [u8; 32],
283 parent_pid: u32,
284 child_pid: u32,
285 ) -> Result<Self, LinuxError> {
286 let peer = peer_credentials(&stream)?;
287 if peer != expected || nonce == [0; 32] {
288 return Err(LinuxError::WrongPeer);
289 }
290 stream
291 .set_read_timeout(Some(Duration::from_secs(10)))
292 .map_err(|_| LinuxError::Bootstrap)?;
293 stream
294 .set_write_timeout(Some(Duration::from_secs(10)))
295 .map_err(|_| LinuxError::Bootstrap)?;
296 let pidfd = pidfd_open(peer.pid)?;
297 Ok(Self {
298 stream,
299 nonce,
300 peer,
301 pidfd,
302 parent_pid,
303 child_pid,
304 next_transfer_id: 1,
305 poisoned: false,
306 })
307 }
308
309 pub const fn peer(&self) -> PeerCredentials {
311 self.peer
312 }
313
314 pub fn peer_exited(&self) -> Result<bool, LinuxError> {
316 let mut poll = libc::pollfd {
317 fd: self.pidfd.as_raw_fd(),
318 events: libc::POLLIN,
319 revents: 0,
320 };
321 let result = unsafe { libc::poll(&mut poll, 1, 0) };
323 if result < 0 {
324 return Err(last_os("poll(pidfd)"));
325 }
326 Ok(result == 1 && poll.revents != 0)
327 }
328
329 pub fn transfer_writer(
342 &mut self,
343 prepared: PreparedWriter,
344 ) -> Result<WriterRegion<LinuxWriterMapping>, LinuxError> {
345 let result = (|| {
346 self.ensure_live()?;
347 let manifest = self.manifest(prepared.expected, prepared.len, PeerAccess::ReadOnly)?;
348 send_fd(&self.stream, &manifest, prepared.capability.fd.as_raw_fd())?;
349 receive_control(&self.stream, READY_MAGIC, &manifest)?;
350 send_control(&self.stream, COMMIT_MAGIC, &manifest)?;
351 let writer = prepared.region;
352 self.next_transfer_id = self
353 .next_transfer_id
354 .checked_add(1)
355 .ok_or(LinuxError::InvalidFrame)?;
356 Ok(writer)
357 })();
358 if result.is_err() {
359 self.poison();
360 }
361 result
362 }
363
364 pub fn receive_reader(
375 &mut self,
376 expected_len: usize,
377 expected: ValidationExpectations,
378 topology: RegionSetLayout,
379 ) -> Result<ReaderRegion<LinuxReaderMapping>, LinuxError> {
380 let result = (|| {
381 self.ensure_live()?;
382 let manifest = self.manifest(expected, expected_len, PeerAccess::ReadOnly)?;
383 let fd = receive_fd(&self.stream, &manifest)?;
384 let pending = LinuxReaderMapping::import(fd, expected_len, expected, topology)?;
385 send_control(&self.stream, READY_MAGIC, &manifest)?;
386 receive_control(&self.stream, COMMIT_MAGIC, &manifest)?;
387 let reader = pending.bind();
388 self.next_transfer_id = self
389 .next_transfer_id
390 .checked_add(1)
391 .ok_or(LinuxError::InvalidFrame)?;
392 Ok(reader)
393 })();
394 if result.is_err() {
395 self.poison();
396 }
397 result
398 }
399
400 fn manifest(
401 &self,
402 expected: ValidationExpectations,
403 len: usize,
404 access: PeerAccess,
405 ) -> Result<TransferManifest, LinuxError> {
406 let entry =
407 ManifestEntry::validated(expected, len, access).ok_or(LinuxError::InvalidFrame)?;
408 TransferManifest::new(
409 self.nonce,
410 self.parent_pid,
411 self.child_pid,
412 self.next_transfer_id,
413 vec![entry],
414 )
415 .ok_or(LinuxError::InvalidFrame)
416 }
417
418 fn ensure_live(&self) -> Result<(), LinuxError> {
419 if self.poisoned || self.peer_exited()? {
420 Err(LinuxError::Bootstrap)
421 } else {
422 Ok(())
423 }
424 }
425
426 fn poison(&mut self) {
427 self.poisoned = true;
428 let _ = self.stream.shutdown(std::net::Shutdown::Both);
429 if std::process::id() == self.parent_pid {
430 let _ = unsafe {
433 libc::syscall(
434 libc::SYS_pidfd_send_signal,
435 self.pidfd.as_raw_fd(),
436 libc::SIGKILL,
437 std::ptr::null::<libc::siginfo_t>(),
438 0,
439 )
440 };
441 }
442 }
443}
444
445pub struct QuiescentRegion {
447 fd: OwnedFd,
448 mapping: Mapping,
449 logical_len: usize,
450}
451
452impl QuiescentRegion {
453 pub fn new(logical_len: usize) -> Result<Self, LinuxError> {
455 let len = page_align(logical_len)?;
456 let name = c"native-ipc";
457 let raw = unsafe {
459 libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING)
460 };
461 if raw < 0 {
462 return Err(last_os("memfd_create"));
463 }
464 let fd = unsafe { OwnedFd::from_raw_fd(raw) };
466 if unsafe { libc::ftruncate(fd.as_raw_fd(), len as libc::off_t) } != 0 {
468 return Err(last_os("ftruncate"));
469 }
470 let mapping = Mapping::map(fd.as_raw_fd(), len, libc::PROT_READ | libc::PROT_WRITE)?;
471 unsafe { std::slice::from_raw_parts_mut(mapping.base.as_ptr(), len) }.fill(0);
473 mapping.advise()?;
474 Ok(Self {
475 fd,
476 mapping,
477 logical_len,
478 })
479 }
480
481 pub const fn len(&self) -> usize {
483 self.mapping.len
484 }
485 pub const fn is_empty(&self) -> bool {
487 false
488 }
489 pub const fn logical_len(&self) -> usize {
491 self.logical_len
492 }
493 pub fn as_bytes(&self) -> &[u8] {
495 unsafe { std::slice::from_raw_parts(self.mapping.base.as_ptr(), self.mapping.len) }
497 }
498 pub fn as_bytes_mut(&mut self) -> &mut [u8] {
500 unsafe { std::slice::from_raw_parts_mut(self.mapping.base.as_ptr(), self.mapping.len) }
502 }
503
504 pub fn prepare_writer(
506 self,
507 expected: ValidationExpectations,
508 topology: RegionSetLayout,
509 ) -> Result<PreparedWriter, LinuxError> {
510 let layout =
512 unsafe { ValidatedRegionLayout::validate(self.as_bytes(), expected, &topology) }?;
513 if unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_ADD_SEALS, REQUIRED_SEALS) } < 0 {
515 return Err(last_os("fcntl(F_ADD_SEALS)"));
516 }
517 validate_fd(self.fd.as_raw_fd(), self.mapping.len)?;
518 let mapping = LinuxWriterMapping {
519 fd: self.fd,
520 mapping: self.mapping,
521 };
522 let raw = unsafe { libc::fcntl(mapping.fd.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) };
523 if raw < 0 {
524 return Err(last_os("fcntl(F_DUPFD_CLOEXEC)"));
525 }
526 let len = mapping.mapping.len;
527 let region = WriterRegion::new(mapping, layout, topology)?;
528 Ok(PreparedWriter {
529 region,
530 capability: ExportedReaderCapability {
531 fd: unsafe { OwnedFd::from_raw_fd(raw) },
533 },
534 expected,
535 len,
536 })
537 }
538}
539
540pub struct PreparedWriter {
549 region: WriterRegion<LinuxWriterMapping>,
550 capability: ExportedReaderCapability,
551 expected: ValidationExpectations,
552 len: usize,
553}
554
555pub struct ExportedReaderCapability {
557 fd: OwnedFd,
558}
559
560pub struct LinuxWriterMapping {
562 fd: OwnedFd,
563 mapping: Mapping,
564}
565unsafe impl SoleWriterMapping for LinuxWriterMapping {
568 fn base(&self) -> NonNull<u8> {
569 self.mapping.base
570 }
571 fn len(&self) -> usize {
572 self.mapping.len
573 }
574}
575
576pub struct LinuxReaderMapping {
578 _fd: OwnedFd,
579 mapping: Mapping,
580}
581
582struct PendingLinuxReader {
583 region: ReaderRegion<LinuxReaderMapping>,
584}
585
586impl PendingLinuxReader {
587 fn bind(self) -> ReaderRegion<LinuxReaderMapping> {
588 self.region
589 }
590}
591unsafe impl ReadOnlyMapping for LinuxReaderMapping {
593 fn base(&self) -> NonNull<u8> {
594 self.mapping.base
595 }
596 fn len(&self) -> usize {
597 self.mapping.len
598 }
599}
600
601impl LinuxReaderMapping {
602 fn import(
603 fd: OwnedFd,
604 len: usize,
605 expected: ValidationExpectations,
606 topology: RegionSetLayout,
607 ) -> Result<PendingLinuxReader, LinuxError> {
608 validate_fd(fd.as_raw_fd(), len)?;
609 let mapping = Mapping::map(fd.as_raw_fd(), len, libc::PROT_READ)?;
610 mapping.advise()?;
611 let bytes = unsafe { std::slice::from_raw_parts(mapping.base.as_ptr(), len) };
613 let layout = unsafe { ValidatedRegionLayout::validate(bytes, expected, &topology) }?;
614 Ok(PendingLinuxReader {
615 region: ReaderRegion::new(Self { _fd: fd, mapping }, layout, topology)?,
616 })
617 }
618}
619
620struct Mapping {
621 base: NonNull<u8>,
622 len: usize,
623}
624impl Mapping {
625 fn map(fd: RawFd, len: usize, protection: libc::c_int) -> Result<Self, LinuxError> {
626 let pointer = unsafe {
628 libc::mmap(
629 std::ptr::null_mut(),
630 len,
631 protection,
632 libc::MAP_SHARED,
633 fd,
634 0,
635 )
636 };
637 if pointer == libc::MAP_FAILED {
638 return Err(last_os("mmap"));
639 }
640 let base = NonNull::new(pointer.cast()).ok_or(LinuxError::InvalidCapability)?;
641 Ok(Self { base, len })
642 }
643 fn advise(&self) -> Result<(), LinuxError> {
644 for advice in [libc::MADV_DONTDUMP, libc::MADV_DONTFORK] {
645 if unsafe { libc::madvise(self.base.as_ptr().cast(), self.len, advice) } != 0 {
647 return Err(last_os("madvise"));
648 }
649 }
650 Ok(())
651 }
652}
653impl Drop for Mapping {
654 fn drop(&mut self) {
655 let _ = unsafe { libc::munmap(self.base.as_ptr().cast(), self.len) };
657 }
658}
659
660fn validate_fd(fd: RawFd, expected_len: usize) -> Result<(), LinuxError> {
661 let mut stat: libc::stat = unsafe { zeroed() };
663 if unsafe { libc::fstat(fd, &mut stat) } != 0 {
664 return Err(last_os("fstat"));
665 }
666 let mut statfs: libc::statfs = unsafe { zeroed() };
667 if unsafe { libc::fstatfs(fd, &mut statfs) } != 0 {
668 return Err(last_os("fstatfs"));
669 }
670 let seals = unsafe { libc::fcntl(fd, libc::F_GET_SEALS) };
672 if seals < 0
673 || seals & REQUIRED_SEALS != REQUIRED_SEALS
674 || stat.st_size != expected_len as libc::off_t
675 || stat.st_nlink != 0
676 || stat.st_mode & libc::S_IFMT != libc::S_IFREG
677 || statfs.f_type != TMPFS_MAGIC
678 {
679 return Err(LinuxError::InvalidCapability);
680 }
681 Ok(())
682}
683
684fn peer_credentials(stream: &UnixStream) -> Result<PeerCredentials, LinuxError> {
685 let mut credentials: libc::ucred = unsafe { zeroed() };
686 let mut length = size_of::<libc::ucred>() as libc::socklen_t;
687 if unsafe {
689 libc::getsockopt(
690 stream.as_raw_fd(),
691 libc::SOL_SOCKET,
692 libc::SO_PEERCRED,
693 (&mut credentials as *mut libc::ucred).cast(),
694 &mut length,
695 )
696 } != 0
697 || length as usize != size_of::<libc::ucred>()
698 {
699 return Err(last_os("getsockopt(SO_PEERCRED)"));
700 }
701 Ok(PeerCredentials {
702 pid: credentials.pid as u32,
703 uid: credentials.uid,
704 gid: credentials.gid,
705 })
706}
707
708fn pidfd_open(pid: u32) -> Result<OwnedFd, LinuxError> {
709 let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0) } as libc::c_int;
711 if raw < 0 {
712 return Err(last_os("pidfd_open"));
713 }
714 Ok(unsafe { OwnedFd::from_raw_fd(raw) })
716}
717
718fn send_fd(stream: &UnixStream, manifest: &TransferManifest, fd: RawFd) -> Result<(), LinuxError> {
719 let mut frame = manifest.encode(CAPABILITY_MAGIC);
720 let mut iovec = libc::iovec {
721 iov_base: frame.as_mut_ptr().cast(),
722 iov_len: frame.len(),
723 };
724 let control_len = unsafe { libc::CMSG_SPACE(size_of::<RawFd>() as u32) } as usize;
725 let mut control = vec![0_u8; control_len];
726 let mut message: libc::msghdr = unsafe { zeroed() };
727 message.msg_iov = &mut iovec;
728 message.msg_iovlen = 1;
729 message.msg_control = control.as_mut_ptr().cast();
730 message.msg_controllen = control.len();
731 unsafe {
733 let header = libc::CMSG_FIRSTHDR(&message);
734 (*header).cmsg_level = libc::SOL_SOCKET;
735 (*header).cmsg_type = libc::SCM_RIGHTS;
736 (*header).cmsg_len = libc::CMSG_LEN(size_of::<RawFd>() as u32) as usize;
737 std::ptr::write(libc::CMSG_DATA(header).cast::<RawFd>(), fd);
738 }
739 let sent = loop {
741 let sent = unsafe { libc::sendmsg(stream.as_raw_fd(), &message, libc::MSG_NOSIGNAL) };
744 if sent >= 0 {
745 break sent as usize;
746 }
747 if io::Error::last_os_error().kind() != io::ErrorKind::Interrupted {
748 return Err(last_os("sendmsg"));
749 }
750 };
751 if sent == 0 || sent > CONTROL_FRAME_LEN {
752 return Err(LinuxError::InvalidFrame);
753 }
754 if sent < CONTROL_FRAME_LEN {
755 let mut stream = stream;
758 stream
759 .write_all(&frame[sent..])
760 .map_err(|_| LinuxError::InvalidFrame)?;
761 }
762 Ok(())
763}
764
765fn receive_fd(stream: &UnixStream, manifest: &TransferManifest) -> Result<OwnedFd, LinuxError> {
766 let (frame, mut descriptors, flags, ancillary_valid) = receive_message(stream)?;
767 if flags & (libc::MSG_TRUNC | libc::MSG_CTRUNC) != 0
768 || !manifest.matches_frame(CAPABILITY_MAGIC, &frame)
769 {
770 return Err(LinuxError::InvalidFrame);
771 }
772 if !ancillary_valid || descriptors.len() != 1 {
773 return Err(LinuxError::InvalidAncillaryData);
774 }
775 Ok(descriptors.pop().expect("exactly one descriptor"))
776}
777
778fn send_control(
779 stream: &UnixStream,
780 magic: [u8; 8],
781 manifest: &TransferManifest,
782) -> Result<(), LinuxError> {
783 let frame = manifest.encode(magic);
784 let mut stream = stream;
785 stream
786 .write_all(&frame)
787 .map_err(|_| LinuxError::InvalidFrame)
788}
789
790fn receive_control(
791 stream: &UnixStream,
792 magic: [u8; 8],
793 manifest: &TransferManifest,
794) -> Result<(), LinuxError> {
795 let (frame, descriptors, flags, ancillary_valid) = receive_message(stream)?;
796 if flags & (libc::MSG_TRUNC | libc::MSG_CTRUNC) != 0 || !manifest.matches_frame(magic, &frame) {
797 return Err(LinuxError::InvalidFrame);
798 }
799 if !ancillary_valid || !descriptors.is_empty() {
800 return Err(LinuxError::InvalidAncillaryData);
801 }
802 Ok(())
803}
804
805fn receive_message(
806 stream: &UnixStream,
807) -> Result<([u8; CONTROL_FRAME_LEN], Vec<OwnedFd>, libc::c_int, bool), LinuxError> {
808 let mut frame = [0_u8; CONTROL_FRAME_LEN];
809 const MAX_RECEIVED_FDS: u32 = 16;
812 let control_len =
813 unsafe { libc::CMSG_SPACE((size_of::<RawFd>() * MAX_RECEIVED_FDS as usize) as u32) }
814 as usize;
815 let mut descriptors = Vec::new();
816 let mut ancillary_valid = true;
817 let mut flags = 0;
818 let mut offset = 0;
819 while offset < frame.len() {
820 let mut iovec = libc::iovec {
821 iov_base: unsafe { frame.as_mut_ptr().add(offset) }.cast(),
824 iov_len: frame.len() - offset,
825 };
826 let mut control = vec![0_u8; control_len];
827 let mut message: libc::msghdr = unsafe { zeroed() };
828 message.msg_iov = &mut iovec;
829 message.msg_iovlen = 1;
830 message.msg_control = control.as_mut_ptr().cast();
831 message.msg_controllen = control.len();
832 let received = loop {
833 let received =
835 unsafe { libc::recvmsg(stream.as_raw_fd(), &mut message, libc::MSG_CMSG_CLOEXEC) };
836 if received >= 0 {
837 break received as usize;
838 }
839 if io::Error::last_os_error().kind() != io::ErrorKind::Interrupted {
840 return Err(last_os("recvmsg"));
841 }
842 };
843
844 let mut header = unsafe { libc::CMSG_FIRSTHDR(&message) };
848 while !header.is_null() {
849 let current = unsafe { &*header };
851 let minimum = unsafe { libc::CMSG_LEN(0) } as usize;
852 if current.cmsg_len < minimum || current.cmsg_len > message.msg_controllen {
853 ancillary_valid = false;
854 break;
855 }
856 if current.cmsg_level != libc::SOL_SOCKET || current.cmsg_type != libc::SCM_RIGHTS {
857 ancillary_valid = false;
858 } else {
859 let payload_len = current.cmsg_len - minimum;
860 if payload_len == 0 || !payload_len.is_multiple_of(size_of::<RawFd>()) {
861 ancillary_valid = false;
862 } else {
863 let count = payload_len / size_of::<RawFd>();
864 for index in 0..count {
865 let raw = unsafe {
867 std::ptr::read_unaligned(
868 libc::CMSG_DATA(header).cast::<RawFd>().add(index),
869 )
870 };
871 if raw < 0 {
872 ancillary_valid = false;
873 } else {
874 descriptors.push(unsafe { OwnedFd::from_raw_fd(raw) });
876 }
877 }
878 }
879 }
880 header = unsafe { libc::CMSG_NXTHDR(&message, header) };
882 }
883 flags |= message.msg_flags;
884 if received == 0 || received > frame.len() - offset {
885 return Err(LinuxError::InvalidFrame);
886 }
887 offset += received;
888 }
889 Ok((frame, descriptors, flags, ancillary_valid))
890}
891
892fn page_align(size: usize) -> Result<usize, LinuxError> {
893 if size == 0 {
894 return Err(LinuxError::InvalidSize(size));
895 }
896 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
898 if page <= 0 {
899 return Err(last_os("sysconf(_SC_PAGESIZE)"));
900 }
901 let page = page as usize;
902 size.checked_add(page - 1)
903 .map(|value| value & !(page - 1))
904 .filter(|value| *value <= isize::MAX as usize)
905 .ok_or(LinuxError::InvalidSize(size))
906}
907
908fn hex(bytes: &[u8]) -> String {
909 const DIGITS: &[u8; 16] = b"0123456789abcdef";
910 let mut encoded = String::with_capacity(bytes.len() * 2);
911 for byte in bytes {
912 encoded.push(DIGITS[(byte >> 4) as usize] as char);
913 encoded.push(DIGITS[(byte & 0x0f) as usize] as char);
914 }
915 encoded
916}
917
918fn parse_nonce(encoded: &str) -> Result<[u8; 32], LinuxError> {
919 if encoded.len() != 64 {
920 return Err(LinuxError::Bootstrap);
921 }
922 let mut nonce = [0_u8; 32];
923 for (output, pair) in nonce.iter_mut().zip(encoded.as_bytes().chunks_exact(2)) {
924 let high = (pair[0] as char)
925 .to_digit(16)
926 .ok_or(LinuxError::Bootstrap)?;
927 let low = (pair[1] as char)
928 .to_digit(16)
929 .ok_or(LinuxError::Bootstrap)?;
930 *output = ((high << 4) | low) as u8;
931 }
932 Ok(nonce)
933}
934
935fn last_os(operation: &'static str) -> LinuxError {
936 LinuxError::Os {
937 operation,
938 code: io::Error::last_os_error().raw_os_error().unwrap_or(-1),
939 }
940}
941
942pub const fn status() -> BackendStatus {
944 BackendStatus::Available
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950 use native_ipc_core::layout::{
951 AcknowledgementRouteSpec, Endpoint, LayoutError, LayoutLimits, RegionSpec, RoleId,
952 };
953
954 fn send_fragmented_with_fds(
955 stream: &UnixStream,
956 frame: &[u8; CONTROL_FRAME_LEN],
957 fds: &[RawFd],
958 first_bytes: usize,
959 ) {
960 send_chunk_with_fds(stream, &frame[..first_bytes], fds);
961 let mut stream = stream;
962 stream.write_all(&frame[first_bytes..]).unwrap();
963 }
964
965 fn send_chunk_with_fds(stream: &UnixStream, bytes: &[u8], fds: &[RawFd]) {
966 let mut prefix = bytes.to_vec();
967 let mut iovec = libc::iovec {
968 iov_base: prefix.as_mut_ptr().cast(),
969 iov_len: prefix.len(),
970 };
971 let control_len = unsafe { libc::CMSG_SPACE(std::mem::size_of_val(fds) as u32) } as usize;
972 let mut control = vec![0_u8; control_len];
973 let mut message: libc::msghdr = unsafe { zeroed() };
974 message.msg_iov = &mut iovec;
975 message.msg_iovlen = 1;
976 message.msg_control = control.as_mut_ptr().cast();
977 message.msg_controllen = control.len();
978 unsafe {
980 let header = libc::CMSG_FIRSTHDR(&message);
981 (*header).cmsg_level = libc::SOL_SOCKET;
982 (*header).cmsg_type = libc::SCM_RIGHTS;
983 (*header).cmsg_len = libc::CMSG_LEN(std::mem::size_of_val(fds) as u32) as usize;
984 std::ptr::copy_nonoverlapping(
985 fds.as_ptr(),
986 libc::CMSG_DATA(header).cast::<RawFd>(),
987 fds.len(),
988 );
989 assert_eq!(
990 libc::sendmsg(stream.as_raw_fd(), &message, libc::MSG_NOSIGNAL),
991 bytes.len() as isize
992 );
993 }
994 }
995
996 fn open_fd_count() -> usize {
997 std::fs::read_dir("/proc/self/fd").unwrap().count()
998 }
999
1000 #[test]
1001 fn backend_reports_available() {
1002 assert_eq!(status(), BackendStatus::Available);
1003 }
1004
1005 #[test]
1006 fn peer_credentials_are_kernel_derived() {
1007 let (left, right) = UnixStream::pair().unwrap();
1008 let expected = peer_credentials(&right).unwrap();
1009 let channel = AuthenticatedChannel::new(left, expected, [7; 32]).unwrap();
1010 assert_eq!(channel.peer(), expected);
1011 }
1012
1013 #[test]
1014 fn invalid_sizes_capabilities_and_peer_identity_fail_exactly() {
1015 assert!(matches!(
1016 QuiescentRegion::new(0),
1017 Err(LinuxError::InvalidSize(0))
1018 ));
1019 assert!(matches!(
1020 QuiescentRegion::new(usize::MAX),
1021 Err(LinuxError::InvalidSize(usize::MAX))
1022 ));
1023
1024 let unsealed = QuiescentRegion::new(1).unwrap();
1025 assert!(matches!(
1026 validate_fd(unsealed.fd.as_raw_fd(), unsealed.len()),
1027 Err(LinuxError::InvalidCapability)
1028 ));
1029
1030 let (left, right) = UnixStream::pair().unwrap();
1031 let actual = peer_credentials(&right).unwrap();
1032 let wrong = PeerCredentials {
1033 pid: actual.pid.wrapping_add(1),
1034 ..actual
1035 };
1036 assert!(matches!(
1037 AuthenticatedChannel::new(left, wrong, [9; 32]),
1038 Err(LinuxError::WrongPeer)
1039 ));
1040
1041 let (left, right) = UnixStream::pair().unwrap();
1042 let actual = peer_credentials(&right).unwrap();
1043 assert!(matches!(
1044 AuthenticatedChannel::new(left, actual, [0; 32]),
1045 Err(LinuxError::WrongPeer)
1046 ));
1047 }
1048
1049 #[test]
1050 fn fragmented_stream_frame_preserves_ancillary_ownership() {
1051 let expected = ValidationExpectations {
1052 schema_id: [3; 32],
1053 generation: 9,
1054 role: RoleId::new(7).unwrap(),
1055 writer: Endpoint::Initiator,
1056 maximum_mapping_size: 4096,
1057 };
1058 let entry = ManifestEntry::validated(expected, 4096, PeerAccess::ReadOnly).unwrap();
1059 let nonce = [4; 32];
1060 let manifest = TransferManifest::new(nonce, 1, 2, 1, vec![entry]).unwrap();
1061 let frame = manifest.encode(CAPABILITY_MAGIC);
1062 let file = std::fs::File::open("/dev/null").unwrap();
1063 let (sender, receiver) = UnixStream::pair().unwrap();
1064 let received = std::thread::scope(|scope| {
1065 let task = scope.spawn(|| receive_fd(&receiver, &manifest));
1066 send_fragmented_with_fds(&sender, &frame, &[file.as_raw_fd()], 1);
1067 task.join().unwrap().unwrap()
1068 });
1069 assert!(received.as_raw_fd() >= 0);
1070 }
1071
1072 #[test]
1073 #[ignore = "spawned in an isolated process by descriptor_cleanup_is_zero_growth"]
1074 fn malformed_extra_descriptor_frame_has_zero_fd_growth() {
1075 let before = open_fd_count();
1076 {
1077 let expected = ValidationExpectations {
1078 schema_id: [5; 32],
1079 generation: 11,
1080 role: RoleId::new(8).unwrap(),
1081 writer: Endpoint::Responder,
1082 maximum_mapping_size: 4096,
1083 };
1084 let entry = ManifestEntry::validated(expected, 4096, PeerAccess::ReadOnly).unwrap();
1085 let nonce = [6; 32];
1086 let manifest = TransferManifest::new(nonce, 1, 2, 1, vec![entry]).unwrap();
1087 let frame = manifest.encode(CAPABILITY_MAGIC);
1088 let first = std::fs::File::open("/dev/null").unwrap();
1089 let second = std::fs::File::open("/dev/null").unwrap();
1090 let (sender, receiver) = UnixStream::pair().unwrap();
1091 std::thread::scope(|scope| {
1092 let task = scope.spawn(|| receive_fd(&receiver, &manifest));
1093 send_fragmented_with_fds(
1094 &sender,
1095 &frame,
1096 &[first.as_raw_fd(), second.as_raw_fd()],
1097 7,
1098 );
1099 assert!(matches!(
1100 task.join().unwrap(),
1101 Err(LinuxError::InvalidAncillaryData)
1102 ));
1103 });
1104 }
1105 assert_eq!(open_fd_count(), before);
1106 }
1107
1108 #[test]
1109 #[ignore = "spawned in an isolated process by descriptor_cleanup_is_zero_growth"]
1110 fn ancillary_on_later_stream_fragment_is_adopted_and_rejected() {
1111 let before = open_fd_count();
1112 {
1113 let expected = ValidationExpectations {
1114 schema_id: [7; 32],
1115 generation: 13,
1116 role: RoleId::new(9).unwrap(),
1117 writer: Endpoint::Initiator,
1118 maximum_mapping_size: 4096,
1119 };
1120 let entry = ManifestEntry::validated(expected, 4096, PeerAccess::ReadOnly).unwrap();
1121 let manifest = TransferManifest::new([8; 32], 1, 2, 1, vec![entry]).unwrap();
1122 let frame = manifest.encode(CAPABILITY_MAGIC);
1123 let first = std::fs::File::open("/dev/null").unwrap();
1124 let second = std::fs::File::open("/dev/null").unwrap();
1125 let (sender, receiver) = UnixStream::pair().unwrap();
1126 std::thread::scope(|scope| {
1127 let task = scope.spawn(|| receive_fd(&receiver, &manifest));
1128 send_chunk_with_fds(&sender, &frame[..7], &[first.as_raw_fd()]);
1129 send_chunk_with_fds(&sender, &frame[7..], &[second.as_raw_fd()]);
1130 assert!(matches!(
1131 task.join().unwrap(),
1132 Err(LinuxError::InvalidAncillaryData)
1133 ));
1134 });
1135 }
1136 assert_eq!(open_fd_count(), before);
1137 }
1138
1139 #[test]
1140 fn descriptor_cleanup_is_zero_growth() {
1141 let executable = std::env::current_exe().unwrap();
1142 for test in [
1143 "linux::tests::malformed_extra_descriptor_frame_has_zero_fd_growth",
1144 "linux::tests::ancillary_on_later_stream_fragment_is_adopted_and_rejected",
1145 ] {
1146 let status = Command::new(&executable)
1147 .args(["--exact", test, "--ignored", "--nocapture"])
1148 .status()
1149 .unwrap();
1150 assert!(status.success(), "isolated descriptor test failed: {test}");
1151 }
1152 }
1153
1154 #[test]
1155 fn sealed_capability_transfers_and_binds_payload_path() {
1156 let producer = RoleId::new(1).unwrap();
1157 let peer = RoleId::new(2).unwrap();
1158 let specs = [
1159 RegionSpec {
1160 role: producer,
1161 writer: Endpoint::Initiator,
1162 slot_count: 1,
1163 payload_bytes: 16,
1164 acknowledgement_count: 1,
1165 },
1166 RegionSpec {
1167 role: peer,
1168 writer: Endpoint::Responder,
1169 slot_count: 1,
1170 payload_bytes: 16,
1171 acknowledgement_count: 1,
1172 },
1173 ];
1174 let routes = [
1175 AcknowledgementRouteSpec {
1176 owner: peer,
1177 target: producer,
1178 slot_index: 0,
1179 cell_index: 0,
1180 },
1181 AcknowledgementRouteSpec {
1182 owner: producer,
1183 target: peer,
1184 slot_index: 0,
1185 cell_index: 0,
1186 },
1187 ];
1188 let topology = RegionSetLayout::calculate(
1189 [5; 32],
1190 13,
1191 &specs,
1192 &routes,
1193 LayoutLimits {
1194 maximum_mapping_size: 1 << 20,
1195 maximum_slot_count: 2,
1196 maximum_acknowledgement_count: 2,
1197 maximum_payload_bytes: 64,
1198 },
1199 )
1200 .unwrap();
1201 let layout = topology.region(producer).unwrap();
1202 let mut owner = QuiescentRegion::new(layout.total_size() as usize).unwrap();
1203 layout.encode_into(owner.as_bytes_mut()).unwrap();
1204 let expected = ValidationExpectations {
1205 schema_id: [5; 32],
1206 generation: 13,
1207 role: producer,
1208 writer: Endpoint::Initiator,
1209 maximum_mapping_size: owner.len() as u64,
1210 };
1211 let prepared = owner.prepare_writer(expected, topology.clone()).unwrap();
1212 let transfer_len = prepared.len;
1213 let capability = &prepared.capability;
1214
1215 let denied = unsafe {
1217 libc::mmap(
1218 std::ptr::null_mut(),
1219 transfer_len,
1220 libc::PROT_READ | libc::PROT_WRITE,
1221 libc::MAP_SHARED,
1222 capability.fd.as_raw_fd(),
1223 0,
1224 )
1225 };
1226 assert_eq!(denied, libc::MAP_FAILED);
1227 assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
1228
1229 let byte = 0xff_u8;
1231 let written =
1232 unsafe { libc::pwrite(capability.fd.as_raw_fd(), (&raw const byte).cast(), 1, 0) };
1233 assert_eq!(written, -1);
1234 assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
1235 assert_eq!(
1236 unsafe {
1237 libc::ftruncate(
1238 capability.fd.as_raw_fd(),
1239 transfer_len.saturating_add(4096) as libc::off_t,
1240 )
1241 },
1242 -1
1243 );
1244 assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
1245 assert_eq!(unsafe { libc::ftruncate(capability.fd.as_raw_fd(), 1) }, -1);
1246 assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM));
1247
1248 let (left, right) = UnixStream::pair().unwrap();
1249 let credentials = peer_credentials(&left).unwrap();
1250 let mut sender = AuthenticatedChannel::new(left, credentials, [8; 32]).unwrap();
1251 let mut receiver = AuthenticatedChannel::new(right, credentials, [8; 32]).unwrap();
1252 std::thread::scope(|scope| {
1253 let received = scope.spawn(|| {
1254 let reader = receiver
1255 .receive_reader(transfer_len, expected, topology.clone())
1256 .unwrap();
1257 for _ in 0..10_000 {
1258 if let Ok(payload) = reader.copy_payload(0, 1) {
1259 assert_eq!(payload, b"linux");
1260 return;
1261 }
1262 std::thread::yield_now();
1263 }
1264 panic!("reader never observed publication");
1265 });
1266 let mut writer = sender.transfer_writer(prepared).unwrap();
1267 assert_eq!(
1268 writer.publish(0, 1, None, &[0xaa; 17]).unwrap_err(),
1269 BindingError::Layout(LayoutError::PayloadOutOfBounds {
1270 length: 17,
1271 capacity: 16,
1272 })
1273 );
1274 writer.publish(0, 1, None, b"linux").unwrap();
1275 received.join().unwrap();
1276 });
1277 }
1278
1279 static BOOTSTRAP_DIR_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1282
1283 fn bootstrap_dirs() -> std::collections::BTreeSet<PathBuf> {
1284 std::fs::read_dir(std::env::temp_dir())
1285 .unwrap()
1286 .filter_map(Result::ok)
1287 .map(|entry| entry.path())
1288 .filter(|path| {
1289 path.file_name()
1290 .and_then(|name| name.to_str())
1291 .is_some_and(|name| name.starts_with("native-ipc-"))
1292 })
1293 .collect()
1294 }
1295
1296 #[test]
1297 fn spawned_helper_is_pid_authenticated_and_owned() {
1298 let _serial = BOOTSTRAP_DIR_LOCK
1299 .lock()
1300 .unwrap_or_else(std::sync::PoisonError::into_inner);
1301 let executable = std::env::current_exe().unwrap();
1302 let arguments = [
1303 OsStr::new("--exact"),
1304 OsStr::new("linux::tests::spawned_helper_entry"),
1305 OsStr::new("--ignored"),
1306 OsStr::new("--nocapture"),
1307 ];
1308 let session = ChildSession::spawn(executable.as_os_str(), &arguments).unwrap();
1309 assert_eq!(session.channel().peer().pid, session.child_id());
1310 session.terminate().unwrap();
1311 }
1312
1313 #[test]
1314 fn failed_spawn_cleans_bootstrap_resources() {
1315 let _serial = BOOTSTRAP_DIR_LOCK
1316 .lock()
1317 .unwrap_or_else(std::sync::PoisonError::into_inner);
1318 let before = bootstrap_dirs();
1319 let Err(error) =
1320 ChildSession::spawn(OsStr::new("/definitely/not/a/real/native-ipc-helper"), &[])
1321 else {
1322 panic!("spawning a nonexistent helper must fail");
1323 };
1324 assert!(matches!(error, LinuxError::Bootstrap));
1325 assert_eq!(bootstrap_dirs(), before);
1326 }
1327
1328 #[test]
1329 fn timed_out_helper_cleans_bootstrap_resources_and_child() {
1330 let _serial = BOOTSTRAP_DIR_LOCK
1331 .lock()
1332 .unwrap_or_else(std::sync::PoisonError::into_inner);
1333 let before = bootstrap_dirs();
1334 let Err(error) = ChildSession::spawn(OsStr::new("/bin/sleep"), &[OsStr::new("30")]) else {
1337 panic!("a helper that never connects must time out");
1338 };
1339 assert!(matches!(error, LinuxError::Bootstrap));
1340 assert_eq!(bootstrap_dirs(), before);
1341 }
1342
1343 #[test]
1344 #[ignore = "spawned only by the owned-child integration test"]
1345 fn spawned_helper_entry() {
1346 let channel = connect_spawned_helper().unwrap();
1347 assert_ne!(channel.peer().pid, 0);
1348 std::thread::sleep(Duration::from_secs(30));
1349 }
1350}