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