From a60abe4f270094112710842ac271b4f1ebf3c277 Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:24:40 -0700 Subject: [PATCH 1/2] Fix OrtGlobals::Allocator destruction order OrtGlobals::Allocator declared `allocator_` before `session_`, so destruction ran in the wrong order: `session_` was destroyed first, invalidating the session-scoped allocator per OrtApi::CreateAllocator's documented contract ("the allocator wraps the internal allocator from the OrtSession and becomes invalid when the session does"). ~allocator_ then released an invalid handle, crashing with INVALID_POINTER_READ_AVRF inside the ORT plugin-EP deleter lambda at shutdown. Reproduced deterministically by the Windows ML EP cert tool against the WebGPU EP under App Verifier (12 models / 53 tasks, 3 LLM AVRF failures). With this one-line swap and no other changes, all 53/53 tasks pass; all 10 app-verifier tasks succeed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/generators.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/generators.h b/src/generators.h index 514dc4069c..f70d599b9f 100644 --- a/src/generators.h +++ b/src/generators.h @@ -151,8 +151,14 @@ struct OrtGlobals { std::unique_ptr env_; struct Allocator { - std::unique_ptr allocator_; + // Field order matters here. The OrtAllocator returned by OrtApi::CreateAllocator (called via + // Ort::Allocator::Create) "wraps the internal allocator from the OrtSession and becomes invalid when the session + // does" -- see + // https://github.com/microsoft/onnxruntime/blob/3c8c46029735a89c8d1ea0aa6c1812db5b78ad72/include/onnxruntime/core/session/onnxruntime_c_api.h#L2852-L2862 + // Members are destroyed in reverse declaration order, so session_ must be declared BEFORE allocator_ so that + // ~allocator_ runs first. std::unique_ptr session_; + std::unique_ptr allocator_; }; Allocator device_allocators_[static_cast(DeviceType::MAX)]; From 733a9cadc5d390222318a8203b6319fd04c0e4af Mon Sep 17 00:00:00 2001 From: edgchen1 <18449977+edgchen1@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:47:21 -0700 Subject: [PATCH 2/2] Document OgaShutdown / OgaHandle / Allocator::Create lifetime contracts - ort_genai_c.h: expand OgaShutdown doxygen to warn that skipping the explicit shutdown can crash (globals destroyed in undefined static-destruction order), point C++/C# callers at the OgaHandle wrappers, and note that all GenAI handles must be released first. - ort_genai.h: add a docstring on OgaHandle mirroring the OgaShutdown structure, including a note that only one OgaHandle should be live in the process since GenAI's globals are not re-creatable after OgaShutdown(). - models/onnxruntime_api.h: document Allocator::Create's lifetime constraint (becomes invalid when the OrtSession is destroyed). This is the contract whose violation caused the destruction-order bug fixed in the previous commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/models/onnxruntime_api.h | 3 +++ src/ort_genai.h | 12 ++++++++++++ src/ort_genai_c.h | 11 ++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/models/onnxruntime_api.h b/src/models/onnxruntime_api.h index 55d71af55a..ff727f11b6 100644 --- a/src/models/onnxruntime_api.h +++ b/src/models/onnxruntime_api.h @@ -426,6 +426,9 @@ struct Abstract { */ struct Allocator : OrtAllocator { static Allocator& GetWithDefaultOptions(); ///< ::OrtAllocator default instance that is owned by Onnxruntime + /// Wraps the internal allocator owned by `session` (forwards to OrtApi::CreateAllocator). The returned Allocator + /// becomes invalid when `session` is destroyed -- callers must ensure the Allocator is destroyed before the + /// OrtSession. static std::unique_ptr Create(const OrtSession& session, const OrtMemoryInfo& memory_info); void* Alloc(size_t size); diff --git a/src/ort_genai.h b/src/ort_genai.h index b2e7cd43c7..15d9af9d0a 100644 --- a/src/ort_genai.h +++ b/src/ort_genai.h @@ -847,6 +847,18 @@ struct OgaEngine : OgaAbstract { static void operator delete(void* p) { OgaDestroyEngine(reinterpret_cast(p)); } }; +/** + * \brief RAII wrapper that calls OgaShutdown() on destruction. + * + * \warning Without explicit shutdown, GenAI's globals are destroyed at static-destruction time in undefined order, + * which may crash. + * + * \note Typical usage is to construct an instance early in the program so its destructor runs before process exit. + * + * \note Only one OgaHandle should be live in the process, and its scope must encompass all GenAI use. GenAI's globals + * are not re-creatable after OgaShutdown(); a second OgaHandle whose lifetime starts after the first one's + * destruction would leave subsequent GenAI calls broken. + */ struct OgaHandle { OgaHandle() = default; ~OgaHandle() noexcept { diff --git a/src/ort_genai_c.h b/src/ort_genai_c.h index f79c6ef42e..c553caa17c 100644 --- a/src/ort_genai_c.h +++ b/src/ort_genai_c.h @@ -88,7 +88,16 @@ typedef struct OgaStreamingProcessor OgaStreamingProcessor; */ /** - * \brief Call this on process exit to cleanly shutdown the genai library & its onnxruntime usage + * \brief Shuts down the GenAI library and releases all GenAI-owned ONNX Runtime globals. + * + * \warning Callers SHOULD invoke OgaShutdown() before process exit. If it is not called, GenAI's globals are destroyed + * at static-destruction time in undefined order, which may crash. + * + * \note C++ callers should prefer the OgaHandle RAII wrapper in ort_genai.h; C# callers should prefer the + * OgaHandle IDisposable wrapper. Both invoke OgaShutdown() on destruction. + * + * \note Must be the last GenAI call in the process. Any OgaModel / OgaGenerator / OgaTokenizer / etc. handles owned + * by the caller must be released first. */ OGA_EXPORT void OGA_API_CALL OgaShutdown();