Skip to content

Use-after-free: Managed<> releases UR resource through a freed adapter_impl (Windows/icx SEH 0xC0000005) #22367

Description

@koparasy

Describe the bug

Summary

sycl::detail::Managed<ur_program_handle_t> / Managed<ur_kernel_handle_t>
hold a raw adapter_impl * and, in ~Managed, call urProgramRelease /
urKernelRelease through it. Adapters are owned by GlobalHandler as raw
new/delete and are destroyed in GlobalHandler::unloadAdapters(). A
Managed that outlives adapter teardown therefore releases its UR resource
through a freed adapter_impl — a use-after-free.

On Windows built with icx this is deterministic and fatal: the freed
adapter's per-instance Windows function-pointer table (UrFuncPtrs) reads back
all-zero, so call_nocheck calls a null function pointer and the process
takes an access violation jumping to address 0x0
(SEH exception with code 0x3221225477 == 0xC0000005). On other
configurations (MSVC cl, Linux/gcc) the freed memory stays usable, so the
bug is latent and the same tests pass.

When it started

Bisected to #22047 ("[SYCL] Remove iostream from the SYCL header path",
commit 94a600c). That PR is not the cause — it only re-ordered the
include graph of adapter_impl.hpp (added #include <detail/backend_impl.hpp>
because the moved iostream_proxy.hpp no longer pulled it transitively). The
re-order shifted heap/object layout and teardown timing just enough to turn the
pre-existing latent use-after-free into a deterministic crash under icx.
build-win was green on the parent commit (#22305) and red from #22047 onward;
the follow-up fixes #22354 and #22356 did not address this (they fixed an
unrelated undefined reference and a separate real heap-overflow in the
BuildLog test mock respectively).

Root cause (confirmed via crash dump)

Full crash dump captured on the failing icx runner (Sysinternals procdump),
analyzed with the test exe + PDBs:

  • EXCEPTION_ACCESS_VIOLATION, RIP = 0x0, exception info [8, 0] (execute
    fault) — a call through a null function pointer.
  • Faulting stack:
    SubDevices.cpp:~191  (discarded Managed temporary from getBuiltURProgram)
      sycl::detail::Managed<ur_program_handle_t>::~Managed          adapter_impl.hpp
        sycl::detail::adapter_impl::call<urProgramRelease>          adapter_impl.hpp:124
          call_nocheck: F = UrApiInfo.getFuncPtr(&UrFuncPtrs); F(...)  adapter_impl.hpp:112  (F == nullptr)
    
  • The adapter_impl the Managed points at has:
    • MBackend and adapterReleased carrying 0xBAADF00D (uninitialized-heap)
      bytes, 0xFEEEFEEE (freed-heap) immediately before the object;
    • UrFuncPtrs (the Windows GetProcAddress table) entirely zero.
      A separate, correctly-populated mock adapter exists elsewhere in the dump,
      confirming the Managed references a freed/stale adapter, not the live one.

UrFuncPtrs is populated only in the adapter_impl constructor
(PopulateUrFuncPtrTable) and defaults every entry to nullptr; an all-zero
table is the signature of a freed/never-properly-initialized adapter object.

Who outlives the adapter

getBuiltURProgram() returns ResProgram.retain() — a Managed sharing the
adapter. The built program is also cached as a Managed in the context's
KernelProgramCache. Either the cached Managed or a discarded retain()
temporary can be destroyed at/after unloadAdapters() deletes the adapters.

To reproduce

Affected tests (Windows, icx, check-sycl-unittests / build-win)

  • SubDevices.BuildProgramForSubSubDevices (sycl/unittests/program_manager/SubDevices.cpp)
  • ImageRemoval.NativePrograms (sycl/unittests/program_manager/Cleanup.cpp)

Both fail only on the SYCL Post Commit build-win job, which builds with
cxx: icx. They pass with cl, on Linux, and under Linux AddressSanitizer.

Reproduction

SYCL Post Commitbuild-win (cxx=icx). Locally: build with
-DCMAKE_CXX_COMPILER=icx on Windows and run
ProgramManagerTests-Non_Preview_Tests.exe --gtest_filter=SubDevices.BuildProgramForSubSubDevices.

You may need to set these variables as well.
-DCMAKE_C_FLAGS="/fp:precise /clang:-Wno-nonportable-include-path /clang:-Wno-cast-function-type-mismatch" -DCMAKE_CXX_FLAGS="/fp:precise /clang:-Wno-nonportable-include-path /clang:-Wno-cast-function-type-mismatch" -DCMAKE_EXE_LINKER_FLAGS=/manifest:no -DCMAKE_MODULE_LINKER_FLAGS=/manifest:no -DCMAKE_SHARED_LINKER_FLAGS=/manifest:no

Environment

You can use this workflow . Set the compiler to icx and add these compiler options to the extra flags field:

-DCMAKE_C_FLAGS="/fp:precise /clang:-Wno-nonportable-include-path /clang:-Wno-cast-function-type-mismatch" -DCMAKE_CXX_FLAGS="/fp:precise /clang:-Wno-nonportable-include-path /clang:-Wno-cast-function-type-mismatch" -DCMAKE_EXE_LINKER_FLAGS=/manifest:no -DCMAKE_MODULE_LINKER_FLAGS=/manifest:no -DCMAKE_SHARED_LINKER_FLAGS=/manifest:no

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions