Skip to content

Model Extern Callback Calls in LLVM Call Graphs#849

Open
e345ee wants to merge 4 commits into
secure-software-engineering:developmentfrom
e345ee:development
Open

Model Extern Callback Calls in LLVM Call Graphs#849
e345ee wants to merge 4 commits into
secure-software-engineering:developmentfrom
e345ee:development

Conversation

@e345ee

@e345ee e345ee commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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/Clang callback metadata.

Why

Some external functions receive a function pointer and call it internally. For example, pthread_create receives 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.h

  • lib/PhasarLLVM/ControlFlow/ExternCallbackModel.cpp

    These 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.h

  • lib/PhasarLLVM/ControlFlow/LLVMBasedCallGraphBuilder.cpp

    These 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.h

  • lib/PhasarLLVM/ControlFlow/LLVMBasedICFG.cpp

    These 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*.c

  • unittests/PhasarLLVM/ControlFlow/CMakeLists.txt

  • unittests/PhasarLLVM/ControlFlow/LLVMBasedICFGExternCallbackTest.cpp

    These 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.cpp

  • unittests/PhasarLLVM/ControlFlow/LLVMBasedICFGGlobCtorDtorTest.cpp

    These 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:

ctest --test-dir cmake-build/docker-full --output-on-failure -j2

Result:

100% tests passed, 0 tests failed out of 60

Also checked formatting and patch hygiene with git diff --check and clang-format --dry-run --Werror.

I would be happy to hear your feedback and address any requested changes.

Copilot AI review requested due to automatic review settings July 6, 2026 18:18
@e345ee e345ee requested review from MMory and fabianbs96 as code owners July 6, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ExternCallbackModel to detect supported broker call-sites, validate callback specs, generate model functions, and rewrite call-sites to those models.
  • Integrate rewriting into LLVMBasedICFG construction and mutable buildLLVMBasedCallGraph overloads 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.

Comment on lines +265 to +274
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;
Comment on lines +430 to +432
auto *Call =
IRB.CreateCall(Broker.getFunctionType(), &Broker, Args, OperandBundles);
Call->setCallingConv(BrokerCall.getCallingConv());

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.

Comment on lines +446 to +448
if (const auto *CallInst = llvm::dyn_cast<llvm::CallInst>(&BrokerCall)) {
Call->setTailCallKind(CallInst->getTailCallKind());
}
Comment on lines +357 to +361
PHASAR_LOG_LEVEL(WARNING, "Cannot adapt extern callback argument "
<< psr::llvmIRToString(Value) << " to "
<< psr::llvmTypeToString(ExpectedTy, true));
return llvm::PoisonValue::get(ExpectedTy);
}
Comment on lines +394 to +398
auto *ExpectedTy = CBTy.getParamType(Idx);
if (!Args[Idx]) {
Args[Idx] = llvm::PoisonValue::get(ExpectedTy);
continue;
}
Comment on lines +538 to +542
for (auto &Candidate : BrokerCalls) {
auto *Model =
createModel(IRDB, *Candidate.Broker, *Candidate.Call, Candidate.Specs);
Candidate.Call->setCalledFunction(Model->getFunctionType(), Model);
}
Comment on lines +367 to +370
const auto *LoadFoo = findLoadFrom(GetFoo, FooStorage);
ASSERT_NE(nullptr, LoadFoo);
const auto *FooGet = LoadFoo->getNextNode();
ASSERT_NE(nullptr, FooGet);
@e345ee

e345ee commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Copilot left a few comments on the generated IR details, and I pushed a follow-up addressing them.
I changed the model so it no longer copies tail-call kind, uses undef for unknown modeled values, clears stale call-site attributes after rewriting, and makes the ctor/dtor test use the next non-debug instruction.
I re-ran the tests locally in Docker, and the full suite passes

@fabianbs96 fabianbs96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

if (IncludeGlobals) {
auto *EntryFun = GlobalCtorsDtorsModel::buildModel(*IRDB, EntryPoints);
this->CG = buildLLVMBasedCallGraph(*IRDB, CGResolver, {EntryFun}, S);
this->CG = buildLLVMBasedCallGraph(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.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)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same


llvm::Type *getCallbackArgType(const llvm::CallBase &BrokerCall, int ArgNo) {
if (ArgNo < 0 || static_cast<unsigned>(ArgNo) >= BrokerCall.arg_size()) {
return getPointerType(BrokerCall.getContext());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this fallback?

}

if (Value->getType()->isPointerTy() && ExpectedTy->isPointerTy()) {
return IRB.CreatePointerCast(Value, ExpectedTy);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead code?


const auto *LoadFoo = findLoadFrom(GetFoo, FooStorage);
ASSERT_NE(nullptr, LoadFoo);
const auto *FooGet = LoadFoo->getNextNonDebugInstruction();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handling Extern Callbacks in CallGraph

3 participants