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