Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Avoid reading unused lvars in Primitive.cexpr
Previously on builds with optimizations disabled, this could result in
an out of bounds read. When we had all of:
* built with -O0
* Leaf builtin
* Primitive.mandatory_only
* "no args builtin", called by vm_call_single_noarg_inline_builti
* The stack is escaped to the heap via binding or a proc

This is because mk_builtin_loader generated reads for all locals
regardless of whether they were used and in the case we generated a
mandatory_only iseq that would include more variables than were actually
available.

On optimized builds, the invalid accesses would be optimized away, and
this also was often unnoticed as the invalid access would just hit
another part of the stack unless it had been escaped to the heap.

The fix here is imperfect, as this could have false positives, but since
Primitive.cexpr! is only available within the cruby codebase itself
that's probably fine as a proper fix would be much more challenging (the
only false positives we found were in rjit.rb).

Fixes [Bug #20178]

Co-authored-by: Adam Hess <[email protected]>
  • Loading branch information
2 people authored and djensenius committed Jan 31, 2024
commit 256350b765a51ecd9595dc05bf2ddebb8780b87f
9 changes: 9 additions & 0 deletions bootstraptest/test_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1190,3 +1190,12 @@ def test2 o, args, block
test2 o1, [], block
$result.join
}

assert_equal 'ok', %q{
def foo
binding
["ok"].first
end
foo
foo
}, '[Bug #20178]'
6 changes: 6 additions & 0 deletions tool/mk_builtin_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,17 @@ def collect_iseq iseq_ary

def generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_name)
f = StringIO.new

# Avoid generating fetches of lvars we don't need. This is imperfect as it
# will match text inside strings or other false positives.
local_candidates = text.scan(/[a-zA-Z_][a-zA-Z0-9_]*/)

f.puts '{'
lineno += 1
# locals is nil outside methods
locals&.reverse_each&.with_index{|param, i|
next unless Symbol === param
next unless local_candidates.include?(param.to_s)
f.puts "MAYBE_UNUSED(const VALUE) #{param} = rb_vm_lvar(ec, #{-3 - i});"
lineno += 1
}
Expand Down