From e29bcf6c419b66165f3cba01162b441f49075275 Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Wed, 17 Jun 2026 12:36:50 -0700 Subject: [PATCH 1/7] gc: Fix inconsistent args in rb_gc_unregister_address that can cause heap-use-after-free The destination should be an address just like the source. Added a regression test based on the nokogiri code that highlighted the bug under ASan. --- ext/-test-/gc/register/extconf.rb | 2 + ext/-test-/gc/register/register.c | 62 +++++++++++++++++++++++++++++++ gc.c | 2 +- test/-ext-/gc/test_register.rb | 12 ++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 ext/-test-/gc/register/extconf.rb create mode 100644 ext/-test-/gc/register/register.c create mode 100644 test/-ext-/gc/test_register.rb diff --git a/ext/-test-/gc/register/extconf.rb b/ext/-test-/gc/register/extconf.rb new file mode 100644 index 00000000000000..92c142fb1eef56 --- /dev/null +++ b/ext/-test-/gc/register/extconf.rb @@ -0,0 +1,2 @@ +# frozen_string_literal: false +create_makefile("-test-/gc/register") diff --git a/ext/-test-/gc/register/register.c b/ext/-test-/gc/register/register.c new file mode 100644 index 00000000000000..903b8f4074f44d --- /dev/null +++ b/ext/-test-/gc/register/register.c @@ -0,0 +1,62 @@ +#include "ruby.h" + +/* + * Regression test for a heap-use-after-free in rb_gc_unregister_address(). + * + * This mirrors the pattern used by some C extensions (e.g. Nokogiri's XPath + * argument marshalling): a buffer is ruby_xcalloc()'d, the address of each of + * its slots is registered with rb_gc_register_address(), and later each slot + * is unregistered before the buffer is ruby_xfree()'d. + * https://github.com/sparklemotion/nokogiri/blob/fea733159ad8c0328c591125bb1fb30681859a0d/ext/nokogiri/xml_xpath_context.c#L231-L255 + * + * A buggy rb_gc_unregister_address() wrote *through* the address being removed + * (corrupting the sibling slots) and failed to drop the entry from its internal + * list, leaving a dangling pointer into the freed buffer that the next GC would + * dereference. + * + * Returns true if unregistering one slot leaves the other registered slots + * untouched. The trailing ruby_xfree()+gc additionally reproduces the literal + * use-after-free for ASAN/Valgrind builds. + */ +static VALUE +gc_unregister_address_keeps_siblings(VALUE self) +{ + const int n = 4; + VALUE *buf = ruby_xcalloc((size_t)n, sizeof(VALUE)); + VALUE saved = rb_ary_new_capa(n); + VALUE result = Qtrue; + + for (int i = 0; i < n; i++) { + buf[i] = rb_sprintf("registered address %d", i); + rb_gc_register_address(&buf[i]); + rb_ary_push(saved, buf[i]); /* keep the objects reachable */ + } + + /* Unregistering a middle slot must only update the internal list; it must + * not write through &buf[1] and corrupt the sibling slots. */ + rb_gc_unregister_address(&buf[1]); + for (int i = 0; i < n; i++) { + if (i == 1) continue; + if (buf[i] != rb_ary_entry(saved, i)) result = Qfalse; + } + + /* Clean up the way an extension would. With the bug, a dangling registered + * address into this freed buffer triggers a use-after-free in the GC. */ + rb_gc_unregister_address(&buf[0]); + rb_gc_unregister_address(&buf[2]); + rb_gc_unregister_address(&buf[3]); + ruby_xfree(buf); + rb_gc(); + + RB_GC_GUARD(saved); + return result; +} + +void +Init_register(void) +{ + VALUE mBug = rb_define_module("Bug"); + VALUE mGC = rb_define_module_under(mBug, "GC"); + rb_define_singleton_method(mGC, "unregister_address_keeps_siblings?", + gc_unregister_address_keeps_siblings, 0); +} diff --git a/gc.c b/gc.c index 7407fa66420a30..057ee17cf933e3 100644 --- a/gc.c +++ b/gc.c @@ -3762,7 +3762,7 @@ rb_gc_unregister_address(VALUE *addr) for (index = 0; index < vm->global_object_list_size; index++) { if (addr == vm->global_object_list[index]) { MEMMOVE( - vm->global_object_list[index], + &vm->global_object_list[index], &vm->global_object_list[index + 1], VALUE *, vm->global_object_list_size - index - 1 diff --git a/test/-ext-/gc/test_register.rb b/test/-ext-/gc/test_register.rb new file mode 100644 index 00000000000000..cd5fa6a5907b55 --- /dev/null +++ b/test/-ext-/gc/test_register.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: false +require 'test/unit' +require '-test-/gc/register' + +class Test_GCRegisterAddress < Test::Unit::TestCase + # Regression test for a heap-use-after-free in rb_gc_unregister_address(): + # unregistering one registered address must not corrupt the sibling slots or + # leave a dangling pointer for the next GC to mark. + def test_unregister_address_keeps_other_registered_addresses + assert_equal(true, Bug::GC.unregister_address_keeps_siblings?) + end +end From 5eb78494be514699a670980d073f9170a6d50ca7 Mon Sep 17 00:00:00 2001 From: XrXr Date: Wed, 17 Jun 2026 12:39:55 -0400 Subject: [PATCH 2/7] Copyedit test-all -j crash message. Add hints about RUBY_CRASH_REPORT The GitHub Actions part particularly is for ZJIT and YJIT jobs which have a separate "Dump crash logs" fold. There doesn't seem to be a way to ask for the web UI to unfold on loading of the logs. --- tool/lib/test/unit.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb index 2663b7b76a10bf..0eb8392179a69b 100644 --- a/tool/lib/test/unit.rb +++ b/tool/lib/test/unit.rb @@ -460,10 +460,15 @@ def after_worker_down(worker, e=nil, c=false) real_file = worker.real_file and warn "running file: #{real_file}" @need_quit = true warn "" - warn "A test worker crashed. It might be an interpreter bug or" - warn "a bug in test/unit/parallel.rb. Try again without the -j" - warn "option." + warn "A test worker crashed. It might be a bug in the" + warn "Ruby runtime or a bug in test/unit/parallel.rb." + warn "You may want to try again without the -j option." warn "" + if ENV["RUBY_CRASH_REPORT"] + warn "hint: RUBY_CRASH_REPORT is set and may have produced additional information." + warn " Crash logs might be collapsed under a different fold on CI." if "true" == ENV["GITHUB_ACTIONS"] + warn "" + end if File.exist?('core') require 'fileutils' require 'time' From 60aec1dccb65aa907d7724db0821af447e6d2260 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 17 Jun 2026 14:57:58 -0700 Subject: [PATCH 3/7] ZJIT: Centralize stack index management in StackState (#17372) --- zjit/src/backend/arm64/mod.rs | 80 +++++++++--------- zjit/src/backend/lir.rs | 147 ++++++++++++++++++++++----------- zjit/src/backend/x86_64/mod.rs | 74 ++++++++--------- zjit/src/codegen.rs | 26 +----- 4 files changed, 171 insertions(+), 156 deletions(-) diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index 87b388eefa748b..fda896860ab4a1 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -680,14 +680,14 @@ impl Assembler { } /// If opnd is Opnd::Mem with MemBase::Stack, lower it to Opnd::Mem with MemBase::Reg, and split a large disp. - fn split_stack_membase(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd, stack_state: &StackState) -> Opnd { - let opnd = split_only_stack_membase(asm, opnd, scratch_opnd, stack_state); + fn split_stack_membase(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd) -> Opnd { + let opnd = split_only_stack_membase(asm, opnd, scratch_opnd); split_large_disp(asm, opnd, scratch_opnd) } /// split_stack_membase but without split_large_disp. This should be used only by lea, /// whose lowering already handles large displacements in arm64_emit. - fn split_only_stack_membase(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd, stack_state: &StackState) -> Opnd { + fn split_only_stack_membase(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd) -> Opnd { match opnd { Opnd::Mem(Mem { base: stack_membase @ MemBase::Stack { .. }, disp: opnd_disp, num_bits: opnd_num_bits }) => { // Convert MemBase::Stack to MemBase::Reg(NATIVE_BASE_PTR) with the @@ -695,13 +695,13 @@ impl Assembler { // [NATIVE_BASE_PTR + stack_disp], so we just adjust the base and // combine displacements -- no indirection needed. Large // displacements are handled by split_stack_membase(). - let Mem { base, disp: stack_disp, .. } = stack_state.stack_membase_to_mem(stack_membase); + let Mem { base, disp: stack_disp, .. } = asm.stack_state.stack_membase_to_mem(stack_membase); Opnd::Mem(Mem { base, disp: stack_disp + opnd_disp, num_bits: opnd_num_bits }) } Opnd::Mem(Mem { base: MemBase::StackIndirect { stack_idx }, disp: opnd_disp, num_bits: opnd_num_bits }) => { // The spilled value (a pointer) lives at a stack slot. Load it // into a scratch register, then use the register as the base. - let stack_mem = stack_state.stack_membase_to_mem(MemBase::Stack { stack_idx, num_bits: 64 }); + let stack_mem = asm.stack_state.stack_membase_to_mem(MemBase::Stack { stack_idx, num_bits: 64 }); let stack_opnd = split_large_disp(asm, Opnd::Mem(stack_mem), scratch_opnd); asm.load_into(scratch_opnd, stack_opnd); Opnd::Mem(Mem { @@ -715,9 +715,9 @@ impl Assembler { } /// If opnd is Opnd::Mem, lower it to scratch_opnd. You should use this when `opnd` is read by the instruction, not written. - fn split_memory_read(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd, stack_state: &StackState) -> Opnd { + fn split_memory_read(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd) -> Opnd { if let Opnd::Mem(_) = opnd { - let opnd = split_stack_membase(asm, opnd, scratch_opnd, stack_state); + let opnd = split_stack_membase(asm, opnd, scratch_opnd); let scratch_opnd = opnd.num_bits().map(|num_bits| scratch_opnd.with_num_bits(num_bits)).unwrap_or(scratch_opnd); asm.load_into(scratch_opnd, opnd); scratch_opnd @@ -737,14 +737,8 @@ impl Assembler { } } - // Prepare StackState to lower MemBase::Stack - let stack_state = StackState::new(self.stack_base_idx); - - let mut asm_local = Assembler::new(); + let mut asm_local = Assembler::new_with_asm_without_blocks(&self); asm_local.accept_scratch_reg = true; - asm_local.stack_base_idx = self.stack_base_idx; - asm_local.label_names = self.label_names.clone(); - asm_local.num_vregs = self.num_vregs; // Create one giant block to linearize everything into asm_local.new_block_without_id("linearized"); @@ -771,27 +765,27 @@ impl Assembler { Insn::CSelLE { truthy: left, falsy: right, out } | Insn::CSelG { truthy: left, falsy: right, out } | Insn::CSelGE { truthy: left, falsy: right, out } => { - *left = split_memory_read(asm, *left, SCRATCH0_OPND, &stack_state); - *right = split_memory_read(asm, *right, SCRATCH1_OPND, &stack_state); + *left = split_memory_read(asm, *left, SCRATCH0_OPND); + *right = split_memory_read(asm, *right, SCRATCH1_OPND); let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); if let Some(mem_out) = mem_out { - let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND, &stack_state); + let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND); asm.store(mem_out, SCRATCH0_OPND); } } Insn::Mul { left, right, out } => { - *left = split_memory_read(asm, *left, SCRATCH0_OPND, &stack_state); - *right = split_memory_read(asm, *right, SCRATCH1_OPND, &stack_state); + *left = split_memory_read(asm, *left, SCRATCH0_OPND); + *right = split_memory_read(asm, *right, SCRATCH1_OPND); let mem_out = split_memory_write(out, SCRATCH0_OPND); let reg_out = out.clone(); asm.push_insn(insn); if let Some(mem_out) = mem_out { - let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND, &stack_state); + let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND); asm.store(mem_out, SCRATCH0_OPND); }; @@ -804,20 +798,20 @@ impl Assembler { } Insn::LShift { opnd, out, .. } | Insn::RShift { opnd, out, .. } => { - *opnd = split_memory_read(asm, *opnd, SCRATCH0_OPND, &stack_state); + *opnd = split_memory_read(asm, *opnd, SCRATCH0_OPND); let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); if let Some(mem_out) = mem_out { - let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND, &stack_state); + let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND); asm.store(mem_out, SCRATCH0_OPND); } } Insn::Cmp { left, right } | Insn::Test { left, right } => { - *left = split_memory_read(asm, *left, SCRATCH0_OPND, &stack_state); - *right = split_memory_read(asm, *right, SCRATCH1_OPND, &stack_state); + *left = split_memory_read(asm, *left, SCRATCH0_OPND); + *right = split_memory_read(asm, *right, SCRATCH1_OPND); asm.push_insn(insn); } // For compile_exits, support splitting simple C arguments here @@ -837,20 +831,20 @@ impl Assembler { asm.cret(C_RET_OPND); } Insn::Lea { opnd, out } => { - *opnd = split_only_stack_membase(asm, *opnd, SCRATCH0_OPND, &stack_state); + *opnd = split_only_stack_membase(asm, *opnd, SCRATCH0_OPND); let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); if let Some(mem_out) = mem_out { - let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND, &stack_state); + let mem_out = split_stack_membase(asm, mem_out, SCRATCH1_OPND); asm.store(mem_out, SCRATCH0_OPND); } } Insn::Load { opnd, out } | Insn::LoadInto { opnd, dest: out } => { - *opnd = split_stack_membase(asm, *opnd, SCRATCH0_OPND, &stack_state); - *out = split_stack_membase(asm, *out, SCRATCH1_OPND, &stack_state); + *opnd = split_stack_membase(asm, *opnd, SCRATCH0_OPND); + *out = split_stack_membase(asm, *out, SCRATCH1_OPND); if let Opnd::Mem(_) = out { // If NATIVE_STACK_PTR is used as a source for Store, it's handled as xzr, storeing zero. @@ -883,13 +877,13 @@ impl Assembler { asm.push_insn(Insn::Jne(label)); } Insn::Store { dest, src } => { - *dest = split_stack_membase(asm, *dest, SCRATCH0_OPND, &stack_state); - *src = split_stack_membase(asm, *src, SCRATCH1_OPND, &stack_state); + *dest = split_stack_membase(asm, *dest, SCRATCH0_OPND); + *src = split_stack_membase(asm, *src, SCRATCH1_OPND); asm.push_insn(insn); } Insn::Mov { dest, src } => { - *src = split_stack_membase(asm, *src, SCRATCH0_OPND, &stack_state); - *dest = split_stack_membase(asm, *dest, SCRATCH1_OPND, &stack_state); + *src = split_stack_membase(asm, *src, SCRATCH0_OPND); + *dest = split_stack_membase(asm, *dest, SCRATCH1_OPND); match dest { Opnd::Reg(_) => asm.load_into(*dest, *src), Opnd::Mem(_) => asm.store(*dest, *src), @@ -1643,8 +1637,9 @@ impl Assembler { let preferred_registers = trace_compile_phase("preferred_registers", || asm.preferred_register_assignments(&intervals)); let (assignments, num_stack_slots) = trace_compile_phase("linear_scan", || asm.linear_scan(intervals.clone(), regs.len(), &preferred_registers)); - let total_stack_slots = asm.stack_base_idx + num_stack_slots; - if total_stack_slots > Self::MAX_FRAME_STACK_SLOTS { + asm.stack_state.num_spill_slots = num_stack_slots; + let stack_slot_count = asm.stack_state.stack_slot_count(); + if stack_slot_count > Self::MAX_FRAME_STACK_SLOTS { return Err(CompileError::NativeStackTooLarge); } @@ -1668,21 +1663,20 @@ impl Assembler { } } - // Update FrameSetup slot_count to account for: - // 1) stack slots reserved for block params (stack_base_idx), and - // 2) register allocator spills (num_stack_slots). + // Update FrameSetup slot_count now that StackState knows the + // register allocator spill count. trace_compile_phase("count_stack_slots", || { for block in asm.basic_blocks.iter_mut() { for insn in block.insns.iter_mut() { if let Insn::FrameSetup { slot_count, .. } = insn { - *slot_count = total_stack_slots; + *slot_count = stack_slot_count; } } } }); trace_compile_phase("resolve_ssa", || { - asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS, total_stack_slots); + asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS); asm.resolve_ssa(&intervals, &assignments); }); @@ -1770,7 +1764,7 @@ mod tests { let mut asm = Assembler::new(); asm.new_block_without_id("test"); - asm.stack_base_idx = 1; + asm.stack_state.stack_base_idx = 1; let label = asm.new_label("bb0"); asm.write_label(label.clone()); @@ -1970,7 +1964,7 @@ mod tests { // Test 3 preserved regs (odd), odd slot_count let cb1 = { let (mut asm, mut cb) = setup_asm(); - asm.stack_base_idx = 3; + asm.stack_state.stack_base_idx = 3; asm.frame_setup(THREE_REGS); asm.frame_teardown(THREE_REGS); asm.compile_with_num_regs(&mut cb, 0); @@ -1980,7 +1974,7 @@ mod tests { // Test 3 preserved regs (odd), even slot_count let cb2 = { let (mut asm, mut cb) = setup_asm(); - asm.stack_base_idx = 4; + asm.stack_state.stack_base_idx = 4; asm.frame_setup(THREE_REGS); asm.frame_teardown(THREE_REGS); asm.compile_with_num_regs(&mut cb, 0); @@ -1991,7 +1985,7 @@ mod tests { let cb3 = { static FOUR_REGS: &[Opnd] = &[Opnd::Reg(X19_REG), Opnd::Reg(X20_REG), Opnd::Reg(X21_REG), Opnd::Reg(X22_REG)]; let (mut asm, mut cb) = setup_asm(); - asm.stack_base_idx = 3; + asm.stack_state.stack_base_idx = 3; asm.frame_setup(FOUR_REGS); asm.frame_teardown(FOUR_REGS); asm.compile_with_num_regs(&mut cb, 0); diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 84c8c8ba0f1365..dcbc6e5243bc0a 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -1362,16 +1362,85 @@ impl Allocation { } } -/// StackState converts abstract stack slots into concrete stack addresses. +/// StackState tracks the native stack layout and converts abstract stack slots +/// into concrete stack addresses. +#[derive(Clone)] pub struct StackState { - /// Copy of Assembler::stack_base_idx. Used for calculating stack slot offsets. - stack_base_idx: usize, + /// The number of stack slots reserved before register allocator spills. + pub(crate) stack_base_idx: usize, + + /// The number of stack slots needed by register allocator spills. + pub(crate) num_spill_slots: usize, } impl StackState { - /// Initialize a stack allocator - pub(super) fn new(stack_base_idx: usize) -> Self { - StackState { stack_base_idx } + /// Initialize an empty stack state. + fn new() -> Self { + StackState { stack_base_idx: 0, num_spill_slots: 0 } + } + + /// Initialize a stack state with a fixed number of reserved stack slots. + fn new_with_stack_slots(stack_base_idx: usize) -> Self { + StackState { stack_base_idx, num_spill_slots: 0 } + } + + /// Reserve native stack slots for JITFrame storage and stack-allocated operands. + /// Returns the total number of reserved slots for the current allocation. + /// + /// Native stack layout: + /// + /// high addr + /// +------------------------+ + /// | return address | + /// +------------------------+ + /// | previous frame pointer | <- NATIVE_BASE_PTR (== depth-0 cfp->jit_return) + /// +------------------------+ + /// | JITFrame slot depth 0 | <- [NATIVE_BASE_PTR - 8]; read by CFP_ZJIT_FRAME for the top-level frame + /// +------------------------+ + /// | ... | one slot per inlining depth (jit_frame_size slots total) + /// +------------------------+ + /// | JITFrame slot depth N | <- innermost inlined frame's slot + /// +------------------------+ + /// | opnds.last() | + /// +------------------------+ + /// | ... | + /// +------------------------+ + /// | opnds.first() | <- pointer returned by Assembler::alloc_stack() + /// +------------------------+ + /// | register spill slots | if any + /// +------------------------+ + /// | FrameSetup align slot | if needed + /// +------------------------+ + /// low addr + pub(crate) fn reserve_stack_slots(&mut self, jit_frame_size: usize, stack_size: usize) -> usize { + let total_stack_size = jit_frame_size + stack_size; + self.stack_base_idx = self.stack_base_idx.max(total_stack_size); + total_stack_size + } + + /// Return the total number of native stack slots used for the frame's + /// reserved data and register allocator spills. + pub(crate) fn stack_slot_count(&self) -> usize { + self.stack_base_idx + self.num_spill_slots + } + + /// Return the stack-map index for a VReg stored in an allocator spill slot. + /// rb_zjit_materialize_frames() reads this as cfp->jit_return[-index]. + fn stack_map_index_for_spill(&self, stack_idx: usize) -> usize { + self.stack_base_idx + .checked_add(1) + .expect("StackMap requires a JITFrame slot") + + stack_idx + } + + /// Return the stack-map index for a VReg saved by handle_caller_saved_regs(). + /// rb_zjit_materialize_frames() reads this as cfp->jit_return[-index]. + fn stack_map_index_for_caller_saved_reg(&self, position: usize) -> usize { + let frame_alignment_slots = self.stack_slot_count() % 2; + (self.stack_slot_count() + frame_alignment_slots) + .checked_add(1) + .expect("StackMap requires a JITFrame slot") + + position } /// Convert a stack index to the `disp` of the stack slot @@ -1426,9 +1495,8 @@ pub struct Assembler { /// On `compile`, it also disables the backend's use of them. pub(super) accept_scratch_reg: bool, - /// The maximum number of stack slots that have been reserved - /// by Assembler::alloc_stack(). - pub stack_base_idx: usize, + /// Native stack layout state. + pub(crate) stack_state: StackState, /// If Some, the next ccall should verify its leafness leaf_ccall_stack_size: Option, @@ -1449,7 +1517,7 @@ impl Assembler Self { label_names: Vec::default(), accept_scratch_reg: false, - stack_base_idx: 0, + stack_state: StackState::new(), leaf_ccall_stack_size: None, basic_blocks: Vec::default(), current_block_id: BlockId(0), @@ -1461,7 +1529,7 @@ impl Assembler /// Create an Assembler, reserving a specified number of stack slots pub fn new_with_stack_slots(stack_base_idx: usize) -> Self { - Self { stack_base_idx, ..Self::new() } + Self { stack_state: StackState::new_with_stack_slots(stack_base_idx), ..Self::new() } } /// Create an Assembler that allows the use of scratch registers. @@ -1473,12 +1541,7 @@ impl Assembler /// Create an Assembler with parameters of another Assembler and empty instructions. /// Compiler passes build a next Assembler with this API and insert new instructions to it. pub(super) fn new_with_asm(old_asm: &Assembler) -> Self { - let mut asm = Self { - label_names: old_asm.label_names.clone(), - accept_scratch_reg: old_asm.accept_scratch_reg, - stack_base_idx: old_asm.stack_base_idx, - ..Self::new() - }; + let mut asm = Self::new_with_asm_without_blocks(old_asm); // Initialize basic blocks from the old assembler, preserving hir_block_id and entry flag // but with empty instruction lists @@ -1486,6 +1549,18 @@ impl Assembler asm.new_block_from_old_block(&old_block); } + asm + } + + /// Create an Assembler with parameters of another Assembler, but without basic blocks. + pub(super) fn new_with_asm_without_blocks(old_asm: &Assembler) -> Self { + let mut asm = Self { + label_names: old_asm.label_names.clone(), + accept_scratch_reg: old_asm.accept_scratch_reg, + stack_state: old_asm.stack_state.clone(), + ..Self::new() + }; + // Initialize num_vregs to match the old assembler's size // This allows reusing VRegs from the old assembler asm.num_vregs = old_asm.num_vregs; @@ -2124,7 +2199,6 @@ impl Assembler intervals: &[Interval], assignments: &[Option], regs: &[Reg], - total_stack_slots: usize, ) { use crate::backend::parcopy; use crate::backend::current::{C_RET_OPND, SCRATCH_REG, ALLOC_REGS}; @@ -2204,33 +2278,13 @@ impl Assembler value } Opnd::VReg { idx: VRegId(vreg_id), .. } => { - // Calculate the offset from NATIVE_BASE_PTR to the stack slot for this VReg. let vreg_stack_index = match assignments[*vreg_id].expect("StackMap VReg should have an allocation") { Allocation::Reg(_) | Allocation::Fixed(_) => { let position = survivors.iter().position(|&survivor_id| survivor_id == *vreg_id).unwrap(); - // See Assembler::alloc_stack's native stack diagram. rb_zjit_materialize_frames() - // reads vreg_stack_index as cfp->jit_return[-vreg_stack_index]. - // For both arches, FrameSetup may add one native stack slot for - // alignment before these CPushPairs. - // TODO: Centralize stack slot offset calculation in StackState. - let frame_alignment_slots = if total_stack_slots % 2 == 1 { - 1 - } else { - 0 - }; - (total_stack_slots + frame_alignment_slots) - .checked_add(1) - .expect("StackMap requires a JITFrame slot") - + position + self.stack_state.stack_map_index_for_caller_saved_reg(position) } Allocation::Stack(stack_idx) => { - // StackState places allocator spills at: - // cfp->jit_return[-(self.stack_base_idx + stack_idx + 1)] - // so encode the matching materializer index. - self.stack_base_idx - .checked_add(1) - .expect("StackMap requires a JITFrame slot") - + stack_idx + self.stack_state.stack_map_index_for_spill(stack_idx) } }; @@ -3469,7 +3523,7 @@ impl Assembler { } pub fn frame_setup(&mut self, preserved_regs: &'static [Opnd]) { - let slot_count = self.stack_base_idx; + let slot_count = self.stack_state.stack_slot_count(); self.push_insn(Insn::FrameSetup { preserved: preserved_regs, slot_count }); } @@ -3648,11 +3702,7 @@ impl Assembler { /// This is used for trampolines that don't allow scratch registers. /// Linearizes all blocks into a single giant block. pub fn resolve_parallel_mov_pass(self) -> Assembler { - let mut asm_local = Assembler::new(); - asm_local.accept_scratch_reg = self.accept_scratch_reg; - asm_local.stack_base_idx = self.stack_base_idx; - asm_local.label_names = self.label_names.clone(); - asm_local.num_vregs = self.num_vregs; + let mut asm_local = Assembler::new_with_asm_without_blocks(&self); // Create one giant block to linearize everything into asm_local.new_block_without_id("linearized"); @@ -4529,7 +4579,8 @@ mod tests { let intervals = asm.build_intervals(live_in); let num_regs = 2; let preferred_registers = asm.preferred_register_assignments(&intervals); - let (assignments, _) = asm.linear_scan(intervals.clone(), num_regs, &preferred_registers); + let (assignments, num_stack_slots) = asm.linear_scan(intervals.clone(), num_regs, &preferred_registers); + asm.stack_state.num_spill_slots = num_stack_slots; let regs = &ALLOC_REGS[..num_regs]; @@ -4541,7 +4592,7 @@ mod tests { "v1 should be in a register"); // Run the pipeline: handle_caller_saved_regs then resolve_ssa - asm.handle_caller_saved_regs(&intervals, &assignments, regs, 0); + asm.handle_caller_saved_regs(&intervals, &assignments, regs); asm.resolve_ssa(&intervals, &assignments); let insns = &asm.basic_blocks[b1.0].insns; diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 0f4acf1cef3400..702046058065fd 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -350,14 +350,14 @@ impl Assembler { /// If a given operand is Opnd::Mem and it uses MemBase::Stack, lower it to MemBase::Reg(NATIVE_BASE_PTR). /// In general, `out` operand is a `VReg`, so it may use MemBase::Stack, but not MemBase::StackIndirect. - fn lower_stack_membase(opnd: Opnd, stack_state: &StackState) -> Opnd { + fn lower_stack_membase(asm: &Assembler, opnd: Opnd) -> Opnd { match opnd { Opnd::Mem(Mem { base: stack_membase @ MemBase::Stack { .. }, disp: opnd_disp, num_bits: opnd_num_bits }) => { // Convert MemBase::Stack to MemBase::Reg(NATIVE_BASE_PTR) with the // correct stack displacement. The stack slot value lives directly at // [NATIVE_BASE_PTR + stack_disp], so we just adjust the base and // combine displacements -- no indirection needed. - let Mem { base, disp: stack_disp, .. } = stack_state.stack_membase_to_mem(stack_membase); + let Mem { base, disp: stack_disp, .. } = asm.stack_state.stack_membase_to_mem(stack_membase); Opnd::Mem(Mem { base, disp: stack_disp + opnd_disp, num_bits: opnd_num_bits }) } Opnd::Mem(Mem { base: MemBase::StackIndirect { .. }, .. }) => { @@ -369,13 +369,13 @@ impl Assembler { /// If a given operand is Opnd::Mem and it uses MemBase::Stack, lower it to MemBase::Reg(NATIVE_BASE_PTR). /// For MemBase::StackIndirect, load the pointer from the stack slot into a scratch register. - fn split_stack_membase(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd, stack_state: &StackState) -> Opnd { + fn split_stack_membase(asm: &mut Assembler, opnd: Opnd, scratch_opnd: Opnd) -> Opnd { match opnd { - Opnd::Mem(Mem { base: MemBase::Stack { .. }, .. }) => lower_stack_membase(opnd, stack_state), + Opnd::Mem(Mem { base: MemBase::Stack { .. }, .. }) => lower_stack_membase(asm, opnd), Opnd::Mem(Mem { base: MemBase::StackIndirect { stack_idx }, disp: opnd_disp, num_bits: opnd_num_bits }) => { // The spilled value (a pointer) lives at a stack slot. Load it // into a scratch register, then use the register as the base. - let stack_mem = stack_state.stack_membase_to_mem(MemBase::Stack { stack_idx, num_bits: 64 }); + let stack_mem = asm.stack_state.stack_membase_to_mem(MemBase::Stack { stack_idx, num_bits: 64 }); asm.load_into(scratch_opnd, Opnd::Mem(stack_mem)); Opnd::Mem(Mem { base: MemBase::Reg(scratch_opnd.unwrap_reg().reg_no), @@ -434,14 +434,8 @@ impl Assembler { } } - // Prepare StackState to lower MemBase::Stack - let stack_state = StackState::new(self.stack_base_idx); - - let mut asm_local = Assembler::new(); + let mut asm_local = Assembler::new_with_asm_without_blocks(&self); asm_local.accept_scratch_reg = true; - asm_local.stack_base_idx = self.stack_base_idx; - asm_local.label_names = self.label_names.clone(); - asm_local.num_vregs = self.num_vregs; // Create one giant block to linearize everything into asm_local.new_block_without_id("linearized"); @@ -468,16 +462,16 @@ impl Assembler { // since we'll overwrite out when moving left into it. // Compare before lowering (Stack membases change during lowering). let right_eq_out = out == right; - *out = lower_stack_membase(*out, &stack_state); + *out = lower_stack_membase(asm, *out); if right_eq_out { asm.mov(SCRATCH1_OPND, *out); *right = SCRATCH1_OPND; } // Phase 2: Lower stack memory bases - *left = split_stack_membase(asm, *left, SCRATCH0_OPND, &stack_state); + *left = split_stack_membase(asm, *left, SCRATCH0_OPND); if !right_eq_out { - *right = split_stack_membase(asm, *right, SCRATCH1_OPND, &stack_state); + *right = split_stack_membase(asm, *right, SCRATCH1_OPND); } // Phase 3: If right is a Mem whose base register equals the out @@ -512,11 +506,11 @@ impl Assembler { Insn::Mul { left, right, out } => { assert_out_is_phys_reg_or_stack_mem(*out); - *left = split_stack_membase(asm, *left, SCRATCH0_OPND, &stack_state); + *left = split_stack_membase(asm, *left, SCRATCH0_OPND); *left = split_if_both_memory(asm, *left, *right, SCRATCH0_OPND); - *right = split_stack_membase(asm, *right, SCRATCH1_OPND, &stack_state); + *right = split_stack_membase(asm, *right, SCRATCH1_OPND); *right = split_64bit_immediate(asm, *right, SCRATCH1_OPND); - *out = lower_stack_membase(*out, &stack_state); + *out = lower_stack_membase(asm, *out); // imul doesn't have (Mem, Reg) encoding. Swap left and right in that case. if let (Opnd::Mem(_), Opnd::Reg(_)) = (&left, &right) { @@ -536,8 +530,8 @@ impl Assembler { } Insn::Test { left, right } | Insn::Cmp { left, right } => { - *left = split_stack_membase(asm, *left, SCRATCH1_OPND, &stack_state); - *right = split_stack_membase(asm, *right, SCRATCH0_OPND, &stack_state); + *left = split_stack_membase(asm, *left, SCRATCH1_OPND); + *right = split_stack_membase(asm, *right, SCRATCH0_OPND); *right = split_if_both_memory(asm, *right, *left, SCRATCH0_OPND); let num_bits = match right { @@ -574,10 +568,10 @@ impl Assembler { Insn::CSelLE { truthy: left, falsy: right, out } | Insn::CSelG { truthy: left, falsy: right, out } | Insn::CSelGE { truthy: left, falsy: right, out } => { - *left = split_stack_membase(asm, *left, SCRATCH1_OPND, &stack_state); - *right = split_stack_membase(asm, *right, SCRATCH0_OPND, &stack_state); + *left = split_stack_membase(asm, *left, SCRATCH1_OPND); + *right = split_stack_membase(asm, *right, SCRATCH0_OPND); *right = split_if_both_memory(asm, *right, *left, SCRATCH0_OPND); - *out = lower_stack_membase(*out, &stack_state); + *out = lower_stack_membase(asm, *out); let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); if let Some(mem_out) = mem_out { @@ -585,8 +579,8 @@ impl Assembler { } } Insn::Lea { opnd, out } => { - *opnd = split_stack_membase(asm, *opnd, SCRATCH0_OPND, &stack_state); - *out = lower_stack_membase(*out, &stack_state); + *opnd = split_stack_membase(asm, *opnd, SCRATCH0_OPND); + *out = lower_stack_membase(asm, *out); let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); if let Some(mem_out) = mem_out { @@ -601,9 +595,9 @@ impl Assembler { } Insn::Load { out, opnd } | Insn::LoadInto { dest: out, opnd } => { - *opnd = split_stack_membase(asm, *opnd, SCRATCH0_OPND, &stack_state); + *opnd = split_stack_membase(asm, *opnd, SCRATCH0_OPND); // Split stack membase on out before checking for memory write - *out = lower_stack_membase(*out, &stack_state); + *out = lower_stack_membase(asm, *out); let mem_out = split_memory_write(out, SCRATCH0_OPND); asm.push_insn(insn); if let Some(mem_out) = mem_out { @@ -618,15 +612,15 @@ impl Assembler { asm.incr_counter(Opnd::mem(64, SCRATCH0_OPND, 0), value); } &mut Insn::Mov { dest, src } => { - let dest = split_stack_membase(asm, dest, SCRATCH1_OPND, &stack_state); - let src = split_stack_membase(asm, src, SCRATCH0_OPND, &stack_state); + let dest = split_stack_membase(asm, dest, SCRATCH1_OPND); + let src = split_stack_membase(asm, src, SCRATCH0_OPND); asm_mov(asm, dest, src, SCRATCH0_OPND); } // Handle various operand combinations for spills on compile_exits. &mut Insn::Store { dest, src } => { let num_bits = dest.rm_num_bits(); - let src = split_stack_membase(asm, src, SCRATCH0_OPND, &stack_state); - let dest = split_stack_membase(asm, dest, SCRATCH1_OPND, &stack_state); + let src = split_stack_membase(asm, src, SCRATCH0_OPND); + let dest = split_stack_membase(asm, dest, SCRATCH1_OPND); let src = match src { Opnd::Reg(_) => src, @@ -1156,8 +1150,9 @@ impl Assembler { let preferred_registers = trace_compile_phase("preferred_registers", || asm.preferred_register_assignments(&intervals)); let (assignments, num_stack_slots) = trace_compile_phase("linear_scan", || asm.linear_scan(intervals.clone(), regs.len(), &preferred_registers)); - let total_stack_slots = asm.stack_base_idx + num_stack_slots; - if total_stack_slots > Self::MAX_FRAME_STACK_SLOTS { + asm.stack_state.num_spill_slots = num_stack_slots; + let stack_slot_count = asm.stack_state.stack_slot_count(); + if stack_slot_count > Self::MAX_FRAME_STACK_SLOTS { return Err(CompileError::NativeStackTooLarge); } @@ -1181,21 +1176,20 @@ impl Assembler { } } - // Update FrameSetup slot_count to account for: - // 1) stack slots reserved for block params (stack_base_idx), and - // 2) register allocator spills (num_stack_slots). + // Update FrameSetup slot_count now that StackState knows the + // register allocator spill count. trace_compile_phase("count_stack_slots", || { for block in asm.basic_blocks.iter_mut() { for insn in block.insns.iter_mut() { if let Insn::FrameSetup { slot_count, .. } = insn { - *slot_count = total_stack_slots; + *slot_count = stack_slot_count; } } } }); trace_compile_phase("resolve_ssa", || { - asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS, total_stack_slots); + asm.handle_caller_saved_regs(&intervals, &assignments, &C_ARG_REGREGS); asm.resolve_ssa(&intervals, &assignments); }); @@ -1384,7 +1378,7 @@ mod tests { let mut asm = Assembler::new(); asm.new_block_without_id("test"); - asm.stack_base_idx = 1; + asm.stack_state.stack_base_idx = 1; let label = asm.new_label("bb0"); asm.write_label(label.clone()); @@ -2123,7 +2117,7 @@ mod tests { #[test] fn frame_setup_teardown_stack_base_idx() { let (mut asm, mut cb) = setup_asm(); - asm.stack_base_idx = 5; + asm.stack_state.stack_base_idx = 5; asm.frame_setup(&[]); asm.frame_teardown(&[]); asm.compile_with_num_regs(&mut cb, 0); diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index bb21c3dda2ae5c..7b30e23669e21b 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -3710,31 +3710,7 @@ impl Assembler { /// Allocate stack space on top of the stack slots reserved for JITFrame, /// and return a pointer to the allocated space. fn alloc_stack(&mut self, jit: &JITState, stack_size: usize) -> Opnd { - let total_stack_size = jit.jit_frame_size + stack_size; - self.stack_base_idx = self.stack_base_idx.max(total_stack_size); - // high addr - // +------------------------+ - // | return address | - // +------------------------+ - // | previous frame pointer | <- NATIVE_BASE_PTR (== depth-0 cfp->jit_return) - // +------------------------+ - // | JITFrame slot depth 0 | <- [NATIVE_BASE_PTR - 8]; read by CFP_ZJIT_FRAME for the top-level frame - // +------------------------+ - // | ... | one slot per inlining depth (jit.jit_frame_size slots total) - // +------------------------+ - // | JITFrame slot depth N | <- innermost inlined frame's slot - // +------------------------+ - // | opnds.last() | - // +------------------------+ - // | ... | - // +------------------------+ - // | opnds.first() | <- pointer returned by alloc_stack() - // +------------------------+ - // | register spill slots | if any - // +------------------------+ - // | FrameSetup align slot | if needed - // +------------------------+ - // low addr + let total_stack_size = self.stack_state.reserve_stack_slots(jit.jit_frame_size, stack_size); self.sub(NATIVE_BASE_PTR, (SIZEOF_VALUE * total_stack_size).into()) } From 7976aee9d572319d4c8654f890758f88cde7f1cb Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Wed, 17 Jun 2026 18:34:39 -0500 Subject: [PATCH 4/7] [DOC] Timestamps doc --- doc/file/timestamps.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/doc/file/timestamps.md b/doc/file/timestamps.md index 60934edb05e678..cd8b064e837571 100644 --- a/doc/file/timestamps.md +++ b/doc/file/timestamps.md @@ -1,15 +1,13 @@ -# \File System Timestamps +# \Filesystem Timestamps -A file system entry (the name of a file or directory) +A filesystem entry (the name of a file or directory) has several times (called timestamps) associated with it. -The Ruby methods that return these timestamps (each as a Time object) -are actually returning "whatever the OS says," -and so their behaviors may vary among OS platforms. -If a platform does not support a particular timestamp, -the corresponding Ruby methods raise NotImplementedError. +A Ruby method that returns a filesystem timestamp (as a Time object) +is actually returning "whatever the filesystem says"; +the returned times may vary among filesystems, even on the same machine. -These timestamps are: +These timestamps methods are: | Name | Meaning | Changes | |:--------------------------------:|----------------------------------------|-----------------------| @@ -18,6 +16,9 @@ These timestamps are: | [`atime`](#access-time) | Access time. | When read or written. | | [`ctime`](#metadata-change-time) | Metadata-change time (or create time). | See below. | +A method raises an exception if the filesystem does not support +the corresponding timestamp. + ## Birth \Time The birth time for an entry is the time the entry was created. @@ -42,7 +43,7 @@ On Windows, each of these methods also returns the birth time: The modification time for an entry is the time the entry was last modified. The modification time is updated when the entry is written, -though some file systems may delay the update. +though some filesystems may delay the update. Each of these methods returns the modification time for an entry as a Time object: @@ -60,9 +61,11 @@ The modification time (along with the access time) may also be updated explicitl ## Access \Time -The access time for an entry is the time the entry last read. -The access time is updated when the entry is read, -though some file systems may delay the update. +The access time for an entry is the time of the most recent read of or write to +the content of the entry, as reported by the underlying filesystem. + +Depending on a filesystem's settings, reading an entry may cause the access time +to be updated immediately, later, or never. Each of these methods returns the access time for an entry as a Time object: @@ -83,7 +86,7 @@ The access time (along with the modification time) may also be updated explicitl The metadata-change time for an entry is the time the entry last read. The metadata-change time is updated when the entry's metadata is changed; changing access mode or permissions may update the metadata-change time, -though some file systems may delay the update. +though some filesystems may delay the update. On non-Windows systems, each of these methods returns the metadata-change time for an entry: From 5e742e2c453c131b5ee1d7fc3a8c581e233bd215 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Wed, 17 Jun 2026 18:41:41 -0500 Subject: [PATCH 5/7] [DOC] Update Pathname.atime (#17366) --- pathname_builtin.rb | 67 ++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 8fdf871201cc61..db4fc3a104c978 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1072,48 +1072,39 @@ def write(...) File.write(@path, ...) end # with ASCII-8BIT encoding. def binwrite(...) File.binwrite(@path, ...) end + # :markup: markdown + # # call-seq: # atime -> new_time # - # Returns a new Time object containing the time of the most recent - # access (read or write) to the entry represented by `self`; - # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: - # - # # Work in a temporary directory. - # require 'tmpdir' - # Dir.mktmpdir do |tmpdirpath| - # # A subdirectory therein, and its Pathname. - # dirpath = File.join(tmpdirpath, 'subdir') - # Dir.mkdir(dirpath) - # dir_pn = Pathname(dirpath) - # puts "Create directory; establishes atime for directory." - # puts " Directory atime: #{dir_pn.atime}" - # sleep(1) - # - # # A file in the subdirectory, and its Pathname. - # filepath = File.join(dirpath, 't.txt') - # puts "Create file; establishes atime for file, updates atime for directory." - # File.write(filepath, 'foo') - # file_pn = Pathname(filepath) - # puts " File atime: #{file_pn.atime}" - # puts " Directory atime: #{dir_pn.atime}" - # sleep(1) - # puts "Write file; updates atimes for file and directory." - # File.write(filepath, 'bar') - # puts " File atime: #{file_pn.atime}" - # puts " Directory atime: #{dir_pn.atime}" - # end - # - # Output: + # Returns a Time object containing the access time + # of the entry represented by `self`, as reported by the filesystem; + # see {File System Access Time}[rdoc-ref:file/timestamps.md@Access+Time]: # - # Create directory; establishes atime for directory. - # Directory atime: 2026-05-14 14:36:43 +0100 - # Create file; establishes atime for file, updates atime for directory. - # File atime: 2026-05-14 14:36:44 +0100 - # Directory atime: 2026-05-14 14:36:44 +0100 - # Write file; updates atimes for file and directory. - # File atime: 2026-05-14 14:36:45 +0100 - # Directory atime: 2026-05-14 14:36:45 +0100 + # ```ruby + # # Pathname for a (non-existent) directory. + # dir_pn = Pathname('doc/foo') # => # + # # Create directory; establishes atime for directory. + # dir_pn.mkdir + # dir_pn.atime # => 2026-06-17 10:10:20.801115774 -0500 + # # Pathname for a (non-existent) file in the directory. + # file_pn = dir_pn.join('t.tmp') # => # + # # Create file; establishes atime for file, updates atime for directory. + # file_pn.write('foo') + # file_pn.atime # => 2026-06-17 10:11:40.987171568 -0500 + # dir_pn.atime # => 2026-06-17 10:11:40.96617277 -0500 + # # Write file; updates atime for file,but not directory. + # file_pn.write('bar') + # file_pn.atime # => 2026-06-17 10:13:22.062904563 -0500 + # dir_pn.atime # => 2026-06-17 10:11:40.96617277 -0500 + # # Read file; may update atime for file, but not directory. + # file_pn.read + # file_pn.atime # => 2026-06-17 10:13:22.062904563 -0500 + # dir_pn.atime # => 2026-06-17 10:11:40.96617277 -0500 + # # Clean up. + # file_pn.delete + # dir_pn.rmdir + # ``` # def atime() File.atime(@path) end From c01ef466ff5bfd096898521ea98d57858f287a81 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 17 Jun 2026 11:06:00 -0500 Subject: [PATCH 6/7] [DOC] Update Pathname.chmod --- pathname_builtin.rb | 71 ++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index db4fc3a104c978..507f2f35fcf11a 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1304,47 +1304,38 @@ def lchmod(mode) File.lchmod(mode, @path) end # Changes the owner and group of an entry (directory or file): # # ```ruby - # # Work in a temporary directory. - # Pathname.mktmpdir do |tmpdirpath| - # # A subdirectory therein, and its Pathname. - # dirpath = File.join(tmpdirpath, 'subdir') - # dir_pn = Pathname(dirpath) - # dir_pn.mkdir - # dir_stat = File.stat(dirpath) - # puts "Original directory owner: #{dir_stat.uid}" - # puts "Original directory group: #{dir_stat.gid}" - # dir_pn.chown(1000, 1000) - # dir_stat = File.stat(dirpath) - # puts "New directory owner: #{dir_stat.uid}" - # puts "New directory group: #{dir_stat.gid}" - # - # # A file in the subdirectory, and its Pathname. - # filepath = File.join(dirpath, 't.txt') - # file_pn = Pathname(filepath) - # # Create the file. - # file_pn.write('foo') - # file_stat = File.stat(filepath) - # puts "Original file owner: #{file_stat.uid}" - # puts "Original file group: #{file_stat.gid}" - # file_pn = Pathname(dirpath) - # file_pn.chown(1000, 1000) - # file_stat = File.stat(dirpath) - # puts "New file owner: #{file_stat.uid}" - # puts "New file group: #{file_stat.gid}" - # end - # ``` - # - # Output: + # # Super user; all privileges. + # Process.uid # => 0 + # Process.gid # => 0 # - # ```text - # Original directory owner: 0 - # Original directory group: 0 - # New directory owner: 1000 - # New directory group: 1000 - # Original file owner: 0 - # Original file group: 0 - # New file owner: 1000 - # New file group: 1000 + # # Pathname for a (non-existent) directory. + # dir_pn = Pathname('doc/foo') # => # + # # Create the directory; fetch original owner and group. + # dir_pn.mkdir + # dir_stat = dir_pn.stat + # dir_stat.uid # => 0 + # dir_stat.gid # => 0 + # # Change owner; fetch current owner and group. + # dir_pn.chown(1000, 1000) + # dir_stat = dir_pn.stat + # dir_stat.uid # => 1000 + # dir_stat.gid # => 1000 + # + # Pathname for a (non-existent) file in the directory. + # file_pn = dir_pn.join('t.tmp') # => # + # # Create the directory; fetch original owner and group. + # file_pn.write('foo') + # file_stat = file_pn.stat + # file_stat.uid # => 0 + # file_stat.gid # => 0 + # # Change owner; fetch current owner and group. + # file_pn.chown(1000, 1000) + # file_stat = file_pn.stat + # file_stat.uid # => 1000 + # file_stat.gid # => 1000 + # # Clean up. + # file_pn.delete + # dir_pn.rmdir # ``` # # Notes: From d8014cd596c3f67dbd619bba57bf2a8847dcce08 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 17 Jun 2026 15:03:38 -0500 Subject: [PATCH 7/7] [DOC] Update Pathname.chmod --- pathname_builtin.rb | 55 +++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 507f2f35fcf11a..a8a392d9eaa971 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1229,45 +1229,30 @@ def mtime() File.mtime(@path) end # chmod(mode) -> 1 # # Changes the mode (i.e., permissions) of the entry represented by `self`; - # see {File Permissions}[rdoc-ref:File@File+Permissions]; - # returns `1`: + # see {File Permissions}[rdoc-ref:File@File+Permissions]: # # ```ruby - # # A helper method to make an integer mode display as octal. - # def pretty(mode); '0' + (mode & 0777).to_s(8); end - # - # # Work in a temporary directory. - # Pathname.mktmpdir do |tmpdirpath| - # # A subdirectory therein, and its Pathname. - # dirpath = File.join(tmpdirpath, 'subdir') - # dir_pn = Pathname(dirpath) - # dir_pn.mkdir - # # The directory mode. - # puts "Original directory mode: #{pretty(dir_pn.stat.mode)}" - # # Change the directory mode. - # dir_pn.chmod(0777) - # puts "New directory mode: #{pretty(dir_pn.stat.mode)}" - # - # # A file in the subdirectory, and its Pathname. - # filepath = File.join(dirpath, 't.txt') - # file_pn = Pathname(filepath) - # # Create the file. - # file_pn.write('foo') - # # The file mode. - # puts "Original file mode: #{pretty(file_pn.stat.mode)}" - # # Change the file modes. - # file_pn.chmod(0777) - # puts "New file mode: #{pretty(file_pn.stat.mode)}" - # end - # ``` + # # Pathname for a (non-existent) directory. + # dir_pn = Pathname('doc/foo') # => # + # # Create the directory and fetch its mode. + # dir_pn.mkdir + # dir_pn.stat.mode.to_s(8) # => "40775" + # # Change the directory mode and fetch the new mode. + # dir_pn.chmod(0777) + # dir_pn.stat.mode.to_s(8) # => "40777" # - # Output: + # # Pathname for a (non-existent) file in the directory. + # file_pn = dir_pn.join('t.tmp') # => # + # # Create the file and fetch its mode. + # file_pn.write('foo') + # file_pn.stat.mode.to_s(8) # => "100664" + # # Change the file mode and fetch its new mode. + # file_pn.chmod(0777) + # file_pn.stat.mode.to_s(8) # => "100777" # - # ```text - # Original directory mode: 0775 - # New directory mode: 0777 - # Original file mode: 0664 - # New file mode: 0777 + # # Clean up. + # file_pn.delete + # dir_pn.rmdir # ``` # def chmod(mode) File.chmod(mode, @path) end