clang-cl: Wrong this-pointer adjustment in vtable thunk for virtual function override through virtual inheritance across DLLs
- 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
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