3838#include <sys/mman.h>
3939#include <pthread.h>
4040
41+ // On macOS arm64 (Apple Silicon), W+X mmap requires MAP_JIT and the
42+ // thread-local pthread_jit_write_protect_np guard to toggle between
43+ // writable and executable states on the same page.
44+ #ifdef __APPLE__
45+ # define JIT_MMAP_FLAGS (MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT)
46+ # define JIT_WRITE_START () pthread_jit_write_protect_np(0)
47+ # define JIT_WRITE_END () pthread_jit_write_protect_np(1)
48+ #else
49+ # define JIT_MMAP_FLAGS (MAP_PRIVATE | MAP_ANONYMOUS)
50+ # define JIT_WRITE_START () ((void)0)
51+ # define JIT_WRITE_END () ((void)0)
52+ #endif
53+
4154// Forward decl from arc.c. Closure envs created by Tin are ARC blocks
4255// with a destructor at offset 0; _tin_release_closure invokes the
4356// destructor when rc reaches 0 to release captured RC values, then
@@ -227,8 +240,10 @@ static void atexit_release_all_pages(void) {
227240 void * env = data [1 ];
228241 // Scrub before release in case the dtor somehow re-enters
229242 // and re-walks this slot.
243+ JIT_WRITE_START ();
230244 data [0 ] = NULL ;
231245 data [1 ] = NULL ;
246+ JIT_WRITE_END ();
232247 _tin_release_closure (env );
233248 }
234249
@@ -241,11 +256,13 @@ static void atexit_release_all_pages(void) {
241256static TrampPage * new_page (void ) {
242257 void * raw = mmap (NULL , TRAMP_PAGE_SIZE ,
243258 PROT_READ | PROT_WRITE | PROT_EXEC ,
244- MAP_PRIVATE | MAP_ANONYMOUS , -1 , 0 );
259+ JIT_MMAP_FLAGS , -1 , 0 );
245260 if (raw == MAP_FAILED ) {
246261 return NULL ;
247262 }
248263
264+ JIT_WRITE_START ();
265+
249266 TrampPage * p = (TrampPage * )raw ;
250267 p -> next = NULL ;
251268 p -> free_head = 0 ;
@@ -258,6 +275,8 @@ static TrampPage *new_page(void) {
258275 ((i + 1 ) < TRAMP_SLOTS ) ? (int32_t )(i + 1 ) : -1 );
259276 }
260277
278+ JIT_WRITE_END ();
279+
261280 // Append to the all-pages registry for atexit cleanup.
262281 if (_tramp_all_pages_len == _tramp_all_pages_cap ) {
263282 size_t new_cap = _tramp_all_pages_cap ? _tramp_all_pages_cap * 2 : 16 ;
@@ -328,6 +347,8 @@ void *tin_make_trampoline(void *fn, void *env, void *dispatcher) {
328347 // Lay out the slot:
329348 // slot+0..15 : TinClosureData = { fn, env }
330349 // slot+16..31 : machine code
350+ JIT_WRITE_START ();
351+
331352 void * * data = (void * * )slot ;
332353 data [0 ] = fn ;
333354 data [1 ] = env ;
@@ -336,6 +357,8 @@ void *tin_make_trampoline(void *fn, void *env, void *dispatcher) {
336357 uint8_t * code = slot + TRAMP_DATA_BYTES ;
337358 size_t n = emit_trampoline (code , closure_data_ptr , dispatcher );
338359
360+ JIT_WRITE_END ();
361+
339362 icache_flush (code , n );
340363
341364 // The C caller's function pointer is the address of the CODE,
@@ -412,15 +435,15 @@ void tin_interop_closure_free(void *tramp) {
412435 return ;
413436 }
414437
415- p -> in_use &= ~bit ;
416-
417- // Snapshot env BEFORE handing the slot back to the free-list, but
418- // AFTER confirming this is a first free. Released outside the
419- // mutex: the closure dtor may run user-defined Tin deinit code
420- // which can allocate, take other locks, or transitively allocate
421- // another trampoline.
438+ // Snapshot env (read) before toggling write protection; reads are
439+ // always safe from JIT pages regardless of write-protect state.
422440 void * * data = (void * * )slot ;
423441 void * env = data [1 ];
442+
443+ JIT_WRITE_START ();
444+
445+ p -> in_use &= ~bit ;
446+
424447 // Scrub so a use-after-free of the trampoline pointer crashes
425448 // loudly instead of jumping through stale bytes.
426449 data [0 ] = NULL ;
@@ -442,6 +465,8 @@ void tin_interop_closure_free(void *tramp) {
442465 // a page from _tramp_all_pages and keeps the per-call free path
443466 // lock-and-mutate only.
444467
468+ JIT_WRITE_END ();
469+
445470 pthread_mutex_unlock (& _tramp_mu );
446471
447472 // Release the captured env after dropping the lock.
0 commit comments