Skip to content

CFI record

MaoJiaping edited this page Jul 22, 2026 · 2 revisions

for the case

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;
}

If we have a module like:

                         ┌──> 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.

For the regular CFI pipeline, we will get the following result:

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

With MTPC, CFI behaves as follows:

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.

Clone this wiki locally