Skip to content

Six SIMD packed stores expose stale typed destination values with GCC at -O2 #355

Open

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-sse2

The 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions