Skip to content

Commit 3a00ebb

Browse files
committed
new function pointer identity rule
1 parent 1b00276 commit 3a00ebb

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

src/coding-guidelines/types-and-traits.rst

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,216 @@ Types and Traits
6363
current.checked_add(velocity).expect("Position calculation overflowed")
6464
}
6565
66+
.. guideline:: Do Not Depend on Function Pointer Identity Across Crates
67+
:id: gui_QbvIknd9qNF6
68+
:category: required
69+
:status: draft
70+
:release: unclear-latest
71+
:fls: fls_1kg1mknf4yx7
72+
:decidability: decidable
73+
:scope: system
74+
:tags: surprising-behavior
75+
76+
Do not rely on the equality or stable identity of function pointers originating from different crates or that may be inlined, duplicated, or instantiated differently across compilation units, codegen units, or optimization profiles.
77+
78+
Avoid assumptions about low-level metadata (such as symbol addresses) unless explicitly guaranteed by the Ferrocene Language Specification (FLS). Function address identity is not guaranteed by Rust and must not be treated as stable. Rust’s fn type is a zero-sized function item promoted to a function pointer, whose address is determined by the compiler backend. When a function resides in a different crate, or when optimizations such as inlining, link-time optimization, or codegen-unit partitioning are enabled, the compiler may generate multiple distinct code instances for the same function or alter the address at which it is emitted.
79+
80+
Consequently, the following operations are not reliable:
81+
82+
- Comparing function pointers for equality (``fn1 == fn2``)
83+
- Assuming a unique function address
84+
- Using function pointers as identity keys (e.g., in maps, registries, matchers)
85+
- Matching behavior based on function address
86+
87+
This rule applies even when the functions are semantically identical, exported as ``pub``, or defined once in source form.
88+
89+
.. rationale::
90+
:id: rat_xcVE5Hfnbb2u
91+
:status: draft
92+
93+
Compiler optimizations may cause function pointers originating from different crates to lose stable identity. Observed behaviors include:
94+
95+
- Cross-crate inlining producing multiple code instantiations
96+
- Codegen-unit separation causing function emission in multiple units
97+
- Incremental builds producing variant symbol addresses
98+
- Link-time optimization merging or splitting functions unpredictably
99+
100+
This behavior has resulted in real-world issues, such as the bug reported in rust-lang/rust#117047,
101+
where function pointer comparisons unexpectedly failed due to cross-crate inlining.
102+
103+
Violating this rule may cause:
104+
105+
- Silent logic failures: callbacks not matching, dispatch tables misbehaving.
106+
- Inappropriate branching: identity-based dispatch selecting wrong handler.
107+
- Security issues: adversary-controlled conditions bypassing function-based authorization/dispatch logic.
108+
- Nondeterministic behavior: correctness depending on build flags or incremental state.
109+
- Test-only correctness: function pointer equality passing in debug builds but failing in release/LTO builds.
110+
111+
In short, dependence on function address stability introduces non-portable, build-profile-dependent behavior, which is incompatible with high-integrity Rust.
112+
113+
.. non_compliant_example::
114+
:id: non_compl_ex_MkAkFxjRTijx
115+
:status: draft
116+
117+
Due to cross-crate inlining or codegen-unit partitioning,
118+
the address of ``handler_a`` in crate ``B`` may differ from its address in crate A,
119+
causing comparisons to fail as shown in this noncompliant code example:
120+
121+
.. code-block:: rust
122+
123+
// crate A
124+
pub fn handler_a() {}
125+
pub fn handler_b() {}
126+
rust
127+
128+
// crate B
129+
use crate_a::{handler_a, handler_b};
130+
131+
fn dispatch(f: fn()) {
132+
if f == handler_a {
133+
println!("Handled by A");
134+
} else if f == handler_b {
135+
println!("Handled by B");
136+
}
137+
}
138+
139+
dispatch(handler_a);
140+
141+
// Error: This may fail unpredictably if handler_a is inlined or duplicated.
142+
143+
.. compliant_example::
144+
:id: compl_ex_oiqSSclTXmIi
145+
:status: draft
146+
147+
Replace function pointer comparison with an explicit enum as shown in this compliant example:
148+
149+
.. code-block:: rust
150+
151+
// crate A
152+
pub enum HandlerId { A, B }
153+
154+
pub fn handler(id: HandlerId) {
155+
match id {
156+
HandlerId::A => handler_a(),
157+
HandlerId::B => handler_b(),
158+
}
159+
}
160+
161+
// crate B
162+
use crate_a::{handler, HandlerId};
163+
164+
fn dispatch(id: HandlerId) {
165+
handler(id);
166+
}
167+
168+
dispatch(HandlerId::A); // OK: semantically stable identity
169+
170+
.. non_compliant_example::
171+
:id: non_compl_ex_MkAkFxjRTijy
172+
:status: draft
173+
174+
Function pointer used as a key is not guaranteed to have stable identity, as shown in this noncompliant example:
175+
176+
.. code-block:: rust
177+
178+
// crate A
179+
pub fn op_mul(x: i32) -> i32 { x * 2 }
180+
181+
// crate B
182+
use crate_a::op_mul;
183+
use std::collections::HashMap;
184+
185+
let mut registry: HashMap<fn(i32) -> i32, &'static str> = HashMap::new();
186+
registry.insert(op_mul, "double");
187+
188+
let f = op_mul;
189+
190+
// ❌ Lookup may fail if `op_mul` has multiple emitted instances.
191+
assert_eq!(registry.get(&f), Some(&"double"));
192+
193+
.. compliant_example::
194+
:id: compl_ex_oiqSSclTXmIj
195+
:status: draft
196+
197+
This compliant example uses a stable identity wrappers as identity keys.
198+
The ``id`` is a stable, programmer-defined identity, immune to compiler optimizations.
199+
The function pointer is preserved for behavior (``func``) but never used as the identity key.
200+
201+
.. code-block:: rust
202+
203+
// crate A
204+
205+
pub fn op_mul(x: i32) -> i32 { x * 2 }
206+
pub fn op_add(x: i32) -> i32 { x + 2 }
207+
208+
// Stable identity wrapper for an operation.
209+
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
210+
pub struct Operation {
211+
pub id: u32,
212+
pub func: fn(i32) -> i32,
213+
}
214+
215+
// Export stable descriptors.
216+
pub const OP_MUL: Operation = Operation { id: 1, func: op_mul };
217+
pub const OP_ADD: Operation = Operation { id: 2, func: op_add };
218+
219+
// crate B
220+
221+
use crate_a::{Operation, OP_MUL, OP_ADD};
222+
use std::collections::HashMap;
223+
224+
fn main() {
225+
let mut registry: HashMap<u32, &'static str> = HashMap::new();
226+
227+
// Insert using stable identity key (ID), not function pointer.
228+
registry.insert(OP_MUL.id, "double");
229+
registry.insert(OP_ADD.id, "increment");
230+
231+
// Later: lookup using ID
232+
let op = OP_MUL;
233+
234+
// lookup works reliably regardless of inlining, LTO, CGUs, cross-crate instantiation, etc.
235+
assert_eq!(registry.get(&op.id), Some(&"double"));
236+
237+
println!("OP_MUL maps to: {}", registry[&op.id]);
238+
}
239+
240+
.. non_compliant_example::
241+
:id: non_compl_ex_MkAkFxjRTijz
242+
:status: draft
243+
244+
This noncompliant example relies on function pointer identity for deduplication:
245+
246+
.. code-block:: rust
247+
248+
// crate B
249+
let mut handlers: Vec<fn()> = Vec::new();
250+
251+
fn register(h: fn()) {
252+
if !handlers.contains(&h) {
253+
handlers.push(h);
254+
}
255+
}
256+
257+
register(handler); // Error: ❌ may be inserted twice under some builds
258+
259+
.. compliant_example::
260+
:id: compl_ex_oiqSSclTXmIj
261+
:status: draft
262+
263+
This compliant example keeps identity-sensitive logic inside a single crate:
264+
265+
.. code-block:: rust
266+
267+
// crate A (single crate boundary)
268+
#[inline(never)]
269+
pub fn important_handler() {}
270+
271+
pub fn is_important(f: fn()) -> bool {
272+
// Safe because identity and comparison are confined to one crate,
273+
// and inlining is prohibited.
274+
f == important_handler
275+
}
276+
277+
278+

0 commit comments

Comments
 (0)