From b7ec2adb82a083f83b43364da0f6ed5fd458e53f Mon Sep 17 00:00:00 2001 From: Raul Estrada Date: Sun, 6 Sep 2026 03:46:05 -0600 Subject: [PATCH 1/2] feat(fuzzer): generate device static operands Signed-off-by: Raul Estrada --- .../fuzzer/rustlantis/config.toml.example | 1 + .../fuzzer/rustlantis/config/src/lib.rs | 16 + .../rustlantis/generate/src/generation/mod.rs | 184 +++- .../fuzzer/rustlantis/generate/src/mem/mod.rs | 83 ++ .../fuzzer/rustlantis/generate/src/pgraph.rs | 872 +++++++++++++++++- .../rustlantis/generate/src/place_select.rs | 111 ++- .../fuzzer/rustlantis/mir/src/serialize.rs | 54 +- .../fuzzer/rustlantis/mir/src/syntax.rs | 26 +- .../crates/fuzzer/tools/mir_generator.py | 50 +- 9 files changed, 1347 insertions(+), 50 deletions(-) diff --git a/cuda-oxide/crates/fuzzer/rustlantis/config.toml.example b/cuda-oxide/crates/fuzzer/rustlantis/config.toml.example index 116af103ab..2e45326c29 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/config.toml.example +++ b/cuda-oxide/crates/fuzzer/rustlantis/config.toml.example @@ -5,6 +5,7 @@ max_bb_count_hard = 100 max_fn_count = 20 max_args_count = 12 var_dump_chance = 0.5 +static_count = 0 tuple_max_len = 4 array_max_len = 8 struct_max_fields = 8 diff --git a/cuda-oxide/crates/fuzzer/rustlantis/config/src/lib.rs b/cuda-oxide/crates/fuzzer/rustlantis/config/src/lib.rs index 030ef3c5ea..5f0ab5692a 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/config/src/lib.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/config/src/lib.rs @@ -81,6 +81,12 @@ pub struct GenerationConfig { /// Expected proportion of variables to be dumped #[serde(default = "var_dump_chance")] pub var_dump_chance: f32, + + /// Number of scalar static allocations to make available to the generator. + /// + /// The default is zero so enabling static generation does not perturb existing seeds. + #[serde(default = "static_count")] + pub static_count: usize, } fn bb_max_len() -> usize { @@ -111,6 +117,10 @@ fn var_dump_chance() -> f32 { 0.5 } +fn static_count() -> usize { + 0 +} + #[derive(Deserialize, Clone)] pub struct TyConfig { /// Max. arity of tuple @@ -178,3 +188,9 @@ impl Default for TyConfig { fn example_parses() { let _ = load("../config.toml.example"); } + +#[test] +fn static_count_defaults_to_zero() { + let config: Config = toml::from_str("[backends]\n").unwrap(); + assert_eq!(config.generation.static_count, 0); +} diff --git a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/generation/mod.rs b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/generation/mod.rs index e0f01d6209..0a90197723 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/generation/mod.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/generation/mod.rs @@ -10,7 +10,7 @@ use mir::serialize::Serialize; use mir::syntax::{ AggregateKind, BasicBlock, BasicBlockData, BinOp, Body, Callee, Function, IntTy, Literal, Local, LocalDecls, Mutability, Operand, Place, Program, ProjectionElem, Rvalue, Statement, - SwitchTargets, Terminator, TyId, TyKind, UnOp, VariantIdx, + Static, StaticDecl, SwitchTargets, Terminator, TyId, TyKind, UnOp, VariantIdx, }; use mir::tyctxt::TyCtxt; use rand::{Rng, RngCore, SeedableRng, seq::IteratorRandom}; @@ -111,6 +111,31 @@ impl GenerationCtx { } }) } + + fn choose_static_operand(&self, ty: TyId) -> Result { + let candidates = self + .program + .statics + .iter_enumerated() + .filter(|(id, _)| self.pt.can_acquire_static(*id)) + .filter_map(|(id, decl)| match ty.kind(&self.tcx) { + TyKind::Ref(pointee, Mutability::Not) + if decl.mutability == Mutability::Not && decl.ty == *pointee => + { + Some(Operand::Static(id, ty)) + } + TyKind::RawPtr(pointee, Mutability::Mut) + if decl.mutability == Mutability::Mut && decl.ty == *pointee => + { + Some(Operand::StaticMut(id, ty)) + } + _ => None, + }); + + candidates + .choose(&mut *self.rng.borrow_mut()) + .ok_or(SelectionError::Exhausted) + } } // Rvalue @@ -126,7 +151,18 @@ impl GenerationCtx { lhs.serialize_value(&self.tcx), lhs.ty(self.current_decls(), &self.tcx).serialize(&self.tcx) ); - let operand = self.choose_operand(&[lhs.ty(self.current_decls(), &self.tcx)], lhs)?; + let ty = lhs.ty(self.current_decls(), &self.tcx); + let operand = if self.program.statics.is_empty() { + self.choose_operand(&[ty], lhs)? + } else { + self.make_choice([false, true].into_iter(), |use_static| { + if use_static { + self.choose_static_operand(ty) + } else { + self.choose_operand(&[ty], lhs) + } + })? + }; Ok(Rvalue::Use(operand)) } @@ -348,6 +384,9 @@ impl GenerationCtx { let mut selector = PlaceSelector::for_pointee(self.tcx.clone(), false) .of_ty(*source_ty) .except(lhs); + if !self.program.statics.is_empty() && *mutability == Mutability::Mut { + selector = selector.requiring_pointee_write(); + } if let Some(_) = self.pt.pointee(lhs.to_place_index(&self.pt).unwrap()) { // The MIR linter doesn't like it if we do x = &(*x) selector = selector.except( @@ -886,12 +925,75 @@ impl GenerationCtx { self.enter_bb(bb); } + fn insert_static_mut_workload(&mut self) { + if self.program.statics.is_empty() || self.cursor.function.index() != 0 { + return; + } + + let mutable_statics: Vec<(Static, TyId)> = self + .program + .statics + .iter_enumerated() + .filter_map(|(id, decl)| { + (decl.mutability == Mutability::Mut && self.pt.can_acquire_static(id)) + .then_some((id, decl.ty)) + }) + .collect(); + let Some(&(static_id, pointee_ty)) = + mutable_statics.iter().choose(&mut *self.rng.borrow_mut()) + else { + return; + }; + + let pointer_ty = self + .tcx + .iter_enumerated() + .find_map(|(ty, kind)| match kind { + TyKind::RawPtr(pointee, Mutability::Mut) if *pointee == pointee_ty => Some(ty), + _ => None, + }) + .expect("mutable static pointee has a matching raw pointer type"); + + let pointer_local = self.declare_new_var(Mutability::Mut, pointer_ty); + let before_local = self.declare_new_var(Mutability::Mut, pointee_ty); + let after_local = self.declare_new_var(Mutability::Mut, pointee_ty); + + let pointer = Place::from_local(pointer_local); + let mut pointee = pointer.clone(); + pointee + .project(ProjectionElem::Deref, self.current_decls(), &self.tcx) + .expect("mutable static pointer can be dereferenced"); + let before = Place::from_local(before_local); + let after = Place::from_local(after_local); + let write_value = self + .rng + .borrow_mut() + .gen_literal(pointee_ty, &self.tcx) + .expect("static pointee type is literalble"); + + let statements = [ + Statement::Assign( + pointer.clone(), + Rvalue::Use(Operand::StaticMut(static_id, pointer_ty)), + ), + Statement::Assign(before, Rvalue::Use(Operand::Copy(pointee.clone()))), + Statement::Assign(pointee.clone(), Rvalue::Use(Operand::Constant(write_value))), + Statement::Assign(after, Rvalue::Use(Operand::Copy(pointee))), + ]; + + for statement in statements { + self.post_generation(&statement); + self.current_bb_mut().insert_statement(statement); + } + } + // Generate a Return terminator, returns false if it's being // generated in fn0 fn add_return(&mut self) -> bool { trace!("generating a Return terminator to {:?}", self.cursor); debug_assert!(self.pt.can_return()); + self.insert_static_mut_workload(); self.insert_dump_var_gadget(); self.current_bb_mut().set_terminator(Terminator::Return); @@ -1168,17 +1270,59 @@ impl GenerationCtx { pub fn new(config: Config, seed: u64, debug_dump: bool) -> Self { let rng = RefCell::new(Box::new(rand::rngs::SmallRng::seed_from_u64(seed))); + let static_count = config.generation.static_count; let mut tcx = TyCtxt::from_primitives(config.ty); seed_tys(&mut tcx, &mut *rng.borrow_mut()); let tcx = Rc::new(tcx); let ty_weights = TySelect::new(&tcx); + let mut program = Program::new(debug_dump); + let mut pt = PlaceGraph::new(tcx.clone()); + + // Keep the default-zero path byte-for-byte reproducible: do not even + // draw from the RNG unless static generation is explicitly enabled. + if static_count > 0 { + let static_specs: Vec<(TyId, Mutability)> = tcx + .indices() + .filter_map(|ty| match ty.kind(&tcx) { + TyKind::Ref(pointee, Mutability::Not) + if ::is_literalble(*pointee, &tcx) => + { + Some((*pointee, Mutability::Not)) + } + TyKind::RawPtr(pointee, Mutability::Mut) + if ::is_literalble(*pointee, &tcx) => + { + Some((*pointee, Mutability::Mut)) + } + _ => None, + }) + .collect(); + + for _ in 0..static_count { + let Some(&(ty, mutability)) = static_specs.iter().choose(&mut *rng.borrow_mut()) + else { + break; + }; + let init = rng + .borrow_mut() + .gen_literal(ty, &tcx) + .expect("static pointee type is literalble"); + let id = program.push_static(StaticDecl { + ty, + mutability, + init, + }); + pt.allocate_static(id, ty, mutability, init); + } + } + // TODO: don't zero-initialize current_function and current_bb Self { rng, tcx: tcx.clone(), ty_weights, - program: Program::new(debug_dump), - pt: PlaceGraph::new(tcx.clone()), + program, + pt, return_stack: vec![], cursor: Cursor { function: Function::new(0), @@ -1301,10 +1445,26 @@ impl GenerationCtx { pt.mark_place_init(lhs); })); match rvalue { - Rvalue::AddressOf(_, referent) | Rvalue::Ref(_, referent) => { + Rvalue::AddressOf(_, referent) => { + let inherited_from = matches!( + referent.projection().first(), + Some(ProjectionElem::Deref) + ) + .then(|| referent.local().to_place_index(&self.pt).unwrap()); + let referent = referent.to_place_index(&self.pt).unwrap(); + actions.push(Box::new(move |pt| { + pt.set_address_of(lhs, referent, inherited_from); + })); + } + Rvalue::Ref(_, referent) => { + let derived_from = matches!( + referent.projection().first(), + Some(ProjectionElem::Deref) + ) + .then(|| referent.local().to_place_index(&self.pt).unwrap()); let referent = referent.to_place_index(&self.pt).unwrap(); actions.push(Box::new(move |pt| { - pt.set_ref(lhs, referent, None); + pt.set_ref_from_source(lhs, referent, derived_from); })); } _ => { @@ -1356,6 +1516,12 @@ impl GenerationCtx { pt.assign_literal(lhs, Some(*lit)); })); } + Operand::Static(id, _) | Operand::StaticMut(id, _) => { + let id = *id; + actions.push(Box::new(move |pt| { + pt.set_static_ref(lhs, id); + })); + } }, agg @ Rvalue::Aggregate(agg_kind, ..) => { if self.pt.ty(lhs).kind(&self.tcx).is_enum() { @@ -1379,6 +1545,12 @@ impl GenerationCtx { pt.assign_literal(target, Some(*lit)); })); } + Operand::Static(id, _) | Operand::StaticMut(id, _) => { + let id = *id; + actions.push(Box::new(move |pt| { + pt.set_static_ref(target, id); + })); + } } } } diff --git a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/mem/mod.rs b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/mem/mod.rs index b5f0627dee..7bd6345e61 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/mem/mod.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/mem/mod.rs @@ -109,6 +109,15 @@ impl Run { } } + pub fn unprotect(&mut self, offset: Size, len: Size, tag: Tag) { + for (_, stack) in self.ref_stack.iter_mut(offset, len) { + if let Some(i) = stack.iter().position(|b| b.tag == tag) { + assert!(stack[i].protected); + stack[i].protected = false; + } + } + } + /// Returns a list of tags above a certain tag pub fn tags_above(&self, offset: Size, len: Size, tag: Tag) -> Vec { let mut edges = BTreeSet::new(); @@ -352,6 +361,10 @@ pub struct BasicMemory { // a lookup table to aid removal from borrow stacks // a tag may cover multiple runs, e.g. &(u32, u32), pointers: HashMap>, + + // Static-derived references can be protected by nested calls. Keep that + // accounting separate so the ordinary reference model remains unchanged. + static_protection_counts: HashMap, } impl BasicMemory { @@ -361,6 +374,7 @@ impl BasicMemory { Self { allocations: IndexVec::new(), pointers: HashMap::new(), + static_protection_counts: HashMap::new(), } } @@ -517,6 +531,75 @@ impl BasicMemory { ) } + pub fn protect_tag(&mut self, tag: Tag) { + let old_count = self + .static_protection_counts + .get(&tag) + .copied() + .unwrap_or(0); + if old_count == 0 { + let run_ptrs = self + .pointers + .get(&tag) + .cloned() + .expect("tag has registered pointee ranges"); + for run_ptr in run_ptrs { + self.allocations[run_ptr.alloc_id].runs[run_ptr.run()].protect( + run_ptr.offset(), + run_ptr.size, + tag, + ); + } + } + self.static_protection_counts.insert(tag, old_count + 1); + } + + pub fn unprotect_tag(&mut self, tag: Tag) { + let should_unprotect = { + let count = self + .static_protection_counts + .get_mut(&tag) + .expect("tag is protected by a static-aware frame"); + assert!(*count > 0); + *count -= 1; + *count == 0 + }; + if !should_unprotect { + return; + } + self.static_protection_counts.remove(&tag); + + let run_ptrs = self + .pointers + .get(&tag) + .cloned() + .expect("tag has registered pointee ranges"); + for run_ptr in run_ptrs { + self.allocations[run_ptr.alloc_id].runs[run_ptr.run()].unprotect( + run_ptr.offset(), + run_ptr.size, + tag, + ); + } + } + + pub fn tag_is_protected(&self, tag: Tag) -> bool { + self.static_protection_counts.contains_key(&tag) + } + + pub fn remove_tag(&mut self, tag: Tag) { + let Some(run_ptrs) = self.pointers.remove(&tag) else { + return; + }; + for run_ptr in run_ptrs { + self.allocations[run_ptr.alloc_id].runs[run_ptr.run()].remove_borrow( + run_ptr.offset(), + run_ptr.size, + tag, + ); + } + } + pub fn can_read_with(&self, run_ptr: RunPointer, tag: Tag) -> bool { self.allocations[run_ptr.alloc_id].runs[run_ptr.run()].can_read_with( run_ptr.offset(), diff --git a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs index 3ff46147e2..e32264494f 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs @@ -10,7 +10,7 @@ use index_vec::IndexVec; use mir::{ syntax::{ Body, FieldIdx, Literal, Local, LocalDecls, Mutability, Operand, Place, ProjectionElem, - Rvalue, TyId, TyKind, UintTy, VariantIdx, + Rvalue, Static, TyId, TyKind, UintTy, VariantIdx, }, tyctxt::TyCtxt, }; @@ -30,6 +30,13 @@ pub type Path = SmallVec<[ProjectionIndex; 4]>; pub const MAX_COMPLEXITY: usize = 100; +#[derive(Clone, Copy)] +struct StaticState { + place: PlaceIndex, + root_tag: Tag, + mutability: Mutability, +} + #[derive(Clone)] struct Frame { locals: BiHashMap, @@ -41,6 +48,7 @@ struct Frame { // while the frame is on stack return_destination: PlaceIndex, moved_in: SmallVec<[PlaceIndex; 4]>, + protected_tags: SmallVec<[Tag; 4]>, } impl Frame { @@ -50,6 +58,7 @@ impl Frame { locals_ordered: BinaryHeap::new(), return_destination: dest, moved_in: SmallVec::from_iter(moved_in), + protected_tags: SmallVec::new(), } } @@ -87,6 +96,9 @@ impl PlaceOperand { PlaceOperand::Move(index) } Operand::Constant(lit) => PlaceOperand::Constant(*lit), + Operand::Static(..) | Operand::StaticMut(..) => { + unreachable!("static operands are only generated as direct rvalue sources") + } } } } @@ -98,6 +110,10 @@ pub struct PlaceGraph { frames: Vec, index_candidates: HashMap>, pointer_tags: IndexVec>, + tag_parents: IndexVec>, + tag_child_counts: IndexVec, + tag_alive: IndexVec, + statics: IndexVec, places: Graph, memory: BasicMemory, @@ -181,6 +197,10 @@ impl PlaceGraph { )], index_candidates: HashMap::new(), pointer_tags: IndexVec::new(), + tag_parents: IndexVec::new(), + tag_child_counts: IndexVec::new(), + tag_alive: IndexVec::new(), + statics: IndexVec::new(), places: StableGraph::default(), memory: BasicMemory::new(), tcx, @@ -357,17 +377,63 @@ impl PlaceGraph { let old_frame = self.frames.pop().expect("call stack isn't empty"); self.index_candidates.clear(); // Invalidate cache - // Copy ret + if self.statics.is_empty() { + // Keep the default-zero path identical to the pre-static model. + self.copy_place(old_frame.return_destination, callee_ret); + + // Remove ref edges into places that are about to be deallocated. + // This is necessary to prevent dangling references. + let mut ref_edges = vec![]; + let mut alloc_ids = vec![]; + + for pidx in old_frame.locals.right_values() { + self.visit_transitive_subfields(*pidx, |node| { + ref_edges.extend(self.pointers_to(node).iter().map(|(_, edge)| edge)); + VisitAction::Continue + }); + alloc_ids.push(self.places[*pidx].alloc_id); + } + + for edge in ref_edges { + self.remove_edge(edge); + } + + for alloc_id in alloc_ids { + self.memory.deallocate(alloc_id); + } + return; + } + + // Static-aware path: copy the return value before dropping callee-owned + // pointer edges so returned references keep their provenance. self.copy_place(old_frame.return_destination, callee_ret); - // Remove ref edges into places that are about to be deallocated. - // This is necessary to prevent dangling references. + for tag in old_frame.protected_tags.iter().copied() { + self.memory.unprotect_tag(tag); + } + for tag in old_frame.protected_tags.iter().copied() { + self.retire_tag_if_unused(tag); + } + + // Persistent statics outlive the frame, but aliases owned by the frame do + // not. Remove both outgoing pointer edges and incoming references to dying + // locals before deallocating the local allocations. let mut ref_edges = vec![]; let mut alloc_ids = vec![]; for pidx in old_frame.locals.right_values() { self.visit_transitive_subfields(*pidx, |node| { - ref_edges.extend(self.pointers_to(node).iter().map(|(_, edge)| edge)); + if self.places[node].ty.is_any_ptr(&self.tcx) + && let Some(edge) = self.ref_edge(node) + && !ref_edges.contains(&edge) + { + ref_edges.push(edge); + } + for (_, edge) in self.pointers_to(node) { + if !ref_edges.contains(&edge) { + ref_edges.push(edge); + } + } VisitAction::Continue }); alloc_ids.push(self.places[*pidx].alloc_id); @@ -377,7 +443,6 @@ impl PlaceGraph { self.remove_edge(edge); } - // Deallocate places. for alloc_id in alloc_ids { self.memory.deallocate(alloc_id); } @@ -392,6 +457,121 @@ impl PlaceGraph { pidx } + pub fn allocate_static( + &mut self, + id: Static, + ty: TyId, + mutability: Mutability, + init: Literal, + ) -> PlaceIndex { + let mut pidx = Default::default(); + self.memory.allocate_with_builder(|builder| { + pidx = Self::add_place(&mut self.places, ty, &self.tcx, builder, None); + }); + self.mark_place_init(pidx); + self.assign_literal(pidx, Some(init)); + self.update_complexity(pidx, 1); + + // A static has one persistent root provenance. Reacquiring Static/StaticMut + // reuses this tag instead of minting an independent borrow that could jump + // over a live alias. + let root_tag = self.new_tag(BTreeSet::new(), None); + let root_borrow = match mutability { + Mutability::Mut => BorrowType::Raw, + Mutability::Not => BorrowType::Shared, + }; + self.add_borrow_for_place(pidx, root_borrow, root_tag); + + let inserted = self.statics.push(StaticState { + place: pidx, + root_tag, + mutability, + }); + assert_eq!(inserted, id); + pidx + } + + #[cfg(test)] + pub fn static_place(&self, id: Static) -> PlaceIndex { + self.statics[id].place + } + + #[cfg(test)] + fn static_root_tag(&self, id: Static) -> Tag { + self.statics[id].root_tag + } + + fn is_static_root_tag(&self, tag: Tag) -> bool { + self.statics.iter().any(|state| state.root_tag == tag) + } + + fn new_tag(&mut self, pointers: BTreeSet, parent: Option) -> Tag { + let tag = self.pointer_tags.push(pointers); + let parent_idx = self.tag_parents.push(parent); + let child_count_idx = self.tag_child_counts.push(0); + let alive_idx = self.tag_alive.push(true); + assert_eq!(tag, parent_idx); + assert_eq!(tag, child_count_idx); + assert_eq!(tag, alive_idx); + + if let Some(parent) = parent { + assert!(self.tag_alive[parent], "parent provenance tag must be live"); + self.tag_child_counts[parent] += 1; + } + + tag + } + + fn retire_tag_if_unused(&mut self, tag: Tag) { + if !self.tag_alive[tag] + || !self.pointer_tags[tag].is_empty() + || self.tag_child_counts[tag] != 0 + || self.is_static_root_tag(tag) + || self.memory.tag_is_protected(tag) + { + return; + } + + let parent = self.tag_parents[tag]; + self.memory.remove_tag(tag); + self.tag_alive[tag] = false; + self.tag_parents[tag] = None; + + if let Some(parent) = parent { + assert!(self.tag_child_counts[parent] > 0); + self.tag_child_counts[parent] -= 1; + self.retire_tag_if_unused(parent); + } + } + + fn is_static_allocation(&self, place: PlaceIndex) -> bool { + let alloc_id = self.places[place].alloc_id; + self.statics + .iter() + .any(|state| self.places[state.place].alloc_id == alloc_id) + } + + pub fn can_acquire_static(&self, id: Static) -> bool { + let state = self.statics[id]; + let mut can_acquire = true; + self.visit_transitive_subfields(state.place, |place| { + if let Some(run) = self.places[place].run_ptr { + can_acquire = match state.mutability { + Mutability::Mut => self.memory.can_write_with(run, state.root_tag), + Mutability::Not => self.memory.can_read_with(run, state.root_tag), + }; + if can_acquire { + VisitAction::Stop + } else { + VisitAction::ShortCircuit + } + } else { + VisitAction::Continue + } + }); + can_acquire + } + pub fn deallocate_local(&mut self, local: Local) { // FIXME: should we need to remove local from the frame? let pidx = local.to_place_index(self).expect("place exists"); @@ -599,11 +779,30 @@ impl PlaceGraph { } if dst_node.ty.is_any_ptr(&self.tcx) { - if let Some(pointee) = self.pointee(src) { - self.set_ref(dst, pointee, Some(src)); - } - if let Some(old) = self.ref_edge(dst) { - self.remove_edge(old); + let pointee = self.pointee(src); + let points_into_static = + pointee.is_some_and(|pointee| self.is_static_allocation(pointee)); + + if points_into_static { + // Static-derived aliases must keep the source provenance. Remove + // the destination's old edge before installing the copied edge. + if let Some(old) = self.ref_edge(dst) { + self.remove_edge(old); + } + if let Some(pointee) = pointee + && (!self.ty(src).is_ref(&self.tcx) || self.is_ref_valid(src)) + { + self.set_ref(dst, pointee, Some(src)); + } + } else { + // Preserve the legacy non-static behavior exactly so + // static_count = 0 does not perturb existing seeds. + if let Some(pointee) = pointee { + self.set_ref(dst, pointee, Some(src)); + } + if let Some(old) = self.ref_edge(dst) { + self.remove_edge(old); + } } self.places[dst].offset = self.places[src].offset; } @@ -744,9 +943,23 @@ impl PlaceGraph { fn mark_ref_protected(&mut self, p: impl ToPlaceIndex) { let p = p.to_place_index(&self).expect("place exists"); assert!(self.ty(p).is_ref(&self.tcx)); - let run = self.places[p].run_ptr.expect("has run"); - self.memory - .mark_protected(run, self.places[p].tag.expect("has tag")); + let tag = self.places[p].tag.expect("has tag"); + + let points_into_static = self + .pointee(p) + .is_some_and(|pointee| self.is_static_allocation(pointee)); + if points_into_static { + assert!( + self.is_ref_valid(p), + "cannot protect an invalid reference into a static" + ); + self.memory.protect_tag(tag); + self.current_frame_mut().protected_tags.push(tag); + } else { + // Preserve the legacy call model for ordinary references. + let run = self.places[p].run_ptr.expect("has run"); + self.memory.mark_protected(run, tag); + } } pub fn mark_place_moved(&mut self, p: impl ToPlaceIndex) { @@ -786,13 +999,40 @@ impl PlaceGraph { }); } - /// Whether a reference value is legal to produce + /// Whether a reference value is legal to produce. + /// + /// For references into statics, pointee initialization is not sufficient: + /// the reference's provenance tag must still grant read permission. A write + /// through the static root may invalidate that tag while the graph edge remains. fn is_ref_valid(&self, r: PlaceIndex) -> bool { assert!(self.ty(r).is_ref(&self.tcx)); let Some(target) = self.pointee(r) else { return false; }; - self.is_place_init(target) + if !self.is_place_init(target) { + return false; + } + + if !self.is_static_allocation(target) { + return true; + } + + let Some(tag) = self.places[r].tag else { + return false; + }; + let mut valid = true; + self.visit_transitive_subfields(target, |node| { + if let Some(run) = self.places[node].run_ptr { + if !self.memory.can_read_with(run, tag) { + valid = false; + return VisitAction::ShortCircuit; + } + VisitAction::Stop + } else { + VisitAction::Continue + } + }); + valid } /// Whether all refs contained in a place are all valid @@ -878,7 +1118,19 @@ impl PlaceGraph { .map(|deref| deref.id()) } - /// Creates an edge pointer -[Deref]-> pointee + fn add_borrow_for_place(&mut self, pointee: PlaceIndex, ref_type: BorrowType, tag: Tag) { + self.update_transitive_subfields(pointee, |this, place| { + if let Some(run) = this.places[place].run_ptr { + this.memory.add_ref(run, ref_type, tag); + VisitAction::Stop + } else { + VisitAction::Continue + } + }); + } + + /// Creates an edge pointer -[Deref]-> pointee. A copied pointer keeps the + /// source tag; a newly-created reference gets a fresh tag. pub fn set_ref( &mut self, pointer: impl ToPlaceIndex, @@ -911,7 +1163,6 @@ impl PlaceGraph { self.places[pointer].offset = None; self.update_complexity(pointer, self.places[pointee].complexity); - // Add new ref edge self.places .add_edge(pointer, pointee, ProjectionElem::Deref); @@ -920,16 +1171,9 @@ impl PlaceGraph { self.places[pointer].tag = Some(tag); self.pointer_tags[tag].insert(pointer); } else { - let tag = self.pointer_tags.push(BTreeSet::from([pointer])); + let tag = self.new_tag(BTreeSet::from([pointer]), None); self.places[pointer].tag = Some(tag); - self.update_transitive_subfields(pointee, |this, place| { - if let Some(run) = this.places[place].run_ptr { - this.memory.add_ref(run, ref_type, tag); - VisitAction::Stop - } else { - VisitAction::Continue - } - }); + self.add_borrow_for_place(pointee, ref_type, tag); } assert_eq!( @@ -940,6 +1184,136 @@ impl PlaceGraph { ); } + /// Creates a new reference through an existing pointer/reference into a static. + /// The new borrow gets its own tag, but that tag keeps the source tag alive so + /// removing the source graph alias cannot relax the derived reference's permissions. + pub fn set_ref_from_source( + &mut self, + pointer: impl ToPlaceIndex, + pointee: impl ToPlaceIndex, + derived_from: Option, + ) { + let pointee = pointee.to_place_index(self).expect("place exists"); + let Some(source) = derived_from.filter(|_| self.is_static_allocation(pointee)) else { + self.set_ref(pointer, pointee, None); + return; + }; + let pointer = pointer.to_place_index(self).expect("place exists"); + + assert_ne!( + pointer, source, + "a reborrow must use a distinct destination" + ); + assert!(self.ty(source).is_any_ptr(&self.tcx)); + assert_eq!( + self.places[pointer].ty.pointee_ty(&self.tcx).unwrap(), + self.places[pointee].ty + ); + + let ref_type = match self.ty(pointer).kind(&self.tcx) { + TyKind::Ref(_, Mutability::Mut) => BorrowType::Exclusive, + TyKind::Ref(_, Mutability::Not) => BorrowType::Shared, + _ => panic!("reborrow destination must be a reference"), + }; + + match ref_type { + BorrowType::Exclusive => assert!( + self.can_write_through(source, pointee), + "mutable reborrow requires source write permission" + ), + BorrowType::Shared => assert!( + self.can_read_through(source, pointee), + "shared reborrow requires source read permission" + ), + BorrowType::Raw => unreachable!(), + } + assert!(self.is_place_init(pointee)); + + let parent_tag = self.places[source].tag.expect("source has tag"); + assert!( + self.tag_alive[parent_tag], + "source provenance tag must be live" + ); + + if let Some(old) = self.ref_edge(pointer) { + self.remove_edge(old); + } + + self.places[pointer].offset = None; + self.update_complexity(pointer, self.places[pointee].complexity); + self.places + .add_edge(pointer, pointee, ProjectionElem::Deref); + + let tag = self.new_tag(BTreeSet::from([pointer]), Some(parent_tag)); + self.places[pointer].tag = Some(tag); + self.add_borrow_for_place(pointee, ref_type, tag); + + assert_eq!( + self.places + .edges_directed(pointer, Direction::Outgoing) + .count(), + 1 + ); + } + + /// Creates an AddressOf edge. Raw pointers created through a static-derived + /// pointer inherit that pointer's provenance so dropping the source graph + /// alias cannot relax the derived pointer's access permissions. + pub fn set_address_of( + &mut self, + pointer: impl ToPlaceIndex, + pointee: impl ToPlaceIndex, + inherited_from: Option, + ) { + let pointee = pointee.to_place_index(self).expect("place exists"); + let inherited_from = if self.is_static_allocation(pointee) { + inherited_from + } else { + None + }; + self.set_ref(pointer, pointee, inherited_from); + } + + /// Connects a Static/StaticMut operand to its persistent global-root tag. + pub fn set_static_ref(&mut self, pointer: impl ToPlaceIndex, id: Static) { + assert!( + self.can_acquire_static(id), + "static root is blocked by a live alias" + ); + + let pointer = pointer.to_place_index(self).expect("place exists"); + let state = self.statics[id]; + let pointee = state.place; + + assert_eq!( + self.places[pointer].ty.pointee_ty(&self.tcx).unwrap(), + self.places[pointee].ty + ); + match (state.mutability, self.ty(pointer).kind(&self.tcx)) { + (Mutability::Not, TyKind::Ref(_, Mutability::Not)) + | (Mutability::Mut, TyKind::RawPtr(_, Mutability::Mut)) => {} + _ => panic!("static operand pointer kind does not match static mutability"), + } + + if let Some(old) = self.ref_edge(pointer) { + self.remove_edge(old); + } + + self.places[pointer].offset = None; + self.update_complexity(pointer, self.places[pointee].complexity); + self.places + .add_edge(pointer, pointee, ProjectionElem::Deref); + self.places[pointer].tag = Some(state.root_tag); + self.pointer_tags[state.root_tag].insert(pointer); + + assert_eq!( + self.places + .edges_directed(pointer, Direction::Outgoing) + .count(), + 1 + ); + } + pub fn is_place_live(&self, p: impl ToPlaceIndex) -> bool { let Some(pidx) = p.to_place_index(self) else { return false; @@ -1268,15 +1642,24 @@ impl PlaceGraph { } } - // We need to mark reference uninit if the edge is removed (though not raw pointers) + // We need to mark reference uninit if the edge is removed (though not raw pointers). + // Static-derived tags additionally retire when their final graph alias dies. fn remove_edge(&mut self, e: ProjectionIndex) { - let (source, _) = self.places.edge_endpoints(e).expect("edge exists"); + let (source, target) = self.places.edge_endpoints(e).expect("edge exists"); let tag = self.places[source].tag.expect("has tag"); - let edges: &mut BTreeSet = &mut self.pointer_tags[tag]; - edges.remove(&source); + let points_into_static = self.is_static_allocation(target); + let tag_is_unused = { + let edges: &mut BTreeSet = &mut self.pointer_tags[tag]; + edges.remove(&source); + edges.is_empty() + }; let removed = self.places.remove_edge(e).expect("edge exists"); assert!(removed.is_deref()); + + if points_into_static && tag_is_unused { + self.retire_tag_if_unused(tag); + } } // Returns if a ConstantIndex can be turned into an Index with local. @@ -1482,7 +1865,7 @@ impl HasComplexity for Operand { fn complexity(&self, pt: &PlaceGraph) -> usize { match self { Operand::Copy(place) | Operand::Move(place) => place.complexity(pt), - Operand::Constant(_) => 1, + Operand::Constant(_) | Operand::Static(..) | Operand::StaticMut(..) => 1, } } } @@ -1522,8 +1905,9 @@ mod tests { use index_vec::IndexVec; use mir::{ syntax::{ - Adt, BinOp, FieldIdx, Literal, Local, LocalDecl, LocalDecls, Mutability, Operand, - Place, ProjectionElem, Rvalue, TyId, TyKind, UintTy, VariantDef, VariantIdx, + Adt, BinOp, Body, FieldIdx, IntTy, Literal, Local, LocalDecl, LocalDecls, Mutability, + Operand, Place, ProjectionElem, Rvalue, Static, TyId, TyKind, UintTy, VariantDef, + VariantIdx, }, tyctxt::{AdtMeta, TyCtxt}, }; @@ -2162,6 +2546,426 @@ mod tests { index_path_place(None); } + #[test] + fn static_allocation_is_persistent() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Not, 7_i32.into()); + assert!(pt.is_place_init(static_place)); + assert!(matches!( + pt.known_val(static_place), + Some(&Literal::Int(7, IntTy::I32)) + )); + assert!(pt.current_frame().get_by_index(static_place).is_none()); + assert_eq!(pt.get_complexity(static_place), 1); + + let reference = Local::new(1); + pt.allocate_local(reference, t_ref); + pt.mark_place_init(reference); + pt.set_static_ref(reference, static_id); + + assert_eq!(pt.static_place(static_id), static_place); + assert_eq!( + pt.pointee(reference.to_place_index(&pt).unwrap()), + Some(static_place) + ); + } + + #[test] + fn static_reacquisition_reuses_global_root_provenance() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_mut_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Mut)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + let root_tag = pt.static_root_tag(static_id); + + let ptr1 = Local::new(1); + let ptr1_p = pt.allocate_local(ptr1, t_ptr); + pt.mark_place_init(ptr1_p); + pt.set_static_ref(ptr1_p, static_id); + + let ptr2 = Local::new(2); + let ptr2_p = pt.allocate_local(ptr2, t_ptr); + pt.mark_place_init(ptr2_p); + pt.set_static_ref(ptr2_p, static_id); + + assert_eq!(pt.places[ptr1_p].tag, Some(root_tag)); + assert_eq!(pt.places[ptr2_p].tag, Some(root_tag)); + assert_eq!(pt.pointer_tags[root_tag].len(), 2); + assert!(pt.can_acquire_static(static_id)); + + let exclusive = Local::new(3); + let exclusive_p = pt.allocate_local(exclusive, t_mut_ref); + pt.mark_place_init(exclusive_p); + pt.set_ref(exclusive_p, static_place, None); + + assert!(!pt.can_acquire_static(static_id)); + + pt.mark_place_uninit(exclusive_p); + assert!(pt.can_acquire_static(static_id)); + } + + #[test] + fn static_reacquisition_rejects_live_reference_argument() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_mut_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Mut)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let caller = Body::new(&[], TyCtxt::UNIT, false); + pt.enter_fn0(&caller); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let reference = Local::new(1); + let reference_p = pt.allocate_local(reference, t_mut_ref); + pt.mark_place_init(reference_p); + pt.set_ref(reference_p, static_place, None); + let reference_tag = pt.places[reference_p].tag.expect("has tag"); + assert!(!pt.can_acquire_static(static_id)); + + let callee = Body::new(&[t_mut_ref], TyCtxt::UNIT, false); + pt.enter_fn( + &callee, + &[Operand::Move(Place::from_local(reference))], + &Place::RETURN_SLOT, + ); + + let callee_arg = Local::new(1).to_place_index(&pt).unwrap(); + assert_eq!(pt.pointee(callee_arg), Some(static_place)); + assert_eq!(pt.places[callee_arg].tag, Some(reference_tag)); + assert!(!pt.can_acquire_static(static_id)); + } + + #[test] + fn static_reference_argument_protection_ends_at_frame_exit() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let caller = Body::new(&[], TyCtxt::UNIT, false); + pt.enter_fn0(&caller); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let reference = Local::new(1); + let reference_p = pt.allocate_local(reference, t_shared_ref); + pt.mark_place_init(reference_p); + pt.set_ref(reference_p, static_place, None); + + let callee = Body::new(&[t_shared_ref], TyCtxt::UNIT, false); + pt.enter_fn( + &callee, + &[Operand::Copy(Place::from_local(reference))], + &Place::RETURN_SLOT, + ); + assert!(!pt.can_acquire_static(static_id)); + + pt.mark_place_init(Local::RET); + pt.exit_fn(); + + let caller_reference = reference.to_place_index(&pt).unwrap(); + assert_eq!(pt.pointee(caller_reference), Some(static_place)); + assert!(pt.can_acquire_static(static_id)); + } + + #[test] + fn invalidated_static_shared_alias_is_not_valid() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let root = Local::new(1); + let root_p = pt.allocate_local(root, t_ptr); + pt.mark_place_init(root_p); + pt.set_static_ref(root_p, static_id); + + let alias = Local::new(2); + let alias_p = pt.allocate_local(alias, t_shared_ref); + pt.mark_place_init(alias_p); + pt.set_ref(alias_p, static_place, None); + + assert!(pt.contains_only_valid_ref(alias_p)); + assert!(pt.can_read_through(alias_p, static_place)); + + let decls = decls(&[(root, t_ptr), (alias, t_shared_ref)]); + let mut through_root = Place::from_local(root); + through_root + .project(ProjectionElem::Deref, &decls, &pt.tcx) + .unwrap(); + let root_tag = pt.accessing_tag(&through_root); + pt.place_written(&through_root, root_tag); + + // The graph edge intentionally remains, but the write invalidated the + // shared alias's memory tag. Reference validity must observe both. + assert_eq!(pt.pointee(alias_p), Some(static_place)); + assert!(!pt.can_read_through(alias_p, static_place)); + assert!(!pt.contains_only_valid_ref(alias_p)); + } + + #[test] + #[should_panic(expected = "mutable reborrow requires source write permission")] + fn static_shared_source_cannot_create_mutable_reborrow() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let t_mut_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Mut)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let root = Local::new(1); + let root_p = pt.allocate_local(root, t_ptr); + pt.mark_place_init(root_p); + pt.set_static_ref(root_p, static_id); + + let shared = Local::new(2); + let shared_p = pt.allocate_local(shared, t_shared_ref); + pt.mark_place_init(shared_p); + pt.set_ref(shared_p, static_place, None); + assert!(!pt.can_write_through(shared_p, static_place)); + + let derived = Local::new(3); + let derived_p = pt.allocate_local(derived, t_mut_ref); + pt.mark_place_init(derived_p); + pt.set_ref_from_source(derived_p, static_place, Some(shared_p)); + } + + #[test] + fn copied_static_reborrow_keeps_source_permission_until_last_copy_dies() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_mut = Static::new(0); + let static_mut_place = + pt.allocate_static(static_mut, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + let static_shared = Static::new(1); + pt.allocate_static(static_shared, TyCtxt::I32, Mutability::Not, 11_i32.into()); + + let source = Local::new(1); + let source_p = pt.allocate_local(source, t_shared_ref); + pt.mark_place_init(source_p); + pt.set_ref(source_p, static_mut_place, None); + let source_tag = pt.places[source_p].tag.expect("has tag"); + + let derived = Local::new(2); + let derived_p = pt.allocate_local(derived, t_shared_ref); + pt.mark_place_init(derived_p); + pt.set_ref_from_source(derived_p, static_mut_place, Some(source_p)); + let derived_tag = pt.places[derived_p].tag.expect("has tag"); + assert_eq!(pt.tag_parents[derived_tag], Some(source_tag)); + + let copied = Local::new(3); + let copied_p = pt.allocate_local(copied, t_shared_ref); + pt.mark_place_init(copied_p); + pt.copy_place(copied_p, derived_p); + assert_eq!(pt.places[copied_p].tag, Some(derived_tag)); + + pt.set_static_ref(source_p, static_shared); + + assert!(pt.tag_alive[source_tag]); + assert!(pt.can_read_through(derived_p, static_mut_place)); + assert!(!pt.can_write_through(derived_p, static_mut_place)); + assert!(pt.can_read_through(copied_p, static_mut_place)); + assert!(!pt.can_write_through(copied_p, static_mut_place)); + + pt.mark_place_uninit(derived_p); + assert!(pt.tag_alive[derived_tag]); + pt.mark_place_uninit(copied_p); + assert!(!pt.tag_alive[derived_tag]); + assert!(!pt.tag_alive[source_tag]); + } + + #[test] + fn returned_static_reborrow_keeps_source_permission_across_frame_cleanup() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let caller = Body::new(&[], TyCtxt::UNIT, false); + pt.enter_fn0(&caller); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let returned = Local::new(1); + pt.allocate_local(returned, t_shared_ref); + + let callee = Body::new(&[], t_shared_ref, false); + pt.enter_fn(&callee, &[], &Place::from_local(returned)); + + let source = Local::new(1); + let source_p = pt.allocate_local(source, t_shared_ref); + pt.mark_place_init(source_p); + pt.set_ref(source_p, static_place, None); + let source_tag = pt.places[source_p].tag.expect("has tag"); + + let derived = Local::new(2); + let derived_p = pt.allocate_local(derived, t_shared_ref); + pt.mark_place_init(derived_p); + pt.set_ref_from_source(derived_p, static_place, Some(source_p)); + let derived_tag = pt.places[derived_p].tag.expect("has tag"); + + pt.mark_place_init(Local::RET); + pt.copy_place(Local::RET, derived_p); + pt.exit_fn(); + + let returned_p = returned.to_place_index(&pt).unwrap(); + assert_eq!(pt.pointee(returned_p), Some(static_place)); + assert_eq!(pt.places[returned_p].tag, Some(derived_tag)); + assert!(pt.tag_alive[source_tag]); + assert!(pt.tag_alive[derived_tag]); + assert!(pt.can_read_through(returned_p, static_place)); + assert!(!pt.can_write_through(returned_p, static_place)); + + pt.mark_place_uninit(returned_p); + assert!(!pt.tag_alive[derived_tag]); + assert!(!pt.tag_alive[source_tag]); + } + + #[test] + fn static_shared_derived_raw_pointer_stays_read_only_after_source_overwrite() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_mut = Static::new(0); + let static_mut_place = + pt.allocate_static(static_mut, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + let static_shared = Static::new(1); + pt.allocate_static(static_shared, TyCtxt::I32, Mutability::Not, 11_i32.into()); + + let root = Local::new(1); + let root_p = pt.allocate_local(root, t_ptr); + pt.mark_place_init(root_p); + pt.set_static_ref(root_p, static_mut); + + let shared = Local::new(2); + let shared_p = pt.allocate_local(shared, t_shared_ref); + pt.mark_place_init(shared_p); + pt.set_ref(shared_p, static_mut_place, None); + let shared_tag = pt.places[shared_p].tag.expect("has tag"); + + let derived = Local::new(3); + let derived_p = pt.allocate_local(derived, t_ptr); + pt.mark_place_init(derived_p); + pt.set_address_of(derived_p, static_mut_place, Some(shared_p)); + + assert_eq!(pt.places[derived_p].tag, Some(shared_tag)); + assert!(!pt.can_write_through(derived_p, static_mut_place)); + + // Reassigning the final direct shared-reference local must not retire + // the inherited provenance while a derived raw pointer still uses it. + pt.set_static_ref(shared_p, static_shared); + + assert_eq!(pt.pointee(derived_p), Some(static_mut_place)); + assert_eq!(pt.places[derived_p].tag, Some(shared_tag)); + assert!(!pt.can_write_through(derived_p, static_mut_place)); + } + + #[test] + fn returned_static_shared_derived_raw_pointer_stays_read_only() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let caller = Body::new(&[], TyCtxt::UNIT, false); + pt.enter_fn0(&caller); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let returned = Local::new(1); + pt.allocate_local(returned, t_ptr); + + let callee = Body::new(&[], t_ptr, false); + pt.enter_fn(&callee, &[], &Place::from_local(returned)); + + let root = Local::new(1); + let root_p = pt.allocate_local(root, t_ptr); + pt.mark_place_init(root_p); + pt.set_static_ref(root_p, static_id); + + let shared = Local::new(2); + let shared_p = pt.allocate_local(shared, t_shared_ref); + pt.mark_place_init(shared_p); + pt.set_ref(shared_p, static_place, None); + + let derived = Local::new(3); + let derived_p = pt.allocate_local(derived, t_ptr); + pt.mark_place_init(derived_p); + pt.set_address_of(derived_p, static_place, Some(shared_p)); + assert!(!pt.can_write_through(derived_p, static_place)); + + pt.mark_place_init(Local::RET); + pt.copy_place(Local::RET, derived_p); + pt.exit_fn(); + + let returned_p = returned.to_place_index(&pt).unwrap(); + assert_eq!(pt.pointee(returned_p), Some(static_place)); + assert!(!pt.can_write_through(returned_p, static_place)); + } + + #[test] + fn returned_direct_static_mut_raw_pointer_remains_writable() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let caller = Body::new(&[], TyCtxt::UNIT, false); + pt.enter_fn0(&caller); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let returned = Local::new(1); + pt.allocate_local(returned, t_ptr); + + let callee = Body::new(&[], t_ptr, false); + pt.enter_fn(&callee, &[], &Place::from_local(returned)); + + let direct = Local::new(1); + let direct_p = pt.allocate_local(direct, t_ptr); + pt.mark_place_init(direct_p); + pt.set_static_ref(direct_p, static_id); + + pt.mark_place_init(Local::RET); + pt.copy_place(Local::RET, direct_p); + pt.exit_fn(); + + let returned_p = returned.to_place_index(&pt).unwrap(); + assert_eq!(pt.pointee(returned_p), Some(static_place)); + assert_eq!( + pt.places[returned_p].tag, + Some(pt.static_root_tag(static_id)) + ); + assert!(pt.can_write_through(returned_p, static_place)); + } + #[test] fn shared_reference() { let mut tcx = TyCtxt::from_primitives(TyConfig::default()); diff --git a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/place_select.rs b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/place_select.rs index 3b103274ca..bff7cf1264 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/place_select.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/place_select.rs @@ -33,6 +33,7 @@ pub struct PlaceSelector { refed: Vec, size: Option, allow_uninit: bool, + pointee_write: bool, usage: PlaceUsage, tcx: Rc, } @@ -64,6 +65,7 @@ impl PlaceSelector { usage: PlaceUsage::Operand, exclusions: vec![], allow_uninit: false, + pointee_write: false, tcx, moved: vec![], refed: vec![], @@ -120,6 +122,14 @@ impl PlaceSelector { } } + pub fn requiring_pointee_write(self) -> Self { + assert_eq!(self.usage, PlaceUsage::Pointee); + Self { + pointee_write: true, + ..self + } + } + pub fn of_ty(self, ty: TyId) -> Self { let tys = Some(vec![ty]); Self { tys, ..self } @@ -246,6 +256,11 @@ impl PlaceSelector { return false; } } + PlaceUsage::Pointee if self.pointee_write => { + if !pt.can_write_through(ppath.source(), index) { + return false; + } + } // reads that will be done as moves PlaceUsage::Operand | PlaceUsage::Argument if !pt.ty(index).is_copy(&self.tcx) => { if !pt.can_write_through(ppath.source(), index) { @@ -374,7 +389,7 @@ mod tests { use config::TyConfig; use mir::{ - syntax::{Local, LocalDecl, LocalDecls, Place}, + syntax::{Local, LocalDecl, LocalDecls, Mutability, Place, ProjectionElem, Static, TyKind}, tyctxt::TyCtxt, }; use rand::{ @@ -409,6 +424,100 @@ mod tests { (pt, tcx, decls) } + #[test] + fn invalidated_static_reference_is_not_an_argument_candidate() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let tcx = Rc::new(tcx); + let mut pt = PlaceGraph::new(tcx.clone()); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let root = Local::new(1); + let root_p = pt.allocate_local(root, t_ptr); + pt.mark_place_init(root_p); + pt.set_static_ref(root_p, static_id); + + let alias = Local::new(2); + let alias_p = pt.allocate_local(alias, t_shared_ref); + pt.mark_place_init(alias_p); + pt.set_ref(alias_p, static_place, None); + + let mut decls = LocalDecls::new(); + decls.push(LocalDecl::new_mut(TyCtxt::UNIT)); + decls.push(LocalDecl::new_mut(t_ptr)); + decls.push(LocalDecl::new_mut(t_shared_ref)); + + let before: Vec = PlaceSelector::for_argument(tcx.clone()) + .of_ty(t_shared_ref) + .into_iter_place(&pt, &decls) + .collect(); + assert!(before.iter().any(|place| place.local() == alias)); + + let mut through_root = Place::from_local(root); + through_root + .project(ProjectionElem::Deref, &decls, &tcx) + .unwrap(); + let root_tag = pt.accessing_tag(&through_root); + pt.place_written(&through_root, root_tag); + + let after: Vec = PlaceSelector::for_argument(tcx.clone()) + .of_ty(t_shared_ref) + .into_iter_place(&pt, &decls) + .collect(); + assert!(!after.iter().any(|place| place.local() == alias)); + } + + #[test] + fn mutable_reference_pointee_requires_write_permission() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let t_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I32, Mutability::Mut)); + let t_shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let tcx = Rc::new(tcx); + let mut pt = PlaceGraph::new(tcx.clone()); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let root = Local::new(1); + let root_p = pt.allocate_local(root, t_ptr); + pt.mark_place_init(root_p); + pt.set_static_ref(root_p, static_id); + + let shared = Local::new(2); + let shared_p = pt.allocate_local(shared, t_shared_ref); + pt.mark_place_init(shared_p); + pt.set_ref(shared_p, static_place, None); + + let mut decls = LocalDecls::new(); + decls.push(LocalDecl::new_mut(TyCtxt::UNIT)); + decls.push(LocalDecl::new_mut(t_ptr)); + decls.push(LocalDecl::new_mut(t_shared_ref)); + + let readable: Vec = PlaceSelector::for_pointee(tcx.clone(), false) + .of_ty(TyCtxt::I32) + .into_iter_place(&pt, &decls) + .collect(); + assert!(readable.iter().any(|place| { + place.local() == shared + && matches!(place.projection().first(), Some(ProjectionElem::Deref)) + })); + + let writable: Vec = PlaceSelector::for_pointee(tcx.clone(), false) + .requiring_pointee_write() + .of_ty(TyCtxt::I32) + .into_iter_place(&pt, &decls) + .collect(); + assert!(!writable.iter().any(|place| { + place.local() == shared + && matches!(place.projection().first(), Some(ProjectionElem::Deref)) + })); + } + #[bench] fn bench_select(b: &mut Bencher) { let mut rng = SmallRng::seed_from_u64(0); diff --git a/cuda-oxide/crates/fuzzer/rustlantis/mir/src/serialize.rs b/cuda-oxide/crates/fuzzer/rustlantis/mir/src/serialize.rs index dfd883406a..30d64a8e6d 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/mir/src/serialize.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/mir/src/serialize.rs @@ -157,6 +157,8 @@ impl Serialize for Operand { Operand::Copy(place) => place.serialize_value(tcx), Operand::Move(place) => format!("Move({})", place.serialize_value(tcx)), Operand::Constant(lit) => lit.serialize(tcx), + Operand::Static(id, _) => format!("Static({})", id.identifier()), + Operand::StaticMut(id, _) => format!("StaticMut({})", id.identifier()), } } } @@ -376,6 +378,19 @@ impl Program { program += Program::DUMPER; } + program.extend(self.statics.iter_enumerated().map(|(idx, decl)| { + let keyword = match decl.mutability { + Mutability::Not => "static", + Mutability::Mut => "static mut", + }; + format!( + "{keyword} {}: {} = {};\n", + idx.identifier(), + decl.ty.serialize(tcx), + decl.init.serialize(tcx) + ) + })); + program.extend(self.functions.iter_enumerated().map(|(idx, body)| { let args_list: String = body .args_iter() @@ -436,7 +451,7 @@ impl Program { #[cfg(test)] mod tests { - use super::Serialize; + use super::{CallSynatx, Serialize}; use crate::{syntax::*, tyctxt::TyCtxt}; use config::TyConfig; @@ -462,4 +477,41 @@ mod tests { let inf = Literal::Float(f32::INFINITY as f64, FloatTy::F32); assert_eq!(inf.serialize(&tcx), "f32::INFINITY"); } + + #[test] + fn serialize_static_operands() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let shared_ref = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut_ptr = tcx.push(TyKind::RawPtr(TyCtxt::I64, Mutability::Mut)); + let mut program = Program::new(false); + let immutable = program.push_static(StaticDecl { + ty: TyCtxt::I32, + mutability: Mutability::Not, + init: 1_i32.into(), + }); + let mutable = program.push_static(StaticDecl { + ty: TyCtxt::I64, + mutability: Mutability::Mut, + init: 2_i64.into(), + }); + + assert_eq!( + Operand::Static(immutable, shared_ref).serialize(&tcx), + "Static(static0)" + ); + assert_eq!( + Operand::StaticMut(mutable, mut_ptr).serialize(&tcx), + "StaticMut(static1)" + ); + + let mut body = Body::new(&[], TyCtxt::UNIT, true); + body.new_basic_block(BasicBlockData { + statements: vec![], + terminator: Terminator::Return, + }); + program.push_fn(body); + let source = program.serialize(&tcx, CallSynatx::V4); + assert!(source.contains("static static0: i32 = 1_i32;")); + assert!(source.contains("static mut static1: i64 = 2_i64;")); + } } diff --git a/cuda-oxide/crates/fuzzer/rustlantis/mir/src/syntax.rs b/cuda-oxide/crates/fuzzer/rustlantis/mir/src/syntax.rs index b67b9bec7b..3953a40c7f 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/mir/src/syntax.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/mir/src/syntax.rs @@ -7,11 +7,20 @@ use crate::tyctxt::TyCtxt; #[derive(Clone)] pub struct Program { + pub statics: IndexVec, pub functions: IndexVec, pub entry_args: Vec, pub use_debug_dumper: bool, } +define_index_type! {pub struct Static = u32;} +#[derive(Clone)] +pub struct StaticDecl { + pub ty: TyId, + pub mutability: Mutability, + pub init: Literal, +} + pub type LocalDecls = IndexVec; define_index_type! {pub struct Function = u32;} @@ -361,9 +370,10 @@ pub enum Operand { // define!("mir_move", fn Move(place: T) -> T); Move(Place), Constant(Literal), - // TODO: the following // define!("mir_static", fn Static(s: T) -> &'static T); + Static(Static, TyId), // define!("mir_static_mut", fn StaticMut(s: T) -> *mut T); + StaticMut(Static, TyId), } impl Operand { @@ -371,13 +381,14 @@ impl Operand { match self { Operand::Copy(place) | Operand::Move(place) => place.ty(local_decls, tcx), Operand::Constant(lit) => lit.ty(), + Operand::Static(_, ty) | Operand::StaticMut(_, ty) => *ty, } } pub fn place(&self) -> Option<&Place> { match self { Operand::Copy(place) | Operand::Move(place) => Some(place), - Operand::Constant(..) => None, + Operand::Constant(..) | Operand::Static(..) | Operand::StaticMut(..) => None, } } } @@ -926,12 +937,17 @@ impl Program { // A new, empty function pub fn new(debug: bool) -> Self { Self { + statics: IndexVec::default(), functions: IndexVec::default(), entry_args: vec![], use_debug_dumper: debug, } } + pub fn push_static(&mut self, decl: StaticDecl) -> Static { + self.statics.push(decl) + } + pub fn push_fn(&mut self, body: Body) -> Function { self.functions.push(body) } @@ -941,6 +957,12 @@ impl Program { } } +impl Static { + pub fn identifier(&self) -> String { + format!("static{}", self.index()) + } +} + impl Function { pub fn identifier(&self) -> String { format!("fn{}", self.index()) diff --git a/cuda-oxide/crates/fuzzer/tools/mir_generator.py b/cuda-oxide/crates/fuzzer/tools/mir_generator.py index 97756a0487..db63825d5c 100755 --- a/cuda-oxide/crates/fuzzer/tools/mir_generator.py +++ b/cuda-oxide/crates/fuzzer/tools/mir_generator.py @@ -26,6 +26,7 @@ from __future__ import annotations import argparse +import os import re import subprocess import sys @@ -43,6 +44,7 @@ max_fn_count = 1 max_args_count = 3 var_dump_chance = 1.0 +static_count = {static_count} tuple_max_len = 2 array_max_len = 2 struct_max_fields = 2 @@ -72,7 +74,9 @@ def run(cmd: list[str], *, cwd: Path) -> str: return proc.stdout -def generate_source(rustlantis_dir: Path, seed: int, *, build: bool) -> str: +def generate_source( + rustlantis_dir: Path, seed: int, *, build: bool, static_count: int = 0 +) -> str: if build: run(["cargo", "build", "-q", "-p", "generate"], cwd=rustlantis_dir) @@ -82,10 +86,20 @@ def generate_source(rustlantis_dir: Path, seed: int, *, build: bool) -> str: with tempfile.TemporaryDirectory(prefix="rustlantis-cuda-oxide-") as tmp: tmpdir = Path(tmp) - (tmpdir / "config.toml").write_text(TINY_CONFIG) + (tmpdir / "config.toml").write_text(TINY_CONFIG.format(static_count=static_count)) return run([str(generator), str(seed)], cwd=tmpdir) +GENERATED_STATIC_RE = re.compile( + r"(?m)^[ \t]*static(?:[ \t]+mut)?[ \t]+static\d+[ \t]*:[^\n;]+;[ \t]*$" +) + + +def generated_static_decls(source: str, *, before: int | None = None) -> list[str]: + prefix = source if before is None else source[:before] + return [match.group(0).strip() for match in GENERATED_STATIC_RE.finditer(prefix)] + + def extract_first_custom_mir_fn(source: str) -> str: start = source.find("#[custom_mir") if start < 0: @@ -107,7 +121,12 @@ def extract_first_custom_mir_fn(source: str) -> str: elif ch == "}": depth -= 1 if depth == 0: - return source[start : idx + 1] + fn_src = source[start : idx + 1] + statics = generated_static_decls(source, before=start) + if not statics: + return fn_src + static_prefix = "\n".join(statics) + return f"{static_prefix}\n\n{fn_src}" raise SystemExit("unterminated custom MIR function") @@ -386,6 +405,9 @@ def rewrite_dump(match: re.Match[str]) -> str: def generated_module(fn_src: str, fn_name: str, seed: int) -> str: adapted = adapt_function(fn_src, fn_name) + generated_lints = "unused_assignments, unused_parens, overflowing_literals" + if generated_static_decls(fn_src): + generated_lints += ", non_upper_case_globals" args = [literal_for_type(ty, idx) for idx, (_, ty) in enumerate(function_args(fn_src))] call_args = ", ".join(args) ret_ty = return_type(fn_src) @@ -420,7 +442,7 @@ def generated_module(fn_src: str, fn_name: str, seed: int) -> str: "// (explicit casts, redundant temps); clippy findings carry no", "// signal here, and checked-in cases sit inside the example's", "// `cargo clippy -- -D warnings` CI gate.", - "#![allow(unused_assignments, unused_parens, overflowing_literals)]", + f"#![allow({generated_lints})]", "#![allow(clippy::all)]", "", "use core::intrinsics::mir::*;", @@ -441,6 +463,15 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--seed", type=int, default=83) parser.add_argument("--fn-name", default="fn1") + parser.add_argument( + "--static-count", + type=int, + default=int(os.environ.get("RUSTLANTIS_STATIC_COUNT", "0")), + help=( + "number of scalar statics to mint; defaults to " + "RUSTLANTIS_STATIC_COUNT or 0" + ), + ) parser.add_argument("--rustlantis-dir", type=Path, default=DEFAULT_RUSTLANTIS_DIR) parser.add_argument("--output", type=Path) parser.add_argument("--no-build", action="store_true") @@ -450,8 +481,15 @@ def main() -> int: help="emit only the adapted custom-MIR function instead of a generated_case.rs module", ) args = parser.parse_args() - - source = generate_source(args.rustlantis_dir, args.seed, build=not args.no_build) + if args.static_count < 0: + parser.error("--static-count must be non-negative") + + source = generate_source( + args.rustlantis_dir, + args.seed, + build=not args.no_build, + static_count=args.static_count, + ) fn_src = extract_first_custom_mir_fn(source) adapted = ( adapt_function(fn_src, args.fn_name) From b648b635daebcaa1923e53fb711ddb044f5ccb6b Mon Sep 17 00:00:00 2001 From: Raul Estrada Date: Fri, 16 Oct 2026 01:16:01 -0600 Subject: [PATCH 2/2] fix(rustlantis): preserve reference provenance across copies and calls Signed-off-by: Raul Estrada --- cuda-oxide/crates/fuzzer/README.md | 29 ++ .../fuzzer/rustlantis/generate/src/pgraph.rs | 213 +++++++++++- cuda-oxide/crates/fuzzer/tools/miri_sweep.py | 303 ++++++++++++++++++ .../tools/tests/miri_static_provenance.rs | 77 +++++ .../tests/miri_static_provenance_invalid.rs | 17 + 5 files changed, 629 insertions(+), 10 deletions(-) create mode 100644 cuda-oxide/crates/fuzzer/tools/miri_sweep.py create mode 100644 cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance.rs create mode 100644 cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance_invalid.rs diff --git a/cuda-oxide/crates/fuzzer/README.md b/cuda-oxide/crates/fuzzer/README.md index 4e6a429e1a..003a18ec8d 100644 --- a/cuda-oxide/crates/fuzzer/README.md +++ b/cuda-oxide/crates/fuzzer/README.md @@ -226,3 +226,32 @@ results: seed 1437: PASS [run] CPU/GPU traces matched summary: PASS=1, UNSUPPORTED=1 ``` + +## Miri Tree Borrows regression sweep + +Run generated Rustlantis programs through Miri without requiring CUDA: + +```bash +python3 cuda-oxide/crates/fuzzer/tools/miri_sweep.py --start 358 --count 1 +python3 cuda-oxide/crates/fuzzer/tools/miri_sweep.py --count 500 +``` + +Before generating programs, the sweep runs two deterministic Miri +controls: a valid reference-provenance example that must succeed and +an invalid protected-reborrow example that must produce undefined behavior. + +The sweep also executes seed 5 with `static_count = 0`. +This is a regression for a pre-existing local-reference provenance bug: +the original generator produced an invalid reborrow rejected by Miri +Tree Borrows. Checking local-reference read permissions prevents the +invalid reference from being selected. An isolated ablation reproduced +both the original generated program and its Miri error. + +The sweep uses `nightly-2026-10-11`, enables `-Zmiri-tree-borrows`, +and generates programs with `static_count = 8`. + +Failures preserve the generated Rust source and Miri diagnostics under +`/tmp/rustlantis-miri-regressions/`. + +The reported reference-call coverage is a structural approximation; +it does not establish that every reference argument originates from a static. diff --git a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs index e32264494f..13f04c7ed0 100644 --- a/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs +++ b/cuda-oxide/crates/fuzzer/rustlantis/generate/src/pgraph.rs @@ -784,8 +784,8 @@ impl PlaceGraph { pointee.is_some_and(|pointee| self.is_static_allocation(pointee)); if points_into_static { - // Static-derived aliases must keep the source provenance. Remove - // the destination's old edge before installing the copied edge. + // Raw pointers preserve their provenance. Copied references + // receive a fresh child tag derived from the source tag. if let Some(old) = self.ref_edge(dst) { self.remove_edge(old); } @@ -793,6 +793,9 @@ impl PlaceGraph { && (!self.ty(src).is_ref(&self.tcx) || self.is_ref_valid(src)) { self.set_ref(dst, pointee, Some(src)); + if self.ty(dst).is_ref(&self.tcx) { + self.retag_static_copy(dst); + } } } else { // Preserve the legacy non-static behavior exactly so @@ -940,6 +943,37 @@ impl PlaceGraph { } } + /// Creates a child provenance tag when a static-derived reference + /// is copied or moved into another place. + fn retag_static_copy(&mut self, reference: PlaceIndex) { + let Some(pointee) = self.pointee(reference) else { + return; + }; + + if !self.is_static_allocation(pointee) { + return; + } + + let borrow_type = match self.ty(reference).kind(&self.tcx) { + TyKind::Ref(_, Mutability::Mut) => BorrowType::Exclusive, + TyKind::Ref(_, Mutability::Not) => BorrowType::Shared, + _ => return, + }; + + let parent = self.places[reference] + .tag + .expect("reference has a provenance tag"); + + assert!(self.tag_alive[parent]); + + let child = self.new_tag(BTreeSet::from([reference]), Some(parent)); + + assert!(self.pointer_tags[parent].remove(&reference)); + self.places[reference].tag = Some(child); + + self.add_borrow_for_place(pointee, borrow_type, child); + } + fn mark_ref_protected(&mut self, p: impl ToPlaceIndex) { let p = p.to_place_index(&self).expect("place exists"); assert!(self.ty(p).is_ref(&self.tcx)); @@ -1013,17 +1047,20 @@ impl PlaceGraph { return false; } - if !self.is_static_allocation(target) { - return true; - } - let Some(tag) = self.places[r].tag else { return false; }; + let requires_write = matches!(self.ty(r).kind(&self.tcx), TyKind::Ref(_, Mutability::Mut)); + let mut valid = true; self.visit_transitive_subfields(target, |node| { if let Some(run) = self.places[node].run_ptr { - if !self.memory.can_read_with(run, tag) { + let permitted = if requires_write { + self.memory.can_write_with(run, tag) + } else { + self.memory.can_read_with(run, tag) + }; + if !permitted { valid = false; return VisitAction::ShortCircuit; } @@ -2613,6 +2650,32 @@ mod tests { assert!(pt.can_acquire_static(static_id)); } + #[test] + fn invalidated_local_shared_reference_is_not_valid() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let shared_ty = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let local = Local::new(1); + let local_index = pt.allocate_local(local, TyCtxt::I32); + pt.mark_place_init(local_index); + + let reference = Local::new(2); + let reference_index = pt.allocate_local(reference, shared_ty); + pt.mark_place_init(reference_index); + pt.set_ref(reference_index, local_index, None); + + assert!(pt.contains_only_valid_ref(reference_index)); + + // A direct write invalidates the shared reference's provenance. + pt.place_written(&Place::from_local(local), None); + + assert!( + !pt.contains_only_valid_ref(reference_index), + "a reference invalidated by a direct write must not be selected" + ); + } + #[test] fn static_reacquisition_rejects_live_reference_argument() { let mut tcx = TyCtxt::from_primitives(TyConfig::default()); @@ -2642,10 +2705,70 @@ mod tests { let callee_arg = Local::new(1).to_place_index(&pt).unwrap(); assert_eq!(pt.pointee(callee_arg), Some(static_place)); - assert_eq!(pt.places[callee_arg].tag, Some(reference_tag)); + + let argument_tag = pt.places[callee_arg] + .tag + .expect("callee argument has a tag"); + + assert_ne!(argument_tag, reference_tag); + assert_eq!(pt.tag_parents[argument_tag], Some(reference_tag)); + assert!(pt.memory.tag_is_protected(argument_tag)); assert!(!pt.can_acquire_static(static_id)); } + fn check_static_argument_retag(mutability: Mutability, move_argument: bool) { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let reference_ty = tcx.push(TyKind::Ref(TyCtxt::I32, mutability)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let caller = Body::new(&[], TyCtxt::UNIT, false); + pt.enter_fn0(&caller); + + let static_id = Static::new(0); + let static_place = + pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let source = Local::new(1); + let source_index = pt.allocate_local(source, reference_ty); + pt.mark_place_init(source_index); + pt.set_ref(source_index, static_place, None); + + let source_tag = pt.places[source_index].tag.expect("source has a tag"); + + let argument = if move_argument { + Operand::Move(Place::from_local(source)) + } else { + Operand::Copy(Place::from_local(source)) + }; + + let callee = Body::new(&[reference_ty], TyCtxt::UNIT, false); + pt.enter_fn(&callee, &[argument], &Place::RETURN_SLOT); + + let argument_index = Local::new(1) + .to_place_index(&pt) + .expect("callee argument exists"); + let argument_tag = pt.places[argument_index] + .tag + .expect("callee argument has a tag"); + + assert_ne!( + argument_tag, source_tag, + "function arguments must receive fresh provenance tags" + ); + assert_eq!(pt.tag_parents[argument_tag], Some(source_tag)); + assert!(pt.memory.tag_is_protected(argument_tag)); + } + + #[test] + fn provenance_static_move_creates_child_tag() { + check_static_argument_retag(Mutability::Mut, true); + } + + #[test] + fn provenance_static_copy_creates_child_tag() { + check_static_argument_retag(Mutability::Not, false); + } + #[test] fn static_reference_argument_protection_ends_at_frame_exit() { let mut tcx = TyCtxt::from_primitives(TyConfig::default()); @@ -2749,6 +2872,73 @@ mod tests { pt.set_ref_from_source(derived_p, static_place, Some(shared_p)); } + #[test] + fn protected_shared_borrow_blocks_mutable_reference_transfer() { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let mutable_ty = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Mut)); + let shared_ty = tcx.push(TyKind::Ref(TyCtxt::I32, Mutability::Not)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_id = Static::new(0); + let pointee = pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let mutable = pt.allocate_local(Local::new(1), mutable_ty); + pt.mark_place_init(mutable); + pt.set_ref(mutable, pointee, None); + + let shared = pt.allocate_local(Local::new(2), shared_ty); + pt.mark_place_init(shared); + pt.set_ref(shared, pointee, None); + + let shared_tag = pt.places[shared].tag.expect("shared tag"); + pt.memory.protect_tag(shared_tag); + + assert!(pt.can_read_through(mutable, pointee)); + assert!(!pt.can_write_through(mutable, pointee)); + + assert!( + !pt.contains_only_valid_ref(mutable), + "a mutable reference without write permission must not be transferred" + ); + } + + #[test] + fn static_reference_assignment_creates_child_provenance() { + for mutability in [Mutability::Not, Mutability::Mut] { + let mut tcx = TyCtxt::from_primitives(TyConfig::default()); + let reference_ty = tcx.push(TyKind::Ref(TyCtxt::I32, mutability)); + let mut pt = PlaceGraph::new(Rc::new(tcx)); + + let static_id = Static::new(0); + let pointee = pt.allocate_static(static_id, TyCtxt::I32, Mutability::Mut, 7_i32.into()); + + let source = Local::new(1); + let source_index = pt.allocate_local(source, reference_ty); + pt.mark_place_init(source_index); + pt.set_ref(source_index, pointee, None); + + let destination = Local::new(2); + let destination_index = pt.allocate_local(destination, reference_ty); + pt.mark_place_init(destination_index); + + let parent = pt.places[source_index].tag.expect("source tag"); + pt.copy_place(destination_index, source_index); + + let child = pt.places[destination_index].tag.expect("child tag"); + + assert_ne!(child, parent); + assert_eq!(pt.tag_parents[child], Some(parent)); + assert_eq!(pt.pointee(destination_index), Some(pointee)); + + if mutability == Mutability::Mut { + pt.mark_place_moved(source_index); + assert!(pt.can_write_through(destination_index, pointee)); + } else { + assert!(pt.can_read_through(destination_index, pointee)); + } + } + } + #[test] fn copied_static_reborrow_keeps_source_permission_until_last_copy_dies() { let mut tcx = TyCtxt::from_primitives(TyConfig::default()); @@ -2778,7 +2968,9 @@ mod tests { let copied_p = pt.allocate_local(copied, t_shared_ref); pt.mark_place_init(copied_p); pt.copy_place(copied_p, derived_p); - assert_eq!(pt.places[copied_p].tag, Some(derived_tag)); + let copied_tag = pt.places[copied_p].tag.expect("copied tag"); + assert_ne!(copied_tag, derived_tag); + assert_eq!(pt.tag_parents[copied_tag], Some(derived_tag)); pt.set_static_ref(source_p, static_shared); @@ -2832,7 +3024,8 @@ mod tests { let returned_p = returned.to_place_index(&pt).unwrap(); assert_eq!(pt.pointee(returned_p), Some(static_place)); - assert_eq!(pt.places[returned_p].tag, Some(derived_tag)); + let returned_tag = pt.places[returned_p].tag.expect("returned tag"); + assert_ne!(returned_tag, derived_tag); assert!(pt.tag_alive[source_tag]); assert!(pt.tag_alive[derived_tag]); assert!(pt.can_read_through(returned_p, static_place)); diff --git a/cuda-oxide/crates/fuzzer/tools/miri_sweep.py b/cuda-oxide/crates/fuzzer/tools/miri_sweep.py new file mode 100644 index 0000000000..5fa33e7f4f --- /dev/null +++ b/cuda-oxide/crates/fuzzer/tools/miri_sweep.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +"""Validate generated Rustlantis programs with Miri Tree Borrows.""" + +import argparse +import os +import re +import subprocess +import tempfile +from pathlib import Path + +RUSTLANTIS = Path(__file__).resolve().parent.parent / "rustlantis" + +CONFIG = """ +bb_max_len = 12 +max_switch_targets = 3 +max_bb_count = 6 +max_bb_count_hard = 12 +max_fn_count = 8 +max_args_count = 6 +var_dump_chance = 0.0 +static_count = 8 +tuple_max_len = 3 +array_max_len = 3 +struct_max_fields = 3 +adt_max_variants = 2 +composite_count = 8 +adt_count = 0 + +[backends] +""" + + +def execute(command, cwd, env): + try: + return subprocess.run( + command, + cwd=cwd, + env=env, + capture_output=True, + text=True, + timeout=180, + check=False, + ) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess( + command, 124, "", "Process timed out after 180 seconds." + ) + + +def has_reference_call(source): + signatures = re.findall( + r"(?m)^\s*(?:pub\s+)?fn\s+(fn\d+)\((.*?)\)\s*->", + source, + ) + reference_functions = { + name for name, parameters in signatures + if "&'static " in parameters + } + called_functions = set( + re.findall(r"\bCall\([^\n]*\b(fn\d+)\(", source) + ) + return bool(reference_functions & called_functions) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--start", type=int, default=0) + parser.add_argument("--count", type=int, default=20) + parser.add_argument("--toolchain", default="nightly-2026-10-11") + parser.add_argument( + "--artifacts", + type=Path, + default=Path("/tmp/rustlantis-miri-regressions"), + ) + args = parser.parse_args() + + if args.start < 0 or args.count < 1: + parser.error("--start must be nonnegative and --count must be positive") + + env = os.environ.copy() + env.pop("RUSTFLAGS", None) + env.pop("RUSTC_WRAPPER", None) + + miri_env = env.copy() + miri_env["MIRIFLAGS"] = "-Zmiri-tree-borrows" + + passed = 0 + static_programs = 0 + reference_call_programs = 0 + overlapping_programs = 0 + + with tempfile.TemporaryDirectory(prefix="rustlantis-miri-") as directory: + work = Path(directory) + (work / "src").mkdir() + (work / "config.toml").write_text(CONFIG) + (work / "Cargo.toml").write_text( + '[package]\n' + 'name = "rustlantis-miri-sweep"\n' + 'version = "0.1.0"\n' + 'edition = "2024"\n' + ) + + negative_fixture = ( + Path(__file__).resolve().parent + / "tests" + / "miri_static_provenance_invalid.rs" + ) + + (work / "src/main.rs").write_text(negative_fixture.read_text()) + + negative_result = execute( + [ + "cargo", f"+{args.toolchain}", "miri", + "run", "--quiet", + "--manifest-path", str(work / "Cargo.toml"), + ], + work, + miri_env, + ) + + if ( + negative_result.returncode == 0 + or "Undefined Behavior" not in negative_result.stderr + ): + print("FAIL: Miri did not detect the expected provenance violation") + print(negative_result.stderr[-3000:]) + return 1 + + print("Miri negative control: PASS (UB detected)", flush=True) + + fixture_path = ( + Path(__file__).resolve().parent + / "tests" + / "miri_static_provenance.rs" + ) + + (work / "src/main.rs").write_text(fixture_path.read_text()) + + fixture_result = execute( + [ + "cargo", f"+{args.toolchain}", "miri", + "run", "--quiet", + "--manifest-path", str(work / "Cargo.toml"), + ], + work, + miri_env, + ) + + if fixture_result.returncode: + args.artifacts.mkdir(parents=True, exist_ok=True) + log = args.artifacts / "miri_static_provenance.log" + log.write_text(fixture_result.stdout + fixture_result.stderr) + print("FAIL: deterministic provenance fixture") + print(f"Log: {log}") + print(fixture_result.stderr[-4000:]) + return 1 + + print("Deterministic provenance fixture: PASS", flush=True) + + # Regression: seed 5 previously generated an invalid local + # reference reborrow even with static generation disabled. + assert CONFIG.count("static_count = 8\n") == 1 + zero_static_config = CONFIG.replace("static_count = 8\n", "") + (work / "config.toml").write_text(zero_static_config) + + zero_generated = execute( + [ + "cargo", f"+{args.toolchain}", "run", "--quiet", + "--manifest-path", str(RUSTLANTIS / "Cargo.toml"), + "-p", "generate", "--", "--debug", "5", + ], + work, + env, + ) + + zero_source = zero_generated.stdout + + unexpected_static = bool( + re.search(r"\bStatic(?:Mut)?\(static\d+\)", zero_source) + or re.search( + r"(?m)^\s*static(?:\s+mut)?\s+static\d+\s*:", + zero_source, + ) + ) + + if zero_generated.returncode: + zero_result = zero_generated + zero_stage = "generator" + elif unexpected_static: + zero_result = subprocess.CompletedProcess( + [], + 1, + "", + "Unexpected static declaration or operand with static_count=0", + ) + zero_stage = "configuration" + else: + (work / "src/main.rs").write_text(zero_source) + + zero_result = execute( + [ + "cargo", f"+{args.toolchain}", "miri", + "run", "--quiet", + "--manifest-path", str(work / "Cargo.toml"), + ], + work, + miri_env, + ) + zero_stage = "miri" + + if zero_result.returncode: + args.artifacts.mkdir(parents=True, exist_ok=True) + + (args.artifacts / "seed-5-zero-static.rs").write_text( + zero_source + ) + + (args.artifacts / "seed-5-zero-static.log").write_text( + zero_result.stdout + zero_result.stderr + ) + + print( + f"FAIL: zero-static seed 5 regression " + f"(stage={zero_stage})" + ) + print(zero_result.stderr[-3000:]) + return 1 + + print("Zero-static seed 5 regression: PASS", flush=True) + + # Restore the configured static workload for the requested sweep. + (work / "config.toml").write_text(CONFIG) + + for seed in range(args.start, args.start + args.count): + generated = execute( + [ + "cargo", f"+{args.toolchain}", "run", "--quiet", + "--manifest-path", str(RUSTLANTIS / "Cargo.toml"), + "-p", "generate", "--", "--debug", str(seed), + ], + work, + env, + ) + + source = generated.stdout + if generated.returncode: + failure = generated + stage = "generator" + else: + uses_static = bool( + re.search(r"\bStatic(?:Mut)?\(static\d+\)", source) + ) + calls_with_refs = has_reference_call(source) + + static_programs += uses_static + reference_call_programs += calls_with_refs + overlapping_programs += uses_static and calls_with_refs + + (work / "src/main.rs").write_text(source) + + failure = execute( + [ + "cargo", f"+{args.toolchain}", "miri", + "run", "--quiet", + "--manifest-path", str(work / "Cargo.toml"), + ], + work, + miri_env, + ) + stage = "miri" + + if failure.returncode: + args.artifacts.mkdir(parents=True, exist_ok=True) + prefix = args.artifacts / f"seed-{seed}" + + if source: + prefix.with_suffix(".rs").write_text(source) + + prefix.with_suffix(".log").write_text( + failure.stdout + failure.stderr + ) + + print(f"FAIL: seed={seed}, stage={stage}") + print(f"Artifacts: {prefix}") + print(failure.stderr[-2500:]) + break + + passed += 1 + + if passed % 25 == 0: + print(f"Miri PASS: {passed}/{args.count}", flush=True) + + print("\n=== MIRI SWEEP SUMMARY ===") + print(f"Passed: {passed}/{args.count}") + print(f"Programs using static operands: {static_programs}") + print(f"Programs calling functions with reference parameters: {reference_call_programs}") + print(f"Programs containing both features: {overlapping_programs}") + + return 0 if passed == args.count else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance.rs b/cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance.rs new file mode 100644 index 0000000000..3c1ff04b0d --- /dev/null +++ b/cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance.rs @@ -0,0 +1,77 @@ +#![feature(custom_mir, core_intrinsics)] +#![allow(internal_features)] + +use core::intrinsics::mir::*; + +static SHARED: i32 = 21; +static mut MUTABLE: i32 = 5; + +#[inline(never)] +fn read_shared(value: &'static i32) -> i32 { + *value +} + +#[inline(never)] +fn update_mutable(value: &'static mut i32) -> i32 { + *value += 1; + *value +} + +#[custom_mir(dialect = "runtime", phase = "initial")] +fn shared_reference_flow() -> i32 { + mir! { + type RET = i32; + + let _1: &'static i32; + let _2: &'static i32; + let _3: &'static i32; + + { + _1 = Static(SHARED); + _2 = _1; + _3 = Move(_2); + + Call( + RET = read_shared(Move(_3)), + ReturnTo(bb1), + UnwindUnreachable() + ) + } + + bb1 = { + Return() + } + } +} + +#[custom_mir(dialect = "runtime", phase = "initial")] +fn mutable_reference_flow() -> i32 { + mir! { + type RET = i32; + + let _1: *mut i32; + let _2: &'static mut i32; + let _3: &'static mut i32; + + { + _1 = StaticMut(MUTABLE); + _2 = &mut (*_1); + _3 = Move(_2); + + Call( + RET = update_mutable(Move(_3)), + ReturnTo(bb1), + UnwindUnreachable() + ) + } + + bb1 = { + Return() + } + } +} + +fn main() { + assert_eq!(shared_reference_flow(), 21); + assert_eq!(mutable_reference_flow(), 6); +} diff --git a/cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance_invalid.rs b/cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance_invalid.rs new file mode 100644 index 0000000000..423d446056 --- /dev/null +++ b/cuda-oxide/crates/fuzzer/tools/tests/miri_static_provenance_invalid.rs @@ -0,0 +1,17 @@ +static mut VALUE: i32 = 0; + +#[inline(never)] +fn callee(child: &mut i32, root: *mut i32) { + unsafe { + *root = 1; + } + + *child = 2; +} + +fn main() { + let root = &raw mut VALUE; + let child = unsafe { &mut *root }; + + callee(child, root); +}