Skip to content

Commit 639e93f

Browse files
committed
Ensure that static initializers are acyclic for NVPTX
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 639e93f

9 files changed

Lines changed: 211 additions & 2 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use rustc_data_structures::fx::FxIndexSet;
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, Allocation, ConstAllocation, 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
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 statics: FxIndexSet<LocalDefId> = tcx
45+
.hir_free_items()
46+
.filter_map(|item_id| {
47+
let item = tcx.hir_item(item_id);
48+
match item.kind {
49+
hir::ItemKind::Static(..) => Some(item.owner_id.def_id),
50+
_ => None,
51+
}
52+
})
53+
.collect();
54+
55+
// Fast path
56+
if statics.is_empty() {
57+
return Ok(());
58+
}
59+
60+
// For all statics collect all reachable statics to create a graph
61+
let graph = StaticRefGraph {
62+
succ: statics
63+
.iter()
64+
.map(|&id| {
65+
if let Ok(root_alloc) = tcx.eval_static_initializer(id) {
66+
collect_referenced_local_statics(tcx, root_alloc, &statics)
67+
} else {
68+
Vec::new()
69+
}
70+
})
71+
.collect(),
72+
};
73+
74+
// Calculate all SCCs from the graph
75+
let sccs: Sccs<StaticNodeIdx, StaticSccIdx> = Sccs::new(&graph);
76+
// Group statics by SCCs
77+
let mut members: IndexVec<StaticSccIdx, Vec<StaticNodeIdx>> =
78+
IndexVec::from_elem_n(Vec::new(), sccs.num_sccs());
79+
for i in graph.succ.indices() {
80+
members[sccs.scc(i)].push(i);
81+
}
82+
let mut first_guar: Option<ErrorGuaranteed> = None;
83+
84+
for scc in sccs.all_sccs() {
85+
let nodes = &members[scc];
86+
let acyclic = match nodes.len() {
87+
0 => true,
88+
1 => !graph.successors(nodes[0]).any(|x| x == nodes[0]),
89+
2.. => false,
90+
};
91+
92+
if acyclic {
93+
continue;
94+
}
95+
96+
let head_def = statics.get_index(nodes[0].into()).unwrap();
97+
let head_span = tcx.def_span(*head_def);
98+
99+
let mut diag = tcx.dcx().struct_span_err(
100+
head_span,
101+
format!(
102+
"static initializer forms a cycle involving `{}`",
103+
tcx.def_path_str(head_def.to_def_id()),
104+
),
105+
);
106+
diag.span_labels(
107+
nodes.iter().map(|&n| tcx.def_span(*statics.get_index(n.into()).unwrap())),
108+
"part of this cycle",
109+
)
110+
.note(format!(
111+
"cyclic static initializer references are not supported for target `{}`",
112+
tcx.sess.target.llvm_target
113+
));
114+
first_guar.get_or_insert(diag.emit());
115+
}
116+
117+
match first_guar {
118+
Some(g) => Err(g),
119+
None => Ok(()),
120+
}
121+
}
122+
123+
// Traverse allocations reachable from the static initializer allocation and collect local-static targets.
124+
fn collect_referenced_local_statics<'tcx>(
125+
tcx: TyCtxt<'tcx>,
126+
root_alloc: ConstAllocation<'tcx>,
127+
node_of: &FxIndexSet<LocalDefId>,
128+
) -> Vec<StaticNodeIdx> {
129+
let mut nodes: Vec<StaticNodeIdx> = Vec::default();
130+
let mut alloc_ids: FxIndexSet<AllocId> = FxIndexSet::default();
131+
132+
let add_ids_from_alloc = |alloc: &Allocation, ids: &mut FxIndexSet<AllocId>| {
133+
ids.extend(alloc.provenance().ptrs().iter().map(|(_, prov)| prov.alloc_id()));
134+
};
135+
136+
// Scan the root allocation for pointers first.
137+
add_ids_from_alloc(root_alloc.inner(), &mut alloc_ids);
138+
139+
let mut visited_allocs: usize = 0;
140+
141+
while let Some(&alloc_id) = alloc_ids.get_index(visited_allocs) {
142+
match tcx.global_alloc(alloc_id) {
143+
GlobalAlloc::Static(def_id) => {
144+
if let Some(local_def) = def_id.as_local()
145+
&& let Some(node) = node_of.get_index_of(&local_def)
146+
{
147+
nodes.push(node.into());
148+
}
149+
}
150+
151+
GlobalAlloc::Memory(const_alloc) => {
152+
add_ids_from_alloc(const_alloc.inner(), &mut alloc_ids);
153+
}
154+
155+
_ => {
156+
// Functions, vtables, etc: ignore
157+
}
158+
}
159+
visited_allocs += 1;
160+
}
161+
nodes
162+
}

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,9 @@ fn run_required_analyses(tcx: TyCtxt<'_>) {
11331133
}
11341134
});
11351135
});
1136-
1136+
if tcx.sess.target.options.static_initializer_must_be_acyclic {
1137+
tcx.ensure_ok().check_static_initializer_acyclic(());
1138+
}
11371139
sess.time("layout_testing", || layout_test::test_layout(tcx));
11381140
sess.time("abi_testing", || abi_test::test_abi(tcx));
11391141
}

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!(static_initializer_must_be_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!(static_initializer_must_be_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+
static_initializer_must_be_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 static_initializer_must_be_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+
static_initializer_must_be_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+
static_initializer_must_be_acyclic: true,
64+
6265
..Default::default()
6366
},
6467
}

tests/auxiliary/minicore.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ impl Neg for isize {
211211
}
212212

213213
#[lang = "sync"]
214-
trait Sync {}
214+
pub trait Sync {}
215215
impl_marker_trait!(
216216
Sync => [
217217
char, bool,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@ add-minicore
2+
//@ needs-llvm-components: nvptx
3+
//@ compile-flags: --target=nvptx64-nvidia-cuda
4+
//@ ignore-backends: gcc
5+
#![crate_type = "rlib"]
6+
#![feature(no_core)]
7+
#![no_std]
8+
#![no_core]
9+
10+
extern crate minicore;
11+
use minicore::*;
12+
13+
struct Foo(&'static Foo);
14+
impl Sync for Foo {}
15+
16+
static A: Foo = Foo(&A); //~ ERROR static initializer forms a cycle involving `A`
17+
18+
static B0: Foo = Foo(&B1); //~ ERROR static initializer forms a cycle involving `B0`
19+
static B1: Foo = Foo(&B0);
20+
21+
static C0: Foo = Foo(&C1); //~ ERROR static initializer forms a cycle involving `C0`
22+
static C1: Foo = Foo(&C2);
23+
static C2: Foo = Foo(&C0);
24+
25+
struct Bar(&'static u32);
26+
impl Sync for Bar {}
27+
28+
static BAR: Bar = Bar(&INT);
29+
static INT: u32 = 42u32;

0 commit comments

Comments
 (0)