blob: 3876036abc01b18e6591f894b6fa1943c595d0e0 [file] [log] [blame] [edit]
//===- Interpreter.cpp - Interpreter Loop for llubi -----------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file implements the evaluation loop for each kind of instruction.
//
//===----------------------------------------------------------------------===//
#include "Context.h"
#include "Value.h"
#include "llvm/IR/GetElementPtrTypeIterator.h"
#include "llvm/IR/InlineAsm.h"
#include "llvm/IR/InstVisitor.h"
#include "llvm/IR/Operator.h"
#include "llvm/IR/PatternMatch.h"
#include "llvm/Support/Allocator.h"
namespace llvm::ubi {
using namespace PatternMatch;
enum class FrameState {
// It is about to enter the function.
// Valid transition:
// -> Running
Entry,
// It is executing instructions inside the function.
// Valid transitions:
// -> Pending (on call)
// -> Exit (on return)
Running,
// It is about to enter a callee or handle return value from the callee.
// Valid transitions:
// -> Running (after returning from callee)
Pending,
// It is about to return the control to the caller.
Exit,
};
/// Context for a function call.
/// This struct maintains the state during the execution of a function,
/// including the control flow, values of executed instructions, and stack
/// objects.
struct Frame {
Function &Func;
Frame *LastFrame;
CallBase *CallSite;
ArrayRef<AnyValue> Args;
AnyValue &RetVal;
TargetLibraryInfo TLI;
BasicBlock *BB;
BasicBlock::iterator PC;
FrameState State = FrameState::Entry;
// Stack objects allocated in this frame. They will be automatically freed
// when the function returns.
SmallVector<IntrusiveRefCntPtr<MemoryObject>> Allocas;
// Values of arguments and executed instructions in this function.
DenseMap<Value *, AnyValue> ValueMap;
// Reserved for in-flight subroutines.
Function *ResolvedCallee = nullptr;
SmallVector<AnyValue> CalleeArgs;
AnyValue CalleeRetVal;
Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
ArrayRef<AnyValue> Args, AnyValue &RetVal,
const TargetLibraryInfoImpl &TLIImpl)
: Func(F), LastFrame(LastFrame), CallSite(CallSite), Args(Args),
RetVal(RetVal), TLI(TLIImpl, &F) {
assert((Args.size() == F.arg_size() ||
(F.isVarArg() && Args.size() >= F.arg_size())) &&
"Expected enough arguments to call the function.");
BB = &Func.getEntryBlock();
PC = BB->begin();
for (Argument &Arg : F.args())
ValueMap[&Arg] = Args[Arg.getArgNo()];
}
};
static AnyValue addNoWrap(const APInt &LHS, const APInt &RHS, bool HasNSW,
bool HasNUW) {
APInt Res = LHS + RHS;
if (HasNUW && Res.ult(RHS))
return AnyValue::poison();
if (HasNSW && LHS.isNonNegative() == RHS.isNonNegative() &&
LHS.isNonNegative() != Res.isNonNegative())
return AnyValue::poison();
return Res;
}
static AnyValue subNoWrap(const APInt &LHS, const APInt &RHS, bool HasNSW,
bool HasNUW) {
APInt Res = LHS - RHS;
if (HasNUW && Res.ugt(LHS))
return AnyValue::poison();
if (HasNSW && LHS.isNonNegative() != RHS.isNonNegative() &&
LHS.isNonNegative() != Res.isNonNegative())
return AnyValue::poison();
return Res;
}
static AnyValue mulNoWrap(const APInt &LHS, const APInt &RHS, bool HasNSW,
bool HasNUW) {
bool Overflow = false;
APInt Res = LHS.smul_ov(RHS, Overflow);
if (HasNSW && Overflow)
return AnyValue::poison();
if (HasNUW) {
(void)LHS.umul_ov(RHS, Overflow);
if (Overflow)
return AnyValue::poison();
}
return Res;
}
/// Instruction executor using the visitor pattern.
/// Unlike the Context class that manages the global state,
/// InstExecutor only maintains the state for call frames.
class InstExecutor : public InstVisitor<InstExecutor, void> {
Context &Ctx;
const DataLayout &DL;
EventHandler &Handler;
std::list<Frame> CallStack;
// Used to indicate whether the interpreter should continue execution.
bool Status;
Frame *CurrentFrame = nullptr;
AnyValue None;
void reportImmediateUB(StringRef Msg) {
// Check if we have already reported an immediate UB.
if (!Status)
return;
Status = false;
// TODO: Provide stack trace information.
Handler.onImmediateUB(Msg);
}
void reportError(StringRef Msg) {
// Check if we have already reported an error message.
if (!Status)
return;
Status = false;
Handler.onError(Msg);
}
const AnyValue &getValue(Value *V) {
if (auto *C = dyn_cast<Constant>(V))
return Ctx.getConstantValue(C);
return CurrentFrame->ValueMap.at(V);
}
void setResult(Instruction &I, AnyValue V) {
if (Status)
Status &= Handler.onInstructionExecuted(I, V);
CurrentFrame->ValueMap.insert_or_assign(&I, std::move(V));
}
AnyValue computeUnOp(Type *Ty, const AnyValue &Operand,
function_ref<AnyValue(const AnyValue &)> ScalarFn) {
if (Ty->isVectorTy()) {
auto &OperandVec = Operand.asAggregate();
std::vector<AnyValue> ResVec;
ResVec.reserve(OperandVec.size());
for (const auto &Scalar : OperandVec)
ResVec.push_back(ScalarFn(Scalar));
return std::move(ResVec);
}
return ScalarFn(Operand);
}
void visitUnOp(Instruction &I,
function_ref<AnyValue(const AnyValue &)> ScalarFn) {
setResult(I, computeUnOp(I.getType(), getValue(I.getOperand(0)), ScalarFn));
}
void visitIntUnOp(Instruction &I,
function_ref<AnyValue(const APInt &)> ScalarFn) {
visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
if (Operand.isPoison())
return AnyValue::poison();
return ScalarFn(Operand.asInteger());
});
}
AnyValue computeBinOp(
Type *Ty, const AnyValue &LHS, const AnyValue &RHS,
function_ref<AnyValue(const AnyValue &, const AnyValue &)> ScalarFn) {
if (Ty->isVectorTy()) {
auto &LHSVec = LHS.asAggregate();
auto &RHSVec = RHS.asAggregate();
std::vector<AnyValue> ResVec;
ResVec.reserve(LHSVec.size());
for (const auto &[ScalarLHS, ScalarRHS] : zip(LHSVec, RHSVec))
ResVec.push_back(ScalarFn(ScalarLHS, ScalarRHS));
return std::move(ResVec);
}
return ScalarFn(LHS, RHS);
}
void visitBinOp(
Instruction &I,
function_ref<AnyValue(const AnyValue &, const AnyValue &)> ScalarFn) {
setResult(I, computeBinOp(I.getType(), getValue(I.getOperand(0)),
getValue(I.getOperand(1)), ScalarFn));
}
void
visitIntBinOp(Instruction &I,
function_ref<AnyValue(const APInt &, const APInt &)> ScalarFn) {
visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
if (LHS.isPoison() || RHS.isPoison())
return AnyValue::poison();
return ScalarFn(LHS.asInteger(), RHS.asInteger());
});
}
void jumpTo(Instruction &Terminator, BasicBlock *DestBB) {
if (!Handler.onBBJump(Terminator, *DestBB)) {
Status = false;
return;
}
BasicBlock *From = CurrentFrame->BB;
CurrentFrame->BB = DestBB;
CurrentFrame->PC = DestBB->begin();
// Update PHI nodes in batch to avoid the interference between PHI nodes.
// We need to store the incoming values into a temporary buffer.
// Otherwise, the incoming value may be overwritten before it is
// used by other PHI nodes.
SmallVector<std::pair<PHINode *, AnyValue>> IncomingValues;
PHINode *PHI = nullptr;
while ((PHI = dyn_cast<PHINode>(CurrentFrame->PC))) {
Value *Incoming = PHI->getIncomingValueForBlock(From);
// TODO: handle fast-math flags.
IncomingValues.emplace_back(PHI, getValue(Incoming));
++CurrentFrame->PC;
}
for (auto &[K, V] : IncomingValues)
setResult(*K, std::move(V));
}
/// Helper function to determine whether an inline asm is a no-op, which is
/// used to implement black_box style optimization blockers.
bool isNoopInlineAsm(Value *V, Type *RetTy) {
if (auto *Asm = dyn_cast<InlineAsm>(V))
return Asm->getAsmString().empty() && RetTy->isVoidTy();
return false;
}
/// Check if the upcoming memory access is valid. Returns the offset relative
/// to the underlying object if it is valid.
std::optional<uint64_t> verifyMemAccess(const MemoryObject &MO,
const APInt &Address,
uint64_t AccessSize, Align Alignment,
bool IsStore) {
// Loading from a stack object outside its lifetime is not undefined
// behavior and returns a poison value instead. Storing to it is still
// undefined behavior.
if (IsStore ? MO.getState() != MemoryObjectState::Alive
: MO.getState() == MemoryObjectState::Freed) {
reportImmediateUB("Try to access a dead memory object.");
return std::nullopt;
}
if (Address.countr_zero() < Log2(Alignment)) {
reportImmediateUB("Misaligned memory access.");
return std::nullopt;
}
if (AccessSize > MO.getSize() || Address.ult(MO.getAddress())) {
reportImmediateUB("Memory access is out of bounds.");
return std::nullopt;
}
APInt Offset = Address - MO.getAddress();
if (Offset.ugt(MO.getSize() - AccessSize)) {
reportImmediateUB("Memory access is out of bounds.");
return std::nullopt;
}
return Offset.getZExtValue();
}
AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy) {
if (Ptr.isPoison()) {
reportImmediateUB("Invalid memory access with a poison pointer.");
return AnyValue::getPoisonValue(Ctx, ValTy);
}
auto &PtrVal = Ptr.asPointer();
auto *MO = PtrVal.getMemoryObject();
if (!MO) {
reportImmediateUB(
"Invalid memory access via a pointer with nullary provenance.");
return AnyValue::getPoisonValue(Ctx, ValTy);
}
// TODO: pointer capability check
if (auto Offset =
verifyMemAccess(*MO, PtrVal.address(),
Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
/*IsStore=*/false)) {
// Load from a dead stack object yields poison value.
if (MO->getState() == MemoryObjectState::Dead)
return AnyValue::getPoisonValue(Ctx, ValTy);
return Ctx.load(*MO, *Offset, ValTy);
}
return AnyValue::getPoisonValue(Ctx, ValTy);
}
void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
Type *ValTy) {
if (Ptr.isPoison()) {
reportImmediateUB("Invalid memory access with a poison pointer.");
return;
}
auto &PtrVal = Ptr.asPointer();
auto *MO = PtrVal.getMemoryObject();
if (!MO) {
reportImmediateUB(
"Invalid memory access via a pointer with nullary provenance.");
return;
}
// TODO: pointer capability check
if (auto Offset =
verifyMemAccess(*MO, PtrVal.address(),
Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
/*IsStore=*/true))
Ctx.store(*MO, *Offset, Val, ValTy);
}
AnyValue computePtrAdd(const Pointer &Ptr, const APInt &Offset,
GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset) {
if (Offset.isZero())
return Ptr;
APInt IndexBits = Ptr.address().trunc(Offset.getBitWidth());
auto NewIndex = addNoWrap(IndexBits, Offset, /*HasNSW=*/false,
Flags.hasNoUnsignedWrap());
if (NewIndex.isPoison())
return AnyValue::poison();
if (Flags.hasNoUnsignedSignedWrap()) {
// The successive addition of the current address, truncated to the
// pointer index type and interpreted as an unsigned number, and each
// offset, interpreted as a signed number, does not wrap the pointer index
// type.
if (Offset.isNonNegative() ? NewIndex.asInteger().ult(IndexBits)
: NewIndex.asInteger().ugt(IndexBits))
return AnyValue::poison();
}
APInt NewAddr = Ptr.address();
NewAddr.insertBits(NewIndex.asInteger(), 0);
auto *MO = Ptr.getMemoryObject();
if (Flags.isInBounds() && (!MO || !MO->inBounds(NewAddr)))
return AnyValue::poison();
if (!AccumulatedOffset.isPoison()) {
AccumulatedOffset =
addNoWrap(AccumulatedOffset.asInteger(), Offset,
Flags.hasNoUnsignedSignedWrap(), Flags.hasNoUnsignedWrap());
if (AccumulatedOffset.isPoison())
return AnyValue::poison();
}
// Should not expose provenance here even if the new address doesn't point
// to the original object.
return Ptr.getWithNewAddr(NewAddr);
}
AnyValue computePtrAdd(const AnyValue &Ptr, const APInt &Offset,
GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset) {
if (Ptr.isPoison())
return AnyValue::poison();
return computePtrAdd(Ptr.asPointer(), Offset, Flags, AccumulatedOffset);
}
AnyValue computeScaledPtrAdd(const AnyValue &Ptr, const AnyValue &Index,
const APInt &Scale, GEPNoWrapFlags Flags,
AnyValue &AccumulatedOffset) {
if (Ptr.isPoison() || Index.isPoison())
return AnyValue::poison();
assert(Ptr.isPointer() && Index.isInteger() && "Unexpected type.");
if (Scale.isOne())
return computePtrAdd(Ptr, Index.asInteger(), Flags, AccumulatedOffset);
auto ScaledOffset =
mulNoWrap(Index.asInteger(), Scale, Flags.hasNoUnsignedSignedWrap(),
Flags.hasNoUnsignedWrap());
if (ScaledOffset.isPoison())
return AnyValue::poison();
return computePtrAdd(Ptr, ScaledOffset.asInteger(), Flags,
AccumulatedOffset);
}
AnyValue canonicalizeIndex(const AnyValue &Idx, unsigned IndexBitWidth,
GEPNoWrapFlags Flags) {
if (Idx.isPoison())
return AnyValue::poison();
auto &IdxInt = Idx.asInteger();
if (IdxInt.getBitWidth() == IndexBitWidth)
return Idx;
if (IdxInt.getBitWidth() > IndexBitWidth) {
if (Flags.hasNoUnsignedSignedWrap() &&
!IdxInt.isSignedIntN(IndexBitWidth))
return AnyValue::poison();
if (Flags.hasNoUnsignedWrap() && !IdxInt.isIntN(IndexBitWidth))
return AnyValue::poison();
return IdxInt.trunc(IndexBitWidth);
}
return IdxInt.sext(IndexBitWidth);
}
public:
InstExecutor(Context &C, EventHandler &H, Function &F,
ArrayRef<AnyValue> Args, AnyValue &RetVal)
: Ctx(C), DL(Ctx.getDataLayout()), Handler(H), Status(true) {
CallStack.emplace_back(F, /*CallSite=*/nullptr, /*LastFrame=*/nullptr, Args,
RetVal, Ctx.getTLIImpl());
}
void visitReturnInst(ReturnInst &RI) {
if (auto *RV = RI.getReturnValue())
CurrentFrame->RetVal = getValue(RV);
CurrentFrame->State = FrameState::Exit;
Status &= Handler.onInstructionExecuted(RI, None);
}
void visitBranchInst(BranchInst &BI) {
if (BI.isConditional()) {
switch (getValue(BI.getCondition()).asBoolean()) {
case BooleanKind::True:
jumpTo(BI, BI.getSuccessor(0));
return;
case BooleanKind::False:
jumpTo(BI, BI.getSuccessor(1));
return;
case BooleanKind::Poison:
reportImmediateUB("Branch on poison condition.");
return;
}
}
jumpTo(BI, BI.getSuccessor(0));
}
void visitSwitchInst(SwitchInst &SI) {
auto &Cond = getValue(SI.getCondition());
if (Cond.isPoison()) {
reportImmediateUB("Switch on poison condition.");
return;
}
for (auto &Case : SI.cases()) {
if (Case.getCaseValue()->getValue() == Cond.asInteger()) {
jumpTo(SI, Case.getCaseSuccessor());
return;
}
}
jumpTo(SI, SI.getDefaultDest());
}
void visitUnreachableInst(UnreachableInst &) {
reportImmediateUB("Unreachable code.");
}
void visitCallBrInst(CallBrInst &CI) {
if (isNoopInlineAsm(CI.getCalledOperand(), CI.getType())) {
jumpTo(CI, CI.getDefaultDest());
return;
}
Handler.onUnrecognizedInstruction(CI);
Status = false;
}
void visitIndirectBrInst(IndirectBrInst &IBI) {
auto &Target = getValue(IBI.getAddress());
if (Target.isPoison()) {
reportImmediateUB("Indirect branch on poison.");
return;
}
if (BasicBlock *DestBB = Ctx.getTargetBlock(Target.asPointer())) {
if (any_of(IBI.successors(),
[DestBB](BasicBlock *Succ) { return Succ == DestBB; }))
jumpTo(IBI, DestBB);
else
reportImmediateUB("Indirect branch on unlisted target BB.");
return;
}
reportImmediateUB("Indirect branch on invalid target BB.");
}
void returnFromCallee() {
// TODO: handle retval attributes (Attributes from known callee should be
// applied if available).
// TODO: handle metadata
auto &CB = cast<CallBase>(*CurrentFrame->PC);
CurrentFrame->CalleeArgs.clear();
AnyValue &RetVal = CurrentFrame->CalleeRetVal;
setResult(CB, std::move(RetVal));
if (auto *II = dyn_cast<InvokeInst>(&CB))
jumpTo(*II, II->getNormalDest());
else if (CurrentFrame->State == FrameState::Pending)
++CurrentFrame->PC;
}
AnyValue callIntrinsic(CallBase &CB) {
Intrinsic::ID IID = CB.getIntrinsicID();
switch (IID) {
case Intrinsic::assume:
switch (getValue(CB.getArgOperand(0)).asBoolean()) {
case BooleanKind::True:
break;
case BooleanKind::False:
case BooleanKind::Poison:
reportImmediateUB("Assume on false or poison condition.");
break;
}
// TODO: handle llvm.assume with operand bundles
return AnyValue();
case Intrinsic::lifetime_start:
case Intrinsic::lifetime_end: {
auto *Ptr = CB.getArgOperand(0);
if (isa<PoisonValue>(Ptr))
return AnyValue();
auto *MO = getValue(Ptr).asPointer().getMemoryObject();
assert(MO && "Memory object accessed by lifetime intrinsic should be "
"always valid.");
if (IID == Intrinsic::lifetime_start) {
MO->setState(MemoryObjectState::Alive);
fill(MO->getBytes(), Byte::undef());
} else {
MO->setState(MemoryObjectState::Dead);
}
return AnyValue();
}
default:
Handler.onUnrecognizedInstruction(CB);
Status = false;
return AnyValue();
}
}
AnyValue callLibFunc(CallBase &CB, Function *ResolvedCallee) {
LibFunc LF;
// Respect nobuiltin attributes on call site.
if (CB.isNoBuiltin() ||
!CurrentFrame->TLI.getLibFunc(*ResolvedCallee, LF)) {
Handler.onUnrecognizedInstruction(CB);
Status = false;
return AnyValue();
}
Handler.onUnrecognizedInstruction(CB);
Status = false;
return AnyValue();
}
void enterCall(CallBase &CB) {
Function *Callee = CB.getCalledFunction();
// TODO: handle parameter attributes (Attributes from known callee should be
// applied if available).
// TODO: handle byval/initializes
auto &CalleeArgs = CurrentFrame->CalleeArgs;
assert(CalleeArgs.empty() &&
"Forgot to call returnFromCallee before entering a new call.");
for (Value *Arg : CB.args())
CalleeArgs.push_back(getValue(Arg));
if (!Callee) {
Value *CalledOperand = CB.getCalledOperand();
if (isNoopInlineAsm(CalledOperand, CB.getType())) {
CurrentFrame->ResolvedCallee = nullptr;
returnFromCallee();
return;
}
if (isa<InlineAsm>(CalledOperand)) {
Handler.onUnrecognizedInstruction(CB);
Status = false;
return;
}
auto &CalleeVal = getValue(CalledOperand);
if (CalleeVal.isPoison()) {
reportImmediateUB("Indirect call through poison function pointer.");
return;
}
Callee = Ctx.getTargetFunction(CalleeVal.asPointer());
if (!Callee) {
reportImmediateUB("Indirect call through invalid function pointer.");
return;
}
if (Callee->getFunctionType() != CB.getFunctionType()) {
reportImmediateUB("Indirect call through a function pointer with "
"mismatched signature.");
return;
}
}
assert(Callee && "Expected a resolved callee function.");
assert(
Callee->getFunctionType() == CB.getFunctionType() &&
"Expected the callee function type to match the call site signature.");
CurrentFrame->ResolvedCallee = Callee;
if (Callee->isIntrinsic()) {
CurrentFrame->CalleeRetVal = callIntrinsic(CB);
returnFromCallee();
return;
} else if (Callee->isDeclaration()) {
CurrentFrame->CalleeRetVal = callLibFunc(CB, Callee);
returnFromCallee();
return;
} else {
uint32_t MaxStackDepth = Ctx.getMaxStackDepth();
if (MaxStackDepth && CallStack.size() >= MaxStackDepth) {
reportError("Maximum stack depth exceeded.");
return;
}
assert(!Callee->empty() && "Expected a defined function.");
// Suspend the current frame and push the callee frame onto the stack.
ArrayRef<AnyValue> Args = CurrentFrame->CalleeArgs;
AnyValue &RetVal = CurrentFrame->CalleeRetVal;
CurrentFrame->State = FrameState::Pending;
CallStack.emplace_back(*Callee, &CB, CurrentFrame, Args, RetVal,
Ctx.getTLIImpl());
}
}
void visitCallInst(CallInst &CI) { enterCall(CI); }
void visitInvokeInst(InvokeInst &II) {
// TODO: handle exceptions
enterCall(II);
}
void visitAdd(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) {
return addNoWrap(LHS, RHS, I.hasNoSignedWrap(), I.hasNoUnsignedWrap());
});
}
void visitSub(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) {
return subNoWrap(LHS, RHS, I.hasNoSignedWrap(), I.hasNoUnsignedWrap());
});
}
void visitMul(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) {
return mulNoWrap(LHS, RHS, I.hasNoSignedWrap(), I.hasNoUnsignedWrap());
});
}
void visitSDiv(BinaryOperator &I) {
visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
// Priority: Immediate UB > poison > normal value
if (RHS.isPoison()) {
reportImmediateUB("Division by zero (refine RHS to 0).");
return AnyValue::poison();
}
const APInt &RHSVal = RHS.asInteger();
if (RHSVal.isZero()) {
reportImmediateUB("Division by zero.");
return AnyValue::poison();
}
if (LHS.isPoison()) {
if (RHSVal.isAllOnes())
reportImmediateUB(
"Signed division overflow (refine LHS to INT_MIN).");
return AnyValue::poison();
}
const APInt &LHSVal = LHS.asInteger();
if (LHSVal.isMinSignedValue() && RHSVal.isAllOnes()) {
reportImmediateUB("Signed division overflow.");
return AnyValue::poison();
}
if (I.isExact()) {
APInt Q, R;
APInt::sdivrem(LHSVal, RHSVal, Q, R);
if (!R.isZero())
return AnyValue::poison();
return Q;
} else {
return LHSVal.sdiv(RHSVal);
}
});
}
void visitSRem(BinaryOperator &I) {
visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
// Priority: Immediate UB > poison > normal value
if (RHS.isPoison()) {
reportImmediateUB("Division by zero (refine RHS to 0).");
return AnyValue::poison();
}
const APInt &RHSVal = RHS.asInteger();
if (RHSVal.isZero()) {
reportImmediateUB("Division by zero.");
return AnyValue::poison();
}
if (LHS.isPoison()) {
if (RHSVal.isAllOnes())
reportImmediateUB(
"Signed division overflow (refine LHS to INT_MIN).");
return AnyValue::poison();
}
const APInt &LHSVal = LHS.asInteger();
if (LHSVal.isMinSignedValue() && RHSVal.isAllOnes()) {
reportImmediateUB("Signed division overflow.");
return AnyValue::poison();
}
return LHSVal.srem(RHSVal);
});
}
void visitUDiv(BinaryOperator &I) {
visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
// Priority: Immediate UB > poison > normal value
if (RHS.isPoison()) {
reportImmediateUB("Division by zero (refine RHS to 0).");
return AnyValue::poison();
}
const APInt &RHSVal = RHS.asInteger();
if (RHSVal.isZero()) {
reportImmediateUB("Division by zero.");
return AnyValue::poison();
}
if (LHS.isPoison())
return AnyValue::poison();
const APInt &LHSVal = LHS.asInteger();
if (I.isExact()) {
APInt Q, R;
APInt::udivrem(LHSVal, RHSVal, Q, R);
if (!R.isZero())
return AnyValue::poison();
return Q;
} else {
return LHSVal.udiv(RHSVal);
}
});
}
void visitURem(BinaryOperator &I) {
visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
// Priority: Immediate UB > poison > normal value
if (RHS.isPoison()) {
reportImmediateUB("Division by zero (refine RHS to 0).");
return AnyValue::poison();
}
const APInt &RHSVal = RHS.asInteger();
if (RHSVal.isZero()) {
reportImmediateUB("Division by zero.");
return AnyValue::poison();
}
if (LHS.isPoison())
return AnyValue::poison();
const APInt &LHSVal = LHS.asInteger();
return LHSVal.urem(RHSVal);
});
}
void visitTruncInst(TruncInst &Trunc) {
visitIntUnOp(Trunc, [&](const APInt &Operand) -> AnyValue {
unsigned DestBW = Trunc.getType()->getScalarSizeInBits();
if (Trunc.hasNoSignedWrap() && Operand.getSignificantBits() > DestBW)
return AnyValue::poison();
if (Trunc.hasNoUnsignedWrap() && Operand.getActiveBits() > DestBW)
return AnyValue::poison();
return Operand.trunc(DestBW);
});
}
void visitZExtInst(ZExtInst &ZExt) {
visitIntUnOp(ZExt, [&](const APInt &Operand) -> AnyValue {
uint32_t DestBW = ZExt.getDestTy()->getScalarSizeInBits();
if (ZExt.hasNonNeg() && Operand.isNegative())
return AnyValue::poison();
return Operand.zext(DestBW);
});
}
void visitSExtInst(SExtInst &SExt) {
visitIntUnOp(SExt, [&](const APInt &Operand) -> AnyValue {
uint32_t DestBW = SExt.getDestTy()->getScalarSizeInBits();
return Operand.sext(DestBW);
});
}
void visitAnd(BinaryOperator &I) {
visitIntBinOp(I, [](const APInt &LHS, const APInt &RHS) -> AnyValue {
return LHS & RHS;
});
}
void visitXor(BinaryOperator &I) {
visitIntBinOp(I, [](const APInt &LHS, const APInt &RHS) -> AnyValue {
return LHS ^ RHS;
});
}
void visitOr(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) -> AnyValue {
if (cast<PossiblyDisjointInst>(I).isDisjoint() && LHS.intersects(RHS))
return AnyValue::poison();
return LHS | RHS;
});
}
void visitShl(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) -> AnyValue {
if (RHS.uge(LHS.getBitWidth()))
return AnyValue::poison();
if (I.hasNoSignedWrap() && RHS.uge(LHS.getNumSignBits()))
return AnyValue::poison();
if (I.hasNoUnsignedWrap() && RHS.ugt(LHS.countl_zero()))
return AnyValue::poison();
return LHS.shl(RHS);
});
}
void visitLShr(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) -> AnyValue {
if (RHS.uge(cast<PossiblyExactOperator>(I).isExact()
? LHS.countr_zero() + 1
: LHS.getBitWidth()))
return AnyValue::poison();
return LHS.lshr(RHS);
});
}
void visitAShr(BinaryOperator &I) {
visitIntBinOp(I, [&](const APInt &LHS, const APInt &RHS) -> AnyValue {
if (RHS.uge(cast<PossiblyExactOperator>(I).isExact()
? LHS.countr_zero() + 1
: LHS.getBitWidth()))
return AnyValue::poison();
return LHS.ashr(RHS);
});
}
void visitICmpInst(ICmpInst &I) {
visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
if (LHS.isPoison() || RHS.isPoison())
return AnyValue::poison();
// TODO: handle pointer comparison.
const APInt &LHSVal = LHS.asInteger();
const APInt &RHSVal = RHS.asInteger();
if (I.hasSameSign() && LHSVal.isNonNegative() != RHSVal.isNonNegative())
return AnyValue::poison();
return AnyValue::boolean(
ICmpInst::compare(LHSVal, RHSVal, I.getPredicate()));
});
}
void visitSelect(SelectInst &SI) {
// TODO: handle fast-math flags.
if (SI.getCondition()->getType()->isIntegerTy(1)) {
switch (getValue(SI.getCondition()).asBoolean()) {
case BooleanKind::True:
setResult(SI, getValue(SI.getTrueValue()));
return;
case BooleanKind::False:
setResult(SI, getValue(SI.getFalseValue()));
return;
case BooleanKind::Poison:
setResult(SI, AnyValue::getPoisonValue(Ctx, SI.getType()));
return;
}
}
auto &Cond = getValue(SI.getCondition()).asAggregate();
auto &TV = getValue(SI.getTrueValue()).asAggregate();
auto &FV = getValue(SI.getFalseValue()).asAggregate();
std::vector<AnyValue> Res;
size_t Len = Cond.size();
Res.reserve(Len);
for (uint32_t I = 0; I != Len; ++I) {
switch (Cond[I].asBoolean()) {
case BooleanKind::True:
Res.push_back(TV[I]);
break;
case BooleanKind::False:
Res.push_back(FV[I]);
break;
case BooleanKind::Poison:
Res.push_back(AnyValue::poison());
break;
}
}
setResult(SI, std::move(Res));
}
void visitAllocaInst(AllocaInst &AI) {
uint64_t AllocSize = Ctx.getEffectiveTypeAllocSize(AI.getAllocatedType());
if (AI.isArrayAllocation()) {
auto &Size = getValue(AI.getArraySize());
if (Size.isPoison()) {
reportImmediateUB("Alloca with poison array size.");
return;
}
if (Size.asInteger().getActiveBits() > 64) {
reportImmediateUB(
"Alloca with large array size that overflows uint64_t.");
return;
}
bool Overflowed = false;
AllocSize = SaturatingMultiply(AllocSize, Size.asInteger().getZExtValue(),
&Overflowed);
if (Overflowed) {
reportImmediateUB(
"Alloca with allocation size that overflows uint64_t.");
return;
}
}
// If it is used by llvm.lifetime.start, it should be initially dead.
bool IsInitiallyDead = any_of(AI.users(), [](User *U) {
return match(U, m_Intrinsic<Intrinsic::lifetime_start>());
});
auto Obj = Ctx.allocate(AllocSize, AI.getPointerAlignment(DL).value(),
AI.getName(), AI.getAddressSpace(),
IsInitiallyDead ? MemInitKind::Poisoned
: MemInitKind::Uninitialized);
if (!Obj) {
reportError("Insufficient stack space.");
return;
}
CurrentFrame->Allocas.push_back(Obj);
setResult(AI, Ctx.deriveFromMemoryObject(Obj));
}
void visitGetElementPtrInst(GetElementPtrInst &GEP) {
uint32_t IndexBitWidth =
DL.getIndexSizeInBits(GEP.getType()->getPointerAddressSpace());
GEPNoWrapFlags Flags = GEP.getNoWrapFlags();
AnyValue Res = getValue(GEP.getPointerOperand());
AnyValue AccumulatedOffset = APInt(IndexBitWidth, 0);
if (Res.isAggregate())
AccumulatedOffset =
AnyValue::getVectorSplat(AccumulatedOffset, Res.asAggregate().size());
auto ApplyScaledOffset = [&](const AnyValue &Index, const APInt &Scale) {
if (Index.isAggregate() && !Res.isAggregate()) {
Res = AnyValue::getVectorSplat(Res, Index.asAggregate().size());
AccumulatedOffset = AnyValue::getVectorSplat(
AccumulatedOffset, Index.asAggregate().size());
}
if (Index.isAggregate() && Res.isAggregate()) {
for (auto &&[ResElem, IndexElem, OffsetElem] :
zip(Res.asAggregate(), Index.asAggregate(),
AccumulatedOffset.asAggregate()))
ResElem = computeScaledPtrAdd(
ResElem, canonicalizeIndex(IndexElem, IndexBitWidth, Flags),
Scale, Flags, OffsetElem);
} else {
AnyValue CanonicalIndex =
canonicalizeIndex(Index, IndexBitWidth, Flags);
if (Res.isAggregate()) {
for (auto &&[ResElem, OffsetElem] :
zip(Res.asAggregate(), AccumulatedOffset.asAggregate()))
ResElem = computeScaledPtrAdd(ResElem, CanonicalIndex, Scale, Flags,
OffsetElem);
} else {
Res = computeScaledPtrAdd(Res, CanonicalIndex, Scale, Flags,
AccumulatedOffset);
}
}
};
for (gep_type_iterator GTI = gep_type_begin(GEP), GTE = gep_type_end(GEP);
GTI != GTE; ++GTI) {
Value *V = GTI.getOperand();
// Fast path for zero offsets.
if (auto *CI = dyn_cast<ConstantInt>(V)) {
if (CI->isZero())
continue;
}
if (isa<ConstantAggregateZero>(V))
continue;
// Handle a struct index, which adds its field offset to the pointer.
if (StructType *STy = GTI.getStructTypeOrNull()) {
unsigned ElementIdx = cast<ConstantInt>(V)->getZExtValue();
const StructLayout *SL = DL.getStructLayout(STy);
// Element offset is in bytes.
ApplyScaledOffset(
APInt(IndexBitWidth, SL->getElementOffset(ElementIdx)),
APInt(IndexBitWidth, 1));
continue;
}
// Truncate if type size exceeds index space.
// TODO: Should be documented in LangRef: GEPs with nowrap flags should
// return poison when the type size exceeds index space.
TypeSize Offset = GTI.getSequentialElementStride(DL);
APInt Scale(IndexBitWidth, Ctx.getEffectiveTypeSize(Offset),
/*isSigned=*/false, /*implicitTrunc=*/true);
if (!Scale.isZero())
ApplyScaledOffset(getValue(V), Scale);
}
setResult(GEP, std::move(Res));
}
void visitIntToPtr(IntToPtrInst &I) {
return visitUnOp(I, [&](const AnyValue &V) -> AnyValue {
if (V.isPoison())
return AnyValue::poison();
// TODO: expose provenance
// TODO: check metadata
return Pointer(V.asInteger().zextOrTrunc(
DL.getPointerSizeInBits(I.getType()->getPointerAddressSpace())));
});
}
void visitLoadInst(LoadInst &LI) {
auto RetVal =
load(getValue(LI.getPointerOperand()), LI.getAlign(), LI.getType());
// TODO: track volatile loads
// TODO: handle metadata
setResult(LI, std::move(RetVal));
}
void visitStoreInst(StoreInst &SI) {
auto &Ptr = getValue(SI.getPointerOperand());
auto &Val = getValue(SI.getValueOperand());
// TODO: track volatile stores
// TODO: handle metadata
store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType());
if (Status)
Status &= Handler.onInstructionExecuted(SI, AnyValue());
}
void visitInstruction(Instruction &I) {
Handler.onUnrecognizedInstruction(I);
Status = false;
}
void visitExtractValueInst(ExtractValueInst &EVI) {
auto &Res = getValue(EVI.getAggregateOperand());
const AnyValue *Pos = &Res;
for (unsigned Idx : EVI.indices())
Pos = &Pos->asAggregate()[Idx];
setResult(EVI, *Pos);
}
void visitInsertValueInst(InsertValueInst &IVI) {
AnyValue Res = getValue(IVI.getAggregateOperand());
AnyValue *Pos = &Res;
for (unsigned Idx : IVI.indices())
Pos = &Pos->asAggregate()[Idx];
*Pos = getValue(IVI.getInsertedValueOperand());
setResult(IVI, std::move(Res));
}
void visitInsertElementInst(InsertElementInst &IEI) {
auto Res = getValue(IEI.getOperand(0));
auto &ResVec = Res.asAggregate();
auto &Idx = getValue(IEI.getOperand(2));
if (Idx.isPoison() || Idx.asInteger().uge(ResVec.size())) {
setResult(IEI, AnyValue::getPoisonValue(Ctx, IEI.getType()));
return;
}
ResVec[Idx.asInteger().getZExtValue()] = getValue(IEI.getOperand(1));
setResult(IEI, std::move(Res));
}
void visitExtractElementInst(ExtractElementInst &EEI) {
auto &SrcVec = getValue(EEI.getOperand(0)).asAggregate();
auto &Idx = getValue(EEI.getOperand(1));
if (Idx.isPoison() || Idx.asInteger().uge(SrcVec.size())) {
setResult(EEI, AnyValue::getPoisonValue(Ctx, EEI.getType()));
return;
}
setResult(EEI, SrcVec[Idx.asInteger().getZExtValue()]);
}
void visitShuffleVectorInst(ShuffleVectorInst &SVI) {
auto &LHSVec = getValue(SVI.getOperand(0)).asAggregate();
auto &RHSVec = getValue(SVI.getOperand(1)).asAggregate();
uint32_t Size = cast<VectorType>(SVI.getOperand(0)->getType())
->getElementCount()
.getKnownMinValue();
std::vector<AnyValue> Res;
uint32_t DstLen = Ctx.getEVL(SVI.getType()->getElementCount());
Res.reserve(DstLen);
uint32_t Stride = SVI.getShuffleMask().size();
// For scalable vectors, we need to repeat the shuffle mask until we fill
// the destination vector.
for (uint32_t Off = 0; Off != DstLen; Off += Stride) {
for (int Idx : SVI.getShuffleMask()) {
if (Idx == PoisonMaskElem)
Res.push_back(AnyValue::poison());
else if (Idx < static_cast<int>(Size))
Res.push_back(LHSVec[Idx]);
else
Res.push_back(RHSVec[Idx - Size]);
}
}
setResult(SVI, std::move(Res));
}
void visitBitCastInst(BitCastInst &BCI) {
// The conversion is done as if the value had been stored to memory and read
// back as the target type.
SmallVector<Byte> Bytes;
Bytes.resize(Ctx.getEffectiveTypeStoreSize(BCI.getType()),
Byte::concrete(0));
Ctx.toBytes(getValue(BCI.getOperand(0)), BCI.getOperand(0)->getType(),
Bytes);
setResult(BCI, Ctx.fromBytes(Bytes, BCI.getType()));
}
void visitFreezeInst(FreezeInst &FI) {
AnyValue Val = getValue(FI.getOperand(0));
Ctx.freeze(Val, FI.getType());
setResult(FI, std::move(Val));
}
/// This function implements the main interpreter loop.
/// It handles function calls in a non-recursive manner to avoid stack
/// overflows.
bool runMainLoop() {
uint32_t MaxSteps = Ctx.getMaxSteps();
uint32_t Steps = 0;
while (Status && !CallStack.empty()) {
Frame &Top = CallStack.back();
CurrentFrame = &Top;
if (Top.State == FrameState::Entry) {
Handler.onFunctionEntry(Top.Func, Top.Args, Top.CallSite);
} else {
assert(Top.State == FrameState::Pending &&
"Expected to return from a callee.");
returnFromCallee();
}
Top.State = FrameState::Running;
// Interpreter loop inside a function
while (Status) {
assert(Top.State == FrameState::Running &&
"Expected to be in running state.");
if (MaxSteps != 0 && Steps >= MaxSteps) {
reportError("Exceeded maximum number of execution steps.");
break;
}
++Steps;
Instruction &I = *Top.PC;
visit(&I);
if (!Status)
break;
// A function call or return has occurred.
// We need to exit the inner loop and switch to a different frame.
if (Top.State != FrameState::Running)
break;
// Otherwise, move to the next instruction if it is not a terminator.
// For terminators, the PC is updated in the visit* method.
if (!I.isTerminator())
++Top.PC;
}
if (!Status)
break;
if (Top.State == FrameState::Exit) {
assert((Top.Func.getReturnType()->isVoidTy() || !Top.RetVal.isNone()) &&
"Expected return value to be set on function exit.");
Handler.onFunctionExit(Top.Func, Top.RetVal);
// Free stack objects allocated in this frame.
for (auto &Obj : Top.Allocas)
Ctx.free(Obj->getAddress());
CallStack.pop_back();
} else {
assert(Top.State == FrameState::Pending &&
"Expected to enter a callee.");
}
}
return Status;
}
};
bool Context::runFunction(Function &F, ArrayRef<AnyValue> Args,
AnyValue &RetVal, EventHandler &Handler) {
InstExecutor Executor(*this, Handler, F, Args, RetVal);
return Executor.runMainLoop();
}
} // namespace llvm::ubi