blob: 571be140cc83f975ff803ce7c90435edde785234 [file] [edit]
//===--- Context.h - State Tracking for llubi -------------------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//
#ifndef LLVM_TOOLS_LLUBI_CONTEXT_H
#define LLVM_TOOLS_LLUBI_CONTEXT_H
#include "Value.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/Analysis/TargetLibraryInfo.h"
#include "llvm/AsmParser/AsmParserContext.h"
#include "llvm/IR/FPEnv.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Operator.h"
#include <map>
#include <optional>
#include <random>
namespace llvm::ubi {
enum class MemInitKind {
Zeroed,
Uninitialized,
Poisoned,
};
enum class MemAllocKind {
Global,
BlockAddress,
Stack,
Malloc,
New,
NewArray,
};
enum class MemoryObjectState {
// This memory object is accessible.
// Valid transitions:
// -> Dead (after the end of lifetime of an alloca)
// -> Freed (after free is called on a heap object)
Alive,
// This memory object is out of lifetime. Its contents are poison. Loads and
// memory transfers from it are allowed and propagate poison, stores to it
// cause immediate UB, and non-accessing operations such as getelementptr are
// allowed.
// Valid transition:
// -> Alive (after the start of lifetime of an alloca)
Dead,
// This heap memory object has been freed. Any access to it
// causes immediate UB. Like dead objects, it is still possible to
// perform operations that do not access its content.
Freed,
};
enum class UndefValueBehavior {
NonDeterministic, // Each use of the undef value can yield different results.
Zero, // All uses of the undef value yield zero.
};
enum class NaNPropagationBehavior {
NonDeterministic, // Non-deterministically choose from valid NaN results
PreferredNaN, // The quiet bit is set and the payload is all-zero
QuietingNaN, // The quiet bit is set and the payload is copied from any input
// operand that is a NaN
UnchangedNaN, // The quiet bit and payload are copied from any input operand
// that is a NaN
TargetSpecificNaN // The quiet bit is set and the payload is picked from a
// known target-specific set of "extra" possible NaN
// payloads
};
struct ProgramExitInfo {
enum class ProgramExitKind {
// Program exited via a normal return
Returned,
// Program exited with an interpreter error (UB/Unsupported
// instruction/etc.)
Failed,
// Program exited via a call to exit()
Exited,
// Program exited via a call to abort()
Aborted,
// Program exited via a call to terminate()
Terminated,
};
ProgramExitKind Kind;
uint64_t ExitCode;
explicit ProgramExitInfo(ProgramExitKind Kind, uint64_t ExitCode)
: Kind(Kind), ExitCode(ExitCode) {}
bool isExitedByLibcall() const {
return Kind == ProgramExitKind::Exited ||
Kind == ProgramExitKind::Aborted ||
Kind == ProgramExitKind::Terminated;
}
};
class MemoryObject : public RefCountedBase<MemoryObject> {
uint64_t Address;
uint64_t Size;
SmallVector<Byte, 8> Bytes;
StringRef Name;
unsigned AS;
MemoryObjectState State;
MemAllocKind AllocKind;
bool IsConstant = false;
bool IsIRGlobalValue = false;
// Tagged provenances related to this memory object.
// It is used to erasing the tags after the memory object is freed.
SmallVector<APInt> AssociatedTags;
friend class Context;
public:
MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name, unsigned AS,
MemInitKind InitKind, MemAllocKind AllocKind,
bool IsIRGlobalValue = false);
MemoryObject(const MemoryObject &) = delete;
MemoryObject(MemoryObject &&) = delete;
MemoryObject &operator=(const MemoryObject &) = delete;
MemoryObject &operator=(MemoryObject &&) = delete;
~MemoryObject();
uint64_t getAddress() const { return Address; }
uint64_t getSize() const { return Size; }
StringRef getName() const { return Name; }
unsigned getAddressSpace() const { return AS; }
MemoryObjectState getState() const { return State; }
void setState(MemoryObjectState S) { State = S; }
MemAllocKind getAllocKind() const { return AllocKind; }
bool isIRGlobalValue() const { return IsIRGlobalValue; }
bool isConstant() const { return IsConstant; }
void setIsConstant(bool C) { IsConstant = C; }
bool inBounds(const APInt &NewAddr) const {
return NewAddr.uge(Address) && NewAddr.ule(Address + Size);
}
Byte &operator[](uint64_t Offset) {
assert(Offset < Size && "Offset out of bounds");
return Bytes[Offset];
}
ArrayRef<Byte> getBytes() const { return Bytes; }
MutableArrayRef<Byte> getBytes() { return Bytes; }
bool isGlobal() const;
bool isStackAllocated() const;
bool isHeapAllocated() const;
};
/// An interface for handling events and managing outputs during interpretation.
/// If the handler returns false from any of the methods, the interpreter will
/// stop execution immediately.
class EventHandler {
public:
virtual ~EventHandler() = default;
virtual bool onInstructionExecuted(Instruction &I, const AnyValue &Result) {
return true;
}
virtual void onError(StringRef Msg) {}
virtual void onUnrecognizedInstruction(Instruction &I) {}
virtual void onImmediateUB(StringRef Msg) {}
virtual bool onBBJump(Instruction &I, BasicBlock &To) { return true; }
virtual bool onFunctionEntry(Function &F, ArrayRef<AnyValue> Args,
CallBase *CallSite) {
return true;
}
virtual bool onFunctionExit(Function &F, const AnyValue &RetVal) {
return true;
}
virtual void onProgramExit(const ProgramExitInfo &ExitInfo) {}
virtual bool onPrint(StringRef Msg) {
outs() << Msg;
outs().flush();
return true;
}
};
/// Endianness aware accessor for bytes.
template <typename ArrayRefT> class BytesView {
ArrayRefT Bytes;
bool IsLittleEndian;
public:
explicit BytesView(ArrayRefT Ref, bool IsLittleEndian)
: Bytes(Ref), IsLittleEndian(IsLittleEndian) {}
explicit BytesView(ArrayRefT Ref, const DataLayout &DL)
: BytesView(Ref, DL.isLittleEndian()) {}
auto &operator[](uint32_t Index) {
return Bytes[IsLittleEndian ? Index : Bytes.size() - 1 - Index];
}
size_t size() const { return Bytes.size(); }
};
using ConstBytesView = BytesView<ArrayRef<Byte>>;
using MutableBytesView = BytesView<MutableArrayRef<Byte>>;
class MaterializedConstant : public AnyValue {
bool Cacheable;
public:
MaterializedConstant(std::nullopt_t) : Cacheable(false) {}
MaterializedConstant(AnyValue V, bool Cacheable)
: AnyValue(std::move(V)), Cacheable(Cacheable) {}
bool isCacheable() const { return Cacheable; }
};
/// The global context for the interpreter.
/// It tracks global state such as heap memory objects and floating point
/// environment.
class Context {
// Module
LLVMContext &Ctx;
Module &M;
const AsmParserContext *ParserContext;
const DataLayout &DL;
const TargetLibraryInfoImpl TLIImpl;
// Configuration
uint64_t MaxMem = 0;
uint32_t VScale = 4;
uint32_t MaxSteps = 0;
uint32_t MaxStackDepth = 256;
bool Deterministic = false;
UndefValueBehavior UndefBehavior = UndefValueBehavior::NonDeterministic;
NaNPropagationBehavior NaNBehavior = NaNPropagationBehavior::NonDeterministic;
bool FusedMultiplyAdd = false;
std::mt19937_64 Rng;
/// Always returns a random APInt value. It is not controlled by
/// Deterministic.
APInt generateRandomAPInt(uint32_t BitWidth);
// Memory
uint64_t UsedMem = 0;
// The addresses of memory objects are monotonically increasing.
// For now we don't model the behavior of address reuse, which is common
// with stack coloring.
uint64_t AllocationBase = 8;
// All live memory objects.
DenseMap<uint64_t, IntrusiveRefCntPtr<MemoryObject>> MemoryObjects;
// Mapping from tags to provenances. Tags are lazily generated when a
// pointer is captured by memory.
DenseMap<APInt, IntrusiveRefCntPtr<Provenance>> TaggedProvenances;
// Maintains a global list of 'exposed' provenances. This is used to convert
// an address back to a pointer with a previously exposed provenance. In
// theory the provenance is picked from all previously exposed provenances
// using angelic non-determinism. Since llubi is just an interpreter, we make
// two approximations:
// 1. Each address maps to at most one memory object during the execution of
// the program, as AllocationBase increases monotonically.
// 2. We maintain the set of exposed provenances. When ptrtoint executes,
// the provenance is inserted to the set. When inttoptr executes, it yields
// a pointer with a wildcard provenance. That is, each later use will check
// whether there is an exposed provenance in the snapshot allowing the
// operation. The invalid provenance will be masked out after the operation.
// If we cannot pick one, it is UB.
/// Exposed provenances are grouped by associated memory objects for efficient
/// invalidation.
struct ExposedProvenance {
IntrusiveRefCntPtr<Provenance> Prov;
uint64_t Generation;
bool operator<(const ExposedProvenance &RHS) const {
return Generation < RHS.Generation;
}
};
struct ExposedProvenanceSet {
// (Provenance, Generation)
SmallVector<ExposedProvenance> List;
// FIXME: Implement a partial order comparator for provenance instead of
// deduplicating by pointers.
SmallPtrSet<Provenance *, 4> Set;
};
std::map<uint64_t, ExposedProvenanceSet> ExposedProvenances;
// Global version number for the set of exposed provenances.
uint64_t ExposedProvenanceSetGeneration = 0;
/// Get the tag for the given pointer provenance.
APInt getTag(uint32_t BitWidth, Provenance &Prov);
AnyValue fromBytes(ConstBytesView Bytes, Type *Ty, uint32_t OffsetInBits,
bool CheckPaddingBits, bool *ContainsUndefinedBits);
void toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
MutableBytesView Bytes, bool PaddingBits);
AnyValue computePtrAdd(const Pointer &Ptr, const APInt &Offset,
GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset);
AnyValue computePtrAdd(const AnyValue &Ptr, const APInt &Offset,
GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset);
AnyValue computeScaledPtrAdd(const AnyValue &Ptr, const AnyValue &Index,
const APInt &Scale, GEPNoWrapFlags Flags,
AnyValue &AccumulatedOffset);
// Constants
// Use std::map to avoid iterator/reference invalidation.
std::map<Constant *, MaterializedConstant> ConstCache;
// Temporary buffer for non-cacheable constants (e.g.,
// undef/ptrtoint/inttoptr).
SpecificBumpPtrAllocator<MaterializedConstant> NoncacheableConstBuffer;
size_t NoncacheableConstCount = 0;
DenseMap<Function *, Pointer> FuncAddrMap;
DenseMap<BasicBlock *, Pointer> BlockAddrMap;
DenseMap<uint64_t, std::pair<Function *, IntrusiveRefCntPtr<MemoryObject>>>
ValidFuncTargets;
DenseMap<uint64_t, std::pair<BasicBlock *, IntrusiveRefCntPtr<MemoryObject>>>
ValidBlockTargets;
DenseMap<GlobalVariable *, Pointer> GlobalAddrMap;
MaterializedConstant getConstantValueImpl(Constant *C);
MaterializedConstant evaluateConstantExpression(ConstantExpr *CE);
// Floating-point environment
RoundingMode CurrentRoundingMode = RoundingMode::NearestTiesToEven;
fp::ExceptionBehavior CurrentExceptionBehavior =
fp::ExceptionBehavior::ebIgnore;
// TODO: errno
public:
explicit Context(Module &M, const AsmParserContext *ParserContext);
Context(const Context &) = delete;
Context(Context &&) = delete;
Context &operator=(const Context &) = delete;
Context &operator=(Context &&) = delete;
~Context();
void setMemoryLimit(uint64_t Max) { MaxMem = Max; }
void setVScale(uint32_t VS) { VScale = VS; }
void setMaxSteps(uint32_t MS) { MaxSteps = MS; }
void setMaxStackDepth(uint32_t Depth) { MaxStackDepth = Depth; }
void setFusedMultiplyAdd(bool F) { FusedMultiplyAdd = F; }
uint64_t getMemoryLimit() const { return MaxMem; }
uint32_t getVScale() const { return VScale; }
uint32_t getMaxSteps() const { return MaxSteps; }
uint32_t getMaxStackDepth() const { return MaxStackDepth; }
void setDeterministic(bool D) { Deterministic = D; }
bool isDeterministic() const { return Deterministic; }
bool mayUseNonDeterminism() const { return !Deterministic; }
UndefValueBehavior getEffectiveUndefValueBehavior() const;
NaNPropagationBehavior getEffectiveNaNPropagationBehavior() const;
bool fuseMultiplyAdd() const { return FusedMultiplyAdd; }
void setUndefValueBehavior(UndefValueBehavior UB) { UndefBehavior = UB; }
void setNaNPropagationBehavior(NaNPropagationBehavior NaNBehav) {
NaNBehavior = NaNBehav;
}
void reseed(uint32_t Seed) { Rng.seed(Seed); }
LLVMContext &getContext() const { return Ctx; }
Module &getModule() const { return M; }
const AsmParserContext *getParserContext() const { return ParserContext; }
const DataLayout &getDataLayout() const { return DL; }
const Triple &getTargetTriple() const { return M.getTargetTriple(); }
const TargetLibraryInfoImpl &getTLIImpl() const { return TLIImpl; }
/// Get the effective vector length for a vector type.
uint32_t getEVL(ElementCount EC) const {
if (EC.isScalable())
return VScale * EC.getKnownMinValue();
return EC.getFixedValue();
}
/// The result is multiplied by VScale for scalable type sizes.
uint64_t getEffectiveTypeSize(TypeSize Size) const {
if (Size.isScalable())
return VScale * Size.getKnownMinValue();
return Size.getFixedValue();
}
/// Returns DL.getTypeAllocSize/getTypeStoreSize for the given type.
/// An exception to this is that for scalable vector types, the size is
/// computed as if the vector has getEVL(ElementCount) elements.
uint64_t getEffectiveTypeAllocSize(Type *Ty);
uint64_t getEffectiveTypeStoreSize(Type *Ty);
/// Returns a pointer to an evaluated constant \p C. If it cannot be
/// evaluated, returns nullptr. Note that it returns a pointer to a temporary
/// buffer when \p C is not context-free. The caller is responsible for
/// calling resetNoncacheableConstantBuffer after all references are dropped.
const MaterializedConstant *getConstantValue(Constant *C);
void resetNoncacheableConstantBuffer();
IntrusiveRefCntPtr<MemoryObject> allocate(uint64_t Size, uint64_t Align,
StringRef Name, unsigned AS,
MemInitKind InitKind,
MemAllocKind AllocKind,
bool IsIRGlobalValue = false);
bool free(const MemoryObject &Obj);
/// Derive a pointer from a memory object with offset 0.
/// Please use Pointer's interface for further manipulations.
Pointer deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj);
/// Mark this provenance as exposed. It is no-op if it is not associated with
/// a memory object or a wildcard provenance.
void exposeProvenance(Provenance &Prov);
/// A helper to check both concrete and wildcard provenance. Please don't
/// report UB inside the \p Check callback due to the existence of wildcard
/// provenance.
/// Returns the resolved memory object if success. \p Ptr is guaranteed to be
/// within the bounds of the returned memory object. But the state is not
/// checked, for better diagnostic messages. If \p HasSideEffect is true, some
/// invalid provenances will be masked out. Note that in this case the caller
/// must report UB when the result is nullptr.
MemoryObject *checkProvenance(const Pointer &Ptr,
function_ref<bool(const Provenance &)> Check,
bool HasSideEffect = true);
/// Returns the snapshot of currently exposed provenances.
IntrusiveRefCntPtr<Provenance> getWildcardProvenance();
/// Convert byte sequence to a value of the given type. Uninitialized bits are
/// flushed according to the options.
/// If \p ContainsUndefinedBits is provided, it will be set to true when there
/// are poison or undef bits in the value (i.e., padding bits are ignored).
AnyValue fromBytes(ArrayRef<Byte> Bytes, Type *Ty,
bool *ContainsUndefinedBits = nullptr);
/// Convert a value to byte sequence. Padding bits are set to zero.
void toBytes(const AnyValue &Val, Type *Ty, MutableArrayRef<Byte> Bytes);
/// Direct memory load without checks.
AnyValue load(MemoryObject &MO, uint64_t Offset, Type *ValTy,
bool *ContainsUndefinedBits = nullptr);
/// Direct memory store without checks.
void store(MemoryObject &MO, uint64_t Offset, const AnyValue &Val,
Type *ValTy);
void storeRawBytes(MemoryObject &MO, uint64_t Offset, const void *Data,
uint64_t Size);
/// Freeze the value in-place.
void freeze(AnyValue &Val, Type *Ty);
AnyValue computeGEP(GEPOperator &GEP,
function_ref<const AnyValue &(Value *V)> GetValue);
Function *getTargetFunction(const Pointer &Ptr);
BasicBlock *getTargetBlock(const Pointer &Ptr);
/// Initialize global variables and function/block objects. This function
/// should be called before executing any function. Returns false if the
/// initialization fails (e.g., the memory limit is exceeded during
/// initialization).
bool initGlobalValues();
/// Execute the function \p F with arguments \p Args, and store the return
/// value in \p RetVal if the function is not void.
/// Returns a `ProgramExitInfo` indicating how the program finished:
/// Kind = Returned: The program executed successfully and returned normally.
/// Kind = Failed: The interpreter encountered an error and could not execute
/// the program.
/// Kind = Exited/Aborted/Terminated: The program ended via an
/// explicit call to `exit()`, `abort()`, or `terminate()`.
ProgramExitInfo runFunction(Function &F, ArrayRef<AnyValue> Args,
AnyValue &RetVal, EventHandler &Handler);
RoundingMode getCurrentRoundingMode() const;
fp::ExceptionBehavior getCurrentExceptionBehavior() const;
void setCurrentRoundingMode(RoundingMode RM);
void setCurrentExceptionBehavior(fp::ExceptionBehavior EB);
bool isDefaultFPEnv() const;
bool getRandomBool();
uint64_t getRandomUInt64();
};
} // namespace llvm::ubi
#endif