Skip to content

feat(catalyst): ship V8 with its JIT tiers on Mac Catalyst - #10

Open
edusperoni wants to merge 1 commit into
mainfrom
feat/catalyst-jit
Open

edusperoni wants to merge 1 commit into
mainfrom
feat/catalyst-jit

Conversation

@edusperoni

Copy link
Copy Markdown
Collaborator

What

  • build-ios.sh: Catalyst variants now pass v8_enable_lite_mode=false, so they ship Sparkplug, Maglev and TurboFan.
  • New patches/v8_catalyst_jit_write_protect.patch, applied by fetch.sh for iOS fetches, so V8's JIT works inside a Catalyst process.
  • tests/apple-args.py asserts the Catalyst arg; the README documents the patch and the arg.

Why

Catalyst has been lite mode all along. The comment in build-ios.sh says Catalyst is "deliberately not lite", but 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. The shipped -7 Catalyst libv8_compiler.a is ~1.5 KB, --trace-opt reports nothing, and hot JS runs at --jitless speed.

A JIT build alone crashes on Apple silicon. 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 MAP_JIT pages are per-thread W^X, so the first JIT code write raises SIGBUS, even with com.apple.security.cs.allow-jit. The patch gives Catalyst the macOS model:

  • V8_HAS_PTHREAD_JIT_WRITE_PROTECT for Catalyst arm64, so RwxMemoryWriteScope toggles pthread_jit_write_protect_np. The SDK marks that function unavailable on Catalyst, so it's resolved once with dlsym; libpthread on macOS exports it.
  • The macOS code-range pre-commit and the recommit when re-embedding builtins, which the iOS branches skip.

Everything is gated on a new internal V8_OS_MACCATALYST (set in src/base/build_config.h, not in a public header), so device, simulator, tvOS and Android builds compile the same code as before.

Verification

Local arm64 Catalyst build of 14.9.207.39 with the existing patches plus this one, linked into a small macabi probe that runs a hot loop and a regex:

Signing --jitless JIT --trace-opt
ad-hoc 385 ms 11 ms 7 optimized
hardened + allow-jit 379 ms 11 ms 7 optimized
hardened + allow-unsigned-executable-memory 383 ms 13 ms 7 optimized
hardened, no entitlement 391 ms SIGTRAP at startup —

Without either entitlement the process can't map JIT memory, and V8 doesn't fall back on its own, so the iOS runtime passes --jitless in that case (companion runtime PR). Without the patch, all three JIT rows SIGBUS.

python3 tests/apple-args.py passes. x64-catalyst picks up the same arg; Intel Macs have no per-thread W^X, so the patch is a no-op there. I couldn't run that slice locally.

Size

Stripped arm64 Catalyst NativeScript.framework: 13.27 MB → 22.89 MB (+9.6 MB), about +2.7 MB zipped. This affects the Catalyst slice only.

Release

The runtime consumes this through a tagged release (v8-*), so it needs a tag after merge, e.g. v8-14.9.207.39-8.

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.
@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8b0ad370-9331-48ca-b472-a68d127e94dd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant