Skip to content

Conversation

@ankith26
Copy link
Member

@ankith26 ankith26 commented Oct 9, 2025

PR to fix #3601

The central issue here is that when pygame.quit/pygame.display.quit is called, all pygame window objects may hold invalid references in their attributes (like _win and surface). It therefore becomes necessary to do the equivalent of calling window.destroy on all active windows before doing the quit.

For this implementation, I went with a weakref approach where each window holds a self weakref upon creation. A new reference to this weakref is kept in a global set of weakrefs. Cleanup can happen in two paths:

  1. window is dealloc'ed before quit happens: in this case, that specific weakref is removed from our global set and cleaned up.
  2. quit happens before window(s) are dealloced: in this case we have to iterate over all weakrefs, deref them and call destroy on all windows.

This PR also adds a "window is alive" explicit check to all window methods/properties for consistent error raising.

tests will be added soon:tm:

@ankith26 ankith26 requested a review from a team as a code owner October 9, 2025 06:35
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

📝 Walkthrough

Walkthrough

Adds weak-reference support and lifecycle tracking to the Window type. Introduces a global weak-ref set, a custom tp_new, destroyed-state guards, and revised deallocation. Header gains weakref fields; type exposes tp_weaklistoffset. Module init/quit manage the weak-ref set and clean up remaining windows on quit.

Changes

Cohort / File(s) Summary of Changes
Window struct layout (public header)
src_c/include/_pygame.h
Added two PyObject* fields to pgWindowObject: weakreflist and selfweakref, expanding the public struct layout to enable weak-ref support.
Window lifecycle, weakrefs, and type init
src_c/window.c
Introduced global pg_window_weakrefs set; added WINDOW_INIT_CHECK guard macro; injected destroyed-state checks across Window methods; implemented window_new and set as .tp_new; set .tp_weaklistoffset = offsetof(pgWindowObject, weakreflist); reworked window_dealloc to destroy window and clear weakrefs; module init/quit now create/clear the weak-ref set and iterate remaining windows to destroy on quit.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Py as Python code
  participant WT as WindowType
  participant W as Window instance
  participant WR as pg_window_weakrefs

  Note over WT,W: Creation with weak-ref registration
  Py->>WT: WT.__new__()
  WT->>WT: window_new(subtype,args,kwds)
  WT->>W: allocate & init fields
  WT->>WR: add weakref(W)
  WT-->>Py: return W

  Note over Py,W: Method call guarded against destroyed state
  Py->>W: some_window_method()
  W->>W: WINDOW_INIT_CHECK(self->_win)
  alt window destroyed
    W-->>Py: return error/None per method
  else window alive
    W->>W: proceed with operation
    W-->>Py: return result
  end
Loading
sequenceDiagram
  autonumber
  participant Py as Python code
  participant Mod as _window module
  participant WR as pg_window_weakrefs
  participant W as Window instance

  Note over Mod,WR: Module quit cleanup
  Py->>Mod: _window_internal_mod_quit()
  Mod->>WR: iterate weak refs
  loop each live Window
    WR->>W: destroy via window_destroy()
    W->>W: clear selfweakref / weakreflist
  end
  Mod->>WR: clear & dealloc set
  Mod-->>Py: return

  Note over W: Deallocation
  Py->>W: GC/dealloc
  W->>W: window_dealloc()
  W->>W: window_destroy()
  W->>WR: remove weakref
  W->>W: clear weakref list
  W-->>Py: memory freed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

bugfix, window, display

Suggested reviewers

  • Starbuck5

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The PR implements weakref-based cleanup and explicit destruction checks for Window objects to prevent use-after-free errors after quit, addressing the window lifecycle objectives from issue #3601, but it does not modify the Surface type to provide a safe “dead display” representation or guard surface method calls as required to fully prevent segfaults on surfaces after quit [#3601]. Update the Surface type or wrap surface methods to return a safe “dead display” object or explicitly check display state after quit, ensuring surface method calls no longer segfault as described in issue #3601.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely and accurately summarizes the primary change—preventing undefined behavior and segfaults for Window objects after quit or destruction—without extraneous details.
Out of Scope Changes Check ✅ Passed All modifications are confined to the Window module and its lifecycle management, directly supporting the linked issue’s objective of safe cleanup on quit without introducing unrelated changes.
Description Check ✅ Passed The description clearly outlines the problem of invalid references after pygame.quit(), explains the weakref-based approach to tracking and cleaning up Window objects, and specifies the explicit alive checks added to window methods.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ankith26-fix-window-segfault

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abe99c2 and 3af0bfe.

📒 Files selected for processing (2)
  • src_c/include/_pygame.h (1 hunks)
  • src_c/window.c (37 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src_c/window.c (2)
src_c/include/pythoncapi_compat.h (1)
  • PyWeakref_GetRef (570-590)
src_c/base.c (1)
  • pg_mod_autoinit (137-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: x86_64
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (ubuntu-24.04)
  • GitHub Check: dev-check
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.13.5)
  • GitHub Check: i686
  • GitHub Check: x86_64
  • GitHub Check: aarch64
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.14.0rc1)
  • GitHub Check: debug_coverage (ubuntu-24.04, 3.9.23)
  • GitHub Check: msys2 (clang64, clang-x86_64)
  • GitHub Check: Debian (Bookworm - 12) [s390x]
  • GitHub Check: msys2 (mingw64, x86_64)
  • GitHub Check: msys2 (ucrt64, ucrt-x86_64)
  • GitHub Check: Debian (Bookworm - 12) [armv7]
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: Debian (Bookworm - 12) [ppc64le]
  • GitHub Check: Debian (Bookworm - 12) [armv6]
  • GitHub Check: AMD64
  • GitHub Check: x86

Comment on lines 993 to 1009
window_dealloc(pgWindowObject *self, PyObject *_null)
{
if (self->_win) {
if (!self->_is_borrowed) {
if (self->context != NULL) {
SDL_GL_DeleteContext(self->context);
}
SDL_DestroyWindow(self->_win);
}
else if (pg_get_pg_window(self->_win) != NULL) {
pg_set_pg_window(self->_win, NULL);
Py_XDECREF(window_destroy(self, _null));

if (self->selfweakref) {
/* First attempt to remove it from our set (errors ignored). Then
* free the weakref */
if (pg_window_weakrefs) {
PySet_Discard(pg_window_weakrefs, self->selfweakref);
}
Py_DECREF(self->selfweakref);
}
if (self->surf) {
// Set the internal surface to NULL to make pygame surface invalid
// since this surface will be deallocated by SDL when the window is
// destroyed.
self->surf->surf = NULL;

Py_DECREF(self->surf);
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject *)self);
}

Py_TYPE(self)->tp_free(self);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix tp_dealloc signature mismatch.

tp_dealloc is invoked with a single PyObject *. Defining window_dealloc with two parameters means CPython will call it with only the first argument, leaving _null as indeterminate data. That is undefined behaviour and can corrupt the stack on some ABIs. Switch back to the single-argument signature and call window_destroy with an explicit NULL.

-static void
-window_dealloc(pgWindowObject *self, PyObject *_null)
+static void
+window_dealloc(pgWindowObject *self)
 {
-    Py_XDECREF(window_destroy(self, _null));
+    Py_XDECREF(window_destroy(self, NULL));
 
     if (self->selfweakref) {
         /* First attempt to remove it from our set (errors ignored). Then
          * free the weakref */
         if (pg_window_weakrefs) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window_dealloc(pgWindowObject *self, PyObject *_null)
{
if (self->_win) {
if (!self->_is_borrowed) {
if (self->context != NULL) {
SDL_GL_DeleteContext(self->context);
}
SDL_DestroyWindow(self->_win);
}
else if (pg_get_pg_window(self->_win) != NULL) {
pg_set_pg_window(self->_win, NULL);
Py_XDECREF(window_destroy(self, _null));
if (self->selfweakref) {
/* First attempt to remove it from our set (errors ignored). Then
* free the weakref */
if (pg_window_weakrefs) {
PySet_Discard(pg_window_weakrefs, self->selfweakref);
}
Py_DECREF(self->selfweakref);
}
if (self->surf) {
// Set the internal surface to NULL to make pygame surface invalid
// since this surface will be deallocated by SDL when the window is
// destroyed.
self->surf->surf = NULL;
Py_DECREF(self->surf);
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject *)self);
}
Py_TYPE(self)->tp_free(self);
static void
window_dealloc(pgWindowObject *self)
{
Py_XDECREF(window_destroy(self, NULL));
if (self->selfweakref) {
/* First attempt to remove it from our set (errors ignored). Then
* free the weakref */
if (pg_window_weakrefs) {
PySet_Disard(pg_window_weakrefs, self->selfweakref);
}
Py_DECREF(self->selfweakref);
}
if (self->weakreflist) {
PyObject_ClearWeakRefs((PyObject *)self);
}
Py_TYPE(self)->tp_free(self);
}
🤖 Prompt for AI Agents
In src_c/window.c around lines 993 to 1009, the tp_dealloc implementation uses a
two-argument signature window_dealloc(pgWindowObject *self, PyObject *_null)
which is incorrect because CPython will call tp_dealloc with a single PyObject*,
leaving the second parameter indeterminate; change the function to the correct
single-argument signature window_dealloc(pgWindowObject *self), update the body
to call window_destroy(self, NULL) instead of passing _null, and keep the rest
of the cleanup (weakref set removal, clearing weak refs, and tp_free) unchanged.

@ankith26 ankith26 marked this pull request as draft October 21, 2025 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling pygame.quit() and then Surface.get_bitsize() overflowing and segfaulting

1 participant