Skip to content

Commit b1a308b

Browse files
committed
non-deterministically truncate reads/writes
1 parent bb6eb71 commit b1a308b

20 files changed

+312
-122
lines changed

src/shims/files.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ pub trait FileDescription: std::fmt::Debug + FileDescriptionExt {
167167
throw_unsup_format!("cannot write to {}", self.name());
168168
}
169169

170+
/// Determines whether this FD non-deterministically has its reads and writes shortened.
171+
fn nondet_short_accesses(&self) -> bool {
172+
true
173+
}
174+
170175
/// Seeks to the given offset (which can be relative to the beginning, end, or current position).
171176
/// Returns the new position from the start of the stream.
172177
fn seek<'tcx>(

src/shims/unix/fd.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::io;
55
use std::io::ErrorKind;
66

7+
use rand::Rng;
78
use rustc_abi::Size;
89

910
use crate::helpers::check_min_vararg_count;
@@ -263,9 +264,18 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
263264
return this.set_last_error_and_return(LibcError("EBADF"), dest);
264265
};
265266

267+
// Non-deterministically decide to further reduce the count, simulating a partial read (but
268+
// never to 0, that has different behavior).
269+
let count =
270+
if fd.nondet_short_accesses() && count >= 2 && this.machine.rng.get_mut().random() {
271+
count / 2
272+
} else {
273+
count
274+
};
275+
266276
trace!("read: FD mapped to {fd:?}");
267277
// 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
278+
// because it was a target's `usize`. Also we are sure that it's smaller than
269279
// `usize::MAX` because it is bounded by the host's `isize`.
270280

271281
let finish = {
@@ -328,6 +338,15 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
328338
return this.set_last_error_and_return(LibcError("EBADF"), dest);
329339
};
330340

341+
// Non-deterministically decide to further reduce the count, simulating a partial write (but
342+
// never to 0, that has different behavior).
343+
let count =
344+
if fd.nondet_short_accesses() && count >= 2 && this.machine.rng.get_mut().random() {
345+
count / 2
346+
} else {
347+
count
348+
};
349+
331350
let finish = {
332351
let dest = dest.clone();
333352
callback!(

src/shims/unix/linux_like/eventfd.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ impl FileDescription for EventFd {
3737
"event"
3838
}
3939

40+
fn nondet_short_accesses(&self) -> bool {
41+
// We always read and write exactly one `u64`.
42+
false
43+
}
44+
4045
fn close<'tcx>(
4146
self,
4247
_communicate_allowed: bool,

src/shims/windows/fs.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
462462
};
463463
let io_status_info = this.project_field_named(&io_status_block, "Information")?;
464464

465+
// It seems like short writes are not a thing on Windows, so we don't truncate `count` here.
466+
// FIXME: if we are on a Unix host, short host writes are still visible to the program!
467+
465468
let finish = {
466469
let io_status = io_status.clone();
467470
let io_status_info = io_status_info.clone();
@@ -491,7 +494,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
491494
}}
492495
)
493496
};
494-
495497
desc.write(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
496498

497499
// Return status is written to `dest` and `io_status_block` on callback completion.
@@ -556,6 +558,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
556558
};
557559
let io_status_info = this.project_field_named(&io_status_block, "Information")?;
558560

561+
let fd = match handle {
562+
Handle::File(fd) => fd,
563+
_ => this.invalid_handle("NtWriteFile")?,
564+
};
565+
566+
let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtReadFile")? };
567+
568+
// It seems like short reads are not a thing on Windows, so we don't truncate `count` here.
569+
// FIXME: if we are on a Unix host, short host reads are still visible to the program!
570+
559571
let finish = {
560572
let io_status = io_status.clone();
561573
let io_status_info = io_status_info.clone();
@@ -585,14 +597,6 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
585597
}}
586598
)
587599
};
588-
589-
let fd = match handle {
590-
Handle::File(fd) => fd,
591-
_ => this.invalid_handle("NtWriteFile")?,
592-
};
593-
594-
let Some(desc) = this.machine.fds.get(fd) else { this.invalid_handle("NtReadFile")? };
595-
596600
desc.read(this.machine.communicate(), buf, count.try_into().unwrap(), this, finish)?;
597601

598602
// See NtWriteFile for commentary on this

tests/fail-dep/libc/libc-epoll-data-race.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use std::convert::TryInto;
1010
use std::thread;
1111
use std::thread::spawn;
1212

13+
#[path = "../../utils/libc.rs"]
14+
mod libc_utils;
15+
1316
#[track_caller]
1417
fn check_epoll_wait<const N: usize>(epfd: i32, expected_notifications: &[(u32, u64)]) {
1518
let epoll_event = libc::epoll_event { events: 0, u64: 0 };
@@ -69,12 +72,12 @@ fn main() {
6972
unsafe { VAL_ONE = 41 };
7073

7174
let data = "abcde".as_bytes().as_ptr();
72-
let res = unsafe { libc::write(fds_a[0], data as *const libc::c_void, 5) };
75+
let res = unsafe { libc_utils::write_all(fds_a[0], data as *const libc::c_void, 5) };
7376
assert_eq!(res, 5);
7477

7578
unsafe { VAL_TWO = 51 };
7679

77-
let res = unsafe { libc::write(fds_b[0], data as *const libc::c_void, 5) };
80+
let res = unsafe { libc_utils::write_all(fds_b[0], data as *const libc::c_void, 5) };
7881
assert_eq!(res, 5);
7982
});
8083
thread::yield_now();

tests/fail-dep/libc/libc-read-and-uninit-premature-eof.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use std::mem::MaybeUninit;
1010
#[path = "../../utils/mod.rs"]
1111
mod utils;
1212

13+
#[path = "../../utils/libc.rs"]
14+
mod libc_utils;
15+
1316
fn main() {
1417
let path =
1518
utils::prepare_with_content("fail-libc-read-and-uninit-premature-eof.txt", &[1u8, 2, 3]);
@@ -18,8 +21,9 @@ fn main() {
1821
let fd = libc::open(cpath.as_ptr(), libc::O_RDONLY);
1922
assert_ne!(fd, -1);
2023
let mut buf: MaybeUninit<[u8; 4]> = std::mem::MaybeUninit::uninit();
21-
// Read 4 bytes from a 3-byte file.
22-
assert_eq!(libc::read(fd, buf.as_mut_ptr().cast::<std::ffi::c_void>(), 4), 3);
24+
// Read as much as we can from a 3-byte file.
25+
let res = libc_utils::read_all(fd, buf.as_mut_ptr().cast::<std::ffi::c_void>(), 4);
26+
assert!(res == 3);
2327
buf.assume_init(); //~ERROR: encountered uninitialized memory, but expected an integer
2428
assert_eq!(libc::close(fd), 0);
2529
}

tests/fail-dep/libc/libc_epoll_block_two_thread.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ fn main() {
7575
});
7676

7777
let thread3 = spawn(move || {
78+
// Just a single write, so we only wake up one of them.
7879
let data = "abcde".as_bytes().as_ptr();
7980
let res = unsafe { libc::write(fds[1], data as *const libc::c_void, 5) };
80-
assert_eq!(res, 5);
81+
assert!(res > 0 && res <= 5);
8182
});
8283

8384
thread1.join().unwrap();

tests/fail-dep/libc/socketpair_block_read_twice.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// test_race depends on a deterministic schedule.
55
//@compile-flags: -Zmiri-deterministic-concurrency
66
//@error-in-other-file: deadlock
7+
//@require-annotations-for-level: error
78

89
use std::thread;
910

@@ -22,24 +23,26 @@ fn main() {
2223
assert_eq!(res, 0);
2324
let thread1 = thread::spawn(move || {
2425
// Let this thread block on read.
25-
let mut buf: [u8; 3] = [0; 3];
26+
let mut buf: [u8; 1] = [0; 1];
2627
let res = unsafe { libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t) };
27-
assert_eq!(res, 3);
28-
assert_eq!(&buf, "abc".as_bytes());
28+
assert_eq!(res, buf.len().cast_signed());
29+
assert_eq!(&buf, "a".as_bytes());
2930
});
3031
let thread2 = thread::spawn(move || {
3132
// Let this thread block on read.
32-
let mut buf: [u8; 3] = [0; 3];
33-
let res = unsafe { libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t) };
34-
//~^ERROR: deadlocked
35-
assert_eq!(res, 3);
36-
assert_eq!(&buf, "abc".as_bytes());
33+
let mut buf: [u8; 1] = [0; 1];
34+
let res = unsafe {
35+
libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t)
36+
//~^ERROR: deadlock
37+
};
38+
assert_eq!(res, buf.len().cast_signed());
39+
assert_eq!(&buf, "a".as_bytes());
3740
});
3841
let thread3 = thread::spawn(move || {
3942
// Unblock thread1 by writing something.
40-
let data = "abc".as_bytes().as_ptr();
41-
let res = unsafe { libc::write(fds[0], data as *const libc::c_void, 3) };
42-
assert_eq!(res, 3);
43+
let data = "a".as_bytes();
44+
let res = unsafe { libc::write(fds[0], data.as_ptr() as *const libc::c_void, data.len()) };
45+
assert_eq!(res, data.len().cast_signed());
4346
});
4447
thread1.join().unwrap();
4548
thread2.join().unwrap();

tests/fail-dep/libc/socketpair_block_read_twice.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ error: the evaluated program deadlocked
2323
error: the evaluated program deadlocked
2424
--> tests/fail-dep/libc/socketpair_block_read_twice.rs:LL:CC
2525
|
26-
LL | let res = unsafe { libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t) };
27-
| ^ this thread got stuck here
26+
LL | libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t)
27+
| ^ this thread got stuck here
2828
|
2929
= note: BACKTRACE on thread `unnamed-ID`:
3030
= note: inside closure at tests/fail-dep/libc/socketpair_block_read_twice.rs:LL:CC

tests/fail-dep/libc/socketpair_block_write_twice.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,49 @@
44
// test_race depends on a deterministic schedule.
55
//@compile-flags: -Zmiri-deterministic-concurrency
66
//@error-in-other-file: deadlock
7+
//@require-annotations-for-level: error
78

89
use std::thread;
910

11+
#[path = "../../utils/libc.rs"]
12+
mod libc_utils;
13+
1014
// Test the behaviour of a thread being blocked on write, get unblocked, then blocked again.
1115

1216
// The expected execution is
1317
// 1. Thread 1 blocks.
1418
// 2. Thread 2 blocks.
1519
// 3. Thread 3 unblocks both thread 1 and thread 2.
16-
// 4. Thread 1 reads.
20+
// 4. Thread 1 writes.
1721
// 5. Thread 2's `write` can never complete -> deadlocked.
1822
fn main() {
1923
let mut fds = [-1, -1];
2024
let res = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_STREAM, 0, fds.as_mut_ptr()) };
2125
assert_eq!(res, 0);
2226
let arr1: [u8; 212992] = [1; 212992];
2327
// Exhaust the space in the buffer so the subsequent write will block.
24-
let res = unsafe { libc::write(fds[0], arr1.as_ptr() as *const libc::c_void, 212992) };
28+
let res =
29+
unsafe { libc_utils::write_all(fds[0], arr1.as_ptr() as *const libc::c_void, 212992) };
2530
assert_eq!(res, 212992);
2631
let thread1 = thread::spawn(move || {
27-
let data = "abc".as_bytes().as_ptr();
32+
let data = "a".as_bytes();
2833
// The write below will be blocked because the buffer is already full.
29-
let res = unsafe { libc::write(fds[0], data as *const libc::c_void, 3) };
30-
assert_eq!(res, 3);
34+
let res = unsafe { libc::write(fds[0], data.as_ptr() as *const libc::c_void, data.len()) };
35+
assert_eq!(res, data.len().cast_signed());
3136
});
3237
let thread2 = thread::spawn(move || {
33-
let data = "abc".as_bytes().as_ptr();
38+
let data = "a".as_bytes();
3439
// The write below will be blocked because the buffer is already full.
35-
let res = unsafe { libc::write(fds[0], data as *const libc::c_void, 3) };
36-
//~^ERROR: deadlocked
37-
assert_eq!(res, 3);
40+
let res = unsafe { libc::write(fds[0], data.as_ptr() as *const libc::c_void, data.len()) };
41+
//~^ERROR: deadlock
42+
assert_eq!(res, data.len().cast_signed());
3843
});
3944
let thread3 = thread::spawn(move || {
4045
// Unblock thread1 by freeing up some space.
41-
let mut buf: [u8; 3] = [0; 3];
46+
let mut buf: [u8; 1] = [0; 1];
4247
let res = unsafe { libc::read(fds[1], buf.as_mut_ptr().cast(), buf.len() as libc::size_t) };
43-
assert_eq!(res, 3);
44-
assert_eq!(buf, [1, 1, 1]);
48+
assert_eq!(res, buf.len().cast_signed());
49+
assert_eq!(buf, [1]);
4550
});
4651
thread1.join().unwrap();
4752
thread2.join().unwrap();

0 commit comments

Comments
 (0)