Architecture Note: Out-of-Tree LLVM Passes That Insert llvm.assume
How to design, register, and verify an out-of-tree LLVM function pass that inserts llvm.assume without introducing silent miscompilation.
23 Oct 2025, 15:18 UTC

Problem and takeaway
You need LLVM’s optimizer to treat a condition as true at a specific program point—for example, to prove a loop bound or eliminate a bounds check. Inserting llvm.assume into LLVM IR is mechanically simple, but an incorrect assumption causes silent miscompilation: the optimizer deletes code it considers unreachable, and the program returns wrong results instead of failing loudly.
Takeaway: build a minimal out-of-tree function pass with LLVM’s new Pass Manager, insert llvm.assume only for conditions your analysis can justify, run the IR verifier, and compare generated assembly with and without the pass. Pin one LLVM major version; the out-of-tree plugin ABI is not stable across releases.
Requirements
- LLVM 12 or later, using the new Pass Manager (
PassBuilder,PassInfoMixin,AnalysisManager). The legacy Pass Manager is deprecated and removed in recent releases. - A C++ toolchain matching the host LLVM build’s RTTI,
NDEBUG, and ABI settings. - A condition you can prove locally inside a single function.
- A build system that can locate LLVM headers and libraries, typically through
LLVM_DIRorllvm-config.
Smallest suitable design
The pass should be a function pass registered through PassBuilder::registerPipelineParsingCallback so it can be invoked by name from opt. It must declare required properties through getRequiredProperties and return an appropriate PreservedAnalyses value from its run method.
A minimal skeleton looks like this. Exact callback signatures changed between LLVM 12, 14, and 17, so adapt the code to your pinned major version.
#include "llvm/IR/PassManager.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Intrinsics.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
using namespace llvm;
struct AssumePass : public PassInfoMixin {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) {
for (BasicBlock &BB : F) {
// Insert llvm.assume only at a point where the condition is provable.
// Do not capture BasicBlock or Instruction iterators across operations
// that may invalidate the function body.
}
return PreservedAnalyses::all();
}
};
static bool registerMyPass(PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef) {
if (Name == "my-assume-pass") {
FPM.addPass(AssumePass());
return true;
}
return false;
});
return true;
}
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "MyAssumePass", LLVM_VERSION_STRING,
[](PassBuilder &PB) { registerMyPass(PB); }};
}
Trust boundary: what llvm.assume means to the optimizer
llvm.assume is an intrinsic that tells the optimizer a condition is true at that point. It is not a security boundary. If the assumed condition is false at runtime, the optimizer may remove branches, memory-safety checks, or entire blocks because it considers them unreachable. The result is a silent miscompilation rather than a crash.
The LLVM IR verifier checks that the IR is well-formed; it does not validate whether an inserted assumption is semantically true. Verification after the pass catches malformed IR, not incorrect assumptions.
Operational checks
Run these commands on the build host. You need read access to the LLVM installation and write access to the output directory. Replace ${LLVM_ROOT} with your LLVM install path and ${PASS_SOURCE} with your plugin source file.
cmake -S . -B build -DLLVM_DIR=${LLVM_ROOT}/lib/cmake/llvm
cmake --build build
Load the plugin and run it on a test module:
opt -load-pass-plugin build/MyAssumePass.so \
-passes='my-assume-pass' \
-print-after-all \
input.ll -o output.ll
Expected checks:
- The plugin loads without a dynamic-loader error.
optexits with status zero.- The output IR contains
call void @llvm.assume(i1 ...)at the intended location. - Running
opt -verifyor callingllvm::verifyModulefrom the pass reports no malformed-IR errors.
Risk: a mismatch in RTTI, NDEBUG, or ABI between the plugin and the host LLVM shared library can cause crashes or undefined behavior, often inside the pass manager rather than in your pass logic.
Failure modes
- Malformed IR: inserting an
llvm.assumecall after an existing terminator, or using an invalid basic-block iterator, produces IR the verifier rejects. - Debug-build assertion failures: capturing iterators or pointers across pass invalidation points can trigger LLVM assertions in debug builds.
- Silent miscompilation: an assumption that does not hold at runtime lets the optimizer delete code it considers unreachable.
- Performance regression: excessive assumptions can constrain the optimizer and block transformations that would otherwise improve code.
- Stale analysis reuse: returning the wrong
PreservedAnalysesvalue causes the new Pass Manager to reuse invalidated results.
Conditions that would change the design
- If you must support the legacy Pass Manager or an LLVM release before 12, the registration mechanism and pass base class differ.
- If the assumption is meant to enforce a security property, do not use
llvm.assume; use a runtime check instead. - If the condition spans functions or modules, a function pass is the wrong scope; you need a module-level analysis and pass.
- If you upgrade LLVM major versions, re-verify the plugin ABI,
getRequiredPropertiessignature, and pipeline parsing callback before redeploying.
Limitations and practical verification
This note describes a design skeleton, not a production-ready pass. Because writing a plugin does not mutate a running system, there is no rollback procedure; if a build fails, remove the build directory and rebuild.
Practical check: compile a small C example to IR, run your pass, inspect the result with opt -print-after-all, then generate assembly with llc from both the original and transformed IR. Diff the two assembly files and look for unexpected removed branches or checks. If the output differs beyond the intended assumption, the pass is inserting conditions too aggressively.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.