mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-22 06:44:35 +00:00
Auto merge of #109241 - saethlin:miri, r=oli-obk
update Miri r? `@oli-obk`
This commit is contained in:
commit
c50c62d225
8
src/tools/miri/.github/workflows/ci.yml
vendored
8
src/tools/miri/.github/workflows/ci.yml
vendored
@ -54,8 +54,8 @@ jobs:
|
||||
# contains package information of crates installed via `cargo install`.
|
||||
~/.cargo/.crates.toml
|
||||
~/.cargo/.crates2.json
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo
|
||||
key: ${{ runner.os }}-cargo-reset20230315-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-reset20230315
|
||||
|
||||
- name: Install rustup-toolchain-install-master
|
||||
if: ${{ steps.cache.outputs.cache-hit != 'true' }}
|
||||
@ -106,8 +106,8 @@ jobs:
|
||||
# contains package information of crates installed via `cargo install`.
|
||||
~/.cargo/.crates.toml
|
||||
~/.cargo/.crates2.json
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo
|
||||
key: ${{ runner.os }}-cargo-reset20230315-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-reset20230315
|
||||
|
||||
- name: Install rustup-toolchain-install-master
|
||||
if: ${{ steps.cache.outputs.cache-hit != 'true' }}
|
||||
|
@ -129,18 +129,15 @@ development version of Miri using
|
||||
./miri install
|
||||
```
|
||||
|
||||
and then you can use it as if it was installed by `rustup`. Make sure you use
|
||||
the same toolchain when calling `cargo miri` that you used when installing Miri!
|
||||
Usually this means you have to write `cargo +miri miri ...` to select the `miri`
|
||||
toolchain that was installed by `./miri toolchain`.
|
||||
and then you can use it as if it was installed by `rustup` as a component of the
|
||||
`miri` toolchain. Note that the `miri` and `cargo-miri` executables are placed
|
||||
in the `miri` toolchain's sysroot to prevent conflicts with other toolchains.
|
||||
The Miri binaries in the `cargo` bin directory (usually `~/.cargo/bin`) are managed by rustup.
|
||||
|
||||
There's a test for the cargo wrapper in the `test-cargo-miri` directory; run
|
||||
`./run-test.py` in there to execute it. Like `./miri test`, this respects the
|
||||
`MIRI_TEST_TARGET` environment variable to execute the test for another target.
|
||||
|
||||
Note that installing Miri like this will "take away" Miri management from `rustup`.
|
||||
If you want to later go back to a rustup-installed Miri, run `rustup update`.
|
||||
|
||||
### Using a modified standard library
|
||||
|
||||
Miri re-builds the standard library into a custom sysroot, so it is fairly easy
|
||||
|
@ -15,6 +15,8 @@ for example:
|
||||
or an invalid enum discriminant)
|
||||
* **Experimental**: Violations of the [Stacked Borrows] rules governing aliasing
|
||||
for reference types
|
||||
* **Experimental**: Violations of the Tree Borrows aliasing rules, as an optional
|
||||
alternative to [Stacked Borrows]
|
||||
* **Experimental**: Data races
|
||||
|
||||
On top of that, Miri will also tell you about memory leaks: when there is memory
|
||||
@ -225,6 +227,26 @@ degree documented below):
|
||||
reduced feature set. We might ship Miri with a nightly even when some features
|
||||
on these targets regress.
|
||||
|
||||
### Running tests in parallel
|
||||
|
||||
Though it implements Rust threading, Miri itself is a single-threaded interpreter.
|
||||
This means that when running `cargo miri test`, you will probably see a dramatic
|
||||
increase in the amount of time it takes to run your whole test suite due to the
|
||||
inherent interpreter slowdown and a loss of parallelism.
|
||||
|
||||
You can get your test suite's parallelism back by running `cargo miri nextest run -jN`
|
||||
(note that you will need [`cargo-nextest`](https://nexte.st) installed).
|
||||
This works because `cargo-nextest` collects a list of all tests then launches a
|
||||
separate `cargo miri run` for each test. You will need to specify a `-j` or `--test-threads`;
|
||||
by default `cargo miri nextest run` runs one test at a time. For more details, see the
|
||||
[`cargo-nextest` Miri documentation](https://nexte.st/book/miri.html).
|
||||
|
||||
Note: This one-test-per-process model means that `cargo miri test` is able to detect data
|
||||
races where two tests race on a shared resource, but `cargo miri nextest run` will not detect
|
||||
such races.
|
||||
|
||||
Note: `cargo-nextest` does not support doctests, see https://github.com/nextest-rs/nextest/issues/16
|
||||
|
||||
### Common Problems
|
||||
|
||||
When using the above instructions, you may encounter a number of confusing compiler
|
||||
@ -337,9 +359,11 @@ to Miri failing to detect cases of undefined behavior in a program.
|
||||
* `-Zmiri-disable-data-race-detector` disables checking for data races. Using
|
||||
this flag is **unsound**. This implies `-Zmiri-disable-weak-memory-emulation`.
|
||||
* `-Zmiri-disable-stacked-borrows` disables checking the experimental
|
||||
[Stacked Borrows] aliasing rules. This can make Miri run faster, but it also
|
||||
means no aliasing violations will be detected. Using this flag is **unsound**
|
||||
(but the affected soundness rules are experimental).
|
||||
aliasing rules to track borrows ([Stacked Borrows] and Tree Borrows).
|
||||
This can make Miri run faster, but it also means no aliasing violations will
|
||||
be detected. Using this flag is **unsound** (but the affected soundness rules
|
||||
are experimental). Later flags take precedence: borrow tracking can be reactivated
|
||||
by `-Zmiri-tree-borrows`.
|
||||
* `-Zmiri-disable-validation` disables enforcing validity invariants, which are
|
||||
enforced by default. This is mostly useful to focus on other failures (such
|
||||
as out-of-bounds accesses) first. Setting this flag means Miri can miss bugs
|
||||
@ -401,6 +425,9 @@ to Miri failing to detect cases of undefined behavior in a program.
|
||||
* `-Zmiri-track-weak-memory-loads` shows a backtrace when weak memory emulation returns an outdated
|
||||
value from a load. This can help diagnose problems that disappear under
|
||||
`-Zmiri-disable-weak-memory-emulation`.
|
||||
* `-Zmiri-tree-borrows` replaces [Stacked Borrows] with the Tree Borrows rules.
|
||||
The soundness rules are already experimental without this flag, but even more
|
||||
so with this flag.
|
||||
* `-Zmiri-force-page-size=<num>` overrides the default page size for an architecture, in multiples of 1k.
|
||||
`4` is default for most targets. This value should always be a power of 2 and nonzero.
|
||||
|
||||
@ -415,7 +442,7 @@ Some native rustc `-Z` flags are also very relevant for Miri:
|
||||
functions. This is needed so that Miri can execute such functions, so Miri
|
||||
sets this flag per default.
|
||||
* `-Zmir-emit-retag` controls whether `Retag` statements are emitted. Miri
|
||||
enables this per default because it is needed for [Stacked Borrows].
|
||||
enables this per default because it is needed for [Stacked Borrows] and Tree Borrows.
|
||||
|
||||
Moreover, Miri recognizes some environment variables:
|
||||
|
||||
@ -481,120 +508,8 @@ binaries, and as such worth documenting:
|
||||
## Miri `extern` functions
|
||||
|
||||
Miri provides some `extern` functions that programs can import to access
|
||||
Miri-specific functionality:
|
||||
|
||||
```rust
|
||||
#[cfg(miri)]
|
||||
extern "Rust" {
|
||||
/// Miri-provided extern function to mark the block `ptr` points to as a "root"
|
||||
/// for some static memory. This memory and everything reachable by it is not
|
||||
/// considered leaking even if it still exists when the program terminates.
|
||||
///
|
||||
/// `ptr` has to point to the beginning of an allocated block.
|
||||
fn miri_static_root(ptr: *const u8);
|
||||
|
||||
// Miri-provided extern function to get the amount of frames in the current backtrace.
|
||||
// The `flags` argument must be `0`.
|
||||
fn miri_backtrace_size(flags: u64) -> usize;
|
||||
|
||||
/// Miri-provided extern function to obtain a backtrace of the current call stack.
|
||||
/// This writes a slice of pointers into `buf` - each pointer is an opaque value
|
||||
/// that is only useful when passed to `miri_resolve_frame`.
|
||||
/// `buf` must have `miri_backtrace_size(0) * pointer_size` bytes of space.
|
||||
/// The `flags` argument must be `1`.
|
||||
fn miri_get_backtrace(flags: u64, buf: *mut *mut ());
|
||||
|
||||
/// Miri-provided extern function to resolve a frame pointer obtained
|
||||
/// from `miri_get_backtrace`. The `flags` argument must be `1`,
|
||||
/// and `MiriFrame` should be declared as follows:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[repr(C)]
|
||||
/// struct MiriFrame {
|
||||
/// // The size of the name of the function being executed, encoded in UTF-8
|
||||
/// name_len: usize,
|
||||
/// // The size of filename of the function being executed, encoded in UTF-8
|
||||
/// filename_len: usize,
|
||||
/// // The line number currently being executed in `filename`, starting from '1'.
|
||||
/// lineno: u32,
|
||||
/// // The column number currently being executed in `filename`, starting from '1'.
|
||||
/// colno: u32,
|
||||
/// // The function pointer to the function currently being executed.
|
||||
/// // This can be compared against function pointers obtained by
|
||||
/// // casting a function (e.g. `my_fn as *mut ()`)
|
||||
/// fn_ptr: *mut ()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The fields must be declared in exactly the same order as they appear in `MiriFrame` above.
|
||||
/// This function can be called on any thread (not just the one which obtained `frame`).
|
||||
fn miri_resolve_frame(frame: *mut (), flags: u64) -> MiriFrame;
|
||||
|
||||
/// Miri-provided extern function to get the name and filename of the frame provided by `miri_resolve_frame`.
|
||||
/// `name_buf` and `filename_buf` should be allocated with the `name_len` and `filename_len` fields of `MiriFrame`.
|
||||
/// The flags argument must be `0`.
|
||||
fn miri_resolve_frame_names(ptr: *mut (), flags: u64, name_buf: *mut u8, filename_buf: *mut u8);
|
||||
|
||||
/// Miri-provided extern function to begin unwinding with the given payload.
|
||||
///
|
||||
/// This is internal and unstable and should not be used; we give it here
|
||||
/// just to be complete.
|
||||
fn miri_start_panic(payload: *mut u8) -> !;
|
||||
|
||||
/// Miri-provided extern function to get the internal unique identifier for the allocation that a pointer
|
||||
/// points to. If this pointer is invalid (not pointing to an allocation), interpretation will abort.
|
||||
///
|
||||
/// This is only useful as an input to `miri_print_borrow_stacks`, and it is a separate call because
|
||||
/// getting a pointer to an allocation at runtime can change the borrow stacks in the allocation.
|
||||
/// This function should be considered unstable. It exists only to support `miri_print_borrow_stacks` and so
|
||||
/// inherits all of its instability.
|
||||
fn miri_get_alloc_id(ptr: *const ()) -> u64;
|
||||
|
||||
/// Miri-provided extern function to print (from the interpreter, not the program) the contents of all
|
||||
/// borrow stacks in an allocation. The leftmost tag is the bottom of the stack.
|
||||
/// The format of what this emits is unstable and may change at any time. In particular, users should be
|
||||
/// aware that Miri will periodically attempt to garbage collect the contents of all stacks. Callers of
|
||||
/// this function may wish to pass `-Zmiri-tag-gc=0` to disable the GC.
|
||||
///
|
||||
/// This function is extremely unstable. At any time the format of its output may change, its signature may
|
||||
/// change, or it may be removed entirely.
|
||||
fn miri_print_borrow_stacks(alloc_id: u64);
|
||||
|
||||
/// Miri-provided extern function to print (from the interpreter, not the
|
||||
/// program) the contents of a section of program memory, as bytes. Bytes
|
||||
/// written using this function will emerge from the interpreter's stdout.
|
||||
fn miri_write_to_stdout(bytes: &[u8]);
|
||||
|
||||
/// Miri-provided extern function to print (from the interpreter, not the
|
||||
/// program) the contents of a section of program memory, as bytes. Bytes
|
||||
/// written using this function will emerge from the interpreter's stderr.
|
||||
fn miri_write_to_stderr(bytes: &[u8]);
|
||||
|
||||
/// Miri-provided extern function to allocate memory from the interpreter.
|
||||
///
|
||||
/// This is useful when no fundamental way of allocating memory is
|
||||
/// available, e.g. when using `no_std` + `alloc`.
|
||||
fn miri_alloc(size: usize, align: usize) -> *mut u8;
|
||||
|
||||
/// Miri-provided extern function to deallocate memory.
|
||||
fn miri_dealloc(ptr: *mut u8, size: usize, align: usize);
|
||||
|
||||
/// Convert a path from the host Miri runs on to the target Miri interprets.
|
||||
/// Performs conversion of path separators as needed.
|
||||
///
|
||||
/// Usually Miri performs this kind of conversion automatically. However, manual conversion
|
||||
/// might be necessary when reading an environment variable that was set on the host
|
||||
/// (such as TMPDIR) and using it as a target path.
|
||||
///
|
||||
/// Only works with isolation disabled.
|
||||
///
|
||||
/// `in` must point to a null-terminated string, and will be read as the input host path.
|
||||
/// `out` must point to at least `out_size` many bytes, and the result will be stored there
|
||||
/// with a null terminator.
|
||||
/// Returns 0 if the `out` buffer was large enough, and the required size otherwise.
|
||||
fn miri_host_to_target_path(path: *const std::ffi::c_char, out: *mut std::ffi::c_char, out_size: usize) -> usize;
|
||||
}
|
||||
```
|
||||
Miri-specific functionality. They are declared in
|
||||
[/tests/utils/miri\_extern.rs](/tests/utils/miri_extern.rs).
|
||||
|
||||
## Contributing and getting help
|
||||
|
||||
|
@ -62,8 +62,8 @@ function run_tests {
|
||||
if [ "$HOST_TARGET" = x86_64-unknown-linux-gnu ]; then
|
||||
# These act up on Windows (`which miri` produces a filename that does not exist?!?),
|
||||
# so let's do this only on Linux. Also makes sure things work without these set.
|
||||
export RUSTC=$(which rustc)
|
||||
export MIRI=$(which miri)
|
||||
export RUSTC=$(which rustc) # Produces a warning unless we also set MIRI
|
||||
export MIRI=$(rustc +miri --print sysroot)/bin/miri
|
||||
fi
|
||||
mkdir -p .cargo
|
||||
echo 'build.rustc-wrapper = "thisdoesnotexist"' > .cargo/config.toml
|
||||
|
@ -6,8 +6,8 @@ USAGE=$(cat <<"EOF"
|
||||
./miri install <flags>:
|
||||
Installs the miri driver and cargo-miri. <flags> are passed to `cargo
|
||||
install`. Sets up the rpath such that the installed binary should work in any
|
||||
working directory. However, the rustup toolchain when invoking `cargo miri`
|
||||
needs to be the same one used for `./miri install`.
|
||||
working directory. Note that the binaries are placed in the `miri` toolchain
|
||||
sysroot, to prevent conflicts with other toolchains.
|
||||
|
||||
./miri build <flags>:
|
||||
Just build miri. <flags> are passed to `cargo build`.
|
||||
@ -281,8 +281,9 @@ find_sysroot() {
|
||||
case "$COMMAND" in
|
||||
install)
|
||||
# "--locked" to respect the Cargo.lock file if it exists.
|
||||
$CARGO install $CARGO_EXTRA_FLAGS --path "$MIRIDIR" --force --locked "$@"
|
||||
$CARGO install $CARGO_EXTRA_FLAGS --path "$MIRIDIR"/cargo-miri --force --locked "$@"
|
||||
# Install binaries to the miri toolchain's sysroot so they do not interact with other toolchains.
|
||||
$CARGO install $CARGO_EXTRA_FLAGS --path "$MIRIDIR" --force --locked --root "$SYSROOT" "$@"
|
||||
$CARGO install $CARGO_EXTRA_FLAGS --path "$MIRIDIR"/cargo-miri --force --locked --root "$SYSROOT" "$@"
|
||||
;;
|
||||
check)
|
||||
# Check, and let caller control flags.
|
||||
|
@ -1 +1 @@
|
||||
c4e0cd966062ca67daed20775f4e8a60c28e57df
|
||||
511364e7874dba9649a264100407e4bffe7b5425
|
||||
|
@ -32,7 +32,7 @@ use rustc_middle::{
|
||||
};
|
||||
use rustc_session::{config::CrateType, search_paths::PathKind, CtfeBacktrace};
|
||||
|
||||
use miri::{BacktraceStyle, ProvenanceMode, RetagFields};
|
||||
use miri::{BacktraceStyle, BorrowTrackerMethod, ProvenanceMode, RetagFields};
|
||||
|
||||
struct MiriCompilerCalls {
|
||||
miri_config: miri::MiriConfig,
|
||||
@ -317,6 +317,8 @@ fn main() {
|
||||
miri_config.validate = false;
|
||||
} else if arg == "-Zmiri-disable-stacked-borrows" {
|
||||
miri_config.borrow_tracker = None;
|
||||
} else if arg == "-Zmiri-tree-borrows" {
|
||||
miri_config.borrow_tracker = Some(BorrowTrackerMethod::TreeBorrows);
|
||||
} else if arg == "-Zmiri-disable-data-race-detector" {
|
||||
miri_config.data_race_detector = false;
|
||||
miri_config.weak_memory_emulation = false;
|
||||
|
@ -11,6 +11,7 @@ use rustc_target::abi::Size;
|
||||
|
||||
use crate::*;
|
||||
pub mod stacked_borrows;
|
||||
pub mod tree_borrows;
|
||||
|
||||
pub type CallId = NonZeroU64;
|
||||
|
||||
@ -230,8 +231,10 @@ impl GlobalStateInner {
|
||||
/// Which borrow tracking method to use
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum BorrowTrackerMethod {
|
||||
/// Stacked Borrows, as implemented in borrow_tracker/stacked
|
||||
/// Stacked Borrows, as implemented in borrow_tracker/stacked_borrows
|
||||
StackedBorrows,
|
||||
/// Tree borrows, as implemented in borrow_tracker/tree_borrows
|
||||
TreeBorrows,
|
||||
}
|
||||
|
||||
impl BorrowTrackerMethod {
|
||||
@ -258,6 +261,10 @@ impl GlobalStateInner {
|
||||
AllocState::StackedBorrows(Box::new(RefCell::new(Stacks::new_allocation(
|
||||
id, alloc_size, self, kind, machine,
|
||||
)))),
|
||||
BorrowTrackerMethod::TreeBorrows =>
|
||||
AllocState::TreeBorrows(Box::new(RefCell::new(Tree::new_allocation(
|
||||
id, alloc_size, self, kind, machine,
|
||||
)))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,6 +280,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
|
||||
match method {
|
||||
BorrowTrackerMethod::StackedBorrows => this.sb_retag_ptr_value(kind, val),
|
||||
BorrowTrackerMethod::TreeBorrows => this.tb_retag_ptr_value(kind, val),
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,6 +293,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
|
||||
match method {
|
||||
BorrowTrackerMethod::StackedBorrows => this.sb_retag_place_contents(kind, place),
|
||||
BorrowTrackerMethod::TreeBorrows => this.tb_retag_place_contents(kind, place),
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,6 +302,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
|
||||
match method {
|
||||
BorrowTrackerMethod::StackedBorrows => this.sb_retag_return_place(),
|
||||
BorrowTrackerMethod::TreeBorrows => this.tb_retag_return_place(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,6 +311,34 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
|
||||
match method {
|
||||
BorrowTrackerMethod::StackedBorrows => this.sb_expose_tag(alloc_id, tag),
|
||||
BorrowTrackerMethod::TreeBorrows => this.tb_expose_tag(alloc_id, tag),
|
||||
}
|
||||
}
|
||||
|
||||
fn give_pointer_debug_name(
|
||||
&mut self,
|
||||
ptr: Pointer<Option<Provenance>>,
|
||||
nth_parent: u8,
|
||||
name: &str,
|
||||
) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
|
||||
match method {
|
||||
BorrowTrackerMethod::StackedBorrows => {
|
||||
this.tcx.tcx.sess.warn("Stacked Borrows does not support named pointers; `miri_pointer_name` is a no-op");
|
||||
Ok(())
|
||||
}
|
||||
BorrowTrackerMethod::TreeBorrows =>
|
||||
this.tb_give_pointer_debug_name(ptr, nth_parent, name),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_borrow_state(&mut self, alloc_id: AllocId, show_unnamed: bool) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
let method = this.machine.borrow_tracker.as_ref().unwrap().borrow().borrow_tracker_method;
|
||||
match method {
|
||||
BorrowTrackerMethod::StackedBorrows => this.print_stacks(alloc_id),
|
||||
BorrowTrackerMethod::TreeBorrows => this.print_tree(alloc_id, show_unnamed),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -310,6 +348,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
pub enum AllocState {
|
||||
/// Data corresponding to Stacked Borrows
|
||||
StackedBorrows(Box<RefCell<stacked_borrows::AllocState>>),
|
||||
/// Data corresponding to Tree Borrows
|
||||
TreeBorrows(Box<RefCell<tree_borrows::AllocState>>),
|
||||
}
|
||||
|
||||
impl machine::AllocExtra {
|
||||
@ -328,6 +368,14 @@ impl machine::AllocExtra {
|
||||
_ => panic!("expected Stacked Borrows borrow tracking, got something else"),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn borrow_tracker_tb(&self) -> &RefCell<tree_borrows::AllocState> {
|
||||
match self.borrow_tracker {
|
||||
Some(AllocState::TreeBorrows(ref tb)) => tb,
|
||||
_ => panic!("expected Tree Borrows borrow tracking, got something else"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AllocState {
|
||||
@ -341,6 +389,14 @@ impl AllocState {
|
||||
match self {
|
||||
AllocState::StackedBorrows(sb) =>
|
||||
sb.borrow_mut().before_memory_read(alloc_id, prov_extra, range, machine),
|
||||
AllocState::TreeBorrows(tb) =>
|
||||
tb.borrow_mut().before_memory_access(
|
||||
AccessKind::Read,
|
||||
alloc_id,
|
||||
prov_extra,
|
||||
range,
|
||||
machine,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,6 +410,14 @@ impl AllocState {
|
||||
match self {
|
||||
AllocState::StackedBorrows(sb) =>
|
||||
sb.get_mut().before_memory_write(alloc_id, prov_extra, range, machine),
|
||||
AllocState::TreeBorrows(tb) =>
|
||||
tb.get_mut().before_memory_access(
|
||||
AccessKind::Write,
|
||||
alloc_id,
|
||||
prov_extra,
|
||||
range,
|
||||
machine,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,12 +431,15 @@ impl AllocState {
|
||||
match self {
|
||||
AllocState::StackedBorrows(sb) =>
|
||||
sb.get_mut().before_memory_deallocation(alloc_id, prov_extra, range, machine),
|
||||
AllocState::TreeBorrows(tb) =>
|
||||
tb.get_mut().before_memory_deallocation(alloc_id, prov_extra, range, machine),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_unreachable_tags(&self, tags: &FxHashSet<BorTag>) {
|
||||
match self {
|
||||
AllocState::StackedBorrows(sb) => sb.borrow_mut().remove_unreachable_tags(tags),
|
||||
AllocState::TreeBorrows(tb) => tb.borrow_mut().remove_unreachable_tags(tags),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -381,6 +448,7 @@ impl VisitTags for AllocState {
|
||||
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
|
||||
match self {
|
||||
AllocState::StackedBorrows(sb) => sb.visit_tags(visit),
|
||||
AllocState::TreeBorrows(tb) => tb.visit_tags(visit),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
592
src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs
Normal file
592
src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs
Normal file
@ -0,0 +1,592 @@
|
||||
use rustc_data_structures::fx::FxHashMap;
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::borrow_tracker::tree_borrows::{
|
||||
err_tb_ub, perms::Permission, tree::LocationState, unimap::UniIndex,
|
||||
};
|
||||
use crate::borrow_tracker::{AccessKind, ProtectorKind};
|
||||
use crate::*;
|
||||
|
||||
/// Some information that is irrelevant for the algorithm but very
|
||||
/// convenient to know about a tag for debugging and testing.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NodeDebugInfo {
|
||||
/// The tag in question.
|
||||
pub tag: BorTag,
|
||||
/// Name(s) that were associated with this tag (comma-separated).
|
||||
/// Typically the name of the variable holding the corresponding
|
||||
/// pointer in the source code.
|
||||
/// Helps match tag numbers to human-readable names.
|
||||
pub name: Option<String>,
|
||||
}
|
||||
impl NodeDebugInfo {
|
||||
/// New node info with a name.
|
||||
pub fn new(tag: BorTag) -> Self {
|
||||
Self { tag, name: None }
|
||||
}
|
||||
|
||||
/// Add a name to the tag. If a same tag is associated to several pointers,
|
||||
/// it can have several names which will be separated by commas.
|
||||
fn add_name(&mut self, name: &str) {
|
||||
if let Some(ref mut prev_name) = &mut self.name {
|
||||
prev_name.push(',');
|
||||
prev_name.push_str(name);
|
||||
} else {
|
||||
self.name = Some(String::from(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NodeDebugInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref name) = self.name {
|
||||
write!(f, "{tag:?} (also named '{name}')", tag = self.tag)
|
||||
} else {
|
||||
write!(f, "{tag:?}", tag = self.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'tcx> Tree {
|
||||
/// Climb the tree to get the tag of a distant ancestor.
|
||||
/// Allows operations on tags that are unreachable by the program
|
||||
/// but still exist in the tree. Not guaranteed to perform consistently
|
||||
/// if `tag-gc=1`.
|
||||
fn nth_parent(&self, tag: BorTag, nth_parent: u8) -> Option<BorTag> {
|
||||
let mut idx = self.tag_mapping.get(&tag).unwrap();
|
||||
for _ in 0..nth_parent {
|
||||
let node = self.nodes.get(idx).unwrap();
|
||||
idx = node.parent?;
|
||||
}
|
||||
Some(self.nodes.get(idx).unwrap().tag)
|
||||
}
|
||||
|
||||
/// Debug helper: assign name to tag.
|
||||
pub fn give_pointer_debug_name(
|
||||
&mut self,
|
||||
tag: BorTag,
|
||||
nth_parent: u8,
|
||||
name: &str,
|
||||
) -> InterpResult<'tcx> {
|
||||
let tag = self.nth_parent(tag, nth_parent).unwrap();
|
||||
let idx = self.tag_mapping.get(&tag).unwrap();
|
||||
if let Some(node) = self.nodes.get_mut(idx) {
|
||||
node.debug_info.add_name(name);
|
||||
} else {
|
||||
eprintln!("Tag {tag:?} (to be named '{name}') not found!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Debug helper: determines if the tree contains a tag.
|
||||
pub fn is_allocation_of(&self, tag: BorTag) -> bool {
|
||||
self.tag_mapping.contains_key(&tag)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) enum TransitionError {
|
||||
/// This access is not allowed because some parent tag has insufficient permissions.
|
||||
/// For example, if a tag is `Frozen` and encounters a child write this will
|
||||
/// produce a `ChildAccessForbidden(Frozen)`.
|
||||
/// This kind of error can only occur on child accesses.
|
||||
ChildAccessForbidden(Permission),
|
||||
/// A protector was triggered due to an invalid transition that loses
|
||||
/// too much permissions.
|
||||
/// For example, if a protected tag goes from `Active` to `Frozen` due
|
||||
/// to a foreign write this will produce a `ProtectedTransition(Active, Frozen)`.
|
||||
/// This kind of error can only occur on foreign accesses.
|
||||
ProtectedTransition(Permission, Permission),
|
||||
/// Cannot deallocate because some tag in the allocation is strongly protected.
|
||||
/// This kind of error can only occur on deallocations.
|
||||
ProtectedDealloc,
|
||||
}
|
||||
|
||||
/// Failures that can occur during the execution of Tree Borrows procedures.
|
||||
pub(super) struct TbError<'node> {
|
||||
/// What failure occurred.
|
||||
pub error_kind: TransitionError,
|
||||
/// The tag on which the error was triggered.
|
||||
/// On protector violations, this is the tag that was protected.
|
||||
/// On accesses rejected due to insufficient permissions, this is the
|
||||
/// tag that lacked those permissions.
|
||||
pub faulty_tag: &'node NodeDebugInfo,
|
||||
/// Whether this was a Read or Write access. This field is ignored
|
||||
/// when the error was triggered by a deallocation.
|
||||
pub access_kind: AccessKind,
|
||||
/// Which tag the access that caused this error was made through, i.e.
|
||||
/// which tag was used to read/write/deallocate.
|
||||
pub tag_of_access: &'node NodeDebugInfo,
|
||||
}
|
||||
|
||||
impl TbError<'_> {
|
||||
/// Produce a UB error.
|
||||
pub fn build<'tcx>(self) -> InterpErrorInfo<'tcx> {
|
||||
use TransitionError::*;
|
||||
err_tb_ub(match self.error_kind {
|
||||
ChildAccessForbidden(perm) => {
|
||||
format!(
|
||||
"{kind} through {initial} is forbidden because it is a child of {current} which is {perm}.",
|
||||
kind=self.access_kind,
|
||||
initial=self.tag_of_access,
|
||||
current=self.faulty_tag,
|
||||
perm=perm,
|
||||
)
|
||||
}
|
||||
ProtectedTransition(start, end) => {
|
||||
format!(
|
||||
"{kind} through {initial} is forbidden because it is a foreign tag for {current}, which would hence change from {start} to {end}, but {current} is protected",
|
||||
current=self.faulty_tag,
|
||||
start=start,
|
||||
end=end,
|
||||
kind=self.access_kind,
|
||||
initial=self.tag_of_access,
|
||||
)
|
||||
}
|
||||
ProtectedDealloc => {
|
||||
format!(
|
||||
"the allocation of {initial} also contains {current} which is strongly protected, cannot deallocate",
|
||||
initial=self.tag_of_access,
|
||||
current=self.faulty_tag,
|
||||
)
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
type S = &'static str;
|
||||
/// Pretty-printing details
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// DisplayFmtWrapper {
|
||||
/// top: '>',
|
||||
/// bot: '<',
|
||||
/// warning_text: "Some tags have been hidden",
|
||||
/// }
|
||||
/// ```
|
||||
/// will wrap the entire text with
|
||||
/// ```text
|
||||
/// >>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
/// Some tags have been hidden
|
||||
///
|
||||
/// [ main display here ]
|
||||
///
|
||||
/// <<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
/// ```
|
||||
struct DisplayFmtWrapper {
|
||||
/// Character repeated to make the upper border.
|
||||
top: char,
|
||||
/// Character repeated to make the lower border.
|
||||
bot: char,
|
||||
/// Warning about some tags (unnamed) being hidden.
|
||||
warning_text: S,
|
||||
}
|
||||
|
||||
/// Formating of the permissions on each range.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// DisplayFmtPermission {
|
||||
/// open: "[",
|
||||
/// sep: "|",
|
||||
/// close: "]",
|
||||
/// uninit: "___",
|
||||
/// range_sep: "..",
|
||||
/// }
|
||||
/// ```
|
||||
/// will show each permission line as
|
||||
/// ```text
|
||||
/// 0.. 1.. 2.. 3.. 4.. 5
|
||||
/// [Act|Res|Frz|Dis|___]
|
||||
/// ```
|
||||
struct DisplayFmtPermission {
|
||||
/// Text that starts the permission block.
|
||||
open: S,
|
||||
/// Text that separates permissions on different ranges.
|
||||
sep: S,
|
||||
/// Text that ends the permission block.
|
||||
close: S,
|
||||
/// Text to show when a permission is not initialized.
|
||||
/// Should have the same width as a `Permission`'s `.short_name()`, i.e.
|
||||
/// 3 if using the `Res/Act/Frz/Dis` notation.
|
||||
uninit: S,
|
||||
/// Text to separate the `start` and `end` values of a range.
|
||||
range_sep: S,
|
||||
}
|
||||
|
||||
/// Formating of the tree structure.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// DisplayFmtPadding {
|
||||
/// join_middle: "|-",
|
||||
/// join_last: "'-",
|
||||
/// join_haschild: "-+-",
|
||||
/// join_default: "---",
|
||||
/// indent_middle: "| ",
|
||||
/// indent_last: " ",
|
||||
/// }
|
||||
/// ```
|
||||
/// will show the tree as
|
||||
/// ```text
|
||||
/// -+- root
|
||||
/// |--+- a
|
||||
/// | '--+- b
|
||||
/// | '---- c
|
||||
/// |--+- d
|
||||
/// | '---- e
|
||||
/// '---- f
|
||||
/// ```
|
||||
struct DisplayFmtPadding {
|
||||
/// Connector for a child other than the last.
|
||||
join_middle: S,
|
||||
/// Connector for the last child. Should have the same width as `join_middle`.
|
||||
join_last: S,
|
||||
/// Connector for a node that itself has a child.
|
||||
join_haschild: S,
|
||||
/// Connector for a node that does not have a child. Should have the same width
|
||||
/// as `join_haschild`.
|
||||
join_default: S,
|
||||
/// Indentation when there is a next child.
|
||||
indent_middle: S,
|
||||
/// Indentation for the last child.
|
||||
indent_last: S,
|
||||
}
|
||||
/// How to show whether a location has been accessed
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// DisplayFmtAccess {
|
||||
/// yes: " ",
|
||||
/// no: "?",
|
||||
/// meh: "_",
|
||||
/// }
|
||||
/// ```
|
||||
/// will show states as
|
||||
/// ```text
|
||||
/// Act
|
||||
/// ?Res
|
||||
/// ____
|
||||
/// ```
|
||||
struct DisplayFmtAccess {
|
||||
/// Used when `State.initialized = true`.
|
||||
yes: S,
|
||||
/// Used when `State.initialized = false`.
|
||||
/// Should have the same width as `yes`.
|
||||
no: S,
|
||||
/// Used when there is no `State`.
|
||||
/// Should have the same width as `yes`.
|
||||
meh: S,
|
||||
}
|
||||
|
||||
/// All parameters to determine how the tree is formated.
|
||||
struct DisplayFmt {
|
||||
wrapper: DisplayFmtWrapper,
|
||||
perm: DisplayFmtPermission,
|
||||
padding: DisplayFmtPadding,
|
||||
accessed: DisplayFmtAccess,
|
||||
}
|
||||
impl DisplayFmt {
|
||||
/// Print the permission with the format
|
||||
/// ` Res`/` Re*`/` Act`/` Frz`/` Dis` for accessed locations
|
||||
/// and `?Res`/`?Re*`/`?Act`/`?Frz`/`?Dis` for unaccessed locations.
|
||||
fn print_perm(&self, perm: Option<LocationState>) -> String {
|
||||
if let Some(perm) = perm {
|
||||
format!(
|
||||
"{ac}{st}",
|
||||
ac = if perm.is_initialized() { self.accessed.yes } else { self.accessed.no },
|
||||
st = perm.permission().short_name(),
|
||||
)
|
||||
} else {
|
||||
format!("{}{}", self.accessed.meh, self.perm.uninit)
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the tag with the format `<XYZ>` if the tag is unnamed,
|
||||
/// and `<XYZ=name>` if the tag is named.
|
||||
fn print_tag(&self, tag: BorTag, name: &Option<String>) -> String {
|
||||
let printable_tag = tag.get();
|
||||
if let Some(name) = name {
|
||||
format!("<{printable_tag}={name}>")
|
||||
} else {
|
||||
format!("<{printable_tag}>")
|
||||
}
|
||||
}
|
||||
|
||||
/// Print extra text if the tag has a protector.
|
||||
fn print_protector(&self, protector: Option<&ProtectorKind>) -> &'static str {
|
||||
protector
|
||||
.map(|p| {
|
||||
match *p {
|
||||
ProtectorKind::WeakProtector => " Weakly protected",
|
||||
ProtectorKind::StrongProtector => " Strongly protected",
|
||||
}
|
||||
})
|
||||
.unwrap_or("")
|
||||
}
|
||||
}
|
||||
|
||||
/// Track the indentation of the tree.
|
||||
struct DisplayIndent {
|
||||
curr: String,
|
||||
}
|
||||
impl DisplayIndent {
|
||||
fn new() -> Self {
|
||||
Self { curr: " ".to_string() }
|
||||
}
|
||||
|
||||
/// Increment the indentation by one. Note: need to know if this
|
||||
/// is the last child or not because the presence of other children
|
||||
/// changes the way the indentation is shown.
|
||||
fn increment(&mut self, formatter: &DisplayFmt, is_last: bool) {
|
||||
self.curr.push_str(if is_last {
|
||||
formatter.padding.indent_last
|
||||
} else {
|
||||
formatter.padding.indent_middle
|
||||
});
|
||||
}
|
||||
|
||||
/// Pop the last level of indentation.
|
||||
fn decrement(&mut self, formatter: &DisplayFmt) {
|
||||
for _ in 0..formatter.padding.indent_last.len() {
|
||||
let _ = self.curr.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the current indentation.
|
||||
fn write(&self, s: &mut String) {
|
||||
s.push_str(&self.curr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeat a character a number of times.
|
||||
fn char_repeat(c: char, n: usize) -> String {
|
||||
std::iter::once(c).cycle().take(n).collect::<String>()
|
||||
}
|
||||
|
||||
/// Extracted information from the tree, in a form that is readily accessible
|
||||
/// for printing. I.e. resolve parent-child pointers into an actual tree,
|
||||
/// zip permissions with their tag, remove wrappers, stringify data.
|
||||
struct DisplayRepr {
|
||||
tag: BorTag,
|
||||
name: Option<String>,
|
||||
rperm: Vec<Option<LocationState>>,
|
||||
children: Vec<DisplayRepr>,
|
||||
}
|
||||
|
||||
impl DisplayRepr {
|
||||
fn from(tree: &Tree, show_unnamed: bool) -> Option<Self> {
|
||||
let mut v = Vec::new();
|
||||
extraction_aux(tree, tree.root, show_unnamed, &mut v);
|
||||
let Some(root) = v.pop() else {
|
||||
if show_unnamed {
|
||||
unreachable!("This allocation contains no tags, not even a root. This should not happen.");
|
||||
}
|
||||
eprintln!("This allocation does not contain named tags. Use `miri_print_borrow_state(_, true)` to also print unnamed tags.");
|
||||
return None;
|
||||
};
|
||||
assert!(v.is_empty());
|
||||
return Some(root);
|
||||
|
||||
fn extraction_aux(
|
||||
tree: &Tree,
|
||||
idx: UniIndex,
|
||||
show_unnamed: bool,
|
||||
acc: &mut Vec<DisplayRepr>,
|
||||
) {
|
||||
let node = tree.nodes.get(idx).unwrap();
|
||||
let name = node.debug_info.name.clone();
|
||||
let children_sorted = {
|
||||
let mut children = node.children.iter().cloned().collect::<Vec<_>>();
|
||||
children.sort_by_key(|idx| tree.nodes.get(*idx).unwrap().tag);
|
||||
children
|
||||
};
|
||||
if !show_unnamed && name.is_none() {
|
||||
// We skip this node
|
||||
for child_idx in children_sorted {
|
||||
extraction_aux(tree, child_idx, show_unnamed, acc);
|
||||
}
|
||||
} else {
|
||||
// We take this node
|
||||
let rperm = tree
|
||||
.rperms
|
||||
.iter_all()
|
||||
.map(move |(_offset, perms)| {
|
||||
let perm = perms.get(idx);
|
||||
perm.cloned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut children = Vec::new();
|
||||
for child_idx in children_sorted {
|
||||
extraction_aux(tree, child_idx, show_unnamed, &mut children);
|
||||
}
|
||||
acc.push(DisplayRepr { tag: node.tag, name, rperm, children });
|
||||
}
|
||||
}
|
||||
}
|
||||
fn print(
|
||||
&self,
|
||||
fmt: &DisplayFmt,
|
||||
indenter: &mut DisplayIndent,
|
||||
protected_tags: &FxHashMap<BorTag, ProtectorKind>,
|
||||
ranges: Vec<Range<u64>>,
|
||||
print_warning: bool,
|
||||
) {
|
||||
let mut block = Vec::new();
|
||||
// Push the header and compute the required paddings for the body.
|
||||
// Header looks like this: `0.. 1.. 2.. 3.. 4.. 5.. 6.. 7.. 8`,
|
||||
// and is properly aligned with the `|` of the body.
|
||||
let (range_header, range_padding) = {
|
||||
let mut header_top = String::new();
|
||||
header_top.push_str("0..");
|
||||
let mut padding = Vec::new();
|
||||
for (i, range) in ranges.iter().enumerate() {
|
||||
if i > 0 {
|
||||
header_top.push_str(fmt.perm.range_sep);
|
||||
}
|
||||
let s = range.end.to_string();
|
||||
let l = s.chars().count() + fmt.perm.range_sep.chars().count();
|
||||
{
|
||||
let target_len =
|
||||
fmt.perm.uninit.chars().count() + fmt.accessed.yes.chars().count() + 1;
|
||||
let tot_len = target_len.max(l);
|
||||
let header_top_pad_len = target_len.saturating_sub(l);
|
||||
let body_pad_len = tot_len.saturating_sub(target_len);
|
||||
header_top.push_str(&format!("{}{}", char_repeat(' ', header_top_pad_len), s));
|
||||
padding.push(body_pad_len);
|
||||
}
|
||||
}
|
||||
([header_top], padding)
|
||||
};
|
||||
for s in range_header {
|
||||
block.push(s);
|
||||
}
|
||||
// This is the actual work
|
||||
print_aux(
|
||||
self,
|
||||
&range_padding,
|
||||
fmt,
|
||||
indenter,
|
||||
protected_tags,
|
||||
true, /* root _is_ the last child */
|
||||
&mut block,
|
||||
);
|
||||
// Then it's just prettifying it with a border of dashes.
|
||||
{
|
||||
let wr = &fmt.wrapper;
|
||||
let max_width = {
|
||||
let block_width = block.iter().map(|s| s.chars().count()).max().unwrap();
|
||||
if print_warning {
|
||||
block_width.max(wr.warning_text.chars().count())
|
||||
} else {
|
||||
block_width
|
||||
}
|
||||
};
|
||||
eprintln!("{}", char_repeat(wr.top, max_width));
|
||||
if print_warning {
|
||||
eprintln!("{}", wr.warning_text,);
|
||||
}
|
||||
for line in block {
|
||||
eprintln!("{line}");
|
||||
}
|
||||
eprintln!("{}", char_repeat(wr.bot, max_width));
|
||||
}
|
||||
|
||||
// Here is the function that does the heavy lifting
|
||||
fn print_aux(
|
||||
tree: &DisplayRepr,
|
||||
padding: &[usize],
|
||||
fmt: &DisplayFmt,
|
||||
indent: &mut DisplayIndent,
|
||||
protected_tags: &FxHashMap<BorTag, ProtectorKind>,
|
||||
is_last_child: bool,
|
||||
acc: &mut Vec<String>,
|
||||
) {
|
||||
let mut line = String::new();
|
||||
// Format the permissions on each range.
|
||||
// Looks like `| Act| Res| Res| Act|`.
|
||||
line.push_str(fmt.perm.open);
|
||||
for (i, (perm, &pad)) in tree.rperm.iter().zip(padding.iter()).enumerate() {
|
||||
if i > 0 {
|
||||
line.push_str(fmt.perm.sep);
|
||||
}
|
||||
let show_perm = fmt.print_perm(*perm);
|
||||
line.push_str(&format!("{}{}", char_repeat(' ', pad), show_perm));
|
||||
}
|
||||
line.push_str(fmt.perm.close);
|
||||
// Format the tree structure.
|
||||
// Main difficulty is handling the indentation properly.
|
||||
indent.write(&mut line);
|
||||
{
|
||||
// padding
|
||||
line.push_str(if is_last_child {
|
||||
fmt.padding.join_last
|
||||
} else {
|
||||
fmt.padding.join_middle
|
||||
});
|
||||
line.push_str(fmt.padding.join_default);
|
||||
line.push_str(if tree.children.is_empty() {
|
||||
fmt.padding.join_default
|
||||
} else {
|
||||
fmt.padding.join_haschild
|
||||
});
|
||||
line.push_str(fmt.padding.join_default);
|
||||
line.push_str(fmt.padding.join_default);
|
||||
}
|
||||
line.push_str(&fmt.print_tag(tree.tag, &tree.name));
|
||||
let protector = protected_tags.get(&tree.tag);
|
||||
line.push_str(fmt.print_protector(protector));
|
||||
// Push the line to the accumulator then recurse.
|
||||
acc.push(line);
|
||||
let nb_children = tree.children.len();
|
||||
for (i, child) in tree.children.iter().enumerate() {
|
||||
indent.increment(fmt, is_last_child);
|
||||
print_aux(child, padding, fmt, indent, protected_tags, i + 1 == nb_children, acc);
|
||||
indent.decrement(fmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_FORMATTER: DisplayFmt = DisplayFmt {
|
||||
wrapper: DisplayFmtWrapper {
|
||||
top: '─',
|
||||
bot: '─',
|
||||
warning_text: "Warning: this tree is indicative only. Some tags may have been hidden.",
|
||||
},
|
||||
perm: DisplayFmtPermission { open: "|", sep: "|", close: "|", uninit: "---", range_sep: ".." },
|
||||
padding: DisplayFmtPadding {
|
||||
join_middle: "├",
|
||||
join_last: "└",
|
||||
indent_middle: "│ ",
|
||||
indent_last: " ",
|
||||
join_haschild: "┬",
|
||||
join_default: "─",
|
||||
},
|
||||
accessed: DisplayFmtAccess { yes: " ", no: "?", meh: "-" },
|
||||
};
|
||||
|
||||
impl<'tcx> Tree {
|
||||
/// Display the contents of the tree.
|
||||
pub fn print_tree(
|
||||
&self,
|
||||
protected_tags: &FxHashMap<BorTag, ProtectorKind>,
|
||||
show_unnamed: bool,
|
||||
) -> InterpResult<'tcx> {
|
||||
let mut indenter = DisplayIndent::new();
|
||||
let ranges = self.rperms.iter_all().map(|(range, _perms)| range).collect::<Vec<_>>();
|
||||
if let Some(repr) = DisplayRepr::from(self, show_unnamed) {
|
||||
repr.print(
|
||||
&DEFAULT_FORMATTER,
|
||||
&mut indenter,
|
||||
protected_tags,
|
||||
ranges,
|
||||
/* print warning message about tags not shown */ !show_unnamed,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
539
src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs
Normal file
539
src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs
Normal file
@ -0,0 +1,539 @@
|
||||
use log::trace;
|
||||
|
||||
use rustc_target::abi::{Abi, Size};
|
||||
|
||||
use crate::borrow_tracker::{AccessKind, GlobalStateInner, ProtectorKind, RetagFields};
|
||||
use rustc_middle::{
|
||||
mir::{Mutability, RetagKind},
|
||||
ty::{
|
||||
self,
|
||||
layout::{HasParamEnv, LayoutOf},
|
||||
Ty,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
|
||||
mod diagnostics;
|
||||
mod perms;
|
||||
mod tree;
|
||||
mod unimap;
|
||||
use perms::Permission;
|
||||
pub use tree::Tree;
|
||||
|
||||
pub type AllocState = Tree;
|
||||
|
||||
pub fn err_tb_ub<'tcx>(msg: String) -> InterpError<'tcx> {
|
||||
err_machine_stop!(TerminationInfo::TreeBorrowsUb { msg })
|
||||
}
|
||||
|
||||
impl<'tcx> Tree {
|
||||
/// Create a new allocation, i.e. a new tree
|
||||
pub fn new_allocation(
|
||||
id: AllocId,
|
||||
size: Size,
|
||||
state: &mut GlobalStateInner,
|
||||
_kind: MemoryKind<machine::MiriMemoryKind>,
|
||||
machine: &MiriMachine<'_, 'tcx>,
|
||||
) -> Self {
|
||||
let tag = state.base_ptr_tag(id, machine); // Fresh tag for the root
|
||||
Tree::new(tag, size)
|
||||
}
|
||||
|
||||
/// Check that an access on the entire range is permitted, and update
|
||||
/// the tree.
|
||||
pub fn before_memory_access(
|
||||
&mut self,
|
||||
access_kind: AccessKind,
|
||||
alloc_id: AllocId,
|
||||
prov: ProvenanceExtra,
|
||||
range: AllocRange,
|
||||
machine: &MiriMachine<'_, 'tcx>,
|
||||
) -> InterpResult<'tcx> {
|
||||
trace!(
|
||||
"{} with tag {:?}: {:?}, size {}",
|
||||
access_kind,
|
||||
prov,
|
||||
Pointer::new(alloc_id, range.start),
|
||||
range.size.bytes(),
|
||||
);
|
||||
// TODO: for now we bail out on wildcard pointers. Eventually we should
|
||||
// handle them as much as we can.
|
||||
let tag = match prov {
|
||||
ProvenanceExtra::Concrete(tag) => tag,
|
||||
ProvenanceExtra::Wildcard => return Ok(()),
|
||||
};
|
||||
let global = machine.borrow_tracker.as_ref().unwrap();
|
||||
self.perform_access(access_kind, tag, range, global)
|
||||
}
|
||||
|
||||
/// Check that this pointer has permission to deallocate this range.
|
||||
pub fn before_memory_deallocation(
|
||||
&mut self,
|
||||
_alloc_id: AllocId,
|
||||
prov: ProvenanceExtra,
|
||||
range: AllocRange,
|
||||
machine: &MiriMachine<'_, 'tcx>,
|
||||
) -> InterpResult<'tcx> {
|
||||
// TODO: for now we bail out on wildcard pointers. Eventually we should
|
||||
// handle them as much as we can.
|
||||
let tag = match prov {
|
||||
ProvenanceExtra::Concrete(tag) => tag,
|
||||
ProvenanceExtra::Wildcard => return Ok(()),
|
||||
};
|
||||
let global = machine.borrow_tracker.as_ref().unwrap();
|
||||
self.dealloc(tag, range, global)
|
||||
}
|
||||
|
||||
pub fn expose_tag(&mut self, _tag: BorTag) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy for a new borrow.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct NewPermission {
|
||||
/// Whether this borrow requires a read access on its parent.
|
||||
/// `perform_read_access` is `true` for all pointers marked `dereferenceable`.
|
||||
perform_read_access: bool,
|
||||
/// Which permission should the pointer start with.
|
||||
initial_state: Permission,
|
||||
/// Whether this pointer is part of the arguments of a function call.
|
||||
/// `protector` is `Some(_)` for all pointers marked `noalias`.
|
||||
protector: Option<ProtectorKind>,
|
||||
}
|
||||
|
||||
impl<'tcx> NewPermission {
|
||||
/// Determine NewPermission of the reference from the type of the pointee.
|
||||
fn from_ref_ty(
|
||||
pointee: Ty<'tcx>,
|
||||
mutability: Mutability,
|
||||
kind: RetagKind,
|
||||
cx: &crate::MiriInterpCx<'_, 'tcx>,
|
||||
) -> Option<Self> {
|
||||
let ty_is_freeze = pointee.is_freeze(*cx.tcx, cx.param_env());
|
||||
let ty_is_unpin = pointee.is_unpin(*cx.tcx, cx.param_env());
|
||||
let initial_state = match mutability {
|
||||
Mutability::Mut if ty_is_unpin => Permission::new_unique_2phase(ty_is_freeze),
|
||||
Mutability::Not if ty_is_freeze => Permission::new_frozen(),
|
||||
// Raw pointers never enter this function so they are not handled.
|
||||
// However raw pointers are not the only pointers that take the parent
|
||||
// tag, this also happens for `!Unpin` `&mut`s and interior mutable
|
||||
// `&`s, which are excluded above.
|
||||
_ => return None,
|
||||
};
|
||||
// This field happens to be redundant since right now we always do a read,
|
||||
// but it could be useful in the future.
|
||||
let perform_read_access = true;
|
||||
|
||||
let protector = (kind == RetagKind::FnEntry).then_some(ProtectorKind::StrongProtector);
|
||||
Some(Self { perform_read_access, initial_state, protector })
|
||||
}
|
||||
|
||||
// Boxes are not handled by `from_ref_ty`, they need special behavior
|
||||
// implemented here.
|
||||
fn from_box_ty(
|
||||
ty: Ty<'tcx>,
|
||||
kind: RetagKind,
|
||||
cx: &crate::MiriInterpCx<'_, 'tcx>,
|
||||
) -> Option<Self> {
|
||||
let pointee = ty.builtin_deref(true).unwrap().ty;
|
||||
pointee.is_unpin(*cx.tcx, cx.param_env()).then_some(()).map(|()| {
|
||||
// Regular `Unpin` box, give it `noalias` but only a weak protector
|
||||
// because it is valid to deallocate it within the function.
|
||||
let ty_is_freeze = ty.is_freeze(*cx.tcx, cx.param_env());
|
||||
Self {
|
||||
perform_read_access: true,
|
||||
initial_state: Permission::new_unique_2phase(ty_is_freeze),
|
||||
protector: (kind == RetagKind::FnEntry).then_some(ProtectorKind::WeakProtector),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Retagging/reborrowing.
|
||||
/// Policy on which permission to grant to each pointer should be left to
|
||||
/// the implementation of NewPermission.
|
||||
impl<'mir: 'ecx, 'tcx: 'mir, 'ecx> EvalContextPrivExt<'mir, 'tcx, 'ecx>
|
||||
for crate::MiriInterpCx<'mir, 'tcx>
|
||||
{
|
||||
}
|
||||
trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
/// Returns the `AllocId` the reborrow was done in, if there is some actual
|
||||
/// memory associated with this pointer. Returns `None` if there is no actual
|
||||
/// memory allocated. Also checks that the reborrow of size `ptr_size` is
|
||||
/// within bounds of the allocation.
|
||||
///
|
||||
/// Also returns the tag that the pointer should get, which is essentially
|
||||
/// `if new_perm.is_some() { new_tag } else { parent_tag }` along with
|
||||
/// some logging (always) and fake reads (if `new_perm` is
|
||||
/// `Some(NewPermission { perform_read_access: true }`).
|
||||
fn tb_reborrow(
|
||||
&mut self,
|
||||
place: &MPlaceTy<'tcx, Provenance>, // parent tag extracted from here
|
||||
ptr_size: Size,
|
||||
new_perm: Option<NewPermission>,
|
||||
new_tag: BorTag,
|
||||
) -> InterpResult<'tcx, Option<(AllocId, BorTag)>> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
// It is crucial that this gets called on all code paths, to ensure we track tag creation.
|
||||
let log_creation = |this: &MiriInterpCx<'mir, 'tcx>,
|
||||
loc: Option<(AllocId, Size, ProvenanceExtra)>| // alloc_id, base_offset, orig_tag
|
||||
-> InterpResult<'tcx> {
|
||||
let global = this.machine.borrow_tracker.as_ref().unwrap().borrow();
|
||||
let ty = place.layout.ty;
|
||||
if global.tracked_pointer_tags.contains(&new_tag) {
|
||||
let kind_str = format!("{new_perm:?} (pointee type {ty})");
|
||||
this.emit_diagnostic(NonHaltingDiagnostic::CreatedPointerTag(
|
||||
new_tag.inner(),
|
||||
Some(kind_str),
|
||||
loc.map(|(alloc_id, base_offset, orig_tag)| (alloc_id, alloc_range(base_offset, ptr_size), orig_tag)),
|
||||
));
|
||||
}
|
||||
drop(global); // don't hold that reference any longer than we have to
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let (alloc_id, base_offset, parent_prov) = if ptr_size > Size::ZERO {
|
||||
this.ptr_get_alloc_id(place.ptr)?
|
||||
} else {
|
||||
match this.ptr_try_get_alloc_id(place.ptr) {
|
||||
Ok(data) => data,
|
||||
Err(_) => {
|
||||
// This pointer doesn't come with an AllocId, so there's no
|
||||
// memory to do retagging in.
|
||||
trace!(
|
||||
"reborrow of size 0: reference {:?} derived from {:?} (pointee {})",
|
||||
new_tag,
|
||||
place.ptr,
|
||||
place.layout.ty,
|
||||
);
|
||||
log_creation(this, None)?;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
};
|
||||
let orig_tag = match parent_prov {
|
||||
ProvenanceExtra::Wildcard => return Ok(None), // TODO: handle wildcard pointers
|
||||
ProvenanceExtra::Concrete(tag) => tag,
|
||||
};
|
||||
|
||||
// Protection against trying to get a reference to a vtable:
|
||||
// vtables do not have an alloc_extra so the call to
|
||||
// `get_alloc_extra` that follows fails.
|
||||
let (alloc_size, _align, alloc_kind) = this.get_alloc_info(alloc_id);
|
||||
if ptr_size == Size::ZERO && !matches!(alloc_kind, AllocKind::LiveData) {
|
||||
return Ok(Some((alloc_id, orig_tag)));
|
||||
}
|
||||
|
||||
log_creation(this, Some((alloc_id, base_offset, parent_prov)))?;
|
||||
|
||||
// Ensure we bail out if the pointer goes out-of-bounds (see miri#1050).
|
||||
if base_offset + ptr_size > alloc_size {
|
||||
throw_ub!(PointerOutOfBounds {
|
||||
alloc_id,
|
||||
alloc_size,
|
||||
ptr_offset: this.target_usize_to_isize(base_offset.bytes()),
|
||||
ptr_size,
|
||||
msg: CheckInAllocMsg::InboundsTest
|
||||
});
|
||||
}
|
||||
|
||||
trace!(
|
||||
"reborrow: reference {:?} derived from {:?} (pointee {}): {:?}, size {}",
|
||||
new_tag,
|
||||
orig_tag,
|
||||
place.layout.ty,
|
||||
Pointer::new(alloc_id, base_offset),
|
||||
ptr_size.bytes()
|
||||
);
|
||||
|
||||
let Some(new_perm) = new_perm else { return Ok(Some((alloc_id, orig_tag))); };
|
||||
|
||||
if let Some(protect) = new_perm.protector {
|
||||
// We register the protection in two different places.
|
||||
// This makes creating a protector slower, but checking whether a tag
|
||||
// is protected faster.
|
||||
this.frame_mut().extra.borrow_tracker.as_mut().unwrap().protected_tags.push(new_tag);
|
||||
this.machine
|
||||
.borrow_tracker
|
||||
.as_mut()
|
||||
.expect("We should have borrow tracking data")
|
||||
.get_mut()
|
||||
.protected_tags
|
||||
.insert(new_tag, protect);
|
||||
}
|
||||
|
||||
let alloc_extra = this.get_alloc_extra(alloc_id)?;
|
||||
let range = alloc_range(base_offset, ptr_size);
|
||||
let mut tree_borrows = alloc_extra.borrow_tracker_tb().borrow_mut();
|
||||
|
||||
if new_perm.perform_read_access {
|
||||
// Count this reborrow as a read access
|
||||
let global = &this.machine.borrow_tracker.as_ref().unwrap();
|
||||
tree_borrows.perform_access(AccessKind::Read, orig_tag, range, global)?;
|
||||
if let Some(data_race) = alloc_extra.data_race.as_ref() {
|
||||
data_race.read(alloc_id, range, &this.machine)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Record the parent-child pair in the tree.
|
||||
tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range)?;
|
||||
Ok(Some((alloc_id, new_tag)))
|
||||
}
|
||||
|
||||
/// Retags an indidual pointer, returning the retagged version.
|
||||
fn tb_retag_reference(
|
||||
&mut self,
|
||||
val: &ImmTy<'tcx, Provenance>,
|
||||
new_perm: Option<NewPermission>,
|
||||
) -> InterpResult<'tcx, ImmTy<'tcx, Provenance>> {
|
||||
let this = self.eval_context_mut();
|
||||
// We want a place for where the ptr *points to*, so we get one.
|
||||
let place = this.ref_to_mplace(val)?;
|
||||
|
||||
// Get a lower bound of the size of this place.
|
||||
// (When `extern type` are involved, use the size of the known prefix.)
|
||||
let size = this
|
||||
.size_and_align_of_mplace(&place)?
|
||||
.map(|(size, _)| size)
|
||||
.unwrap_or(place.layout.size);
|
||||
|
||||
// This new tag is not guaranteed to actually be used.
|
||||
//
|
||||
// If you run out of tags, consider the following optimization: adjust `tb_reborrow`
|
||||
// so that rather than taking as input a fresh tag and deciding whether it uses this
|
||||
// one or the parent it instead just returns whether a new tag should be created.
|
||||
// This will avoid creating tags than end up never being used.
|
||||
let new_tag = this.machine.borrow_tracker.as_mut().unwrap().get_mut().new_ptr();
|
||||
|
||||
// Compute the actual reborrow.
|
||||
let reborrowed = this.tb_reborrow(&place, size, new_perm, new_tag)?;
|
||||
|
||||
// Adjust pointer.
|
||||
let new_place = place.map_provenance(|p| {
|
||||
p.map(|prov| {
|
||||
match reborrowed {
|
||||
Some((alloc_id, actual_tag)) => {
|
||||
// If `reborrow` could figure out the AllocId of this ptr, hard-code it into the new one.
|
||||
// Even if we started out with a wildcard, this newly retagged pointer is tied to that allocation.
|
||||
Provenance::Concrete { alloc_id, tag: actual_tag }
|
||||
}
|
||||
None => {
|
||||
// Looks like this has to stay a wildcard pointer.
|
||||
assert!(matches!(prov, Provenance::Wildcard));
|
||||
Provenance::Wildcard
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Return new pointer.
|
||||
Ok(ImmTy::from_immediate(new_place.to_ref(this), val.layout))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
|
||||
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
/// Retag a pointer. References are passed to `from_ref_ty` and
|
||||
/// raw pointers are never reborrowed.
|
||||
fn tb_retag_ptr_value(
|
||||
&mut self,
|
||||
kind: RetagKind,
|
||||
val: &ImmTy<'tcx, Provenance>,
|
||||
) -> InterpResult<'tcx, ImmTy<'tcx, Provenance>> {
|
||||
let this = self.eval_context_mut();
|
||||
let new_perm = if let &ty::Ref(_, pointee, mutability) = val.layout.ty.kind() {
|
||||
NewPermission::from_ref_ty(pointee, mutability, kind, this)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
this.tb_retag_reference(val, new_perm)
|
||||
}
|
||||
|
||||
/// Retag all pointers that are stored in this place.
|
||||
fn tb_retag_place_contents(
|
||||
&mut self,
|
||||
kind: RetagKind,
|
||||
place: &PlaceTy<'tcx, Provenance>,
|
||||
) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
let retag_fields = this.machine.borrow_tracker.as_mut().unwrap().get_mut().retag_fields;
|
||||
let mut visitor = RetagVisitor { ecx: this, kind, retag_fields };
|
||||
return visitor.visit_value(place);
|
||||
|
||||
// The actual visitor.
|
||||
struct RetagVisitor<'ecx, 'mir, 'tcx> {
|
||||
ecx: &'ecx mut MiriInterpCx<'mir, 'tcx>,
|
||||
kind: RetagKind,
|
||||
retag_fields: RetagFields,
|
||||
}
|
||||
impl<'ecx, 'mir, 'tcx> RetagVisitor<'ecx, 'mir, 'tcx> {
|
||||
#[inline(always)] // yes this helps in our benchmarks
|
||||
fn retag_ptr_inplace(
|
||||
&mut self,
|
||||
place: &PlaceTy<'tcx, Provenance>,
|
||||
new_perm: Option<NewPermission>,
|
||||
) -> InterpResult<'tcx> {
|
||||
let val = self.ecx.read_immediate(&self.ecx.place_to_op(place)?)?;
|
||||
let val = self.ecx.tb_retag_reference(&val, new_perm)?;
|
||||
self.ecx.write_immediate(*val, place)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl<'ecx, 'mir, 'tcx> MutValueVisitor<'mir, 'tcx, MiriMachine<'mir, 'tcx>>
|
||||
for RetagVisitor<'ecx, 'mir, 'tcx>
|
||||
{
|
||||
type V = PlaceTy<'tcx, Provenance>;
|
||||
|
||||
#[inline(always)]
|
||||
fn ecx(&mut self) -> &mut MiriInterpCx<'mir, 'tcx> {
|
||||
self.ecx
|
||||
}
|
||||
|
||||
fn visit_box(&mut self, place: &PlaceTy<'tcx, Provenance>) -> InterpResult<'tcx> {
|
||||
let new_perm = NewPermission::from_box_ty(place.layout.ty, self.kind, self.ecx);
|
||||
self.retag_ptr_inplace(place, new_perm)
|
||||
}
|
||||
|
||||
fn visit_value(&mut self, place: &PlaceTy<'tcx, Provenance>) -> InterpResult<'tcx> {
|
||||
// If this place is smaller than a pointer, we know that it can't contain any
|
||||
// pointers we need to retag, so we can stop recursion early.
|
||||
// This optimization is crucial for ZSTs, because they can contain way more fields
|
||||
// than we can ever visit.
|
||||
if place.layout.is_sized() && place.layout.size < self.ecx.pointer_size() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check the type of this value to see what to do with it (retag, or recurse).
|
||||
match place.layout.ty.kind() {
|
||||
&ty::Ref(_, pointee, mutability) => {
|
||||
let new_perm =
|
||||
NewPermission::from_ref_ty(pointee, mutability, self.kind, self.ecx);
|
||||
self.retag_ptr_inplace(place, new_perm)?;
|
||||
}
|
||||
ty::RawPtr(_) => {
|
||||
// We definitely do *not* want to recurse into raw pointers -- wide raw
|
||||
// pointers have fields, and for dyn Trait pointees those can have reference
|
||||
// type!
|
||||
// We also do not want to reborrow them.
|
||||
}
|
||||
ty::Adt(adt, _) if adt.is_box() => {
|
||||
// Recurse for boxes, they require some tricky handling and will end up in `visit_box` above.
|
||||
// (Yes this means we technically also recursively retag the allocator itself
|
||||
// even if field retagging is not enabled. *shrug*)
|
||||
self.walk_value(place)?;
|
||||
}
|
||||
_ => {
|
||||
// Not a reference/pointer/box. Only recurse if configured appropriately.
|
||||
let recurse = match self.retag_fields {
|
||||
RetagFields::No => false,
|
||||
RetagFields::Yes => true,
|
||||
RetagFields::OnlyScalar => {
|
||||
// Matching `ArgAbi::new` at the time of writing, only fields of
|
||||
// `Scalar` and `ScalarPair` ABI are considered.
|
||||
matches!(place.layout.abi, Abi::Scalar(..) | Abi::ScalarPair(..))
|
||||
}
|
||||
};
|
||||
if recurse {
|
||||
self.walk_value(place)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// After a stack frame got pushed, retag the return place so that we are sure
|
||||
/// it does not alias with anything.
|
||||
///
|
||||
/// This is a HACK because there is nothing in MIR that would make the retag
|
||||
/// explicit. Also see <https://github.com/rust-lang/rust/issues/71117>.
|
||||
fn tb_retag_return_place(&mut self) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
//this.debug_hint_location();
|
||||
let return_place = &this.frame().return_place;
|
||||
if return_place.layout.is_zst() {
|
||||
// There may not be any memory here, nothing to do.
|
||||
return Ok(());
|
||||
}
|
||||
// We need this to be in-memory to use tagged pointers.
|
||||
let return_place = this.force_allocation(&return_place.clone())?;
|
||||
|
||||
// We have to turn the place into a pointer to use the existing code.
|
||||
// (The pointer type does not matter, so we use a raw pointer.)
|
||||
let ptr_layout = this.layout_of(this.tcx.mk_mut_ptr(return_place.layout.ty))?;
|
||||
let val = ImmTy::from_immediate(return_place.to_ref(this), ptr_layout);
|
||||
// Reborrow it. With protection! That is part of the point.
|
||||
// FIXME: do we truly want a 2phase borrow here?
|
||||
let new_perm = Some(NewPermission {
|
||||
initial_state: Permission::new_unique_2phase(/*freeze*/ false),
|
||||
perform_read_access: true,
|
||||
protector: Some(ProtectorKind::StrongProtector),
|
||||
});
|
||||
let val = this.tb_retag_reference(&val, new_perm)?;
|
||||
// And use reborrowed pointer for return place.
|
||||
let return_place = this.ref_to_mplace(&val)?;
|
||||
this.frame_mut().return_place = return_place.into();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark the given tag as exposed. It was found on a pointer with the given AllocId.
|
||||
fn tb_expose_tag(&mut self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
// Function pointers and dead objects don't have an alloc_extra so we ignore them.
|
||||
// This is okay because accessing them is UB anyway, no need for any Tree Borrows checks.
|
||||
// NOT using `get_alloc_extra_mut` since this might be a read-only allocation!
|
||||
let (_size, _align, kind) = this.get_alloc_info(alloc_id);
|
||||
match kind {
|
||||
AllocKind::LiveData => {
|
||||
// This should have alloc_extra data, but `get_alloc_extra` can still fail
|
||||
// if converting this alloc_id from a global to a local one
|
||||
// uncovers a non-supported `extern static`.
|
||||
let alloc_extra = this.get_alloc_extra(alloc_id)?;
|
||||
trace!("Stacked Borrows tag {tag:?} exposed in {alloc_id:?}");
|
||||
alloc_extra.borrow_tracker_tb().borrow_mut().expose_tag(tag);
|
||||
}
|
||||
AllocKind::Function | AllocKind::VTable | AllocKind::Dead => {
|
||||
// No tree borrows on these allocations.
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display the tree.
|
||||
fn print_tree(&mut self, alloc_id: AllocId, show_unnamed: bool) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
let alloc_extra = this.get_alloc_extra(alloc_id)?;
|
||||
let tree_borrows = alloc_extra.borrow_tracker_tb().borrow();
|
||||
let borrow_tracker = &this.machine.borrow_tracker.as_ref().unwrap().borrow();
|
||||
tree_borrows.print_tree(&borrow_tracker.protected_tags, show_unnamed)
|
||||
}
|
||||
|
||||
/// Give a name to the pointer, usually the name it has in the source code (for debugging).
|
||||
/// The name given is `name` and the pointer that receives it is the `nth_parent`
|
||||
/// of `ptr` (with 0 representing `ptr` itself)
|
||||
fn tb_give_pointer_debug_name(
|
||||
&mut self,
|
||||
ptr: Pointer<Option<Provenance>>,
|
||||
nth_parent: u8,
|
||||
name: &str,
|
||||
) -> InterpResult<'tcx> {
|
||||
let this = self.eval_context_mut();
|
||||
let (tag, alloc_id) = match ptr.provenance {
|
||||
Some(Provenance::Concrete { tag, alloc_id }) => (tag, alloc_id),
|
||||
_ => {
|
||||
eprintln!("Can't give the name {name} to Wildcard pointer");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let alloc_extra = this.get_alloc_extra(alloc_id)?;
|
||||
let mut tree_borrows = alloc_extra.borrow_tracker_tb().borrow_mut();
|
||||
tree_borrows.give_pointer_debug_name(tag, nth_parent, name)
|
||||
}
|
||||
}
|
307
src/tools/miri/src/borrow_tracker/tree_borrows/perms.rs
Normal file
307
src/tools/miri/src/borrow_tracker/tree_borrows/perms.rs
Normal file
@ -0,0 +1,307 @@
|
||||
use std::cmp::{Ordering, PartialOrd};
|
||||
use std::fmt;
|
||||
|
||||
use crate::borrow_tracker::tree_borrows::tree::AccessRelatedness;
|
||||
use crate::borrow_tracker::AccessKind;
|
||||
|
||||
/// The activation states of a pointer
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PermissionPriv {
|
||||
/// represents: a local reference that has not yet been written to;
|
||||
/// allows: child reads, foreign reads, foreign writes if type is freeze;
|
||||
/// rejects: child writes (Active), foreign writes (Disabled, except if type is not freeze).
|
||||
/// special case: behaves differently when protected to adhere more closely to noalias
|
||||
Reserved { ty_is_freeze: bool },
|
||||
/// represents: a unique pointer;
|
||||
/// allows: child reads, child writes;
|
||||
/// rejects: foreign reads (Frozen), foreign writes (Disabled).
|
||||
Active,
|
||||
/// represents: a shared pointer;
|
||||
/// allows: all read accesses;
|
||||
/// rejects child writes (UB), foreign writes (Disabled).
|
||||
Frozen,
|
||||
/// represents: a dead pointer;
|
||||
/// allows: all foreign accesses;
|
||||
/// rejects: all child accesses (UB).
|
||||
Disabled,
|
||||
}
|
||||
use PermissionPriv::*;
|
||||
|
||||
impl PartialOrd for PermissionPriv {
|
||||
/// PermissionPriv is ordered as follows:
|
||||
/// - Reserved(_) < Active < Frozen < Disabled;
|
||||
/// - different kinds of `Reserved` (with or without interior mutability)
|
||||
/// are incomparable to each other.
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
use Ordering::*;
|
||||
Some(match (self, other) {
|
||||
(a, b) if a == b => Equal,
|
||||
(Disabled, _) => Greater,
|
||||
(_, Disabled) => Less,
|
||||
(Frozen, _) => Greater,
|
||||
(_, Frozen) => Less,
|
||||
(Active, _) => Greater,
|
||||
(_, Active) => Less,
|
||||
(Reserved { .. }, Reserved { .. }) => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// This module controls how each permission individually reacts to an access.
|
||||
/// Although these functions take `protected` as an argument, this is NOT because
|
||||
/// we check protector violations here, but because some permissions behave differently
|
||||
/// when protected.
|
||||
mod transition {
|
||||
use super::*;
|
||||
/// A child node was read-accessed: UB on Disabled, noop on the rest.
|
||||
fn child_read(state: PermissionPriv, _protected: bool) -> Option<PermissionPriv> {
|
||||
Some(match state {
|
||||
Disabled => return None,
|
||||
// The inner data `ty_is_freeze` of `Reserved` is always irrelevant for Read
|
||||
// accesses, since the data is not being mutated. Hence the `{ .. }`
|
||||
readable @ (Reserved { .. } | Active | Frozen) => readable,
|
||||
})
|
||||
}
|
||||
|
||||
/// A non-child node was read-accessed: noop on non-protected Reserved, advance to Frozen otherwise.
|
||||
fn foreign_read(state: PermissionPriv, protected: bool) -> Option<PermissionPriv> {
|
||||
use Option::*;
|
||||
Some(match state {
|
||||
// The inner data `ty_is_freeze` of `Reserved` is always irrelevant for Read
|
||||
// accesses, since the data is not being mutated. Hence the `{ .. }`
|
||||
res @ Reserved { .. } if !protected => res,
|
||||
Reserved { .. } => Frozen, // protected reserved
|
||||
Active => Frozen,
|
||||
non_writeable @ (Frozen | Disabled) => non_writeable,
|
||||
})
|
||||
}
|
||||
|
||||
/// A child node was write-accessed: `Reserved` must become `Active` to obtain
|
||||
/// write permissions, `Frozen` and `Disabled` cannot obtain such permissions and produce UB.
|
||||
fn child_write(state: PermissionPriv, _protected: bool) -> Option<PermissionPriv> {
|
||||
Some(match state {
|
||||
// A write always activates the 2-phase borrow, even with interior
|
||||
// mutability
|
||||
Reserved { .. } | Active => Active,
|
||||
Frozen | Disabled => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A non-child node was write-accessed: this makes everything `Disabled` except for
|
||||
/// non-protected interior mutable `Reserved` which stay the same.
|
||||
fn foreign_write(state: PermissionPriv, protected: bool) -> Option<PermissionPriv> {
|
||||
Some(match state {
|
||||
cell @ Reserved { ty_is_freeze: false } if !protected => cell,
|
||||
_ => Disabled,
|
||||
})
|
||||
}
|
||||
|
||||
/// Dispatch handler depending on the kind of access and its position.
|
||||
pub(super) fn perform_access(
|
||||
kind: AccessKind,
|
||||
rel_pos: AccessRelatedness,
|
||||
child: PermissionPriv,
|
||||
protected: bool,
|
||||
) -> Option<PermissionPriv> {
|
||||
match (kind, rel_pos.is_foreign()) {
|
||||
(AccessKind::Write, true) => foreign_write(child, protected),
|
||||
(AccessKind::Read, true) => foreign_read(child, protected),
|
||||
(AccessKind::Write, false) => child_write(child, protected),
|
||||
(AccessKind::Read, false) => child_read(child, protected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionPriv {
|
||||
/// Determines whether a transition that occured is compatible with the presence
|
||||
/// of a Protector. This is not included in the `transition` functions because
|
||||
/// it would distract from the few places where the transition is modified
|
||||
/// because of a protector, but not forbidden.
|
||||
fn protector_allows_transition(self, new: Self) -> bool {
|
||||
match (self, new) {
|
||||
_ if self == new => true,
|
||||
// It is always a protector violation to not be readable anymore
|
||||
(_, Disabled) => false,
|
||||
// In the case of a `Reserved` under a protector, both transitions
|
||||
// `Reserved => Active` and `Reserved => Frozen` can legitimately occur.
|
||||
// The first is standard (Child Write), the second is for Foreign Writes
|
||||
// on protected Reserved where we must ensure that the pointer is not
|
||||
// written to in the future.
|
||||
(Reserved { .. }, Active) | (Reserved { .. }, Frozen) => true,
|
||||
// This pointer should have stayed writeable for the whole function
|
||||
(Active, Frozen) => false,
|
||||
_ => unreachable!("Transition from {self:?} to {new:?} should never be possible"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public interface to the state machine that controls read-write permissions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Permission(PermissionPriv);
|
||||
|
||||
impl fmt::Display for Permission {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self.0 {
|
||||
PermissionPriv::Reserved { .. } => "Reserved",
|
||||
PermissionPriv::Active => "Active",
|
||||
PermissionPriv::Frozen => "Frozen",
|
||||
PermissionPriv::Disabled => "Disabled",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
/// Default initial permission of the root of a new tree.
|
||||
pub fn new_root() -> Self {
|
||||
Self(Active)
|
||||
}
|
||||
|
||||
/// Default initial permission of a reborrowed mutable reference.
|
||||
pub fn new_unique_2phase(ty_is_freeze: bool) -> Self {
|
||||
Self(Reserved { ty_is_freeze })
|
||||
}
|
||||
|
||||
/// Default initial permission of a reborrowed shared reference
|
||||
pub fn new_frozen() -> Self {
|
||||
Self(Frozen)
|
||||
}
|
||||
|
||||
/// Pretty-printing. Needs to be here and not in diagnostics.rs
|
||||
/// because `Self` is private.
|
||||
pub fn short_name(self) -> &'static str {
|
||||
// Make sure there are all of the same length as each other
|
||||
// and also as `diagnostics::DisplayFmtPermission.uninit` otherwise
|
||||
// alignment will be incorrect.
|
||||
match self.0 {
|
||||
Reserved { ty_is_freeze: true } => "Res",
|
||||
Reserved { ty_is_freeze: false } => "Re*",
|
||||
Active => "Act",
|
||||
Frozen => "Frz",
|
||||
Disabled => "Dis",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that there are no complaints from a possible protector.
|
||||
///
|
||||
/// Note: this is not in charge of checking that there *is* a protector,
|
||||
/// it should be used as
|
||||
/// ```
|
||||
/// let no_protector_error = if is_protected(tag) {
|
||||
/// old_perm.protector_allows_transition(new_perm)
|
||||
/// };
|
||||
/// ```
|
||||
pub fn protector_allows_transition(self, new: Self) -> bool {
|
||||
self.0.protector_allows_transition(new.0)
|
||||
}
|
||||
|
||||
/// Apply the transition to the inner PermissionPriv.
|
||||
pub fn perform_access(
|
||||
kind: AccessKind,
|
||||
rel_pos: AccessRelatedness,
|
||||
old_perm: Self,
|
||||
protected: bool,
|
||||
) -> Option<Self> {
|
||||
let old_state = old_perm.0;
|
||||
transition::perform_access(kind, rel_pos, old_state, protected).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod propagation_optimization_checks {
|
||||
pub use super::*;
|
||||
|
||||
mod util {
|
||||
pub use super::*;
|
||||
impl PermissionPriv {
|
||||
/// Enumerate all states
|
||||
pub fn all() -> impl Iterator<Item = PermissionPriv> {
|
||||
vec![
|
||||
Active,
|
||||
Reserved { ty_is_freeze: true },
|
||||
Reserved { ty_is_freeze: false },
|
||||
Frozen,
|
||||
Disabled,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessKind {
|
||||
/// Enumerate all AccessKind.
|
||||
pub fn all() -> impl Iterator<Item = AccessKind> {
|
||||
use AccessKind::*;
|
||||
[Read, Write].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessRelatedness {
|
||||
/// Enumerate all relative positions
|
||||
pub fn all() -> impl Iterator<Item = AccessRelatedness> {
|
||||
use AccessRelatedness::*;
|
||||
[This, StrictChildAccess, AncestorAccess, DistantAccess].into_iter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
// For any kind of access, if we do it twice the second should be a no-op.
|
||||
// Even if the protector has disappeared.
|
||||
fn all_transitions_idempotent() {
|
||||
use transition::*;
|
||||
for old in PermissionPriv::all() {
|
||||
for (old_protected, new_protected) in [(true, true), (true, false), (false, false)] {
|
||||
for access in AccessKind::all() {
|
||||
for rel_pos in AccessRelatedness::all() {
|
||||
if let Some(new) = perform_access(access, rel_pos, old, old_protected) {
|
||||
assert_eq!(
|
||||
new,
|
||||
perform_access(access, rel_pos, new, new_protected).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_read_is_noop_after_write() {
|
||||
use transition::*;
|
||||
let old_access = AccessKind::Write;
|
||||
let new_access = AccessKind::Read;
|
||||
for old in PermissionPriv::all() {
|
||||
for (old_protected, new_protected) in [(true, true), (true, false), (false, false)] {
|
||||
for rel_pos in AccessRelatedness::all().filter(|rel| rel.is_foreign()) {
|
||||
if let Some(new) = perform_access(old_access, rel_pos, old, old_protected) {
|
||||
assert_eq!(
|
||||
new,
|
||||
perform_access(new_access, rel_pos, new, new_protected).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Check that all transitions are consistent with the order on PermissionPriv,
|
||||
// i.e. Reserved -> Active -> Frozen -> Disabled
|
||||
fn access_transitions_progress_increasing() {
|
||||
use transition::*;
|
||||
for old in PermissionPriv::all() {
|
||||
for protected in [true, false] {
|
||||
for access in AccessKind::all() {
|
||||
for rel_pos in AccessRelatedness::all() {
|
||||
if let Some(new) = perform_access(access, rel_pos, old, protected) {
|
||||
assert!(old <= new);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
554
src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs
Normal file
554
src/tools/miri/src/borrow_tracker/tree_borrows/tree.rs
Normal file
@ -0,0 +1,554 @@
|
||||
//! In this file we handle the "Tree" part of Tree Borrows, i.e. all tree
|
||||
//! traversal functions, optimizations to trim branches, and keeping track of
|
||||
//! the relative position of the access to each node being updated. This of course
|
||||
//! also includes the definition of the tree structure.
|
||||
//!
|
||||
//! Functions here manipulate permissions but are oblivious to them: as
|
||||
//! the internals of `Permission` are private, the update process is a black
|
||||
//! box. All we need to know here are
|
||||
//! - the fact that updates depend only on the old state, the status of protectors,
|
||||
//! and the relative position of the access;
|
||||
//! - idempotency properties asserted in `perms.rs` (for optimizations)
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use rustc_const_eval::interpret::InterpResult;
|
||||
use rustc_data_structures::fx::FxHashSet;
|
||||
use rustc_target::abi::Size;
|
||||
|
||||
use crate::borrow_tracker::tree_borrows::{
|
||||
diagnostics::{NodeDebugInfo, TbError, TransitionError},
|
||||
unimap::{UniEntry, UniIndex, UniKeyMap, UniValMap},
|
||||
Permission,
|
||||
};
|
||||
use crate::borrow_tracker::{AccessKind, GlobalState, ProtectorKind};
|
||||
use crate::*;
|
||||
|
||||
/// Data for a single *location*.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct LocationState {
|
||||
/// This pointer's current permission
|
||||
permission: Permission,
|
||||
/// A location is initialized when it is child accessed for the first time,
|
||||
/// and it then stays initialized forever.
|
||||
/// Before initialization we still apply some preemptive transitions on
|
||||
/// `permission` to know what to do in case it ever gets initialized,
|
||||
/// but these can never cause any immediate UB. There can however be UB
|
||||
/// the moment we attempt to initalize (i.e. child-access) because some
|
||||
/// foreign access done between the creation and the initialization is
|
||||
/// incompatible with child accesses.
|
||||
initialized: bool,
|
||||
/// Strongest foreign access whose effects have already been applied to
|
||||
/// this node and all its children since the last child access.
|
||||
/// This is `None` if the most recent access is a child access,
|
||||
/// `Some(Write)` if at least one foreign write access has been applied
|
||||
/// since the previous child access, and `Some(Read)` if at least one
|
||||
/// foreign read and no foreign write have occurred since the last child access.
|
||||
latest_foreign_access: Option<AccessKind>,
|
||||
}
|
||||
|
||||
impl LocationState {
|
||||
/// Default initial state has never been accessed and has been subjected to no
|
||||
/// foreign access.
|
||||
fn new(permission: Permission) -> Self {
|
||||
Self { permission, initialized: false, latest_foreign_access: None }
|
||||
}
|
||||
|
||||
/// Record that this location was accessed through a child pointer by
|
||||
/// marking it as initialized
|
||||
fn with_access(mut self) -> Self {
|
||||
self.initialized = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
self.initialized
|
||||
}
|
||||
|
||||
pub fn permission(&self) -> Permission {
|
||||
self.permission
|
||||
}
|
||||
}
|
||||
|
||||
/// Tree structure with both parents and children since we want to be
|
||||
/// able to traverse the tree efficiently in both directions.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tree {
|
||||
/// Mapping from tags to keys. The key obtained can then be used in
|
||||
/// any of the `UniValMap` relative to this allocation, i.e. both the
|
||||
/// `nodes` and `rperms` of the same `Tree`.
|
||||
/// The parent-child relationship in `Node` is encoded in terms of these same
|
||||
/// keys, so traversing the entire tree needs exactly one access to
|
||||
/// `tag_mapping`.
|
||||
pub(super) tag_mapping: UniKeyMap<BorTag>,
|
||||
/// All nodes of this tree.
|
||||
pub(super) nodes: UniValMap<Node>,
|
||||
/// Maps a tag and a location to a perm, with possible lazy
|
||||
/// initialization.
|
||||
///
|
||||
/// NOTE: not all tags registered in `nodes` are necessarily in all
|
||||
/// ranges of `rperms`, because `rperms` is in part lazily initialized.
|
||||
/// Just because `nodes.get(key)` is `Some(_)` does not mean you can safely
|
||||
/// `unwrap` any `perm.get(key)`.
|
||||
///
|
||||
/// We do uphold the fact that `keys(perms)` is a subset of `keys(nodes)`
|
||||
pub(super) rperms: RangeMap<UniValMap<LocationState>>,
|
||||
/// The index of the root node.
|
||||
pub(super) root: UniIndex,
|
||||
}
|
||||
|
||||
/// A node in the borrow tree. Each node is uniquely identified by a tag via
|
||||
/// the `nodes` map of `Tree`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct Node {
|
||||
/// The tag of this node.
|
||||
pub tag: BorTag,
|
||||
/// All tags except the root have a parent tag.
|
||||
pub parent: Option<UniIndex>,
|
||||
/// If the pointer was reborrowed, it has children.
|
||||
// FIXME: bench to compare this to FxHashSet and to other SmallVec sizes
|
||||
pub children: SmallVec<[UniIndex; 4]>,
|
||||
/// Either `Reserved` or `Frozen`, the permission this tag will be lazily initialized
|
||||
/// to on the first access.
|
||||
default_initial_perm: Permission,
|
||||
/// Some extra information useful only for debugging purposes
|
||||
pub debug_info: NodeDebugInfo,
|
||||
}
|
||||
|
||||
/// Data given to the transition function
|
||||
struct NodeAppArgs<'node> {
|
||||
/// Node on which the transition is currently being applied
|
||||
node: &'node Node,
|
||||
/// Mutable access to its permissions
|
||||
perm: UniEntry<'node, LocationState>,
|
||||
/// Relative position of the access
|
||||
rel_pos: AccessRelatedness,
|
||||
}
|
||||
/// Data given to the error handler
|
||||
struct ErrHandlerArgs<'node, InErr> {
|
||||
/// Kind of error that occurred
|
||||
error_kind: InErr,
|
||||
/// Tag that triggered the error (not the tag that was accessed,
|
||||
/// rather the parent tag that had insufficient permissions or the
|
||||
/// non-parent tag that had a protector).
|
||||
faulty_tag: &'node NodeDebugInfo,
|
||||
}
|
||||
/// Internal contents of `Tree` with the minimum of mutable access for
|
||||
/// the purposes of the tree traversal functions: the permissions (`perms`) can be
|
||||
/// updated but not the tree structure (`tag_mapping` and `nodes`)
|
||||
struct TreeVisitor<'tree> {
|
||||
tag_mapping: &'tree UniKeyMap<BorTag>,
|
||||
nodes: &'tree UniValMap<Node>,
|
||||
perms: &'tree mut UniValMap<LocationState>,
|
||||
}
|
||||
|
||||
/// Whether to continue exploring the children recursively or not.
|
||||
enum ContinueTraversal {
|
||||
Recurse,
|
||||
SkipChildren,
|
||||
}
|
||||
|
||||
impl<'tree> TreeVisitor<'tree> {
|
||||
// Applies `f_propagate` to every vertex of the tree top-down in the following order: first
|
||||
// all ancestors of `start`, then `start` itself, then children of `start`, then the rest.
|
||||
// This ensures that errors are triggered in the following order
|
||||
// - first invalid accesses with insufficient permissions, closest to the root first,
|
||||
// - then protector violations, closest to `start` first.
|
||||
//
|
||||
// `f_propagate` should follow the following format: for a given `Node` it updates its
|
||||
// `Permission` depending on the position relative to `start` (given by an
|
||||
// `AccessRelatedness`).
|
||||
// It outputs whether the tree traversal for this subree should continue or not.
|
||||
fn traverse_parents_this_children_others<InnErr, OutErr>(
|
||||
mut self,
|
||||
start: BorTag,
|
||||
f_propagate: impl Fn(NodeAppArgs<'_>) -> Result<ContinueTraversal, InnErr>,
|
||||
err_builder: impl Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr,
|
||||
) -> Result<(), OutErr>
|
||||
where {
|
||||
struct TreeVisitAux<NodeApp, ErrHandler> {
|
||||
f_propagate: NodeApp,
|
||||
err_builder: ErrHandler,
|
||||
stack: Vec<(UniIndex, AccessRelatedness)>,
|
||||
}
|
||||
impl<NodeApp, InnErr, OutErr, ErrHandler> TreeVisitAux<NodeApp, ErrHandler>
|
||||
where
|
||||
NodeApp: Fn(NodeAppArgs<'_>) -> Result<ContinueTraversal, InnErr>,
|
||||
ErrHandler: Fn(ErrHandlerArgs<'_, InnErr>) -> OutErr,
|
||||
{
|
||||
fn pop(&mut self) -> Option<(UniIndex, AccessRelatedness)> {
|
||||
self.stack.pop()
|
||||
}
|
||||
|
||||
/// Apply the function to the current `tag`, and push its children
|
||||
/// to the stack of future tags to visit.
|
||||
fn exec_and_visit(
|
||||
&mut self,
|
||||
this: &mut TreeVisitor<'_>,
|
||||
tag: UniIndex,
|
||||
exclude: Option<UniIndex>,
|
||||
rel_pos: AccessRelatedness,
|
||||
) -> Result<(), OutErr> {
|
||||
// 1. apply the propagation function
|
||||
let node = this.nodes.get(tag).unwrap();
|
||||
let recurse =
|
||||
(self.f_propagate)(NodeAppArgs { node, perm: this.perms.entry(tag), rel_pos })
|
||||
.map_err(|error_kind| {
|
||||
(self.err_builder)(ErrHandlerArgs {
|
||||
error_kind,
|
||||
faulty_tag: &node.debug_info,
|
||||
})
|
||||
})?;
|
||||
// 2. add the children to the stack for future traversal
|
||||
if matches!(recurse, ContinueTraversal::Recurse) {
|
||||
let child_rel = rel_pos.for_child();
|
||||
for &child in node.children.iter() {
|
||||
// some child might be excluded from here and handled separately
|
||||
if Some(child) != exclude {
|
||||
self.stack.push((child, child_rel));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let start_idx = self.tag_mapping.get(&start).unwrap();
|
||||
let mut stack = TreeVisitAux { f_propagate, err_builder, stack: Vec::new() };
|
||||
{
|
||||
let mut path_ascend = Vec::new();
|
||||
// First climb to the root while recording the path
|
||||
let mut curr = start_idx;
|
||||
while let Some(ancestor) = self.nodes.get(curr).unwrap().parent {
|
||||
path_ascend.push((ancestor, curr));
|
||||
curr = ancestor;
|
||||
}
|
||||
// Then descend:
|
||||
// - execute f_propagate on each node
|
||||
// - record children in visit
|
||||
while let Some((ancestor, next_in_path)) = path_ascend.pop() {
|
||||
// Explore ancestors in descending order.
|
||||
// `next_in_path` is excluded from the recursion because it
|
||||
// will be the `ancestor` of the next iteration.
|
||||
// It also needs a different `AccessRelatedness` than the other
|
||||
// children of `ancestor`.
|
||||
stack.exec_and_visit(
|
||||
&mut self,
|
||||
ancestor,
|
||||
Some(next_in_path),
|
||||
AccessRelatedness::StrictChildAccess,
|
||||
)?;
|
||||
}
|
||||
};
|
||||
// All (potentially zero) ancestors have been explored, call f_propagate on start
|
||||
stack.exec_and_visit(&mut self, start_idx, None, AccessRelatedness::This)?;
|
||||
// up to this point we have never popped from `stack`, hence if the
|
||||
// path to the root is `root = p(n) <- p(n-1)... <- p(1) <- p(0) = start`
|
||||
// then now `stack` contains
|
||||
// `[<children(p(n)) except p(n-1)> ... <children(p(1)) except p(0)> <children(p(0))>]`,
|
||||
// all of which are for now unexplored.
|
||||
// This is the starting point of a standard DFS which will thus
|
||||
// explore all non-ancestors of `start` in the following order:
|
||||
// - all descendants of `start`;
|
||||
// - then the unexplored descendants of `parent(start)`;
|
||||
// ...
|
||||
// - until finally the unexplored descendants of `root`.
|
||||
while let Some((tag, rel_pos)) = stack.pop() {
|
||||
stack.exec_and_visit(&mut self, tag, None, rel_pos)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
/// Create a new tree, with only a root pointer.
|
||||
pub fn new(root_tag: BorTag, size: Size) -> Self {
|
||||
let root_perm = Permission::new_root();
|
||||
let mut tag_mapping = UniKeyMap::default();
|
||||
let root_idx = tag_mapping.insert(root_tag);
|
||||
let nodes = {
|
||||
let mut nodes = UniValMap::<Node>::default();
|
||||
nodes.insert(
|
||||
root_idx,
|
||||
Node {
|
||||
tag: root_tag,
|
||||
parent: None,
|
||||
children: SmallVec::default(),
|
||||
default_initial_perm: root_perm,
|
||||
debug_info: NodeDebugInfo::new(root_tag),
|
||||
},
|
||||
);
|
||||
nodes
|
||||
};
|
||||
let rperms = {
|
||||
let mut perms = UniValMap::default();
|
||||
perms.insert(root_idx, LocationState::new(root_perm).with_access());
|
||||
RangeMap::new(size, perms)
|
||||
};
|
||||
Self { root: root_idx, nodes, rperms, tag_mapping }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'tcx> Tree {
|
||||
/// Insert a new tag in the tree
|
||||
pub fn new_child(
|
||||
&mut self,
|
||||
parent_tag: BorTag,
|
||||
new_tag: BorTag,
|
||||
default_initial_perm: Permission,
|
||||
range: AllocRange,
|
||||
) -> InterpResult<'tcx> {
|
||||
assert!(!self.tag_mapping.contains_key(&new_tag));
|
||||
let idx = self.tag_mapping.insert(new_tag);
|
||||
let parent_idx = self.tag_mapping.get(&parent_tag).unwrap();
|
||||
// Create the node
|
||||
self.nodes.insert(
|
||||
idx,
|
||||
Node {
|
||||
tag: new_tag,
|
||||
parent: Some(parent_idx),
|
||||
children: SmallVec::default(),
|
||||
default_initial_perm,
|
||||
debug_info: NodeDebugInfo::new(new_tag),
|
||||
},
|
||||
);
|
||||
// Register new_tag as a child of parent_tag
|
||||
self.nodes.get_mut(parent_idx).unwrap().children.push(idx);
|
||||
// Initialize perms
|
||||
let perm = LocationState::new(default_initial_perm).with_access();
|
||||
for (_range, perms) in self.rperms.iter_mut(range.start, range.size) {
|
||||
perms.insert(idx, perm);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deallocation requires
|
||||
/// - a pointer that permits write accesses
|
||||
/// - the absence of Strong Protectors anywhere in the allocation
|
||||
pub fn dealloc(
|
||||
&mut self,
|
||||
tag: BorTag,
|
||||
range: AllocRange,
|
||||
global: &GlobalState,
|
||||
) -> InterpResult<'tcx> {
|
||||
self.perform_access(AccessKind::Write, tag, range, global)?;
|
||||
let access_info = &self.nodes.get(self.tag_mapping.get(&tag).unwrap()).unwrap().debug_info;
|
||||
for (_range, perms) in self.rperms.iter_mut(range.start, range.size) {
|
||||
TreeVisitor { nodes: &self.nodes, tag_mapping: &self.tag_mapping, perms }
|
||||
.traverse_parents_this_children_others(
|
||||
tag,
|
||||
|args: NodeAppArgs<'_>| -> Result<ContinueTraversal, TransitionError> {
|
||||
let NodeAppArgs { node, .. } = args;
|
||||
if global.borrow().protected_tags.get(&node.tag)
|
||||
== Some(&ProtectorKind::StrongProtector)
|
||||
{
|
||||
Err(TransitionError::ProtectedDealloc)
|
||||
} else {
|
||||
Ok(ContinueTraversal::Recurse)
|
||||
}
|
||||
},
|
||||
|args: ErrHandlerArgs<'_, TransitionError>| -> InterpErrorInfo<'tcx> {
|
||||
let ErrHandlerArgs { error_kind, faulty_tag } = args;
|
||||
TbError {
|
||||
faulty_tag,
|
||||
access_kind: AccessKind::Write,
|
||||
error_kind,
|
||||
tag_of_access: access_info,
|
||||
}
|
||||
.build()
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Maps the following propagation procedure to each range:
|
||||
/// - initialize if needed;
|
||||
/// - compute new state after transition;
|
||||
/// - check that there is no protector that would forbid this;
|
||||
/// - record this specific location as accessed.
|
||||
pub fn perform_access(
|
||||
&mut self,
|
||||
access_kind: AccessKind,
|
||||
tag: BorTag,
|
||||
range: AllocRange,
|
||||
global: &GlobalState,
|
||||
) -> InterpResult<'tcx> {
|
||||
let access_info = &self.nodes.get(self.tag_mapping.get(&tag).unwrap()).unwrap().debug_info;
|
||||
for (_range, perms) in self.rperms.iter_mut(range.start, range.size) {
|
||||
TreeVisitor { nodes: &self.nodes, tag_mapping: &self.tag_mapping, perms }
|
||||
.traverse_parents_this_children_others(
|
||||
tag,
|
||||
|args: NodeAppArgs<'_>| -> Result<ContinueTraversal, TransitionError> {
|
||||
let NodeAppArgs { node, mut perm, rel_pos } = args;
|
||||
|
||||
let old_state =
|
||||
perm.or_insert_with(|| LocationState::new(node.default_initial_perm));
|
||||
|
||||
// Optimize the tree traversal.
|
||||
// The optimization here consists of observing thanks to the tests
|
||||
// `foreign_read_is_noop_after_write` and `all_transitions_idempotent`
|
||||
// that if we apply twice in a row the effects of a foreign access
|
||||
// we can skip some branches.
|
||||
// "two foreign accesses in a row" occurs when `perm.latest_foreign_access` is `Some(_)`
|
||||
// AND the `rel_pos` of the current access corresponds to a foreign access.
|
||||
if rel_pos.is_foreign() {
|
||||
let new_access_noop =
|
||||
match (old_state.latest_foreign_access, access_kind) {
|
||||
// Previously applied transition makes the new one a guaranteed
|
||||
// noop in the two following cases:
|
||||
// (1) justified by `foreign_read_is_noop_after_write`
|
||||
(Some(AccessKind::Write), AccessKind::Read) => true,
|
||||
// (2) justified by `all_transitions_idempotent`
|
||||
(Some(old), new) if old == new => true,
|
||||
// In all other cases there has been a recent enough
|
||||
// child access that the effects of the new foreign access
|
||||
// need to be applied to this subtree.
|
||||
_ => false,
|
||||
};
|
||||
if new_access_noop {
|
||||
// Abort traversal if the new transition is indeed guaranteed
|
||||
// to be noop.
|
||||
return Ok(ContinueTraversal::SkipChildren);
|
||||
} else {
|
||||
// Otherwise propagate this time, and also record the
|
||||
// access that just occurred so that we can skip the propagation
|
||||
// next time.
|
||||
old_state.latest_foreign_access = Some(access_kind);
|
||||
}
|
||||
} else {
|
||||
// A child access occurred, this breaks the streak of "two foreign
|
||||
// accesses in a row" and we reset this field.
|
||||
old_state.latest_foreign_access = None;
|
||||
}
|
||||
|
||||
let old_perm = old_state.permission;
|
||||
let protected = global.borrow().protected_tags.contains_key(&node.tag);
|
||||
let new_perm =
|
||||
Permission::perform_access(access_kind, rel_pos, old_perm, protected)
|
||||
.ok_or(TransitionError::ChildAccessForbidden(old_perm))?;
|
||||
if protected
|
||||
// Can't trigger Protector on uninitialized locations
|
||||
&& old_state.initialized
|
||||
&& !old_perm.protector_allows_transition(new_perm)
|
||||
{
|
||||
return Err(TransitionError::ProtectedTransition(old_perm, new_perm));
|
||||
}
|
||||
old_state.permission = new_perm;
|
||||
old_state.initialized |= !rel_pos.is_foreign();
|
||||
Ok(ContinueTraversal::Recurse)
|
||||
},
|
||||
|args: ErrHandlerArgs<'_, TransitionError>| -> InterpErrorInfo<'tcx> {
|
||||
let ErrHandlerArgs { error_kind, faulty_tag } = args;
|
||||
TbError { faulty_tag, access_kind, error_kind, tag_of_access: access_info }
|
||||
.build()
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Integration with the BorTag garbage collector
|
||||
impl Tree {
|
||||
pub fn remove_unreachable_tags(&mut self, live_tags: &FxHashSet<BorTag>) {
|
||||
assert!(self.keep_only_needed(self.root, live_tags)); // root can't be removed
|
||||
}
|
||||
|
||||
/// Traverses the entire tree looking for useless tags.
|
||||
/// Returns true iff the tag it was called on is still live or has live children,
|
||||
/// and removes from the tree all tags that have no live children.
|
||||
///
|
||||
/// NOTE: This leaves in the middle of the tree tags that are unreachable but have
|
||||
/// reachable children. There is a potential for compacting the tree by reassigning
|
||||
/// children of dead tags to the nearest live parent, but it must be done with care
|
||||
/// not to remove UB.
|
||||
///
|
||||
/// Example: Consider the tree `root - parent - child`, with `parent: Frozen` and
|
||||
/// `child: Reserved`. This tree can exist. If we blindly delete `parent` and reassign
|
||||
/// `child` to be a direct child of `root` then Writes to `child` are now permitted
|
||||
/// whereas they were not when `parent` was still there.
|
||||
fn keep_only_needed(&mut self, idx: UniIndex, live: &FxHashSet<BorTag>) -> bool {
|
||||
let node = self.nodes.get(idx).unwrap();
|
||||
// FIXME: this function does a lot of cloning, a 2-pass approach is possibly
|
||||
// more efficient. It could consist of
|
||||
// 1. traverse the Tree, collect all useless tags in a Vec
|
||||
// 2. traverse the Vec, remove all tags previously selected
|
||||
// Bench it.
|
||||
let children: SmallVec<_> = node
|
||||
.children
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|child| self.keep_only_needed(*child, live))
|
||||
.collect();
|
||||
let no_children = children.is_empty();
|
||||
let node = self.nodes.get_mut(idx).unwrap();
|
||||
node.children = children;
|
||||
if !live.contains(&node.tag) && no_children {
|
||||
// All of the children and this node are unreachable, delete this tag
|
||||
// from the tree (the children have already been deleted by recursive
|
||||
// calls).
|
||||
// Due to the API of UniMap we must absolutely call
|
||||
// `UniValMap::remove` for the key of this tag on *all* maps that used it
|
||||
// (which are `self.nodes` and every range of `self.rperms`)
|
||||
// before we can safely apply `UniValMap::forget` to truly remove
|
||||
// the tag from the mapping.
|
||||
let tag = node.tag;
|
||||
self.nodes.remove(idx);
|
||||
for perms in self.rperms.iter_mut_all() {
|
||||
perms.remove(idx);
|
||||
}
|
||||
self.tag_mapping.remove(&tag);
|
||||
// The tag has been deleted, inform the caller
|
||||
false
|
||||
} else {
|
||||
// The tag is still live or has live children, it must be kept
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VisitTags for Tree {
|
||||
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
|
||||
// To ensure that the root never gets removed, we visit it
|
||||
// (the `root` node of `Tree` is not an `Option<_>`)
|
||||
visit(self.nodes.get(self.root).unwrap().tag)
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative position of the access
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AccessRelatedness {
|
||||
/// The accessed pointer is the current one
|
||||
This,
|
||||
/// The accessed pointer is a (transitive) child of the current one.
|
||||
// Current pointer is excluded (unlike in some other places of this module
|
||||
// where "child" is inclusive).
|
||||
StrictChildAccess,
|
||||
/// The accessed pointer is a (transitive) parent of the current one.
|
||||
// Current pointer is excluded.
|
||||
AncestorAccess,
|
||||
/// The accessed pointer is neither of the above.
|
||||
// It's a cousin/uncle/etc., something in a side branch.
|
||||
// FIXME: find a better name ?
|
||||
DistantAccess,
|
||||
}
|
||||
|
||||
impl AccessRelatedness {
|
||||
/// Check that access is either Ancestor or Distant, i.e. not
|
||||
/// a transitive child (initial pointer included).
|
||||
pub fn is_foreign(self) -> bool {
|
||||
matches!(self, AccessRelatedness::AncestorAccess | AccessRelatedness::DistantAccess)
|
||||
}
|
||||
|
||||
/// Given the AccessRelatedness for the parent node, compute the AccessRelatedness
|
||||
/// for the child node. This function assumes that we propagate away from the initial
|
||||
/// access.
|
||||
pub fn for_child(self) -> Self {
|
||||
use AccessRelatedness::*;
|
||||
match self {
|
||||
AncestorAccess | This => AncestorAccess,
|
||||
StrictChildAccess | DistantAccess => DistantAccess,
|
||||
}
|
||||
}
|
||||
}
|
304
src/tools/miri/src/borrow_tracker/tree_borrows/unimap.rs
Normal file
304
src/tools/miri/src/borrow_tracker/tree_borrows/unimap.rs
Normal file
@ -0,0 +1,304 @@
|
||||
//! This module implements the `UniMap`, which is a way to get efficient mappings
|
||||
//! optimized for the setting of `tree_borrows/tree.rs`.
|
||||
//!
|
||||
//! A `UniKeyMap<K>` is a (slow) mapping from `K` to `UniIndex`,
|
||||
//! and `UniValMap<V>` is a (fast) mapping from `UniIndex` to `V`.
|
||||
//! Thus a pair `(UniKeyMap<K>, UniValMap<V>)` acts as a virtual `HashMap<K, V>`.
|
||||
//!
|
||||
//! Because of the asymmetry in access time, the use-case for `UniMap` is the following:
|
||||
//! a tuple `(UniKeyMap<K>, Vec<UniValMap<V>>)` is much more efficient than
|
||||
//! the equivalent `Vec<HashMap<K, V>>` it represents if all maps have similar
|
||||
//! sets of keys.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
use rustc_data_structures::fx::FxHashMap;
|
||||
|
||||
/// Intermediate key between a UniKeyMap and a UniValMap.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct UniIndex {
|
||||
idx: u32,
|
||||
}
|
||||
|
||||
/// From K to UniIndex
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UniKeyMap<K> {
|
||||
/// Underlying map that does all the hard work.
|
||||
/// Key invariant: the contents of `deassigned` are disjoint from the
|
||||
/// keys of `mapping`, and together they form the set of contiguous integers
|
||||
/// `0 .. (mapping.len() + deassigned.len())`.
|
||||
mapping: FxHashMap<K, u32>,
|
||||
/// Indexes that can be reused: memory gain when the map gets sparse
|
||||
/// due to many deletions.
|
||||
deassigned: Vec<u32>,
|
||||
}
|
||||
|
||||
/// From UniIndex to V
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UniValMap<V> {
|
||||
/// The mapping data. Thanks to Vec we get both fast accesses, and
|
||||
/// a memory-optimal representation if there are few deletions.
|
||||
data: Vec<Option<V>>,
|
||||
}
|
||||
|
||||
impl<V> Default for UniValMap<V> {
|
||||
fn default() -> Self {
|
||||
Self { data: Vec::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> UniKeyMap<K>
|
||||
where
|
||||
K: Hash + Eq,
|
||||
{
|
||||
/// How many keys/index pairs are currently active.
|
||||
pub fn len(&self) -> usize {
|
||||
self.mapping.len()
|
||||
}
|
||||
|
||||
/// Whether this key has an associated index or not.
|
||||
pub fn contains_key(&self, key: &K) -> bool {
|
||||
self.mapping.contains_key(key)
|
||||
}
|
||||
|
||||
/// Assign this key to a new index. Panics if the key is already assigned,
|
||||
/// use `get_or_insert` for a version that instead returns the existing
|
||||
/// assignment.
|
||||
#[track_caller]
|
||||
pub fn insert(&mut self, key: K) -> UniIndex {
|
||||
// We want an unused index. First we attempt to find one from `deassigned`,
|
||||
// and if `deassigned` is empty we generate a fresh index.
|
||||
let idx = self.deassigned.pop().unwrap_or_else(|| {
|
||||
// `deassigned` is empty, so all keys in use are already in `mapping`.
|
||||
// The next available key is `mapping.len()`.
|
||||
self.mapping.len().try_into().expect("UniMap ran out of useable keys")
|
||||
});
|
||||
if self.mapping.insert(key, idx).is_some() {
|
||||
panic!(
|
||||
"This key is already assigned to a different index; either use `get_or_insert` instead if you care about this data, or first call `remove` to undo the preexisting assignment."
|
||||
);
|
||||
};
|
||||
UniIndex { idx }
|
||||
}
|
||||
|
||||
/// If it exists, the index this key maps to.
|
||||
pub fn get(&self, key: &K) -> Option<UniIndex> {
|
||||
self.mapping.get(key).map(|&idx| UniIndex { idx })
|
||||
}
|
||||
|
||||
/// Either get a previously existing entry, or create a new one if it
|
||||
/// is not yet present.
|
||||
pub fn get_or_insert(&mut self, key: K) -> UniIndex {
|
||||
self.get(&key).unwrap_or_else(|| self.insert(key))
|
||||
}
|
||||
|
||||
/// Return whatever index this key was using to the deassigned pool.
|
||||
///
|
||||
/// Note: calling this function can be dangerous. If the index still exists
|
||||
/// somewhere in a `UniValMap` and is reassigned by the `UniKeyMap` then
|
||||
/// it will inherit the old value of a completely unrelated key.
|
||||
/// If you `UniKeyMap::remove` a key you should make sure to also `UniValMap::remove`
|
||||
/// the associated `UniIndex` from ALL `UniValMap`s.
|
||||
///
|
||||
/// Example of such behavior:
|
||||
/// ```
|
||||
/// let mut keymap = UniKeyMap::<char>::default();
|
||||
/// let mut valmap = UniValMap::<char>::default();
|
||||
/// // Insert 'a' -> _ -> 'A'
|
||||
/// let idx_a = keymap.insert('a');
|
||||
/// valmap.insert(idx_a, 'A');
|
||||
/// // Remove 'a' -> _, but forget to remove _ -> 'A'
|
||||
/// keymap.remove(&'a');
|
||||
/// // valmap.remove(idx_a); // If we uncomment this line the issue is fixed
|
||||
/// // Insert 'b' -> _
|
||||
/// let idx_b = keymap.insert('b');
|
||||
/// let val_b = valmap.get(idx_b);
|
||||
/// assert_eq!(val_b, Some('A')); // Oh no
|
||||
/// // assert_eq!(val_b, None); // This is what we would have expected
|
||||
/// ```
|
||||
pub fn remove(&mut self, key: &K) {
|
||||
if let Some(idx) = self.mapping.remove(key) {
|
||||
self.deassigned.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> UniValMap<V> {
|
||||
/// Whether this index has an associated value.
|
||||
pub fn contains_idx(&self, idx: UniIndex) -> bool {
|
||||
self.data.get(idx.idx as usize).and_then(Option::as_ref).is_some()
|
||||
}
|
||||
|
||||
/// Reserve enough space to insert the value at the right index.
|
||||
fn extend_to_length(&mut self, len: usize) {
|
||||
if len > self.data.len() {
|
||||
let nb = len - self.data.len();
|
||||
self.data.reserve(nb);
|
||||
for _ in 0..nb {
|
||||
self.data.push(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign a value to the index. Permanently overwrites any previous value.
|
||||
pub fn insert(&mut self, idx: UniIndex, val: V) {
|
||||
self.extend_to_length(idx.idx as usize + 1);
|
||||
self.data[idx.idx as usize] = Some(val)
|
||||
}
|
||||
|
||||
/// Get the value at this index, if it exists.
|
||||
pub fn get(&self, idx: UniIndex) -> Option<&V> {
|
||||
self.data.get(idx.idx as usize).and_then(Option::as_ref)
|
||||
}
|
||||
|
||||
/// Get the value at this index mutably, if it exists.
|
||||
pub fn get_mut(&mut self, idx: UniIndex) -> Option<&mut V> {
|
||||
self.data.get_mut(idx.idx as usize).and_then(Option::as_mut)
|
||||
}
|
||||
|
||||
/// Delete any value associated with this index. Ok even if the index
|
||||
/// has no associated value.
|
||||
pub fn remove(&mut self, idx: UniIndex) {
|
||||
if idx.idx as usize >= self.data.len() {
|
||||
return;
|
||||
}
|
||||
self.data[idx.idx as usize] = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// An access to a single value of the map.
|
||||
pub struct UniEntry<'a, V> {
|
||||
inner: &'a mut Option<V>,
|
||||
}
|
||||
|
||||
impl<'a, V> UniValMap<V> {
|
||||
/// Get a wrapper around a mutable access to the value corresponding to `idx`.
|
||||
pub fn entry(&'a mut self, idx: UniIndex) -> UniEntry<'a, V> {
|
||||
self.extend_to_length(idx.idx as usize + 1);
|
||||
UniEntry { inner: &mut self.data[idx.idx as usize] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V> UniEntry<'a, V> {
|
||||
/// Insert in the map and get the value.
|
||||
pub fn or_insert_with<F>(&mut self, default: F) -> &mut V
|
||||
where
|
||||
F: FnOnce() -> V,
|
||||
{
|
||||
if self.inner.is_none() {
|
||||
*self.inner = Some(default());
|
||||
}
|
||||
self.inner.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extend_to_length() {
|
||||
let mut km = UniValMap::<char>::default();
|
||||
km.extend_to_length(10);
|
||||
assert!(km.data.len() == 10);
|
||||
km.extend_to_length(0);
|
||||
assert!(km.data.len() == 10);
|
||||
km.extend_to_length(10);
|
||||
assert!(km.data.len() == 10);
|
||||
km.extend_to_length(11);
|
||||
assert!(km.data.len() == 11);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MapWitness<K, V> {
|
||||
key: UniKeyMap<K>,
|
||||
val: UniValMap<V>,
|
||||
map: FxHashMap<K, V>,
|
||||
}
|
||||
|
||||
impl<K, V> MapWitness<K, V>
|
||||
where
|
||||
K: Copy + Hash + Eq,
|
||||
V: Copy + Eq + std::fmt::Debug,
|
||||
{
|
||||
fn insert(&mut self, k: K, v: V) {
|
||||
// UniMap
|
||||
let i = self.key.get_or_insert(k);
|
||||
self.val.insert(i, v);
|
||||
// HashMap
|
||||
self.map.insert(k, v);
|
||||
// Consistency: nothing to check
|
||||
}
|
||||
|
||||
fn get(&self, k: &K) {
|
||||
// UniMap
|
||||
let v1 = self.key.get(k).and_then(|i| self.val.get(i));
|
||||
// HashMap
|
||||
let v2 = self.map.get(k);
|
||||
// Consistency
|
||||
assert_eq!(v1, v2);
|
||||
}
|
||||
|
||||
fn get_mut(&mut self, k: &K) {
|
||||
// UniMap
|
||||
let v1 = self.key.get(k).and_then(|i| self.val.get_mut(i));
|
||||
// HashMap
|
||||
let v2 = self.map.get_mut(k);
|
||||
// Consistency
|
||||
assert_eq!(v1, v2);
|
||||
}
|
||||
fn remove(&mut self, k: &K) {
|
||||
// UniMap
|
||||
if let Some(i) = self.key.get(k) {
|
||||
self.val.remove(i);
|
||||
}
|
||||
self.key.remove(k);
|
||||
// HashMap
|
||||
self.map.remove(k);
|
||||
// Consistency: nothing to check
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consistency_small() {
|
||||
let mut m = MapWitness::<u64, char>::default();
|
||||
m.insert(1, 'a');
|
||||
m.insert(2, 'b');
|
||||
m.get(&1);
|
||||
m.get_mut(&2);
|
||||
m.remove(&2);
|
||||
m.insert(1, 'c');
|
||||
m.get(&1);
|
||||
m.insert(3, 'd');
|
||||
m.insert(4, 'e');
|
||||
m.insert(4, 'f');
|
||||
m.get(&2);
|
||||
m.get(&3);
|
||||
m.get(&4);
|
||||
m.get(&5);
|
||||
m.remove(&100);
|
||||
m.get_mut(&100);
|
||||
m.get(&100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consistency_large() {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = DefaultHasher::new();
|
||||
let mut map = MapWitness::<u64, u64>::default();
|
||||
for i in 0..1000 {
|
||||
i.hash(&mut hasher);
|
||||
let rng = hasher.finish();
|
||||
let op = rng % 3 == 0;
|
||||
let key = (rng / 2) % 50;
|
||||
let val = (rng / 100) % 1000;
|
||||
if op {
|
||||
map.insert(key, val);
|
||||
} else {
|
||||
map.get(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,10 @@ pub enum TerminationInfo {
|
||||
help: Option<String>,
|
||||
history: Option<TagHistory>,
|
||||
},
|
||||
TreeBorrowsUb {
|
||||
msg: String,
|
||||
// FIXME: incomplete
|
||||
},
|
||||
Int2PtrWithStrictProvenance,
|
||||
Deadlock,
|
||||
MultipleSymbolDefinitions {
|
||||
@ -61,6 +65,7 @@ impl fmt::Display for TerminationInfo {
|
||||
"integer-to-pointer casts and `ptr::from_exposed_addr` are not supported with `-Zmiri-strict-provenance`"
|
||||
),
|
||||
StackedBorrowsUb { msg, .. } => write!(f, "{msg}"),
|
||||
TreeBorrowsUb { msg } => write!(f, "{msg}"),
|
||||
Deadlock => write!(f, "the evaluated program deadlocked"),
|
||||
MultipleSymbolDefinitions { link_name, .. } =>
|
||||
write!(f, "multiple definitions of symbol `{link_name}`"),
|
||||
@ -184,7 +189,8 @@ pub fn report_error<'tcx, 'mir>(
|
||||
Abort(_) => Some("abnormal termination"),
|
||||
UnsupportedInIsolation(_) | Int2PtrWithStrictProvenance =>
|
||||
Some("unsupported operation"),
|
||||
StackedBorrowsUb { .. } | DataRace { .. } => Some("Undefined Behavior"),
|
||||
StackedBorrowsUb { .. } | TreeBorrowsUb { .. } | DataRace { .. } =>
|
||||
Some("Undefined Behavior"),
|
||||
Deadlock => Some("deadlock"),
|
||||
MultipleSymbolDefinitions { .. } | SymbolShimClashing { .. } => None,
|
||||
};
|
||||
@ -212,6 +218,12 @@ pub fn report_error<'tcx, 'mir>(
|
||||
}
|
||||
}
|
||||
helps
|
||||
},
|
||||
TreeBorrowsUb { .. } => {
|
||||
let helps = vec![
|
||||
(None, format!("this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental")),
|
||||
];
|
||||
helps
|
||||
}
|
||||
MultipleSymbolDefinitions { first, first_crate, second, second_crate, .. } =>
|
||||
vec![
|
||||
|
@ -87,7 +87,7 @@ pub struct MiriConfig {
|
||||
pub env: Vec<(OsString, OsString)>,
|
||||
/// Determine if validity checking is enabled.
|
||||
pub validate: bool,
|
||||
/// Determines if Stacked Borrows is enabled.
|
||||
/// Determines if Stacked Borrows or Tree Borrows is enabled.
|
||||
pub borrow_tracker: Option<BorrowTrackerMethod>,
|
||||
/// Controls alignment checking.
|
||||
pub check_alignment: AlignmentCheck,
|
||||
@ -134,7 +134,7 @@ pub struct MiriConfig {
|
||||
pub preemption_rate: f64,
|
||||
/// Report the current instruction being executed every N basic blocks.
|
||||
pub report_progress: Option<u32>,
|
||||
/// Whether Stacked Borrows retagging should recurse into fields of datatypes.
|
||||
/// Whether Stacked Borrows and Tree Borrows retagging should recurse into fields of datatypes.
|
||||
pub retag_fields: RetagFields,
|
||||
/// The location of a shared object file to load when calling external functions
|
||||
/// FIXME! consider allowing users to specify paths to multiple SO files, or to a directory
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod convert;
|
||||
|
||||
use std::any::Any;
|
||||
use std::cmp;
|
||||
use std::iter;
|
||||
use std::num::NonZeroUsize;
|
||||
@ -23,7 +24,23 @@ use rand::RngCore;
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
|
||||
/// A trait to work around not having trait object upcasting:
|
||||
/// Add `AsAny` as supertrait and your trait objects can be turned into `&dyn Any` on which you can
|
||||
/// then call `downcast`.
|
||||
pub trait AsAny: Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
impl<T: Any> AsAny for T {
|
||||
#[inline(always)]
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
#[inline(always)]
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// This mapping should match `decode_error_kind` in
|
||||
// <https://github.com/rust-lang/rust/blob/master/library/std/src/sys/unix/mod.rs>.
|
||||
@ -119,6 +136,7 @@ fn try_resolve_did(tcx: TyCtxt<'_>, path: &[&str], namespace: Option<Namespace>)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
|
||||
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
/// Checks if the given crate/module exists.
|
||||
fn have_module(&self, path: &[&str]) -> bool {
|
||||
|
@ -9,6 +9,7 @@
|
||||
#![feature(nonzero_ops)]
|
||||
#![feature(local_key_cell_methods)]
|
||||
#![feature(is_terminal)]
|
||||
#![feature(round_ties_even)]
|
||||
// Configure clippy and other lints
|
||||
#![allow(
|
||||
clippy::collapsible_else_if,
|
||||
@ -94,6 +95,7 @@ pub use crate::shims::EvalContextExt as _;
|
||||
pub use crate::borrow_tracker::stacked_borrows::{
|
||||
EvalContextExt as _, Item, Permission, Stack, Stacks,
|
||||
};
|
||||
pub use crate::borrow_tracker::tree_borrows::{EvalContextExt as _, Tree};
|
||||
pub use crate::borrow_tracker::{
|
||||
BorTag, BorrowTrackerMethod, CallId, EvalContextExt as _, RetagFields,
|
||||
};
|
||||
|
@ -38,7 +38,7 @@ pub const SIGRTMAX: i32 = 42;
|
||||
|
||||
/// Extra data stored with each stack frame
|
||||
pub struct FrameExtra<'tcx> {
|
||||
/// Extra data for Stacked Borrows.
|
||||
/// Extra data for the Borrow Tracker.
|
||||
pub borrow_tracker: Option<borrow_tracker::FrameState>,
|
||||
|
||||
/// If this is Some(), then this is a special "catch unwind" frame (the frame of `try_fn`
|
||||
@ -146,7 +146,7 @@ impl fmt::Display for MiriMemoryKind {
|
||||
pub enum Provenance {
|
||||
Concrete {
|
||||
alloc_id: AllocId,
|
||||
/// Stacked Borrows tag.
|
||||
/// Borrow Tracker tag.
|
||||
tag: BorTag,
|
||||
},
|
||||
Wildcard,
|
||||
@ -195,7 +195,7 @@ impl fmt::Debug for Provenance {
|
||||
} else {
|
||||
write!(f, "[{alloc_id:?}]")?;
|
||||
}
|
||||
// Print Stacked Borrows tag.
|
||||
// Print Borrow Tracker tag.
|
||||
write!(f, "{tag:?}")?;
|
||||
}
|
||||
Provenance::Wildcard => {
|
||||
|
@ -232,6 +232,19 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read bytes from a `(ptr, len)` argument
|
||||
fn read_byte_slice<'i>(&'i self, bytes: &OpTy<'tcx, Provenance>) -> InterpResult<'tcx, &'i [u8]>
|
||||
where
|
||||
'mir: 'i,
|
||||
{
|
||||
let this = self.eval_context_ref();
|
||||
let (ptr, len) = this.read_immediate(bytes)?.to_scalar_pair();
|
||||
let ptr = ptr.to_pointer(this)?;
|
||||
let len = len.to_target_usize(this)?;
|
||||
let bytes = this.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Emulates calling a foreign item, failing if the item is not supported.
|
||||
/// This function will handle `goto_block` if needed.
|
||||
/// Returns Ok(None) if the foreign item was completely handled
|
||||
@ -427,13 +440,27 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
})?;
|
||||
this.write_scalar(Scalar::from_u64(alloc_id.0.get()), dest)?;
|
||||
}
|
||||
"miri_print_borrow_stacks" => {
|
||||
let [id] = this.check_shim(abi, Abi::Rust, link_name, args)?;
|
||||
"miri_print_borrow_state" => {
|
||||
let [id, show_unnamed] = this.check_shim(abi, Abi::Rust, link_name, args)?;
|
||||
let id = this.read_scalar(id)?.to_u64()?;
|
||||
let show_unnamed = this.read_scalar(show_unnamed)?.to_bool()?;
|
||||
if let Some(id) = std::num::NonZeroU64::new(id) {
|
||||
this.print_stacks(AllocId(id))?;
|
||||
this.print_borrow_state(AllocId(id), show_unnamed)?;
|
||||
}
|
||||
}
|
||||
"miri_pointer_name" => {
|
||||
// This associates a name to a tag. Very useful for debugging, and also makes
|
||||
// tests more strict.
|
||||
let [ptr, nth_parent, name] = this.check_shim(abi, Abi::Rust, link_name, args)?;
|
||||
let ptr = this.read_pointer(ptr)?;
|
||||
let nth_parent = this.read_scalar(nth_parent)?.to_u8()?;
|
||||
let name = this.read_byte_slice(name)?;
|
||||
// We must make `name` owned because we need to
|
||||
// end the shared borrow from `read_byte_slice` before we can
|
||||
// start the mutable borrow for `give_pointer_debug_name`.
|
||||
let name = String::from_utf8_lossy(name).into_owned();
|
||||
this.give_pointer_debug_name(ptr, nth_parent, &name)?;
|
||||
}
|
||||
"miri_static_root" => {
|
||||
let [ptr] = this.check_shim(abi, Abi::Rust, link_name, args)?;
|
||||
let ptr = this.read_pointer(ptr)?;
|
||||
@ -487,12 +514,8 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
// Writes some bytes to the interpreter's stdout/stderr. See the
|
||||
// README for details.
|
||||
"miri_write_to_stdout" | "miri_write_to_stderr" => {
|
||||
let [bytes] = this.check_shim(abi, Abi::Rust, link_name, args)?;
|
||||
let (ptr, len) = this.read_immediate(bytes)?.to_scalar_pair();
|
||||
let ptr = ptr.to_pointer(this)?;
|
||||
let len = len.to_target_usize(this)?;
|
||||
let msg = this.read_bytes_ptr_strip_provenance(ptr, Size::from_bytes(len))?;
|
||||
|
||||
let [msg] = this.check_shim(abi, Abi::Rust, link_name, args)?;
|
||||
let msg = this.read_byte_slice(msg)?;
|
||||
// Note: we're ignoring errors writing to host stdout/stderr.
|
||||
let _ignore = match link_name.as_str() {
|
||||
"miri_write_to_stdout" => std::io::stdout().write_all(msg),
|
||||
|
@ -157,6 +157,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
| "ceilf32"
|
||||
| "truncf32"
|
||||
| "roundf32"
|
||||
| "rintf32"
|
||||
=> {
|
||||
let [f] = check_arg_count(args)?;
|
||||
// FIXME: Using host floats.
|
||||
@ -174,6 +175,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
"ceilf32" => f.ceil(),
|
||||
"truncf32" => f.trunc(),
|
||||
"roundf32" => f.round(),
|
||||
"rintf32" => f.round_ties_even(),
|
||||
_ => bug!(),
|
||||
};
|
||||
this.write_scalar(Scalar::from_u32(f.to_bits()), dest)?;
|
||||
@ -192,6 +194,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
| "ceilf64"
|
||||
| "truncf64"
|
||||
| "roundf64"
|
||||
| "rintf64"
|
||||
=> {
|
||||
let [f] = check_arg_count(args)?;
|
||||
// FIXME: Using host floats.
|
||||
@ -209,6 +212,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
"ceilf64" => f.ceil(),
|
||||
"truncf64" => f.trunc(),
|
||||
"roundf64" => f.round(),
|
||||
"rintf64" => f.round_ties_even(),
|
||||
_ => bug!(),
|
||||
};
|
||||
this.write_scalar(Scalar::from_u64(f.to_bits()), dest)?;
|
||||
|
@ -17,7 +17,6 @@ use crate::shims::os_str::bytes_to_os_str;
|
||||
use crate::*;
|
||||
use shims::os_str::os_str_to_bytes;
|
||||
use shims::time::system_time_to_duration;
|
||||
use shims::unix::linux::fd::epoll::Epoll;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileHandle {
|
||||
@ -25,17 +24,9 @@ pub struct FileHandle {
|
||||
writable: bool,
|
||||
}
|
||||
|
||||
pub trait FileDescriptor: std::fmt::Debug {
|
||||
pub trait FileDescriptor: std::fmt::Debug + helpers::AsAny {
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
fn as_file_handle<'tcx>(&self) -> InterpResult<'tcx, &FileHandle> {
|
||||
throw_unsup_format!("{} cannot be used as FileHandle", self.name());
|
||||
}
|
||||
|
||||
fn as_epoll_handle<'tcx>(&mut self) -> InterpResult<'tcx, &mut Epoll> {
|
||||
throw_unsup_format!("not an epoll file descriptor");
|
||||
}
|
||||
|
||||
fn read<'tcx>(
|
||||
&mut self,
|
||||
_communicate_allowed: bool,
|
||||
@ -69,7 +60,9 @@ pub trait FileDescriptor: std::fmt::Debug {
|
||||
|
||||
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>>;
|
||||
|
||||
fn is_tty(&self) -> bool;
|
||||
fn is_tty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn as_unix_host_fd(&self) -> Option<i32> {
|
||||
@ -82,10 +75,6 @@ impl FileDescriptor for FileHandle {
|
||||
"FILE"
|
||||
}
|
||||
|
||||
fn as_file_handle<'tcx>(&self) -> InterpResult<'tcx, &FileHandle> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn read<'tcx>(
|
||||
&mut self,
|
||||
communicate_allowed: bool,
|
||||
@ -271,10 +260,6 @@ impl FileDescriptor for NullOutput {
|
||||
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
|
||||
Ok(Box::new(NullOutput))
|
||||
}
|
||||
|
||||
fn is_tty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -694,7 +679,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
} else if this.tcx.sess.target.os == "macos" && cmd == this.eval_libc_i32("F_FULLFSYNC") {
|
||||
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
|
||||
// FIXME: Support fullfsync for all FDs
|
||||
let FileHandle { file, writable } = file_descriptor.as_file_handle()?;
|
||||
let FileHandle { file, writable } =
|
||||
file_descriptor.as_any().downcast_ref::<FileHandle>().ok_or_else(|| {
|
||||
err_unsup_format!(
|
||||
"`F_FULLFSYNC` is only supported on file-backed file descriptors"
|
||||
)
|
||||
})?;
|
||||
let io_result = maybe_sync_file(file, *writable, File::sync_all);
|
||||
this.try_unwrap_io_result(io_result)
|
||||
} else {
|
||||
@ -1530,7 +1520,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
Ok(Scalar::from_i32(
|
||||
if let Some(file_descriptor) = this.machine.file_handler.handles.get_mut(&fd) {
|
||||
// FIXME: Support ftruncate64 for all FDs
|
||||
let FileHandle { file, writable } = file_descriptor.as_file_handle()?;
|
||||
let FileHandle { file, writable } =
|
||||
file_descriptor.as_any().downcast_ref::<FileHandle>().ok_or_else(|| {
|
||||
err_unsup_format!(
|
||||
"`ftruncate64` is only supported on file-backed file descriptors"
|
||||
)
|
||||
})?;
|
||||
if *writable {
|
||||
if let Ok(length) = length.try_into() {
|
||||
let result = file.set_len(length);
|
||||
@ -1571,7 +1566,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
|
||||
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
|
||||
// FIXME: Support fsync for all FDs
|
||||
let FileHandle { file, writable } = file_descriptor.as_file_handle()?;
|
||||
let FileHandle { file, writable } =
|
||||
file_descriptor.as_any().downcast_ref::<FileHandle>().ok_or_else(|| {
|
||||
err_unsup_format!("`fsync` is only supported on file-backed file descriptors")
|
||||
})?;
|
||||
let io_result = maybe_sync_file(file, *writable, File::sync_all);
|
||||
this.try_unwrap_io_result(io_result)
|
||||
} else {
|
||||
@ -1593,7 +1591,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
|
||||
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
|
||||
// FIXME: Support fdatasync for all FDs
|
||||
let FileHandle { file, writable } = file_descriptor.as_file_handle()?;
|
||||
let FileHandle { file, writable } =
|
||||
file_descriptor.as_any().downcast_ref::<FileHandle>().ok_or_else(|| {
|
||||
err_unsup_format!(
|
||||
"`fdatasync` is only supported on file-backed file descriptors"
|
||||
)
|
||||
})?;
|
||||
let io_result = maybe_sync_file(file, *writable, File::sync_data);
|
||||
this.try_unwrap_io_result(io_result)
|
||||
} else {
|
||||
@ -1638,7 +1641,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
|
||||
if let Some(file_descriptor) = this.machine.file_handler.handles.get(&fd) {
|
||||
// FIXME: Support sync_data_range for all FDs
|
||||
let FileHandle { file, writable } = file_descriptor.as_file_handle()?;
|
||||
let FileHandle { file, writable } =
|
||||
file_descriptor.as_any().downcast_ref::<FileHandle>().ok_or_else(|| {
|
||||
err_unsup_format!(
|
||||
"`sync_data_range` is only supported on file-backed file descriptors"
|
||||
)
|
||||
})?;
|
||||
let io_result = maybe_sync_file(file, *writable, File::sync_data);
|
||||
Ok(Scalar::from_i32(this.try_unwrap_io_result(io_result)?))
|
||||
} else {
|
||||
@ -1942,7 +1950,16 @@ impl FileMetadata {
|
||||
) -> InterpResult<'tcx, Option<FileMetadata>> {
|
||||
let option = ecx.machine.file_handler.handles.get(&fd);
|
||||
let file = match option {
|
||||
Some(file_descriptor) => &file_descriptor.as_file_handle()?.file,
|
||||
Some(file_descriptor) =>
|
||||
&file_descriptor
|
||||
.as_any()
|
||||
.downcast_ref::<FileHandle>()
|
||||
.ok_or_else(|| {
|
||||
err_unsup_format!(
|
||||
"obtaining metadata is only supported on file-backed file descriptors"
|
||||
)
|
||||
})?
|
||||
.file,
|
||||
None => return ecx.handle_not_found().map(|_: i32| None),
|
||||
};
|
||||
let metadata = file.metadata();
|
||||
|
@ -80,7 +80,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
let event = EpollEvent { events, data };
|
||||
|
||||
if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) {
|
||||
let epfd = epfd.as_epoll_handle()?;
|
||||
let epfd = epfd
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Epoll>()
|
||||
.ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?;
|
||||
|
||||
epfd.file_descriptors.insert(fd, event);
|
||||
Ok(Scalar::from_i32(0))
|
||||
@ -89,7 +92,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
}
|
||||
} else if op == epoll_ctl_del {
|
||||
if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) {
|
||||
let epfd = epfd.as_epoll_handle()?;
|
||||
let epfd = epfd
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Epoll>()
|
||||
.ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_ctl`"))?;
|
||||
|
||||
epfd.file_descriptors.remove(&fd);
|
||||
Ok(Scalar::from_i32(0))
|
||||
@ -148,7 +154,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
|
||||
let numevents = 0;
|
||||
if let Some(epfd) = this.machine.file_handler.handles.get_mut(&epfd) {
|
||||
let _epfd = epfd.as_epoll_handle()?;
|
||||
let _epfd = epfd
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Epoll>()
|
||||
.ok_or_else(|| err_unsup_format!("non-epoll FD passed to `epoll_wait`"))?;
|
||||
|
||||
// FIXME return number of events ready when scheme for marking events ready exists
|
||||
Ok(Scalar::from_i32(numevents))
|
||||
|
@ -32,18 +32,10 @@ impl FileDescriptor for Epoll {
|
||||
"epoll"
|
||||
}
|
||||
|
||||
fn as_epoll_handle<'tcx>(&mut self) -> InterpResult<'tcx, &mut Epoll> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn dup(&mut self) -> io::Result<Box<dyn FileDescriptor>> {
|
||||
Ok(Box::new(self.clone()))
|
||||
}
|
||||
|
||||
fn is_tty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self: Box<Self>,
|
||||
_communicate_allowed: bool,
|
||||
|
@ -28,10 +28,6 @@ impl FileDescriptor for Event {
|
||||
Ok(Box::new(Event { val: self.val.clone() }))
|
||||
}
|
||||
|
||||
fn is_tty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self: Box<Self>,
|
||||
_communicate_allowed: bool,
|
||||
|
@ -19,10 +19,6 @@ impl FileDescriptor for SocketPair {
|
||||
Ok(Box::new(SocketPair))
|
||||
}
|
||||
|
||||
fn is_tty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn close<'tcx>(
|
||||
self: Box<Self>,
|
||||
_communicate_allowed: bool,
|
||||
|
@ -139,8 +139,9 @@ regexes! {
|
||||
STDOUT:
|
||||
// Windows file paths
|
||||
r"\\" => "/",
|
||||
// erase Stacked Borrows tags
|
||||
// erase borrow tags
|
||||
"<[0-9]+>" => "<TAG>",
|
||||
"<[0-9]+=" => "<TAG=",
|
||||
}
|
||||
|
||||
regexes! {
|
||||
@ -149,8 +150,9 @@ regexes! {
|
||||
r"\.rs:[0-9]+:[0-9]+(: [0-9]+:[0-9]+)?" => ".rs:LL:CC",
|
||||
// erase alloc ids
|
||||
"alloc[0-9]+" => "ALLOC",
|
||||
// erase Stacked Borrows tags
|
||||
// erase borrow tags
|
||||
"<[0-9]+>" => "<TAG>",
|
||||
"<[0-9]+=" => "<TAG=",
|
||||
// erase whitespace that differs between platforms
|
||||
r" +at (.*\.rs)" => " at $1",
|
||||
// erase generics in backtraces
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! Make sure that a retag acts like a write for the data race model.
|
||||
//! Make sure that a retag acts like a read for the data race model.
|
||||
//@compile-flags: -Zmiri-preemption-rate=0
|
||||
#[derive(Copy, Clone)]
|
||||
struct SendPtr(*mut u8);
|
||||
|
@ -0,0 +1,19 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// Check that TB properly rejects alternating Reads and Writes, but tolerates
|
||||
// alternating only Reads to Reserved mutable references.
|
||||
pub fn main() {
|
||||
let x = &mut 0u8;
|
||||
let y = unsafe { &mut *(x as *mut u8) };
|
||||
// Foreign Read, but this is a no-op from the point of view of y (still Reserved)
|
||||
let _val = *x;
|
||||
// Now we activate y, for this to succeed y needs to not have been Frozen
|
||||
// by the previous operation
|
||||
*y += 1; // Success
|
||||
// This time y gets Frozen...
|
||||
let _val = *x;
|
||||
// ... and the next Write attempt fails.
|
||||
*y += 1; // Failure //~ ERROR: /write access through .* is forbidden/
|
||||
let _val = *x;
|
||||
*y += 1; // Unreachable
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
error: Undefined Behavior: write access through <TAG> is forbidden because it is a child of <TAG> which is Frozen.
|
||||
--> $DIR/alternate-read-write.rs:LL:CC
|
||||
|
|
||||
LL | *y += 1; // Failure
|
||||
| ^^^^^^^ write access through <TAG> is forbidden because it is a child of <TAG> which is Frozen.
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `main` at $DIR/alternate-read-write.rs:LL:CC
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
42
src/tools/miri/tests/fail/tree-borrows/fragile-data-race.rs
Normal file
42
src/tools/miri/tests/fail/tree-borrows/fragile-data-race.rs
Normal file
@ -0,0 +1,42 @@
|
||||
//! Race-condition-like interaction between a read and a reborrow.
|
||||
//! Even though no write or fake write occurs, reads have an effect on protected
|
||||
//! Reserved. This is a protected-retag/read data race, but is not *detected* as
|
||||
//! a data race violation because reborrows are not writes.
|
||||
//!
|
||||
//! This test is sensitive to the exact schedule so we disable preemption.
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-preemption-rate=0
|
||||
use std::ptr::addr_of_mut;
|
||||
use std::thread;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct SendPtr(*mut u8);
|
||||
|
||||
unsafe impl Send for SendPtr {}
|
||||
|
||||
// First thread is just a reborrow, but for an instant `x` is
|
||||
// protected and thus vulnerable to foreign reads.
|
||||
fn thread_1(x: &mut u8) -> SendPtr {
|
||||
thread::yield_now(); // make the other thread go first
|
||||
SendPtr(x as *mut u8)
|
||||
}
|
||||
|
||||
// Second thread simply performs a read.
|
||||
fn thread_2(x: &u8) {
|
||||
let _val = *x;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut x = 0u8;
|
||||
let x_1 = unsafe { &mut *addr_of_mut!(x) };
|
||||
let xg = unsafe { &*addr_of_mut!(x) };
|
||||
|
||||
// The two threads are executed in parallel on aliasing pointers.
|
||||
// UB occurs if the read of thread_2 occurs while the protector of thread_1
|
||||
// is in place.
|
||||
let hf = thread::spawn(move || thread_1(x_1));
|
||||
let hg = thread::spawn(move || thread_2(xg));
|
||||
let SendPtr(p) = hf.join().unwrap();
|
||||
let () = hg.join().unwrap();
|
||||
|
||||
unsafe { *p = 1 }; //~ ERROR: /write access through .* is forbidden/
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
error: Undefined Behavior: write access through <TAG> is forbidden because it is a child of <TAG> which is Frozen.
|
||||
--> $DIR/fragile-data-race.rs:LL:CC
|
||||
|
|
||||
LL | unsafe { *p = 1 };
|
||||
| ^^^^^^ write access through <TAG> is forbidden because it is a child of <TAG> which is Frozen.
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `main` at $DIR/fragile-data-race.rs:LL:CC
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
22
src/tools/miri/tests/fail/tree-borrows/outside-range.rs
Normal file
22
src/tools/miri/tests/fail/tree-borrows/outside-range.rs
Normal file
@ -0,0 +1,22 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// Check that in the case of locations outside the range of a pointer,
|
||||
// protectors trigger if and only if the location has already been accessed
|
||||
fn main() {
|
||||
unsafe {
|
||||
let data = &mut [0u8, 1, 2, 3];
|
||||
let raw = data.as_mut_ptr();
|
||||
stuff(&mut *raw, raw);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn stuff(x: &mut u8, y: *mut u8) {
|
||||
let xraw = x as *mut u8;
|
||||
// No issue here: location 1 is not accessed
|
||||
*y.add(1) = 42;
|
||||
// Still no issue: location 2 is not invalidated
|
||||
let _val = *xraw.add(2);
|
||||
// However protector triggers if location is both accessed and invalidated
|
||||
let _val = *xraw.add(3);
|
||||
*y.add(3) = 42; //~ ERROR: /write access through .* is forbidden/
|
||||
}
|
19
src/tools/miri/tests/fail/tree-borrows/outside-range.stderr
Normal file
19
src/tools/miri/tests/fail/tree-borrows/outside-range.stderr
Normal file
@ -0,0 +1,19 @@
|
||||
error: Undefined Behavior: write access through <TAG> is forbidden because it is a foreign tag for <TAG>, which would hence change from Reserved to Disabled, but <TAG> is protected
|
||||
--> $DIR/outside-range.rs:LL:CC
|
||||
|
|
||||
LL | *y.add(3) = 42;
|
||||
| ^^^^^^^^^^^^^^ write access through <TAG> is forbidden because it is a foreign tag for <TAG>, which would hence change from Reserved to Disabled, but <TAG> is protected
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `stuff` at $DIR/outside-range.rs:LL:CC
|
||||
note: inside `main`
|
||||
--> $DIR/outside-range.rs:LL:CC
|
||||
|
|
||||
LL | stuff(&mut *raw, raw);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
14
src/tools/miri/tests/fail/tree-borrows/read-to-local.rs
Normal file
14
src/tools/miri/tests/fail/tree-borrows/read-to-local.rs
Normal file
@ -0,0 +1,14 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// Read to local variable kills reborrows *and* raw pointers derived from them.
|
||||
// This test would succeed under Stacked Borrows.
|
||||
fn main() {
|
||||
unsafe {
|
||||
let mut root = 6u8;
|
||||
let mref = &mut root;
|
||||
let ptr = mref as *mut u8;
|
||||
*ptr = 0; // Write
|
||||
assert_eq!(root, 0); // Parent Read
|
||||
*ptr = 0; //~ ERROR: /write access through .* is forbidden/
|
||||
}
|
||||
}
|
14
src/tools/miri/tests/fail/tree-borrows/read-to-local.stderr
Normal file
14
src/tools/miri/tests/fail/tree-borrows/read-to-local.stderr
Normal file
@ -0,0 +1,14 @@
|
||||
error: Undefined Behavior: write access through <TAG> is forbidden because it is a child of <TAG> which is Frozen.
|
||||
--> $DIR/read-to-local.rs:LL:CC
|
||||
|
|
||||
LL | *ptr = 0;
|
||||
| ^^^^^^^^ write access through <TAG> is forbidden because it is a child of <TAG> which is Frozen.
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `main` at $DIR/read-to-local.rs:LL:CC
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
@ -0,0 +1,34 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-tag-gc=0
|
||||
|
||||
// Check how a Reserved with interior mutability
|
||||
// responds to a Foreign Write under a Protector
|
||||
#[path = "../../../utils/mod.rs"]
|
||||
mod utils;
|
||||
use utils::macros::*;
|
||||
|
||||
use std::cell::UnsafeCell;
|
||||
|
||||
fn main() {
|
||||
unsafe {
|
||||
let n = &mut UnsafeCell::new(0u8);
|
||||
name!(n.get(), "base");
|
||||
let x = &mut *(n as *mut UnsafeCell<_>);
|
||||
name!(x.get(), "x");
|
||||
let y = (&mut *n).get();
|
||||
name!(y);
|
||||
write_second(x, y);
|
||||
unsafe fn write_second(x: &mut UnsafeCell<u8>, y: *mut u8) {
|
||||
let alloc_id = alloc_id!(x.get());
|
||||
name!(x.get(), "callee:x");
|
||||
name!(x.get()=>1, "caller:x");
|
||||
name!(y, "callee:y");
|
||||
name!(y, "caller:y");
|
||||
print_state!(alloc_id);
|
||||
// Right before the faulty Write, x is
|
||||
// - Reserved
|
||||
// - with interior mut
|
||||
// - Protected
|
||||
*y = 1; //~ ERROR: /write access through .* is forbidden/
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Re*| └─┬──<TAG=base>
|
||||
| Re*| ├─┬──<TAG=x>
|
||||
| Re*| │ └─┬──<TAG=caller:x>
|
||||
| Re*| │ └────<TAG=callee:x> Strongly protected
|
||||
| Re*| └────<TAG=y,callee:y,caller:y>
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
error: Undefined Behavior: write access through <TAG> (also named 'y,callee:y,caller:y') is forbidden because it is a foreign tag for <TAG> (also named 'callee:x'), which would hence change from Reserved to Disabled, but <TAG> (also named 'callee:x') is protected
|
||||
--> $DIR/cell-protected-write.rs:LL:CC
|
||||
|
|
||||
LL | *y = 1;
|
||||
| ^^^^^^ write access through <TAG> (also named 'y,callee:y,caller:y') is forbidden because it is a foreign tag for <TAG> (also named 'callee:x'), which would hence change from Reserved to Disabled, but <TAG> (also named 'callee:x') is protected
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `main::write_second` at $DIR/cell-protected-write.rs:LL:CC
|
||||
note: inside `main`
|
||||
--> $DIR/cell-protected-write.rs:LL:CC
|
||||
|
|
||||
LL | write_second(x, y);
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
@ -0,0 +1,32 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-tag-gc=0
|
||||
|
||||
#[path = "../../../utils/mod.rs"]
|
||||
mod utils;
|
||||
use utils::macros::*;
|
||||
|
||||
// Check how a Reserved without interior mutability responds to a Foreign
|
||||
// Write when under a protector
|
||||
fn main() {
|
||||
unsafe {
|
||||
let n = &mut 0u8;
|
||||
name!(n);
|
||||
let x = &mut *(n as *mut _);
|
||||
name!(x);
|
||||
let y = (&mut *n) as *mut _;
|
||||
name!(y);
|
||||
write_second(x, y);
|
||||
unsafe fn write_second(x: &mut u8, y: *mut u8) {
|
||||
let alloc_id = alloc_id!(x);
|
||||
name!(x, "callee:x");
|
||||
name!(x=>1, "caller:x");
|
||||
name!(y, "callee:y");
|
||||
name!(y, "caller:y");
|
||||
print_state!(alloc_id);
|
||||
// Right before the faulty Write, x is
|
||||
// - Reserved
|
||||
// - Protected
|
||||
// The Write turns it Disabled
|
||||
*y = 0; //~ ERROR: /write access through .* is forbidden/
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Res| └─┬──<TAG=n>
|
||||
| Res| ├─┬──<TAG=x>
|
||||
| Res| │ └─┬──<TAG=caller:x>
|
||||
| Res| │ └────<TAG=callee:x> Strongly protected
|
||||
| Res| └────<TAG=y,callee:y,caller:y>
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
error: Undefined Behavior: write access through <TAG> (also named 'y,callee:y,caller:y') is forbidden because it is a foreign tag for <TAG> (also named 'callee:x'), which would hence change from Reserved to Disabled, but <TAG> (also named 'callee:x') is protected
|
||||
--> $DIR/int-protected-write.rs:LL:CC
|
||||
|
|
||||
LL | *y = 0;
|
||||
| ^^^^^^ write access through <TAG> (also named 'y,callee:y,caller:y') is forbidden because it is a foreign tag for <TAG> (also named 'callee:x'), which would hence change from Reserved to Disabled, but <TAG> (also named 'callee:x') is protected
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `main::write_second` at $DIR/int-protected-write.rs:LL:CC
|
||||
note: inside `main`
|
||||
--> $DIR/int-protected-write.rs:LL:CC
|
||||
|
|
||||
LL | write_second(x, y);
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
28
src/tools/miri/tests/fail/tree-borrows/retag-data-race.rs
Normal file
28
src/tools/miri/tests/fail/tree-borrows/retag-data-race.rs
Normal file
@ -0,0 +1,28 @@
|
||||
//! Make sure that a retag acts like a read for the data race model.
|
||||
//! This is a retag/write race condition.
|
||||
//!
|
||||
//! This test is sensitive to the exact schedule so we disable preemption.
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-preemption-rate=0
|
||||
#[derive(Copy, Clone)]
|
||||
struct SendPtr(*mut u8);
|
||||
|
||||
unsafe impl Send for SendPtr {}
|
||||
|
||||
unsafe fn thread_1(SendPtr(p): SendPtr) {
|
||||
let _r = &*p;
|
||||
}
|
||||
|
||||
unsafe fn thread_2(SendPtr(p): SendPtr) {
|
||||
*p = 5; //~ ERROR: Data race detected between (1) Read on thread `<unnamed>` and (2) Write on thread `<unnamed>`
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
let p = std::ptr::addr_of_mut!(x);
|
||||
let p = SendPtr(p);
|
||||
|
||||
let t1 = std::thread::spawn(move || unsafe { thread_1(p) });
|
||||
let t2 = std::thread::spawn(move || unsafe { thread_2(p) });
|
||||
let _ = t1.join();
|
||||
let _ = t2.join();
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
error: Undefined Behavior: Data race detected between (1) Read on thread `<unnamed>` and (2) Write on thread `<unnamed>` at ALLOC. (2) just happened here
|
||||
--> $DIR/retag-data-race.rs:LL:CC
|
||||
|
|
||||
LL | *p = 5;
|
||||
| ^^^^^^ Data race detected between (1) Read on thread `<unnamed>` and (2) Write on thread `<unnamed>` at ALLOC. (2) just happened here
|
||||
|
|
||||
help: and (1) occurred earlier here
|
||||
--> $DIR/retag-data-race.rs:LL:CC
|
||||
|
|
||||
LL | let _r = &*p;
|
||||
| ^^^
|
||||
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
|
||||
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
|
||||
= note: BACKTRACE (of the first span):
|
||||
= note: inside `thread_2` at $DIR/retag-data-race.rs:LL:CC
|
||||
note: inside closure
|
||||
--> $DIR/retag-data-race.rs:LL:CC
|
||||
|
|
||||
LL | let t2 = std::thread::spawn(move || unsafe { thread_2(p) });
|
||||
| ^^^^^^^^^^^
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
@ -0,0 +1,27 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// We invalidate a reference during a 2-phase borrow by doing a Foreign
|
||||
// Write in between the initial reborrow and function entry. UB occurs
|
||||
// on function entry when reborrow from a Disabled fails.
|
||||
// This test would pass under Stacked Borrows, but Tree Borrows
|
||||
// is more strict on 2-phase borrows.
|
||||
|
||||
struct Foo(u64);
|
||||
impl Foo {
|
||||
#[rustfmt::skip] // rustfmt is wrong about which line contains an error
|
||||
fn add(&mut self, n: u64) -> u64 { //~ ERROR: /read access through .* is forbidden/
|
||||
self.0 + n
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let mut f = Foo(0);
|
||||
let inner = &mut f.0 as *mut u64;
|
||||
let _res = f.add(unsafe {
|
||||
let n = f.0;
|
||||
// This is the access at fault, but it's not immediately apparent because
|
||||
// the reference that got invalidated is not under a Protector.
|
||||
*inner = 42;
|
||||
n
|
||||
});
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
error: Undefined Behavior: read access through <TAG> is forbidden because it is a child of <TAG> which is Disabled.
|
||||
--> $DIR/write-during-2phase.rs:LL:CC
|
||||
|
|
||||
LL | fn add(&mut self, n: u64) -> u64 {
|
||||
| ^^^^^^^^^ read access through <TAG> is forbidden because it is a child of <TAG> which is Disabled.
|
||||
|
|
||||
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
|
||||
= note: BACKTRACE:
|
||||
= note: inside `Foo::add` at $DIR/write-during-2phase.rs:LL:CC
|
||||
note: inside `main`
|
||||
--> $DIR/write-during-2phase.rs:LL:CC
|
||||
|
|
||||
LL | let _res = f.add(unsafe {
|
||||
| ________________^
|
||||
LL | | let n = f.0;
|
||||
LL | | // This is the access at fault, but it's not immediately apparent because
|
||||
LL | | // the reference that got invalidated is not under a Protector.
|
||||
LL | | *inner = 42;
|
||||
LL | | n
|
||||
LL | | });
|
||||
| |______^
|
||||
|
||||
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
|
||||
|
||||
error: aborting due to previous error
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-permissive-provenance
|
||||
|
||||
fn ensure_allocs_can_be_adjacent() {
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
trait Foo {
|
||||
const ID: i32;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
fn main() {
|
||||
vec![()].into_iter();
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-strict-provenance
|
||||
#![feature(strict_provenance, strict_provenance_atomic_ptr)]
|
||||
use std::sync::atomic::{
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
fn main() {
|
||||
assert_eq!(std::thread::available_parallelism().unwrap().get(), 1);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
#![allow(incomplete_features)] // for trait upcasting
|
||||
#![feature(allocator_api, trait_upcasting)]
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance
|
||||
#![feature(ptr_internals)]
|
||||
|
||||
fn main() {
|
||||
|
3
src/tools/miri/tests/pass/box.tree.stdout
Normal file
3
src/tools/miri/tests/pass/box.tree.stdout
Normal file
@ -0,0 +1,3 @@
|
||||
pair_foo = PairFoo { fst: Foo(42), snd: Foo(1337) }
|
||||
foo #0 = Foo(42)
|
||||
foo #1 = Foo(1337)
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-strict-provenance
|
||||
#![feature(btree_drain_filter)]
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
// Check that you can cast between different pointers to trait objects
|
||||
// whose vtable have the same kind (both lengths, or both trait pointers).
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-strict-provenance
|
||||
|
||||
use std::sync::mpsc::{channel, sync_channel};
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-disable-isolation -Zmiri-strict-provenance
|
||||
|
||||
use std::sync::{Arc, Barrier, Condvar, Mutex, Once, RwLock};
|
||||
|
20
src/tools/miri/tests/pass/concurrency/sync.tree.stdout
Normal file
20
src/tools/miri/tests/pass/concurrency/sync.tree.stdout
Normal file
@ -0,0 +1,20 @@
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
before wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
||||
after wait
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-strict-provenance
|
||||
|
||||
//! The main purpose of this test is to check that if we take a pointer to
|
||||
|
@ -1,3 +1,6 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::thread;
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
Dropping: 8 (should be before 'Continue main 1').
|
||||
Dropping: 8 (should be before 'Continue main 1').
|
||||
Continue main 1.
|
||||
Joining: 7 (should be before 'Continue main 2').
|
||||
Continue main 2.
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-disable-alignment-check
|
||||
|
||||
fn main() {
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
#![feature(arbitrary_self_types, unsize, coerce_unsized, dispatch_from_dyn)]
|
||||
#![feature(rustc_attrs)]
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows -Zmiri-permissive-provenance
|
||||
#![feature(extern_types)]
|
||||
|
||||
extern "C" {
|
||||
|
@ -1,4 +1,5 @@
|
||||
#![feature(stmt_expr_attributes)]
|
||||
#![feature(round_ties_even)]
|
||||
#![allow(arithmetic_overflow)]
|
||||
use std::fmt::Debug;
|
||||
use std::hint::black_box;
|
||||
@ -9,6 +10,7 @@ fn main() {
|
||||
more_casts();
|
||||
ops();
|
||||
nan_casts();
|
||||
rounding();
|
||||
}
|
||||
|
||||
// Helper function to avoid promotion so that this tests "run-time" casts, not CTFE.
|
||||
@ -553,3 +555,31 @@ fn nan_casts() {
|
||||
assert!(nan1_32.is_nan());
|
||||
assert!(nan2_32.is_nan());
|
||||
}
|
||||
|
||||
fn rounding() {
|
||||
// Test cases taken from the library's tests for this feature
|
||||
// f32
|
||||
assert_eq(2.5f32.round_ties_even(), 2.0f32);
|
||||
assert_eq(1.0f32.round_ties_even(), 1.0f32);
|
||||
assert_eq(1.3f32.round_ties_even(), 1.0f32);
|
||||
assert_eq(1.5f32.round_ties_even(), 2.0f32);
|
||||
assert_eq(1.7f32.round_ties_even(), 2.0f32);
|
||||
assert_eq(0.0f32.round_ties_even(), 0.0f32);
|
||||
assert_eq((-0.0f32).round_ties_even(), -0.0f32);
|
||||
assert_eq((-1.0f32).round_ties_even(), -1.0f32);
|
||||
assert_eq((-1.3f32).round_ties_even(), -1.0f32);
|
||||
assert_eq((-1.5f32).round_ties_even(), -2.0f32);
|
||||
assert_eq((-1.7f32).round_ties_even(), -2.0f32);
|
||||
// f64
|
||||
assert_eq(2.5f64.round_ties_even(), 2.0f64);
|
||||
assert_eq(1.0f64.round_ties_even(), 1.0f64);
|
||||
assert_eq(1.3f64.round_ties_even(), 1.0f64);
|
||||
assert_eq(1.5f64.round_ties_even(), 2.0f64);
|
||||
assert_eq(1.7f64.round_ties_even(), 2.0f64);
|
||||
assert_eq(0.0f64.round_ties_even(), 0.0f64);
|
||||
assert_eq((-0.0f64).round_ties_even(), -0.0f64);
|
||||
assert_eq((-1.0f64).round_ties_even(), -1.0f64);
|
||||
assert_eq((-1.3f64).round_ties_even(), -1.0f64);
|
||||
assert_eq((-1.5f64).round_ties_even(), -2.0f64);
|
||||
assert_eq((-1.7f64).round_ties_even(), -2.0f64);
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
use std::future::*;
|
||||
use std::marker::PhantomPinned;
|
||||
use std::pin::*;
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
#![feature(generators, generator_trait, never_type)]
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
use std::collections::HashMap;
|
||||
use std::hash::BuildHasher;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-permissive-provenance
|
||||
|
||||
use std::mem;
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
#![feature(linked_list_cursors)]
|
||||
use std::collections::LinkedList;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
// Make sure validation can handle many overlapping shared borrows for different parts of a data structure
|
||||
use std::cell::RefCell;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-ignore-leaks
|
||||
|
||||
fn main() {
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
// This tests that the size of Option<Box<i32>> is the same as *const i32.
|
||||
fn option_box_deref() -> i32 {
|
||||
let val = Some(Box::new(42));
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-permissive-provenance
|
||||
#![feature(ptr_metadata, const_raw_ptr_comparison)]
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
#![feature(strict_provenance)]
|
||||
#![feature(pointer_byte_offsets)]
|
||||
use std::{mem, ptr};
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-permissive-provenance
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-permissive-provenance
|
||||
#![feature(strict_provenance)]
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
// Test what happens when we read parts of a pointer.
|
||||
// Related to <https://github.com/rust-lang/rust/issues/69488>.
|
||||
fn ptr_partial_read() {
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-strict-provenance
|
||||
#![feature(new_uninit)]
|
||||
#![feature(get_mut_unchecked)]
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn par_for<I, F>(iter: I, f: F)
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-strict-provenance
|
||||
#![feature(new_uninit)]
|
||||
#![feature(slice_as_chunks)]
|
||||
|
@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
extern "Rust" {
|
||||
fn miri_get_alloc_id(ptr: *const u8) -> u64;
|
||||
fn miri_print_borrow_stacks(alloc_id: u64);
|
||||
fn miri_print_borrow_state(alloc_id: u64, show_unnamed: bool);
|
||||
}
|
||||
|
||||
fn get_alloc_id(ptr: *const u8) -> u64 {
|
||||
@ -15,7 +15,9 @@ fn get_alloc_id(ptr: *const u8) -> u64 {
|
||||
}
|
||||
|
||||
fn print_borrow_stacks(alloc_id: u64) {
|
||||
unsafe { miri_print_borrow_stacks(alloc_id) }
|
||||
unsafe {
|
||||
miri_print_borrow_state(alloc_id, /* ignored: show_unnamed */ false)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
//@compile-flags: -Zmiri-ignore-leaks
|
||||
|
||||
//! Test that leaking threads works, and that their destructors are not executed.
|
||||
|
1
src/tools/miri/tests/pass/threadleak_ignored.tree.stderr
Normal file
1
src/tools/miri/tests/pass/threadleak_ignored.tree.stderr
Normal file
@ -0,0 +1 @@
|
||||
Dropping 0
|
@ -1,3 +1,5 @@
|
||||
//@revisions: stack tree
|
||||
//@[tree]compile-flags: -Zmiri-tree-borrows
|
||||
#![feature(strict_provenance)]
|
||||
use std::{mem, ptr};
|
||||
|
||||
|
27
src/tools/miri/tests/pass/tree-borrows/2phase-interiormut.rs
Normal file
27
src/tools/miri/tests/pass/tree-borrows/2phase-interiormut.rs
Normal file
@ -0,0 +1,27 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// Counterpart to tests/fail/tree-borrows/write-during-2phase.rs,
|
||||
// this is the opposite situation: the Write is not problematic because
|
||||
// the Protector has not yet been added and the Reserved has interior
|
||||
// mutability.
|
||||
use core::cell::Cell;
|
||||
|
||||
trait Thing: Sized {
|
||||
fn do_the_thing(&mut self, _s: i32) {}
|
||||
}
|
||||
impl<T> Thing for Cell<T> {}
|
||||
|
||||
fn main() {
|
||||
let mut x = Cell::new(1);
|
||||
let l = &x;
|
||||
|
||||
x.do_the_thing({
|
||||
// Several Foreign accesses (both Reads and Writes) to the location
|
||||
// being reborrowed. Reserved + unprotected + interior mut
|
||||
// makes the pointer immune to everything as long as all accesses
|
||||
// are child accesses to its parent pointer x.
|
||||
x.set(3);
|
||||
l.set(4);
|
||||
x.get() + l.get()
|
||||
});
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-tag-gc=0
|
||||
#[path = "../../utils/mod.rs"]
|
||||
mod utils;
|
||||
use utils::macros::*;
|
||||
|
||||
use std::cell::UnsafeCell;
|
||||
|
||||
// UnsafeCells use the parent tag, so it is possible to use them with
|
||||
// few restrictions when only among themselves.
|
||||
fn main() {
|
||||
unsafe {
|
||||
let data = &mut UnsafeCell::new(0u8);
|
||||
name!(data.get(), "data");
|
||||
let x = &*data;
|
||||
name!(x.get(), "x");
|
||||
let y = &*data;
|
||||
name!(y.get(), "y");
|
||||
let alloc_id = alloc_id!(data.get());
|
||||
print_state!(alloc_id);
|
||||
// y and x tolerate alternating Writes
|
||||
*y.get() = 1;
|
||||
*x.get() = 2;
|
||||
*y.get() = 3;
|
||||
*x.get() = 4;
|
||||
print_state!(alloc_id);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Re*| └────<TAG=data,x,y>
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Act| └────<TAG=data,x,y>
|
||||
──────────────────────────────────────────────────────────────────────
|
@ -0,0 +1,29 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// copy_nonoverlapping works regardless of the order in which we construct
|
||||
// the arguments.
|
||||
pub fn main() {
|
||||
test_to_from();
|
||||
test_from_to();
|
||||
}
|
||||
|
||||
fn test_to_from() {
|
||||
unsafe {
|
||||
let data = &mut [0u64, 1];
|
||||
let to = data.as_mut_ptr().add(1);
|
||||
let from = data.as_ptr();
|
||||
std::ptr::copy_nonoverlapping(from, to, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Stacked Borrows would not have liked this one because the `as_mut_ptr` reborrow
|
||||
// invalidates the earlier pointer obtained from `as_ptr`, but Tree Borrows is fine
|
||||
// with it.
|
||||
fn test_from_to() {
|
||||
unsafe {
|
||||
let data = &mut [0u64, 1];
|
||||
let from = data.as_ptr();
|
||||
let to = data.as_mut_ptr().add(1);
|
||||
std::ptr::copy_nonoverlapping(from, to, 1);
|
||||
}
|
||||
}
|
32
src/tools/miri/tests/pass/tree-borrows/end-of-protector.rs
Normal file
32
src/tools/miri/tests/pass/tree-borrows/end-of-protector.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-tag-gc=0
|
||||
|
||||
// Check that a protector goes back to normal behavior when the function
|
||||
// returns.
|
||||
#[path = "../../utils/mod.rs"]
|
||||
mod utils;
|
||||
use utils::macros::*;
|
||||
|
||||
fn main() {
|
||||
unsafe {
|
||||
let data = &mut 0u8;
|
||||
name!(data);
|
||||
let alloc_id = alloc_id!(data);
|
||||
let x = &mut *data;
|
||||
name!(x);
|
||||
print_state!(alloc_id);
|
||||
do_nothing(x); // creates then removes a Protector for a child of x
|
||||
let y = &mut *data;
|
||||
name!(y);
|
||||
print_state!(alloc_id);
|
||||
// Invalidates the previous reborrow, but its Protector has been removed.
|
||||
*y = 1;
|
||||
print_state!(alloc_id);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn do_nothing(x: &mut u8) {
|
||||
name!(x, "callee:x");
|
||||
name!(x=>1, "caller:x");
|
||||
let alloc_id = alloc_id!(x);
|
||||
print_state!(alloc_id);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Res| └─┬──<TAG=data>
|
||||
| Res| └────<TAG=x>
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Res| └─┬──<TAG=data>
|
||||
| Res| └─┬──<TAG=x>
|
||||
| Res| └─┬──<TAG=caller:x>
|
||||
| Res| └────<TAG=callee:x> Strongly protected
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Res| └─┬──<TAG=data>
|
||||
| Res| ├─┬──<TAG=x>
|
||||
| Res| │ └─┬──<TAG=caller:x>
|
||||
| Res| │ └────<TAG=callee:x>
|
||||
| Res| └────<TAG=y>
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Act| └─┬──<TAG=data>
|
||||
| Dis| ├─┬──<TAG=x>
|
||||
| Dis| │ └─┬──<TAG=caller:x>
|
||||
| Dis| │ └────<TAG=callee:x>
|
||||
| Act| └────<TAG=y>
|
||||
──────────────────────────────────────────────────────────────────────
|
73
src/tools/miri/tests/pass/tree-borrows/formatting.rs
Normal file
73
src/tools/miri/tests/pass/tree-borrows/formatting.rs
Normal file
@ -0,0 +1,73 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-tag-gc=0
|
||||
|
||||
#[path = "../../utils/mod.rs"]
|
||||
mod utils;
|
||||
use utils::macros::*;
|
||||
|
||||
// Check the formatting of the trees.
|
||||
fn main() {
|
||||
unsafe {
|
||||
alignment_check();
|
||||
structure_check();
|
||||
}
|
||||
}
|
||||
|
||||
// Alignment check: we split the array at indexes with different amounts of
|
||||
// decimal digits to verify proper padding.
|
||||
unsafe fn alignment_check() {
|
||||
let data: &mut [u8] = &mut [0; 1024];
|
||||
name!(data.as_ptr()=>2, "data");
|
||||
let alloc_id = alloc_id!(data.as_ptr());
|
||||
let x = &mut data[1];
|
||||
name!(x as *mut _, "data[1]");
|
||||
*x = 1;
|
||||
let x = &mut data[10];
|
||||
name!(x as *mut _, "data[10]");
|
||||
*x = 1;
|
||||
let x = &mut data[100];
|
||||
name!(x as *mut _, "data[100]");
|
||||
*x = 1;
|
||||
let _val = data[100]; // So that the above is Frz
|
||||
let x = &mut data[1000];
|
||||
name!(x as *mut _, "data[1000]");
|
||||
*x = 1;
|
||||
print_state!(alloc_id);
|
||||
}
|
||||
|
||||
// Tree structure check: somewhat complex organization of reborrows.
|
||||
unsafe fn structure_check() {
|
||||
let x = &0u8;
|
||||
name!(x);
|
||||
let xa = &*x;
|
||||
name!(xa);
|
||||
let xb = &*x;
|
||||
name!(xb);
|
||||
let xc = &*x;
|
||||
name!(xc);
|
||||
let xaa = &*xa;
|
||||
name!(xaa);
|
||||
let xab = &*xa;
|
||||
name!(xab);
|
||||
let xba = &*xb;
|
||||
name!(xba);
|
||||
let xbaa = &*xba;
|
||||
name!(xbaa);
|
||||
let xbaaa = &*xbaa;
|
||||
name!(xbaaa);
|
||||
let xbaaaa = &*xbaaa;
|
||||
name!(xbaaaa);
|
||||
let xca = &*xc;
|
||||
name!(xca);
|
||||
let xcb = &*xc;
|
||||
name!(xcb);
|
||||
let xcaa = &*xca;
|
||||
name!(xcaa);
|
||||
let xcab = &*xca;
|
||||
name!(xcab);
|
||||
let xcba = &*xcb;
|
||||
name!(xcba);
|
||||
let xcbb = &*xcb;
|
||||
name!(xcbb);
|
||||
let alloc_id = alloc_id!(x);
|
||||
print_state!(alloc_id);
|
||||
}
|
29
src/tools/miri/tests/pass/tree-borrows/formatting.stderr
Normal file
29
src/tools/miri/tests/pass/tree-borrows/formatting.stderr
Normal file
@ -0,0 +1,29 @@
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1.. 2.. 10.. 11..100..101..1000..1001..1024
|
||||
| Res| Act| Res| Act| Res| Act| Res| Act| Res| └─┬──<TAG=data>
|
||||
|----| Act|----|?Dis|----|?Dis| ----| ?Dis| ----| ├────<TAG=data[1]>
|
||||
|----|----|----| Act|----|?Dis| ----| ?Dis| ----| ├────<TAG=data[10]>
|
||||
|----|----|----|----|----| Frz| ----| ?Dis| ----| ├────<TAG=data[100]>
|
||||
|----|----|----|----|----|----| ----| Act| ----| └────<TAG=data[1000]>
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Frz| └─┬──<TAG=x>
|
||||
| Frz| ├─┬──<TAG=xa>
|
||||
| Frz| │ ├────<TAG=xaa>
|
||||
| Frz| │ └────<TAG=xab>
|
||||
| Frz| ├─┬──<TAG=xb>
|
||||
| Frz| │ └─┬──<TAG=xba>
|
||||
| Frz| │ └─┬──<TAG=xbaa>
|
||||
| Frz| │ └─┬──<TAG=xbaaa>
|
||||
| Frz| │ └────<TAG=xbaaaa>
|
||||
| Frz| └─┬──<TAG=xc>
|
||||
| Frz| ├─┬──<TAG=xca>
|
||||
| Frz| │ ├────<TAG=xcaa>
|
||||
| Frz| │ └────<TAG=xcab>
|
||||
| Frz| └─┬──<TAG=xcb>
|
||||
| Frz| ├────<TAG=xcba>
|
||||
| Frz| └────<TAG=xcbb>
|
||||
──────────────────────────────────────────────────────────────────────
|
14
src/tools/miri/tests/pass/tree-borrows/read-only-from-mut.rs
Normal file
14
src/tools/miri/tests/pass/tree-borrows/read-only-from-mut.rs
Normal file
@ -0,0 +1,14 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows
|
||||
|
||||
// Tree Borrows has no issue with several mutable references existing
|
||||
// at the same time, as long as they are used only immutably.
|
||||
// I.e. multiple Reserved can coexist.
|
||||
pub fn main() {
|
||||
unsafe {
|
||||
let base = &mut 42u64;
|
||||
let r1 = &mut *(base as *mut u64);
|
||||
let r2 = &mut *(base as *mut u64);
|
||||
let _l = *r1;
|
||||
let _l = *r2;
|
||||
}
|
||||
}
|
24
src/tools/miri/tests/pass/tree-borrows/reborrow-is-read.rs
Normal file
24
src/tools/miri/tests/pass/tree-borrows/reborrow-is-read.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//@compile-flags: -Zmiri-tree-borrows -Zmiri-tag-gc=0
|
||||
|
||||
#[path = "../../utils/mod.rs"]
|
||||
mod utils;
|
||||
use utils::macros::*;
|
||||
|
||||
// To check that a reborrow is counted as a Read access, we use a reborrow
|
||||
// with no additional Read to Freeze an Active pointer.
|
||||
|
||||
fn main() {
|
||||
unsafe {
|
||||
let parent = &mut 0u8;
|
||||
name!(parent);
|
||||
let alloc_id = alloc_id!(parent);
|
||||
let x = &mut *parent;
|
||||
name!(x);
|
||||
*x = 0; // x is now Active
|
||||
print_state!(alloc_id);
|
||||
let y = &mut *parent;
|
||||
name!(y);
|
||||
// Check in the debug output that x has been Frozen by the reborrow
|
||||
print_state!(alloc_id);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Act| └─┬──<TAG=parent>
|
||||
| Act| └────<TAG=x>
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Warning: this tree is indicative only. Some tags may have been hidden.
|
||||
0.. 1
|
||||
| Act| └─┬──<TAG=parent>
|
||||
| Frz| ├────<TAG=x>
|
||||
| Res| └────<TAG=y>
|
||||
──────────────────────────────────────────────────────────────────────
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user