Model Extern Callback Calls in LLVM Call Graphs#849
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements IR-level rewriting to model external callback brokers (e.g., pthread_create, OpenMP __kmpc_fork_*, and LLVM/Clang callback metadata) by generating per-call-site model functions that contain both the original broker call and an explicit callback call, making these callbacks visible as normal LLVM call-sites for PhASAR analyses.
Changes:
- Add
ExternCallbackModelto detect supported broker call-sites, validate callback specs, generate model functions, and rewrite call-sites to those models. - Integrate rewriting into
LLVMBasedICFGconstruction and mutablebuildLLVMBasedCallGraphoverloads so generated models are present during graph construction. - Add regression tests and test IR sources for callback brokers; stabilize an existing globals ctor/dtor test by avoiding hardcoded instruction IDs.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| include/phasar/PhasarLLVM/ControlFlow/ExternCallbackModel.h | Public interface for extern callback rewriting/model detection. |
| lib/PhasarLLVM/ControlFlow/ExternCallbackModel.cpp | Implements spec parsing/validation, model generation, and call-site rewriting. |
| include/phasar/PhasarLLVM/ControlFlow/LLVMBasedCallGraphBuilder.h | Documents mutating vs non-mutating builder overloads; adds mutating overloads. |
| lib/PhasarLLVM/ControlFlow/LLVMBasedCallGraphBuilder.cpp | Runs extern-callback rewriting in mutable call graph builder overloads. |
| include/phasar/PhasarLLVM/ControlFlow/LLVMBasedICFG.h | Notes extern callback models may be inserted; extends isPhasarGenerated. |
| lib/PhasarLLVM/ControlFlow/LLVMBasedICFG.cpp | Runs extern-callback rewriting before ICFG/call-graph construction. |
| lib/PhasarLLVM/Utils/LLVMShorthands.cpp | Improves guard-load pattern recognition for lazy static init branches. |
| unittests/PhasarLLVM/ControlFlow/LLVMBasedICFGExternCallbackTest.cpp | New unit tests covering rewriting + call graph/ICFG integration. |
| unittests/PhasarLLVM/ControlFlow/CMakeLists.txt | Registers new unit test and adds dependencies on generated .ll targets. |
| test/llvm_test_code/call_graphs/CMakeLists.txt | Adds new extern-callback C sources to .ll generation list. |
| test/llvm_test_code/call_graphs/extern_callback_pthread.c | Test input for direct pthread_create callback. |
| test/llvm_test_code/call_graphs/extern_callback_pthread_indirect.c | Test input for indirect pthread_create callback argument. |
| test/llvm_test_code/call_graphs/extern_callback_pthread_multiple.c | Test input for multiple pthread_create call-sites. |
| test/llvm_test_code/call_graphs/extern_callback_kmpc_fork_call.c | Test input for __kmpc_fork_call callback modeling. |
| test/llvm_test_code/call_graphs/extern_callback_kmpc_fork_teams.c | Test input for __kmpc_fork_teams callback modeling. |
| test/llvm_test_code/call_graphs/extern_callback_attribute.c | Test input for callback attribute/metadata modeling. |
| test/llvm_test_code/call_graphs/extern_callback_attribute_indirect.c | Test input for indirect callback via callback attribute/metadata. |
| unittests/PhasarLLVM/ControlFlow/LLVMBasedICFGGlobCtorDtorTest.cpp | Stabilizes a globals-related test by locating instructions structurally. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!hasBrokerArg(BrokerCall, Spec.CalleeArg)) { | ||
| PHASAR_LOG_LEVEL(WARNING, "Cannot model extern callback broker " | ||
| << Broker.getName() << ": callback callee " | ||
| << "argument " << Spec.CalleeArg | ||
| << " is unavailable in " | ||
| << psr::llvmIRToString(&BrokerCall)); | ||
| return false; | ||
| } | ||
|
|
||
| bool HasMissingCallbackArgs = false; |
| auto *Call = | ||
| IRB.CreateCall(Broker.getFunctionType(), &Broker, Args, OperandBundles); | ||
| Call->setCallingConv(BrokerCall.getCallingConv()); |
| if (const auto *CallInst = llvm::dyn_cast<llvm::CallInst>(&BrokerCall)) { | ||
| Call->setTailCallKind(CallInst->getTailCallKind()); | ||
| } |
| PHASAR_LOG_LEVEL(WARNING, "Cannot adapt extern callback argument " | ||
| << psr::llvmIRToString(Value) << " to " | ||
| << psr::llvmTypeToString(ExpectedTy, true)); | ||
| return llvm::PoisonValue::get(ExpectedTy); | ||
| } |
| auto *ExpectedTy = CBTy.getParamType(Idx); | ||
| if (!Args[Idx]) { | ||
| Args[Idx] = llvm::PoisonValue::get(ExpectedTy); | ||
| continue; | ||
| } |
| for (auto &Candidate : BrokerCalls) { | ||
| auto *Model = | ||
| createModel(IRDB, *Candidate.Broker, *Candidate.Call, Candidate.Specs); | ||
| Candidate.Call->setCalledFunction(Model->getFunctionType(), Model); | ||
| } |
| const auto *LoadFoo = findLoadFrom(GetFoo, FooStorage); | ||
| ASSERT_NE(nullptr, LoadFoo); | ||
| const auto *FooGet = LoadFoo->getNextNode(); | ||
| ASSERT_NE(nullptr, FooGet); |
|
Copilot left a few comments on the generated IR details, and I pushed a follow-up addressing them. |
fabianbs96
left a comment
There was a problem hiding this comment.
Hi @e345ee, thanks for contributing this PR. Already looks quite good.
Only a few comments
| /// functions, rewrite the IRDB before constructing that state or use a CGType | ||
| /// overload. | ||
| [[nodiscard]] LLVMBasedCallGraph | ||
| buildLLVMBasedCallGraph(LLVMProjectIRDB &IRDB, Resolver &CGResolver, |
There was a problem hiding this comment.
At a call-site, this overload is hardly distinguishable from the other overloads (taking the IRDB as const-ref) I would suggest using a different name to make explicit that this function has different behavior.
| /// functions, rewrite the IRDB before constructing that state or use a CGType | ||
| /// overload. | ||
| [[nodiscard]] LLVMBasedCallGraph | ||
| buildLLVMBasedCallGraph(LLVMProjectIRDB &IRDB, Resolver &CGResolver, |
| if (IncludeGlobals) { | ||
| auto *EntryFun = GlobalCtorsDtorsModel::buildModel(*IRDB, EntryPoints); | ||
| this->CG = buildLLVMBasedCallGraph(*IRDB, CGResolver, {EntryFun}, S); | ||
| this->CG = buildLLVMBasedCallGraph( |
There was a problem hiding this comment.
Rewriting is done before every call to initialize(). So, better call the version of buildLLVMBasedCallGraph that does this already.
| add_phasar_unittest(${TEST_SRC}) | ||
| endforeach(TEST_SRC) | ||
|
|
||
| add_dependencies(LLVMBasedICFGExternCallbackTest |
There was a problem hiding this comment.
Should not be necessary as all phasar unittests depend on LLFileGeneration
| BrokerName + llvm::Twine('.')) | ||
| .str(); | ||
| for (const auto *F : IRDB.getAllFunctions()) { | ||
| if (F->getName().startswith(Prefix)) { |
There was a problem hiding this comment.
.startswith() is not compatible with more recent LLVM versions. Use .starts_with() instead.
| BrokerName + llvm::Twine('.')) | ||
| .str(); | ||
| for (const auto *F : IRDB.getAllFunctions()) { | ||
| if (F->getName().startswith(Prefix)) { |
|
|
||
| llvm::Type *getCallbackArgType(const llvm::CallBase &BrokerCall, int ArgNo) { | ||
| if (ArgNo < 0 || static_cast<unsigned>(ArgNo) >= BrokerCall.arg_size()) { | ||
| return getPointerType(BrokerCall.getContext()); |
There was a problem hiding this comment.
What is the reason for this fallback?
| } | ||
|
|
||
| if (Value->getType()->isPointerTy() && ExpectedTy->isPointerTy()) { | ||
| return IRB.CreatePointerCast(Value, ExpectedTy); |
|
|
||
| const auto *LoadFoo = findLoadFrom(GetFoo, FooStorage); | ||
| ASSERT_NE(nullptr, LoadFoo); | ||
| const auto *FooGet = LoadFoo->getNextNonDebugInstruction(); |
There was a problem hiding this comment.
getNextNonDebugInstruction() is incompatible with newer LLVM versions. If you really need this behavior, wrap this into #if LLVM_VERSION_MAJOR <= 18 and otherwise call getNextNode().
|
|
||
| TEST(LLVMBasedICFGExternCallbackTest, InvalidMetadataCallbackArg) { | ||
| expectNoRewrite(R"( | ||
| define void @sink(i32 %value) { |
There was a problem hiding this comment.
Can you create c files for these IR snippets in test/llvm_test_code? Then the unit-test does not break if in the future the IR syntax changes.
Model Extern Callback Calls in LLVM Call Graphs
Summary
Fixes #786.
This PR adds IR-level modeling for external callback brokers. The goal is to make callbacks passed to known external functions visible to PhASAR as regular LLVM call-sites, instead of encoding them as special call graph edges that later analyses would have to understand separately.
The first supported cases are
pthread_create,__kmpc_fork_call,__kmpc_fork_teams, and functions annotated with LLVM/Clangcallbackmetadata.Why
Some external functions receive a function pointer and call it internally. For example,
pthread_createreceives a worker function, but the actual call happens inside the pthread runtime, outside the analyzed LLVM IR.Without modeling, PhASAR can see the call to
pthread_create, but not a normal call-site to the worker function. This PR fixes that by generating a small model function for each supported callback broker call-site.The generated model keeps the original external call and adds a regular call to the callback argument with the modeled argument list. This gives the call graph and later analyses ordinary LLVM IR to work with.
Architecture
The solution follows the direction suggested in the issue: it uses IR rewriting rather than artificial ICFG edges.
For a broker call like
pthread_create(&thread, NULL, worker, arg), PhASAR generates a private model function and redirects the original call-site to it. Inside that model, PhASAR emits the original broker call, then emits a normal callback call, then returns the original broker result.The rewrite is validated before the model is created. If the callback callee argument is missing, if a callback argument index is out of range, or if the callback arity does not match, the original call-site is left unchanged and a warning is emitted. This avoids creating misleading generated IR for malformed or unsupported call-sites.
Files
The main changes are grouped as follows:
include/phasar/PhasarLLVM/ControlFlow/ExternCallbackModel.hlib/PhasarLLVM/ControlFlow/ExternCallbackModel.cppThese files contain the new callback model. The header exposes the small public interface, while the implementation handles metadata parsing, known broker specifications, validation, model generation, and call-site rewriting.
The model also preserves relevant properties of the original broker call when cloning it into the generated function, including calling convention, attributes, debug location, metadata, operand bundles, and tail-call kind.
include/phasar/PhasarLLVM/ControlFlow/LLVMBasedCallGraphBuilder.hlib/PhasarLLVM/ControlFlow/LLVMBasedCallGraphBuilder.cppThese files connect the model to call graph construction. Mutable builder overloads may rewrite supported callback brokers before building the graph, while const overloads remain non-mutating.
The documentation also notes that resolver/helper state should be created after rewriting if it needs to see generated model functions.
include/phasar/PhasarLLVM/ControlFlow/LLVMBasedICFG.hlib/PhasarLLVM/ControlFlow/LLVMBasedICFG.cppThese files integrate callback rewriting into ICFG construction. Rewriting happens before PhASAR creates resolver/helper analyses, so generated model functions are already part of the IRDB. I removed the second rewrite from the shared initialization path because it only repeated the same idempotent full IR pass after the models had already been inserted.
test/llvm_test_code/call_graphs/*extern_callback*.cunittests/PhasarLLVM/ControlFlow/CMakeLists.txtunittests/PhasarLLVM/ControlFlow/LLVMBasedICFGExternCallbackTest.cppThese files add regression coverage for the new feature. The tests cover direct and indirect callbacks, multiple callback broker call-sites, OpenMP brokers, metadata-based brokers, idempotent rewriting, mutable call graph builder integration, and invalid specs such as missing broker arguments, invalid callback argument indices, and mismatched callback arity.
lib/PhasarLLVM/Utils/LLVMShorthands.cppunittests/PhasarLLVM/ControlFlow/LLVMBasedICFGGlobCtorDtorTest.cppThese files stabilize an existing global constructor/destructor related test. The test no longer depends on hardcoded instruction ids, and the guard detection handles the guard-load pattern emitted by the local LLVM lowering. No platform-specific test skip is added.
Tests
Ran the full unit test suite in Docker:
Result:
Also checked formatting and patch hygiene with
git diff --checkandclang-format --dry-run --Werror.I would be happy to hear your feedback and address any requested changes.