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