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