Repository navigation
CFI record
typedef int (*operation_t)(int);
static operation_t volatile selected;
__attribute__((noinline)) static int add_one(int x) {
return x + 1;
}
__attribute__((noinline)) static int times_two(int x) {
return x * 2;
}
// The volatile function-pointer load keeps this as a real indirect call.
__attribute__((noinline, used)) static int checked_call(int x) {
operation_t operation = selected;
return operation(x);
}
// A second independent root gives MTPC something else to place in a partition.
__attribute__((noinline, used)) static int independent_root(int x) {
return x - 1;
}
int main(int argc, char **argv) {
(void)argv;
selected = argc > 1 ? add_one : times_two;
int result = checked_call(argc);
int expected = argc > 1 ? argc + 1 : argc * 2;
return result == expected ? 0 : 1;
}
┌──> add_one
main --> selected -------┤
│ └──> times_two
│
└──> checked_call --> selected(x)
independent_root
Here, checked_call loads selected and performs a real indirect call:
checked_call --> add_one or times_two
For simplicity, the hash suffixes generated by ThinLTO are omitted below.
After CFI creates the canonical jump table:
.cfi.jumptable
entry 0 --> checked_call.cfi
entry 1 --> independent_root.cfi
entry 2 --> add_one.cfi
entry 3 --> times_two.cfi
The original function symbols become aliases to their corresponding jump-table entries:
checked_call --> .cfi.jumptable entry 0
independent_root --> .cfi.jumptable entry 1
add_one --> .cfi.jumptable entry 2
times_two --> .cfi.jumptable entry 3
The actual function bodies are renamed with the .cfi suffix:
main
checked_call.cfi
independent_root.cfi
add_one.cfi
times_two.cfi
Inside checked_call.cfi, CFI inserts a runtime check before the indirect call:
load selected
|
v
Is selected a valid and correctly aligned
entry in the CFI jump table?
|
+--+--+
| |
yes no
| |
v v
call ubsantrap
selected
Therefore, the effective call relationship is:
main --> checked_call.cfi
|
v
CFI runtime check
|
v
add_one.cfi or times_two.cfi
After splitting the implementation module:
P0:
independent_root
selected
P1:
main
checked_call
add_one
times_two
The remaining functions are represented by declarations in each partition when they are referenced from another partition:
P0:
defines independent_root and selected
declares checked_call, main, add_one, and times_two
P1:
defines main, checked_call, add_one, and times_two
declares independent_root
references selected from P0
After CFI:
The canonical CFI jump table remains shared and still contains entries for all functions with the same CFI type:
.cfi.jumptable
entry 0 --> checked_call.cfi
entry 1 --> independent_root.cfi
entry 2 --> add_one.cfi
entry 3 --> times_two.cfi
The function implementations are distributed across the MTPC partitions:
P0:
selected
independent_root.cfi
P1:
main
checked_call.cfi
add_one.cfi
times_two.cfi
P1 references the shared CFI type information and jump table:
P1: checked_call.cfi
|
v
load selected from P0
|
v
validate selected against
the shared CFI jump table
|
+--+--+
| |
valid invalid
| |
v v
indirect ubsantrap
call
After linking, the effective call relationship is:
P1: main --> checked_call.cfi
|
v
CFI runtime check
|
v
add_one.cfi or times_two.cfi
P0: independent_root.cfi
The final definitions are distributed as follows:
CFI jump-table part:
.cfi.jumptable
checked_call alias
independent_root alias
add_one alias
times_two alias
__typeid global address
P0:
selected
independent_root.cfi
P1:
main
checked_call.cfi
add_one.cfi
times_two.cfi
In this example, MTPC changes the ownership of the function implementations, but the CFI transformation itself is preserved. The canonical jump table remains shared, checked_call.cfi performs the same runtime validation, and cross-partition declarations connect the jump-table entries to the corresponding .cfi definitions.