Skip to content

Commit b2ec4b9

Browse files
committed
Fix verification of tracked struct from high-durability query
1 parent 994c988 commit b2ec4b9

File tree

3 files changed

+200
-6
lines changed

3 files changed

+200
-6
lines changed

src/tracked_struct/struct_map.rs

+66-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
use crossbeam::queue::SegQueue;
77
use dashmap::mapref::one::RefMut;
88

9-
use crate::{alloc::Alloc, hash::FxDashMap, Id, Revision};
9+
use crate::{alloc::Alloc, hash::FxDashMap, zalsa::Zalsa, Id, Revision};
1010

1111
use super::{Configuration, KeyStruct, Value};
1212

@@ -100,14 +100,22 @@ where
100100
}
101101

102102
pub fn validate(&self, current_revision: Revision, id: Id) {
103-
let mut data = self.map.get_mut(&id).unwrap();
103+
Self::validate_in_map(&self.map, current_revision, id)
104+
}
105+
106+
pub fn validate_in_map(
107+
map: &FxDashMap<Id, Alloc<Value<C>>>,
108+
current_revision: Revision,
109+
id: Id,
110+
) {
111+
let mut data = map.get_mut(&id).unwrap();
104112

105113
// UNSAFE: We never permit `&`-access in the current revision until data.created_at
106114
// has been updated to the current revision (which we check below).
107115
let data = unsafe { data.as_mut() };
108116

109117
// Never update a struct twice in the same revision.
110-
assert!(data.created_at < current_revision);
118+
assert!(data.created_at <= current_revision);
111119
data.created_at = current_revision;
112120
}
113121

@@ -185,7 +193,7 @@ where
185193
let data_ref: &Value<C> = unsafe { data.as_ref() };
186194

187195
// Before we drop the lock, check that the value has
188-
// been updated in this revision. This is what allows us to return a ``
196+
// been updated in this revision. This is what allows us to return a Struct
189197
let created_at = data_ref.created_at;
190198
assert!(
191199
created_at == current_revision,
@@ -200,6 +208,56 @@ where
200208
unsafe { C::struct_from_raw(data.as_raw()) }
201209
}
202210

211+
/// Lookup an existing tracked struct from the map, maybe validating it to current revision.
212+
///
213+
/// Validates to current revision if the struct was last created/validated in a revision that
214+
/// is still current for the struct's durability. That is, if the struct is HIGH durability
215+
/// (created by a HIGH durability query) and was created in R2, and we are now at R3 but no
216+
/// HIGH durability input has changed since R2, the struct is still valid and we can validate
217+
/// it to R3.
218+
///
219+
/// # Panics
220+
///
221+
/// * If the value is not present in the map.
222+
/// * If the value has not been updated in the last-changed revision for its durability.
223+
fn get_and_validate_last_changed<'db>(
224+
map: &'db FxDashMap<Id, Alloc<Value<C>>>,
225+
zalsa: &Zalsa,
226+
id: Id,
227+
) -> C::Struct<'db> {
228+
let data = map.get(&id).unwrap();
229+
230+
// UNSAFE: We permit `&`-access in the current revision once data.created_at
231+
// has been updated to the current revision (which we ensure below).
232+
let data_ref: &Value<C> = unsafe { data.as_ref() };
233+
234+
// Before we drop the lock, check that the value has been updated in the most recent
235+
// version in which the query that created it could have changed (based on durability).
236+
let created_at = data_ref.created_at;
237+
let last_changed = zalsa.last_changed_revision(data_ref.durability);
238+
assert!(
239+
created_at >= last_changed,
240+
"access to tracked struct from obsolete revision"
241+
);
242+
243+
// Unsafety clause:
244+
//
245+
// * Value will not be updated again in this revision,
246+
// and revision will not change so long as runtime is shared
247+
// * We only remove values from the map when we have `&mut self`
248+
let ret = unsafe { C::struct_from_raw(data.as_raw()) };
249+
250+
drop(data);
251+
252+
// Validate in current revision, if necessary.
253+
let current_revision = zalsa.current_revision();
254+
if last_changed < current_revision {
255+
Self::validate_in_map(map, current_revision, id);
256+
}
257+
258+
ret
259+
}
260+
203261
/// Remove the entry for `id` from the map.
204262
///
205263
/// NB. the data won't actually be freed until `drop_deleted_entries` is called.
@@ -233,6 +291,10 @@ where
233291
pub fn get(&self, current_revision: Revision, id: Id) -> C::Struct<'_> {
234292
StructMap::get_from_map(&self.map, current_revision, id)
235293
}
294+
295+
pub fn get_and_validate_last_changed<'db>(&'db self, zalsa: &Zalsa, id: Id) -> C::Struct<'db> {
296+
StructMap::get_and_validate_last_changed(&self.map, zalsa, id)
297+
}
236298
}
237299

238300
/// A mutable reference to the data for a single struct.

src/tracked_struct/tracked_field.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ where
8383
input: Option<Id>,
8484
revision: crate::Revision,
8585
) -> bool {
86-
let current_revision = db.zalsa().current_revision();
8786
let id = input.unwrap();
88-
let data = self.struct_map.get(current_revision, id);
87+
let data = self
88+
.struct_map
89+
.get_and_validate_last_changed(db.zalsa(), id);
8990
let data = C::deref_struct(data);
9091
let field_changed_at = data.revisions[self.field_index];
9192
field_changed_at > revision
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/// Test that high durabilities can't cause "access tracked struct from previous revision" panic.
2+
///
3+
/// The test models a situation where we have two File inputs (0, 1), where `File(0)` has LOW
4+
/// durability and `File(1)` has HIGH durability. We can query an `index` for each file, and a
5+
/// `definitions` from that index (just a sub-part of the index), and we can `infer` each file. The
6+
/// `index` and `definitions` queries depend only on the `File` they operate on, but the `infer`
7+
/// query has some other dependencies: `infer(0)` depends on `infer(1)`, and `infer(1)` also
8+
/// depends directly on `File(0)`.
9+
///
10+
/// The panic occurs (in versions of Salsa without a fix) because `definitions(1)` is high
11+
/// durability, and depends on `index(1)` which is also high durability. `index(1)` creates the
12+
/// tracked struct `Definition(1)`, and `infer(1)` (which is low durability) depends on
13+
/// `Definition.file(1)`.
14+
///
15+
/// After a change to `File(0)` (low durability), we only shallowly verify `definitions(1)` -- it
16+
/// passes shallow verification due to durability. We take care to mark-validated the outputs of
17+
/// `definitions(1)`, but we never verify `index(1)` at all (deeply or shallowly), which means we
18+
/// never mark `Definition(1)` validated. So when we deep-verify `infer(1)`, we try to access its
19+
/// dependency `Definition.file(1)`, and hit the panic because we are accessing a tracked struct
20+
/// that has never been re-validated or re-recreated in R2.
21+
use salsa::{Durability, Setter};
22+
23+
#[salsa::db]
24+
trait Db: salsa::Database {
25+
fn file(&self, idx: usize) -> File;
26+
}
27+
28+
#[salsa::input]
29+
struct File {
30+
field: usize,
31+
}
32+
33+
#[salsa::tracked]
34+
struct Definition<'db> {
35+
file: File,
36+
}
37+
38+
#[salsa::tracked]
39+
struct Index<'db> {
40+
definitions: Definitions<'db>,
41+
}
42+
43+
#[salsa::tracked]
44+
struct Definitions<'db> {
45+
definition: Definition<'db>,
46+
}
47+
48+
#[salsa::tracked]
49+
struct Inference<'db> {
50+
definition: Definition<'db>,
51+
}
52+
53+
#[salsa::tracked]
54+
fn index<'db>(db: &'db dyn Db, file: File) -> Index<'db> {
55+
let _ = file.field(db);
56+
Index::new(db, Definitions::new(db, Definition::new(db, file)))
57+
}
58+
59+
#[salsa::tracked]
60+
fn definitions<'db>(db: &'db dyn Db, file: File) -> Definitions<'db> {
61+
index(db, file).definitions(db)
62+
}
63+
64+
#[salsa::tracked]
65+
fn infer<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Inference<'db> {
66+
let file = definition.file(db);
67+
if file.field(db) < 1 {
68+
let dependent_file = db.file(1);
69+
infer(db, definitions(db, dependent_file).definition(db))
70+
} else {
71+
db.file(0).field(db);
72+
index(db, file);
73+
Inference::new(db, definition)
74+
}
75+
}
76+
77+
#[salsa::tracked]
78+
fn check<'db>(db: &'db dyn Db, file: File) -> Inference<'db> {
79+
let defs = definitions(db, file);
80+
infer(db, defs.definition(db))
81+
}
82+
83+
#[test]
84+
fn execute() {
85+
#[salsa::db]
86+
#[derive(Default)]
87+
struct Database {
88+
storage: salsa::Storage<Self>,
89+
files: Vec<File>,
90+
}
91+
92+
#[salsa::db]
93+
impl salsa::Database for Database {
94+
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
95+
}
96+
97+
#[salsa::db]
98+
impl Db for Database {
99+
fn file(&self, idx: usize) -> File {
100+
self.files[idx]
101+
}
102+
}
103+
104+
let mut db = Database::default();
105+
// Create a file 0 with low durability, and a file 1 with high durability.
106+
107+
let file0 = File::new(&db, 0);
108+
db.files.push(file0);
109+
110+
let file1 = File::new(&db, 1);
111+
file1
112+
.set_field(&mut db)
113+
.with_durability(Durability::HIGH)
114+
.to(1);
115+
db.files.push(file1);
116+
117+
// check(0) -> infer(0) -> definitions(0) -> index(0)
118+
// \-> infer(1) -> definitions(1) -> index(1)
119+
120+
assert_eq!(check(&db, file0).definition(&db).file(&db).field(&db), 1);
121+
122+
// update the low durability file 0
123+
file0.set_field(&mut db).to(0);
124+
125+
// Re-query check(0). definitions(1) is high durability so it short-circuits in shallow-verify,
126+
// meaning we never verify index(1) at all, but index(1) created the tracked struct
127+
// Definition(1), so we never validate Definition(1) in R2, so when we try to verify
128+
// Definition.file(1) (as an input of infer(1) ) we hit a panic for trying to use a struct that
129+
// isn't validated in R2.
130+
check(&db, file0);
131+
}

0 commit comments

Comments
 (0)