Skip to content

Commit 18143a8

Browse files
author
kulst
committed
Ensure that static initializers are acyclic for targets that require this
Some targets (for now only NVPTX) do not support cycles in static initializers (see #146787). LLVM produces an error when attempting to codegen such constructs (like self referential structs). This PR attempts to instead error on Rust side before reaching codegen to not produce LLVM UB. This is achieved by computing a new query in rustc_const_eval. It is executed as a required analysis depending on a new flag in TargetOptions. The check 1. organizes all local static items in a DirectedGraph where pointers / references to other local static items represent the edges 2. calculates the strongly connected components (SCCs) of the graph 3. checks for cycles (more than one node in a SCC)
1 parent b49c7d7 commit 18143a8

File tree

8 files changed

+209
-1
lines changed

8 files changed

+209
-1
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
2+
use rustc_data_structures::graph::scc::Sccs;
3+
use rustc_data_structures::graph::{DirectedGraph, Successors};
4+
use rustc_hir as hir;
5+
use rustc_index::{IndexVec, newtype_index};
6+
use rustc_middle::mir::interpret::{AllocId, GlobalAlloc};
7+
use rustc_middle::ty::TyCtxt;
8+
use rustc_span::ErrorGuaranteed;
9+
use rustc_span::def_id::LocalDefId;
10+
11+
// --- graph indices
12+
newtype_index! {
13+
struct StaticNodeIdx {}
14+
}
15+
newtype_index! {
16+
#[derive(Ord, PartialOrd)]
17+
struct StaticSccIdx {}
18+
}
19+
20+
// --- adjacency-list graph for rustc_data_structures::graph::scc
21+
struct StaticRefGraph {
22+
succ: IndexVec<StaticNodeIdx, Vec<StaticNodeIdx>>,
23+
}
24+
25+
impl DirectedGraph for StaticRefGraph {
26+
type Node = StaticNodeIdx;
27+
28+
fn num_nodes(&self) -> usize {
29+
self.succ.len()
30+
}
31+
}
32+
33+
impl Successors for StaticRefGraph {
34+
fn successors(&self, n: StaticNodeIdx) -> impl Iterator<Item = StaticNodeIdx> {
35+
self.succ[n].iter().copied()
36+
}
37+
}
38+
39+
pub(crate) fn check_static_initializer_acyclic(
40+
tcx: TyCtxt<'_>,
41+
_: (),
42+
) -> Result<(), ErrorGuaranteed> {
43+
// Collect local statics
44+
let mut statics: Vec<LocalDefId> = Vec::new();
45+
for item_id in tcx.hir_free_items() {
46+
let item = tcx.hir_item(item_id);
47+
if matches!(item.kind, hir::ItemKind::Static(..)) {
48+
statics.push(item.owner_id.def_id);
49+
}
50+
}
51+
52+
// Fast path
53+
if statics.is_empty() {
54+
return Ok(());
55+
}
56+
57+
// Map statics to dense node indices
58+
let mut node_of: FxHashMap<LocalDefId, StaticNodeIdx> = FxHashMap::default();
59+
let mut def_of: IndexVec<StaticNodeIdx, LocalDefId> = IndexVec::new();
60+
for &def_id in statics.iter() {
61+
let idx = def_of.push(def_id);
62+
node_of.insert(def_id, idx);
63+
}
64+
65+
let mut graph = StaticRefGraph { succ: IndexVec::from_elem_n(Vec::new(), def_of.len()) };
66+
67+
// Build edges by evaluating each static initializer and scanning provenance
68+
for &from_def in &statics {
69+
let from_node = node_of[&from_def];
70+
71+
// If const-eval already errored for this static, skip (we don't want noisy follow-ups).
72+
let Ok(root_alloc) = tcx.eval_static_initializer(from_def) else {
73+
continue;
74+
};
75+
76+
let mut out_edges: FxHashSet<StaticNodeIdx> = FxHashSet::default();
77+
collect_referenced_local_statics(tcx, root_alloc, &node_of, &mut out_edges);
78+
#[allow(rustc::potential_query_instability)]
79+
graph.succ[from_node].extend(out_edges);
80+
}
81+
82+
// 4) SCCs
83+
let sccs: Sccs<StaticNodeIdx, StaticSccIdx> = Sccs::new(&graph); // :contentReference[oaicite:4]{index=4}
84+
85+
// Group members by SCC
86+
let mut members: IndexVec<StaticSccIdx, Vec<StaticNodeIdx>> =
87+
IndexVec::from_elem_n(Vec::new(), sccs.num_sccs());
88+
89+
for i in 0..def_of.len() {
90+
let n = StaticNodeIdx::from_usize(i);
91+
members[sccs.scc(n)].push(n);
92+
}
93+
94+
// 5) Emit errors for cyclic SCCs
95+
let mut first_guar: Option<ErrorGuaranteed> = None;
96+
97+
for scc in sccs.all_sccs() {
98+
let nodes = &members[scc];
99+
if nodes.is_empty() {
100+
continue;
101+
}
102+
103+
let is_cycle = nodes.len() > 1
104+
|| (nodes.len() == 1 && graph.successors(nodes[0]).any(|x| x == nodes[0]));
105+
if !is_cycle {
106+
continue;
107+
}
108+
109+
let head_def = def_of[nodes[0]];
110+
let head_span = tcx.def_span(head_def);
111+
112+
let mut diag = tcx.dcx().struct_span_err(
113+
head_span,
114+
format!(
115+
"static initializer forms a cycle involving `{}`",
116+
tcx.def_path_str(head_def.to_def_id()),
117+
),
118+
);
119+
120+
for &n in nodes {
121+
let d = def_of[n];
122+
diag.span_label(tcx.def_span(d), "part of this cycle");
123+
}
124+
125+
diag.note(format!(
126+
"cyclic static initializer references are not supported for target `{}`",
127+
tcx.sess.target.llvm_target
128+
));
129+
130+
let guar = diag.emit();
131+
first_guar.get_or_insert(guar);
132+
}
133+
134+
match first_guar {
135+
Some(g) => Err(g),
136+
None => Ok(()),
137+
}
138+
}
139+
140+
// Traverse allocations reachable from the static initializer allocation and collect local-static targets.
141+
fn collect_referenced_local_statics<'tcx>(
142+
tcx: TyCtxt<'tcx>,
143+
root_alloc: rustc_middle::mir::interpret::ConstAllocation<'tcx>,
144+
node_of: &FxHashMap<LocalDefId, StaticNodeIdx>,
145+
out: &mut FxHashSet<StaticNodeIdx>,
146+
) {
147+
let mut stack: Vec<AllocId> = Vec::new();
148+
let mut seen: FxHashSet<AllocId> = FxHashSet::default();
149+
150+
// Scan the root allocation for pointers first.
151+
push_ptr_alloc_ids(root_alloc.inner(), &mut stack);
152+
153+
while let Some(alloc_id) = stack.pop() {
154+
if !seen.insert(alloc_id) {
155+
continue;
156+
}
157+
158+
match tcx.global_alloc(alloc_id) {
159+
GlobalAlloc::Static(def_id) => {
160+
if let Some(local_def) = def_id.as_local()
161+
&& let Some(&node) = node_of.get(&local_def)
162+
{
163+
out.insert(node);
164+
}
165+
}
166+
167+
GlobalAlloc::Memory(const_alloc) => {
168+
push_ptr_alloc_ids(const_alloc.inner(), &mut stack);
169+
}
170+
171+
_ => {
172+
// Functions, vtables, etc: ignore
173+
}
174+
}
175+
}
176+
}
177+
178+
// Extract all AllocIds referenced by pointers in this allocation via provenance.
179+
fn push_ptr_alloc_ids(alloc: &rustc_middle::mir::interpret::Allocation, stack: &mut Vec<AllocId>) {
180+
for (_, prov) in alloc.provenance().ptrs().iter() {
181+
stack.push(prov.alloc_id());
182+
}
183+
}

compiler/rustc_const_eval/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// tidy-alphabetical-end
1616

1717
pub mod check_consts;
18+
mod check_static_initializer_acyclic;
1819
pub mod const_eval;
1920
mod errors;
2021
pub mod interpret;
@@ -48,6 +49,8 @@ pub fn provide(providers: &mut Providers) {
4849
};
4950
providers.hooks.validate_scalar_in_layout =
5051
|tcx, scalar, layout| util::validate_scalar_in_layout(tcx, scalar, layout);
52+
providers.check_static_initializer_acyclic =
53+
check_static_initializer_acyclic::check_static_initializer_acyclic;
5154
}
5255

5356
/// `rustc_driver::main` installs a handler that will set this to `true` if

compiler/rustc_interface/src/passes.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,10 @@ fn run_required_analyses(tcx: TyCtxt<'_>) {
11331133
}
11341134
});
11351135
});
1136-
1136+
// NEW: target-gated pre-codegen error
1137+
if tcx.sess.target.options.requires_static_initializer_acyclic {
1138+
tcx.ensure_ok().check_static_initializer_acyclic(());
1139+
}
11371140
sess.time("layout_testing", || layout_test::test_layout(tcx));
11381141
sess.time("abi_testing", || abi_test::test_abi(tcx));
11391142
}

compiler/rustc_middle/src/query/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ rustc_queries! {
258258
desc { |tcx| "getting HIR parent of `{}`", tcx.def_path_str(key) }
259259
}
260260

261+
query check_static_initializer_acyclic(_: ()) -> Result<(), ErrorGuaranteed> {
262+
desc { "checking that static initializers are acyclic" }
263+
}
261264
/// Gives access to the HIR nodes and bodies inside `key` if it's a HIR owner.
262265
///
263266
/// This can be conveniently accessed by `tcx.hir_*` methods.

compiler/rustc_target/src/spec/json.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ impl Target {
163163
forward!(relro_level);
164164
forward!(archive_format);
165165
forward!(allow_asm);
166+
forward!(requires_static_initializer_acyclic);
166167
forward!(main_needs_argc_argv);
167168
forward!(has_thread_local);
168169
forward!(obj_is_bitcode);
@@ -360,6 +361,7 @@ impl ToJson for Target {
360361
target_option_val!(relro_level);
361362
target_option_val!(archive_format);
362363
target_option_val!(allow_asm);
364+
target_option_val!(requires_static_initializer_acyclic);
363365
target_option_val!(main_needs_argc_argv);
364366
target_option_val!(has_thread_local);
365367
target_option_val!(obj_is_bitcode);
@@ -581,6 +583,7 @@ struct TargetSpecJson {
581583
relro_level: Option<RelroLevel>,
582584
archive_format: Option<StaticCow<str>>,
583585
allow_asm: Option<bool>,
586+
requires_static_initializer_acyclic: Option<bool>,
584587
main_needs_argc_argv: Option<bool>,
585588
has_thread_local: Option<bool>,
586589
obj_is_bitcode: Option<bool>,

compiler/rustc_target/src/spec/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2394,6 +2394,9 @@ pub struct TargetOptions {
23942394
pub archive_format: StaticCow<str>,
23952395
/// Is asm!() allowed? Defaults to true.
23962396
pub allow_asm: bool,
2397+
/// Static initializers must be acyclic.
2398+
/// Defaults to false
2399+
pub requires_static_initializer_acyclic: bool,
23972400
/// Whether the runtime startup code requires the `main` function be passed
23982401
/// `argc` and `argv` values.
23992402
pub main_needs_argc_argv: bool,
@@ -2777,6 +2780,7 @@ impl Default for TargetOptions {
27772780
archive_format: "gnu".into(),
27782781
main_needs_argc_argv: true,
27792782
allow_asm: true,
2783+
requires_static_initializer_acyclic: false,
27802784
has_thread_local: false,
27812785
obj_is_bitcode: false,
27822786
min_atomic_width: None,

compiler/rustc_target/src/spec/targets/nvptx64_nvidia_cuda.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ pub(crate) fn target() -> Target {
5959
// Support using `self-contained` linkers like the llvm-bitcode-linker
6060
link_self_contained: LinkSelfContainedDefault::True,
6161

62+
// Static initializers must not have cycles on this target
63+
requires_static_initializer_acyclic: true,
64+
6265
..Default::default()
6366
},
6467
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//@ only-nvptx64
2+
//@ ignore-backends: gcc
3+
#![crate_type = "rlib"]
4+
#![no_std]
5+
struct Bar(&'static Bar);
6+
static FOO: Bar = Bar(&FOO); //~ ERROR static initializer forms a cycle involving `FOO`

0 commit comments

Comments
 (0)