Skip to content

csv: reject reentrant reader advancement - #8324

Merged
youknowone merged 3 commits into
RustPython:mainfrom
widehyo1:fix-csv-reader-reentrant-iterator
Jul 20, 2026
Merged

youknowone merged 3 commits into
RustPython:mainfrom
widehyo1:fix-csv-reader-reentrant-iterator

Conversation

@widehyo1

@widehyo1 widehyo1 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

RustPython's CSV reader allowed an outer next() call to continue parsing after
its input iterator re-entered and advanced the same reader. CPython raises
csv.Error in this situation to avoid continuing with stale reader state (see
CPython gh-145105).

Track the reader generation and compare the generation before and after
calling the input iterator. If a reentrant call advanced the reader, raise
csv.Error("iterator has already advanced the reader"), matching CPython's
handling of this regression.

import csv


class ReentrantIterator:
    def __init__(self):
        self.reader = None
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.index += 1
        if self.index == 1:
            next(self.reader)
            return "a,b"
        if self.index == 2:
            return "x"
        raise StopIteration


iterator = ReentrantIterator()
reader = csv.reader(iterator)
iterator.reader = reader
print(next(reader))

# before: ['a', 'b']
# after:  csv.Error: iterator has already advanced the reader

The existing CPython regression test now passes without its
expectedFailure marker.

Test plan

The following validation was completed by the author:

  • Unmarks Test_Csv.test_reader_reentrant_iterator.
  • cargo run --release Lib/test/test_csv.py: 128 tests run, 7 skipped,
    18 expected failures, SUCCESS.
  • cargo run -- extra_tests/snippets/stdlib_csv.py: passed.
  • cargo fmt --check: passed.
  • cargo clippy -p rustpython-stdlib --all-targets: passed with unrelated
    unfulfilled_lint_expectations warnings.
  • The configured workspace test suite passed.
  • Compared the reproducer against CPython 3.14.6.

Summary by CodeRabbit

  • Bug Fixes
    • Improved CSV reader reliability by detecting attempts to reuse an already-advanced iterator, preventing incorrect reads.
    • Added a clear error when the CSV reader is advanced unexpectedly during iteration.

Snapshot the reader generation before calling the input iterator. If a nested
call completes a record and advances the same reader, reject the outer call
with csv.Error instead of continuing with stale parser state.

Increment the generation on each successful record path, matching CPython's
handling of the existing reentrant-reader regression. Enable that test by
removing its expectedFailure marker.

Assisted-by: Codex:gpt-5.6-sol
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e5e2a798-598a-4f78-94d5-a93e25108dc2

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0f6fb and 298bb36.

📒 Files selected for processing (1)
  • crates/stdlib/src/csv.rs

📝 Walkthrough

Walkthrough

The CSV reader adds an internal generation counter, detects re-entrant advancement during __next__, raises csv.Error on mismatch, and updates the counter before parsing records.

Changes

CSV reader generation tracking

Layer / File(s) Summary
Reader generation state
crates/stdlib/src/csv.rs
Initializes and stores a generation counter in the reader’s internal state.
Advancement validation and updates
crates/stdlib/src/csv.rs
Checks for prior reader advancement, increments the generation, and excludes the field from parsing state destructuring.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: jinmay, shaharnaveh, doma17

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting reentrant CSV reader advancement.
Linked Issues check ✅ Passed The reader now tracks generation changes and raises the expected csv.Error when a nested next() advances the same reader.
Out of Scope Changes check ✅ Passed The changes stay focused on csv.reader reentrancy detection with no unrelated behavior added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

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

[x] lib: cpython/Lib/csv.py
[x] test: cpython/Lib/test/test_csv.py (TODO: 19)

dependencies:

  • csv

dependent tests: (4 tests)

  • csv: test_csv test_genericalias
    • importlib.metadata: test_importlib test_zoneinfo

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/stdlib/src/csv.rs (1)

1062-1093: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Increment generation unconditionally immediately after the reentrancy check.

Delaying the generation increment to successful read paths allows an inner reentrant call that mutates the parser state but subsequently errors out (e.g. due to an invalid newline) to bypass the generation update. The outer call will then fail to detect the reentrancy and proceed with corrupted parser state. Furthermore, performing the reentrancy check after downcasting to PyStr can incorrectly shadow the reentrancy error with a type error.

Consolidate the generation updates to execute exactly once right after acquiring the state lock, mirroring CPython's exact sequence of operations.

  • crates/stdlib/src/csv.rs#L1062-L1093: Move the state.generation += 1 to immediately follow the reentrancy check, before downcasting. Update the destructuring to safely ignore the now-handled generation field.
🐛 Proposed fix for the early increment
-            let generation = zelf.state.lock().generation;
-            let string = raise_if_stop!(zelf.iter.next(vm)?);
-            let string = string.downcast::<PyStr>().map_err(|obj| {
-                new_csv_error(
-                    vm,
-                    format!(
-                "iterator should return strings, not {} (the file should be opened in text mode)",
-                obj.class().name()
-            ),
-                )
-            })?;
-            let input = string.as_bytes();
+            let generation = zelf.state.lock().generation;
+            let string_obj = raise_if_stop!(zelf.iter.next(vm)?);
+            
             let mut state = zelf.state.lock();
             if state.generation != generation {
                 return Err(new_csv_error(
                     vm,
                     "iterator has already advanced the reader",
                 ));
             }
+            state.generation += 1;
+
+            let string = string_obj.downcast::<PyStr>().map_err(|obj| {
+                new_csv_error(
+                    vm,
+                    format!(
+                        "iterator should return strings, not {} (the file should be opened in text mode)",
+                        obj.class().name()
+                    ),
+                )
+            })?;
+            let input = string.as_bytes();
+
             if input.is_empty() || input.starts_with(b"\n") {
-                state.generation += 1;
                 return Ok(PyIterReturn::Return(vm.ctx.new_list(vec![]).into()));
             }
             let ReadState {
                 buffer,
                 output_ends,
                 reader,
                 skipinitialspace,
                 delimiter,
                 line_num,
-                generation,
+                generation: _,
             } = &mut *state;
  • crates/stdlib/src/csv.rs#L1100-L1105: Remove the now-redundant *generation += 1;.
🐛 Proposed fix for the `QUOTE_NONE` branch
             if zelf.dialect.quoting == QuoteStyle::None && zelf.dialect.escapechar.is_some() {
                 let out = read_quote_none_record(input, zelf.dialect, field_limit, vm)?;
                 *line_num += 1;
-                *generation += 1;
                 return Ok(PyIterReturn::Return(vm.ctx.new_list(out).into()));
             }
  • crates/stdlib/src/csv.rs#L1188-L1192: Remove the now-redundant *generation += 1;.
🐛 Proposed fix for the standard branch
             // if out.last().unwrap().length(vm).unwrap() == 0 {
             //     out.pop();
             // }
             *line_num += 1;
-            *generation += 1;
             Ok(PyIterReturn::Return(vm.ctx.new_list(out).into()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/stdlib/src/csv.rs` around lines 1062 - 1093, Update
crates/stdlib/src/csv.rs:1062-1093 in the iterator read flow to increment
state.generation exactly once immediately after the reentrancy check and before
PyStr downcasting; destructure ReadState while safely ignoring the
already-handled generation field. Remove the redundant generation increments in
the QUOTE_NONE branch at crates/stdlib/src/csv.rs:1100-1105 and the standard
branch at crates/stdlib/src/csv.rs:1188-1192, preserving the existing reentrancy
validation and parsing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/stdlib/src/csv.rs`:
- Around line 1062-1093: Update crates/stdlib/src/csv.rs:1062-1093 in the
iterator read flow to increment state.generation exactly once immediately after
the reentrancy check and before PyStr downcasting; destructure ReadState while
safely ignoring the already-handled generation field. Remove the redundant
generation increments in the QUOTE_NONE branch at
crates/stdlib/src/csv.rs:1100-1105 and the standard branch at
crates/stdlib/src/csv.rs:1188-1192, preserving the existing reentrancy
validation and parsing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 953f84da-969d-4d85-8a79-dda49549885a

📥 Commits

Reviewing files that changed from the base of the PR and between 3290f28 and 25732c1.

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

@widehyo1

widehyo1 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

thank you for the review.

I checked CPython 3.14.6 behavior, by following code:

import csv


class ReentrantIterator:
    def __init__(self):
        self.reader = None
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.index += 1
        if self.index == 1:
            try:
                next(self.reader)
            except csv.Error as exc:
                print("inner:", exc)
            return "a,b"
        if self.index == 2:
            return "bad\nline"
        raise StopIteration


iterator = ReentrantIterator()
reader = csv.reader(iterator)
iterator.reader = reader

try:
    print("outer:", next(reader))
except csv.Error as exc:
    print("outer:", exc)

# inner: new-line character seen in unquoted field - do you need to open the file with newline=''?
# outer: new-line character seen in unquoted field - do you need to open the file with newline=''?

It shows CPython 3.14.6 prints the newline parsing error for both calls; it does not raise iterator has already advanced the reader for the outer call.

CPython checks that the iterator result is a string before checking whether self->fields is NULL, and it sets self->fields = NULL only after a record has been parsed successfully. In other words, the sentinel represents a successfully advanced reader, not every reentrant call that obtained an item from the input iterator.

If i move the increment immediately after the check, it would classify this failed inner call as a successful reader advancement. So that will change the outer exception to iterator has already advanced the reader.

I also checked the downcast ordering by following code:

import csv


class ReentrantIterator:
    def __init__(self):
        self.reader = None
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.index += 1
        if self.index == 1:
            next(self.reader)
            return 1
        if self.index == 2:
            return "x"
        raise StopIteration


iterator = ReentrantIterator()
reader = csv.reader(iterator)
iterator.reader = reader

try:
    next(reader)
except csv.Error as exc:
    print(exc)

# iterator should return strings, not int (the file should be opened in text mode)

If the inner call successfully reads "x" but the outer iterator invocation returns 1, CPython 3.14.6 prioritizes the type error.

I think the CPython implementation incorrectly check above cases. Considering the purpose of blocking reenterant reader iterator, I agree your comment

Advance the reader generation immediately after the reentrancy check, before
validating or parsing the returned input item. This invalidates the outer call
even when a nested read later fails validation or parsing.

Remove the per-return generation updates now that the common path advances it
exactly once.

Assisted-by: Codex:gpt-5.6-sol

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/stdlib/src/csv.rs (1)

965-965: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use u64 to prevent potential integer overflow on 32-bit platforms.

On 32-bit targets (like WebAssembly, which RustPython supports), usize is 32-bit. Iterating through a CSV with more than 4.2 billion rows could cause generation += 1 to panic (in debug builds or release builds with overflow checks enabled). Using u64, similar to line_num, completely mitigates this risk.

🐛 Proposed fix
-        generation: usize,
+        generation: u64,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/stdlib/src/csv.rs` at line 965, Change the CSV iteration state field
generation from usize to u64, and update its initialization or increment sites
as needed so generation += 1 remains type-correct. Keep the existing
line_num-style counter behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/stdlib/src/csv.rs`:
- Line 965: Change the CSV iteration state field generation from usize to u64,
and update its initialization or increment sites as needed so generation += 1
remains type-correct. Keep the existing line_num-style counter behavior
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 70dcf22b-10ad-4293-b1ee-fe8bf63098fb

📥 Commits

Reviewing files that changed from the base of the PR and between 25732c1 and 0d0f6fb.

📒 Files selected for processing (1)
  • crates/stdlib/src/csv.rs

On 32-bit targets (like WebAssembly, which RustPython supports), usize is 32-bit.
Csv with over 32-bit rows can raise panic on these platform.
type conversion from usize to u64.

@ShaharNaveh ShaharNaveh 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.

tysm!

@youknowone youknowone added the z-ca-2026 Tag to track Contribution Academy 2026 label Jul 20, 2026
@youknowone
youknowone merged commit a7f0496 into RustPython:main Jul 20, 2026
27 checks passed
@widehyo1
widehyo1 deleted the fix-csv-reader-reentrant-iterator branch July 20, 2026 11:09
youknowone pushed a commit that referenced this pull request Sep 16, 2026
* csv: reject reentrant reader advancement

Snapshot the reader generation before calling the input iterator. If a nested
call completes a record and advances the same reader, reject the outer call
with csv.Error instead of continuing with stale parser state.

Increment the generation on each successful record path, matching CPython's
handling of the existing reentrant-reader regression. Enable that test by
removing its expectedFailure marker.

Assisted-by: Codex:gpt-5.6-sol

* csv: invalidate outer reads after reentry

Advance the reader generation immediately after the reentrancy check, before
validating or parsing the returned input item. This invalidates the outer call
even when a nested read later fails validation or parsing.

Remove the per-return generation updates now that the common path advances it
exactly once.

Assisted-by: Codex:gpt-5.6-sol

* csv: generation type conversion(usize to u64)

On 32-bit targets (like WebAssembly, which RustPython supports), usize is 32-bit.
Csv with over 32-bit rows can raise panic on these platform.
type conversion from usize to u64.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

csv: csv.reader does not detect re-entrant iterator advancement

3 participants