clang 23 miscompilation due to strict aliasing violation #9557

Closed
opened 2026-09-25 15:49:32 +00:00 by sthalik · 0 comments
sthalik commented 2026-09-25 15:49:32 +00:00 (Migrated from github.com)

Version/Branch of Dear ImGui:

Version 1.92.9b-59-ge0a2f6dea, Branch: master

Back-ends:

Magnum

Compiler, OS:

clang 23.1.1, x86-64 MinGW + lld, Windows 11

Full config/build information:

No response

Details:

Please note that the report below was diagnosed and written by AI. I've reviewed it fully, but there's still a chance of complete nonsense. The assertion failure is very real however.

ImHashData: strict-aliasing violation miscompiled under LTO

Version, back-ends, compiler, OS

  • Dear ImGui 1.93.0 WIP (IMGUI_VERSION_NUM 19297), master, commit e0a2f6dea (2026-09-10),
    unmodified
  • Back-ends: Magnum ImGuiIntegration, Magnum GL renderer, Magnum Sdl2Application on SDL 2.32.
    No imgui_impl_*.cpp.
  • Microsoft Windows [Version 10.0.26200.9457], AMD Ryzen 9 9950X3D
  • clang version 23.1.1, target x86_64-w64-windows-gnu, lld 23.1.1, libc++, UCRT (mingw-w64)

Full config/build information

Dear ImGui 1.93.0 WIP (19297)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=202302
define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS
define: _WIN32
define: _WIN64
define: __MINGW32__
define: __MINGW64__
define: __GNUC__=4
define: __clang_version__=23.1.1
IM_ASSERT: runs expression: OK. expand size: OK

Not in that list:

  • IMGUI_DISABLE_DEFAULT_FONT
  • IMGUI_USER_CONFIG="Magnum/ImGuiIntegration/visibility.h", which only defines export macros
  • IMGUI_ENABLE_SSE4_2_CRC, which -mavx turns on through imgui_internal.h:66-73

The build flags don't matter: the repro below needs only -O2 -msse4.2 -flto.

Assert

Assertion failed: baked->Size == font_size && baked->OwnerFont == font && baked->BakedId == baked_id, file D:/dev/floormat/external/imgui/imgui_draw.cpp, line 5473

The app requests its font at 16px, later at 19px. Both requests get the same BakedId, so
BakedMap returns the 16px baked font and baked->Size == font_size fails. With NDEBUG,
every size of a font silently renders at the first size that was baked.

Problem

ImFontAtlasBakedGetId (imgui_draw.cpp:5429) hashes a struct holding two floats:

struct { ImGuiID FontId; float BakedSize; float RasterizerDensity; } hashed_data;
...
return ImHashData(&hashed_data, sizeof(hashed_data));

The SSE4.2 path of ImHashData (imgui.cpp:2495) reads it 4 bytes at a time:

crc = _mm_crc32_u32(crc, *(ImU32*)data);

That reads float objects through an unsigned int glvalue, which is undefined behaviour under
[basic.lval]. Clang's type-based alias analysis assumes an unsigned int load never observes a
float store. Once full LTO inlines ImHashData from imgui.cpp into its caller in
imgui_draw.cpp, the float stores are dropped, and the size never reaches the hash.

The same violation is in every ImHashData caller that hashes something other than int:

Call site Hashes
imgui.cpp:9345 GetID(const void*) a pointer
imgui.cpp:9372 GetIDFromPos an ImVec2
imgui.cpp:9381 GetIDFromRectangle (used by BeginDragDropTarget, BeginDragDropSource) an ImRect
imgui_draw.cpp:5435 ImFontAtlasBakedGetId the struct above

The int callers (imgui.cpp:7010, :7021, :9357, :9445) are legal: int may be
read as unsigned int.

Minimal repro

Two files, so that ImHashData gets inlined only at link time, as in ImGui.

// hash.cpp: ImHashData's SSE4.2 path, ImU32 and ImGuiID spelled unsigned
#include <nmmintrin.h>
#include <stddef.h>

unsigned ImHashData(const void* data_p, size_t data_size, unsigned seed)
{
    unsigned crc = ~seed;
    const unsigned char* data = (const unsigned char*)data_p;
    const unsigned char* data_end = data + data_size;
    while (data + 4 <= data_end)
    {
        crc = _mm_crc32_u32(crc, *(unsigned*)data);
        data += 4;
    }
    while (data < data_end)
        crc = _mm_crc32_u8(crc, *data++);
    return ~crc;
}
// main.cpp
#include <stddef.h>
#include <stdio.h>

unsigned ImHashData(const void* data_p, size_t data_size, unsigned seed);

unsigned ImFontAtlasBakedGetId(unsigned font_id, float baked_size, float rasterizer_density)
{
    struct { unsigned FontId; float BakedSize; float RasterizerDensity; } hashed_data;
    hashed_data.FontId = font_id;
    hashed_data.BakedSize = baked_size;
    hashed_data.RasterizerDensity = rasterizer_density;
    return ImHashData(&hashed_data, sizeof(hashed_data), 0);
}

int main()
{
    printf("%u %u\n", ImFontAtlasBakedGetId(1, 13, 1), ImFontAtlasBakedGetId(1, 16, 1));
}

The fix, under #ifdef FIX:

--- a/hash.cpp
+++ b/hash.cpp
@@ -1,5 +1,6 @@
 #include <nmmintrin.h>
 #include <stddef.h>
+#include <string.h>
 
 unsigned ImHashData(const void* data_p, size_t data_size, unsigned seed)
 {
@@ -8,7 +9,13 @@
     const unsigned char* data_end = data + data_size;
     while (data + 4 <= data_end)
     {
+#ifdef FIX
+        unsigned v;
+        memcpy(&v, data, sizeof(v));
+        crc = _mm_crc32_u32(crc, v);
+#else
         crc = _mm_crc32_u32(crc, *(unsigned*)data);
+#endif
         data += 4;
     }
     while (data < data_end)
$ clang++ -O2 -msse4.2 -flto hash.cpp main.cpp && ./a.exe
2389227250 2389227250
$ clang++ -O2 -msse4.2 -flto -DFIX hash.cpp main.cpp && ./a.exe
1315602646 2624216277
$ llvm-objdump -d -M intel --no-show-raw-insn --disassemble-symbols=main a.exe

Without FIX, main hashes the constant FontId three times and prints one result twice:

mov    eax, 0x1
mov    esi, 0xffffffff
crc32  esi, eax          ; FontId = 1
crc32  esi, eax          ; WRONG: should be BakedSize
crc32  esi, eax          ; WRONG: should be RasterizerDensity
not    esi
...
mov    edx, esi          ; both printf arguments
mov    r8d, esi

With FIX, the float bits 0x41500000 (13.0f), 0x41800000 (16.0f) and 0x3f800000 (1.0f)
go into crc32. 2389227250 is CRC32C of the words {1, 1, 1}, the same BakedId the debugger
showed for both requests in the app.

At -Os, main stores only FontId. Both float stores are gone, and the crc32 loop reads 8
bytes of uninitialized stack, so the id changes on every run. With __attribute__((noinline))
on ImFontAtlasBakedGetId, its out-of-line copy hashes the running CRC into itself
(crc32 eax, eax twice), as the out-of-line copy in the app does.

Correct: -O1, no LTO, -fno-strict-aliasing, or both functions in one file. -mcrc32 in
place of -msse4.2 also reproduces it, but ImGui enables its SSE4.2 path only for __SSE4_2__
or __AVX__. Tested only with this clang 23.1.1 on x86_64-w64-windows-gnu.

Fix

The same memcpy change at imgui.cpp:2495. A fixed 4-byte memcpy compiles to a plain load
that isn't typed unsigned int, so it sees the float stores. One line covers every caller in the
table above. Verified with all five imgui TUs and a small driver, full LTO: unpatched gives
2389227250 for every size, patched gives the CRC32C ids.

Workaround without patching: -DIMGUI_USE_LEGACY_CRC32_ADLER. It switches to the byte-table
CRC, which reads through unsigned char, and changes every ImGui ID.

But at what cost?

The memcpy(3) call costs nothing. Compiled on its own, ImHashData produces an identical instruction stream with and without -DFIX. I've checked with -O2 -msse4.2 and with -O3 -march=x86-64-v2 -mavx, and diff of the two .s files, ignoring assembler directives, was empty in both cases. The fixed loop:

mov    eax, 0x1
mov    esi, 0xffffffff
crc32  esi, eax          ; FontId = 1, both calls
mov    eax, 0x41800000
mov    edi, esi
crc32  edi, eax          ; BakedSize = 16.0f, 2nd call
mov    eax, 0x3f800000
crc32  edi, eax          ; RasterizerDensity = 1.0f, 2nd call
mov    ecx, 0x41500000
crc32  esi, ecx          ; BakedSize = 13.0f, 1st call
crc32  esi, eax          ; RasterizerDensity = 1.0f, 1st call
not    esi
not    edi
...
mov    edx, esi          ; 1st printf argument
mov    r8d, edi          ; 2nd printf argument

### Screenshots/Video:

_No response_

### Minimal, Complete and Verifiable Example code:

See `Minimal repro` in the previous section.
### Version/Branch of Dear ImGui: Version 1.92.9b-59-ge0a2f6dea, Branch: master ### Back-ends: Magnum ### Compiler, OS: clang 23.1.1, x86-64 MinGW + lld, Windows 11 ### Full config/build information: _No response_ ### Details: Please note that the report below was diagnosed and written by AI. I've reviewed it fully, but there's still a chance of complete nonsense. The assertion failure is very real however. # ImHashData: strict-aliasing violation miscompiled under LTO ## Version, back-ends, compiler, OS - Dear ImGui 1.93.0 WIP (`IMGUI_VERSION_NUM` 19297), master, commit `e0a2f6dea` (2026-09-10), unmodified - Back-ends: Magnum ImGuiIntegration, Magnum GL renderer, Magnum Sdl2Application on SDL 2.32. No `imgui_impl_*.cpp`. - Microsoft Windows [Version 10.0.26200.9457], AMD Ryzen 9 9950X3D - clang version 23.1.1, target x86_64-w64-windows-gnu, lld 23.1.1, libc++, UCRT (mingw-w64) ## Full config/build information ``` Dear ImGui 1.93.0 WIP (19297) -------------------------------- sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20 define: __cplusplus=202302 define: IMGUI_DISABLE_OBSOLETE_FUNCTIONS define: _WIN32 define: _WIN64 define: __MINGW32__ define: __MINGW64__ define: __GNUC__=4 define: __clang_version__=23.1.1 IM_ASSERT: runs expression: OK. expand size: OK ``` Not in that list: - `IMGUI_DISABLE_DEFAULT_FONT` - `IMGUI_USER_CONFIG="Magnum/ImGuiIntegration/visibility.h"`, which only defines export macros - `IMGUI_ENABLE_SSE4_2_CRC`, which `-mavx` turns on through `imgui_internal.h:66-73` The build flags don't matter: the repro below needs only `-O2 -msse4.2 -flto`. ## Assert ``` Assertion failed: baked->Size == font_size && baked->OwnerFont == font && baked->BakedId == baked_id, file D:/dev/floormat/external/imgui/imgui_draw.cpp, line 5473 ``` The app requests its font at 16px, later at 19px. Both requests get the same `BakedId`, so `BakedMap` returns the 16px baked font and `baked->Size == font_size` fails. With `NDEBUG`, every size of a font silently renders at the first size that was baked. ## Problem `ImFontAtlasBakedGetId` (`imgui_draw.cpp:5429`) hashes a struct holding two floats: ```cpp struct { ImGuiID FontId; float BakedSize; float RasterizerDensity; } hashed_data; ... return ImHashData(&hashed_data, sizeof(hashed_data)); ``` The SSE4.2 path of `ImHashData` (`imgui.cpp:2495`) reads it 4 bytes at a time: ```cpp crc = _mm_crc32_u32(crc, *(ImU32*)data); ``` That reads `float` objects through an `unsigned int` glvalue, which is undefined behaviour under [basic.lval]. Clang's type-based alias analysis assumes an `unsigned int` load never observes a `float` store. Once full LTO inlines `ImHashData` from `imgui.cpp` into its caller in `imgui_draw.cpp`, the float stores are dropped, and the size never reaches the hash. The same violation is in every `ImHashData` caller that hashes something other than `int`: | Call site | Hashes | |---|---| | `imgui.cpp:9345` `GetID(const void*)` | a pointer | | `imgui.cpp:9372` `GetIDFromPos` | an `ImVec2` | | `imgui.cpp:9381` `GetIDFromRectangle` (used by `BeginDragDropTarget`, `BeginDragDropSource`) | an `ImRect` | | `imgui_draw.cpp:5435` `ImFontAtlasBakedGetId` | the struct above | The `int` callers (`imgui.cpp:7010`, `:7021`, `:9357`, `:9445`) are legal: `int` may be read as `unsigned int`. ## Minimal repro Two files, so that `ImHashData` gets inlined only at link time, as in ImGui. ```cpp // hash.cpp: ImHashData's SSE4.2 path, ImU32 and ImGuiID spelled unsigned #include <nmmintrin.h> #include <stddef.h> unsigned ImHashData(const void* data_p, size_t data_size, unsigned seed) { unsigned crc = ~seed; const unsigned char* data = (const unsigned char*)data_p; const unsigned char* data_end = data + data_size; while (data + 4 <= data_end) { crc = _mm_crc32_u32(crc, *(unsigned*)data); data += 4; } while (data < data_end) crc = _mm_crc32_u8(crc, *data++); return ~crc; } ``` ```cpp // main.cpp #include <stddef.h> #include <stdio.h> unsigned ImHashData(const void* data_p, size_t data_size, unsigned seed); unsigned ImFontAtlasBakedGetId(unsigned font_id, float baked_size, float rasterizer_density) { struct { unsigned FontId; float BakedSize; float RasterizerDensity; } hashed_data; hashed_data.FontId = font_id; hashed_data.BakedSize = baked_size; hashed_data.RasterizerDensity = rasterizer_density; return ImHashData(&hashed_data, sizeof(hashed_data), 0); } int main() { printf("%u %u\n", ImFontAtlasBakedGetId(1, 13, 1), ImFontAtlasBakedGetId(1, 16, 1)); } ``` The fix, under `#ifdef FIX`: ```diff --- a/hash.cpp +++ b/hash.cpp @@ -1,5 +1,6 @@ #include <nmmintrin.h> #include <stddef.h> +#include <string.h> unsigned ImHashData(const void* data_p, size_t data_size, unsigned seed) { @@ -8,7 +9,13 @@ const unsigned char* data_end = data + data_size; while (data + 4 <= data_end) { +#ifdef FIX + unsigned v; + memcpy(&v, data, sizeof(v)); + crc = _mm_crc32_u32(crc, v); +#else crc = _mm_crc32_u32(crc, *(unsigned*)data); +#endif data += 4; } while (data < data_end) ``` ``` $ clang++ -O2 -msse4.2 -flto hash.cpp main.cpp && ./a.exe 2389227250 2389227250 $ clang++ -O2 -msse4.2 -flto -DFIX hash.cpp main.cpp && ./a.exe 1315602646 2624216277 $ llvm-objdump -d -M intel --no-show-raw-insn --disassemble-symbols=main a.exe ``` Without `FIX`, `main` hashes the constant `FontId` three times and prints one result twice: ```asm mov eax, 0x1 mov esi, 0xffffffff crc32 esi, eax ; FontId = 1 crc32 esi, eax ; WRONG: should be BakedSize crc32 esi, eax ; WRONG: should be RasterizerDensity not esi ... mov edx, esi ; both printf arguments mov r8d, esi ``` With `FIX`, the float bits `0x41500000` (13.0f), `0x41800000` (16.0f) and `0x3f800000` (1.0f) go into `crc32`. 2389227250 is CRC32C of the words `{1, 1, 1}`, the same `BakedId` the debugger showed for both requests in the app. At `-Os`, `main` stores only `FontId`. Both float stores are gone, and the `crc32` loop reads 8 bytes of uninitialized stack, so the id changes on every run. With `__attribute__((noinline))` on `ImFontAtlasBakedGetId`, its out-of-line copy hashes the running CRC into itself (`crc32 eax, eax` twice), as the out-of-line copy in the app does. Correct: `-O1`, no LTO, `-fno-strict-aliasing`, or both functions in one file. `-mcrc32` in place of `-msse4.2` also reproduces it, but ImGui enables its SSE4.2 path only for `__SSE4_2__` or `__AVX__`. Tested only with this clang 23.1.1 on x86_64-w64-windows-gnu. ## Fix The same `memcpy` change at `imgui.cpp:2495`. A fixed 4-byte `memcpy` compiles to a plain load that isn't typed `unsigned int`, so it sees the float stores. One line covers every caller in the table above. Verified with all five imgui TUs and a small driver, full LTO: unpatched gives 2389227250 for every size, patched gives the CRC32C ids. Workaround without patching: `-DIMGUI_USE_LEGACY_CRC32_ADLER`. It switches to the byte-table CRC, which reads through `unsigned char`, and changes every ImGui ID. ### But at what cost? The `memcpy(3)` call costs nothing. Compiled on its own, ImHashData produces an identical instruction stream with and without `-DFIX`. I've checked with `-O2 -msse4.2` and with `-O3 -march=x86-64-v2 -mavx`, and diff of the two `.s` files, ignoring assembler directives, was empty in both cases. The fixed loop: ```asm mov eax, 0x1 mov esi, 0xffffffff crc32 esi, eax ; FontId = 1, both calls mov eax, 0x41800000 mov edi, esi crc32 edi, eax ; BakedSize = 16.0f, 2nd call mov eax, 0x3f800000 crc32 edi, eax ; RasterizerDensity = 1.0f, 2nd call mov ecx, 0x41500000 crc32 esi, ecx ; BakedSize = 13.0f, 1st call crc32 esi, eax ; RasterizerDensity = 1.0f, 1st call not esi not edi ... mov edx, esi ; 1st printf argument mov r8d, edi ; 2nd printf argument ### Screenshots/Video: _No response_ ### Minimal, Complete and Verifiable Example code: See `Minimal repro` in the previous section.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ocornut/imgui#9557
No description provided.