llvm / llvm/llvm-project

clang: (C) `-fsanitize=cfi-icall` on plain callback emits broken cfi check

Open
#192,694 1 comment 0 reactions 0 assignees View on GitHub
compiler-rt:cfi
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

https://gcc.godbolt.org/z/d95eooMoj

Source is just this:
```c
void func(void (*callback)(void *arg), void *arg)
{
callback(arg);
}
```

When compiling with `-shared -flto`, everything is fine.

When compiling with `-shared -flto -fsanitize=cfi-icall`, a `0 & 1` check is added, and if counter to all odds 0 happens to be 0, it triggers the intentional sanitizer crash…

[it looks like `-shared` is not needed, but without it the function is optimized away in LTO.]

LLVM IR in dropdown

```LLVM
@.src = private unnamed_addr constant [15 x i8] c"/app/example.c\00", align 1
@anon.8df5ed46d55bff59767f606307d1477d.0 = private unnamed_addr constant { i16, i16, [16 x i8] } { i16 -1, i16 0, [16 x i8] c"'void (void *)'\00" }

define dso_local void @func(ptr noundef %callback, ptr noundef %arg) {
entry:
%callback.addr = alloca ptr, align 8
%arg.addr = alloca ptr, align 8
store ptr %callback, ptr %callback.addr, align 8
store ptr %arg, ptr %arg.addr, align 8
%0 = load ptr, ptr %callback.addr, align 8
%1 = call i1 @llvm.type.test(ptr %0, metadata !"_ZTSFvPvE")
br i1 %1, label %cont, label %trap

trap:
call void @llvm.ubsantrap(i8 2) #3
unreachable

cont:
%2 = load ptr, ptr %arg.addr, align 8
call void %0(ptr noundef %2)
ret void
}

declare i1 @llvm.type.test(ptr, metadata) #1

declare void @llvm.ubsantrap(i8 immarg) #2

define dso_local i32 @main() {
entry:
%retval = alloca i32, align 4
store i32 0, ptr %retval, align 4
ret i32 0
}

^0 = module: (path: "[Regular LTO]", hash: (0, 0, 0, 0, 0))
^1 = gv: (name: "anon.8df5ed46d55bff59767f606307d1477d.0", summaries: (variable: (module: ^0, flags: (linkage: private, visibility: default, notEligibleToImport: 1, live: 0, dsoLocal: 1, canAutoHide: 0, importType: definition, noRenameOnPromotion: 0), varFlags: (readonly: 1, writeonly: 0, constant: 1))))
^2 = gv: (name: "func", summaries: (function: (module: ^0, flags: (linkage: external, visibility: default, notEligibleToImport: 1, live: 0, dsoLocal: 1, canAutoHide: 0, importType: definition, noRenameOnPromotion: 0), insts: 12, funcFlags: (readNone: 0, readOnly: 0, noRecurse: 0, returnDoesNotAlias: 0, noInline: 1, alwaysInline: 0, noUnwind: 1, mayThrow: 0, hasUnknownCall: 1, mustBeUnreachable: 0), typeIdInfo: (typeTests: (5500305860835734721)))))
^3 = gv: (name: "main", summaries: (function: (module: ^0, flags: (linkage: external, visibility: default, notEligibleToImport: 1, live: 0, dsoLocal: 1, canAutoHide: 0, importType: definition, noRenameOnPromotion: 0), insts: 3, funcFlags: (readNone: 0, readOnly: 0, noRecurse: 0, returnDoesNotAlias: 0, noInline: 1, alwaysInline: 0, noUnwind: 1, mayThrow: 0, hasUnknownCall: 0, mustBeUnreachable: 0))))
^4 = gv: (name: ".src", summaries: (variable: (module: ^0, flags: (linkage: private, visibility: default, notEligibleToImport: 1, live: 0, dsoLocal: 1, canAutoHide: 0, importType: definition, noRenameOnPromotion: 0), varFlags: (readonly: 1, writeonly: 0, constant: 1))))
^5 = flags: 8
^6 = blockcount: 0
```

If you turn on optimization, it removes the entire function (because, well, yeah, it can't do anything other than crash...)

On the plus side, I think the `tbnz wzr, #0, label` it produces on aarch64 is the funniest output machine code I have seen in my entire life 🤣.

Contributor guide

Open the contributing guide

Research direction

Start by compiling the source from the Godbolt link with -shared -flto and then with -fsanitize=cfi-icall, comparing the generated LLVM IR and AArch64 output. Trace the clang/LLVM CFI path responsible for the llvm.type.test on the plain callback; done means the generated check no longer behaves like an unconditional 0 & 1 failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.