Skip to main content

miri/shims/unix/
fd.rs

1//! General management of file descriptors, and support for
2//! standard file descriptors (stdin/stdout/stderr).
3
4use std::io;
5use std::io::ErrorKind;
6
7use rand::RngExt;
8use rustc_abi::{Align, Size};
9use rustc_target::spec::Os;
10
11use crate::shims::FileDescriptionRef;
12use crate::shims::files::{DynFileDescriptionRef, FdNum, FileDescription};
13use crate::shims::sig::Varargs;
14use crate::shims::unix::socket::UnixSocketFileDescription;
15use crate::shims::unix::*;
16use crate::*;
17
18#[derive(Debug, Clone, Copy, Eq, PartialEq)]
19pub enum FlockOp {
20    SharedLock { nonblocking: bool },
21    ExclusiveLock { nonblocking: bool },
22    Unlock,
23}
24
25/// Represents unix-specific file descriptions.
26pub trait UnixFileDescription: FileDescription {
27    /// Reads as much as possible into the given buffer `ptr` from a given offset.
28    /// `len` indicates how many bytes we should try to read.
29    /// `dest` is where the return value should be stored: number of bytes read, or `-1` in case of error.
30    fn pread<'tcx>(
31        &self,
32        _communicate_allowed: bool,
33        _offset: u64,
34        _ptr: Pointer,
35        _len: usize,
36        _ecx: &mut MiriInterpCx<'tcx>,
37        _finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
38    ) -> InterpResult<'tcx> {
39        throw_unsup_format!("cannot pread from {}", self.name());
40    }
41
42    /// Writes as much as possible from the given buffer `ptr` starting at a given offset.
43    /// `ptr` is the pointer to the user supplied read buffer.
44    /// `len` indicates how many bytes we should try to write.
45    /// `dest` is where the return value should be stored: number of bytes written, or `-1` in case of error.
46    fn pwrite<'tcx>(
47        &self,
48        _communicate_allowed: bool,
49        _ptr: Pointer,
50        _len: usize,
51        _offset: u64,
52        _ecx: &mut MiriInterpCx<'tcx>,
53        _finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
54    ) -> InterpResult<'tcx> {
55        throw_unsup_format!("cannot pwrite to {}", self.name());
56    }
57
58    fn flock<'tcx>(
59        &self,
60        _communicate_allowed: bool,
61        _op: FlockOp,
62    ) -> InterpResult<'tcx, io::Result<()>> {
63        throw_unsup_format!("cannot flock {}", self.name());
64    }
65
66    /// Implementation of fcntl(F_GETFL) for this FD.
67    fn get_flags<'tcx>(&self, _ecx: &mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, Scalar> {
68        throw_unsup_format!("fcntl: {} is not supported for F_GETFL", self.name());
69    }
70
71    /// Implementation of fcntl(F_SETFL) for this FD.
72    fn set_flags<'tcx>(
73        &self,
74        _flag: i32,
75        _ecx: &mut MiriInterpCx<'tcx>,
76    ) -> InterpResult<'tcx, Scalar> {
77        throw_unsup_format!("fcntl: {} is not supported for F_SETFL", self.name());
78    }
79
80    /// Modifies device parameters.
81    /// `op` is the device-dependent operation code. It's either a `c_long` or `c_int`, depending on
82    /// the target and whether it uses glibc or musl.
83    /// `arg` is the optional third argument which exists depending on the operation code. It's either
84    /// an integer or a pointer.
85    fn ioctl<'tcx>(
86        &self,
87        _op: Scalar,
88        _args: Varargs<'tcx, '_>,
89        _ecx: &mut MiriInterpCx<'tcx>,
90    ) -> InterpResult<'tcx, i32> {
91        throw_unsup_format!("cannot use ioctl on {}", self.name());
92    }
93
94    /// Returns this file description as a Unix socket, if it represents one.
95    fn as_socket(
96        self: FileDescriptionRef<Self>,
97    ) -> Option<FileDescriptionRef<dyn UnixSocketFileDescription>> {
98        None
99    }
100}
101
102impl UnixFileDescription for shims::files::Stdin {}
103impl UnixFileDescription for shims::files::Stdout {}
104impl UnixFileDescription for shims::files::Stderr {}
105impl UnixFileDescription for shims::files::NullOutput {}
106
107impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
108pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
109    fn close(&mut self, fd_num: FdNum) -> InterpResult<'tcx, Scalar> {
110        let this = self.eval_context_mut();
111
112        let Some(fd) = this.machine.fds.remove(fd_num) else {
113            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
114        };
115        let fd = fd.as_unix(); // sanity-check
116        if this.tcx.sess.target.os == Os::Illumos {
117            // Illumos didn't like the Linux semantics of epoll tracking file *descriptions*
118            // rather than file *descriptors*. So on Illumos, when a file description is closed,
119            // de-register it from everything watching it.
120            // > While a best effort has been made to mimic the Linux semantics, there are
121            // > some semantics that are too peculiar or ill-conceived to merit
122            // > accommodation. In particular, the Linux epoll facility will -- by design
123            // > -- continue to generate events for closed file descriptors where/when the
124            // > underlying file description remains open. [...]
125            // > This epoll facility refuses to honor these semantics;
126            // > closing the EPOLL_CTL_ADD'd file descriptor will always result in no
127            // > further events being generated for that event description.
128            if let Some(watched) = fd.readiness_watched() {
129                watched.remove_file_num_interests(fd.id(), fd_num);
130            }
131        }
132        drop(fd);
133        // Our close is always successful. Close does not reliably return errors anyway so it is
134        // not worth the effort to try and return anything here.
135        interp_ok(Scalar::from_i32(0))
136    }
137
138    fn dup(&mut self, old_fd_num: FdNum) -> InterpResult<'tcx, Scalar> {
139        let this = self.eval_context_mut();
140
141        let Some(fd) = this.machine.fds.get(old_fd_num) else {
142            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
143        };
144        interp_ok(Scalar::from_i32(this.machine.fds.insert(fd)))
145    }
146
147    fn dup2(&mut self, old_fd_num: FdNum, new_fd_num: FdNum) -> InterpResult<'tcx, Scalar> {
148        let this = self.eval_context_mut();
149
150        let Some(fd) = this.machine.fds.get(old_fd_num) else {
151            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
152        };
153        if new_fd_num != old_fd_num {
154            if this.machine.fds.get(new_fd_num).is_some() {
155                // Close the FD currently holding this spot.
156                let ret = this.close(new_fd_num)?;
157                assert!(ret.to_i32().unwrap() == 0);
158            }
159            // Insert new FD in this spot.
160            let actual_fd_num = this.machine.fds.insert_with_min_num(fd, new_fd_num);
161            assert_eq!(actual_fd_num, new_fd_num);
162        }
163        interp_ok(Scalar::from_i32(new_fd_num))
164    }
165
166    fn flock(&mut self, fd_num: FdNum, op: i32) -> InterpResult<'tcx, Scalar> {
167        let this = self.eval_context_mut();
168        let Some(fd) = this.machine.fds.get(fd_num) else {
169            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
170        };
171
172        // We need to check that there aren't unsupported options in `op`.
173        let lock_sh = this.eval_libc_i32("LOCK_SH");
174        let lock_ex = this.eval_libc_i32("LOCK_EX");
175        let lock_nb = this.eval_libc_i32("LOCK_NB");
176        let lock_un = this.eval_libc_i32("LOCK_UN");
177
178        use FlockOp::*;
179        let parsed_op = if op == lock_sh {
180            SharedLock { nonblocking: false }
181        } else if op == lock_sh | lock_nb {
182            SharedLock { nonblocking: true }
183        } else if op == lock_ex {
184            ExclusiveLock { nonblocking: false }
185        } else if op == lock_ex | lock_nb {
186            ExclusiveLock { nonblocking: true }
187        } else if op == lock_un {
188            Unlock
189        } else {
190            throw_unsup_format!("unsupported flags {:#x}", op);
191        };
192
193        let result = fd.as_unix().flock(this.machine.communicate(), parsed_op)?;
194        // return `0` if flock is successful
195        let result = result.map(|()| 0i32);
196        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
197    }
198
199    fn ioctl(
200        &mut self,
201        fd: &OpTy<'tcx>,
202        op: &OpTy<'tcx>,
203        varargs: Varargs<'tcx, '_>,
204    ) -> InterpResult<'tcx, Scalar> {
205        let this = self.eval_context_mut();
206
207        let fd = this.read_scalar(fd)?.to_i32()?;
208        let op = this.read_scalar(op)?;
209
210        let Some(fd) = this.machine.fds.get(fd) else {
211            return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
212        };
213
214        // Handle common opcodes.
215        let fioclex = this.eval_libc("FIOCLEX");
216        let fionclex = this.eval_libc("FIONCLEX");
217        if op == fioclex || op == fionclex {
218            // Since we don't support `exec`, those are NOPs.
219            return interp_ok(Scalar::from_i32(0));
220        }
221
222        // Since some ioctl operations use the return value as an output parameter, we cannot strictly use the convention of
223        // zero indicating success and -1 indicating an error.
224        let return_value = fd.as_unix().ioctl(op, varargs, this)?;
225        interp_ok(Scalar::from_i32(return_value))
226    }
227
228    fn fcntl(
229        &mut self,
230        fd_num: &OpTy<'tcx>,
231        cmd: &OpTy<'tcx>,
232        varargs: Varargs<'tcx, '_>,
233    ) -> InterpResult<'tcx, Scalar> {
234        let this = self.eval_context_mut();
235
236        let fd_num = this.read_scalar(fd_num)?.to_i32()?;
237        let cmd = this.read_scalar(cmd)?.to_i32()?;
238
239        let f_getfd = this.eval_libc_i32("F_GETFD");
240        let f_dupfd = this.eval_libc_i32("F_DUPFD");
241        let f_dupfd_cloexec = this.eval_libc_i32("F_DUPFD_CLOEXEC");
242        let f_getfl = this.eval_libc_i32("F_GETFL");
243        let f_setfl = this.eval_libc_i32("F_SETFL");
244
245        // We only support getting the flags for a descriptor.
246        match cmd {
247            cmd if cmd == f_getfd => {
248                // Currently this is the only flag that `F_GETFD` returns. It is OK to just return the
249                // `FD_CLOEXEC` value without checking if the flag is set for the file because `std`
250                // always sets this flag when opening a file. However we still need to check that the
251                // file itself is open.
252                if !this.machine.fds.is_fd_num(fd_num) {
253                    this.set_errno_and_return_neg1_i32(LibcError("EBADF"))
254                } else {
255                    interp_ok(this.eval_libc("FD_CLOEXEC"))
256                }
257            }
258            cmd if cmd == f_dupfd || cmd == f_dupfd_cloexec => {
259                // Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
260                // because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
261                // differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
262                // thus they can share the same implementation here.
263                let cmd_name = if cmd == f_dupfd {
264                    "fcntl(fd, F_DUPFD, ...)"
265                } else {
266                    "fcntl(fd, F_DUPFD_CLOEXEC, ...)"
267                };
268
269                let ([start], _) = this.check_varargs(shim_varargs![i32], varargs, cmd_name)?;
270                let start = this.read_scalar(start)?.to_i32()?;
271
272                if let Some(fd) = this.machine.fds.get(fd_num) {
273                    interp_ok(Scalar::from_i32(this.machine.fds.insert_with_min_num(fd, start)))
274                } else {
275                    this.set_errno_and_return_neg1_i32(LibcError("EBADF"))
276                }
277            }
278            cmd if cmd == f_getfl => {
279                // Check if this is a valid open file descriptor.
280                let Some(fd) = this.machine.fds.get(fd_num) else {
281                    return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
282                };
283
284                fd.as_unix().get_flags(this)
285            }
286            cmd if cmd == f_setfl => {
287                // Check if this is a valid open file descriptor.
288                let Some(fd) = this.machine.fds.get(fd_num) else {
289                    return this.set_errno_and_return_neg1_i32(LibcError("EBADF"));
290                };
291
292                let ([flag], _) =
293                    this.check_varargs(shim_varargs![i32], varargs, "fcntl(fd, F_SETFL, ...)")?;
294                let flag = this.read_scalar(flag)?.to_i32()?;
295
296                // Ignore flags that never get stored by SETFL.
297                // "File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file
298                // creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC)
299                // in arg are ignored."
300                let ignored_flags = this.eval_libc_i32("O_RDONLY")
301                    | this.eval_libc_i32("O_WRONLY")
302                    | this.eval_libc_i32("O_RDWR")
303                    | this.eval_libc_i32("O_CREAT")
304                    | this.eval_libc_i32("O_EXCL")
305                    | this.eval_libc_i32("O_NOCTTY")
306                    | this.eval_libc_i32("O_TRUNC");
307
308                fd.as_unix().set_flags(flag & !ignored_flags, this)
309            }
310            cmd if this.tcx.sess.target.os == Os::MacOs
311                && cmd == this.eval_libc_i32("F_FULLFSYNC") =>
312            {
313                // Reject if isolation is enabled.
314                if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
315                    this.reject_in_isolation("`fcntl`", reject_with)?;
316                    return this.set_errno_and_return_neg1_i32(ErrorKind::PermissionDenied);
317                }
318
319                this.ffullsync_fd(fd_num)
320            }
321            cmd if this.tcx.sess.target.os == Os::MacOs
322                && this.frame_in_std()
323                && cmd == this.eval_libc_i32("F_GETPATH") =>
324            {
325                // Std uses this when debug-printing `File`. We stub it out by returning an error.
326                this.set_errno_and_return_neg1_i32(LibcError("EOPNOTSUPP"))
327            }
328            cmd if this.tcx.sess.target.os == Os::FreeBsd
329                && this.frame_in_std()
330                && cmd == this.eval_libc_i32("F_KINFO") =>
331            {
332                // Std uses this when debug-printing `File`. We stub it out by returning an error.
333                this.set_errno_and_return_neg1_i32(LibcError("EOPNOTSUPP"))
334            }
335            cmd => {
336                throw_unsup_format!("fcntl: unsupported command {cmd:#x}");
337            }
338        }
339    }
340
341    /// Read data from `fd` into buffer specified by `buf` and `count`.
342    ///
343    /// If `offset` is `None`, reads data from current cursor position associated with `fd`
344    /// and updates cursor position on completion. Otherwise, reads from the specified offset
345    /// and keeps the cursor unchanged.
346    fn read(
347        &mut self,
348        fd_num: i32,
349        buf: Pointer,
350        count: u64,
351        offset: Option<i128>,
352        dest: &MPlaceTy<'tcx>,
353    ) -> InterpResult<'tcx> {
354        let this = self.eval_context_mut();
355
356        // Isolation check is done via `FileDescription` trait.
357
358        trace!("Reading from FD {}, size {}", fd_num, count);
359
360        // Check that the *entire* buffer is actually valid memory.
361        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccess)?;
362
363        // We cap the number of read bytes to the largest value that we are able to fit in both the
364        // host's and target's `isize`. This saves us from having to handle overflows later.
365        let count = count
366            .min(u64::try_from(this.target_isize_max()).unwrap())
367            .min(u64::try_from(isize::MAX).unwrap());
368        let count = usize::try_from(count).unwrap(); // now it fits in a `usize`
369
370        // Get the FD.
371        let Some(fd) = this.machine.fds.get(fd_num) else {
372            trace!("read: FD not found");
373            return this.set_errno_and_return_neg1(LibcError("EBADF"), dest);
374        };
375
376        trace!("read: FD mapped to {fd:?}");
377        // We want to read at most `count` bytes. We are sure that `count` is not negative
378        // because it was a target's `usize`. Also we are sure that it's smaller than
379        // `usize::MAX` because it is bounded by the host's `isize`.
380
381        let dest = dest.clone();
382        this.read_from_fd(
383            fd,
384            buf,
385            count,
386            offset,
387            callback!(
388                @capture<'tcx> {
389                    count: usize,
390                    dest: MPlaceTy<'tcx>,
391                }
392                |this, result: Result<usize, IoError>| {
393                    match result {
394                        Ok(read_size) => {
395                            assert!(read_size <= count);
396                            // This must fit since `count` fits.
397                            this.write_int(u64::try_from(read_size).unwrap(), &dest)
398                        }
399                        Err(e) => this.set_errno_and_return_neg1(e, &dest)
400                }}
401            ),
402        )
403    }
404
405    fn write(
406        &mut self,
407        fd_num: i32,
408        buf: Pointer,
409        count: u64,
410        offset: Option<i128>,
411        dest: &MPlaceTy<'tcx>,
412    ) -> InterpResult<'tcx> {
413        let this = self.eval_context_mut();
414
415        // Isolation check is done via `FileDescription` trait.
416
417        // Check that the *entire* buffer is actually valid memory.
418        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccess)?;
419
420        // We cap the number of written bytes to the largest value that we are able to fit in both the
421        // host's and target's `isize`. This saves us from having to handle overflows later.
422        let count = count
423            .min(u64::try_from(this.target_isize_max()).unwrap())
424            .min(u64::try_from(isize::MAX).unwrap());
425        let count = usize::try_from(count).unwrap(); // now it fits in a `usize`
426
427        // We temporarily dup the FD to be able to retain mutable access to `this`.
428        let Some(fd) = this.machine.fds.get(fd_num) else {
429            return this.set_errno_and_return_neg1(LibcError("EBADF"), dest);
430        };
431
432        let dest = dest.clone();
433        this.write_to_fd(
434            fd,
435            buf,
436            count,
437            offset,
438            callback!(
439                @capture<'tcx> {
440                    count: usize,
441                    dest: MPlaceTy<'tcx>,
442                }
443                |this, result: Result<usize, IoError>| {
444                    match result {
445                        Ok(write_size) => {
446                            assert!(write_size <= count);
447                            // This must fit since `count` fits.
448                            this.write_int(u64::try_from(write_size).unwrap(), &dest)
449                        }
450                        Err(e) => this.set_errno_and_return_neg1(e, &dest)
451
452                }}
453            ),
454        )
455    }
456
457    /// Vectored reads are implemented by first reading bytes from `fd`
458    /// into a temporary buffer which has the combined size of all buffers in
459    /// `iov`. After that we split the bytes of the combined buffer into the
460    /// buffers of `iov`. This ensures that the vectored read occurs atomically.
461    fn readv(
462        &mut self,
463        fd: &OpTy<'tcx>,
464        iov: &OpTy<'tcx>,
465        iovcnt: &OpTy<'tcx>,
466        offset: Option<&OpTy<'tcx>>,
467        dest: &MPlaceTy<'tcx>,
468    ) -> InterpResult<'tcx> {
469        let this = self.eval_context_mut();
470
471        let fd = this.read_scalar(fd)?.to_i32()?;
472        let iov_ptr = this.read_pointer(iov)?;
473        let iovcnt: u64 = this.read_scalar(iovcnt)?.to_i32()?.try_into().unwrap();
474        // `readv` is the same as `preadv` without an offset.
475        let offset = if let Some(offset) = offset {
476            if matches!(this.tcx.sess.target.os, Os::Solaris) {
477                throw_unsup_format!(
478                    "preadv: vectored reads with offsets aren't supported on Solaris"
479                )
480            }
481            Some(this.read_scalar(offset)?.to_int(offset.layout.size)?)
482        } else {
483            None
484        };
485
486        // Check that the FD exists.
487        let Some(fd) = this.machine.fds.get(fd) else {
488            return this.set_errno_and_return_neg1(LibcError("EBADF"), dest);
489        };
490
491        let iovec_layout = this.libc_array_ty_layout("iovec", iovcnt);
492        let iov_ptr_mplace = this.ptr_to_mplace(iov_ptr, iovec_layout);
493
494        // Read list of buffers from `iov`.
495        let mut buffers = Vec::new();
496
497        let mut array = this.project_array_fields(&iov_ptr_mplace)?;
498        while let Some((_idx, iovec)) = array.next(this)? {
499            let iov_len_field = this.project_field_named(&iovec, "iov_len")?;
500            let iov_len: u64 = this
501                .read_scalar(&iov_len_field)?
502                .to_int(iov_len_field.layout.size)?
503                .try_into()
504                .unwrap();
505
506            let iov_base_field = this.project_field_named(&iovec, "iov_base")?;
507            let iov_base_ptr = this.read_pointer(&iov_base_field)?;
508
509            buffers.push((iov_base_ptr, iov_len));
510        }
511
512        let total_bytes = buffers.iter().map(|(_, len)| len).sum::<u64>();
513
514        // Allocate a temporary buffer which has the combined size of all buffers provided in `iov`.
515        let tmp_ptr: Pointer = this
516            .allocate_ptr(
517                Size::from_bytes(total_bytes),
518                Align::ONE,
519                MemoryKind::Stack,
520                AllocInit::Uninit,
521            )?
522            .into();
523
524        let dest = dest.clone();
525        this.read_from_fd(
526            fd,
527            tmp_ptr,
528            usize::try_from(total_bytes).unwrap(),
529            offset,
530            callback!(
531                @capture<'tcx> {
532                    tmp_ptr: Pointer,
533                    buffers: Vec<(Pointer, u64)>,
534                    dest: MPlaceTy<'tcx>
535                } |this, result: Result<usize, IoError>| {
536                    let bytes_read = match result {
537                        Ok(size) => {
538                            this.write_scalar(Scalar::from_target_isize(size.try_into().unwrap(), this), &dest)?;
539                            u64::try_from(size).unwrap()
540                        },
541                        Err(e) => {
542                            this.deallocate_ptr(tmp_ptr, None, MemoryKind::Stack)?;
543                            return this.set_errno_and_return_neg1(e, &dest)
544                        }
545                    };
546                    let mut remaining_bytes = bytes_read;
547
548                    // Split the bytes from the temporary buffer into the buffers provided in `iov`.
549                    // We start at the first buffer and fill them in order, until we reach the end of the
550                    // initialized bytes in the temporary buffer.
551                    for (buffer_ptr, buffer_len) in buffers {
552                        // Offset temporary buffer by the amount of bytes we already copied into previous buffers.
553                        let tmp_ptr_with_offset =
554                            this.ptr_offset_inbounds(tmp_ptr, i64::try_from(bytes_read.strict_sub(remaining_bytes)).unwrap())?;
555
556                        // Copy at most as many bytes as the buffer fits but without reading
557                        // any uninitialized bytes from the temporary buffer.
558                        let copy_amount = buffer_len.min(remaining_bytes);
559                        this.mem_copy(
560                            tmp_ptr_with_offset,
561                            buffer_ptr,
562                            Size::from_bytes(copy_amount),
563                            // The buffers are guaranteed to not overlap because we just newly allocated
564                            // the `tmp_ptr`, and `tmp_ptr_with_offset` is guaranteed to be
565                            // within those boundaries.
566                            true,
567                        )?;
568
569                        remaining_bytes = remaining_bytes.strict_sub(copy_amount);
570                        if remaining_bytes == 0 {
571                            // We don't have anything left to copy; exit the loop.
572                            break;
573                        }
574                    }
575
576                    this.deallocate_ptr(tmp_ptr, None, MemoryKind::Stack)
577                }),
578        )
579    }
580
581    /// Vectored writes are implemented by first writing the bytes from all
582    /// buffers of `iov` into a combined temporary buffer and then writing this
583    /// combined buffer into `fd`. This ensures that the vectored write occurs atomically.
584    fn writev(
585        &mut self,
586        fd: &OpTy<'tcx>,
587        iov: &OpTy<'tcx>,
588        iovcnt: &OpTy<'tcx>,
589        offset: Option<&OpTy<'tcx>>,
590        dest: &MPlaceTy<'tcx>,
591    ) -> InterpResult<'tcx> {
592        let this = self.eval_context_mut();
593
594        let fd = this.read_scalar(fd)?.to_i32()?;
595        let iov_ptr = this.read_pointer(iov)?;
596        let iovcnt: u64 = this.read_scalar(iovcnt)?.to_i32()?.try_into().unwrap();
597        // `writev` is the same as `pwritev` without an offset.
598        let offset = if let Some(offset) = offset {
599            if matches!(this.tcx.sess.target.os, Os::Solaris) {
600                throw_unsup_format!(
601                    "pwritev: vectored writes with offsets aren't supported on Solaris"
602                )
603            }
604            Some(this.read_scalar(offset)?.to_int(offset.layout.size)?)
605        } else {
606            None
607        };
608
609        // Check that the FD exists.
610        let Some(fd) = this.machine.fds.get(fd) else {
611            return this.set_errno_and_return_neg1(LibcError("EBADF"), dest);
612        };
613
614        let iovec_layout = this.libc_array_ty_layout("iovec", iovcnt);
615        let iov_ptr_mplace = this.ptr_to_mplace(iov_ptr, iovec_layout);
616
617        // Read list of buffers from `iov`.
618        let mut buffers = Vec::new();
619
620        let mut array = this.project_array_fields(&iov_ptr_mplace)?;
621        while let Some((_idx, iovec)) = array.next(this)? {
622            let iov_len_field = this.project_field_named(&iovec, "iov_len")?;
623            let iov_len: u64 = this
624                .read_scalar(&iov_len_field)?
625                .to_int(iov_len_field.layout.size)?
626                .try_into()
627                .unwrap();
628
629            let iov_base_field = this.project_field_named(&iovec, "iov_base")?;
630            let iov_base_ptr = this.read_pointer(&iov_base_field)?;
631
632            buffers.push((iov_base_ptr, iov_len));
633        }
634
635        let total_bytes = buffers.iter().map(|(_, len)| len).sum::<u64>();
636
637        // Allocate a temporary buffer which has the combined size of all buffers provided in `iov`.
638        let tmp_ptr: Pointer = this
639            .allocate_ptr(
640                Size::from_bytes(total_bytes),
641                Align::ONE,
642                MemoryKind::Stack,
643                AllocInit::Uninit,
644            )?
645            .into();
646
647        // Copy the bytes from all buffers provided in `iov` into the temporary buffer.
648        // We start at the first buffer and then continue buffer by buffer.
649        let mut bytes_copied: u64 = 0;
650        for (buffer_ptr, buffer_len) in buffers {
651            // Offset temporary buffer by the amount of bytes we already copied from previous buffers.
652            let tmp_ptr_with_offset =
653                this.ptr_offset_inbounds(tmp_ptr, i64::try_from(bytes_copied).unwrap())?;
654
655            this.mem_copy(
656                buffer_ptr,
657                tmp_ptr_with_offset,
658                Size::from_bytes(buffer_len),
659                // The buffers are guaranteed to not overlap because we just newly allocated
660                // the `tmp_ptr`, and `tmp_ptr_with_offset` is guaranteed to be
661                // within those boundaries.
662                true,
663            )?;
664
665            bytes_copied = bytes_copied.strict_add(buffer_len);
666        }
667
668        let dest = dest.clone();
669        // Write bytes from the temporary buffer. This ensures the write is atomic.
670        this.write_to_fd(
671            fd,
672            tmp_ptr,
673            usize::try_from(total_bytes).unwrap(),
674            offset,
675            callback!(
676                @capture<'tcx> {
677                    tmp_ptr: Pointer,
678                    dest: MPlaceTy<'tcx>,
679                }
680                |this, result: Result<usize, IoError>| {
681                    this.deallocate_ptr(tmp_ptr, None, MemoryKind::Stack)?;
682                    match result {
683                        Ok(size) => this.write_scalar(Scalar::from_target_isize(size.try_into().unwrap(), this), &dest),
684                        Err(e) => this.set_errno_and_return_neg1(e, &dest)
685                    }
686            }),
687        )
688    }
689}
690
691impl<'tcx> EvalContextPrivExt<'tcx> for crate::MiriInterpCx<'tcx> {}
692trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
693    /// Read `len` bytes from the `fd` file description at `offset` into the buffer
694    /// pointed to by `ptr`.
695    /// If `offset` is [`Some`], the read occurs at the given absolute position rather
696    /// than the current file position (`read_at` semantics rather than `read`).
697    /// `finish` will be invoked when the read is done (which might be way after
698    /// this function returns as the read may block).
699    fn read_from_fd(
700        &mut self,
701        fd: DynFileDescriptionRef,
702        ptr: Pointer,
703        len: usize,
704        offset: Option<i128>,
705        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
706    ) -> InterpResult<'tcx> {
707        let this = self.eval_context_mut();
708
709        // Handle the zero-sized case. The man page says:
710        // > If count is zero, read() may detect the errors described below.  In the absence of any
711        // > errors, or if read() does not check for errors, a read() with a count of 0 returns zero
712        // > and has no other effects.
713        if len == 0 {
714            return finish.call(this, Ok(0));
715        }
716
717        // Non-deterministically decide to further reduce the length, simulating a partial read (but
718        // never to 0, that would indicate EOF).
719        let len = if this.machine.short_fd_operations
720            && fd.short_fd_operations()
721            && len >= 2
722            && this.machine.rng.get_mut().random()
723        {
724            len / 2 // since `len` is at least 2, the result is still at least 1
725        } else {
726            len
727        };
728
729        match offset {
730            None => fd.read(this.machine.communicate(), ptr, len, this, finish)?,
731            Some(offset) => {
732                let Ok(offset) = u64::try_from(offset) else {
733                    return finish.call(this, Err(LibcError("EINVAL")));
734                };
735                fd.as_unix().pread(this.machine.communicate(), offset, ptr, len, this, finish)?
736            }
737        };
738        interp_ok(())
739    }
740
741    /// Write `len` bytes at `offset` from the buffer pointed to by `ptr` into the `fd`
742    /// file description.
743    /// If `offset` is [`Some`], the write occurs at the given absolute position rather
744    /// than the current file position (`write_at` semantics rather than `write`).
745    /// `finish` will be invoked when the write is done (which might be way after
746    /// this function returns as the write may block).
747    fn write_to_fd(
748        &mut self,
749        fd: DynFileDescriptionRef,
750        ptr: Pointer,
751        len: usize,
752        offset: Option<i128>,
753        finish: DynMachineCallback<'tcx, Result<usize, IoError>>,
754    ) -> InterpResult<'tcx> {
755        let this = self.eval_context_mut();
756
757        // Handle the zero-sized case. The man page says:
758        // > If count is zero and fd refers to a regular file, then write() may return a failure
759        // > status if one of the errors below is detected.  If no errors are detected, or error
760        // > detection is not performed, 0 is returned without causing any other effect.   If  count
761        // > is  zero  and  fd refers to a file other than a regular file, the results are not
762        // > specified.
763        if len == 0 {
764            // For now let's not open the can of worms of what exactly "not specified" could mean...
765            return finish.call(this, Ok(0));
766        }
767
768        // Non-deterministically decide to further reduce the length, simulating a partial write.
769        // We avoid reducing the write size to 0: the docs seem to be entirely fine with that,
770        // but the standard library is not (https://github.com/rust-lang/rust/issues/145959).
771        let len = if this.machine.short_fd_operations
772            && fd.short_fd_operations()
773            && len >= 2
774            && this.machine.rng.get_mut().random()
775        {
776            len / 2
777        } else {
778            len
779        };
780
781        match offset {
782            None => fd.write(this.machine.communicate(), ptr, len, this, finish)?,
783            Some(offset) => {
784                let Ok(offset) = u64::try_from(offset) else {
785                    return finish.call(this, Err(LibcError("EINVAL")));
786                };
787                fd.as_unix().pwrite(this.machine.communicate(), ptr, len, offset, this, finish)?
788            }
789        };
790        interp_ok(())
791    }
792}