Skip to content

Commit 25ca647

Browse files
authored
ZJIT: Hook into GC W^X callbacks for compaction (ruby#16635)
1 parent c341399 commit 25ca647

5 files changed

Lines changed: 52 additions & 6 deletions

File tree

gc.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2475,6 +2475,9 @@ rb_gc_before_updating_jit_code(void)
24752475
#if USE_YJIT
24762476
rb_yjit_mark_all_writeable();
24772477
#endif
2478+
#if USE_ZJIT
2479+
rb_zjit_mark_all_writable();
2480+
#endif
24782481
}
24792482

24802483
/*
@@ -2488,6 +2491,9 @@ rb_gc_after_updating_jit_code(void)
24882491
#if USE_YJIT
24892492
rb_yjit_mark_all_executable();
24902493
#endif
2494+
#if USE_ZJIT
2495+
rb_zjit_mark_all_executable();
2496+
#endif
24912497
}
24922498

24932499
static void

zjit.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ void rb_zjit_invalidate_no_ep_escape(const rb_iseq_t *iseq);
4040
void rb_zjit_constant_state_changed(ID id);
4141
void rb_zjit_iseq_mark(void *payload);
4242
void rb_zjit_iseq_update_references(void *payload);
43+
void rb_zjit_mark_all_writable(void);
44+
void rb_zjit_mark_all_executable(void);
4345
void rb_zjit_iseq_free(const rb_iseq_t *iseq);
4446
void rb_zjit_before_ractor_spawn(void);
4547
void rb_zjit_tracing_invalidate_all(void);

zjit/src/asm/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ impl CodeBlock {
294294
}
295295

296296
/// Make all the code in the region executable. Call this at the end of a write session.
297+
pub fn mark_all_writable(&mut self) {
298+
self.mem_block.borrow_mut().mark_all_writable();
299+
}
300+
297301
pub fn mark_all_executable(&mut self) {
298302
self.mem_block.borrow_mut().mark_all_executable();
299303
}

zjit/src/gc.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ fn iseq_version_update_references(mut version: IseqVersionRef) {
158158
}
159159
}
160160

161-
// Move objects baked in JIT code
161+
// Move objects baked in JIT code.
162+
// The code region is already writable because rb_zjit_mark_all_writable() was called
163+
// before the GC update_references phase. We write directly to avoid per-page mprotect calls.
162164
let cb = ZJITState::get_code_block();
163165
for &offset in unsafe { version.as_ref() }.gc_offsets.iter() {
164166
let value_ptr: *const u8 = offset.raw_ptr(cb);
@@ -170,13 +172,10 @@ fn iseq_version_update_references(mut version: IseqVersionRef) {
170172

171173
// Only write when the VALUE moves, to be copy-on-write friendly.
172174
if new_addr != object {
173-
for (byte_idx, &byte) in new_addr.as_u64().to_le_bytes().iter().enumerate() {
174-
let byte_code_ptr = offset.add_bytes(byte_idx);
175-
cb.write_mem(byte_code_ptr, byte).expect("patching existing code should be within bounds");
176-
}
175+
let value_ptr = value_ptr as *mut VALUE;
176+
unsafe { value_ptr.write_unaligned(new_addr) };
177177
}
178178
}
179-
cb.mark_all_executable();
180179
}
181180

182181
/// Append a set of gc_offsets to the iseq's payload
@@ -211,6 +210,25 @@ fn ranges_overlap<T>(left: &Range<T>, right: &Range<T>) -> bool where T: Partial
211210
left.start < right.end && right.start < left.end
212211
}
213212

213+
/// GC callback for making all JIT code writable before updating references in bulk.
214+
/// This avoids toggling W^X permissions per-page during GC compaction.
215+
#[unsafe(no_mangle)]
216+
pub extern "C" fn rb_zjit_mark_all_writable() {
217+
if !ZJITState::has_instance() {
218+
return;
219+
}
220+
ZJITState::get_code_block().mark_all_writable();
221+
}
222+
223+
/// GC callback for making all JIT code executable after updating references in bulk.
224+
#[unsafe(no_mangle)]
225+
pub extern "C" fn rb_zjit_mark_all_executable() {
226+
if !ZJITState::has_instance() {
227+
return;
228+
}
229+
ZJITState::get_code_block().mark_all_executable();
230+
}
231+
214232
/// Callback for marking GC objects inside [crate::invariants::Invariants].
215233
#[unsafe(no_mangle)]
216234
pub extern "C" fn rb_zjit_root_mark() {

zjit/src/virtualmem.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,22 @@ impl<A: Allocator> VirtualMemory<A> {
265265
memory_usage_bytes + self.page_size_bytes < memory_limit_bytes
266266
}
267267

268+
/// Make all the code in the region writable. Call this before bulk writes (e.g. GC
269+
/// reference updates). See [Self] for usual usage flow.
270+
pub fn mark_all_writable(&mut self) {
271+
self.current_write_page = None;
272+
273+
let region_start = self.region_start;
274+
let mapped_region_bytes: u32 = self.mapped_region_bytes.try_into().unwrap();
275+
276+
// Make mapped region writable
277+
if mapped_region_bytes > 0 {
278+
if !self.allocator.mark_writable(region_start.as_ptr(), mapped_region_bytes) {
279+
panic!("Cannot make JIT memory region writable");
280+
}
281+
}
282+
}
283+
268284
/// Make all the code in the region executable. Call this at the end of a write session.
269285
/// See [Self] for usual usage flow.
270286
pub fn mark_all_executable(&mut self) {

0 commit comments

Comments
 (0)