[ORCJIT] Add shared session and high-level load_module#658
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a high-level loading API for the TVM-FFI ORCJIT addon, adding a process-wide shared default_session() and a unified load_module() method that accepts both file paths and in-memory bytes. It also implements support for deserializing embedded library binaries to reconstruct import trees, introduces recursive mutex synchronization to serialize compound JITDylib operations, and updates the C++ build system to bypass partial linking for single objects. The review feedback highlights several critical security and robustness issues in the newly added C++ binary blob parser, such as potential integer overflows, excessive memory allocation, out-of-bounds reads in CSR parsing, and null-pointer dereferences. Additionally, the reviewer noted a thread-safety issue in the lazy initialization of the Python default_session().
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| uint64_t ReadU64() { | ||
| TVM_FFI_CHECK(cursor_ + sizeof(uint64_t) <= size_, RuntimeError) | ||
| << "Corrupt library binary: unexpected end of blob"; |
There was a problem hiding this comment.
The check cursor_ + sizeof(uint64_t) <= size_ is susceptible to integer overflow if size_ is extremely large. To prevent integer overflow, rewrite the check using subtraction: size_ - cursor_ >= sizeof(uint64_t).
uint64_t ReadU64() {
TVM_FFI_CHECK(size_ - cursor_ >= sizeof(uint64_t), RuntimeError)
<< "Corrupt library binary: unexpected end of blob";| std::string ReadString() { | ||
| uint64_t nbytes = ReadU64(); | ||
| TVM_FFI_CHECK(cursor_ + nbytes <= size_, RuntimeError) | ||
| << "Corrupt library binary: string length exceeds blob"; |
There was a problem hiding this comment.
The check cursor_ + nbytes <= size_ is susceptible to integer overflow if nbytes is extremely large (e.g., 0xFFFFFFFFFFFFFFFF). If an overflow occurs, the check can be bypassed, leading to out-of-bounds reads or excessive memory allocation. To prevent integer overflow, rewrite the check using subtraction: nbytes <= size_ - cursor_.
std::string ReadString() {
uint64_t nbytes = ReadU64();
TVM_FFI_CHECK(nbytes <= size_ - cursor_, RuntimeError)
<< "Corrupt library binary: string length exceeds blob";| std::vector<uint64_t> ReadU64Vector() { | ||
| uint64_t count = ReadU64(); | ||
| std::vector<uint64_t> out; | ||
| out.reserve(static_cast<size_t>(count)); | ||
| for (uint64_t i = 0; i < count; ++i) { | ||
| out.push_back(ReadU64()); | ||
| } | ||
| return out; | ||
| } |
There was a problem hiding this comment.
In ReadU64Vector(), out.reserve(static_cast<size_t>(count)) is called with a count read directly from the binary blob. If count is extremely large or corrupt, this will attempt to allocate a massive amount of memory, leading to an out-of-memory crash (DoS).
Since each element in the vector is a uint64_t (8 bytes), the remaining size in the blob must be at least count * sizeof(uint64_t). We should validate this before reserving memory to prevent excessive allocation and out-of-bounds reads.
std::vector<uint64_t> ReadU64Vector() {
uint64_t count = ReadU64();
TVM_FFI_CHECK(count <= (size_ - cursor_) / sizeof(uint64_t), RuntimeError)
<< "Corrupt library binary: vector size exceeds remaining blob size";
std::vector<uint64_t> out;
out.reserve(static_cast<size_t>(count));
for (uint64_t i = 0; i < count; ++i) {
out.push_back(ReadU64());
}
return out;
}| std::vector<uint64_t> indptr = reader.ReadU64Vector(); | ||
| std::vector<uint64_t> child_indices = reader.ReadU64Vector(); | ||
| TVM_FFI_CHECK(!indptr.empty(), RuntimeError) << "Corrupt library binary: empty import tree"; | ||
| size_t num_modules = indptr.size() - 1; |
There was a problem hiding this comment.
The CSR (Compressed Sparse Row) import tree is parsed without validating that indptr elements are monotonically non-decreasing, that indptr[0] == 0, or that indptr.back() <= child_indices.size(). If any of these properties are violated, the loop for (uint64_t j = indptr[i]; j < indptr[i + 1]; ++j) can perform out-of-bounds reads on child_indices.
Add validation checks for the CSR structure to ensure safety.
std::vector<uint64_t> indptr = reader.ReadU64Vector();
std::vector<uint64_t> child_indices = reader.ReadU64Vector();
TVM_FFI_CHECK(!indptr.empty(), RuntimeError) << "Corrupt library binary: empty import tree";
TVM_FFI_CHECK(indptr[0] == 0, RuntimeError)
<< "Corrupt library binary: indptr[0] must be 0";
size_t num_modules = indptr.size() - 1;
for (size_t i = 0; i < num_modules; ++i) {
TVM_FFI_CHECK(indptr[i] <= indptr[i + 1], RuntimeError)
<< "Corrupt library binary: indptr must be non-decreasing";
}
TVM_FFI_CHECK(indptr.back() <= child_indices.size(), RuntimeError)
<< "Corrupt library binary: indptr.back() exceeds child_indices size";| for (size_t i = 0; i < modules.size(); ++i) { | ||
| for (uint64_t j = indptr[i]; j < indptr[i + 1]; ++j) { | ||
| uint64_t child = child_indices[j]; | ||
| TVM_FFI_CHECK(child < modules.size(), RuntimeError) | ||
| << "Corrupt library binary: child index out of range"; | ||
| modules[i]->ImportModule(modules[static_cast<size_t>(child)]); | ||
| } | ||
| } |
There was a problem hiding this comment.
If a custom module loader fails or returns an uninitialized/null Module, calling ImportModule on it will cause a segmentation fault. We should defensively check that both the parent and child modules are defined before importing.
// Wire the import tree (CSR) using the public ModuleObj::ImportModule.
for (size_t i = 0; i < modules.size(); ++i) {
for (uint64_t j = indptr[i]; j < indptr[i + 1]; ++j) {
uint64_t child = child_indices[j];
TVM_FFI_CHECK(child < modules.size(), RuntimeError)
<< "Corrupt library binary: child index out of range";
TVM_FFI_CHECK(modules[i].defined(), RuntimeError)
<< "Corrupt library binary: module at index " << i << " is null";
TVM_FFI_CHECK(modules[static_cast<size_t>(child)].defined(), RuntimeError)
<< "Corrupt library binary: child module at index " << child << " is null";
modules[i]->ImportModule(modules[static_cast<size_t>(child)]);
}
}| _default_session: ExecutionSession | None = None | ||
|
|
||
|
|
||
| def default_session() -> ExecutionSession: | ||
| """Return the process-wide shared execution session. | ||
|
|
||
| A single leaked, never-destroyed session shared by all callers in the | ||
| process, so they share one LLVM ``ExecutionSession`` — hence process | ||
| symbols, the slab arena, and cross-library linking. Created on first call | ||
| and cached for the lifetime of the process. | ||
|
|
||
| The ORC runtime path is resolved in order: an embedder-registered default | ||
| (via ``ffi_orcjit.SetDefaultOrcRuntimePath``), then the runtime bundled next | ||
| to this extension, then none. For an isolated session or a tuned arena, | ||
| construct an :class:`ExecutionSession` directly instead. | ||
|
|
||
| Returns | ||
| ------- | ||
| ExecutionSession | ||
| The shared execution session. | ||
|
|
||
| Examples | ||
| -------- | ||
| >>> import tvm_ffi_orcjit as oj | ||
| >>> session = oj.default_session() | ||
| >>> mod = session.load_module("dylib0.o") | ||
|
|
||
| """ | ||
| global _default_session | ||
| if _default_session is None: | ||
| # Honor an embedder-registered default; otherwise fall back to the | ||
| # runtime bundled next to the extension. This must happen before the | ||
| # first DefaultSession() call, which creates the leaked singleton. | ||
| if not _ffi_api.GetDefaultOrcRuntimePath() and (bundled := _find_orc_rt_library()): | ||
| _ffi_api.SetDefaultOrcRuntimePath(bundled) | ||
| _default_session = _ffi_api.DefaultSession() | ||
| return _default_session |
There was a problem hiding this comment.
In default_session(), the lazy initialization of _default_session is not thread-safe. Since _ffi_api.GetDefaultOrcRuntimePath() and _ffi_api.SetDefaultOrcRuntimePath() are FFI calls that may release the Python GIL, multiple threads calling default_session() concurrently during the first invocation could race, leading to multiple initializations of the process-wide shared session.
Use a threading.Lock to ensure thread-safe lazy initialization.
_default_session: ExecutionSession | None = None
_session_lock = __import__("threading").Lock()
def default_session() -> ExecutionSession:
"""Return the process-wide shared execution session.
A single leaked, never-destroyed session shared by all callers in the
process, so they share one LLVM ``ExecutionSession`` — hence process
symbols, the slab arena, and cross-library linking. Created on first call
and cached for the lifetime of the process.
The ORC runtime path is resolved in order: an embedder-registered default
(via ``ffi_orcjit.SetDefaultOrcRuntimePath``), then the runtime bundled next
to this extension, then none. For an isolated session or a tuned arena,
construct an :class:`ExecutionSession` directly instead.
Returns
-------
ExecutionSession
The shared execution session.
Examples
--------
>>> import tvm_ffi_orcjit as oj
>>> session = oj.default_session()
>>> mod = session.load_module("dylib0.o")
"""
global _default_session
if _default_session is None:
with _session_lock:
if _default_session is None:
# Honor an embedder-registered default; otherwise fall back to the
# runtime bundled next to the extension. This must happen before the
# first DefaultSession() call, which creates the leaked singleton.
if not _ffi_api.GetDefaultOrcRuntimePath() and (bundled := _find_orc_rt_library()):
_ffi_api.SetDefaultOrcRuntimePath(bundled)
_default_session = _ffi_api.DefaultSession()
return _default_session| context_symbol_refreshed_.store(false, std::memory_order_release); | ||
| } | ||
|
|
||
| Module ORCJITExecutionSessionObj::LoadModule(const Array<Any>& objects, const String& name) { |
| String path; | ||
|
|
||
| static DefaultOrcRuntimePath* Global() { | ||
| static auto* inst = new DefaultOrcRuntimePath(); |
There was a problem hiding this comment.
this is not needed, maybe we can simply have python register the tvm_ffi_orcjit.DefaultOrcRuntimePath function
| * | ||
| * \return The shared execution session. | ||
| */ | ||
| static ORCJITExecutionSession Default(); |
|
|
||
| // Compound topology op — serialize against concurrent create / add / lookup / | ||
| // teardown on this shared session (lock order: session lock first). | ||
| std::lock_guard<std::recursive_mutex> lock(session_mutex_); |
There was a problem hiding this comment.
do we need recursive muted here? seems normal one is fine
| * | ||
| * \return Reference to the recursive session mutex. | ||
| */ | ||
| std::recursive_mutex& mutex() { return session_mutex_; } |
| return ORCJITExecutionSession(orc_rt_path, slab_size_bytes); | ||
| }) | ||
| .def("orcjit.ExecutionSessionCreateDynamicLibrary", | ||
| .def("ffi_orcjit.DefaultSession", []() { return ORCJITExecutionSessionObj::Default(); }) |
There was a problem hiding this comment.
consider write tvm_ffi_orcjit as full path name
|
|
||
| // Compound lookup+init: serialize against concurrent session operations | ||
| // (lock order: session lock → refresh mutex → LLVM lookup). The returned | ||
| // Function, once resolved, is invoked lock-free on the hot path. |
There was a problem hiding this comment.
i think we can further simplify if we assume ORCJITDynamicLibrary is unit operation, we explicitly do refresh in LoadModule, then GetFunction do not need to do that, avoid exposing functions like SetLinkOrder AddObjectBytes, leave those as internal
968c95b to
7a2b8a5
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the tvm_ffi_orcjit addon to introduce a high-level load_module API and a process-wide shared default_session(), replacing the previous low-level incremental library loading mechanism. It also adds support for deserializing embedded library binaries and implements thread-safe locking for JIT operations. Additionally, single object files now bypass partial linking in extension.py to prevent section address rewriting issues. One critical issue was identified in the deserialization parser where an empty or single-element indptr vector can cause an out-of-bounds access when returning the root module; adding a size check is recommended to prevent undefined behavior.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
7a2b8a5 to
ccdeba2
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the tvm_ffi_orcjit addon to introduce a high-level, process-wide shared session API (default_session()) and a unified module loading API (load_module(...)), replacing the previous low-level incremental library APIs. It also implements support for deserializing and wiring embedded library binaries (__tvm_ffi__library_bin), improves thread safety with robust locking, and optimizes single-object compilation by bypassing partial linking. Feedback on the changes highlights a security concern in ProcessEmbeddedLibraryBin where the untrusted nbytes header from the embedded binary is read without an upper-bound check, which could lead to integer overflow or out-of-memory crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| Module ProcessEmbeddedLibraryBin(const char* library_bin, const Module& lib_module) { | ||
| uint64_t nbytes = 0; | ||
| for (size_t i = 0; i < sizeof(nbytes); ++i) { | ||
| nbytes |= static_cast<uint64_t>(static_cast<unsigned char>(library_bin[i])) << (i * 8); | ||
| } | ||
| BlobReader reader(library_bin + sizeof(nbytes), static_cast<size_t>(nbytes)); |
There was a problem hiding this comment.
The nbytes header read from the embedded library binary is untrusted and can be arbitrarily large (e.g., due to corruption or malicious input). Relying on it directly to size the BlobReader and subsequently guard vector/string allocations can lead to integer overflow bypasses and process crashes via out-of-memory (OOM) or allocation failures (e.g., in reserve()). Enforcing a sane upper limit on nbytes (such as 2 GB) prevents these issues.
Module ProcessEmbeddedLibraryBin(const char* library_bin, const Module& lib_module) {
uint64_t nbytes = 0;
for (size_t i = 0; i < sizeof(nbytes); ++i) {
nbytes |= static_cast<uint64_t>(static_cast<unsigned char>(library_bin[i])) << (i * 8);
}
// Sanity check to prevent integer overflow or absurd memory allocation if the header is corrupt.
constexpr uint64_t kMaxLibraryBinSize = 2ULL * 1024 * 1024 * 1024; // 2 GB
TVM_FFI_CHECK(nbytes < kMaxLibraryBinSize, RuntimeError)
<< "Corrupt library binary: size header " << nbytes << " exceeds maximum allowed size (2 GB)";
BlobReader reader(library_bin + sizeof(nbytes), static_cast<size_t>(nbytes));ad860f6 to
43c90af
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the tvm_ffi_orcjit addon to introduce a high-level, unified loading API. It replaces the low-level incremental library API (create_library and add) with ExecutionSession.load_module, which accepts file paths, in-memory object bytes, or a sequence of both, returning a plain tvm_ffi.Module. It also introduces default_session(), a process-wide shared execution session, and implements support for deserializing embedded library binaries (__tvm_ffi__library_bin) to reconstruct import trees. Additionally, a copy_object rule is added to the C++ builder to bypass ld -r for single object files, preventing section address rewriting issues. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
bc8987c to
c85377d
Compare
Introduce a process-wide shared ExecutionSession and a high-level load_module unit operation on the ORCJIT addon, replacing the low-level create_library / add / set_link_order surface. - Session: GlobalDefault() singleton resolving the ORC runtime path from a Python-registered tvm_ffi_orcjit.DefaultOrcRuntimePath hook; plain (non-recursive) mutex; low-level dylib ops (add object / lookup / finalize) are internal, composed by LoadModule. - load_module(objects, name, keep_module_alive): links one or more object files/images into a fresh JITDylib, injects context symbols eagerly, and expands any embedded library binary. keep_module_alive pins the module in the runtime's global registry so JIT-allocated Objects may outlive the local handle. - Reduced in-addon library-binary parser with adversarial bounds checks. - CI: build the wheel against the in-tree core (--no-build-isolation) so the addon's tvm_ffi ABI matches the core the tests reinstall. - Link libstdc++/libgcc dynamically and hide static-archive (LLVM/zlib/zstd) symbols via --exclude-libs,ALL, so the JIT resolves the C++ runtime from the process without leaking LLVM symbols that could interpose with the host. - Tests for session/load_module, malformed blobs, concurrency, and container returns; docs and examples updated for the unit-op API.
c85377d to
ff5485a
Compare
Adds a process-wide shared execution session and a unified
load_moduleon thetvm_ffi_orcjitaddon, mirroring coretvm_ffi.load_module.Changes
default_session()— leaked, process-wide sharedExecutionSession; settable ORC-runtime-path registry resolved on first use.ExecutionSession.load_module(objects, name)— loads one or more objects (pathstr/Pathor in-memory bytes; single item or list) into one fresh JITDylib, injects context symbols eagerly, expands any embedded library binary into an import tree, and returns a plaintvm_ffi.Module.ffi_orcjit;set_link_orderkept but dropped from the advertised surface.cpp.build— copy a single object through instead ofld -r, which some system linkers use to stamp a nonzerosh_addron zero-size sections (e.g..note.GNU-stack), producing a JITLink block that overlaps inEHFrameEdgeFixerand breaks C++ (eh_frame) loads.Also folds in earlier addon cleanups (hide static-linked archive symbols; numpydoc docstrings).
Testing
New
tests/test_session_load_module.pycovers input shapes, eager context, drop, embedded-binary expansion, and concurrent shared-session use. Full addon suite green (154 passed, 3 skipped) on Linux aarch64.