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::Rng;
8use rustc_abi::Size;
9use rustc_target::spec::Os;
10
11use crate::shims::files::FileDescription;
12use crate::shims::sig::check_min_vararg_count;
13use crate::shims::unix::linux_like::epoll::EpollEvents;
14use crate::shims::unix::*;
15use crate::*;
16
17#[derive(Debug, Clone, Copy, Eq, PartialEq)]
18pub(crate) 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    /// Return which epoll events are currently active.
66    fn epoll_active_events<'tcx>(&self) -> InterpResult<'tcx, EpollEvents> {
67        throw_unsup_format!("{}: epoll does not support this file description", self.name());
68    }
69}
70
71impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
72pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
73    fn dup(&mut self, old_fd_num: i32) -> InterpResult<'tcx, Scalar> {
74        let this = self.eval_context_mut();
75
76        let Some(fd) = this.machine.fds.get(old_fd_num) else {
77            return this.set_last_error_and_return_i32(LibcError("EBADF"));
78        };
79        interp_ok(Scalar::from_i32(this.machine.fds.insert(fd)))
80    }
81
82    fn dup2(&mut self, old_fd_num: i32, new_fd_num: i32) -> InterpResult<'tcx, Scalar> {
83        let this = self.eval_context_mut();
84
85        let Some(fd) = this.machine.fds.get(old_fd_num) else {
86            return this.set_last_error_and_return_i32(LibcError("EBADF"));
87        };
88        if new_fd_num != old_fd_num {
89            // Close new_fd if it is previously opened.
90            // If old_fd and new_fd point to the same description, then `dup_fd` ensures we keep the underlying file description alive.
91            if let Some(old_new_fd) = this.machine.fds.fds.insert(new_fd_num, fd) {
92                // Ignore close error (not interpreter's) according to dup2() doc.
93                old_new_fd.close_ref(this.machine.communicate(), this)?.ok();
94            }
95        }
96        interp_ok(Scalar::from_i32(new_fd_num))
97    }
98
99    fn flock(&mut self, fd_num: i32, op: i32) -> InterpResult<'tcx, Scalar> {
100        let this = self.eval_context_mut();
101        let Some(fd) = this.machine.fds.get(fd_num) else {
102            return this.set_last_error_and_return_i32(LibcError("EBADF"));
103        };
104
105        // We need to check that there aren't unsupported options in `op`.
106        let lock_sh = this.eval_libc_i32("LOCK_SH");
107        let lock_ex = this.eval_libc_i32("LOCK_EX");
108        let lock_nb = this.eval_libc_i32("LOCK_NB");
109        let lock_un = this.eval_libc_i32("LOCK_UN");
110
111        use FlockOp::*;
112        let parsed_op = if op == lock_sh {
113            SharedLock { nonblocking: false }
114        } else if op == lock_sh | lock_nb {
115            SharedLock { nonblocking: true }
116        } else if op == lock_ex {
117            ExclusiveLock { nonblocking: false }
118        } else if op == lock_ex | lock_nb {
119            ExclusiveLock { nonblocking: true }
120        } else if op == lock_un {
121            Unlock
122        } else {
123            throw_unsup_format!("unsupported flags {:#x}", op);
124        };
125
126        let result = fd.as_unix(this).flock(this.machine.communicate(), parsed_op)?;
127        // return `0` if flock is successful
128        let result = result.map(|()| 0i32);
129        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
130    }
131
132    fn fcntl(
133        &mut self,
134        fd_num: &OpTy<'tcx>,
135        cmd: &OpTy<'tcx>,
136        varargs: &[OpTy<'tcx>],
137    ) -> InterpResult<'tcx, Scalar> {
138        let this = self.eval_context_mut();
139
140        let fd_num = this.read_scalar(fd_num)?.to_i32()?;
141        let cmd = this.read_scalar(cmd)?.to_i32()?;
142
143        let f_getfd = this.eval_libc_i32("F_GETFD");
144        let f_dupfd = this.eval_libc_i32("F_DUPFD");
145        let f_dupfd_cloexec = this.eval_libc_i32("F_DUPFD_CLOEXEC");
146        let f_getfl = this.eval_libc_i32("F_GETFL");
147        let f_setfl = this.eval_libc_i32("F_SETFL");
148
149        // We only support getting the flags for a descriptor.
150        match cmd {
151            cmd if cmd == f_getfd => {
152                // Currently this is the only flag that `F_GETFD` returns. It is OK to just return the
153                // `FD_CLOEXEC` value without checking if the flag is set for the file because `std`
154                // always sets this flag when opening a file. However we still need to check that the
155                // file itself is open.
156                if !this.machine.fds.is_fd_num(fd_num) {
157                    this.set_last_error_and_return_i32(LibcError("EBADF"))
158                } else {
159                    interp_ok(this.eval_libc("FD_CLOEXEC"))
160                }
161            }
162            cmd if cmd == f_dupfd || cmd == f_dupfd_cloexec => {
163                // Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
164                // because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
165                // differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
166                // thus they can share the same implementation here.
167                let cmd_name = if cmd == f_dupfd {
168                    "fcntl(fd, F_DUPFD, ...)"
169                } else {
170                    "fcntl(fd, F_DUPFD_CLOEXEC, ...)"
171                };
172
173                let [start] = check_min_vararg_count(cmd_name, varargs)?;
174                let start = this.read_scalar(start)?.to_i32()?;
175
176                if let Some(fd) = this.machine.fds.get(fd_num) {
177                    interp_ok(Scalar::from_i32(this.machine.fds.insert_with_min_num(fd, start)))
178                } else {
179                    this.set_last_error_and_return_i32(LibcError("EBADF"))
180                }
181            }
182            cmd if cmd == f_getfl => {
183                // Check if this is a valid open file descriptor.
184                let Some(fd) = this.machine.fds.get(fd_num) else {
185                    return this.set_last_error_and_return_i32(LibcError("EBADF"));
186                };
187
188                fd.get_flags(this)
189            }
190            cmd if cmd == f_setfl => {
191                // Check if this is a valid open file descriptor.
192                let Some(fd) = this.machine.fds.get(fd_num) else {
193                    return this.set_last_error_and_return_i32(LibcError("EBADF"));
194                };
195
196                let [flag] = check_min_vararg_count("fcntl(fd, F_SETFL, ...)", varargs)?;
197                let flag = this.read_scalar(flag)?.to_i32()?;
198
199                // Ignore flags that never get stored by SETFL.
200                // "File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file
201                // creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC)
202                // in arg are ignored."
203                let ignored_flags = this.eval_libc_i32("O_RDONLY")
204                    | this.eval_libc_i32("O_WRONLY")
205                    | this.eval_libc_i32("O_RDWR")
206                    | this.eval_libc_i32("O_CREAT")
207                    | this.eval_libc_i32("O_EXCL")
208                    | this.eval_libc_i32("O_NOCTTY")
209                    | this.eval_libc_i32("O_TRUNC");
210
211                fd.set_flags(flag & !ignored_flags, this)
212            }
213            cmd if this.tcx.sess.target.os == Os::MacOs
214                && cmd == this.eval_libc_i32("F_FULLFSYNC") =>
215            {
216                // Reject if isolation is enabled.
217                if let IsolatedOp::Reject(reject_with) = this.machine.isolated_op {
218                    this.reject_in_isolation("`fcntl`", reject_with)?;
219                    return this.set_last_error_and_return_i32(ErrorKind::PermissionDenied);
220                }
221
222                this.ffullsync_fd(fd_num)
223            }
224            cmd => {
225                throw_unsup_format!("fcntl: unsupported command {cmd:#x}");
226            }
227        }
228    }
229
230    fn close(&mut self, fd_op: &OpTy<'tcx>) -> InterpResult<'tcx, Scalar> {
231        let this = self.eval_context_mut();
232
233        let fd_num = this.read_scalar(fd_op)?.to_i32()?;
234
235        let Some(fd) = this.machine.fds.remove(fd_num) else {
236            return this.set_last_error_and_return_i32(LibcError("EBADF"));
237        };
238        let result = fd.close_ref(this.machine.communicate(), this)?;
239        // return `0` if close is successful
240        let result = result.map(|()| 0i32);
241        interp_ok(Scalar::from_i32(this.try_unwrap_io_result(result)?))
242    }
243
244    /// Read data from `fd` into buffer specified by `buf` and `count`.
245    ///
246    /// If `offset` is `None`, reads data from current cursor position associated with `fd`
247    /// and updates cursor position on completion. Otherwise, reads from the specified offset
248    /// and keeps the cursor unchanged.
249    fn read(
250        &mut self,
251        fd_num: i32,
252        buf: Pointer,
253        count: u64,
254        offset: Option<i128>,
255        dest: &MPlaceTy<'tcx>,
256    ) -> InterpResult<'tcx> {
257        let this = self.eval_context_mut();
258
259        // Isolation check is done via `FileDescription` trait.
260
261        trace!("Reading from FD {}, size {}", fd_num, count);
262
263        // Check that the *entire* buffer is actually valid memory.
264        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccess)?;
265
266        // We cap the number of read bytes to the largest value that we are able to fit in both the
267        // host's and target's `isize`. This saves us from having to handle overflows later.
268        let count = count
269            .min(u64::try_from(this.target_isize_max()).unwrap())
270            .min(u64::try_from(isize::MAX).unwrap());
271        let count = usize::try_from(count).unwrap(); // now it fits in a `usize`
272        let communicate = this.machine.communicate();
273
274        // Get the FD.
275        let Some(fd) = this.machine.fds.get(fd_num) else {
276            trace!("read: FD not found");
277            return this.set_last_error_and_return(LibcError("EBADF"), dest);
278        };
279
280        // Handle the zero-sized case. The man page says:
281        // > If count is zero, read() may detect the errors described below.  In the absence of any
282        // > errors, or if read() does not check for errors, a read() with a count of 0 returns zero
283        // > and has no other effects.
284        if count == 0 {
285            this.write_null(dest)?;
286            return interp_ok(());
287        }
288        // Non-deterministically decide to further reduce the count, simulating a partial read (but
289        // never to 0, that would indicate EOF).
290        let count = if this.machine.short_fd_operations
291            && fd.short_fd_operations()
292            && count >= 2
293            && this.machine.rng.get_mut().random()
294        {
295            count / 2 // since `count` is at least 2, the result is still at least 1
296        } else {
297            count
298        };
299
300        trace!("read: FD mapped to {fd:?}");
301        // We want to read at most `count` bytes. We are sure that `count` is not negative
302        // because it was a target's `usize`. Also we are sure that it's smaller than
303        // `usize::MAX` because it is bounded by the host's `isize`.
304
305        let finish = {
306            let dest = dest.clone();
307            callback!(
308                @capture<'tcx> {
309                    count: usize,
310                    dest: MPlaceTy<'tcx>,
311                }
312                |this, result: Result<usize, IoError>| {
313                    match result {
314                        Ok(read_size) => {
315                            assert!(read_size <= count);
316                            // This must fit since `count` fits.
317                            this.write_int(u64::try_from(read_size).unwrap(), &dest)
318                        }
319                        Err(e) => {
320                            this.set_last_error_and_return(e, &dest)
321                        }
322                }}
323            )
324        };
325        match offset {
326            None => fd.read(communicate, buf, count, this, finish)?,
327            Some(offset) => {
328                let Ok(offset) = u64::try_from(offset) else {
329                    return this.set_last_error_and_return(LibcError("EINVAL"), dest);
330                };
331                fd.as_unix(this).pread(communicate, offset, buf, count, this, finish)?
332            }
333        };
334        interp_ok(())
335    }
336
337    fn write(
338        &mut self,
339        fd_num: i32,
340        buf: Pointer,
341        count: u64,
342        offset: Option<i128>,
343        dest: &MPlaceTy<'tcx>,
344    ) -> InterpResult<'tcx> {
345        let this = self.eval_context_mut();
346
347        // Isolation check is done via `FileDescription` trait.
348
349        // Check that the *entire* buffer is actually valid memory.
350        this.check_ptr_access(buf, Size::from_bytes(count), CheckInAllocMsg::MemoryAccess)?;
351
352        // We cap the number of written bytes to the largest value that we are able to fit in both the
353        // host's and target's `isize`. This saves us from having to handle overflows later.
354        let count = count
355            .min(u64::try_from(this.target_isize_max()).unwrap())
356            .min(u64::try_from(isize::MAX).unwrap());
357        let count = usize::try_from(count).unwrap(); // now it fits in a `usize`
358        let communicate = this.machine.communicate();
359
360        // We temporarily dup the FD to be able to retain mutable access to `this`.
361        let Some(fd) = this.machine.fds.get(fd_num) else {
362            return this.set_last_error_and_return(LibcError("EBADF"), dest);
363        };
364
365        // Handle the zero-sized case. The man page says:
366        // > If count is zero and fd refers to a regular file, then write() may return a failure
367        // > status if one of the errors below is detected.  If no errors are detected, or error
368        // > detection is not performed, 0 is returned without causing any other effect.   If  count
369        // > is  zero  and  fd refers to a file other than a regular file, the results are not
370        // > specified.
371        if count == 0 {
372            // For now let's not open the can of worms of what exactly "not specified" could mean...
373            this.write_null(dest)?;
374            return interp_ok(());
375        }
376        // Non-deterministically decide to further reduce the count, simulating a partial write.
377        // We avoid reducing the write size to 0: the docs seem to be entirely fine with that,
378        // but the standard library is not (https://github.com/rust-lang/rust/issues/145959).
379        let count = if this.machine.short_fd_operations
380            && fd.short_fd_operations()
381            && count >= 2
382            && this.machine.rng.get_mut().random()
383        {
384            count / 2
385        } else {
386            count
387        };
388
389        let finish = {
390            let dest = dest.clone();
391            callback!(
392                @capture<'tcx> {
393                    count: usize,
394                    dest: MPlaceTy<'tcx>,
395                }
396                |this, result: Result<usize, IoError>| {
397                    match result {
398                        Ok(write_size) => {
399                            assert!(write_size <= count);
400                            // This must fit since `count` fits.
401                            this.write_int(u64::try_from(write_size).unwrap(), &dest)
402                        }
403                        Err(e) => {
404                            this.set_last_error_and_return(e, &dest)
405                        }
406                }}
407            )
408        };
409        match offset {
410            None => fd.write(communicate, buf, count, this, finish)?,
411            Some(offset) => {
412                let Ok(offset) = u64::try_from(offset) else {
413                    return this.set_last_error_and_return(LibcError("EINVAL"), dest);
414                };
415                fd.as_unix(this).pwrite(communicate, buf, count, offset, this, finish)?
416            }
417        };
418        interp_ok(())
419    }
420}