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