From 7222a079a901cdd7dccd44563a329234d933c608 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 30 Oct 2026 15:19:20 -0300 Subject: [PATCH] feat(catalyst): ship V8 with its JIT tiers on Mac Catalyst Catalyst was built as lite mode despite the build script saying otherwise: gni/v8.gni turns v8_enable_lite_mode on for every target_platform "iphoneos" build, and Catalyst is only a target_environment of that platform, so the archives shipped without Sparkplug, Maglev or TurboFan. Set it off explicitly for both Catalyst variants. A JIT build alone faults on Apple silicon: Catalyst compiles as iOS, so V8 uses its iOS JIT-memory model and writes MAP_JIT pages without macOS's per-thread W^X toggle (SIGBUS on the first code write, even with the allow-jit entitlement). v8_catalyst_jit_write_protect.patch gives Catalyst the macOS model: pthread_jit_write_protect_np around code writes (resolved with dlsym, since the SDK marks it unavailable on Catalyst) and the macOS code-range pre-commit. It is applied for iOS fetches and is a no-op outside Catalyst. Verified on an arm64 macabi probe linking the archives: hot JS runs ~35x faster than --jitless and --trace-opt reports optimized functions under ad-hoc signing, hardened runtime + allow-jit, and hardened runtime + allow-unsigned-executable-memory. Without either entitlement the JIT cannot map memory, so the runtime must pass --jitless there. --- README.md | 11 ++- patches/v8_catalyst_jit_write_protect.patch | 89 +++++++++++++++++++++ scripts/matrix/build-ios.sh | 10 ++- scripts/matrix/fetch.sh | 6 ++ tests/apple-args.py | 4 + 5 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 patches/v8_catalyst_jit_write_protect.patch diff --git a/README.md b/README.md index 1a6aa0c..1074daa 100644 --- a/README.md +++ b/README.md @@ -129,11 +129,20 @@ platform), dispatch the workflow with `platforms: android` or `platforms: ios`. |---|---| | `v8_resurrecting_finalizers.patch` | Restores `WeakCallbackType::kFinalizer`, removed upstream immediately after 10.3.22. Both runtimes' object managers depend on resurrecting finalizers. It also lifts, for the finalizer drain only, the `DisallowJavascriptExecution` scope that `Heap::CollectGarbage` now holds across the whole collection: 14.9 turns entering JS from a GC callback into a `GRACEFUL_FATAL`, and the iOS runtime reaches JS whenever a released ObjC object's `-dealloc` hits a JS-backed override. | | `android_build.patch` | Lowers chromium's minimum SDK to 21 (that floor is for Java/dex tooling; this builds only the native target), makes `android_ndk_root` a gn arg, and widens the hard Linux-host assert so macOS can build the 64-bit ABIs. | +| `v8_catalyst_jit_write_protect.patch` | Mac Catalyst compiles as iOS (`TARGET_OS_IPHONE`), so V8 picks its iOS JIT-memory model, which assumes `MAP_JIT` pages are writable and executable at once. A Catalyst process runs on macOS, where Apple silicon makes `MAP_JIT` pages per-thread W^X, so the first JIT code write faults (`SIGBUS`). The patch gives Catalyst the macOS model: `pthread_jit_write_protect_np` around code writes (resolved with `dlsym`, since the SDK marks it unavailable on Catalyst) and the macOS code-range pre-commit. Applied to iOS fetches only; it is a no-op outside Catalyst. | ## The gn args are load-bearing They are commented at their definition in `scripts/matrix/build-android.sh` and -`scripts/matrix/build-ios.sh`. Three worth knowing: +`scripts/matrix/build-ios.sh`. Four worth knowing: + +- `v8_enable_lite_mode=false` (Catalyst only) — `gni/v8.gni` turns lite mode + on for every `target_platform="iphoneos"` build, and Catalyst is only a + `target_environment` of that platform, so without the explicit override the + Catalyst archives ship without Sparkplug, Maglev or TurboFan. Catalyst keeps + the full tier set so apps entitled to JIT (`com.apple.security.cs.allow-jit`) + get it; the iOS runtime passes `--jitless` when the process cannot map JIT + memory. This adds about 9.6 MB to the stripped arm64 Catalyst framework. - `cppgc_enable_caged_heap=false` (iOS only) — it defaults to **true on arm64** and, contrary to how the flags read, does *not* track diff --git a/patches/v8_catalyst_jit_write_protect.patch b/patches/v8_catalyst_jit_write_protect.patch new file mode 100644 index 0000000..139c372 --- /dev/null +++ b/patches/v8_catalyst_jit_write_protect.patch @@ -0,0 +1,89 @@ +diff --git a/src/base/build_config.h b/src/base/build_config.h +index ee745539dfd..4182c592472 100644 +--- a/src/base/build_config.h ++++ b/src/base/build_config.h +@@ -36,8 +36,16 @@ + #endif + #define V8_HAS_PKU_JIT_WRITE_PROTECT V8_HAS_PKU_SUPPORT + ++// Mac Catalyst compiles as iOS (TARGET_OS_IPHONE) but runs on macOS, so its ++// JIT pages follow the macOS MAP_JIT + pthread_jit_write_protect_np rules. ++#if defined(V8_OS_IOS) && defined(TARGET_OS_MACCATALYST) && \ ++ TARGET_OS_MACCATALYST ++#define V8_OS_MACCATALYST 1 ++#endif ++ + // pthread_jit_write_protect is only available on arm64 Mac. +-#if defined(V8_HOST_ARCH_ARM64) && defined(V8_OS_MACOS) ++#if defined(V8_HOST_ARCH_ARM64) && \ ++ (defined(V8_OS_MACOS) || defined(V8_OS_MACCATALYST)) + #define V8_HAS_PTHREAD_JIT_WRITE_PROTECT 1 + #else + #define V8_HAS_PTHREAD_JIT_WRITE_PROTECT 0 +diff --git a/src/base/platform/platform-darwin.cc b/src/base/platform/platform-darwin.cc +index 41fc5ec3302..004ae7c002b 100644 +--- a/src/base/platform/platform-darwin.cc ++++ b/src/base/platform/platform-darwin.cc +@@ -234,6 +234,24 @@ V8_BASE_EXPORT void SetJitWriteProtected(int enable) { + } + + #pragma clang diagnostic pop ++#elif V8_HAS_PTHREAD_JIT_WRITE_PROTECT && defined(V8_OS_MACCATALYST) ++// The SDK marks pthread_jit_write_protect_np unavailable on Mac Catalyst, but ++// a Catalyst process runs against macOS's libpthread, which exports it. ++namespace { ++using JitWriteProtectFn = void (*)(int); ++ ++JitWriteProtectFn ResolveJitWriteProtect() { ++ auto fn = reinterpret_cast( ++ dlsym(RTLD_DEFAULT, "pthread_jit_write_protect_np")); ++ CHECK_NOT_NULL(fn); ++ return fn; ++} ++} // namespace ++ ++V8_BASE_EXPORT void SetJitWriteProtected(int enable) { ++ static const JitWriteProtectFn jit_write_protect = ResolveJitWriteProtect(); ++ jit_write_protect(enable); ++} + #endif + + } // namespace base +diff --git a/src/heap/code-range.cc b/src/heap/code-range.cc +index ca0b1742b38..b979862d76f 100644 +--- a/src/heap/code-range.cc ++++ b/src/heap/code-range.cc +@@ -382,8 +382,8 @@ bool CodeRange::InitReservation(v8::PageAllocator* page_allocator, + // Don't pre-commit the code cage on Windows since it uses memory and it's not + // required for recommit. + // iOS cannot adjust page permissions for MAP_JIT'd pages, they are set as RWX +-// at the start. +-#if !defined(V8_OS_WIN) && !defined(V8_OS_IOS) ++// at the start. Mac Catalyst runs on macOS and needs the pre-commit. ++#if !defined(V8_OS_WIN) && (!defined(V8_OS_IOS) || defined(V8_OS_MACCATALYST)) + if (params.page_initialization_mode == + base::PageInitializationMode::kRecommitOnly) { + void* base = reinterpret_cast(page_allocator_->begin() + +@@ -605,7 +605,7 @@ uint8_t* CodeRange::RemapEmbeddedBuiltins(Isolate* isolate, + if (V8_HEAP_USE_PTHREAD_JIT_WRITE_PROTECT || + V8_HEAP_USE_BECORE_JIT_WRITE_PROTECT || ThreadIsolation::Enabled()) { + // iOS code pages are already RWX and don't need to be modified. +-#if !defined(V8_TARGET_OS_IOS) ++#if !defined(V8_TARGET_OS_IOS) || defined(V8_OS_MACCATALYST) + if (!page_allocator()->RecommitPages(embedded_blob_code_copy, code_size, + PageAllocator::kReadWriteExecute)) { + V8::FatalProcessOutOfMemory(isolate, +diff --git a/src/heap/code-range.h b/src/heap/code-range.h +index b42081b8aa1..acb2f57d420 100644 +--- a/src/heap/code-range.h ++++ b/src/heap/code-range.h +@@ -177,7 +177,8 @@ class CodeRange final : public VirtualMemoryCage { + // Red zones that we should not allocate in. + RedZones red_zones_; + +-#if !defined(V8_OS_WIN) && !defined(V8_OS_IOS) && defined(DEBUG) ++#if !defined(V8_OS_WIN) && \ ++ (!defined(V8_OS_IOS) || defined(V8_OS_MACCATALYST)) && defined(DEBUG) + bool immutable_ = false; + #endif + }; diff --git a/scripts/matrix/build-ios.sh b/scripts/matrix/build-ios.sh index 19e832a..174243b 100755 --- a/scripts/matrix/build-ios.sh +++ b/scripts/matrix/build-ios.sh @@ -77,8 +77,13 @@ MODULES=( ) # iOS proper cannot JIT, so it is built in lite mode. Catalyst runs on macOS, -# where JIT is permitted, and is deliberately not lite -- keeping the two arg -# sets distinct preserves what each currently ships. +# where an app holding com.apple.security.cs.allow-jit may JIT, so it ships the +# full tier set (Sparkplug, Maglev, TurboFan) and the runtime passes --jitless +# when the process may not map JIT memory. lite mode must be turned off +# explicitly: gni/v8.gni defaults it on for target_platform "iphoneos", and +# Catalyst is only a target_environment of that platform. The Catalyst JIT also +# needs patches/v8_catalyst_jit_write_protect.patch; without it V8 writes JIT +# code without macOS's per-thread W^X toggle and faults on Apple silicon. # # v8_expose_public_symbols marks the V8_EXPORT API default-visibility. Plugins # (e.g. @nativescript/canvas) compile against the vendored V8 headers and @@ -128,6 +133,7 @@ if [ "$TARGET_ENV" = "catalyst" ]; then GN_ARGS="$GN_ARGS is_official_build=true use_thin_lto=false + v8_enable_lite_mode=false ios_deployment_target=\"$CATALYST_DEPLOYMENT_TARGET\"" else GN_ARGS="$GN_ARGS diff --git a/scripts/matrix/fetch.sh b/scripts/matrix/fetch.sh index 3abda01..16ddf31 100755 --- a/scripts/matrix/fetch.sh +++ b/scripts/matrix/fetch.sh @@ -87,6 +87,12 @@ git -C v8 apply "$ROOT_DIR/patches/v8_resurrecting_finalizers.patch" # reserved once per isolate. git -C v8 apply "$ROOT_DIR/patches/v8_js_dispatch_table_reservation.patch" +if [ "$PLATFORM" = "ios" ]; then + # Mac Catalyst compiles as iOS but runs on macOS; this gives its JIT build + # the macOS MAP_JIT + pthread_jit_write_protect_np handling. + git -C v8 apply "$ROOT_DIR/patches/v8_catalyst_jit_write_protect.patch" +fi + if [ "$PLATFORM" = "android" ]; then # API 21 and a selectable android_ndk_root are needed on every host; the # host-assert change in the same patch is a no-op on Linux. diff --git a/tests/apple-args.py b/tests/apple-args.py index fcd1590..ee43d58 100644 --- a/tests/apple-args.py +++ b/tests/apple-args.py @@ -35,5 +35,9 @@ def test_ios_and_catalyst_do_not_receive_tvos_overrides(self): self.assertIn('is_debug=false', args) self.assertEqual('v8_enable_lite_mode=true' in args, 'catalyst' not in variant) + def test_catalyst_is_built_with_the_jit(self): + for variant in ['arm64-catalyst', 'x64-catalyst']: + self.assertIn('v8_enable_lite_mode=false', self.args_for(variant)) + if __name__ == '__main__': unittest.main()