|
| 1 | +.. SPDX-License-Identifier: MIT OR Apache-2.0 |
| 2 | + SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors |
| 3 | + |
| 4 | +.. default-domain:: coding-guidelines |
| 5 | + |
| 6 | +.. guideline:: Do not depend on function pointer identity |
| 7 | + :id: gui_QbvIknd9qNF6 |
| 8 | + :category: required |
| 9 | + :status: draft |
| 10 | + :release: unclear-latest |
| 11 | + :fls: fls_1kg1mknf4yx7 |
| 12 | + :decidability: decidable |
| 13 | + :scope: system |
| 14 | + :tags: surprising-behavior |
| 15 | + |
| 16 | + Do not rely on the equality or stable identity of function pointers. |
| 17 | + |
| 18 | + .. rationale:: |
| 19 | + :id: rat_kYiIiW8R2qD3 |
| 20 | + :status: draft |
| 21 | + |
| 22 | + Functions may be instantiated multiple times. |
| 23 | + They may, for example, be instantiated every time they are referenced. |
| 24 | + Only ``#[no_mangle]`` functions are guaranteed to be instantiated a single time, |
| 25 | + but can cause undefined behavior if they share a symbol with other identifiers. |
| 26 | + |
| 27 | + Avoid assumptions about low-level metadata (such as symbol addresses) unless explicitly guaranteed by the Ferrocene Language Specification (FLS). |
| 28 | + Function address identity is not guaranteed and must not be treated as stable. |
| 29 | + Rust's ``fn`` type is a zero-sized function item promoted to a function pointer, whose address is determined by the compiler backend. |
| 30 | + When a function resides in a different crate or codegen-unit partitioning is enabled, |
| 31 | + the compiler may generate multiple distinct code instances for the same function or alter the address at which it is emitted. |
| 32 | + |
| 33 | + Consequently, the following operations are unreliable for functions which are not ``#[no_mangle]``: |
| 34 | + |
| 35 | + - Comparing function pointers for equality (``fn1 == fn2``) |
| 36 | + - Assuming a unique function address |
| 37 | + - Using function pointers as identity keys (e.g., in maps, registries, matchers) |
| 38 | + - Matching behavior based on function address unless you instruct the linker to put a (#[no_mangle]) function at a specific address |
| 39 | + |
| 40 | + This rule applies even when the functions are semantically identical, exported as ``pub``, or defined once in source form. |
| 41 | + |
| 42 | + **Exception** |
| 43 | + |
| 44 | + ``#[no_mangle]`` functions are guaranteed to have a single instance. |
| 45 | + |
| 46 | + .. rationale:: |
| 47 | + :id: rat_xcVE5Hfnbb2u |
| 48 | + :status: draft |
| 49 | + |
| 50 | + Compiler optimizations may cause function pointers to lose stable identity, for example: |
| 51 | + |
| 52 | + - Cross-crate inlining can produce multiple code instantiations |
| 53 | + - Codegen-unit separation can cause function emission in multiple codegen units |
| 54 | + - Identical function implementations may be automatically merged as an optimization. |
| 55 | + Functions that are equivalent based only on specific hardware semantics may be merged in the machine-specific backend. |
| 56 | + Merging may also be performed as link-time optimization. |
| 57 | + |
| 58 | + This behavior has resulted in real-world issues, |
| 59 | + such as the bug reported in `rust-lang/rust#117047 <https://github.com/rust-lang/rust/issues/117047>`_, |
| 60 | + where function pointer comparisons unexpectedly failed because the function in question was instantiated multiple times. |
| 61 | + |
| 62 | + Violating this rule may cause: |
| 63 | + |
| 64 | + - Silent logic failures: callbacks not matching, dispatch tables misbehaving. |
| 65 | + - Inappropriate branching: identity-based dispatch selecting wrong handler. |
| 66 | + - Security issues: adversary-controlled conditions bypassing function-based authorization/dispatch logic. |
| 67 | + - Nondeterministic behavior: correctness depending on build flags or incremental state. |
| 68 | + - Test-only correctness: function pointer equality passing in debug builds but failing in release/link-time optimization builds. |
| 69 | + |
| 70 | + In short, dependence on function address stability introduces non-portable, build-profile-dependent behavior, |
| 71 | + which is incompatible with high-integrity Rust. |
| 72 | + |
| 73 | + .. non_compliant_example:: |
| 74 | + :id: non_compl_ex_MkAkFxjRTijx |
| 75 | + :status: draft |
| 76 | + |
| 77 | + Due to cross-crate inlining or codegen-unit partitioning, |
| 78 | + the address of ``handler_a`` in crate ``B`` may differ from its address in crate A, |
| 79 | + causing comparisons to fail as shown in this noncompliant code example: |
| 80 | + |
| 81 | + .. rust-example:: |
| 82 | + |
| 83 | + // crate A |
| 84 | + pub fn handler_a() {} |
| 85 | + pub fn handler_b() {} |
| 86 | + |
| 87 | + // crate B |
| 88 | + use crate_a::{handler_a, handler_b}; |
| 89 | + |
| 90 | + fn dispatch(f: fn()) { |
| 91 | + if f == handler_a { |
| 92 | + println!("Handled by A"); |
| 93 | + } else if f == handler_b { |
| 94 | + println!("Handled by B"); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + dispatch(handler_a); |
| 99 | + |
| 100 | + // Error: This may fail unpredictably if handler_a is inlined or duplicated. |
| 101 | + |
| 102 | + .. compliant_example:: |
| 103 | + :id: compl_ex_oiqSSclTXmIi |
| 104 | + :status: draft |
| 105 | + |
| 106 | + Replace function pointer comparison with an explicit enum as shown in this compliant example: |
| 107 | + |
| 108 | + .. rust-example:: |
| 109 | + |
| 110 | + // crate A |
| 111 | + pub enum HandlerId { A, B } |
| 112 | + |
| 113 | + pub fn handler(id: HandlerId) { |
| 114 | + match id { |
| 115 | + HandlerId::A => handler_a(), |
| 116 | + HandlerId::B => handler_b(), |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + // crate B |
| 121 | + use crate_a::{handler, HandlerId}; |
| 122 | + |
| 123 | + fn dispatch(id: HandlerId) { |
| 124 | + handler(id); |
| 125 | + } |
| 126 | + |
| 127 | + dispatch(HandlerId::A); // OK: semantically stable identity |
| 128 | + |
| 129 | + .. non_compliant_example:: |
| 130 | + :id: non_compl_ex_MkAkFxjRTijy |
| 131 | + :status: draft |
| 132 | + |
| 133 | + A function pointer used as a key is not guaranteed to have stable identity, as shown in this noncompliant example: |
| 134 | + |
| 135 | + .. rust-example:: |
| 136 | + |
| 137 | + // crate A |
| 138 | + pub fn op_mul(x: i32) -> i32 { x * 2 } |
| 139 | + |
| 140 | + // crate B |
| 141 | + use crate_a::op_mul; |
| 142 | + use std::collections::HashMap; |
| 143 | + |
| 144 | + let mut registry: HashMap<fn(i32) -> i32, &'static str> = HashMap::new(); |
| 145 | + registry.insert(op_mul, "double"); |
| 146 | +
|
| 147 | + let f = op_mul; |
| 148 | +
|
| 149 | + // Error: Lookup may fail if `op_mul` has multiple emitted instances. |
| 150 | + assert_eq!(registry.get(&f), Some(&"double")); |
| 151 | +
|
| 152 | + .. compliant_example:: |
| 153 | + :id: compl_ex_oiqSSclTXmIj |
| 154 | + :status: draft |
| 155 | +
|
| 156 | + This compliant example uses a stable identity wrappers as identity keys. |
| 157 | + The ``id`` is a stable, programmer-defined identity, immune to compiler optimizations. |
| 158 | + The function pointer is preserved for behavior (``func``) but never used as the identity key. |
| 159 | +
|
| 160 | + .. rust-example:: |
| 161 | +
|
| 162 | + // crate A |
| 163 | +
|
| 164 | + pub fn op_mul(x: i32) -> i32 { x * 2 } |
| 165 | + pub fn op_add(x: i32) -> i32 { x + 2 } |
| 166 | +
|
| 167 | + // Stable identity wrapper for an operation. |
| 168 | + #[derive(Copy, Clone, PartialEq, Eq, Hash)] |
| 169 | + pub struct Operation { |
| 170 | + pub id: u32, |
| 171 | + pub func: fn(i32) -> i32, |
| 172 | + } |
| 173 | +
|
| 174 | + // Export stable descriptors. |
| 175 | + pub const OP_MUL: Operation = Operation { id: 1, func: op_mul }; |
| 176 | + pub const OP_ADD: Operation = Operation { id: 2, func: op_add }; |
| 177 | +
|
| 178 | + // crate B |
| 179 | +
|
| 180 | + use crate_a::{Operation, OP_MUL, OP_ADD}; |
| 181 | + use std::collections::HashMap; |
| 182 | +
|
| 183 | + fn main() { |
| 184 | + let mut registry: HashMap<u32, &'static str> = HashMap::new(); |
| 185 | + |
| 186 | + // Insert using stable identity key (ID), not function pointer. |
| 187 | + registry.insert(OP_MUL.id, "double"); |
| 188 | + registry.insert(OP_ADD.id, "increment"); |
| 189 | + |
| 190 | + // Later: lookup using ID |
| 191 | + let op = OP_MUL; |
| 192 | + |
| 193 | + // lookup works reliably regardless of inlining, LTO, CGUs, cross-crate instantiation, etc. |
| 194 | + assert_eq!(registry.get(&op.id), Some(&"double")); |
| 195 | + |
| 196 | + println!("OP_MUL maps to: {}", registry[&op.id]); |
| 197 | + } |
| 198 | + |
| 199 | + .. non_compliant_example:: |
| 200 | + :id: non_compl_ex_MkAkFxjRTijz |
| 201 | + :status: draft |
| 202 | + |
| 203 | + This noncompliant example relies on function pointer identity for deduplication: |
| 204 | + |
| 205 | + .. rust-example:: |
| 206 | + |
| 207 | + // crate B |
| 208 | + let mut handlers: Vec<fn()> = Vec::new(); |
| 209 | + |
| 210 | + fn register(h: fn()) { |
| 211 | + if !handlers.contains(&h) { |
| 212 | + handlers.push(h); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + register(handler); // Error: may be inserted twice under some builds |
| 217 | + |
| 218 | + .. compliant_example:: |
| 219 | + :id: compl_ex_oiqSSclTXmIk |
| 220 | + :status: draft |
| 221 | + |
| 222 | + This compliant example keeps identity-sensitive logic inside a single crate: |
| 223 | + |
| 224 | + .. rust-example:: |
| 225 | + |
| 226 | + // crate A (single crate boundary) |
| 227 | + #[inline(never)] |
| 228 | + pub fn important_handler() {} |
| 229 | + |
| 230 | + pub fn is_important(f: fn()) -> bool { |
| 231 | + // Safe because identity and comparison are confined to one crate, |
| 232 | + // and inlining is prohibited. |
| 233 | + f == important_handler |
| 234 | + } |
0 commit comments