llvm / llvm/llvm-project

clang-cl: Wrong this-pointer adjustment in vtable thunk for virtual function override through virtual inheritance across DLLs

Open
#205,392 0 comments 0 reactions 0 assignees View on GitHub
clang-cl
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

I tried to switch from MSC to clang as newer gRPC does not compile wit MSC anymore ...
After some adjustments clang builds my stuff but I run into problems behind my scope.
My new, never tired, colleague Claude did his best to help and it seems to be a clang bug.

# [Bug] clang-cl: Wrong this-pointer adjustment in vtable thunk for virtual function override through virtual inheritance across DLLs

## Environment

- Clang version: 22.1.8 (clang-cl.exe)
- Target: i686-pc-windows-msvc (32-bit x86, cross-compiled from 64-bit host)
- Linker: lld-link.exe
- OS: Windows Server 2025 (10.0.26100)
- C++ Standard: C++20
- Flags: `/EHsc /Zp8` (tested both with and without `/vd2`)

## Description

When a virtual function override is dispatched through a virtual base class across DLL boundaries, the adjustor thunk in the vtable computes an incorrect `this` pointer. This results in an access violation at runtime.

The issue occurs with complex multiple inheritance involving virtual bases, where:
- A base class hierarchy is defined and exported from one DLL (e.g., `giOsal.dll`)
- A derived class in another DLL (e.g., `giAculabSrv.dll`) overrides a virtual function from a virtual base
- The virtual function is called through a pointer to the virtual base class

The same code compiles and runs correctly with MSVC (cl.exe 19.51).

## Class hierarchy (simplified)

```c++
// In DLL A (giOsal.dll):

class __declspec(dllexport) giLockableObject {
public:
virtual int lock() const = 0; // pure virtual
virtual int unlock() const = 0; // pure virtual
};

class __declspec(dllexport) giMultiObjectImpl
: virtual public giLockableObject // virtual inheritance
{
public:
int lock() const override; // override of pure virtual
int unlock() const override;
private:
struct Data;
Data* pData; // Pimpl, contains the actual mutex
};

// giMultiObjectImpl::lock() implemented in DLL A:
int giMultiObjectImpl::lock() const {
return pData->cSec.lock(); // delegates to member
}

class __declspec(dllexport) giCritSec
: virtual public giLockableObject // also virtual inheritance
{
public:
int lock() const override;
private:
struct CritSecData;
CritSecData* pD; // Pimpl, wraps CRITICAL_SECTION
};

// In DLL B (giAculabSrv.dll):

class giResProvider
: public giMultiObjectImpl // inherits from DLL A
{ };

class giTrunkProvider
: public virtual giResProvider
{ };

class AcuMonitorBase {
public:
virtual void dispatchEvent(const Event& ev) = 0;
};

class giAcuTrunkProvider final
: public giTrunkProvider, // deep chain through virtual bases
public AcuMonitorBase // second base, no virtual inheritance
{
void dispatchEvent(const Event& ev) override {
// This macro does: static_cast(*this).lock()
// Virtual dispatch should go to giMultiObjectImpl::lock()
GI_LOCK_THIS(); // <-- CRASH HERE
// ...
}
};
```

## What happens

1. `GI_LOCK_THIS()` expands to `static_cast(*this).lock()`
2. The vtable for `giLockableObject` in the `giAcuTrunkProvider` object correctly points to an adjustor thunk for `giMultiObjectImpl::lock`
3. The adjustor thunk computes the wrong `this` pointer for `giMultiObjectImpl`
4. The wrong `this` pointer causes `pData` to be read from invalid memory
5. Access violation when attempting to use `pData->cSec`

## Disassembly of the faulty adjustor thunk

The vtable entry for `lock()` on the `giLockableObject` virtual base dispatches through vtable slot +0x0C to this thunk in DLL B:

```asm
; giMultiObjectImpl::lock adjustor thunk (in giAculabSrv.dll)
; ecx = giLockableObject* (virtual base subobject within giAcuTrunkProvider)
sub ecx, dword ptr [ecx-4] ; vtordisp adjustment (value is 0 outside ctor)
mov eax, dword ptr [ecx-6Ch] ; read vbtable pointer from object
mov eax, dword ptr [eax+14h] ; read displacement from vbtable (= 0x80)
add ecx, eax ; ecx = giLockableObject* + 0x80
add ecx, 0FFFFFFE0h ; ecx -= 0x20
jmp dword ptr [import_giMultiObjectImpl_lock] ; tail-call to DLL A
```

The computed `this` pointer (`giLockableObject* + 0x60`) overshoots the actual object allocation by ~16 bytes, landing in uninitialized heap memory.

## Expected behavior

The adjustor thunk should compute the correct offset from the `giLockableObject` virtual base subobject to the `giMultiObjectImpl` subobject. With MSVC, this works correctly — the `giMultiObjectImpl` subobject is found at a smaller offset within the object.

## Observed behavior

| Build | ecx entering giCritSec::lock | Result |
|-------|------------------------------|--------|
| MSVC 19.51 | valid pointer | Works correctly |
| clang-cl 22.1.8 with `/vd2` | `0xe6a5e8d3` (garbage, unmapped memory) | Access violation |
| clang-cl 22.1.8 without `/vd2` | `0x00000018` (near-null) | Access violation |

## Key observations

- The vbtable displacement at index 5 (`[vbtable+0x14] = 0x80`) appears to be incorrect for the `giAcuTrunkProvider` object layout
- The error is consistent across `/vd2` and non-`/vd2` builds, but with different wrong offsets
- All DLLs are compiled with the same clang-cl, same flags — no mixed MSVC/Clang objects
- The hierarchy uses `__declspec(dllexport/dllimport)` across DLL boundaries
- The same binary works correctly when built with MSVC cl.exe 19.51

## Reproducer

A minimal reproducer would require two DLLs with:
1. DLL A: virtual base class with pure virtual function, intermediate class with virtual inheritance that overrides it, using Pimpl pattern
2. DLL B: deeply derived class with multiple inheritance (one chain through virtual bases to DLL A, one independent base), calling the overridden virtual function through `static_cast` to the virtual base

The critical ingredients appear to be:
- Virtual inheritance (`virtual public`) of the base class in multiple places in the hierarchy
- Cross-DLL virtual function override (override in DLL A, vtable constructed in DLL B)
- Multiple inheritance in the most-derived class
- 32-bit x86 target (i686-pc-windows-msvc)

Contributor guide

Open the contributing guide

Research direction

No source files or tests are named. Start by reducing the reported clang-cl 22.1.8 failure to the proposed two-DLL reproducer on i686-pc-windows-msvc, focusing on virtual inheritance, cross-DLL overrides, and the adjustor thunk shown in the report. Done should include a minimal reproducible case and correct dispatch matching MSVC behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.