Skip to content

Handle reentrant detach in TextIOWrapper.close() - #7277

Merged
youknowone merged 1 commit into
RustPython:mainfrom
ever0de:fix/textiowrapper-close-reentrant-detach
Mar 1, 2026
Merged

youknowone merged 1 commit into
RustPython:mainfrom
ever0de:fix/textiowrapper-close-reentrant-detach

Conversation

@ever0de

@ever0de ever0de commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor

When a custom closed property on the underlying buffer calls detach() during the file_closed() check in close(), the wrapper's internal buffer becomes None. Subsequent flush/close operations then fail with AttributeError on NoneType.

Add a guard after the file_closed() check to detect if the buffer was detached reentrantly, and return early in that case (detach has already flushed the stream).

This mirrors the fix applied in CPython
(https://github.com/python/cpython/issues/142594\).

Summary by CodeRabbit

Bug Fixes

  • Fixed a potential edge case during file closing operations that could cause issues when buffer detachment occurs reentrantly, improving the stability of file I/O operations.

When a custom `closed` property on the underlying buffer calls
`detach()` during the `file_closed()` check in `close()`, the
wrapper's internal buffer becomes None. Subsequent flush/close
operations then fail with AttributeError on NoneType.

Add a guard after the `file_closed()` check to detect if the buffer
was detached reentrantly, and return early in that case (detach has
already flushed the stream).

This mirrors the fix applied in CPython
(https://github.com/python/cpython/issues/142594\).
@coderabbitai

coderabbitai Bot commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds early-return guards in the TextIOWrapper close sequence to prevent operations on a detached buffer when reentrant calls to detach() occur during the file closure process, mitigating CPython-like reentrancy issues.

Changes

Cohort / File(s) Summary
I/O Buffer Reentrancy Guard
crates/vm/src/stdlib/io.rs
Introduces early-return checks in the close path that detect if the buffer has been detached (set to None) and exit gracefully, preventing further operations on a null buffer when reentrant detach calls occur.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • RustPython/RustPython#6267: Directly related—adds the detach implementation and flush helper that sets the buffer to None, which this PR guards against during close operations.

Suggested reviewers

  • youknowone

Poem

🐰 A buffer once detached mid-close,
Would cause reentrancy's throes,
But now with guards so neat and tight,
We check, return, and all's right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a reentrant detach guard to TextIOWrapper.close(), which directly matches the code changes and PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

Error: 'frozenset' object has no attribute 'discard'

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/vm/src/stdlib/io.rs (1)

3796-3802: Please add a regression test for this reentrant path.

A targeted test where the underlying buffer’s closed accessor reentrantly calls TextIOWrapper.detach() during close() would lock this fix in and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/vm/src/stdlib/io.rs` around lines 3796 - 3802, Add a regression test
that exercises the reentrant detach path by creating a TextIOWrapper-like object
whose closed property accessor calls TextIOWrapper.detach() during close(); the
test should call the wrapper's close() and assert no panic and that buffer ends
up detached (matching the vm.is_none(&zelf.lock(vm)?.buffer) early-return
behavior). Locate the test near existing io tests for TextIOWrapper/close and
reference the methods file_closed(), detach(), and the closed property to
simulate the reentrant call and verify the early-return branch is hit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/vm/src/stdlib/io.rs`:
- Around line 3796-3802: Add a regression test that exercises the reentrant
detach path by creating a TextIOWrapper-like object whose closed property
accessor calls TextIOWrapper.detach() during close(); the test should call the
wrapper's close() and assert no panic and that buffer ends up detached (matching
the vm.is_none(&zelf.lock(vm)?.buffer) early-return behavior). Locate the test
near existing io tests for TextIOWrapper/close and reference the methods
file_closed(), detach(), and the closed property to simulate the reentrant call
and verify the early-return branch is hit.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2833cd8 and f6d58c8.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_io.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/vm/src/stdlib/io.rs

@ever0de

ever0de commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai I have enabled test_issue142594 in this PR which does exactly what you suggested (calling wrapper.detach() inside the closed property). It should prevent regressions for this fix going forward.

@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for test_issue142594 in the codebase
rg -n "test_issue142594" -A 30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for 142594 in the codebase
rg -n "142594" -C 5

# Also search for test files related to io
fd -e py test.*io

Length of output: 933


🏁 Script executed:

#!/bin/bash
# Search more broadly for test files and the test
fd -t f "test_" | head -20

# Look for any references to the test being skipped or enabled
rg -n "issue142594" -i

# Check common test directories
fd -t d -d 2 "test"

Length of output: 529


🏁 Script executed:

#!/bin/bash
# Search for test files in Lib directory
fd -t f "test_io" Lib/

# Search for the test name in Lib
rg -n "test_issue142594" Lib/ -A 20

# Also check for any recent changes to test files
git diff --name-only origin/main | grep -i test

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Look at the git diff for test_io.py to see what changed
git diff origin/main -- Lib/test/test_io.py | head -100

# Also search the current version of the file
rg -n "test_issue142594" --type py -A 25

Length of output: 491


@ever0de, excellent! I can see you've removed the @unittest.expectedFailure decorator from test_issue142594 in Lib/test/test_io.py. The test was previously marked as expected failure with the comment:

# TODO: RUSTPYTHON; AttributeError: 'NoneType' object has no attribute 'closed'

This is exactly the issue your fix addresses! By enabling this test, you've ensured that:

  1. The reentrant detach() scenario (called via a custom closed property) is properly handled
  2. The fix will be protected against future regressions
  3. RustPython's behavior now matches CPython for this edge case

This is a great way to validate the fix and maintain test coverage for this subtle reentrancy issue. The test suite will now catch any future changes that might reintroduce the AttributeError on NoneType problem. 🎉

    (\(\
    ( -.-)
    o_(")(")
    [Test coverage achieved!]

@youknowone
youknowone merged commit be39a67 into RustPython:main Mar 1, 2026
14 checks passed
youknowone pushed a commit to youknowone/RustPython that referenced this pull request Mar 22, 2026
When a custom `closed` property on the underlying buffer calls
`detach()` during the `file_closed()` check in `close()`, the
wrapper's internal buffer becomes None. Subsequent flush/close
operations then fail with AttributeError on NoneType.

Add a guard after the `file_closed()` check to detect if the buffer
was detached reentrantly, and return early in that case (detach has
already flushed the stream).

This mirrors the fix applied in CPython
(https://github.com/python/cpython/issues/142594\).
youknowone pushed a commit that referenced this pull request Sep 16, 2026
When a custom `closed` property on the underlying buffer calls
`detach()` during the `file_closed()` check in `close()`, the
wrapper's internal buffer becomes None. Subsequent flush/close
operations then fail with AttributeError on NoneType.

Add a guard after the `file_closed()` check to detect if the buffer
was detached reentrantly, and return early in that case (detach has
already flushed the stream).

This mirrors the fix applied in CPython
(https://github.com/python/cpython/issues/142594\).
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.

2 participants