1
1
//! This module provides a framework on top of the normal MIR dataflow framework to simplify the
2
- //! implementation of analyses that track the values stored in places of interest.
2
+ //! implementation of analyses that track information about the values stored in certain places.
3
+ //! We are using the term "place" here to refer to a `mir::Place` (a place expression) instead of
4
+ //! an `interpret::Place` (a memory location).
3
5
//!
4
6
//! The default methods of [`ValueAnalysis`] (prefixed with `super_` instead of `handle_`)
5
7
//! provide some behavior that should be valid for all abstract domains that are based only on the
6
8
//! value stored in a certain place. On top of these default rules, an implementation should
7
9
//! override some of the `handle_` methods. For an example, see `ConstAnalysis`.
8
10
//!
9
- //! An implementation must also provide a [`Map`]. Before the anaylsis begins, all places that
10
- //! should be tracked during the analysis must be registered. Currently, the projections of these
11
- //! places may only contain derefs, fields and downcasts (otherwise registration fails). During the
12
- //! analysis, no new places can be registered .
11
+ //! An implementation must also provide a [`Map`]. Before the analysis begins, all places that
12
+ //! should be tracked during the analysis must be registered. During the analysis, no new places
13
+ //! can be registered. The [`State`] can be queried to retrieve the abstract value stored for a
14
+ //! certain place by passing the map .
13
15
//!
14
- //! Note that if you want to track values behind references, you have to register the dereferenced
15
- //! place. For example: Assume `let x = (0, 0)` and that we want to propagate values from `x.0` and
16
- //! `x.1` also through the assignment `let y = &x`. In this case, we should register `x.0`, `x.1`,
17
- //! `(*y).0` and `(*y).1`.
16
+ //! This framework is currently experimental. In particular, the features related to references are
17
+ //! currently guarded behind `-Zunsound-mir-opts`, because their correctness relies on Stacked
18
+ //! Borrows. Also, only places with scalar types can be tracked currently. This is because scalar
19
+ //! types are indivisible, which simplifies the current implementation. But this limitation could be
20
+ //! lifted in the future.
18
21
//!
19
22
//!
20
- //! # Correctness
23
+ //! # Notes
21
24
//!
22
- //! Warning: This is a semi-formal attempt to argue for the correctness of this analysis. If you
23
- //! find any weak spots, let me know! Recommended reading: Abstract Interpretation. We will use the
24
- //! term "place" to refer to a place expression (like `mir::Place`), and we will call the
25
- //! underlying entity "object". For instance, `*_1` and `*_2` are not the same place, but depending
26
- //! on the value of `_1` and `_2`, they could refer to the same object. Also, the same place can
27
- //! refer to different objects during execution. If `_1` is reassigned, then `*_1` may refer to
28
- //! different objects before and after assignment. Additionally, when saying "access to a place",
29
- //! what we really mean is "access to an object denoted by arbitrary projections of that place".
25
+ //! - The bottom state denotes uninitialized memory. Because we are only doing a sound approximation
26
+ //! of the actual execution, we can also use this state for places where access would be UB.
30
27
//!
31
- //! In the following, we will assume a constant propagation analysis. Our analysis is correct if
32
- //! every transfer function is correct. This is the case if for every pair (f, f#) and abstract
33
- //! state s, we have f(y(s)) <= y(f#(s)), where s is a mapping from tracked place to top, bottom or
34
- //! a constant. Since pointers (and mutable references) are not tracked, but can be used to change
35
- //! values in the concrete domain, f# must assume that all places that can be affected in this way
36
- //! for a given program point are already marked with top in s (otherwise many assignments and
37
- //! function calls would have no choice but to mark all tracked places with top). This leads us to
38
- //! an invariant: For all possible program points where there could possibly exist means of mutable
39
- //! access to a tracked place (in the concrete domain), this place must be assigned to top (in the
40
- //! abstract domain). The concretization function y can be defined as expected for the constant
41
- //! propagation analysis, although the concrete state of course contains all kinds of non-tracked
42
- //! data. However, by the invariant above, no mutable access to tracked places that are not marked
43
- //! with top may be introduced.
28
+ //! - The assignment logic in `State::assign_place_idx` assumes that the places are non-overlapping,
29
+ //! or identical. Note that this refers to place expressions, not memory locations.
44
30
//!
45
- //! Note that we (at least currently) do not differentiate between "this place may assume different
46
- //! values" and "a pointer to this place escaped the analysis". However, we still want to handle
47
- //! assignments to constants as usual for f#. This adds an assumption: Whenever we have an
48
- //! assignment that is captured by the analysis, all mutable access to the underlying place (which
49
- //! is not observable by the analysis) must be invalidated. This is (hopefully) covered by Stacked
50
- //! Borrows.
31
+ //! - Since pointers (and mutable references) are not tracked, but can be used to change the
32
+ //! underlying values, we are conservative and immediately flood the referenced place upon creation
33
+ //! of the pointer. Also, we have to uphold the invariant that the place must stay that way as long
34
+ //! as this mutable access could exist. However...
51
35
//!
52
- //! To be continued...
36
+ //! - Without an aliasing model like Stacked Borrows (i.e., `-Zunsound-mir-opts` is not given),
37
+ //! such mutable access is never revoked. And even shared references could be used to obtain the
38
+ //! address of a value an modify it. When not assuming Stacked Borrows, we prevent such places from
39
+ //! being tracked at all. This means that the analysis itself can assume that writes to a *tracked*
40
+ //! place always invalidate all other means of mutable access, regardless of the aliasing model.
53
41
//!
54
- //! The bottom state denotes uninitalized memory.
55
- //!
56
- //!
57
- //! # Assumptions
58
- //!
59
- //! - (A1) Assignment to any tracked place invalidates all pointers that could be used to change
60
- //! the underlying value.
61
- //! - (A2) `StorageLive`, `StorageDead` and `Deinit` make the underlying memory at least
62
- //! uninitialized (at least in the sense that declaring access UB is also fine).
63
- //! - (A3) An assignment with `State::assign_place_idx` either involves non-overlapping places, or
64
- //! the places are the same.
65
- //! - (A4) If the value behind a reference to a `Freeze` place is changed, dereferencing the
66
- //! reference is UB.
42
+ //! - Likewise, the analysis itself assumes that if the value of a *tracked* place behind a shared
43
+ //! reference is changed, the reference may not be used to access that value anymore. This is true
44
+ //! for all places if the referenced type is `Freeze` and we assume Stacked Borrows. If we are not
45
+ //! assuming Stacking Borrows (or if the referenced type could be `!Freeze`), we again prevent such
46
+ //! places from being tracked at all, making this assertion trivially true.
67
47
68
48
use std:: fmt:: { Debug , Formatter } ;
69
49
@@ -107,11 +87,12 @@ pub trait ValueAnalysis<'tcx> {
107
87
self . handle_intrinsic ( intrinsic, state) ;
108
88
}
109
89
StatementKind :: StorageLive ( local) | StatementKind :: StorageDead ( local) => {
110
- // (A2)
90
+ // StorageLive leaves the local in an uninitialized state.
91
+ // StorageDead makes it UB to access the local afterwards.
111
92
state. flood_with ( Place :: from ( * local) . as_ref ( ) , self . map ( ) , Self :: Value :: bottom ( ) ) ;
112
93
}
113
94
StatementKind :: Deinit ( box place) => {
114
- // (A2)
95
+ // Deinit makes the place uninitialized.
115
96
state. flood_with ( place. as_ref ( ) , self . map ( ) , Self :: Value :: bottom ( ) ) ;
116
97
}
117
98
StatementKind :: Retag ( ..) => {
@@ -477,9 +458,9 @@ impl<V: Clone + HasTop + HasBottom> State<V> {
477
458
/// Copies `source` to `target`, including all tracked places beneath.
478
459
///
479
460
/// If `target` contains a place that is not contained in `source`, it will be overwritten with
480
- /// Top. Also, because this will copy all entries one after another, it may only be
461
+ /// Top. Also, because this will copy all entries one after another, it may only be used for
462
+ /// places that are non-overlapping or identical.
481
463
pub fn assign_place_idx ( & mut self , target : PlaceIndex , source : PlaceIndex , map : & Map ) {
482
- // We use (A3) and copy all entries one after another.
483
464
let StateData :: Reachable ( values) = & mut self . 0 else { return } ;
484
465
485
466
// If both places are tracked, we copy the value to the target. If the target is tracked,
@@ -528,21 +509,24 @@ impl<V: Clone + HasTop + HasBottom> State<V> {
528
509
if let Some ( value_index) = map. places [ target] . value_index {
529
510
values[ value_index] = V :: top ( ) ;
530
511
}
531
- // Instead of tracking of *where* a reference points to (as in, which place), we
532
- // track *what* it points to (as in, what do we know about the target). For an
533
- // assignment `x = &y`, we thus copy the info we have for `y` to `*x`. This is
534
- // sound because we only track places that are `Freeze`, and (A4).
512
+ // Instead of tracking of *where* a reference points to (as in, which memory
513
+ // location), we track *what* it points to (as in, what do we know about the
514
+ // target). For an assignment `x = &y`, we thus copy the info of `y` to `*x`.
535
515
if let Some ( target_deref) = map. apply ( target, TrackElem :: Deref ) {
516
+ // We know here that `*x` is `Freeze`, because we only track through
517
+ // dereferences if the target type is `Freeze`.
536
518
self . assign_place_idx ( target_deref, source, map) ;
537
519
}
538
520
}
539
521
}
540
522
}
541
523
524
+ /// Retrieve the value stored for a place, or ⊥ if it is not tracked.
542
525
pub fn get ( & self , place : PlaceRef < ' _ > , map : & Map ) -> V {
543
526
map. find ( place) . map ( |place| self . get_idx ( place, map) ) . unwrap_or ( V :: top ( ) )
544
527
}
545
528
529
+ /// Retrieve the value stored for a place index, or ⊥ if it is not tracked.
546
530
pub fn get_idx ( & self , place : PlaceIndex , map : & Map ) -> V {
547
531
match & self . 0 {
548
532
StateData :: Reachable ( values) => {
@@ -569,11 +553,11 @@ impl<V: JoinSemiLattice + Clone> JoinSemiLattice for State<V> {
569
553
}
570
554
}
571
555
572
- /// A partial mapping from `Place` to `PlaceIndex`.
556
+ /// A partial mapping from `Place` to `PlaceIndex`, where some place indices have value indices .
573
557
///
574
- /// Some additioanl bookkeeping is done to speed up traversal:
558
+ /// Some additional bookkeeping is done to speed up traversal:
575
559
/// - For iteration, every [`PlaceInfo`] contains an intrusive linked list of its children.
576
- /// - To directly get the child for a specific projection, there is `projections` map.
560
+ /// - To directly get the child for a specific projection, there is a `projections` map.
577
561
#[ derive( Debug ) ]
578
562
pub struct Map {
579
563
locals : IndexVec < Local , Option < PlaceIndex > > ,
@@ -595,7 +579,7 @@ impl Map {
595
579
/// Returns a map that only tracks places whose type passes the filter.
596
580
///
597
581
/// This is currently the only way to create a [`Map`]. The way in which the tracked places are
598
- /// chosen is an implementation detail an may not be relied upon.
582
+ /// chosen is an implementation detail and may not be relied upon.
599
583
pub fn from_filter < ' tcx > (
600
584
tcx : TyCtxt < ' tcx > ,
601
585
body : & Body < ' tcx > ,
@@ -609,7 +593,7 @@ impl Map {
609
593
// not yet guaranteed).
610
594
if tcx. sess . opts . unstable_opts . unsound_mir_opts {
611
595
// We might want to add additional limitations. If a struct has 10 boxed fields of
612
- // itself, will currently be `10.pow(max_derefs)` tracked places.
596
+ // itself, there will currently be `10.pow(max_derefs)` tracked places.
613
597
map. register_with_filter ( tcx, body, 2 , filter, & [ ] ) ;
614
598
} else {
615
599
map. register_with_filter ( tcx, body, 0 , filter, & escaped_places ( body) ) ;
@@ -668,7 +652,7 @@ impl Map {
668
652
669
653
if max_derefs > 0 {
670
654
if let Some ( ty:: TypeAndMut { ty : deref_ty, .. } ) = ty. builtin_deref ( false ) {
671
- // References can only be tracked if the target is `! Freeze`.
655
+ // Values behind references can only be tracked if the target is `Freeze`.
672
656
if deref_ty. is_freeze ( tcx. at ( DUMMY_SP ) , param_env) {
673
657
projection. push ( PlaceElem :: Deref ) ;
674
658
self . register_with_filter_rec (
@@ -953,6 +937,8 @@ fn iter_fields<'tcx>(
953
937
}
954
938
955
939
/// Returns all places, that have their reference or address taken.
940
+ ///
941
+ /// This includes shared references.
956
942
fn escaped_places < ' tcx > ( body : & Body < ' tcx > ) -> Vec < Place < ' tcx > > {
957
943
struct Collector < ' tcx > {
958
944
result : Vec < Place < ' tcx > > ,
0 commit comments