csv: reject reentrant reader advancement - #8324
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe CSV reader adds an internal generation counter, detects re-entrant advancement during ChangesCSV reader generation tracking
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [x] lib: cpython/Lib/csv.py dependencies:
dependent tests: (4 tests)
Legend:
|
There was a problem hiding this comment.
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 winIncrement
generationunconditionally 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
PyStrcan incorrectly shadow the reentrancy error with a type error.Consolidate the
generationupdates 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 thestate.generation += 1to immediately follow the reentrancy check, before downcasting. Update the destructuring to safely ignore the now-handledgenerationfield.🐛 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
⛔ Files ignored due to path filters (1)
Lib/test/test_csv.pyis excluded by!Lib/**
📒 Files selected for processing (1)
crates/stdlib/src/csv.rs
|
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 CPython checks that the iterator result is a string before checking whether 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 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 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
There was a problem hiding this comment.
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 winUse
u64to prevent potential integer overflow on 32-bit platforms.On 32-bit targets (like WebAssembly, which RustPython supports),
usizeis 32-bit. Iterating through a CSV with more than 4.2 billion rows could causegeneration += 1to panic (in debug builds or release builds with overflow checks enabled). Usingu64, similar toline_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
📒 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.
* 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.
Summary
RustPython's CSV reader allowed an outer
next()call to continue parsing afterits input iterator re-entered and advanced the same reader. CPython raises
csv.Errorin this situation to avoid continuing with stale reader state (seeCPython 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'shandling of this regression.
The existing CPython regression test now passes without its
expectedFailuremarker.Test plan
The following validation was completed by the author:
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 unrelatedunfulfilled_lint_expectationswarnings.Summary by CodeRabbit