Skip to content

Commit 2d02e10

Browse files
committed
Auto merge of #1156 - divergentdave:fcntl-F_DUPFD_CLOEXEC, r=RalfJung
Add F_DUPFD/F_DUPFD_CLOEXEC to fcntl shim This adds support for `F_DUPFD` and `F_DUPFD_CLOEXEC` to the shim for `fcntl`. (The `FileHandler` does not track the `FD_CLOEXEC` flag for open files, so these commands are effectively the same.) These changes enable using `File::try_clone`. I also changed the initial value of the `low` field in `FileHandler`, so that it matches the intent of the preceding comment. The `open` shim was pre-incrementing it when choosing new file descriptor numbers, so FD 3 was being skipped.
2 parents 329383a + ae7d98b commit 2d02e10

File tree

3 files changed

+202
-56
lines changed

3 files changed

+202
-56
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![feature(rustc_private)]
22
#![feature(option_expect_none, option_unwrap_none)]
3+
#![feature(map_first_last)]
34
#![warn(rust_2018_idioms)]
45
#![allow(clippy::cast_lossless)]
56

src/shims/fs.rs

Lines changed: 83 additions & 31 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};
@@ -18,18 +18,48 @@ pub struct FileHandle {
1818
writable: bool,
1919
}
2020

21+
#[derive(Debug, Default)]
2122
pub struct FileHandler {
22-
handles: HashMap<i32, FileHandle>,
23-
low: i32,
23+
handles: BTreeMap<i32, FileHandle>,
2424
}
2525

26-
impl Default for FileHandler {
27-
fn default() -> Self {
28-
FileHandler {
29-
handles: Default::default(),
30-
// 0, 1 and 2 are reserved for stdin, stdout and stderr.
31-
low: 3,
32-
}
26+
// fd numbers 0, 1, and 2 are reserved for stdin, stdout, and stderr
27+
const MIN_NORMAL_FILE_FD: i32 = 3;
28+
29+
impl FileHandler {
30+
fn insert_fd(&mut self, file_handle: FileHandle) -> i32 {
31+
self.insert_fd_with_min_fd(file_handle, 0)
32+
}
33+
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, MIN_NORMAL_FILE_FD);
36+
37+
// Find the lowest unused FD, starting from min_fd. If the first such unused FD is in
38+
// between used FDs, the find_map combinator will return it. If the first such unused FD
39+
// is after all other used FDs, the find_map combinator will return None, and we will use
40+
// the FD following the greatest FD thus far.
41+
let candidate_new_fd = self
42+
.handles
43+
.range(min_fd..)
44+
.zip(min_fd..)
45+
.find_map(|((fd, _fh), counter)| {
46+
if *fd != counter {
47+
// There was a gap in the fds stored, return the first unused one
48+
// (note that this relies on BTreeMap iterating in key order)
49+
Some(counter)
50+
} else {
51+
// This fd is used, keep going
52+
None
53+
}
54+
});
55+
let new_fd = candidate_new_fd.unwrap_or_else(|| {
56+
// find_map ran out of BTreeMap entries before finding a free fd, use one plus the
57+
// maximum fd in the map
58+
self.handles.last_entry().map(|entry| entry.key() + 1).unwrap_or(min_fd)
59+
});
60+
61+
self.handles.insert(new_fd, file_handle).unwrap_none();
62+
new_fd
3363
}
3464
}
3565

@@ -107,10 +137,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
107137
let path = this.read_os_str_from_c_str(this.read_scalar(path_op)?.not_undef()?)?;
108138

109139
let fd = options.open(&path).map(|file| {
110-
let mut fh = &mut this.machine.file_handler;
111-
fh.low += 1;
112-
fh.handles.insert(fh.low, FileHandle { file, writable }).unwrap_none();
113-
fh.low
140+
let fh = &mut this.machine.file_handler;
141+
fh.insert_fd(FileHandle { file, writable })
114142
});
115143

116144
this.try_unwrap_io_result(fd)
@@ -120,7 +148,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
120148
&mut self,
121149
fd_op: OpTy<'tcx, Tag>,
122150
cmd_op: OpTy<'tcx, Tag>,
123-
_arg1_op: Option<OpTy<'tcx, Tag>>,
151+
start_op: Option<OpTy<'tcx, Tag>>,
124152
) -> InterpResult<'tcx, i32> {
125153
let this = self.eval_context_mut();
126154

@@ -139,6 +167,31 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
139167
} else {
140168
this.handle_not_found()
141169
}
170+
} else if cmd == this.eval_libc_i32("F_DUPFD")?
171+
|| cmd == this.eval_libc_i32("F_DUPFD_CLOEXEC")?
172+
{
173+
// Note that we always assume the FD_CLOEXEC flag is set for every open file, in part
174+
// because exec() isn't supported. The F_DUPFD and F_DUPFD_CLOEXEC commands only
175+
// differ in whether the FD_CLOEXEC flag is pre-set on the new file descriptor,
176+
// thus they can share the same implementation here.
177+
if fd < MIN_NORMAL_FILE_FD {
178+
throw_unsup_format!("Duplicating file descriptors for stdin, stdout, or stderr is not supported")
179+
}
180+
let start_op = start_op.ok_or_else(|| {
181+
err_unsup_format!(
182+
"fcntl with command F_DUPFD or F_DUPFD_CLOEXEC requires a third argument"
183+
)
184+
})?;
185+
let start = this.read_scalar(start_op)?.to_i32()?;
186+
let fh = &mut this.machine.file_handler;
187+
let (file_result, writable) = match fh.handles.get(&fd) {
188+
Some(FileHandle { file, writable }) => (file.try_clone(), *writable),
189+
None => return this.handle_not_found(),
190+
};
191+
let fd_result = file_result.map(|duplicated| {
192+
fh.insert_fd_with_min_fd(FileHandle { file: duplicated, writable }, start)
193+
});
194+
this.try_unwrap_io_result(fd_result)
142195
} else {
143196
throw_unsup_format!("The {:#x} command is not supported for `fcntl`)", cmd);
144197
}
@@ -151,23 +204,23 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
151204

152205
let fd = this.read_scalar(fd_op)?.to_i32()?;
153206

154-
if let Some(handle) = this.machine.file_handler.handles.remove(&fd) {
207+
if let Some(FileHandle { file, writable }) = this.machine.file_handler.handles.remove(&fd) {
155208
// We sync the file if it was opened in a mode different than read-only.
156-
if handle.writable {
209+
if writable {
157210
// `File::sync_all` does the checks that are done when closing a file. We do this to
158211
// to handle possible errors correctly.
159-
let result = this.try_unwrap_io_result(handle.file.sync_all().map(|_| 0i32));
212+
let result = this.try_unwrap_io_result(file.sync_all().map(|_| 0i32));
160213
// Now we actually close the file.
161-
drop(handle);
214+
drop(file);
162215
// And return the result.
163216
result
164217
} else {
165218
// We drop the file, this closes it but ignores any errors produced when closing
166-
// it. This is done because `File::sync_call` cannot be done over files like
219+
// it. This is done because `File::sync_all` cannot be done over files like
167220
// `/dev/urandom` which are read-only. Check
168221
// https://github.com/rust-lang/miri/issues/999#issuecomment-568920439 for a deeper
169222
// discussion.
170-
drop(handle);
223+
drop(file);
171224
Ok(0)
172225
}
173226
} else {
@@ -200,16 +253,15 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
200253
// host's and target's `isize`. This saves us from having to handle overflows later.
201254
let count = count.min(this.isize_max() as u64).min(isize::max_value() as u64);
202255

203-
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
256+
if let Some(FileHandle { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
204257
// This can never fail because `count` was capped to be smaller than
205258
// `isize::max_value()`.
206259
let count = isize::try_from(count).unwrap();
207260
// We want to read at most `count` bytes. We are sure that `count` is not negative
208261
// because it was a target's `usize`. Also we are sure that its smaller than
209262
// `usize::max_value()` because it is a host's `isize`.
210263
let mut bytes = vec![0; count as usize];
211-
let result = handle
212-
.file
264+
let result = file
213265
.read(&mut bytes)
214266
// `File::read` never returns a value larger than `count`, so this cannot fail.
215267
.map(|c| i64::try_from(c).unwrap());
@@ -255,9 +307,9 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
255307
// host's and target's `isize`. This saves us from having to handle overflows later.
256308
let count = count.min(this.isize_max() as u64).min(isize::max_value() as u64);
257309

258-
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
310+
if let Some(FileHandle { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
259311
let bytes = this.memory.read_bytes(buf, Size::from_bytes(count))?;
260-
let result = handle.file.write(&bytes).map(|c| i64::try_from(c).unwrap());
312+
let result = file.write(&bytes).map(|c| i64::try_from(c).unwrap());
261313
this.try_unwrap_io_result(result)
262314
} else {
263315
this.handle_not_found()
@@ -290,8 +342,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
290342
return Ok(-1);
291343
};
292344

293-
if let Some(handle) = this.machine.file_handler.handles.get_mut(&fd) {
294-
let result = handle.file.seek(seek_from).map(|offset| offset as i64);
345+
if let Some(FileHandle { file, writable: _ }) = this.machine.file_handler.handles.get_mut(&fd) {
346+
let result = file.seek(seek_from).map(|offset| offset as i64);
295347
this.try_unwrap_io_result(result)
296348
} else {
297349
this.handle_not_found()
@@ -652,11 +704,11 @@ impl FileMetadata {
652704
fd: i32,
653705
) -> InterpResult<'tcx, Option<FileMetadata>> {
654706
let option = ecx.machine.file_handler.handles.get(&fd);
655-
let handle = match option {
656-
Some(handle) => handle,
707+
let file = match option {
708+
Some(FileHandle { file, writable: _ }) => file,
657709
None => return ecx.handle_not_found().map(|_: i32| None),
658710
};
659-
let metadata = handle.file.metadata();
711+
let metadata = file.metadata();
660712

661713
FileMetadata::from_meta(ecx, metadata)
662714
}

0 commit comments

Comments
 (0)