Memory Checking (MemCheck)
How Oclgrind validates every load and store — bounds checking, permission enforcement, map-region tracking, and structured error reporting via Context::Message.
1 Overview
The MemCheck plugin intercepts every memory operation (load, store, atomic load/store, map, unmap) through the Oclgrind Plugin interface. It performs three categories of validation:
- Address validity — via
Memory::isAddressValid() - Permission flags —
CL_MEM_READ_ONLY/CL_MEM_WRITE_ONLY - Map-region conflicts — detecting reads/writes to currently mapped buffers
Additionally, it walks GetElementPtrInst chains to catch static array out-of-bounds errors at the LLVM-IR level.
graph TD
A[Memory Operation Fired] --> B{Load or Store?}
B -->|Load| C[checkLoad]
B -->|Store| D[checkStore]
C --> E{isAddressValid?}
D --> E
E -->|No| F[logInvalidAccess]
E -->|Yes| G{Permission OK?}
G -->|WRITE_ONLY on read| H[logError: invalid read]
G -->|READ_ONLY on write| I[logError: invalid write]
G -->|OK| J{Mapped Region Conflict?}
J -->|Conflict| K[logError: mapped buffer]
J -->|OK| L[Access Allowed]
2 Hook Points
MemCheck overrides the following Plugin virtual methods:
| Hook | Triggers On | Validation |
|---|---|---|
memoryLoad | Regular loads (WorkItem / WorkGroup) | checkLoad |
memoryStore | Regular stores | checkStore |
memoryAtomicLoad | Atomic reads | checkLoad |
memoryAtomicStore | Atomic writes | checkStore |
memoryMap / memoryUnmap | Host buffer mapping | Track MapRegion list |
instructionExecuted | Every LLVM IR instruction | GEP chain walk |
3 checkLoad & checkStore In Detail
Both functions follow the same three-stage pipeline but differ in their permission check:
void MemCheck::checkLoad(const Memory* memory, size_t address,
size_t size) const
{
// Stage 1: Address validity
if (!memory->isAddressValid(address, size))
{
logInvalidAccess(true, memory->getAddressSpace(), address, size);
return;
}
// Stage 2: Permission check
if (memory->getBuffer(address)->flags & CL_MEM_WRITE_ONLY)
{
m_context->logError("Invalid read from write-only buffer");
}
// Skip map-region check for local/private memory
if (memory->getAddressSpace() == AddrSpaceLocal ||
memory->getAddressSpace() == AddrSpacePrivate)
return;
// Stage 3: Map-region conflict detection
for (auto region = m_mapRegions.begin();
region != m_mapRegions.end(); region++)
{
if (region->type == MapRegion::WRITE &&
address < region->address + region->size &&
address + size >= region->address)
{
m_context->logError("Invalid read from buffer mapped for writing");
}
}
}
m_mapRegions.
4 GEP Chain Walking
When instructionExecuted fires for a LoadInst or StoreInst, MemCheck walks the chain of GetElementPtrInst (GEP) instructions that produced the pointer operand. For each GEP, it calls checkArrayAccess to validate every index against the static type bounds.
graph LR
SI[StoreInst / LoadInst] --> GEP1[GEP: array index 5]
GEP1 --> GEP2[GEP: struct field 2]
GEP2 --> BASE[Base Pointer]
GEP1 -.->|checkArrayAccess| V1{index < arraySize?}
GEP2 -.->|checkArrayAccess| V2{field valid?}
V1 -->|No| ERR[logError]
V1 -->|Yes| OK1[OK]
V2 -->|Yes| OK2[OK]
void MemCheck::checkArrayAccess(const WorkItem* workItem,
const llvm::GetElementPtrInst* GEPI) const
{
const llvm::Type* ptrType = GEPI->getPointerOperandType();
for (auto opIndex = GEPI->idx_begin();
opIndex != GEPI->idx_end(); opIndex++)
{
int64_t index = workItem->getOperand(opIndex->get()).getSInt();
if (ptrType->isArrayTy())
{
uint64_t size = ptrType->getArrayNumElements();
if ((uint64_t)index >= size)
{
ostringstream info;
info << "Index (" << index
<< ") exceeds static array size (" << size << ")";
m_context->logError(info.str().c_str());
}
ptrType = ptrType->getArrayElementType();
}
else if (ptrType->isPointerTy())
{
ptrType = GEPI->getSourceElementType();
}
else if (ptrType->isVectorTy())
{
ptrType = llvm::cast(ptrType)
->getElementType();
}
else if (ptrType->isStructTy())
{
ptrType = ptrType->getStructElementType(index);
}
}
}
checkLoad/checkStore.
5 Map-Region Tracking
When the host maps a buffer via clEnqueueMapBuffer, Oclgrind fires memoryMap. MemCheck records the address, offset, size, host pointer, and whether the mapping is READ or WRITE.
- A write-mapped region blocks device-side reads.
- Any mapped region blocks device-side writes (even read-mapped ones).
- On
memoryUnmap, the region is removed from the tracking list by matching the host pointer.
The overlap test uses the classic interval intersection: address < region.address + region.size && address + size >= region.address.
6 Error Reporting
MemCheck uses two error paths:
m_context->logError(msg)— a simple one-line diagnostic for permission and map-region violations.Context::Message— a rich, multi-line message used bylogInvalidAccessthat includes kernel name, entity (work-item or work-group), source location, and the invalid address in hex.
The Context::Message object supports stream-style insertion with special tokens like msg.INDENT, msg.CURRENT_KERNEL, msg.CURRENT_ENTITY, and msg.CURRENT_LOCATION for structured output.
7 Summary of Checks
| Check | Method | Error Text |
|---|---|---|
| Out-of-bounds address | checkLoad / checkStore | Invalid read/write of size N at … address 0xADDR |
| Read from WRITE_ONLY | checkLoad | Invalid read from write-only buffer |
| Write to READ_ONLY | checkStore | Invalid write to read-only buffer |
| Read from write-mapped | checkLoad | Invalid read from buffer mapped for writing |
| Write to mapped | checkStore | Invalid write to mapped buffer |
| Static array OOB | checkArrayAccess | Index (I) exceeds static array size (S) |
OCLGRIND_CHECK_API=1 or by passing --check-api to the Oclgrind command line. It is one of the default plugins loaded automatically.
8 Design Decisions & Trade-offs
8.1 Why Check Every Byte?
MemCheck validates the entire range [address, address+size) with a single call to isAddressValid. This catches misaligned vector loads that partially overlap a buffer boundary — a common bug in hand-optimized OpenCL kernels using vload16.
8.2 MapRegion as a Linked List
The m_mapRegions collection uses std::list rather than a vector. This avoids iterator invalidation during memoryUnmap calls that erase elements mid-iteration. The typical number of concurrent mappings is very small (single digits), so linear scan cost is negligible.
8.3 No Alignment Enforcement
Unlike hardware GPU memory controllers, MemCheck does not reject misaligned accesses. It only validates that the address range falls within an allocated buffer and that permissions are correct. This matches the OpenCL specification where alignment is undefined behavior rather than a guaranteed fault.
8.4 Atomic vs Regular Paths
Atomic loads and stores route through memoryAtomicLoad / memoryAtomicStore but ultimately call the same checkLoad / checkStore — the validation logic is identical. The separate hooks exist because the Plugin interface distinguishes atomics for other plugins (e.g., the RaceDetector uses the atomic flag to suppress false-positive race reports).