← 输入框|github.com|刷新
Skip to content

Is lazy type realization intended to be thread-safe on GIL builds (3.11)? #271

Open

Description

@calderwoodra

Context: we saw repeated SIGSEGVs in a threaded ASGI service on 3.11 that correlated with a transitive bump to cffi 2.0.0, and they stopped when we pinned back to 1.17.1. I'd like to know whether GIL builds are supposed to be safe here, so I know whether to keep looking elsewhere for our root cause.

Disclaimer: The below is written by AI. If that frustrates you, feel free to close. If not, thank you for reading on 🙏


Since 2.0.0 the type-realization paths carry explicit thread-safety machinery, but as far as I can tell all of it is compiled only for free-threaded builds. On a GIL build (I'm on CPython 3.11):

  • CFFI_LOCK() / CFFI_UNLOCK() expand to Py_BEGIN/END_CRITICAL_SECTION, and misc_thread_common.h redefines those to a bare { / } for PY_VERSION_HEX <= 0x030d00b3. So on 3.11 the lock is an empty scope.
  • The publish in _realize_c_struct_or_union is cffi_atomic_store(&builder->ctx.types[s->type_index], x) under Py_GIL_DISABLED, and a plain store otherwise.
  • The primitive pre-initialization loop in init_global_types_dict — the one commented "Ensure that all primitive types are initialised to avoid race conditions on the first access" — is itself inside #ifdef Py_GIL_DISABLED.

My question is simply whether that asymmetry is deliberate: is the GIL considered sufficient to serialize first-time type realization, so that no additional locking is needed on non-free-threaded builds?

The reason I'm unsure is that realization doesn't look like a single uninterruptible C step. do_realize_lazy_struct / complete_struct_or_union allocate, and in the s->size == (size_t)-2 branch the "unrealized" flag is cleared and the type published before do_realize_lazy_struct(ct) runs:

if (ct != NULL) {
    cffi_set_flag(ct->ct_unrealized_struct_or_union, 0);
}
...
builder->ctx.types[s->type_index] = x;
if (ct != NULL && s->size == (size_t)-2) {
    if (do_realize_lazy_struct(ct) < 0) { ... }
}

If a thread switch can land in that window, it looks like another thread could observe a type marked realized but not yet completed. I may well be misreading the invariant — if allocation there can't drop the GIL in a way that matters, that would explain it.

Activity

  1. mattip commented on Sep 8, 2026

    @mattip
    Contributor

    @ngoldbaum thoughts?

  2. ngoldbaum commented on Sep 8, 2026

    @ngoldbaum
    Contributor

    Critical sections are supposed to provide exactly as much safety as the GIL provides.

    Can you by any chance make a runnable minimal example that demonstrates the crash you're running into?

  3. ngoldbaum commented on Sep 8, 2026

    @ngoldbaum
    Contributor

    also ping @kumaraditya303

  4. kumaraditya303 commented on Sep 9, 2026

    @kumaraditya303
    Contributor

    It is not thread safe on GIL enabled builds, the atomics are only used on free-threading so there are no fences on gil enabled builds to protect against concurrent realization.

  5. ngoldbaum commented on Sep 9, 2026

    @ngoldbaum
    Contributor

    Hmm, well we should probably fix that!

  6. ngoldbaum commented on Sep 11, 2026

    @ngoldbaum
    Contributor

    I took another look at this issue. The problem is actually that on Python 3.11 and older, allocating a GC-tracked object can trigger GC before the allocation returns. This behavior makes lots more things possibly re-entrant and is the likely source of the crash the reporter ran into, although we still don't have a reproducer.

    3.12 and newer merely schedules a GC pass, so the same operations are no longer re-entrant.

    Just making the GIL-enabled build use atomic operations like the free-threaded build does won't fix that.

  7. ngoldbaum commented on Sep 14, 2026

    @ngoldbaum
    Contributor

    @calderwoodra I have a branch that I think avoids the crash you ran into:

    main...ngoldbaum:cffi-ft:toggle-gc

    It avoids the crash by toggling the GC, which avoids the possibility of reentrancy I described above. It also adds a test for this scenario that runs on Python 3.11 and older. Could you see if that fixes the crash you ran into? I'd also appreciate it if you could check for if this introduces memory spikes or other downsides.

  8. mattip commented on Sep 22, 2026

    @mattip
    Contributor

    @calderwoodra did you get a chance to check the crash?

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