From ba6e3a0b380bdfa06c0413adae900eae3f7a4c7d Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Tue, 2 Jun 2026 09:25:03 +0200 Subject: [PATCH 1/8] sc598/macOS arm64: wrap snapshot-load in-place MAP_JIT writes for W^X On Apple Silicon, snapshot_load fixes up the loaded code's data in place in MAP_JIT (W^X) memory; the bare stores fault with SIGBUS (KERN_PROTECTION_FAILURE). Wrap the three load-side write sites in JITDataReadWriteMaybeExecute() / JITDataReadExecute() (same pattern as the other JIT-literal write sites): - the code-literals memcpy, - the fixup_objects walk (walk_temporary_root_objects), - the fixup_internals walk. (The save path fixes up a RW copy of the buffer, so it never hit this.) This unblocks snapshot load past the W^X SIGBUS. The remaining failure -- the position-independent function-pointer relocation decode (decodeEntryPoint / fixedAddress) -- is separate, pre-existing sc598 relocation work. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gctools/snapshotSaveLoad.cc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/gctools/snapshotSaveLoad.cc b/src/gctools/snapshotSaveLoad.cc index 8b32e4c90a..36356e61f2 100644 --- a/src/gctools/snapshotSaveLoad.cc +++ b/src/gctools/snapshotSaveLoad.cc @@ -2598,7 +2598,14 @@ void snapshot_load(void* maybeStartOfSnapshot, void* maybeEndOfSnapshot, const s // if (oldCodeClient->literalsSize() != 0 && (newCodeDataStart <= newCodeLiteralsStart && newCodeLiteralsEnd <= newCodeDataEnd)) { + // The new code's literal vector lives in the CodeBlock's MAP_JIT data + // region, which on Apple Silicon is write-protected (execute mode) by + // default; switch this thread to write mode around the store or snapshot + // LOAD faults with SIGBUS (KERN_PROTECTION_FAILURE), same W^X hazard as the + // other JIT-literal write sites (compiler.cc literals_vset, loadltv, etc.). + llvmo::JITDataReadWriteMaybeExecute(); memcpy((void*)newCodeLiteralsStart, (void*)oldCodeClient->literalsStart(), newCodeClient->literalsSize()); + llvmo::JITDataReadExecute(); } // // Now set the forwarding pointer from the oldCode object to the newCodeClient @@ -2632,7 +2639,12 @@ void snapshot_load(void* maybeStartOfSnapshot, void* maybeEndOfSnapshot, const s fixup_objects_t fixup_objects(LoadOp, (gctools::clasp_ptr_t)islbuffer, &islInfo); globalPointerFix = maybe_follow_forwarding_pointer; globalPointerFixStage = "snapshot_load/fixupObjects"; + // Load fixup writes relocated pointers INTO the loaded objects in place; code + // objects live in MAP_JIT (W^X) memory, so run in write mode on Apple Silicon or + // the store faults with SIGBUS. (The save path fixes up a RW copy, so it's fine.) + llvmo::JITDataReadWriteMaybeExecute(); walk_temporary_root_objects(root_holder, fixup_objects); + llvmo::JITDataReadExecute(); } #ifdef DEBUG_GUARD @@ -2650,7 +2662,10 @@ void snapshot_load(void* maybeStartOfSnapshot, void* maybeEndOfSnapshot, const s } fixup_internals_t internals(&fixup, &islInfo); + // Same MAP_JIT W^X hazard as fixupObjects above (in-place fixup of code objects). + llvmo::JITDataReadWriteMaybeExecute(); walk_temporary_root_objects(root_holder, internals); + llvmo::JITDataReadExecute(); // // Release the temporary roots From ac2f3dd86d29002a9e9be075d6f8b6fb79cc44bc Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Tue, 2 Jun 2026 09:49:10 +0200 Subject: [PATCH 2/8] sc598/macOS arm64: fix snapshot relocation decode + executable-link path quoting Make snapshot save/load/executable work on macOS arm64 (SLAD-SNAPSHOT and SLAD-EXECUTABLE now pass): 1. Relocation decode: the entry-point decode validated the resolved address by comparing its first byte against a saved firstByte and ABORTED on mismatch (decodeEntryPointForCompiledCode) / warned (fixedAddress). But a function's first instruction is frequently a relocatable instruction (e.g. ADRP) whose encoded immediate bytes legitimately differ between the save-time and load-time JIT (different load addresses => different page offsets). The offset/symbol-based decode is correct (verified: snapshot loads and returns the right value); drop the firstByte equality check, which was a false positive on every relocated first insn. 2. Executable link: save-lisp-and-die :executable builds a clang++ command run via system() (a shell); the -Wl,-force_load,/libiclasp.a path was unquoted, so a build dir containing a space (e.g. a Dropbox path) split the arg and the link failed. Quote the output, sectcreate, and force_load paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gctools/snapshotSaveLoad.cc | 35 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/gctools/snapshotSaveLoad.cc b/src/gctools/snapshotSaveLoad.cc index 36356e61f2..2252201d17 100644 --- a/src/gctools/snapshotSaveLoad.cc +++ b/src/gctools/snapshotSaveLoad.cc @@ -417,12 +417,11 @@ uintptr_t Fixup::fixedAddress(bool functionP, uintptr_t* ptrptr, const char* add uintptr_t address = this->_ISLLibraries[libidx]._GroupedPointers[pointerIndex]._address; uintptr_t addressOffset = this->_ISLLibraries[libidx]._SymbolInfo[pointerIndex]._AddressOffset; uintptr_t ptr = address + addressOffset; - if (functionP && *(uint8_t*)ptr != firstByte) { - printf("%s:%d:%s during decode %s codedAddress: %p ptr-> %p must be readable and point to first byte: 0x%x - but it points to " - "0x%x libidx: %lu\n", - __FILE__, __LINE__, __FUNCTION__, addressName, (void*)codedAddress, (void*)ptr, (uint32_t)firstByte, - (uint32_t) * (uint8_t*)ptr, libidx); - } + // The saved firstByte is only a hint: a function's first instruction may be a relocatable + // instruction (e.g. ADRP) whose bytes differ across loads, so a mismatch here is expected + // and not an error -- ptr (resolved symbol address + offset) is correct. Do not warn/abort. + (void)firstByte; + (void)functionP; return ptr; } @@ -541,12 +540,15 @@ bool decodeEntryPointForCompiledCode(Fixup* fixup, uintptr_t* ptrptr, llvmo::Obj if (epType != CODE_LIBRARY_ID) return false; // it's not a COMPILED_CODE_EPTYPE it must be to a library uintptr_t result = decodeEntryPointAddress(offset, codeStart, codeEnd, code); - if (*(uint8_t*)result != firstByte) { - ISL_ERROR("during decode function pointer %p must be readable and point to 0x%x (first byte) - instead it points to 0x%x " - "vaddress = %p codeStart = %p\n", - (void*)result, (uint32_t)firstByte, (uint)(*(uint8_t*)result), (void*)vaddress, - (void*)codeStart); - } + // Do NOT validate *result against the saved firstByte. A compiled-code entry point's + // first instruction is frequently a relocatable instruction (e.g. ADRP), whose encoded + // immediate bytes legitimately differ between the save-time and load-time JIT (different + // load addresses => different page offsets). The offset-based decode (codeStart+offset) + // is the correct entry point -- the object file re-JITs to the same layout, so the offset + // is structurally valid (and bounded: offset < codeEnd-codeStart at encode time). A + // firstByte mismatch is therefore expected for relocated first instructions and must not + // abort the load (it previously did, breaking SLAD-SNAPSHOT / SLAD-EXECUTABLE on arm64). + (void)firstByte; *ptrptr = result; return true; } @@ -1877,9 +1879,12 @@ void* snapshot_save_impl(void* data) { BUILD_LIB; #endif #ifdef _TARGET_OS_DARWIN - cmd = CXX_BINARY " " BUILD_LINKFLAGS " -o" + snapshot_data->_FileName + - " -sectcreate " SNAPSHOT_SEGMENT " " SNAPSHOT_SECTION " " + filename + " -Wl,-force_load," + snapshot_data->_LibDir + - "/libiclasp.a -lclasp " BUILD_LIB; + // Quote every path that may contain spaces (the build/lib dir can, e.g. a Dropbox + // path "/Users/.../gbt Dropbox/..."); this command is run through system() (a shell), + // so an unquoted -force_load path with a space gets split and the link fails. + cmd = CXX_BINARY " " BUILD_LINKFLAGS " -o\"" + snapshot_data->_FileName + + "\" -sectcreate " SNAPSHOT_SEGMENT " " SNAPSHOT_SECTION " \"" + filename + "\" -Wl,-force_load,\"" + + snapshot_data->_LibDir + "/libiclasp.a\" -lclasp " BUILD_LIB; #endif std::cout << "Link command:" << std::endl << std::flush; From aedda03f1eaaf570adc07fa223658be80d88d529 Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Tue, 2 Jun 2026 10:05:50 +0200 Subject: [PATCH 3/8] sc598/macOS arm64: link save-lisp-and-die :executable with an absolute -L The executable link command (run via system()) carried only the RELATIVE -Lboehmprecise/lib from BUILD_LINKFLAGS, so -lclasp resolved only when save-lisp-and-die :executable ran with CWD=build/; from any other directory the link failed ("library not found for -lclasp"). Add an absolute, quoted -L"<_LibDir>" (as the Linux branch already does). The runtime rpath is already absolute, so the produced executable both links and runs from any CWD. Verified: created and ran a standalone executable from /tmp (returns the right value); SLAD-EXECUTABLE now passes from the repo root, not just build/. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gctools/snapshotSaveLoad.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/gctools/snapshotSaveLoad.cc b/src/gctools/snapshotSaveLoad.cc index 2252201d17..60752aa56e 100644 --- a/src/gctools/snapshotSaveLoad.cc +++ b/src/gctools/snapshotSaveLoad.cc @@ -1881,8 +1881,12 @@ void* snapshot_save_impl(void* data) { #ifdef _TARGET_OS_DARWIN // Quote every path that may contain spaces (the build/lib dir can, e.g. a Dropbox // path "/Users/.../gbt Dropbox/..."); this command is run through system() (a shell), - // so an unquoted -force_load path with a space gets split and the link fails. - cmd = CXX_BINARY " " BUILD_LINKFLAGS " -o\"" + snapshot_data->_FileName + + // so an unquoted path with a space gets split and the link fails. + // Add an absolute -L for the lib dir (as the Linux branch already does): BUILD_LINKFLAGS + // only carries a RELATIVE -Lboehmprecise/lib, so without this, -lclasp resolves only when + // save-lisp-and-die :executable is run with CWD=build/. The absolute -L makes it link from + // any working directory (the runtime rpath is already absolute). + cmd = CXX_BINARY " " BUILD_LINKFLAGS " -L\"" + snapshot_data->_LibDir + "\" -o\"" + snapshot_data->_FileName + "\" -sectcreate " SNAPSHOT_SEGMENT " " SNAPSHOT_SECTION " \"" + filename + "\" -Wl,-force_load,\"" + snapshot_data->_LibDir + "/libiclasp.a\" -lclasp " BUILD_LIB; #endif From 12e5cd55cf6bf8bfea504723bc0d940b3494abad Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Fri, 22 May 2026 15:22:24 +0200 Subject: [PATCH 4/8] koga: fix macOS Apple Silicon build (Homebrew include path, quote rpath) Two problems prevented `ninja -C build` from working on an Apple Silicon Mac with a Homebrew toolchain: * boost (used by clbind/config.h) ships no pkg-config file and is therefore not declared as a koga `library`; clasp relies on it being on the default include path. units.lisp only added the Intel Homebrew prefix `/usr/local/include`, so `/opt/homebrew/include` was never searched and the build failed immediately with "'boost/config.hpp' file not found". Add `/opt/homebrew/include` for darwin when it exists (probe-guarded, so Intel and Linux are unaffected). * The rpath was emitted as an unquoted `-Wl,-rpath,`. When the build directory contains a space, ninja passes the flag through /bin/sh which then splits it at the space, and the link fails with "no such file or directory: ''". Quote the path. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/koga/units.lisp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/koga/units.lisp b/src/koga/units.lisp index 3e5605b322..e5522a71d8 100644 --- a/src/koga/units.lisp +++ b/src/koga/units.lisp @@ -219,6 +219,11 @@ (append-cflags configuration "-std=gnu++20" :type :cxxflags) #+darwin (append-cflags configuration "-stdlib=libc++" :type :cxxflags) #+darwin (append-cflags configuration "-I/usr/local/include") + ;; Apple Silicon Homebrew installs headers (e.g. boost, which ships no + ;; pkg-config file) under /opt/homebrew rather than the Intel /usr/local + ;; prefix. Add it to the search path when present. + #+darwin (when (probe-file #P"/opt/homebrew/include/") + (append-cflags configuration "-I/opt/homebrew/include")) #+linux (append-cflags configuration "-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fno-stack-protector -stdlib=libstdc++" :type :cxxflags) #+linux (append-cflags configuration "-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fno-stack-protector" @@ -317,7 +322,11 @@ has not been set." (message :emph "Configuring non-reproducible build") (loop for variant in (variants configuration) do (append-ldflags variant - (format nil "-Wl,-rpath,~a" + ;; Quote the path: the build directory may + ;; contain spaces, and ninja passes ldflags + ;; through /bin/sh which would otherwise split + ;; the rpath at the space. + (format nil "-Wl,-rpath,\"~a\"" (normalize-directory (uiop:ensure-absolute-pathname (merge-pathnames From de37269bc751a818c1bd9178336cb7a00e499557 Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Fri, 22 May 2026 15:22:24 +0200 Subject: [PATCH 5/8] Fix SIGBUS writing native-module literals on Apple Silicon (JIT W^X) On Apple Silicon, the LLVM ORC JITLink memory slab is mapped MAP_JIT and is write-protected (execute mode) per-thread by default; a thread must call pthread_jit_write_protect_np(false) before writing to it. clasp writes Lisp object pointers into each native module's literals vector, which lives in that JIT memory. Those writes were not bracketed by a switch to write mode, so on Apple Silicon they fault with SIGBUS (EXC_BAD_ACCESS code=2, KERN_PROTECTION_FAILURE) -- which manifested as a crash in loadltv::attr_clasp_module_native while loading freshly compiled native FASLs during the "Compiling Clasp native image" bootstrap step. (On x86-64 and Linux the rwx page is genuinely writable, so the bug was latent there.) The helpers JITDataReadWriteMaybeExecute()/JITDataReadExecute() already exist for exactly this but had no callers. Bracket every write into JIT-resident literals memory with them: * core::core__literals_vset (compiler.cc) * llvmo::code_literal_set (code.cc) * loadltv::attr_clasp_module_native (loadltv.cc) * loadltv::attr_clasp_function_native_estranged (loadltv.cc) * snapshot-load literal relocation memcpy (snapshotSaveLoad.cc) Reads of the literals vector are left untouched: MAP_JIT memory is readable in execute mode, only writes fault. The write-mode window is kept as small as possible (the bare store) so no JIT code is executed while the thread is in write mode. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/compiler.cc | 5 +++++ src/core/loadltv.cc | 12 +++++++++++- src/gctools/snapshotSaveLoad.cc | 4 ++++ src/llvmo/code.cc | 4 ++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/compiler.cc b/src/core/compiler.cc index 73a51179c0..81d901bfe4 100644 --- a/src/core/compiler.cc +++ b/src/core/compiler.cc @@ -210,7 +210,12 @@ CL_DEFUN T_sp core__literals_vref(Pointer_sp lvec, size_t index) { CL_LISPIFY_NAME("core:literals_vref"); CL_DEFUN_SETF T_sp core__literals_vset(T_sp val, Pointer_sp lvec, size_t index) { + // The literals vector lives in JIT (MAP_JIT) memory, which on Apple Silicon + // is write-protected (execute mode) by default; we must switch this thread to + // write mode around the store or it faults with a SIGBUS (KERN_PROTECTION_FAILURE). + llvmo::JITDataReadWriteMaybeExecute(); ((T_sp*)(lvec->ptr()))[index] = val; + llvmo::JITDataReadExecute(); return val; } diff --git a/src/core/loadltv.cc b/src/core/loadltv.cc index eb42aed138..85b1559a49 100644 --- a/src/core/loadltv.cc +++ b/src/core/loadltv.cc @@ -948,7 +948,11 @@ struct loadltv { bool seen = false; for (size_t i = 0; i < nlits; ++i) { if (lits[i] == function.raw_()) { + // lits[] is in write-protected JIT (MAP_JIT) memory on Apple Silicon; + // switch this thread to write mode just for the store. + llvmo::JITDataReadWriteMaybeExecute(); lits[i] = scf.raw_(); + llvmo::JITDataReadExecute(); seen = true; } } @@ -1148,7 +1152,13 @@ struct loadltv { } else { T_O** lits = (T_O**)vlits; for (size_t i = 0; i < nlits; ++i) { - lits[i] = get_ltv(read_index()).raw_(); + // Read the literal in execute mode (reads are fine), then switch this + // thread to write mode only for the store: lits[] is in write-protected + // JIT (MAP_JIT) memory on Apple Silicon and a bare store faults (SIGBUS). + T_O* value = get_ltv(read_index()).raw_(); + llvmo::JITDataReadWriteMaybeExecute(); + lits[i] = value; + llvmo::JITDataReadExecute(); } /* uint16_t nfuns = read_u16(); diff --git a/src/gctools/snapshotSaveLoad.cc b/src/gctools/snapshotSaveLoad.cc index 022193618b..422334cbfd 100644 --- a/src/gctools/snapshotSaveLoad.cc +++ b/src/gctools/snapshotSaveLoad.cc @@ -2626,7 +2626,11 @@ void snapshot_load(void* maybeStartOfSnapshot, void* maybeEndOfSnapshot, const s // if (oldCodeClient->literalsSize() != 0 && (newCodeDataStart <= newCodeLiteralsStart && newCodeLiteralsEnd <= newCodeDataEnd)) { + // newCodeLiteralsStart is in write-protected JIT (MAP_JIT) memory on + // Apple Silicon; switch this thread to write mode around the copy. + llvmo::JITDataReadWriteMaybeExecute(); memcpy((void*)newCodeLiteralsStart, (void*)oldCodeClient->literalsStart(), newCodeClient->literalsSize()); + llvmo::JITDataReadExecute(); } // // Now set the forwarding pointer from the oldCode object to the newCodeClient diff --git a/src/llvmo/code.cc b/src/llvmo/code.cc index 3de4110af1..7e04ec5b89 100644 --- a/src/llvmo/code.cc +++ b/src/llvmo/code.cc @@ -237,7 +237,11 @@ CL_LISPIFY_NAME("CODE-LITERAL"); CL_DEFUN_SETF core::T_sp code_literal_set(core::T_sp lit, ObjectFile_sp code, size_t idx) { core::T_O** literals = (core::T_O**)(code->literalsStart()); + // The literals vector lives in write-protected JIT (MAP_JIT) memory on Apple + // Silicon; switch this thread to write mode around the store to avoid a SIGBUS. + JITDataReadWriteMaybeExecute(); literals[idx] = lit.raw_(); + JITDataReadExecute(); return lit; } From 32be98d82798c5899b86d709eed7d14cfaba3442 Mon Sep 17 00:00:00 2001 From: Bike Date: Tue, 9 Jun 2026 13:05:46 -0400 Subject: [PATCH 6/8] Do JIT de/protection just once per native module load --- src/core/compiler.cc | 4 ++-- src/core/loadltv.cc | 32 +++++++++++++++++++------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/core/compiler.cc b/src/core/compiler.cc index 81d901bfe4..eee7ffabc8 100644 --- a/src/core/compiler.cc +++ b/src/core/compiler.cc @@ -200,8 +200,8 @@ void initializer_functions_invoke() { } // These functions are used to access a runtime module's literals vector. -// The vector is a T_sp[]. This could possibly be done with some normal -// CFFI accessor instead. +// The vector is a T_sp[]. See also CODE-LITERAL in llvmo/code.cc, +// which does the same but through the object file rather than a raw pointer. CL_DEFUN T_sp core__literals_vref(Pointer_sp lvec, size_t index) { return ((T_sp*)(lvec->ptr()))[index]; diff --git a/src/core/loadltv.cc b/src/core/loadltv.cc index 85b1559a49..4c6250985c 100644 --- a/src/core/loadltv.cc +++ b/src/core/loadltv.cc @@ -946,16 +946,16 @@ struct loadltv { // bit paranoid, but beats the alternative (closure with // incompatible function that segfaults or worse when called). bool seen = false; + // lits[] is in write-protected JIT (MAP_JIT) memory on Apple Silicon; + // switch this thread to write mode just for the store. + llvmo::JITDataReadWriteMaybeExecute(); for (size_t i = 0; i < nlits; ++i) { if (lits[i] == function.raw_()) { - // lits[] is in write-protected JIT (MAP_JIT) memory on Apple Silicon; - // switch this thread to write mode just for the store. - llvmo::JITDataReadWriteMaybeExecute(); lits[i] = scf.raw_(); - llvmo::JITDataReadExecute(); seen = true; } } + llvmo::JITDataReadExecute(); if (!seen) SIMPLE_ERROR("While loading native module: Could not find bytecode function {} in modules constants", _rep_(function)); } @@ -1151,15 +1151,21 @@ struct loadltv { module_id); } else { T_O** lits = (T_O**)vlits; - for (size_t i = 0; i < nlits; ++i) { - // Read the literal in execute mode (reads are fine), then switch this - // thread to write mode only for the store: lits[] is in write-protected - // JIT (MAP_JIT) memory on Apple Silicon and a bare store faults (SIGBUS). - T_O* value = get_ltv(read_index()).raw_(); - llvmo::JITDataReadWriteMaybeExecute(); - lits[i] = value; - llvmo::JITDataReadExecute(); - } + // lits is in write-protected JIT memory (MAP_JIT) on Apple Silicon, so we + // have to arrange things a bit for safety. We shouldn't do a simple loop of + // get_ltv read_index within JIT un-protection, since both those functions can + // signal errors and thereby execute arbitrary Lisp code. + // Instead we read all the T_O* into a temporary buffer (on the heap, since + // there may be a lot), and then copy those into lits while JIT-unprotected. + // This is probably faster than deprotecting and reprotecting for every literal. + T_O** tlits = (T_O**)calloc(nlits, sizeof(T_O*)); + for (size_t i = 0; i < nlits; ++i) tlits[i] = get_ltv(read_index()).raw_(); + + llvmo::JITDataReadWriteMaybeExecute(); + memcpy(lits, tlits, nlits * sizeof(T_O*)); + llvmo::JITDataReadExecute(); + + free(tlits); /* uint16_t nfuns = read_u16(); for (size_t j = 0; j < nfuns; ++j) { From e80d0bf8738328a7cd747c0aa15cb661be16413f Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Tue, 2 Jun 2026 09:04:19 +0200 Subject: [PATCH 7/8] Fix Apple Silicon W^X SIGBUS in setf_jit_lookup_t (FFI callbacks) On Apple Silicon, JIT'd code/data lives in MAP_JIT memory that is write-protected (execute mode) by default; a thread must switch it to write mode (pthread_jit_write_protect_np) around any store or the store faults with SIGBUS (KERN_PROTECTION_FAILURE). setf_jit_lookup_t (llvmo:jit-lookup-t setf) stores a Lisp function pointer into a JIT-emitted global. make-callback / clasp-ffi:%defcallback reach it via (setf (llvm-sys:jit-lookup-t dylib varname) function), and on arm64-darwin it SIGBUSes at compile time -- this is what makes the defcallback-native regression test (CFFI-DEFCALLBACK) crash during compile-file. Wrap the store in JITDataReadWriteMaybeExecute()/JITDataReadExecute(), matching the existing W^X guards on the other JIT-literal write sites (core__literals_vset, loadltv op_setf_literals / attr_clasp_module_native). Verified on macOS arm64 (native boehmprecise image): CFFI-DEFCALLBACK now passes (previously a Bus error at compile time); no regressions. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/llvmo/llvmoPackage.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/llvmo/llvmoPackage.cc b/src/llvmo/llvmoPackage.cc index 589d77da60..0e69fb7921 100644 --- a/src/llvmo/llvmoPackage.cc +++ b/src/llvmo/llvmoPackage.cc @@ -55,6 +55,7 @@ THE SOFTWARE. #include #include #include +#include // JITDataReadWriteMaybeExecute / JITDataReadExecute (W^X) #include #include #include @@ -690,7 +691,15 @@ CL_DEFUN_SETF core::T_sp setf_jit_lookup_t(core::T_sp value, JITDylib_sp dylib, if (!found) SIMPLE_ERROR("Could not find pointer for name |{}|", name); core::T_O** tptr = (core::T_O**)ptr; + // The JIT'd global (e.g. an FFI callback's `callback-lisp-function-N`) lives in + // MAP_JIT memory, which on Apple Silicon is write-protected (execute mode) by + // default; switch this thread to write mode around the store or it faults with a + // SIGBUS (KERN_PROTECTION_FAILURE). This is the W^X store site that make-callback + // / %defcallback hits (the defcallback-native regression test); the other literal + // write sites are wrapped in compiler.cc / loadltv.cc. + JITDataReadWriteMaybeExecute(); *tptr = value.raw_(); + JITDataReadExecute(); return value; } From 7052dcb7255e2a4726a00fa0f09018c4a0ecd5d3 Mon Sep 17 00:00:00 2001 From: Bike Date: Thu, 11 Jun 2026 14:40:31 -0400 Subject: [PATCH 8/8] remove first byte from snapshot relocation encoding The only thing we were using it for is a correctness check. If that check is not valid due to relocation, there' no point in putting the byte in. --- src/gctools/snapshotSaveLoad.cc | 41 +++++++++------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/src/gctools/snapshotSaveLoad.cc b/src/gctools/snapshotSaveLoad.cc index 7d13988cad..fdf60d28cf 100644 --- a/src/gctools/snapshotSaveLoad.cc +++ b/src/gctools/snapshotSaveLoad.cc @@ -374,20 +374,19 @@ void SymbolLookup::addAllLibraries(FILE* fout) { namespace snapshotSaveLoad { -void decodeRelocation_(uintptr_t codedAddress, uint8_t& firstByte, uintptr_t& libindex, uintptr_t& offset) { +void decodeRelocation_(uintptr_t codedAddress, uintptr_t& libindex, uintptr_t& offset) { offset = (uintptr_t)codedAddress & (((uintptr_t)1 << 32) - 1); libindex = ((uintptr_t)codedAddress >> 32) & (uintptr_t)0xff; - firstByte = (uint8_t)(((uintptr_t)codedAddress >> 48) & 0xff); } -uintptr_t encodeRelocation_(uint8_t firstByte, size_t libraryIndex, size_t relocationOrOffset) { +uintptr_t encodeRelocation_(size_t libraryIndex, size_t relocationOrOffset) { if ((relocationOrOffset & (((uintptr_t)1 << 32) - 1)) != relocationOrOffset) { ISL_ERROR("relocationOrOffset %lu is too large", relocationOrOffset); } if (libraryIndex > 256) { ISL_ERROR("libraryIndex %lu is too large", libraryIndex); } - uintptr_t result = ((uintptr_t)1 << 56 | (uintptr_t)firstByte << 48) | (libraryIndex << 32) | relocationOrOffset; + uintptr_t result = ((uintptr_t)1 << 56) | (libraryIndex << 32) | relocationOrOffset; return result; } @@ -418,20 +417,15 @@ void Fixup::registerFunctionPointer(size_t libraryIndex, uintptr_t* functionPtrP uintptr_t Fixup::fixedAddress(bool functionP, uintptr_t* ptrptr, const char* addressName) { - uint8_t firstByte; uintptr_t libidx; uintptr_t pointerIndex; if (virtualMethodP(ptrptr)) return *ptrptr; uintptr_t codedAddress = *ptrptr; - decodeRelocation_(codedAddress, firstByte, libidx, pointerIndex); + decodeRelocation_(codedAddress, libidx, pointerIndex); uintptr_t address = this->_ISLLibraries[libidx]._GroupedPointers[pointerIndex]._address; uintptr_t addressOffset = this->_ISLLibraries[libidx]._SymbolInfo[pointerIndex]._AddressOffset; uintptr_t ptr = address + addressOffset; - // The saved firstByte is only a hint: a function's first instruction may be a relocatable - // instruction (e.g. ADRP) whose bytes differ across loads, so a mismatch here is expected - // and not an error -- ptr (resolved symbol address + offset) is correct. Do not warn/abort. - (void)firstByte; (void)functionP; return ptr; } @@ -470,13 +464,13 @@ size_t Fixup::ensureLibraryRegistered(uintptr_t address) { return idx; }; -uintptr_t encodeEntryPointValue(uint8_t firstByte, uint8_t epType, uintptr_t offset) { - uintptr_t result = encodeRelocation_(firstByte, epType, offset); +uintptr_t encodeEntryPointValue(uint8_t epType, uintptr_t offset) { + uintptr_t result = encodeRelocation_(epType, offset); return result; } -void decodeEntryPointValue(uintptr_t value, uint8_t& firstByte, uintptr_t& epType, uintptr_t& offset) { - decodeRelocation_(value, firstByte, epType, offset); +void decodeEntryPointValue(uintptr_t value, uintptr_t& epType, uintptr_t& offset) { + decodeRelocation_(value, epType, offset); }; uintptr_t decodeEntryPointAddress(uintptr_t offset, uintptr_t codeStart, uintptr_t codeEnd, core::T_sp code) { @@ -529,37 +523,26 @@ bool encodeEntryPointForCompiledCode(Fixup* fixup, uintptr_t* ptrptr, llvmo::Obj if (address < 65536) { printf("%s:%d:%s address @%p is %p and is small\n", __FILE__, __LINE__, __FUNCTION__, ptrptr, (void*)address); } - uint8_t firstByte = *(uint8_t*)address; uintptr_t codeStart = (uintptr_t)code->codeStart(); uintptr_t codeEnd = (uintptr_t)code->codeEnd(); if (address < codeStart || codeEnd <= address) return false; uintptr_t offset = encodeEntryPointOffset(address, codeStart, codeEnd, code); - uintptr_t result = encodeEntryPointValue(firstByte, CODE_LIBRARY_ID, offset); + uintptr_t result = encodeEntryPointValue(CODE_LIBRARY_ID, offset); *ptrptr = result; return true; } bool decodeEntryPointForCompiledCode(Fixup* fixup, uintptr_t* ptrptr, llvmo::ObjectFile_sp code) { uintptr_t vaddress = *ptrptr; - uint8_t firstByte; uintptr_t epType; uintptr_t offset; uintptr_t codeStart = code->codeStart(); uintptr_t codeEnd = code->codeEnd(); - decodeEntryPointValue(vaddress, firstByte, epType, offset); + decodeEntryPointValue(vaddress, epType, offset); if (epType != CODE_LIBRARY_ID) return false; // it's not a COMPILED_CODE_EPTYPE it must be to a library uintptr_t result = decodeEntryPointAddress(offset, codeStart, codeEnd, code); - // Do NOT validate *result against the saved firstByte. A compiled-code entry point's - // first instruction is frequently a relocatable instruction (e.g. ADRP), whose encoded - // immediate bytes legitimately differ between the save-time and load-time JIT (different - // load addresses => different page offsets). The offset-based decode (codeStart+offset) - // is the correct entry point -- the object file re-JITs to the same layout, so the offset - // is structurally valid (and bounded: offset < codeEnd-codeStart at encode time). A - // firstByte mismatch is therefore expected for relocated first instructions and must not - // abort the load (it previously did, breaking SLAD-SNAPSHOT / SLAD-EXECUTABLE on arm64). - (void)firstByte; *ptrptr = result; return true; } @@ -1549,9 +1532,7 @@ void prepareRelocationTableForSave(Fixup* fixup, SymbolLookup& symbolLookup) { } // Now encode the relocation void** addr = (void**)curLib._InternalPointers[ii]._ptrptr; - uint8_t* uint8ptr = (uint8_t*)*addr; - uint8_t firstByte = *uint8ptr; - *curLib._InternalPointers[ii]._ptrptr = encodeRelocation_(firstByte, idx, groupPointerIdx); + *curLib._InternalPointers[ii]._ptrptr = encodeRelocation_(idx, groupPointerIdx); } core::lisp_write(fmt::format("{} unique pointers need to be passed to dladdr\n", curLib._GroupedPointers.size())); SaveSymbolCallback thing(curLib);