Repository navigation
Six SIMD packed stores expose stale typed destination values with GCC at -O2 #355
Description
After six packed-store calls, typed destination reads retain their initialized zero values with GCC 12.4 and 13.3 at -O2. For example, XMStoreColor stores opaque red but reading color.c gives 00000000 instead of ffff0000.
Tested: DirectXMath commit 5bbe2ceeec3236c27ad44c877715e1e0f3b9817a (DIRECTX_MATH_VERSION 321), Ubuntu 24.04.5 LTS x86-64, GCC 12.4.0 (Ubuntu 12.4.0-2ubuntu124.04.1), GCC 13.3.0 (Ubuntu 13.3.0-6ubuntu224.04.1), and Clang 18.1.3 (1ubuntu1). Public main matched this exact commit when checked on 2026-10-12. The Linux SAL setup follows the pinned README.
Reproduction
Save this as repro.cpp. This is the verified six-case testbench. The example is provided under CC0.
#include <algorithm>
#include <iterator>
#include <utility>
#include <cstdint>
#include <cstdio>
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
using namespace DirectX;
using namespace DirectX::PackedVector;
int main()
{
unsigned failures = 0;
// Initialize the documented packed member before storing, so an omitted
// update has a defined value. All expected fields are exact integers.
XMCOLOR color(uint32_t(0));
XMStoreColor(&color, XMVectorSet(1, 0, 0, 1));
constexpr uint32_t expectedColor = (255u << 24) | (255u << 16);
std::printf("XMStoreColor: got=%08x expected=%08x\n", color.c, expectedColor);
failures += color.c != expectedColor;
XMUDECN4 normalized(uint32_t(0));
XMStoreUDecN4(&normalized, XMVectorReplicate(1));
constexpr uint32_t expectedN = 1023u | (1023u << 10) | (1023u << 20) | (3u << 30);
std::printf("XMStoreUDecN4: got=%08x expected=%08x\n", normalized.v, expectedN);
failures += normalized.v != expectedN;
XMUDEC4 integer(uint32_t(0));
XMStoreUDec4(&integer, XMVectorSet(1, 2, 3, 1));
constexpr uint32_t expectedI = 1u | (2u << 10) | (3u << 20) | (1u << 30);
std::printf("XMStoreUDec4: got=%08x expected=%08x\n", integer.v, expectedI);
failures += integer.v != expectedI;
XMXDECN4 signedNormalized(uint32_t(0));
XMStoreXDecN4(&signedNormalized, XMVectorReplicate(1));
constexpr uint32_t expectedX = 511u | (511u << 10) | (511u << 20) | (3u << 30);
std::printf("XMStoreXDecN4: got=%08x expected=%08x\n", signedNormalized.v, expectedX);
failures += signedNormalized.v != expectedX;
XMBYTE4 bytes(uint32_t(0));
XMStoreByte4(&bytes, XMVectorSet(1, 2, 3, 4));
constexpr uint32_t expectedB = 1u | (2u << 8) | (3u << 16) | (4u << 24);
std::printf("XMStoreByte4: got=%08x expected=%08x\n", bytes.v, expectedB);
failures += bytes.v != expectedB;
XMUBYTEN4 normalizedBytes(uint32_t(0));
XMStoreUByteN4(&normalizedBytes, XMVectorReplicate(1));
constexpr uint32_t expectedNB = 0xffffffffu;
std::printf("XMStoreUByteN4: got=%08x expected=%08x\n", normalizedBytes.v, expectedNB);
failures += normalizedBytes.v != expectedNB;
std::printf("failures=%u\n", failures);
return failures ? 1 : 0;
}In a fresh directory containing repro.cpp, run the following as a shell script with GCC 13.3.0 as g++. Setup stops if cloning, revision checkout, downloading, or SAL checksum verification fails. The final command exits 1 when a check fails.
set -eu
git clone https://github.com/microsoft/DirectXMath.git DirectXMath
git -C DirectXMath checkout --detach 5bbe2ceeec3236c27ad44c877715e1e0f3b9817a
mkdir -p include
curl -fL https://raw.githubusercontent.com/dotnet/runtime/main/src/coreclr/pal/inc/rt/sal.h -o include/sal.h
echo 'e26ee611231a859b5a800eb2e1606e7ca03b8e3981d7c98cf1ca20c89b658b68 include/sal.h' | sha256sum -c -
g++ -std=c++17 -O2 -ffp-contract=off -Wno-deprecated-declarations \
-IDirectXMath/Inc -Iinclude -msse2 repro.cpp -o repro-sse2
./repro-sse2The SAL SHA-256 above is the header used for both the original run and independent verification. GCC 12.4.0 reproduces the same result when used in place of g++.
Expected and observed
The XMStoreColor algorithm scales RGBA by 255 and packs alpha at bit 24 and red at bit 16. For (1,0,0,1), (255u << 24) | (255u << 16) is ffff0000. The other expectations follow the linked per-function algorithms and use exact endpoint or integer channel codes.
| API and input | Expected packed value | Typed read with GCC SIMD at -O2 |
|---|---|---|
XMStoreColor, (1,0,0,1) |
ffff0000 |
00000000 |
XMStoreUDecN4, (1,1,1,1) |
ffffffff |
00000000 |
XMStoreUDec4, (1,2,3,1) |
40300801 |
00000000 |
XMStoreXDecN4, (1,1,1,1) |
dff7fdff |
00000000 |
XMStoreByte4, (1,2,3,4) |
04030201 |
00000000 |
XMStoreUByteN4, (1,1,1,1) |
ffffffff |
00000000 |
The program prints failures=6. In a control that reads each object's representation with memcpy, all six checks pass. This report concerns stale typed reads. The byte-copy control sees the correct bytes.
Compiler and undefined behavior check
Independent verification reproduced the six failures and found no caller-side type-punning or uninitialized read in this test. The source and controls support a library strict-aliasing defect; the observed failure is not evidence that GCC miscompiles valid C++. Every destination is initialized, and the caller reads documented members without pointer casts. Color initializes and reads the same active union member.
The undefined access is inside the library. Each final SSE store writes integer destination storage through a float* passed to _mm_store_ss. GCC's documented strict-aliasing rules do not exempt this address-and-cast access. Strict aliasing is enabled at -O2.
The affected stores are in Inc/DirectXPackedVector.inl: Color:2043, UDecN4:3008, UDec4:3132, XDecN4:2860, Byte4:3467, and UByteN4:3319.
Replacing only that final store in each function with assignment of the extracted integer lane makes all six checks pass with GCC 12.4, GCC 13.3, and Clang 18.1 in all four configurations below. For Color, the tested substitution is:
pDestination->c = static_cast<uint32_t>(_mm_cvtsi128_si32(vInt));The other five substitutions assign the extracted lane to pDestination->v. Arithmetic and caller code stay the same.
Controls and scope
Replace the ISA flags in the build command with the exact flags below to select each tested configuration.
| Configuration | ISA/control flags |
|---|---|
| SSE2 | -msse2 |
| SSE4.1 | -msse4.1 -D_XM_SSE4_INTRINSICS_ |
| AVX2/FMA/F16C | -mavx2 -mfma -mf16c |
| Scalar | -D_XM_NO_INTRINSICS_ |
With unchanged headers, GCC 12.4 and 13.3 fail all six checks on all three SIMD configurations at -O2. Both GCC versions pass the scalar configuration, all configurations at -O0, and all configurations at -O2 -fno-strict-aliasing. Clang 18.1 passes all four configurations.
Small standalone intrinsic probes did not reproduce the stale read. The claim is limited to these library calls and their optimization context at -O2. Windows, MSVC, ARM/NEON, other compiler versions, and the full upstream test suite were not tested. The store substitution is a directed attribution check, not a fix validated for every platform and input.
Related history
Issue 186, PR 306, and PR 311 cover the address-and-cast idiom fixed in swizzle/permute in May 2026. PR 347 discusses type-punning warnings in half conversion and ARM code. Those concern different functions and do not report these six packed-store results.
Found by ChipLab's AI verification agents and independently reproduced.
ChipLab ref: CLAB-2026-662
Activity