Skip to content

Add AArch64 (ARMv8.2-A) recompiler backend for the generic sandbox - #387

Open
kvpanch wants to merge 13 commits into
masterfrom
kvpanch/arm_recompiler
Open

kvpanch wants to merge 13 commits into
masterfrom
kvpanch/arm_recompiler

Conversation

@kvpanch

@kvpanch kvpanch commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Implements a second native codegen backend targeting ARMv8.2-A (Neoverse N1 baseline; runs on Apple Silicon), alongside the existing x86-64 backend. Runs through the generic sandbox.

  • polkavm-assembler: new aarch64 module
  • polkavm-common: defines guest regs x0-x12, TMP x13, mem-base x14
  • compiler/aarch64.rs: all ~150 ArchVisitor handlers (div/rem with RISC-V divide-by-zero/overflow semantics), trampolines, and a fixed-layout gas stub with an embedded cost literal and a UDF underflow trap.
  • generic sandbox: aarch64 entry/exit asm, signal-handler register/PC reads and Apple-Silicon.

@kvpanch
kvpanch force-pushed the kvpanch/arm_recompiler branch from 7a0ddbd to dd2e909 Compare August 20, 2026 14:35
kvpanch and others added 13 commits September 8, 2026 10:40
Implements a second native codegen backend targeting ARMv8.2-A (Neoverse N1
baseline; runs on Apple Silicon), alongside the existing x86-64 backend. Runs
through the generic (signal-based) sandbox.

- polkavm-assembler: new `aarch64` module (~70 A64 encoders) and a bitfield/ADR
fixup path; `finalize()` is cfg-split so the x86 byte-displacement path is
unchanged.
- polkavm-common: arch-gate `regmap` (guest regs x0-x12, TMP x13, mem-base x14);
enable the arch-neutral `zygote` module on aarch64.
- compiler/aarch64.rs: all ~150 ArchVisitor handlers (div/rem with RISC-V
divide-by-zero/overflow semantics), trampolines, and a fixed-layout gas stub
with an embedded cost literal and a UDF underflow trap. `count_set_bits`
(needs NEON cnt) and `memset` remain unimplemented.
- generic sandbox: aarch64 entry/exit asm, signal-handler register/PC reads,
and Apple-Silicon W^X (MAP_JIT + pthread_jit_write_protect_np + icache flush).
- Gate the compiler on aarch64+macos+generic-sandbox; add the libc dep.

DOOM and 12 new end-to-end backend tests pass on aarch64-apple-darwin; the
x86-64 backend is unaffected. Known gaps: popcount/memset handlers and the
dynamic-paging segfault PC-recovery path
…c comments)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upstream refactored the assembler from an Instruction<T> wrapper struct to an
InstructionT trait (encode/fixup now take EncodeFlags). Port the AArch64 backend:
instruction types impl InstructionT, constructors return the type directly, and
the compiler's push() takes 'impl InstructionT'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AArch64 generic-sandbox recompiler was gated to macOS only. Enable it on
aarch64-linux so it can run at a 4K page size (macOS is 16K-only):

- generic sandbox uses libc for its sys module on aarch64-linux (polkavm-linux-raw
  is x86_64-only); add SIGBUS handling and the Linux ucontext register/fault-address
  extraction; flush the I-cache via __clear_cache after writing code.
- gate the Linux sandbox (zygote/userfaultfd, x86-only) and its shm/allocator
  modules to x86_64-linux; the generic sandbox is the only compiled backend on
  aarch64-linux, and SandboxKind::Linux::is_supported() reflects that.

All 709 generic-sandbox compiler tests pass at a 4K page size (plus
memset_with_dynamic_paging, which is skipped on macOS 16K).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
macOS on Apple Silicon is 16K-only, so the recompiler's paging/mprotect logic is
never exercised at 4K on a Mac. This adds a ci/jobs script that boots a Linux
guest under Virtualization.framework (Lima vz backend, 4K kernel, near-native
speed) and runs the generic-sandbox compiler tests at a real 4K page size, plus a
section in RECOMPILER_ARCHITECTURE.md documenting the workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o it

New `SandboxKind::Hypervisor` behind the `hypervisor-sandbox` feature (aarch64).
On macOS it uses Apple Hypervisor.framework to run guest code in a vCPU with its
own stage-1 MMU (TCR_EL1.TG0 = 4K), giving faithful 4K guest pages / dynamic
paging on any host page size, plus hardware-enforced isolation.

Real: the Hypervisor.framework FFI (from the SDK headers); the micro-VM engine
(VM/vCPU create, guest-RAM + page-table mapping, identity 4K page tables, MMU
sysreg injection, run-loop exit decode, register accessors, per-4K-page
protection); and the memory/register-backed Sandbox trait methods.

Wiring: config/sandbox/api/compiler/error. On aarch64 the default sandbox now
prefers Hypervisor when the feature is enabled (else Generic), gated experimental.
The tests pin Generic/Linux explicitly, so they are unaffected.

Still todo!() (need PolkaVM's VmCtx/ExitReason shared out of the generic module +
compiler-emitted hvc trampolines): run, load_module, prepare_program,
address_table, offset_table, sbrk; plus the KVM backend for aarch64-linux.

Tests: `cargo test -p polkavm` 717 pass; `--features generic-sandbox,
hypervisor-sandbox` 2162 pass; clippy clean. Not yet executed at runtime (needs a
binary signed with com.apple.security.hypervisor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orks)

Builds on the backend scaffold: the Hypervisor.framework sandbox now runs real
PolkaVM programs in a vCPU at a 4K guest granule on a 16K macOS host.

Execution path (hypervisor.rs):
- prepare_program/load_module: copy code into guest RAM (W^X: RW data + RX code
  via separate hv_vm_map, since Apple forbids RWX at stage-2), install the hvc
  trampoline stubs + guest EL1 exception vector, materialize the memory map with
  per-4K-page protections, init the guest VmCtx.
- run/execute: enter via the sysenter stub, decode hvc / data-abort exits into
  InterruptKind mirroring the generic sandbox; offset_table/address_table match
  the shared aarch64 codegen (gas at 0x60).
- Dynamic paging round-trip: a stage-1 fault traps to the guest's own VBAR_EL1
  -> hvc -> host reports Segfault{page_address,page_size}; after the host maps the
  page, run() resumes by re-entering at the faulting instruction start (register
  snapshot + sysenter), honoring host register edits and not re-firing gas/step
  stubs -- same model as generic's handle_guest_pagefault.
- Correct teardown (destroy vCPU before the VM; fixed a vcpu-id-0 sentinel bug),
  and a process-global VM lock + thread-local guard: Apple allows one VM per
  process, so a second simultaneous instance errors cleanly instead of hanging.

Wiring:
- config.rs: default sandbox prefers Hypervisor on macOS+aarch64 (feature on);
  is_supported = macos && aarch64 && feature. Gated experimental.
- compiler/aarch64.rs: vmctx_addr assert accepts Generic | Hypervisor (both put
  the vmctx one page below the memory base -> shared codegen).

Tests (tests.rs) + harness:
- 8 hypervisor smoke tests (add/trap, ret, ecall, sbrk, segfault, static-oob,
  host-calls, dynamic-paging), all passing via a codesigning test runner
  (ci/hypervisor/sign-and-run.sh + hypervisor.entitlements) -- required because
  hv_vm_create needs com.apple.security.hypervisor, which cargo-test binaries lack.
- The existing aarch64_backend corpus routes through the hypervisor under
  POLKAVM_TEST_HYPERVISOR (env-gated, diagnostic only): 13/13 pass.

Mandatory gates unchanged: cargo test -p polkavm = 717; signed both-features
(no env var) = 2170; clippy 0. Tests pin Generic/Linux so the default flip does
not route them through the hypervisor.

Still todo!(): the KVM backend for aarch64-linux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hypervisor sandbox

- jump table via `adrp` (±4 GiB) instead of `adr` (±1 MiB), which panicked
  on any program with >1 MiB of native code; out-of-range fixups are now a
  CompileError, not a panic
- immediates/shifts/rotates/comparisons use the immediate encodings, and
  guest accesses use `[x14, wN, uxtw]`: 1-2 instructions instead of 2-4
- hypervisor: refund gas and preserve registers on mid-run exhaustion,
  invalidate the guest TLB after page-table edits, enforce
  accessible_aux_size, and stop reset_memory revoking static protections
- hypervisor-sandbox no longer needs generic-sandbox to work
- CI: assembler tests actually run, plus aarch64-linux and signed
  hypervisor jobs
Route the whole test corpus through the Hypervisor.framework sandbox via a new
compiler_hypervisor_* lane -- the only lane exercising a real 4K guest page size
on a 16K macOS host.

Fixes found by doing so:

  - Honor the module's page size instead of hardcoding GUEST_PAGE_SIZE. Faults
    are reported at that size; stage-1 descriptors stay 4K, so any multiple works.
  - Recover the guest program counter on plain traps (trap_at): the EL1 vector
    saves nothing, so resolve it from ELR, else from the jump site the
    indirect-jump codegen stashed. program_counter() now reads the VmCtx so the
    guest stubs' own stores are visible.
  - Clear next_program_counter on segfault and gas traps rather than pinning it
    to the faulting PC, which wrongly forced a re-entry target.
  - Start aux data fully accessible, and bound accessible_for by
    accessible_aux_size; also add overflow checks and apply it to protect_memory,
    which previously let absent pages be re-protected.
  - Bound the VM_LOCK wait (5s) instead of blocking forever: the current owner
    may be the caller's own live instance, which never yields.
@kvpanch
kvpanch force-pushed the kvpanch/arm_recompiler branch from dd2e909 to 3644a58 Compare September 10, 2026 12:42
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