Skip to content

[eudsl-llvmpy] Free-threaded data races in unsynchronized static registries (casterMap + Python-pass registries) #614

Description

@makslevental

Summary

eudsl-llvmpy builds its nanobind extension with FREE_THREADED (Py_MOD_GIL_NOT_USED, see projects/eudsl-llvmpy/CMakeLists.txt:106), so it can be imported under a free-threaded (no-GIL) CPython. Several process-wide caches are plain unsynchronized std::unordered_maps. Concurrent writes and reads on them from multiple Python threads are data races.

This is flagged (not urgent): typical usage is register/populate at import, then run, so writes and reads don't actually overlap in practice, and the whole extension currently relies on nb::gil_scoped_acquire in the hot callbacks. But once we care about genuine multi-threaded use under the free-threaded build, these should be made safe together.

Affected registries

  1. Python-pass registries (added in the Python-IR-passes stack, src/IR/Passes.cpp):

    std::unordered_map<std::string, nb::callable> &pythonModulePassRegistry();
    std::unordered_map<std::string, nb::callable> &pythonFunctionPassRegistry();

    register_python_pass(...) writes; the PassBuilder::registerPipelineParsingCallback lambdas read them during run_passes(...). Concurrent register_python_pass (write) + run_passes (read) races.

    Interim mitigation already in place: the register_python_pass docstring tells callers to register from a single thread (e.g. at import) before running pipelines concurrently.

  2. Caster registry (src/IR/Casters.cpp:20):

    std::unordered_map<unsigned, nb::object> &casterMap();

    Written by the caster-registration path, read on every value cast. Same shape of race. The Python-pass registries were intentionally modeled on this existing/accepted pattern.

Possible fixes (to decide later)

  • Wrap each registry in a small accessor guarded by a std::mutex (or std::shared_mutex for read-mostly access), or
  • Document + enforce a "populate-before-use, then immutable" contract (freeze after import), or
  • Use a concurrent map / copy-on-write snapshot for the read-heavy lookup paths.

Whatever we pick, it should be applied consistently to both casterMap() and the two Python-pass registries, since they share the pattern.

References

  • projects/eudsl-llvmpy/src/IR/Passes.cpp — pythonModulePassRegistry() / pythonFunctionPassRegistry() and the parsing-callback readers
  • projects/eudsl-llvmpy/src/IR/Casters.cpp:20 — casterMap()
  • projects/eudsl-llvmpy/CMakeLists.txt:106 — FREE_THREADED build
  • Surfaced during review of [eudsl-llvmpy] Register Python passes by name for textual pipelines #605 (named Python-pass registration).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions