Skip to content

Segmentation fault in free-threading 3.14.6 #515

Description

@ddorian

This is a bit more complex, I had issue making it smaller.
What do you think?

#!/usr/bin/env -S uv run --python 3.14t --script
# /// script
# requires-python = "==3.14.6"
# dependencies = [
#     "flask==3.1.3",
#     "werkzeug==3.1.8",
#     "playwright==1.60.0",
#     "greenlet==3.5.3",
#     "pytest==9.1.1",
# ]
# ///
"""Minimal reproducer: greenlet 3.5.3 SIGSEGVs under free-threaded CPython 3.14t.

Playwright's *sync* API bridges sync->async via greenlet. On no-GIL CPython,
driving it against a threaded Flask server that does per-request cyclic-object
churn crashes the interpreter:

    Fatal Python error: Segmentation fault   (fault PC: _Py_HandlePending)

or intermittently `ValueError: <ContextVar 'flask.app_ctx'> was created in a
different Context` -- the same root cause.

ROOT CAUSE: the free-threaded stop-the-world GC fires while a Playwright greenlet
thread is mid-stack-switch; greenlet's saved C-stack/tstate bounds are inconsistent
at that instant, so the STW collector faults walking the parked thread. It is a
probabilistic race between the GC rate (driven by `_heavy_object_churn`) and the
greenlet-switch rate (driven by the sync API). Kill either rate and it stops.

It is NOT a no-GIL data race in our code: it also crashes with PYTHON_GIL=1, so the
breakage is in the cp314t-ABI greenlet, not the runtime GIL state. The fix is to
run Playwright e2e on a non-freethreaded cpython-3.14.

HOW TO RUN (gated off by default since it crashes the interpreter):

    uv run --python 3.14t --script test_greenlet_ft_repro.py

(or `./test_greenlet_ft_repro.py` once chmod +x -- the shebang bakes in
`--python 3.14t --script`). First time, install Firefox once:
`uvx --from playwright==1.60.0 playwright install firefox`. Expected: SIGSEGV
(exit 139). REPRO_ITERS overrides the cycle count.
"""

import itertools
import os
import socket
import sys
import threading
from collections.abc import Generator

import pytest
from flask import Flask, Response
from playwright.sync_api import Page, sync_playwright
from werkzeug.serving import make_server

_ITERS = int(os.environ.get("REPRO_ITERS", "150"))
_STATIC_HTML = (
    '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
    "<title>repro</title></head><body><h1>static</h1></body></html>"
)
_CHURN_ROUNDS = 10
_CHURN_SIZE = 500


def _heavy_object_churn() -> None:
    """Allocate + discard a cyclic object graph so gen-2 STW GC keeps firing."""
    for _ in range(_CHURN_ROUNDS):
        nodes: list[dict[str, object]] = [
            {"i": i, "p": [i, str(i), (i, i, i)], "next": None} for i in range(_CHURN_SIZE)
        ]
        for a, b in itertools.pairwise(nodes):
            a["next"] = b
        nodes[-1]["next"] = nodes[0]  # close the cycle
        del nodes


def _make_repro_app() -> Flask:
    app = Flask(__name__)

    @app.get("/e/<int:project_id>/<int:video_id>/")
    def page(project_id: int, video_id: int) -> Response:
        _heavy_object_churn()
        return Response(_STATIC_HTML, mimetype="text/html")

    return app


@pytest.fixture
def repro_server() -> Generator[str, None, None]:
    """Run `_make_repro_app` on an in-process threaded werkzeug server."""
    app = _make_repro_app()
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("127.0.0.1", 0))
    host, port = sock.getsockname()
    sock.close()  # make_server re-binds with SO_REUSEADDR; no EADDRINUSE on the freed port
    server = make_server(host, port, app, threaded=True)
    thread = threading.Thread(target=server.serve_forever, name="repro-live-server", daemon=True)
    thread.start()
    try:
        yield f"http://{host}:{port}"
    finally:
        server.shutdown()
        thread.join(timeout=5)
        server.server_close()


@pytest.fixture
def page2() -> Generator[Page, None, None]:
    """Launch Firefox via Playwright's sync (greenlet) API -- the bridge under test."""
    with sync_playwright() as p:
        browser = p.firefox.launch()
        context = browser.new_context()
        page = context.new_page()
        page.set_default_navigation_timeout(20_000)
        page.set_default_timeout(15_000)
        try:
            yield page
        finally:
            context.close()
            browser.close()


def test_greenlet_ft_crash(repro_server: str, page2: Page) -> None:
    url = f"{repro_server}/e/1/1/"
    for i in range(_ITERS):
        page2.goto(url, wait_until="load")
        # Poll the DOM to drive Playwright's sync greenlet bridge; no playback.
        for _ in range(8):
            page2.evaluate("() => document.title")
            page2.wait_for_timeout(25)
        page2.goto("about:blank")
        print(f"iter {i} ok", flush=True)


if __name__ == "__main__":
    # Drive the test via pytest.main so the fixtures/marks stay a normal test module.
    # --noconftest + -c os.devnull keep the run self-contained (no project conftest/addopts).
    sys.exit(pytest.main([__file__, "--noconftest", "-c", os.devnull, "-p", "no:randomly", "-x", "-s"]))
uv run --python 3.14.6t --script test_greenlet_ft_repro.py
Installed 16 packages in 9ms
==================================================================================================================================================================== test session starts ====================================================================================================================================================================
platform linux -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0
rootdir: /dev
configfile: null
collected 1 item                                                                                                                                                                                                                                                                                                                                            

../../../../../dev iter 0 ok
iter 1 ok
iter 2 ok
iter 3 ok
Fatal Python error: Segmentation fault

<Cannot show all threads while the GIL is disabled>
Stack (most recent call first):
  File "/home/guru/Desktop/test_greenlet_ft_repro.py", line 68 in _heavy_object_churn
  File "/home/guru/Desktop/test_greenlet_ft_repro.py", line 81 in page
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/flask/app.py", line 902 in dispatch_request
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/flask/app.py", line 917 in full_dispatch_request
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/flask/app.py", line 1511 in wsgi_app
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/flask/app.py", line 1536 in __call__
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/werkzeug/serving.py", line 332 in execute
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/werkzeug/serving.py", line 371 in run_wsgi
  File "/home/guru/.local/share/uv/python/cpython-3.14.6+freethreaded-linux-x86_64-gnu/lib/python3.14t/http/server.py", line 484 in handle_one_request
  File "/home/guru/.local/share/uv/python/cpython-3.14.6+freethreaded-linux-x86_64-gnu/lib/python3.14t/http/server.py", line 496 in handle
  File "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/lib/python3.14t/site-packages/werkzeug/serving.py", line 399 in handle
  File "/home/guru/.local/share/uv/python/cpython-3.14.6+freethreaded-linux-x86_64-gnu/lib/python3.14t/socketserver.py", line 766 in __init__
  File "__init__", line ??? in __init__
  <invalid frame>

Current thread's C stack trace (most recent call first):
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python", at _Py_DumpStack+0x30 [0x4a7ae0]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x533a9b]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x53393f]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x42520 [0x7b3a31842520]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1cdf0bf]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python", at _Py_HandlePending+0xd5 [0x1b456a1]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1cdb8cf]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a151f2]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1af9c6a]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1af9b9b]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a09930]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a151f2]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a149d0]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a11914]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a151f2]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a14a71]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1b5e688]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a64658]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a151f2]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1a14a71]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1ae14ed]
  Binary file "/home/guru/.cache/uv/environments-v2/test-greenlet-ft-repro-343027ea349605ed/bin/python" [0x1ae1472]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x94ac3 [0x7b3a31894ac3]
  Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x1268d0 [0x7b3a319268d0]

Extension modules: markupsafe._speedups, greenlet._greenlet (total: 2)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions