Look up the nearest user box for :caller_user_box

The box of the immediately previous frame can be the root box, e.g. when
a proc made in the root box calls Marshal.load, and the classes of the
caller's data are invisible there. Walk the caller frames up to the
nearest user box instead, and rename the attribute after what it finds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hiroshi SHIBATAandClaude Opus 5 committed 2026-09-14 12:39:17 +09:00
1 parent 60d29cd2d3
commit 111cc79210
8 files changed
+48 -13
Notes: git 2026-09-14 03:39:53 +00:00

No files matched your search

+2 -2
View File
@@ -9394,8 +9394,8 @@ compile_builtin_attr_symbol(rb_iseq_t *iseq, VALUE symbol)
else if (rb_streql_lit(string, "without_interrupts")) {
ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS;
}
else if (rb_streql_lit(string, "caller_box")) {
ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_CALLER_BOX;
else if (rb_streql_lit(string, "caller_user_box")) {
ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_CALLER_USER_BOX;
}
else {
return COMPILE_NG;
+1 -1
View File
@@ -31,7 +31,7 @@ module Marshal
# # => [1039360, 1039380, 1039360, 1039380] -- only 2 different objects, object_ids repeating
#
def self.load(source, proc = nil, freeze: false)
Primitive.attr! :caller_box
Primitive.attr! :caller_user_box
Primitive.marshal_load(source, proc, freeze)
end
+11 -1
View File
@@ -456,7 +456,7 @@ class TestBox < Test::Unit::TestCase
end;
end
def test_marshal_resolves_classes_in_the_caller_box
def test_marshal_resolves_classes_in_the_caller_user_box
setup_box
obj = @box.eval("class BoxMarshalBar; end; Marshal.load(Marshal.dump(BoxMarshalBar.new))")
@@ -468,6 +468,16 @@ class TestBox < Test::Unit::TestCase
Marshal.load(dump)
end
end
def test_marshal_skips_root_box_frames_in_the_caller_stack
assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true)
begin;
class BoxMarshalBaz; end
# the proc runs in the root box, where BoxMarshalBaz is invisible
loader = Ruby::Box.root.eval("->(dump) { Marshal.load(dump) }")
assert_instance_of BoxMarshalBaz, loader.call(Marshal.dump(BoxMarshalBaz.new))
end;
end
end
class TestBoxDescendantsMain
+1 -1
View File
@@ -7,7 +7,7 @@ require_relative 'ruby_vm/helpers/c_escape'
SUBLIBS = {}
REQUIRED = {}
BUILTIN_ATTRS = %w[leaf inline_block use_block c_trace without_interrupts caller_box]
BUILTIN_ATTRS = %w[leaf inline_block use_block c_trace without_interrupts caller_user_box]
module CompileWarning
@@warnings = 0
+29 -4
View File
@@ -3317,6 +3317,31 @@ rb_vm_frame_flag_set_box_require(const rb_execution_context_t *ec)
VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_BOX_REQUIRE);
}
static const rb_box_t *current_box_on_cfp(const rb_execution_context_t *ec, const rb_control_frame_t *cfp);
/**
* Returns the nearest user box in the caller frames, or NULL if there is none.
*
* Builtin methods written in Ruby are defined in the master box, so their own
* frame tells nothing about the caller. Those marked with
* `Primitive.attr! :caller_user_box` need the box owning the caller code, and the
* frames in between may belong to the master or the root box, e.g. when another
* builtin method or a proc made in the root box calls them.
*/
static const rb_box_t *
caller_user_box_on_cfp(const rb_execution_context_t *ec, const rb_control_frame_t *cfp)
{
const rb_control_frame_t * const eocfp = RUBY_VM_END_CONTROL_FRAME(ec);
while (RUBY_VM_VALID_CONTROL_FRAME_P(cfp, eocfp)) {
const rb_box_t *box = current_box_on_cfp(ec, cfp);
if (BOX_USER_P(box))
return box;
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
}
return NULL;
}
static const rb_box_t *
current_box_on_cfp(const rb_execution_context_t *ec, const rb_control_frame_t *cfp)
{
@@ -3331,12 +3356,12 @@ current_box_on_cfp(const rb_execution_context_t *ec, const rb_control_frame_t *c
VM_BOX_ASSERT(cme, "cme should be valid");
VM_BOX_ASSERT(cme->def, "cme->def shold be valid");
if (cme->def->type == VM_METHOD_TYPE_ISEQ &&
(ISEQ_BODY(cme->def->body.iseq.iseqptr)->builtin_attrs & BUILTIN_ATTR_CALLER_BOX)) {
// Builtin methods with `Primitive.attr! :caller_box` operate on the caller box,
// just like CFUNC frames. See the comment in VM_EP_RUBY_LEP().
(ISEQ_BODY(cme->def->body.iseq.iseqptr)->builtin_attrs & BUILTIN_ATTR_CALLER_USER_BOX)) {
const rb_control_frame_t *owner_cfp = rb_vm_search_cf_from_ep(ec, cfp, lep);
if (owner_cfp) {
return current_box_on_cfp(ec, RUBY_VM_PREVIOUS_CONTROL_FRAME(owner_cfp));
box = caller_user_box_on_cfp(ec, RUBY_VM_PREVIOUS_CONTROL_FRAME(owner_cfp));
if (box)
return box;
}
}
return cme->def->box;
+2 -2
View File
@@ -407,8 +407,8 @@ enum rb_builtin_attr {
BUILTIN_ATTR_C_TRACE = 0x08,
// The iseq uses noint branch/jump opcodes that skip interrupt checking.
BUILTIN_ATTR_WITHOUT_INTERRUPTS = 0x10,
// The iseq operates on its caller's box, like C functions do.
BUILTIN_ATTR_CALLER_BOX = 0x20,
// The iseq operates on the nearest user box in its caller frames.
BUILTIN_ATTR_CALLER_USER_BOX = 0x20,
};
typedef VALUE (*rb_jit_func_t)(struct rb_execution_context_struct *, struct rb_control_frame_struct *);
+1 -1
View File
@@ -487,7 +487,7 @@ pub const BUILTIN_ATTR_SINGLE_NOARG_LEAF: rb_builtin_attr = 2;
pub const BUILTIN_ATTR_INLINE_BLOCK: rb_builtin_attr = 4;
pub const BUILTIN_ATTR_C_TRACE: rb_builtin_attr = 8;
pub const BUILTIN_ATTR_WITHOUT_INTERRUPTS: rb_builtin_attr = 16;
pub const BUILTIN_ATTR_CALLER_BOX: rb_builtin_attr = 32;
pub const BUILTIN_ATTR_CALLER_USER_BOX: rb_builtin_attr = 32;
pub type rb_builtin_attr = u32;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
+1 -1
View File
@@ -642,7 +642,7 @@ pub const BUILTIN_ATTR_SINGLE_NOARG_LEAF: rb_builtin_attr = 2;
pub const BUILTIN_ATTR_INLINE_BLOCK: rb_builtin_attr = 4;
pub const BUILTIN_ATTR_C_TRACE: rb_builtin_attr = 8;
pub const BUILTIN_ATTR_WITHOUT_INTERRUPTS: rb_builtin_attr = 16;
pub const BUILTIN_ATTR_CALLER_BOX: rb_builtin_attr = 32;
pub const BUILTIN_ATTR_CALLER_USER_BOX: rb_builtin_attr = 32;
pub type rb_builtin_attr = u32;
pub type rb_jit_func_t = ::std::option::Option<
unsafe extern "C" fn(