diff options
Diffstat (limited to 'yjit/src/core.rs')
| -rw-r--r-- | yjit/src/core.rs | 133 |
1 files changed, 74 insertions, 59 deletions
diff --git a/yjit/src/core.rs b/yjit/src/core.rs index d42726bcc7..d08cd1fb26 100644 --- a/yjit/src/core.rs +++ b/yjit/src/core.rs @@ -34,7 +34,7 @@ use crate::invariants::*; pub const MAX_CTX_TEMPS: usize = 8; // Maximum number of local variable types or registers we keep track of -const MAX_CTX_LOCALS: usize = 8; +pub const MAX_CTX_LOCALS: usize = 8; /// An index into `ISEQ_BODY(iseq)->iseq_encoded`. Points /// to a YARV instruction or an instruction operand. @@ -1099,7 +1099,7 @@ impl Context { MapToLocal(local_idx) => { bits.push_op(CtxOp::MapTempLocal); bits.push_u3(stack_idx as u8); - bits.push_u3(local_idx as u8); + bits.push_u3(local_idx); } MapToSelf => { @@ -1769,6 +1769,35 @@ impl IseqPayload { // Turn it into an iterator that owns the blocks and return version_map.into_iter().flatten() } + + /// Get or create all blocks for a particular place in an iseq. + fn get_or_create_version_list(&mut self, insn_idx: usize) -> &mut VersionList { + if insn_idx >= self.version_map.len() { + self.version_map.resize(insn_idx + 1, VersionList::default()); + } + + return self.version_map.get_mut(insn_idx).unwrap(); + } + + // We cannot deallocate blocks immediately after invalidation since patching + // the code for setting up return addresses does not affect outstanding return + // addresses that are on stack and will use invalidated branch pointers when + // hit. Example: + // def foo(n) + // if n == 2 + // # 1.times.each to create a cfunc frame to preserve the JIT frame + // # which will return to a stub housed in an invalidated block + // return 1.times.each { Object.define_method(:foo) {} } + // end + // + // foo(n + 1) # The block for this call houses the return branch stub + // end + // p foo(1) + pub fn delayed_deallocation(&mut self, blockref: BlockRef) { + block_assumptions_free(blockref); + unsafe { blockref.as_ref() }.set_iseq_null(); + self.dead_blocks.push(blockref); + } } /// Get the payload for an iseq. For safety it's up to the caller to ensure the returned `&mut` @@ -1814,22 +1843,24 @@ pub fn get_or_create_iseq_payload(iseq: IseqPtr) -> &'static mut IseqPayload { pub fn for_each_iseq<F: FnMut(IseqPtr)>(mut callback: F) { unsafe extern "C" fn callback_wrapper(iseq: IseqPtr, data: *mut c_void) { // SAFETY: points to the local below - let callback: &mut &mut dyn FnMut(IseqPtr) -> bool = unsafe { std::mem::transmute(&mut *data) }; - callback(iseq); + let callback: *mut *mut dyn FnMut(IseqPtr) -> bool = data.cast(); + unsafe { (**callback)(iseq) }; } - let mut data: &mut dyn FnMut(IseqPtr) = &mut callback; - unsafe { rb_yjit_for_each_iseq(Some(callback_wrapper), (&mut data) as *mut _ as *mut c_void) }; + let mut data: *mut dyn FnMut(IseqPtr) = &mut callback; + let data: *mut *mut dyn FnMut(IseqPtr) = &mut data; + unsafe { rb_jit_for_each_iseq(Some(callback_wrapper), data.cast()) }; } /// Iterate over all on-stack ISEQs pub fn for_each_on_stack_iseq<F: FnMut(IseqPtr)>(mut callback: F) { unsafe extern "C" fn callback_wrapper(iseq: IseqPtr, data: *mut c_void) { // SAFETY: points to the local below - let callback: &mut &mut dyn FnMut(IseqPtr) -> bool = unsafe { std::mem::transmute(&mut *data) }; - callback(iseq); + let callback: *mut *mut dyn FnMut(IseqPtr) -> bool = data.cast(); + unsafe { (**callback)(iseq) }; } - let mut data: &mut dyn FnMut(IseqPtr) = &mut callback; - unsafe { rb_jit_cont_each_iseq(Some(callback_wrapper), (&mut data) as *mut _ as *mut c_void) }; + let mut data: *mut dyn FnMut(IseqPtr) = &mut callback; + let data: *mut *mut dyn FnMut(IseqPtr) = &mut data; + unsafe { rb_jit_cont_each_iseq(Some(callback_wrapper), data.cast()) }; } /// Iterate over all on-stack ISEQ payloads @@ -2140,21 +2171,6 @@ fn get_version_list(blockid: BlockId) -> Option<&'static mut VersionList> { } } -/// Get or create all blocks for a particular place in an iseq. -fn get_or_create_version_list(blockid: BlockId) -> &'static mut VersionList { - let payload = get_or_create_iseq_payload(blockid.iseq); - let insn_idx = blockid.idx.as_usize(); - - // Expand the version map as necessary - if insn_idx >= payload.version_map.len() { - payload - .version_map - .resize(insn_idx + 1, VersionList::default()); - } - - return payload.version_map.get_mut(insn_idx).unwrap(); -} - /// Take all of the blocks for a particular place in an iseq pub fn take_version_list(blockid: BlockId) -> VersionList { let insn_idx = blockid.idx.as_usize(); @@ -2343,15 +2359,21 @@ unsafe fn add_block_version(blockref: BlockRef, cb: &CodeBlock) { // Function entry blocks must have stack size 0 debug_assert!(!(block.iseq_range.start == 0 && Context::decode(block.ctx).stack_size > 0)); - let version_list = get_or_create_version_list(block.get_blockid()); + // Use a single payload reference for both version_map and pages access + // to avoid mutable aliasing UB from multiple get_iseq_payload calls. + let iseq_payload = get_or_create_iseq_payload(block.iseq.get()); + let insn_idx = block.get_blockid().idx.as_usize(); - // If this the first block being compiled with this block id - if version_list.len() == 0 { - incr_counter!(compiled_blockid_count); - } + { + let version_list = iseq_payload.get_or_create_version_list(insn_idx); - version_list.push(blockref); - version_list.shrink_to_fit(); + if version_list.is_empty() { + incr_counter!(compiled_blockid_count); + } + + version_list.push(blockref); + version_list.shrink_to_fit(); + } // By writing the new block to the iseq, the iseq now // contains new references to Ruby objects. Run write barriers. @@ -2376,7 +2398,6 @@ unsafe fn add_block_version(blockref: BlockRef, cb: &CodeBlock) { } // Mark code pages for code GC - let iseq_payload = get_iseq_payload(block.iseq.get()).unwrap(); for page in cb.addrs_to_pages(block.start_addr, block.end_addr.get()) { iseq_payload.pages.insert(page); } @@ -2419,7 +2440,9 @@ impl<'a> JITState<'a> { // Pending branches => actual branches outgoing: MutableBranchList(Cell::new(self.pending_outgoing.into_iter().map(|pending_out| { let pending_out = Rc::try_unwrap(pending_out) - .ok().expect("all PendingBranchRefs should be unique when ready to construct a Block"); + .unwrap_or_else(|rc| panic!( + "PendingBranchRef should be unique when ready to construct a Block. \ + strong={} weak={}", Rc::strong_count(&rc), Rc::weak_count(&rc))); pending_out.into_branch(NonNull::new(blockref as *mut Block).expect("no null from Box")) }).collect())) }); @@ -2427,7 +2450,7 @@ impl<'a> JITState<'a> { // SAFETY: allocated with Box above unsafe { ptr::write(blockref, block) }; - // Block is initialized now. Note that MaybeUnint<T> has the same layout as T. + // Block is initialized now. Note that MaybeUninit<T> has the same layout as T. let blockref = NonNull::new(blockref as *mut Block).expect("no null from Box"); // Track all the assumptions the block makes as invariants @@ -2493,6 +2516,11 @@ impl Block { self.incoming.push(branch); } + /// Mark this block as dead by setting its iseq to null. + pub fn set_iseq_null(&self) { + self.iseq.replace(ptr::null()); + } + // Compute the size of the block code pub fn code_size(&self) -> usize { (self.end_addr.get().as_offset() - self.start_addr.as_offset()).try_into().unwrap() @@ -3591,6 +3619,13 @@ fn branch_stub_hit_body(branch_ptr: *const c_void, target_idx: u32, ec: EcPtr) - return CodegenGlobals::get_stub_exit_code().raw_ptr(cb); } + // Bail if this branch is housed in an invalidated (dead) block. + // This only happens in rare invalidation scenarios and we need + // to avoid linking a dead block to a live block with a branch. + if branch.block.get().as_ref().iseq.get().is_null() { + return CodegenGlobals::get_stub_exit_code().raw_ptr(cb); + } + (cfp, original_interp_sp) }; @@ -3790,7 +3825,7 @@ pub fn gen_branch_stub_hit_trampoline(ocb: &mut OutlinedCb) -> Option<CodePtr> { let mut asm = Assembler::new_without_iseq(); // For `branch_stub_hit(branch_ptr, target_idx, ec)`, - // `branch_ptr` and `target_idx` is different for each stub, + // `branch_ptr` and `target_idx` are different for each stub, // but the call and what's after is the same. This trampoline // is the unchanging part. // Since this trampoline is static, it allows code GC inside @@ -4289,7 +4324,9 @@ pub fn invalidate_block_version(blockref: &BlockRef) { // in this function before we removed it, so it's well connected. unsafe { remove_from_graph(*blockref) }; - delayed_deallocation(*blockref); + if let Some(payload) = get_iseq_payload(id_being_invalidated.iseq) { + payload.delayed_deallocation(*blockref); + } ocb.unwrap().mark_all_executable(); cb.mark_all_executable(); @@ -4297,28 +4334,6 @@ pub fn invalidate_block_version(blockref: &BlockRef) { incr_counter!(invalidation_count); } -// We cannot deallocate blocks immediately after invalidation since there -// could be stubs waiting to access branch pointers. Return stubs can do -// this since patching the code for setting up return addresses does not -// affect old return addresses that are already set up to use potentially -// invalidated branch pointers. Example: -// def foo(n) -// if n == 2 -// # 1.times.each to create a cfunc frame to preserve the JIT frame -// # which will return to a stub housed in an invalidated block -// return 1.times.each { Object.define_method(:foo) {} } -// end -// -// foo(n + 1) -// end -// p foo(1) -pub fn delayed_deallocation(blockref: BlockRef) { - block_assumptions_free(blockref); - - let payload = get_iseq_payload(unsafe { blockref.as_ref() }.iseq.get()).unwrap(); - payload.dead_blocks.push(blockref); -} - trait RefUnchecked { type Contained; unsafe fn ref_unchecked(&self) -> &Self::Contained; |
