Skip to content

JIT: remove jit dlopen global#802

Open
LeeLee26 wants to merge 1 commit into
exaloop:developfrom
LeeLee26:user/leelee/remove-jit-dlopen-global
Open

JIT: remove jit dlopen global#802
LeeLee26 wants to merge 1 commit into
exaloop:developfrom
LeeLee26:user/leelee/remove-jit-dlopen-global

Conversation

@LeeLee26

Copy link
Copy Markdown
  • Background: Codon JIT previously relied on RTLD_GLOBAL when importing codon_jit , so LLVM ORC could resolve runtime symbols from the process-wide global symbol table. This is unsafe because it changes symbol visibility for the whole Python process and pollutes the global symbol table.
  • Implementation: We removed the RTLD_GLOBAL requirement and switched to a layered lookup model: LLVM ORC still uses GetForCurrentProcess() for common system/Python symbols, while Codon runtime symbols are resolved by explicitly registering libcodonrt as an additional dynamic library. The runtime path is discovered from the Codon install layout and canonicalized with LLVM filesystem utilities before loading.
@cla-bot cla-bot Bot added the cla-signed label May 11, 2026
@BI71317

BI71317 commented May 19, 2026

Copy link
Copy Markdown
Contributor

Hey, @LeeLee26 !

I’m not very familiar with JIT/linker internals, so this might be a naive question,

but I wanted to clarify the exact scope of the symbol-visibility improvement here.

MRE

I ran a small probe that checks whether a few Codon/GC runtime symbols are visible via dlsym(RTLD_DEFAULT, ...):

from __future__ import annotations

import ctypes
import os
import sys
import traceback


SYMBOLS = [
    "GC_malloc",
    "GC_init",
    "GC_get_version",
    "seq_alloc",
]


def section(title: str) -> None:
    print()
    print(f"== {title} ==")


def dlsym_default(symbol: str) -> int:
    libdl = ctypes.CDLL("libdl.so.2")
    libdl.dlsym.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
    libdl.dlsym.restype = ctypes.c_void_p
    return int(libdl.dlsym(ctypes.c_void_p(0), symbol.encode()) or 0)


def dump_visibility(stage: str) -> None:
    section(stage)
    print(f"dlopenflags: {sys.getdlopenflags()}")
    print(f"RTLD_GLOBAL bit: {bool(sys.getdlopenflags() & os.RTLD_GLOBAL)}")
    for symbol in SYMBOLS:
        addr = dlsym_default(symbol)
        print(f"{symbol}: {'VISIBLE' if addr else 'hidden'}" + (f" @ 0x{addr:x}" if addr else ""))


def main() -> int:
    dump_visibility("before import")

    section("import codon")
    try:
        import codon  # noqa: F401
        from codon.codon_jit import codon_library

        print("import codon: OK")
        print(f"codon_library(): {codon_library()!r}")
    except Exception:
        print("import codon: FAILED")
        traceback.print_exc()
        return 1

    dump_visibility("after import")

    section("first jit call")
    try:
        import codon

        @codon.jit
        def add_one(x):
            return x + 1

        print(f"jit result: {add_one(41)!r}")
    except Exception:
        print("first jit call: FAILED")
        traceback.print_exc()
        return 2

    dump_visibility("after first jit")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Result

On the current codon implementation, sys.getdlopenflags() included RTLD_GLOBAL:

dlopenflags: 258

When it comes to this PR Branch, Python dlopen flags no longer include RTLD_GLOBAL so that it came out:

dlopenflags: 2

But after importing Codon, those runtime symbols (GC_*, seq_*) still become visible through the process-default native symbol lookup scope:

$ python3 probe_gc_visibility.py 

== before import ==
dlopenflags: 2
RTLD_GLOBAL bit: False
GC_malloc: hidden
GC_init: hidden
GC_get_version: hidden
seq_alloc: hidden

== import codon ==
import codon: OK
codon_library(): '/home/swchoi/src/codon/install/lib/codon/libcodonc.so'

== after import ==
dlopenflags: 2
RTLD_GLOBAL bit: False
GC_malloc: VISIBLE @ 0x716ebbf95e50
GC_init: VISIBLE @ 0x716ebbf888b0
GC_get_version: VISIBLE @ 0x716ebbf82ed0
seq_alloc: VISIBLE @ 0x716ebbeb07a0

== first jit call ==
jit result: 42

== after first jit ==
dlopenflags: 2
RTLD_GLOBAL bit: False
GC_malloc: VISIBLE @ 0x716ebbf95e50
GC_init: VISIBLE @ 0x716ebbf888b0
GC_get_version: VISIBLE @ 0x716ebbf82ed0
seq_alloc: VISIBLE @ 0x716ebbeb07a0

One small clarification question about the wording in the PR description.

I confirmed that this PR removes the Python-level RTLD_GLOBAL import behavior for codon_jit: on this branch, sys.getdlopenflags() stays at 2 and no longer has the RTLD_GLOBAL bit set.

While checking that, I also noticed that after import codon, some runtime symbols such as GC_malloc, GC_init, GC_get_version, and seq_alloc are still visible via dlsym(RTLD_DEFAULT, ...).

Given the description:

pollutes the global symbol table

I wanted to make sure I’m reading the scope correctly. Is that phrase mainly referring to the previous codon_jit import path changing Python’s dlopen flags with RTLD_GLOBAL, rather than implying that all Codon/GC runtime symbols are expected to be hidden from RTLD_DEFAULT?

I’m asking just to avoid misunderstanding the intended guarantee of this PR.

@LeeLee26

LeeLee26 commented May 19, 2026

Copy link
Copy Markdown
Author

Hi @BI71317,

Thank you so much for taking the time to confirm its details and ask this great question—it really helps clarify the intent of this PR!

The reason Codon runtime symbols (like GC_*) are still accessible from the main process is that during jit_init, we call jit->getEngine()->addDynamicLibrary(rt) to load the Codon runtime. In the LLVM ORC JIT, this follows the execution chain:

Engine::addDynamicLibrary()
      -> DynamicLibrarySearchGenerator::Load()
      -> getPermanentLibrary()
      -> HandleSet::DLOpen()

On UNIX systems, HandleSet::DLOpen uses RTLD_GLOBAL by default. This means libcodonrt.so is still loaded with the global dlopen mode, which is why symbols like GC_malloc remain visible in your test.

Key difference from the original Python-side RTLD_GLOBAL behavior:

  • Original implementation: The Python-level RTLD_GLOBAL flag forced all dependent shared libraries of Codon JIT (including libcodonc.so) to be loaded with global scope, causing widespread symbol pollution.
  • This PR: Only libcodonrt.so is loaded with RTLD_GLOBAL via the LLVM ORC JIT’s internal mechanism, while other libraries (like libcodonc.so) remain hidden. This drastically reduces the scope of symbol pollution.

Regarding your question about whether all Codon/GC runtime symbols should be hidden:

Is that phrase mainly referring to the previous codon_jit import path changing Python’s dlopen flags with RTLD_GLOBAL, rather than implying that all Codon/GC runtime symbols are expected to be hidden from RTLD_DEFAULT?

Ideally, if we can restrict Codon runtime to expose only a limited, controlled set of symbols, we would not need to rely on loading via RTLD_DEFAULT at all. The only debatable point with this approach is that dependent libraries of the libcodonrt.so will also be loaded via dlopen with global scope.

To confirm once more: the phrase "pollutes the global symbol table" in the PR description specifically refers to the problematic Python dlopen flag modification in the old codon_jit import path — it does not mean we intend to hide all Codon/GC runtime symbols from RTLD_DEFAULT.

Thanks again for your careful review and valuable feedback!

@LeeLee26

LeeLee26 commented May 20, 2026

Copy link
Copy Markdown
Author

Hey @BI71317,

I have opened a new PR at #812 to further mitigate symbol pollution during JIT compilation caused by the global loading of libcodonrt.so. It includes the following key changes:

  • Symbol Isolation: Switched to dlopen with RTLD_LOCAL for loading libcodonrt, preventing its symbols from polluting the global process namespace.
  • Explicit Registration: Introduced a symbol register function (__codon_jit_runtime_init) in the runtime library; the JIT engine now uses a callback to explicitly register required symbols into the LLVM ORC session.
  • Resource Management: Added tracking of dynamic library handles in the Engine class to ensure proper cleanup via dlclose during destruction.
  • Dependency Resolution: Refined search generators for standard libraries (libgcc_s, libstdc++) to prevent the LLVM JIT backend from failing to locate symbols such as _Unwind_*.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants