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