Skip to content

Commit 962a740

Browse files
committed
Rewrite file descriptor handling
1 parent 636ad62 commit 962a740

File tree

2 files changed

+74
-43
lines changed

2 files changed

+74
-43
lines changed

src/shims/fs.rs

Lines changed: 73 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::HashMap;
1+
use std::collections::BTreeMap;
22
use std::convert::{TryFrom, TryInto};
33
use std::fs::{remove_file, rename, File, OpenOptions};
44
use std::io::{Read, Seek, SeekFrom, Write};
@@ -13,34 +13,58 @@ use helpers::immty_from_uint_checked;
1313
use shims::time::system_time_to_duration;
1414

1515
#[derive(Debug)]
16-
pub struct FileHandle {
17-
file: File,
18-
writable: bool,
16+
pub enum FileHandle {
17+
StdInPlaceholder,
18+
StdOutPlaceholder,
19+
StdErrPlaceholder,
20+
File { file: File, writable: bool },
21+
// In the future, could add support for dirfd() and other functions by
22+
// adding a Directory variant here
1923
}
2024

2125
pub struct FileHandler {
22-
handles: HashMap<i32, FileHandle>,
23-
low: i32,
26+
handles: BTreeMap<i32, FileHandle>,
2427
}
2528

2629
impl FileHandler {
27-
fn next_fd(&self) -> i32 {
28-
self.low + 1
30+
fn insert_fd(&mut self, file_handle: FileHandle) -> i32 {
31+
self.insert_fd_with_min_fd(file_handle, 3)
2932
}
3033

31-
fn register_fd(&mut self, fd: i32, handle: FileHandle) {
32-
self.low = fd;
33-
self.handles.insert(fd, handle).unwrap_none();
34+
fn insert_fd_with_min_fd(&mut self, file_handle: FileHandle, min_fd: i32) -> i32 {
35+
let min_fd = std::cmp::max(min_fd, 3);
36+
let candidate_new_fd = self
37+
.handles
38+
.range(min_fd..)
39+
.zip(min_fd..)
40+
.find_map(|((fd, _fh), counter)| {
41+
if *fd != counter {
42+
// There was a gap in the fds stored, return the first unused one
43+
// (note that this relies on BTreeMap iterating in key order)
44+
Some(counter)
45+
} else {
46+
// This fd is used, keep going
47+
None
48+
}
49+
});
50+
let new_fd = candidate_new_fd.unwrap_or_else(|| {
51+
// find_map ran out of BTreeMap entries before finding a free fd, use one plus the
52+
// maximum fd in the map
53+
self.handles.keys().rev().next().map(|last_fd| last_fd + 1).unwrap_or(min_fd)
54+
});
55+
self.handles.insert(new_fd, file_handle).unwrap_none();
56+
new_fd
3457
}
3558
}
3659

3760
impl Default for FileHandler {
3861
fn default() -> Self {
39-
FileHandler {
40-
handles: Default::default(),
41-
// 0, 1 and 2 are reserved for stdin, stdout and stderr.
42-
low: 2,
43-
}
62+
let mut handles: BTreeMap<i32, FileHandle> = Default::default();
63+
// 0, 1 and 2 are reserved for stdin, stdout and stderr.
64+
handles.insert(0, FileHandle::StdInPlaceholder);
65+
handles.insert(1, FileHandle::StdOutPlaceholder);
66+
handles.insert(2, FileHandle::StdErrPlaceholder);
67+
FileHandler { handles }
4468
}
4569
}
4670

@@ -119,9 +143,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
119143

120144
let fd = options.open(&path).map(|file| {
121145
let fh = &mut this.machine.file_handler;
122-
let fd = fh.next_fd();
123-
fh.register_fd(fd, FileHandle { file, writable });
124-
fd
146+
fh.insert_fd(FileHandle::File { file, writable })
125147
});
126148

127149
this.try_unwrap_io_result(fd)
@@ -150,23 +172,27 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
150172
} else {
151173
this.handle_not_found()
152174
}
153-
} else if cmd == this.eval_libc_i32("F_DUPFD")? || cmd == this.eval_libc_i32("F_DUPFD_CLOEXEC")? {
175+
} else if cmd == this.eval_libc_i32("F_DUPFD")?
176+
|| cmd == this.eval_libc_i32("F_DUPFD_CLOEXEC")?
177+
{
154178
// Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
155179
// because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
156180
// differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
157181
// thus they can share the same implementation here.
158-
let arg_op = arg_op
159-
.ok_or_else(|| err_unsup_format!("fcntl with command F_DUPFD or F_DUPFD_CLOEXEC requires a third argument"))?;
182+
let arg_op = arg_op.ok_or_else(|| {
183+
err_unsup_format!(
184+
"fcntl with command F_DUPFD or F_DUPFD_CLOEXEC requires a third argument"
185+
)
186+
})?;
160187
let arg = this.read_scalar(arg_op)?.to_i32()?;
161188
let fh = &mut this.machine.file_handler;
162189
let (file_result, writable) = match fh.handles.get(&fd) {
163-
Some(original) => (original.file.try_clone(), original.writable),
190+
Some(FileHandle::File { file, writable }) => (file.try_clone(), *writable),
191+
Some(_) => throw_unsup_format!("Duplicating file descriptors for stdin, stdout, or stderr is not supported"),
164192
None => return this.handle_not_found(),
165193
};
166194
let fd_result = file_result.map(|duplicated| {
167-
let new_fd = std::cmp::max(fh.next_fd(), arg);
168-
fh.register_fd(new_fd, FileHandle { file: duplicated, writable });
169-
new_fd
195+
fh.insert_fd_with_min_fd(FileHandle::File { file: duplicated, writable }, arg)
170196
});
171197
this.try_unwrap_io_result(fd_result)
172198
} else {
@@ -181,23 +207,28 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
181207

182208
let fd = this.read_scalar(fd_op)?.to_i32()?;
183209

184-
if let Some(handle) = this.machine.file_handler.handles.remove(&fd) {
210+
if fd <= 2 {
211+
// early return to prevent removing StdInPlaceholder, etc., from the handles map
212+
return this.handle_not_found();
213+
}
214+
215+
if let Some(FileHandle::File { file, writable }) = this.machine.file_handler.handles.remove(&fd) {
185216
// We sync the file if it was opened in a mode different than read-only.
186-
if handle.writable {
217+
if writable {
187218
// `File::sync_all` does the checks that are done when closing a file. We do this to
188219
// to handle possible errors correctly.
189-
let result = this.try_unwrap_io_result(handle.file.sync_all().map(|_| 0i32));
220+
let result = this.try_unwrap_io_result(file.sync_all().map(|_| 0i32));
190221
// Now we actually close the file.
191-
drop(handle);
222+
drop(file);
192223
// And return the result.
193224
result
194225
} else {
195226
// We drop the file, this closes it but ignores any errors produced when closing
196-
// it. This is done because `File::sync_call` cannot be done over files like
227+
// it. This is done because `File::sync_all` cannot be done over files like
197228
// `/dev/urandom` which are read-only. Check
198229
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439 for a deeper
199230
// discussion.
200-
drop(handle);
231+
drop(file);
201232
Ok(0)
202233
}
203234
} else {
@@ -230,16 +261,15 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
230261
// host's and target's `isize`. This saves us from having to handle overflows later.
231262
let count = count.min(this.isize_max() as u64).min(isize::max_value() as u64);
232263

233-
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
264+
if let Some(FileHandle::File { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
234265
// This can never fail because `count` was capped to be smaller than
235266
// `isize::max_value()`.
236267
let count = isize::try_from(count).unwrap();
237268
// We want to read at most `count` bytes. We are sure that `count` is not negative
238269
// because it was a target's `usize`. Also we are sure that its smaller than
239270
// `usize::max_value()` because it is a host's `isize`.
240271
let mut bytes = vec![0; count as usize];
241-
let result = handle
242-
.file
272+
let result = file
243273
.read(&mut bytes)
244274
// `File::read` never returns a value larger than `count`, so this cannot fail.
245275
.map(|c| i64::try_from(c).unwrap());
@@ -285,9 +315,9 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
285315
// host's and target's `isize`. This saves us from having to handle overflows later.
286316
let count = count.min(this.isize_max() as u64).min(isize::max_value() as u64);
287317

288-
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
318+
if let Some(FileHandle::File { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
289319
let bytes = this.memory.read_bytes(buf, Size::from_bytes(count))?;
290-
let result = handle.file.write(&bytes).map(|c| i64::try_from(c).unwrap());
320+
let result = file.write(&bytes).map(|c| i64::try_from(c).unwrap());
291321
this.try_unwrap_io_result(result)
292322
} else {
293323
this.handle_not_found()
@@ -320,8 +350,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
320350
return Ok(-1);
321351
};
322352

323-
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
324-
let result = handle.file.seek(seek_from).map(|offset| offset as i64);
353+
if let Some(FileHandle::File { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
354+
let result = file.seek(seek_from).map(|offset| offset as i64);
325355
this.try_unwrap_io_result(result)
326356
} else {
327357
this.handle_not_found()
@@ -682,11 +712,11 @@ impl FileMetadata {
682712
fd: i32,
683713
) -> InterpResult<'tcx, Option<FileMetadata>> {
684714
let option = ecx.machine.file_handler.handles.get(&fd);
685-
let handle = match option {
686-
Some(handle) => handle,
687-
None => return ecx.handle_not_found().map(|_: i32| None),
715+
let file = match option {
716+
Some(FileHandle::File { file, writable: _ }) => file,
717+
Some(_) | None => return ecx.handle_not_found().map(|_: i32| None),
688718
};
689-
let metadata = handle.file.metadata();
719+
let metadata = file.metadata();
690720

691721
FileMetadata::from_meta(ecx, metadata)
692722
}

tests/run-pass/fs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ fn main() {
5050
cloned.read_to_end(&mut contents).unwrap();
5151
assert_eq!(bytes, contents.as_slice());
5252

53+
let mut file = File::open(&path).unwrap();
5354
// Test that seeking to the beginning and reading until EOF gets the text again.
5455
file.seek(SeekFrom::Start(0)).unwrap();
5556
let mut contents = Vec::new();

0 commit comments

Comments
 (0)