⚡ Sangsakala Proxy 🏠 Home
Skip to content

android (bionic): interpreter genesis aborts with "out of TLS keys" — StaticCell thread_local! statics exhaust PTHREAD_KEYS_MAX #9084

Open

Description

@GuYeying

name: Report bugs
about: Report a bug not related to CPython compatibility
title: 'android (bionic): interpreter genesis aborts with "out of TLS keys" — StaticCell thread_local! statics exhaust PTHREAD_KEYS_MAX'
labels: C-bug
assignees: ''

Summary

Embedding rustpython-vm on android (aarch64, bionic libc) without the threading feature aborts during interpreter genesis with:

fatal runtime error: out of TLS keys, aborting

Root cause: StaticCell's non-threading variant is backed by thread_local! statics (LocalKey<OnceCell<...>>). On bionic, each thread_local! static with a destructor consumes one process-wide pthread_key_t, and the pool is capped at PTHREAD_KEYS_MAX = 1024 (shared with every other TLS user in the process, including std and third-party crates).

rustpython-vm instantiates a StaticCell slot per builtin type / exception slot / codec slot via the static_cell! macro — on the order of hundreds of thread_local! statics across crates/vm/src (11 files contain static_cell! invocations; vm/context.rs, exception.rs and the stdlib modules are the big ones). Genesis initializes all of them at once, blowing through the bionic key pool before startup completes. In a real app (game engine embedding), other TLS consumers push the pool close to the limit even before genesis.

Expected

Interpreter genesis completes on android (no-threading configuration), as it does on desktop platforms.

Actual

Genesis aborts during builtin-type initialization:

fatal runtime error: out of TLS keys, aborting

Environment

  • aarch64-linux-android (bionic), NDK r27+, default-features = false, features: compiler, wasmbind, gc, stdio, importlib, encodings, freeze-stdlib (no threading, no host_env)
  • Verified against current main: crates/common/src/static_cell.rs non-threading variant is still LocalKey<OnceCell<...>>-based (no android-specific branch)
  • Observed on: RustPython main snapshot 2026-09-22 (0.6.0-dev), and re-verified the unfixed state on current main

Analysis

  • Bionic (unlike glibc/musl desktop libs, and unlike macOS/Windows TLS implementations rust uses elsewhere) allocates a pthread_key_t per thread_local! static with a destructor, from a process-wide pool of 1024 (PTHREAD_KEYS_MAX).
  • Every static_cell! slot in rustpython-vm is exactly such a static. Genesis (Context::init_genesis + stdlib init) touches all of them in one go.
  • A real application adds its own TLS consumers (std, codecs, GL/math crates), so the effective headroom is smaller than 1024.

Workaround we shipped (for context — not proposed as a direct patch)

We added an android-cfg variant of StaticCell in our vendored copy: a process-global UnsafeCell<OnceCell<T>> + unsafe impl Sync, selected via
#[cfg(all(not(feature = "threading"), feature = "std", target_os = "android"))].

This is safe under our embedding contract (GIL single-thread, one interpreter per process, always driven on the same thread), and genesis + full gameplay now run cleanly on android (aarch64 real device, HarmonyOS NEXT + 卓易通 container).

Caveat: the global-cell approach relies on that single-thread contract and is not thread-compatible — we don't propose it as a direct upstream patch. Upstream would need a thread-compatible design (e.g., one TLS slot holding a registry of all StaticCells, instead of one slot per cell).

Suggested directions

  1. A key-frugal StaticCell for platforms with small pthread key pools (single TLS slot holding a registry/table of cells), selected by cfg on target_os = "android";
  2. Or documenting the android TLS constraint for embedders (no-threading builds abort at genesis once the type count is large enough).

Happy to provide more details, test results, or validate a fix on our android test devices.

Python Documentation

Not applicable — this is a runtime/embedding issue, not a CPython compatibility issue.

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