Focus Deep-Dive

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:

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:

HookTriggers OnValidation
memoryLoadRegular loads (WorkItem / WorkGroup)checkLoad
memoryStoreRegular storescheckStore
memoryAtomicLoadAtomic readscheckLoad
memoryAtomicStoreAtomic writescheckStore
memoryMap / memoryUnmapHost buffer mappingTrack MapRegion list
instructionExecutedEvery LLVM IR instructionGEP 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");
    }
  }
}
Key insight: The map-region check only applies to global memory. Local and private buffers are never host-mapped in OpenCL, so the early return avoids unnecessary iteration over 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);
    }
  }
}
Limitation: GEP-chain checking only catches out-of-bounds accesses on statically sized arrays. Dynamically allocated buffers passed as kernel pointers are validated only by the address-validity check in 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.

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:

  1. m_context->logError(msg) — a simple one-line diagnostic for permission and map-region violations.
  2. Context::Message — a rich, multi-line message used by logInvalidAccess that 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

CheckMethodError Text
Out-of-bounds addresscheckLoad / checkStoreInvalid read/write of size N at … address 0xADDR
Read from WRITE_ONLYcheckLoadInvalid read from write-only buffer
Write to READ_ONLYcheckStoreInvalid write to read-only buffer
Read from write-mappedcheckLoadInvalid read from buffer mapped for writing
Write to mappedcheckStoreInvalid write to mapped buffer
Static array OOBcheckArrayAccessIndex (I) exceeds static array size (S)
Tip: Enable MemCheck at runtime with 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.

False negatives: MemCheck cannot detect use-after-free within a buffer that has been reallocated to a different kernel argument. The address may still be valid in the new allocation. Use the RaceDetector for cross-kernel access ordering.

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