Skip to content

Don't leak kept scanner state in loose parser - #379

Merged
kou merged 1 commit into
ruby:mainfrom
kou:fix-shift-keeps-leak
Oct 28, 2026
Merged

kou merged 1 commit into
ruby:mainfrom
kou:fix-shift-keeps-leak

Conversation

@kou

@kou kou commented Oct 28, 2026

Copy link
Copy Markdown
Member

GitHub: Fix GH-378

Since CSV#shift re-enters CSV::Parser#parse for each row, parse_quotable_loose left one keep per row in the scanner. Each keep retains a StringScanner and its chunk.

A keep was also leaked when an exception was raised while parsing a row. If the caller rescued it and continued reading from IO, every following keep_drop merged its buffer into the leaked keep.

Use ensure to always drop the keep unless it was consumed by keep_back for switching to parse_quotable_robust.

Reported by sswander. Thanks!!!

GitHub: Fix rubyGH-378

Since CSV#shift re-enters CSV::Parser#parse for each row,
parse_quotable_loose left one keep per row in the scanner. Each keep
retains a StringScanner and its chunk.

A keep was also leaked when an exception was raised while parsing a
row. If the caller rescued it and continued reading from IO, every
following keep_drop merged its buffer into the leaked keep.

Use ensure to always drop the keep unless it was consumed by keep_back
for switching to parse_quotable_robust.

Reported by sswander. Thanks!!!

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kou

kou commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

@sswander Could you try this?

@sswander

Copy link
Copy Markdown

@kou thanks for the quick fix!

Seems to work now.

Repro script:
# repro.rb
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "csv", github: "kou/csv", branch: "fix-shift-keeps-leak"
end

require "csv"
require "tmpdir"

path = File.join(Dir.tmpdir, "repro.csv")
File.write(path, "a,b\n" * 1_000_000)

GC.start
before = GC.stat(:heap_live_slots)

csv = CSV.new(File.open(path))
nil while csv.gets

GC.start(full_mark: true, immediate_sweep: true)
puts "csv #{CSV::VERSION}: retained after full GC = #{GC.stat(:heap_live_slots) - before}"

running ruby repro.rb results in

csv 3.3.6: retained after full GC = 163

@kou
kou merged commit c85db27 into ruby:main Oct 28, 2026
64 of 68 checks passed
@kou
kou deleted the fix-shift-keeps-leak branch October 28, 2026 07:49
@kou

kou commented Oct 28, 2026

Copy link
Copy Markdown
Member Author

Thanks.
Merged and released.

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.

CSV#shift causes unbounded memory growth since v3.3.6

2 participants