8000 Fix memory leak in Prism's RubyVM::InstructionSequence.new · ruby/ruby@34b407a · GitHub
[go: up one dir, main page]

Skip to content

Commit 34b407a

Browse files
committed
Fix memory leak in Prism's RubyVM::InstructionSequence.new
[Bug #21394] There are two ways to make RubyVM::InstructionSequence.new raise which would cause the options->scopes to leak memory: 1. Passing in any (non T_FILE) object where the to_str raises. 2. Passing in a T_FILE object where String#initialize_dup raises. This is because rb_io_path dups the string. Example 1: 10.times do 100_000.times do RubyVM::InstructionSequence.new(nil) rescue TypeError end puts `ps -o rss= -p #{$$}` end Before: 13392 17104 20256 23920 27264 30432 33584 36752 40032 43232 After: 9392 11072 11648 11648 11648 11712 11712 11712 11744 11744 Example 2: require "tempfile" MyError = Class.new(StandardError) String.prepend(Module.new do def initialize_dup(_) if $raise_on_dup raise MyError else super end end end) Tempfile.create do |f| 10.times do 100_000.times do $raise_on_dup = true RubyVM::InstructionSequence.new(f) rescue MyError else raise "MyError was not raised during RubyVM::InstructionSequence.new" end puts `ps -o rss= -p #{$$}` ensure $raise_on_dup = false end end Before: 14080 18512 22000 25184 28320 31600 34736 37904 41088 44256 After: 12016 12464 12880 12880 12880 12912 12912 12912 12912 12912
1 parent 135583e commit 34b407a

File tree

2 files changed

+63
-5
lines changed

2 files changed

+63
-5
lines changed

iseq.c

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,15 @@ pm_iseq_compile_with_option(VALUE src, VALUE file, VALUE realpath, VALUE line, V
13271327
ln = NUM2INT(line);
13281328
StringValueCStr(file);
13291329

1330+
bool parse_file = false;
1331+
if (RB_TYPE_P(src, T_FILE)) {
1332+
parse_file = true;
1333+
src = rb_io_path(src);
1334+
}
1335+
else {
1336+
src = StringValue(src);
1337+
}
1338+
13301339
pm_parse_result_t result = { 0 };
13311340
pm_options_line_set(&result.options, NUM2INT(line));
13321341
pm_options_scopes_init(&result.options, 1);
@@ -1349,16 +1358,15 @@ pm_iseq_compile_with_option(VALUE src, VALUE file, VALUE realpath, VALUE line, V
13491358
VALUE script_lines;
13501359
VALUE error;
13511360

1352-
if (RB_TYPE_P(src, T_FILE)) {
1353-
VALUE filepath = rb_io_path(src);
1354-
error = pm_load_parse_file(&result, filepath, ruby_vm_keep_script_lines ? &script_lines : NULL);
1355-
RB_GC_GUARD(filepath);
1361+
if (parse_file) {
1362+
error = pm_load_parse_file(&result, src, ruby_vm_keep_script_lines ? &script_lines : NULL);
13561363
}
13571364
else {
1358-
src = StringValue(src);
13591365
error = pm_parse_string(&result, src, file, ruby_vm_keep_script_lines ? &script_lines : NULL);
13601366
}
13611367

1368+
RB_GC_GUARD(src);
1369+
13621370
if (error == Qnil) {
13631371
int error_state;
13641372
iseq = pm_iseq_new_with_opt(&result.node, name, file, realpath, ln, NULL, 0, ISEQ_TYPE_TOP, &option, &error_state);

test/ruby/test_iseq.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,56 @@ def test_invalid_source
297297
assert_raise(TypeError, bug11159) {compile(1)}
298298
end
299299

300+
def test_invalid_source_no_memory_leak
301+
# [Bug #21394]
302+
assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true)
303+
code = proc do |t|
304+
RubyVM::InstructionSequence.new(nil)
305+
rescue TypeError
306+
else
307+
raise "TypeError was not raised during RubyVM::InstructionSequence.new"
308+
end
309+
310+
10.times(&code)
311+
begin;
312+
1_000_000.times(&code)
313+
end;
314+
315+
# [Bug #21394]
316+
# RubyVM::InstructionSequence.new calls rb_io_path, which dups the string
317+
# and can leak memory if the dup raises
318+
assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true)
319+
MyError = Class.new(StandardError)
320+
String.prepend(Module.new do
321+
def initialize_dup(_)
322+
if $raise_on_dup
323+
raise MyError
324+
else
325+
super
326+
end
327+
end
328+
end)
329+
330+
code = proc do |t|
331+
Tempfile.create do |f|
332+
$raise_on_dup = true
333+
t.times do
334+
RubyVM::InstructionSequence.new(f)
335+
rescue MyError
336+
else
337+
raise "MyError was not raised during RubyVM::InstructionSequence.new"
338+
end
339+
ensure
340+
$raise_on_dup = false
341+
end
342+
end
343+
344+
code.call(100)
345+
begin;
346+
code.call(1_000_000)
347+
end;
348+
end
349+
300350
def test_frozen_string_literal_compile_option
301351
$f = 'f'
302352
line = __LINE__ + 2

0 commit comments

Comments
 (0)
0