Skip to content

heap-use-after-free in Ltx2DitForwardDevice: staged DiT weights are freed and read on the SAME thread, which is not the race #904 closed #1153

Description

@localai-bot

test_ltx2_device fails under ASan with a heap-use-after-free that is not the
cross-thread race #904 described and closed. The staged DiT weights are destroyed
and then read on the same thread, inside Ltx2DitForwardDevice.

Evidence

sanitize-cpu (address,undefined), job on PR #945's head 7389d2600, 2026-08-17
11:46 UTC. 99% tests passed, 1 tests failed out of 48572 - test_ltx2_device.

Read — thread T0:

READ of size 2 at 0x50a000038c40 thread T0
    #0 LoadF32At        src/vt/cpu/cpu_layernorm.cpp:33
    #1 operator()       src/vt/cpu/cpu_layernorm.cpp:121
    #2 AddKernel        (vt::cpu anonymous namespace)
    #6 operator()       src/vt/cpu/cpu_threadpool.cpp:452   (ParallelForRows)

Freed — also thread T0:

freed by thread T0 here:
    #1 FreeAligned64                       src/vt/cpu/cpu_backend.cpp:28
    #2 Free                                src/vt/cpu/cpu_backend.cpp:46
    #3 operator()                          src/vllm/model_executor/models/ltx2_device.cpp:1088
    #4 _M_dispose                          shared_ptr_base.h:527
   #14 vllm::Ltx2DitDeviceWeights::~Ltx2DitDeviceWeights()
   #17 vllm::Ltx2DitForwardDevice(vt::Queue&, vllm::Ltx2DitParams const&, ...)

Allocated:

    #1 AllocAligned64                      src/vt/cpu/cpu_backend.cpp:20
    #2 Alloc                               src/vt/cpu/cpu_backend.cpp:42
    #3 vllm::Ltx2StageDitWeightsToDevice(...)
    #4 DOCTEST_ANON_FUNC_76                tests/vllm/models/test_ltx2_device.cpp:724

The owning handle is out.storage.emplace_back(dev, [&backend](void* p) { backend.Free(p); })
in Ltx2StageDitWeightsToDevice (ltx2_device.cpp ~:1088), so every staged buffer's
lifetime is the Ltx2DitDeviceWeights::storage vector. Frame #14 shows that vector's
destructor running inside Ltx2DitForwardDevice, while view.data raw pointers
handed to the kernels still point into those buffers.

Why this is not #904

#904 ("frees staged buffers while a
threadpool worker is still reading them") and its duplicate
#905 were both closed COMPLETED on
2026-08-15, and describe a cross-thread race: T0 frees, a threadpool worker T2 reads.
Here both the free and the read are T0. ParallelForRows appears in the read stack,
but the faulting access is on the calling thread, so a thread-synchronisation fix does not
address it. The lifetime is simply ending too early on one thread.

This is the failure mode #904's own thread-framing invites: the free site identifies the
bug, not the reading thread.

Why it has not been caught

PR #945 is markdown, CI-workflow and include-only, and cannot reach either file — it is the
messenger, not the cause.

What to look at

Ltx2DitForwardDevice's handling of the Ltx2DitDeviceWeights it receives or constructs:
whether it takes it by value, rebinds it, or lets a temporary expire while vt::Tensor::data
raw pointers derived from storage remain in flight. The destructor firing at frame #14
inside the forward is the anomaly to explain.

Reopening #904 would also be reasonable if maintainers prefer one thread for this defect —
filed separately because the mechanism recorded there is not the one observed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions