From 0b5862f4d9f6a1f86b09eadc54cafa106d996a7c Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 9 Apr 2026 13:22:22 -0700 Subject: [PATCH 1/4] Don't use fixed-size hashes for cdhash This caused out of bounds writes because of converting to a st_table. Co-authored-by: Luke Gruber Co-authored-by: Matt Valentine-House --- compile.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compile.c b/compile.c index e6748d38b309da..ac1bdfe0f9278b 100644 --- a/compile.c +++ b/compile.c @@ -12167,7 +12167,7 @@ iseq_build_from_ary_body(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, case TS_CDHASH: { int i; - VALUE map = rb_hash_alloc_fixed_size(Qfalse, RARRAY_LEN(op)/2); + VALUE map = rb_hash_new_with_size(RARRAY_LEN(op)/2); RHASH_TBL_RAW(map)->type = &cdhash_type; op = rb_to_array_type(op); @@ -12179,7 +12179,7 @@ iseq_build_from_ary_body(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, rb_hash_aset(map, key, (VALUE)label | 1); } RB_GC_GUARD(op); - RB_OBJ_SET_SHAREABLE(map); // allow mutation while compiling + RB_OBJ_SET_SHAREABLE(rb_obj_hide(map)); // allow mutation while compiling argv[j] = map; RB_OBJ_WRITTEN(iseq, Qundef, map); } From 8aa2322bb77120ab39ee63dd27b2d17c60f919ff Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 9 Apr 2026 13:23:06 -0700 Subject: [PATCH 2/4] Add slot size assertion to ar_force_convert_table Co-authored-by: Luke Gruber Co-authored-by: Matt Valentine-House --- hash.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hash.c b/hash.c index 700c429d2aeb49..8507c9c8f5328d 100644 --- a/hash.c +++ b/hash.c @@ -604,6 +604,7 @@ RHASH_AR_TABLE_SIZE_DEC(VALUE h) static inline void RHASH_AR_TABLE_CLEAR(VALUE h) { + RUBY_ASSERT(rb_gc_obj_slot_size(h) >= sizeof(struct RHash) + sizeof(ar_table)); RBASIC(h)->flags &= ~RHASH_AR_TABLE_SIZE_MASK; RBASIC(h)->flags &= ~RHASH_AR_TABLE_BOUND_MASK; @@ -719,6 +720,8 @@ ar_force_convert_table(VALUE hash, const char *file, int line) st_hash_t hashes[RHASH_AR_TABLE_MAX_SIZE]; unsigned int bound, size; + RUBY_ASSERT(rb_gc_obj_slot_size(hash) >= sizeof(struct RHash) + sizeof(ar_table)); + // prepare hash values do { st_data_t keys[RHASH_AR_TABLE_MAX_SIZE]; From 0c1ce03b8c8431d27bc45f88541929881edd4e3e Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Thu, 9 Apr 2026 18:40:17 -0400 Subject: [PATCH 3/4] ZJIT: Make `hir::types::Class` not final and have it include metaclasses Every class boots with a metaclass, and all metaclasses are subclasses of Class, so `types::Class` has no business in `ExactBitsAndClass`. In fact, we should never see an object whose RBasic::class is exactly rb_cClass because classes get a metaclass on boot. So there is no ClassExact type. This fixes the side exits on getivar-module.rb that were introduced in a8f3c34556bac709587ded4e0e4dad08932d5900 ("ZJIT: Add missing guard on ivar access on T_{DATA,CLASS,MODULE}"). The `GuardType v, Class` checked for exactly rb_cClass and never passed. --- zjit/src/hir_type/gen_hir_type.rb | 6 ++++-- zjit/src/hir_type/hir_type.inc.rs | 11 ++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/zjit/src/hir_type/gen_hir_type.rb b/zjit/src/hir_type/gen_hir_type.rb index 37919425cee51f..aca8e574d61457 100644 --- a/zjit/src/hir_type/gen_hir_type.rb +++ b/zjit/src/hir_type/gen_hir_type.rb @@ -89,7 +89,7 @@ def base_type name, c_name: nil [type, exact] end -# Define a new type that cannot be subclassed. +# Define a new type that has no subclasses and cannot be subclassed. # If c_name is given, mark the rb_cXYZ object as equivalent to this type. def final_type name, base: $object, c_name: nil if c_name @@ -109,7 +109,9 @@ def final_type name, base: $object, c_name: nil base_type "Set", c_name: "rb_cSet" base_type "Regexp", c_name: "rb_cRegexp" module_class, _ = base_type "Module", c_name: "rb_cModule" -class_ = final_type "Class", base: module_class, c_name: "rb_cClass" +# Class cannot be subclassed by doing `class Sub < Class`, +# but every metaclass is a subclass of `Class`. It's not final. +module_class.subtype "Class" numeric, _ = base_type "Numeric", c_name: "rb_cNumeric" diff --git a/zjit/src/hir_type/hir_type.inc.rs b/zjit/src/hir_type/hir_type.inc.rs index f37cd57b319445..46c45f17f39e07 100644 --- a/zjit/src/hir_type/hir_type.inc.rs +++ b/zjit/src/hir_type/hir_type.inc.rs @@ -9,7 +9,7 @@ mod bits { pub const BasicObjectSubclass: u64 = 1u64 << 3; pub const Bignum: u64 = 1u64 << 4; pub const BoolExact: u64 = FalseClass | TrueClass; - pub const BuiltinExact: u64 = ArrayExact | BasicObjectExact | Class | FalseClass | Float | HashExact | Integer | ModuleExact | NilClass | NumericExact | ObjectExact | RangeExact | RegexpExact | SetExact | StringExact | Symbol | TrueClass; + pub const BuiltinExact: u64 = ArrayExact | BasicObjectExact | FalseClass | Float | HashExact | Integer | ModuleExact | NilClass | NumericExact | ObjectExact | RangeExact | RegexpExact | SetExact | StringExact | Symbol | TrueClass; pub const CBool: u64 = 1u64 << 5; pub const CDouble: u64 = 1u64 << 6; pub const CInt: u64 = CSigned | CUnsigned; @@ -231,7 +231,7 @@ pub mod types { pub const Truthy: Type = Type::from_bits(bits::Truthy); pub const TypedTData: Type = Type::from_bits(bits::TypedTData); pub const Undef: Type = Type::from_bits(bits::Undef); - pub const ExactBitsAndClass: [(u64, *const VALUE); 17] = [ + pub const ExactBitsAndClass: [(u64, *const VALUE); 16] = [ (bits::ObjectExact, &raw const crate::cruby::rb_cObject), (bits::BasicObjectExact, &raw const crate::cruby::rb_cBasicObject), (bits::StringExact, &raw const crate::cruby::rb_cString), @@ -241,7 +241,6 @@ pub mod types { (bits::SetExact, &raw const crate::cruby::rb_cSet), (bits::RegexpExact, &raw const crate::cruby::rb_cRegexp), (bits::ModuleExact, &raw const crate::cruby::rb_cModule), - (bits::Class, &raw const crate::cruby::rb_cClass), (bits::NumericExact, &raw const crate::cruby::rb_cNumeric), (bits::Integer, &raw const crate::cruby::rb_cInteger), (bits::Float, &raw const crate::cruby::rb_cFloat), @@ -250,9 +249,8 @@ pub mod types { (bits::TrueClass, &raw const crate::cruby::rb_cTrueClass), (bits::FalseClass, &raw const crate::cruby::rb_cFalseClass), ]; - pub const SubclassBitsAndClass: [(u64, *const VALUE); 17] = [ + pub const SubclassBitsAndClass: [(u64, *const VALUE); 16] = [ (bits::ArraySubclass, &raw const crate::cruby::rb_cArray), - (bits::Class, &raw const crate::cruby::rb_cClass), (bits::FalseClass, &raw const crate::cruby::rb_cFalseClass), (bits::Integer, &raw const crate::cruby::rb_cInteger), (bits::HashSubclass, &raw const crate::cruby::rb_cHash), @@ -269,9 +267,8 @@ pub mod types { (bits::ObjectSubclass, &raw const crate::cruby::rb_cObject), (bits::BasicObjectSubclass, &raw const crate::cruby::rb_cBasicObject), ]; - pub const InexactBitsAndClass: [(u64, *const VALUE); 17] = [ + pub const InexactBitsAndClass: [(u64, *const VALUE); 16] = [ (bits::Array, &raw const crate::cruby::rb_cArray), - (bits::Class, &raw const crate::cruby::rb_cClass), (bits::FalseClass, &raw const crate::cruby::rb_cFalseClass), (bits::Integer, &raw const crate::cruby::rb_cInteger), (bits::Hash, &raw const crate::cruby::rb_cHash), From 4c2ae6e2adb40c39276cac860658495f9fa7d967 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 9 Apr 2026 11:48:57 -0700 Subject: [PATCH 4/4] Always ensure room in rb_obj_embedded_size Although the issue only occurred on debug builds, we should always be requesting a size large enough to fit the object when it expands to the heap, rather than just hoping the GC provides enough room. --- internal/object.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/object.h b/internal/object.h index 3cf58d55d940e3..99aa1f524bab5d 100644 --- a/internal/object.h +++ b/internal/object.h @@ -64,9 +64,9 @@ RBASIC_SET_CLASS(VALUE obj, VALUE klass) static inline size_t rb_obj_embedded_size(uint32_t fields_count) { -#if (defined(RACTOR_CHECK_MODE) && RACTOR_CHECK_MODE) || (defined(GC_DEBUG) && GC_DEBUG) - if (fields_count < 1) fields_count = 1; -#endif - return offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); + size_t size = offsetof(struct RObject, as.ary) + (sizeof(VALUE) * fields_count); + // Ensure enough room for the heap pointer if this expands + if (size < sizeof(struct RObject)) size = sizeof(struct RObject); + return size; } #endif /* INTERNAL_OBJECT_H */