Skip to content

Commit 8114e2c

Browse files
committed
Fix verification of tracked struct from high-durability query
1 parent d1b770f commit 8114e2c

File tree

3 files changed

+173
-4
lines changed

3 files changed

+173
-4
lines changed

src/tracked_struct/struct_map.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,15 @@ where
100100
}
101101

102102
pub fn validate<'db>(&'db self, runtime: &'db Runtime, id: Id) {
103-
let mut data = self.map.get_mut(&id).unwrap();
103+
Self::validate_in_map(&self.map, runtime, id)
104+
}
105+
106+
pub fn validate_in_map<'db>(
107+
map: &'db FxDashMap<Id, Alloc<Value<C>>>,
108+
runtime: &'db Runtime,
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).
@@ -186,7 +194,7 @@ where
186194
let data_ref: &Value<C> = unsafe { data.as_ref() };
187195

188196
// Before we drop the lock, check that the value has
189-
// been updated in this revision. This is what allows us to return a ``
197+
// been updated in this revision. This is what allows us to return a Struct
190198
let created_at = data_ref.created_at;
191199
assert!(
192200
created_at == current_revision,
@@ -201,6 +209,50 @@ where
201209
unsafe { C::struct_from_raw(data.as_raw()) }
202210
}
203211

212+
fn get_and_validate_last_changed<'db>(
213+
map: &'db FxDashMap<Id, Alloc<Value<C>>>,
214+
runtime: &'db Runtime,
215+
id: Id,
216+
) -> C::Struct<'db> {
217+
let data = map.get(&id).unwrap();
218+
219+
// UNSAFE: We permit `&`-access in the current revision once data.created_at
220+
// has been updated to the current revision (which we check below).
221+
let data_ref: &Value<C> = unsafe { data.as_ref() };
222+
223+
// Before we drop the lock, check that the value has
224+
// been updated in the most recent version in which the query that created it could have
225+
// changed (based on durability), and validate to the current revision if so.
226+
let created_at = data_ref.created_at;
227+
let last_changed = runtime.last_changed_revision(data_ref.durability);
228+
dbg!(
229+
runtime.current_revision(),
230+
created_at,
231+
last_changed,
232+
data_ref.durability
233+
);
234+
assert!(
235+
created_at >= last_changed,
236+
"access to tracked struct from obsolete revision"
237+
);
238+
239+
// Unsafety clause:
240+
//
241+
// * Value will not be updated again in this revision,
242+
// and revision will not change so long as runtime is shared
243+
// * We only remove values from the map when we have `&mut self`
244+
let ret = unsafe { C::struct_from_raw(data.as_raw()) };
245+
246+
drop(data);
247+
248+
// Validate in current revision, if necessary.
249+
if last_changed < runtime.current_revision() {
250+
Self::validate_in_map(map, runtime, id);
251+
}
252+
253+
ret
254+
}
255+
204256
/// Remove the entry for `id` from the map.
205257
///
206258
/// NB. the data won't actually be freed until `drop_deleted_entries` is called.
@@ -234,6 +286,14 @@ where
234286
pub fn get<'db>(&'db self, current_revision: Revision, id: Id) -> C::Struct<'db> {
235287
StructMap::get_from_map(&self.map, current_revision, id)
236288
}
289+
290+
pub fn get_and_validate_last_changed<'db>(
291+
&'db self,
292+
runtime: &'db Runtime,
293+
id: Id,
294+
) -> C::Struct<'db> {
295+
StructMap::get_and_validate_last_changed(&self.map, runtime, id)
296+
}
237297
}
238298

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

src/tracked_struct/tracked_field.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ where
8585
input: Option<Id>,
8686
revision: crate::Revision,
8787
) -> bool {
88-
let current_revision = db.runtime().current_revision();
8988
let id = input.unwrap();
90-
let data = self.struct_map.get(current_revision, id);
89+
let data = self
90+
.struct_map
91+
.get_and_validate_last_changed(db.runtime(), id);
9192
let data = C::deref_struct(data);
9293
let field_changed_at = data.revisions[self.field_index];
9394
field_changed_at > revision
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use salsa::{Durability, Setter};
2+
3+
#[salsa::db]
4+
trait Db: salsa::Database {
5+
fn file(&self, idx: usize) -> File;
6+
}
7+
8+
#[salsa::input]
9+
struct File {
10+
field: usize,
11+
}
12+
13+
#[salsa::tracked]
14+
struct Definition<'db> {
15+
file: File,
16+
}
17+
18+
#[salsa::tracked]
19+
struct Index<'db> {
20+
definitions: Definitions<'db>,
21+
}
22+
23+
#[salsa::tracked]
24+
struct Definitions<'db> {
25+
definition: Definition<'db>,
26+
}
27+
28+
#[salsa::tracked]
29+
struct Inference<'db> {
30+
definition: Definition<'db>,
31+
}
32+
33+
#[salsa::tracked]
34+
fn index<'db>(db: &'db dyn Db, file: File) -> Index<'db> {
35+
let _ = file.field(db);
36+
Index::new(db, Definitions::new(db, Definition::new(db, file)))
37+
}
38+
39+
#[salsa::tracked]
40+
fn definitions<'db>(db: &'db dyn Db, file: File) -> Definitions<'db> {
41+
index(db, file).definitions(db)
42+
}
43+
44+
#[salsa::tracked]
45+
fn infer<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Inference<'db> {
46+
let file = definition.file(db);
47+
if file.field(db) < 1 {
48+
let dependent_file = db.file(1);
49+
infer(db, definitions(db, dependent_file).definition(db))
50+
} else {
51+
db.file(0).field(db);
52+
Inference::new(db, definition)
53+
}
54+
}
55+
56+
#[salsa::tracked]
57+
fn check<'db>(db: &'db dyn Db, file: File) -> Inference<'db> {
58+
let defs = definitions(db, file);
59+
infer(db, defs.definition(db))
60+
}
61+
62+
#[test]
63+
fn execute() {
64+
#[salsa::db]
65+
#[derive(Default)]
66+
struct Database {
67+
storage: salsa::Storage<Self>,
68+
files: Vec<File>,
69+
}
70+
71+
#[salsa::db]
72+
impl salsa::Database for Database {}
73+
74+
#[salsa::db]
75+
impl Db for Database {
76+
fn file(&self, idx: usize) -> File {
77+
self.files[idx]
78+
}
79+
}
80+
81+
let mut db = Database::default();
82+
// Create a file 0 with low durability, and a file 1 with high durability.
83+
84+
let file0 = File::new(&db, 0);
85+
db.files.push(file0);
86+
87+
let file1 = File::new(&db, 1);
88+
file1
89+
.set_field(&mut db)
90+
.with_durability(Durability::HIGH)
91+
.to(1);
92+
db.files.push(file1);
93+
94+
// check(0) -> infer(0) -> definitions(0) -> index(0)
95+
// \-> infer(1) -> definitions(1) -> index(1)
96+
97+
assert_eq!(check(&db, file0).definition(&db).file(&db).field(&db), 1);
98+
99+
// update the low durability file 0
100+
file0.set_field(&mut db).to(0);
101+
102+
// Re-query check(0). definitions(1) is high durability so it short-circuits in shallow-verify,
103+
// meaning we never verify index(1) at all, but index(1) created the tracked struct
104+
// Definition(1), so we never validate Definition(1) in R2, so when we try to verify
105+
// Definition.file(1) (as an input of infer(1) ) we hit a panic for trying to use a struct that
106+
// isn't validated in R2.
107+
check(&db, file0);
108+
}

0 commit comments

Comments
 (0)