Skip to main content

miri/shims/unix/
unnamed_socket.rs

1//! This implements "anonymous" sockets, that do not correspond to anything on the host system and
2//! are entirely implemented inside Miri.
3//! We also use the same infrastructure to implement unnamed pipes.
4
5use std::cell::{Cell, OnceCell, RefCell};
6use std::collections::VecDeque;
7use std::io::{self, ErrorKind, Read};
8
9use rustc_target::spec::Os;
10
11use crate::concurrency::VClock;
12use crate::shims::files::{
13    EvalContextExt as _, FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef,
14};
15use crate::shims::unix::UnixFileDescription;
16use crate::shims::unix::linux_like::epoll::{EpollEvents, EvalContextExt as _};
17use crate::*;
18
19/// The maximum capacity of the socketpair buffer in bytes.
20/// This number is arbitrary as the value can always
21/// be configured in the real system.
22const MAX_SOCKETPAIR_BUFFER_CAPACITY: usize = 0x34000;
23
24#[derive(Debug, PartialEq)]
25enum AnonSocketType {
26    // Either end of the socketpair fd.
27    Socketpair,
28    // Read end of the pipe.
29    PipeRead,
30    // Write end of the pipe.
31    PipeWrite,
32}
33
34/// One end of a pair of connected unnamed sockets.
35#[derive(Debug)]
36struct AnonSocket {
37    /// The buffer we are reading from, or `None` if this is the writing end of a pipe.
38    /// (In that case, the peer FD will be the reading end of that pipe.)
39    readbuf: Option<RefCell<Buffer>>,
40    /// The `AnonSocket` file descriptor that is our "peer", and that holds the buffer we are
41    /// writing to. This is a weak reference because the other side may be closed before us; all
42    /// future writes will then trigger EPIPE.
43    peer_fd: OnceCell<WeakFileDescriptionRef<AnonSocket>>,
44    /// Indicates whether the peer has lost data when the file description is closed.
45    /// This flag is set to `true` if the peer's `readbuf` is non-empty at the time
46    /// of closure.
47    peer_lost_data: Cell<bool>,
48    /// A list of thread ids blocked because the buffer was empty.
49    /// Once another thread writes some bytes, these threads will be unblocked.
50    blocked_read_tid: RefCell<Vec<ThreadId>>,
51    /// A list of thread ids blocked because the buffer was full.
52    /// Once another thread reads some bytes, these threads will be unblocked.
53    blocked_write_tid: RefCell<Vec<ThreadId>>,
54    /// Whether this fd is non-blocking or not.
55    is_nonblock: Cell<bool>,
56    // Differentiate between different AnonSocket fd types.
57    fd_type: AnonSocketType,
58}
59
60#[derive(Debug)]
61struct Buffer {
62    buf: VecDeque<u8>,
63    clock: VClock,
64}
65
66impl Buffer {
67    fn new() -> Self {
68        Buffer { buf: VecDeque::new(), clock: VClock::default() }
69    }
70}
71
72impl AnonSocket {
73    fn peer_fd(&self) -> &WeakFileDescriptionRef<AnonSocket> {
74        self.peer_fd.get().unwrap()
75    }
76}
77
78impl FileDescription for AnonSocket {
79    fn name(&self) -> &'static str {
80        match self.fd_type {
81            AnonSocketType::Socketpair => "socketpair",
82            AnonSocketType::PipeRead | AnonSocketType::PipeWrite => "pipe",
83        }
84    }
85
86    fn destroy<'tcx>(
87        self,
88        _self_id: FdId,
89        _communicate_allowed: bool,
90        ecx: &mut MiriInterpCx<'tcx>,
91    ) -> InterpResult<'tcx, io::Result<()>> {
92        if let Some(peer_fd) = self.peer_fd().upgrade() {
93            // If the current readbuf is non-empty when the file description is closed,
94            // notify the peer that data lost has happened in current file description.
95            if let Some(readbuf) = &self.readbuf {
96                if !readbuf.borrow().buf.is_empty() {
97                    peer_fd.peer_lost_data.set(true);
98                }
99            }
100            // Notify peer fd that close has happened, since that can unblock reads and writes.
101            ecx.update_epoll_active_events(peer_fd, /* force_edge */ false)?;
102        }
103        interp_ok(Ok(()))
104    }
105
106    fn read<'tcx>(
107        self: FileDescriptionRef<Self>,
108        _communicate_allowed: bool,
109        ptr: Pointer,
110        len: usize,
111        ecx: &mut MiriInterpCx<'tcx>,
112        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
113    ) -> InterpResult<'tcx> {
114        anonsocket_read(self, ptr, len, ecx, finish)
115    }
116
117    fn write<'tcx>(
118        self: FileDescriptionRef<Self>,
119        _communicate_allowed: bool,
120        ptr: Pointer,
121        len: usize,
122        ecx: &mut MiriInterpCx<'tcx>,
123        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
124    ) -> InterpResult<'tcx> {
125        anonsocket_write(self, ptr, len, ecx, finish)
126    }
127
128    fn short_fd_operations(&self) -> bool {
129        // Pipes guarantee that sufficiently small accesses are not broken apart:
130        // <https://pubs.opengroup.org/onlinepubs/9799919799/functions/write.html#tag_17_699_08>.
131        // For now, we don't bother checking for the size, and just entirely disable
132        // short accesses on pipes.
133        matches!(self.fd_type, AnonSocketType::Socketpair)
134    }
135
136    fn as_unix<'tcx>(&self, _ecx: &MiriInterpCx<'tcx>) -> &dyn UnixFileDescription {
137        self
138    }
139
140    fn get_flags<'tcx>(&self, ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Scalar> {
141        let mut flags = 0;
142
143        // Get flag for file access mode.
144        // The flag for both socketpair and pipe will remain the same even when the peer
145        // fd is closed, so we need to look at the original type of this socket, not at whether
146        // the peer socket still exists.
147        match self.fd_type {
148            AnonSocketType::Socketpair => {
149                flags |= ecx.eval_libc_i32("O_RDWR");
150            }
151            AnonSocketType::PipeRead => {
152                flags |= ecx.eval_libc_i32("O_RDONLY");
153            }
154            AnonSocketType::PipeWrite => {
155                flags |= ecx.eval_libc_i32("O_WRONLY");
156            }
157        }
158
159        // Get flag for blocking status.
160        if self.is_nonblock.get() {
161            flags |= ecx.eval_libc_i32("O_NONBLOCK");
162        }
163
164        interp_ok(Scalar::from_i32(flags))
165    }
166
167    fn set_flags<'tcx>(
168        &self,
169        mut flag: i32,
170        ecx: &mut MiriInterpCx<'tcx>,
171    ) -> InterpResult<'tcx, Scalar> {
172        let o_nonblock = ecx.eval_libc_i32("O_NONBLOCK");
173
174        // O_NONBLOCK flag can be set / unset by user.
175        if flag & o_nonblock == o_nonblock {
176            self.is_nonblock.set(true);
177            flag &= !o_nonblock;
178        } else {
179            self.is_nonblock.set(false);
180        }
181
182        // Throw error if there is any unsupported flag.
183        if flag != 0 {
184            throw_unsup_format!(
185                "fcntl: only O_NONBLOCK is supported for F_SETFL on socketpairs and pipes"
186            )
187        }
188
189        interp_ok(Scalar::from_i32(0))
190    }
191}
192
193/// Write to AnonSocket based on the space available and return the written byte size.
194fn anonsocket_write<'tcx>(
195    self_ref: FileDescriptionRef<AnonSocket>,
196    ptr: Pointer,
197    len: usize,
198    ecx: &mut MiriInterpCx<'tcx>,
199    finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
200) -> InterpResult<'tcx> {
201    // Always succeed on write size 0.
202    // ("If count is zero and fd refers to a file other than a regular file, the results are not specified.")
203    if len == 0 {
204        return finish.call(ecx, Ok(0));
205    }
206
207    // We are writing to our peer's readbuf.
208    let Some(peer_fd) = self_ref.peer_fd().upgrade() else {
209        // If the upgrade from Weak to Rc fails, it indicates that all read ends have been
210        // closed. It is an error to write even if there would be space.
211        return finish.call(ecx, Err(ErrorKind::BrokenPipe.into()));
212    };
213
214    let Some(writebuf) = &peer_fd.readbuf else {
215        // Writing to the read end of a pipe.
216        return finish.call(ecx, Err(IoError::LibcError("EBADF")));
217    };
218
219    // Let's see if we can write.
220    let available_space = MAX_SOCKETPAIR_BUFFER_CAPACITY.strict_sub(writebuf.borrow().buf.len());
221    if available_space == 0 {
222        if self_ref.is_nonblock.get() {
223            // Non-blocking socketpair with a full buffer.
224            return finish.call(ecx, Err(ErrorKind::WouldBlock.into()));
225        } else {
226            self_ref.blocked_write_tid.borrow_mut().push(ecx.active_thread());
227            // Blocking socketpair with a full buffer.
228            // Block the current thread; only keep a weak ref for this.
229            let weak_self_ref = FileDescriptionRef::downgrade(&self_ref);
230            ecx.block_thread(
231                BlockReason::UnnamedSocket,
232                None,
233                callback!(
234                    @capture<'tcx> {
235                        weak_self_ref: WeakFileDescriptionRef<AnonSocket>,
236                        ptr: Pointer,
237                        len: usize,
238                        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
239                    }
240                    |this, unblock: UnblockKind| {
241                        assert_eq!(unblock, UnblockKind::Ready);
242                        // If we got unblocked, then our peer successfully upgraded its weak
243                        // ref to us. That means we can also upgrade our weak ref.
244                        let self_ref = weak_self_ref.upgrade().unwrap();
245                        anonsocket_write(self_ref, ptr, len, this, finish)
246                    }
247                ),
248            );
249        }
250    } else {
251        // There is space to write!
252        let mut writebuf = writebuf.borrow_mut();
253        // Remember this clock so `read` can synchronize with us.
254        ecx.release_clock(|clock| {
255            writebuf.clock.join(clock);
256        })?;
257        // Do full write / partial write based on the space available.
258        let write_size = len.min(available_space);
259        let actual_write_size = ecx.write_to_host(&mut writebuf.buf, write_size, ptr)?.unwrap();
260        assert_eq!(actual_write_size, write_size);
261
262        // Need to stop accessing peer_fd so that it can be notified.
263        drop(writebuf);
264
265        // Unblock all threads that are currently blocked on peer_fd's read.
266        let waiting_threads = std::mem::take(&mut *peer_fd.blocked_read_tid.borrow_mut());
267        // FIXME: We can randomize the order of unblocking.
268        for thread_id in waiting_threads {
269            ecx.unblock_thread(thread_id, BlockReason::UnnamedSocket)?;
270        }
271        // Notify epoll waiters: we might be no longer writable, peer might now be readable.
272        // The notification to the peer seems to be always sent on Linux, even if the
273        // FD was readable before.
274        ecx.update_epoll_active_events(self_ref, /* force_edge */ false)?;
275        ecx.update_epoll_active_events(peer_fd, /* force_edge */ true)?;
276
277        return finish.call(ecx, Ok(write_size));
278    }
279    interp_ok(())
280}
281
282/// Read from AnonSocket and return the number of bytes read.
283fn anonsocket_read<'tcx>(
284    self_ref: FileDescriptionRef<AnonSocket>,
285    ptr: Pointer,
286    len: usize,
287    ecx: &mut MiriInterpCx<'tcx>,
288    finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
289) -> InterpResult<'tcx> {
290    // Always succeed on read size 0.
291    if len == 0 {
292        return finish.call(ecx, Ok(0));
293    }
294
295    let Some(readbuf) = &self_ref.readbuf else {
296        // FIXME: This should return EBADF, but there's no nice way to do that as there's no
297        // corresponding ErrorKind variant.
298        throw_unsup_format!("reading from the write end of a pipe")
299    };
300
301    if readbuf.borrow_mut().buf.is_empty() {
302        if self_ref.peer_fd().upgrade().is_none() {
303            // Socketpair with no peer and empty buffer.
304            // 0 bytes successfully read indicates end-of-file.
305            return finish.call(ecx, Ok(0));
306        } else if self_ref.is_nonblock.get() {
307            // Non-blocking socketpair with writer and empty buffer.
308            // https://linux.die.net/man/2/read
309            // EAGAIN or EWOULDBLOCK can be returned for socket,
310            // POSIX.1-2001 allows either error to be returned for this case.
311            // Since there is no ErrorKind for EAGAIN, WouldBlock is used.
312            return finish.call(ecx, Err(ErrorKind::WouldBlock.into()));
313        } else {
314            self_ref.blocked_read_tid.borrow_mut().push(ecx.active_thread());
315            // Blocking socketpair with writer and empty buffer.
316            // Block the current thread; only keep a weak ref for this.
317            let weak_self_ref = FileDescriptionRef::downgrade(&self_ref);
318            ecx.block_thread(
319                BlockReason::UnnamedSocket,
320                None,
321                callback!(
322                    @capture<'tcx> {
323                        weak_self_ref: WeakFileDescriptionRef<AnonSocket>,
324                        ptr: Pointer,
325                        len: usize,
326                        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
327                    }
328                    |this, unblock: UnblockKind| {
329                        assert_eq!(unblock, UnblockKind::Ready);
330                        // If we got unblocked, then our peer successfully upgraded its weak
331                        // ref to us. That means we can also upgrade our weak ref.
332                        let self_ref = weak_self_ref.upgrade().unwrap();
333                        anonsocket_read(self_ref, ptr, len, this, finish)
334                    }
335                ),
336            );
337        }
338    } else {
339        // There's data to be read!
340        let mut readbuf = readbuf.borrow_mut();
341        // Synchronize with all previous writes to this buffer.
342        // FIXME: this over-synchronizes; a more precise approach would be to
343        // only sync with the writes whose data we will read.
344        ecx.acquire_clock(&readbuf.clock)?;
345
346        // Do full read / partial read based on the space available.
347        // Conveniently, `read` exists on `VecDeque` and has exactly the desired behavior.
348        let read_size = ecx.read_from_host(|buf| readbuf.buf.read(buf), len, ptr)?.unwrap();
349        let readbuf_now_empty = readbuf.buf.is_empty();
350
351        // Need to drop before others can access the readbuf again.
352        drop(readbuf);
353
354        // A notification should be provided for the peer file description even when it can
355        // only write 1 byte. This implementation is not compliant with the actual Linux kernel
356        // implementation. For optimization reasons, the kernel will only mark the file description
357        // as "writable" when it can write more than a certain number of bytes. Since we
358        // don't know what that *certain number* is, we will provide a notification every time
359        // a read is successful. This might result in our epoll emulation providing more
360        // notifications than the real system.
361        if let Some(peer_fd) = self_ref.peer_fd().upgrade() {
362            // Unblock all threads that are currently blocked on peer_fd's write.
363            let waiting_threads = std::mem::take(&mut *peer_fd.blocked_write_tid.borrow_mut());
364            // FIXME: We can randomize the order of unblocking.
365            for thread_id in waiting_threads {
366                ecx.unblock_thread(thread_id, BlockReason::UnnamedSocket)?;
367            }
368            // Notify epoll waiters: peer is now writable.
369            // Linux seems to always notify the peer if the read buffer is now empty.
370            // (Linux also does that if this was a "big" read, but to avoid some arbitrary
371            // threshold, we do not match that.)
372            ecx.update_epoll_active_events(peer_fd, /* force_edge */ readbuf_now_empty)?;
373        };
374        // Notify epoll waiters: we might be no longer readable.
375        ecx.update_epoll_active_events(self_ref, /* force_edge */ false)?;
376
377        return finish.call(ecx, Ok(read_size));
378    }
379    interp_ok(())
380}
381
382impl UnixFileDescription for AnonSocket {
383    fn epoll_active_events<'tcx>(&self) -> InterpResult<'tcx, EpollEvents> {
384        // We only check the status of EPOLLIN, EPOLLOUT, EPOLLHUP and EPOLLRDHUP flags.
385        // If other event flags need to be supported in the future, the check should be added here.
386
387        let mut epoll_ready_events = EpollEvents::new();
388
389        // Check if it is readable.
390        if let Some(readbuf) = &self.readbuf {
391            if !readbuf.borrow().buf.is_empty() {
392                epoll_ready_events.epollin = true;
393            }
394        } else {
395            // Without a read buffer, reading never blocks, so we are always ready.
396            epoll_ready_events.epollin = true;
397        }
398
399        // Check if is writable.
400        if let Some(peer_fd) = self.peer_fd().upgrade() {
401            if let Some(writebuf) = &peer_fd.readbuf {
402                let data_size = writebuf.borrow().buf.len();
403                let available_space = MAX_SOCKETPAIR_BUFFER_CAPACITY.strict_sub(data_size);
404                if available_space != 0 {
405                    epoll_ready_events.epollout = true;
406                }
407            } else {
408                // Without a write buffer, writing never blocks.
409                epoll_ready_events.epollout = true;
410            }
411        } else {
412            // Peer FD has been closed. This always sets both the RDHUP and HUP flags
413            // as we do not support `shutdown` that could be used to partially close the stream.
414            epoll_ready_events.epollrdhup = true;
415            epoll_ready_events.epollhup = true;
416            // Since the peer is closed, even if no data is available reads will return EOF and
417            // writes will return EPIPE. In other words, they won't block, so we mark this as ready
418            // for read and write.
419            epoll_ready_events.epollin = true;
420            epoll_ready_events.epollout = true;
421            // If there is data lost in peer_fd, set EPOLLERR.
422            if self.peer_lost_data.get() {
423                epoll_ready_events.epollerr = true;
424            }
425        }
426        interp_ok(epoll_ready_events)
427    }
428}
429
430impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
431pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
432    /// For more information on the arguments see the socketpair manpage:
433    /// <https://linux.die.net/man/2/socketpair>
434    fn socketpair(
435        &mut self,
436        domain: &OpTy<'tcx>,
437        type_: &OpTy<'tcx>,
438        protocol: &OpTy<'tcx>,
439        sv: &OpTy<'tcx>,
440    ) -> InterpResult<'tcx, Scalar> {
441        let this = self.eval_context_mut();
442
443        let domain = this.read_scalar(domain)?.to_i32()?;
444        let mut flags = this.read_scalar(type_)?.to_i32()?;
445        let protocol = this.read_scalar(protocol)?.to_i32()?;
446        // This is really a pointer to `[i32; 2]` but we use a ptr-to-first-element representation.
447        let sv = this.deref_pointer_as(sv, this.machine.layouts.i32)?;
448
449        let mut is_sock_nonblock = false;
450
451        // Interpret the flag. Every flag we recognize is "subtracted" from `flags`, so
452        // if there is anything left at the end, that's an unsupported flag.
453        if matches!(this.tcx.sess.target.os, Os::Linux | Os::Android) {
454            // SOCK_NONBLOCK only exists on Linux.
455            let sock_nonblock = this.eval_libc_i32("SOCK_NONBLOCK");
456            let sock_cloexec = this.eval_libc_i32("SOCK_CLOEXEC");
457            if flags & sock_nonblock == sock_nonblock {
458                is_sock_nonblock = true;
459                flags &= !sock_nonblock;
460            }
461            if flags & sock_cloexec == sock_cloexec {
462                flags &= !sock_cloexec;
463            }
464        }
465
466        // Fail on unsupported input.
467        // AF_UNIX and AF_LOCAL are synonyms, so we accept both in case
468        // their values differ.
469        if domain != this.eval_libc_i32("AF_UNIX") && domain != this.eval_libc_i32("AF_LOCAL") {
470            throw_unsup_format!(
471                "socketpair: domain {:#x} is unsupported, only AF_UNIX \
472                                 and AF_LOCAL are allowed",
473                domain
474            );
475        } else if flags != this.eval_libc_i32("SOCK_STREAM") {
476            throw_unsup_format!(
477                "socketpair: type {:#x} is unsupported, only SOCK_STREAM, \
478                                 SOCK_CLOEXEC and SOCK_NONBLOCK are allowed",
479                flags
480            );
481        } else if protocol != 0 {
482            throw_unsup_format!(
483                "socketpair: socket protocol {protocol} is unsupported, \
484                                 only 0 is allowed",
485            );
486        }
487
488        // Generate file descriptions.
489        let fds = &mut this.machine.fds;
490        let fd0 = fds.new_ref(AnonSocket {
491            readbuf: Some(RefCell::new(Buffer::new())),
492            peer_fd: OnceCell::new(),
493            peer_lost_data: Cell::new(false),
494            blocked_read_tid: RefCell::new(Vec::new()),
495            blocked_write_tid: RefCell::new(Vec::new()),
496            is_nonblock: Cell::new(is_sock_nonblock),
497            fd_type: AnonSocketType::Socketpair,
498        });
499        let fd1 = fds.new_ref(AnonSocket {
500            readbuf: Some(RefCell::new(Buffer::new())),
501            peer_fd: OnceCell::new(),
502            peer_lost_data: Cell::new(false),
503            blocked_read_tid: RefCell::new(Vec::new()),
504            blocked_write_tid: RefCell::new(Vec::new()),
505            is_nonblock: Cell::new(is_sock_nonblock),
506            fd_type: AnonSocketType::Socketpair,
507        });
508
509        // Make the file descriptions point to each other.
510        fd0.peer_fd.set(FileDescriptionRef::downgrade(&fd1)).unwrap();
511        fd1.peer_fd.set(FileDescriptionRef::downgrade(&fd0)).unwrap();
512
513        // Insert the file description to the fd table, generating the file descriptors.
514        let sv0 = fds.insert(fd0);
515        let sv1 = fds.insert(fd1);
516
517        // Return socketpair file descriptors to the caller.
518        let sv0 = Scalar::from_int(sv0, sv.layout.size);
519        let sv1 = Scalar::from_int(sv1, sv.layout.size);
520        this.write_scalar(sv0, &sv)?;
521        this.write_scalar(sv1, &sv.offset(sv.layout.size, sv.layout, this)?)?;
522
523        interp_ok(Scalar::from_i32(0))
524    }
525
526    fn pipe2(
527        &mut self,
528        pipefd: &OpTy<'tcx>,
529        flags: Option<&OpTy<'tcx>>,
530    ) -> InterpResult<'tcx, Scalar> {
531        let this = self.eval_context_mut();
532
533        let pipefd = this.deref_pointer_as(pipefd, this.machine.layouts.i32)?;
534        let mut flags = match flags {
535            Some(flags) => this.read_scalar(flags)?.to_i32()?,
536            None => 0,
537        };
538
539        let cloexec = this.eval_libc_i32("O_CLOEXEC");
540        let o_nonblock = this.eval_libc_i32("O_NONBLOCK");
541
542        // Interpret the flag. Every flag we recognize is "subtracted" from `flags`, so
543        // if there is anything left at the end, that's an unsupported flag.
544        let mut is_nonblock = false;
545        if flags & o_nonblock == o_nonblock {
546            is_nonblock = true;
547            flags &= !o_nonblock;
548        }
549        // As usual we ignore CLOEXEC.
550        if flags & cloexec == cloexec {
551            flags &= !cloexec;
552        }
553        if flags != 0 {
554            throw_unsup_format!("unsupported flags in `pipe2`");
555        }
556
557        // Generate file descriptions.
558        // pipefd[0] refers to the read end of the pipe.
559        let fds = &mut this.machine.fds;
560        let fd0 = fds.new_ref(AnonSocket {
561            readbuf: Some(RefCell::new(Buffer::new())),
562            peer_fd: OnceCell::new(),
563            peer_lost_data: Cell::new(false),
564            blocked_read_tid: RefCell::new(Vec::new()),
565            blocked_write_tid: RefCell::new(Vec::new()),
566            is_nonblock: Cell::new(is_nonblock),
567            fd_type: AnonSocketType::PipeRead,
568        });
569        let fd1 = fds.new_ref(AnonSocket {
570            readbuf: None,
571            peer_fd: OnceCell::new(),
572            peer_lost_data: Cell::new(false),
573            blocked_read_tid: RefCell::new(Vec::new()),
574            blocked_write_tid: RefCell::new(Vec::new()),
575            is_nonblock: Cell::new(is_nonblock),
576            fd_type: AnonSocketType::PipeWrite,
577        });
578
579        // Make the file descriptions point to each other.
580        fd0.peer_fd.set(FileDescriptionRef::downgrade(&fd1)).unwrap();
581        fd1.peer_fd.set(FileDescriptionRef::downgrade(&fd0)).unwrap();
582
583        // Insert the file description to the fd table, generating the file descriptors.
584        let pipefd0 = fds.insert(fd0);
585        let pipefd1 = fds.insert(fd1);
586
587        // Return file descriptors to the caller.
588        let pipefd0 = Scalar::from_int(pipefd0, pipefd.layout.size);
589        let pipefd1 = Scalar::from_int(pipefd1, pipefd.layout.size);
590        this.write_scalar(pipefd0, &pipefd)?;
591        this.write_scalar(pipefd1, &pipefd.offset(pipefd.layout.size, pipefd.layout, this)?)?;
592
593        interp_ok(Scalar::from_i32(0))
594    }
595}