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
This commit is contained in:
parent
135583e37c
commit
34b407a4a8
Notes:
git
2025-06-03 14:00:28 +00:00
18
iseq.c
18
iseq.c
@ -1327,6 +1327,15 @@ pm_iseq_compile_with_option(VALUE src, VALUE file, VALUE realpath, VALUE line, V
|
|||||||
ln = NUM2INT(line);
|
ln = NUM2INT(line);
|
||||||
StringValueCStr(file);
|
StringValueCStr(file);
|
||||||
|
|
||||||
|
bool parse_file = false;
|
||||||
|
if (RB_TYPE_P(src, T_FILE)) {
|
||||||
|
parse_file = true;
|
||||||
|
src = rb_io_path(src);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
src = StringValue(src);
|
||||||
|
}
|
||||||
|
|
||||||
pm_parse_result_t result = { 0 };
|
pm_parse_result_t result = { 0 };
|
||||||
pm_options_line_set(&result.options, NUM2INT(line));
|
pm_options_line_set(&result.options, NUM2INT(line));
|
||||||
pm_options_scopes_init(&result.options, 1);
|
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
|
|||||||
VALUE script_lines;
|
VALUE script_lines;
|
||||||
VALUE error;
|
VALUE error;
|
||||||
|
|
||||||
if (RB_TYPE_P(src, T_FILE)) {
|
if (parse_file) {
|
||||||
VALUE filepath = rb_io_path(src);
|
error = pm_load_parse_file(&result, src, ruby_vm_keep_script_lines ? &script_lines : NULL);
|
||||||
error = pm_load_parse_file(&result, filepath, ruby_vm_keep_script_lines ? &script_lines : NULL);
|
|
||||||
RB_GC_GUARD(filepath);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
src = StringValue(src);
|
|
||||||
error = pm_parse_string(&result, src, file, ruby_vm_keep_script_lines ? &script_lines : NULL);
|
error = pm_parse_string(&result, src, file, ruby_vm_keep_script_lines ? &script_lines : NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RB_GC_GUARD(src);
|
||||||
|
|
||||||
if (error == Qnil) {
|
if (error == Qnil) {
|
||||||
int error_state;
|
int error_state;
|
||||||
iseq = pm_iseq_new_with_opt(&result.node, name, file, realpath, ln, NULL, 0, ISEQ_TYPE_TOP, &option, &error_state);
|
iseq = pm_iseq_new_with_opt(&result.node, name, file, realpath, ln, NULL, 0, ISEQ_TYPE_TOP, &option, &error_state);
|
||||||
|
@ -297,6 +297,56 @@ class TestISeq < Test::Unit::TestCase
|
|||||||
assert_raise(TypeError, bug11159) {compile(1)}
|
assert_raise(TypeError, bug11159) {compile(1)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_invalid_source_no_memory_leak
|
||||||
|
# [Bug #21394]
|
||||||
|
assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true)
|
||||||
|
code = proc do |t|
|
||||||
|
RubyVM::InstructionSequence.new(nil)
|
||||||
|
rescue TypeError
|
||||||
|
else
|
||||||
|
raise "TypeError was not raised during RubyVM::InstructionSequence.new"
|
||||||
|
end
|
||||||
|
|
||||||
|
10.times(&code)
|
||||||
|
begin;
|
||||||
|
1_000_000.times(&code)
|
||||||
|
end;
|
||||||
|
|
||||||
|
# [Bug #21394]
|
||||||
|
# RubyVM::InstructionSequence.new calls rb_io_path, which dups the string
|
||||||
|
# and can leak memory if the dup raises
|
||||||
|
assert_no_memory_leak(["-rtempfile"], "#{<<-"begin;"}", "#{<<-'end;'}", rss: true)
|
||||||
|
MyError = Class.new(StandardError)
|
||||||
|
String.prepend(Module.new do
|
||||||
|
def initialize_dup(_)
|
||||||
|
if $raise_on_dup
|
||||||
|
raise MyError
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
code = proc do |t|
|
||||||
|
Tempfile.create do |f|
|
||||||
|
$raise_on_dup = true
|
||||||
|
t.times do
|
||||||
|
RubyVM::InstructionSequence.new(f)
|
||||||
|
rescue MyError
|
||||||
|
else
|
||||||
|
raise "MyError was not raised during RubyVM::InstructionSequence.new"
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
$raise_on_dup = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
code.call(100)
|
||||||
|
begin;
|
||||||
|
code.call(1_000_000)
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
|
||||||
def test_frozen_string_literal_compile_option
|
def test_frozen_string_literal_compile_option
|
||||||
$f = 'f'
|
$f = 'f'
|
||||||
line = __LINE__ + 2
|
line = __LINE__ + 2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user