Skip to content

run_with_limited_c_stack(size=...) is unsafe below Py_C_STACK_SIZE on platforms without stack-size detection #159879

Open

Description

@serhiy-storchaka

Bug report

Bug description:

test.support.run_with_limited_c_stack() runs a test body in a thread with a chosen C stack size. Its
default is deliberately conservative, and the comment says why
(Lib/test/support/__init__.py:2942-2945):

# Small enough to be exhausted by tens of thousands of recursive calls,
# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter
# assumes if it cannot query the thread stack size.
C_STACK_SIZE = 8 * 1024 * 1024

That reasoning is sound, but nothing enforces it on callers that pass size explicitly, and one of them
goes below the limit the comment warns about:

Lib/test/test_lazy_import/__init__.py:311
    @support.run_with_limited_c_stack(depth=10_000, size=256 * 1024)

Lib/test/test_profiling/test_sampling_profiler/test_collectors.py:614
    @run_with_limited_c_stack(size=1024 * 1024)

Every other caller uses the default.

On a platform where hardware_stack_limits() cannot query the real stack, the interpreter assumes
Py_C_STACK_SIZE and its recursion guard is calibrated to that assumption. Giving the thread less than
that means the guard's soft limit sits below the real stack base and can never fire, so the test does not
get a RecursionError — it gets SIGSEGV.

That is what happens on Alpine Linux / musl today: test_lazy_import.LazyImportTypeTests
.test_deeply_dotted_name crashes the interpreter. Details and the underlying detection problem are in
gh-159876; this report is about the test-side contract, which is worth tightening independently of whether
detection is ever fixed for musl.

The set of affected platforms is exactly the exclusion list in Python/ceval.c (main:150-152):

#  if defined(HAVE_PTHREAD_GETATTR_NP) && !defined(_AIX) && \
        !defined(__NetBSD__) && \
        (defined(__GLIBC__) || defined(__ANDROID__) || !defined(__linux__))

so AIX, NetBSD and non-glibc Linux all run on the assumed-size fallback and are all exposed to a caller
that passes a small size.

Two further wrinkles:

  • The comment's "(4 MiB)" is only the #else default. When configure sets -Wl,-z,stack-size=, the value
    becomes _Py_LINKER_THREAD_STACK_SIZE instead — 1 MiB on a stock Linux build. There are also
    #elif arms of 320000 (s390x), 1200000 (Android), 1600000 (sparc) and 2000000 (hppa, powerpc64). So the
    real floor is build- and platform-dependent, and a literal size= in a test cannot know it.
  • size=1024 * 1024 in test_collectors.py happens to be exactly Py_C_STACK_SIZE on a stock Linux build
    — not currently failing, but with no margin at all.

The helper already has everything it needs to notice: it calls _has_huge_c_stack(), which reads
_testinternalcapi.get_c_recursion_remaining(). A thread whose reported headroom does not change when its
stack size changes is one where detection is not working, which is observable from Python.

Reproducer for the general shape, independent of test_lazy_import (prints the same number twice where
detection is unavailable, two different numbers where it works):

import threading, _testinternalcapi as T
for size in (256 * 1024, 8 * 1024 * 1024):
    threading.stack_size(size)
    def show(size=size):
        print("thread stack %7d B -> c_recursion_remaining=%d"
              % (size, T.get_c_recursion_remaining()))
    t = threading.Thread(target=show); t.start(); t.join()
Alpine 3.24 / musl:  262144 -> 1450    8388608 -> 1450
Debian / glibc:      262144 ->  250    8388608 -> 12650

CPython versions tested on:

3.15, CPython main branch

Operating systems tested on:

Linux

Linked PRs

Activity

  1. serhiy-storchaka commented on Oct 20, 2026

    @serhiy-storchaka
    MemberAuthor

    My apologies, the report was created by Claude, and I wasn't familiar enough with this to shorten it.

  2. added 2 commits that reference this issue on Oct 20, 2026
  3. added
    3.15bugs and security fixes
    3.16new features, bugs and security fixes
    on Oct 20, 2026
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

    3.15bugs and security fixes3.16new features, bugs and security fixes

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions