AArch64 ELF-to-WebAssembly Browser Runtime #
Engineering Build Plan and Agent Handoff #
Status: Phase 8 complete; Phase 9 next
Primary implementation language: Rust
Initial browser target: Google Chrome
Guest architecture: AArch64, little-endian, Linux userspace
Initial libc/toolchain profile: Static musl binaries
Long-term language targets: C, Python, and Rust
Last reviewed: July 26, 2026
1. Executive summary #
Build a browser runtime that can load ordinary AArch64 Linux ELF executables, emulate the Linux userspace environment they expect, and execute their machine code in Chrome.
The project should not emulate an entire computer or boot a Linux kernel. It should behave more like QEMU user mode:
- Parse an AArch64 ELF executable.
- Construct a synthetic Linux process image.
- Execute AArch64 instructions.
- Intercept Linux system calls.
- Implement those calls against browser-backed services.
- Eventually translate hot AArch64 code into WebAssembly for speed.
- Run language compilers and runtimes as guest executables inside that environment.
The recommended implementation path is:
AArch64 ELF
│
▼
ELF loader and Linux process setup
│
▼
AArch64 decoder and semantics
│
▼
Architecture-neutral P-code / intermediate representation
│
├── Interpreter ───────────────────────────────┐
│ │
└── P-code-to-WebAssembly translator │
│ │
▼ │
dynamically compiled Wasm │
│ │
└───────────────┬───────────────┘
▼
Linux syscall runtime
│
┌────────────────┼─────────────────┐
▼ ▼ ▼
Terminal Virtual filesystem Browser services
OPFS, clocks, random,
fetch, WebSocket, UI
The preferred starting point is to investigate reusing Icicle, a Rust multi-architecture emulator that already has AArch64, a SLEIGH-derived intermediate representation, a P-code interpreter, software memory, ELF/Linux userspace support, and instruction-semantic tests.
Icicle must be treated as a candidate foundation rather than a guaranteed dependency. The first milestone is a time-boxed feasibility spike proving that its useful components can compile for the browser or can be separated from native-host assumptions without effectively rewriting the project.
The expected implementation size for a useful, polished environment is approximately:
- 60,000–100,000 lines of production Rust
- 40,000–80,000 lines of tests and fixtures
- Plus packaged third-party toolchains and runtimes that are not counted as project LOC
A convincing static-binary prototype can be much smaller: approximately 8,000–15,000 net-new Rust LOC when reusing Icicle successfully.
2. Product vision #
The user should eventually be able to open a browser page, write ordinary source code, compile it with a real compiler, and execute the resulting native-style Linux binary locally in Chrome.
Example experience:
main.c
│
▼
AArch64 clang running inside the guest runtime
│
▼
AArch64 Linux ELF
│
▼
ELF loader → AArch64 execution → Linux syscall shim
│
▼
Terminal output in the browser
For Python:
script.py
│
▼
AArch64 CPython guest executable
│
▼
Python imports, filesystem access, stdout, exceptions
│
▼
Browser terminal
For Rust:
Cargo project
│
▼
AArch64 cargo + rustc guest processes
│
▼
LLD emits an AArch64 Linux ELF
│
▼
The runtime executes that ELF
The educational value is that the system preserves and can expose the normally hidden layers:
Source
→ compiler
→ assembly
→ machine instructions
→ ELF
→ loader
→ virtual address space
→ ABI
→ syscalls
→ browser capabilities
→ observable output
The project is not merely a way to run C or Rust in a browser. Those languages can already target WebAssembly directly. The differentiator is teaching and exposing a conventional native executable pipeline.
3. Goals #
3.1 Primary goals #
- Load and execute AArch64 Linux ELF binaries in Chrome.
- Support source code that is not specifically written for the browser.
- Support real language runtimes and compilers rather than browser-specific reimplementations.
- Begin with C and externally compiled Rust binaries.
- Add CPython.
- Add an in-browser Clang/LLD toolchain.
- Eventually add an in-browser Rust/Cargo toolchain.
- Provide an interpreter before implementing dynamic translation.
- Translate frequently executed guest code into WebAssembly.
- Make runtime state observable for educational tooling.
- Reuse permissively licensed libraries wherever they materially reduce risk or implementation scope.
- Maintain a clean boundary between architecture emulation and Linux/browser services.
3.2 Secondary goals #
- Deterministic execution mode for lessons and tests.
- Instruction, syscall, process, memory, and ELF tracing.
- Source-to-assembly and source-to-Wasm correlation.
- Persistent project files and build caches through OPFS.
- A test host that can run outside the browser for fast CI.
- A future path to another guest ISA without rewriting the Linux runtime.
4. Non-goals for the initial implementation #
Do not attempt these in the MVP:
- Booting a Linux kernel.
- Emulating an MMU or hardware page tables.
- Emulating peripherals.
- Full system-mode AArch64.
- AArch32/Thumb support.
- Windows PE or macOS Mach-O binaries.
- Arbitrary existing Linux distribution compatibility.
- SVE, SVE2, SME, MTE, PAC, or other newer optional Arm extensions.
- Kernel modules.
fork()with Unix copy-on-write semantics.- Raw sockets or arbitrary direct TCP/UDP from a webpage.
- Perfect floating-point exception behavior.
- Self-modifying code in the first translator.
- Multi-threaded guest execution in the first release.
- Running untrusted binaries without explicit resource quotas and validation.
- Cross-browser compatibility before the architecture is proven.
5. Key architectural decisions #
Decision 1: AArch64 first #
Use:
ISA state: AArch64 / A64
Endianness: Little-endian
Baseline: Armv8-A
Linux data model: LP64
ABI: AAPCS64 + AArch64 System V/Linux ABI
Required initially:
- Base integer instructions
- Scalar floating point
- Advanced SIMD/NEON subset used by ordinary toolchains
Excluded initially:
- SVE/SVE2
- SME
- Pointer Authentication
- Memory Tagging
- AArch32 and Thumb
AArch64 costs more to implement than RISC-V, particularly because ordinary AArch64 Linux binaries expect FP/SIMD support. The payoff is substantially broader compiler/runtime availability and a larger inventory of prebuilt Linux software.
Decision 2: Linux userspace emulation, not full-machine emulation #
There is no guest kernel.
The runtime directly interprets or translates userspace instructions. An AArch64 svc instruction transfers into the project’s Linux syscall layer.
This avoids:
- Kernel boot support
- Devices
- Interrupt controllers
- Timer hardware
- MMU emulation
- Kernel scheduling
- Kernel filesystems
- Browser-hostile hardware behavior
Decision 3: Interpreter first, translator second #
The interpreter is not disposable scaffolding. It is:
- The semantic reference implementation.
- The debugging and single-step engine.
- A fallback for unimplemented translated operations.
- A way to run unseen or cold blocks.
- The oracle for differential tests.
- The mechanism that gets useful software running before performance optimization.
Decision 4: Use an architecture-neutral IR #
Prefer:
AArch64 decoder
↓
P-code or project IR
├── interpreter
└── Wasm backend
Do not directly couple every AArch64 instruction implementation to Wasm emission. This keeps:
- Semantics testable without a browser.
- The interpreter and translator aligned.
- A future RISC-V backend possible.
- Complex instructions lowerable into smaller operations.
- Architecture-specific decoding separate from runtime policy.
Decision 5: Treat Icicle as the preferred foundation, subject to a feasibility gate #
Icicle currently exposes separate components for:
- Core CPU state and SLEIGH management
- P-code interpretation
- Software virtual memory
- Linux userspace emulation
- AArch64 and other guest architectures
- Instruction semantics testing
- JIT infrastructure
- GDB integration
Do not assume it will compile to wasm32-unknown-unknown unchanged.
The first project phase must determine:
- Which Icicle crates are browser-portable.
- Which crates depend on native threads, OS signals,
mmap, executable memory, filesystem paths, or build-time generated data. - Whether the Linux userspace layer can be adapted behind project-defined host traits.
- Whether its memory model can use a browser/Wasm backend.
- Whether its P-code representation is stable and suitable for a WebAssembly backend.
- Whether carrying a fork is acceptable.
If Icicle is unsuitable, preserve the same architecture and replace only the decoder/IR/interpreter foundation.
Decision 6: Chrome-only is acceptable #
Use currently available Chrome WebAssembly features where helpful:
- Memory64
- JSPI
- Tail calls
- Multiple memories
- Typed function references
- Wasm SIMD and Relaxed SIMD
- Finalized Wasm exception handling
- Threads and shared memory later
Perform runtime feature detection and fail with a clear browser compatibility message.
Decision 7: Start with static musl executables #
Initial executable profile:
Format: ELF64
Machine: EM_AARCH64
Endianness: Little
Linking: Static
libc: musl
Code model: Position independent where practical
Threads: Disabled
Signals: Minimal
Static binaries postpone the dynamic linker, symbol versioning, shared-object loading, and glibc-specific behavior.
Decision 8: Keep browser services behind host traits #
The CPU and Linux runtime should not call browser JavaScript directly throughout the codebase.
Use explicit abstractions such as:
pub trait HostFileSystem {
fn open(&mut self, request: OpenRequest) -> HostFuture<Result<HostFd>>;
fn read(&mut self, fd: HostFd, offset: u64, dst: GuestSlice)
-> HostFuture<Result<usize>>;
fn write(&mut self, fd: HostFd, offset: u64, src: GuestSlice)
-> HostFuture<Result<usize>>;
}
pub trait HostClock {
fn monotonic_now(&self) -> Timespec;
fn realtime_now(&self) -> Timespec;
fn sleep(&mut self, duration: Duration) -> HostFuture<Result<()>>;
}
pub trait HostTerminal {
fn write(&mut self, stream: TerminalStream, bytes: &[u8]);
fn read(&mut self, max_len: usize) -> HostFuture<Result<Vec<u8>>>;
}
There should eventually be:
- A browser host.
- A native Rust test host.
- A deterministic host.
- Possibly a Node host.
6. Proposed workspace structure #
workspace/
├── Cargo.toml
├── crates/
│ ├── runtime-core/
│ │ ├── guest process state
│ │ ├── errors and traps
│ │ ├── handles and IDs
│ │ └── architecture-neutral runtime interfaces
│ │
│ ├── arch-aarch64/
│ │ ├── architectural register state
│ │ ├── feature profile
│ │ ├── decoder integration
│ │ ├── instruction semantics integration
│ │ ├── syscall ABI extraction
│ │ └── signal/register context definitions
│ │
│ ├── ir/
│ │ ├── normalized P-code/project IR
│ │ ├── basic blocks
│ │ ├── control-flow metadata
│ │ ├── value types
│ │ └── verification
│ │
│ ├── interpreter/
│ │ ├── IR interpreter
│ │ ├── tracing
│ │ ├── breakpoints
│ │ └── deterministic stepping
│ │
│ ├── wasm-backend/
│ │ ├── P-code/IR lowering
│ │ ├── Wasm module construction
│ │ ├── register caching
│ │ ├── memory operations
│ │ ├── SIMD lowering
│ │ ├── block dispatch
│ │ ├── module batching
│ │ └── validation
│ │
│ ├── translation-cache/
│ │ ├── hot-block counters
│ │ ├── cache keys
│ │ ├── invalidation
│ │ ├── OPFS persistence
│ │ └── compiled-module installation
│ │
│ ├── guest-memory/
│ │ ├── region map
│ │ ├── address validation
│ │ ├── permissions
│ │ ├── stack/heap/mmap allocation
│ │ ├── dirty pages
│ │ └── code invalidation
│ │
│ ├── elf-loader/
│ │ ├── ELF validation
│ │ ├── PT_LOAD mapping
│ │ ├── relocations
│ │ ├── static PIE
│ │ ├── argv/envp/auxv
│ │ ├── TLS
│ │ └── dynamic-linker support later
│ │
│ ├── linux-abi/
│ │ ├── syscall numbers and argument decoding
│ │ ├── Linux data structures
│ │ ├── errno
│ │ ├── signals
│ │ ├── futex definitions
│ │ └── ioctl constants
│ │
│ ├── linux-runtime/
│ │ ├── syscall dispatch
│ │ ├── process table
│ │ ├── file descriptor table
│ │ ├── exec
│ │ ├── pipes
│ │ ├── clocks
│ │ ├── signals
│ │ └── resource limits
│ │
│ ├── vfs/
│ │ ├── inode and mount model
│ │ ├── in-memory filesystem
│ │ ├── procfs
│ │ ├── devfs
│ │ ├── path resolution
│ │ └── permissions
│ │
│ ├── host-api/
│ │ ├── filesystem trait
│ │ ├── terminal trait
│ │ ├── clock trait
│ │ ├── randomness trait
│ │ ├── networking trait
│ │ └── async suspension abstraction
│ │
│ ├── host-browser/
│ │ ├── wasm-bindgen boundary
│ │ ├── JSPI wrappers
│ │ ├── OPFS
│ │ ├── Worker messaging
│ │ ├── browser clocks/randomness
│ │ └── fetch/WebSocket adapters
│ │
│ ├── host-native/
│ │ ├── native filesystem
│ │ ├── deterministic test implementation
│ │ └── CLI runner
│ │
│ ├── toolchain/
│ │ ├── toolchain manifests
│ │ ├── package extraction
│ │ ├── process orchestration
│ │ ├── build cache
│ │ └── compiler diagnostic transport
│ │
│ ├── debug-protocol/
│ │ ├── register snapshots
│ │ ├── memory reads
│ │ ├── stepping and breakpoints
│ │ ├── syscall events
│ │ └── trace serialization
│ │
│ └── browser-api/
│ ├── public Wasm API
│ ├── process lifecycle
│ ├── project filesystem API
│ └── events exposed to the UI
│
├── web/
│ ├── Worker bootstrap
│ ├── feature detection
│ ├── terminal UI
│ ├── editor integration
│ └── debugging/learning UI
│
├── test-programs/
│ ├── assembly/
│ ├── c/
│ ├── rust/
│ ├── python/
│ └── syscall-probes/
│
├── toolchains/
│ ├── manifests only
│ └── scripts for producing distributable toolchain images
│
├── scripts/
│ ├── build browser runtime
│ ├── compile test corpus
│ ├── run differential tests
│ └── produce coverage reports
│
└── docs/
├── architecture.md
├── compatibility.md
├── syscall-status.md
├── instruction-status.md
├── toolchain-builds.md
└── licensing.md
Do not create every crate immediately. Begin with a smaller workspace and split crates when dependency boundaries become useful.
7. Candidate dependencies #
All dependency choices must be verified at implementation time for maintenance health, browser compatibility, license, feature coverage, and minimum supported Rust version.
7.1 Preferred foundation #
Icicle #
Potentially reuse:
icicle-cpuicicle-vmicicle-memicicle-linuxsleigh- Instruction-semantic test infrastructure
Likely not directly reusable in the browser:
- Native JIT backend that assumes executable host memory
- Native process integration
- Native signal machinery
- Host filesystem assumptions
- Fuzzing-specific application entrypoints
Required spike outputs:
- Dependency graph showing browser-incompatible crates.
- A minimal AArch64 instruction decoded and interpreted in a
wasm32build. - A determination of upstream contribution versus maintained fork.
- A design for adapting or replacing Icicle memory.
- A design for adapting or replacing Icicle Linux host calls.
- A decision record: adopt, partially adopt, or reject.
7.2 ELF parsing #
Preferred initial choice:
object
Alternative:
elf
The parser should handle format parsing only. The project remains responsible for process loading semantics, memory placement, relocations, TLS, auxv, and dynamic linking.
7.3 WebAssembly emission and validation #
Candidates:
wasm-encoderwasmparserwasm-smithfor generated/fuzz inputswasmprinterfor readable diagnosticswatfor small test modules
The translator should emit binary Wasm directly. Text WAT is appropriate for debugging and golden tests, not the production translation path.
7.4 Browser boundary #
Candidates:
wasm-bindgenjs-sysweb-sys- Small handwritten JavaScript/TypeScript glue for JSPI and Worker setup
Keep the JavaScript boundary narrow and explicit.
7.5 Error handling and observability #
Candidates:
thiserrortracingserdepostcardor another compact serialization format for debug eventsbitflagssmallvecindexmapslotmapor generational IDs where helpful
Avoid pulling a large async runtime into the browser core unless it clearly solves more than it complicates.
8. Execution architecture #
8.1 CPU state #
A conceptual AArch64 state:
pub struct Aarch64State {
pub x: [u64; 31],
pub sp: u64,
pub pc: u64,
// Shared scalar-FP and SIMD register storage.
pub v: [u128; 32],
pub nzcv: Nzcv,
pub fpcr: u32,
pub fpsr: u32,
pub thread_pointer: u64,
pub exclusive: ExclusiveMonitor,
}
Important details:
- Register 31 is contextually the zero register or stack pointer.
- Writes to
Wnzero the upper half ofXn. XZR/WZRdiscard writes and read as zero.- FP and SIMD values share the
V0–V31register file. NZCVmay be lazily materialized in translated code.- The exclusive monitor must initially behave correctly for one thread and later scale to shared memory.
8.2 Execution tiers #
Tier 0: IR interpreter #
Always available.
Use for:
- Cold code.
- Debugging.
- Unsupported translated operations.
- Single stepping.
- Validation.
- Differential testing.
Tier 1: Wasm basic-block compiler #
Compile hot guest blocks or regions into Wasm.
A block should use a uniform callable shape, such as:
run_block(cpu_state_ptr, runtime_ptr) -> dispatch_result
Possible dispatch results:
pub enum DispatchResult {
Continue { next_pc: u64 },
Syscall,
Trap(TrapKind),
Yield,
Exit(i32),
Recompile { pc: u64 },
}
Tier 2: Region compiler, later #
Combine multiple blocks into larger compilation units to reduce dispatch overhead and enable:
- Register residency across blocks.
- Direct branch lowering.
- Constant propagation.
- Dead-write elimination.
- Lazy flag optimization.
- Better Wasm validation/compilation amortization.
Do not implement Tier 2 before Tier 1 has reliable differential tests.
8.3 Translation cache #
Suggested cache key:
SHA-256(
executable identity
+ mapped code-page bytes
+ guest virtual address
+ guest ISA feature profile
+ translator version
+ runtime ABI version
+ Wasm feature profile
)
The cache must invalidate when:
- Executable memory changes.
mprotectchanges a region into executable memory.- A process performs dynamic relocation into code.
- The translator or runtime ABI changes.
- A cached module fails validation under the current browser.
Persist compiled module bytes where useful, but assume instantiated modules and function objects are session-local.
9. Guest memory model #
Wasm Memory64 is useful because guest addresses and Wasm addresses can both use i64. It does not provide a Linux virtual memory system by itself.
9.1 Initial compact layout #
Example only:
0x0000_0000_0000 null guard
0x0000_0001_0000 executable mappings
0x0000_1000_0000 shared objects
0x0000_2000_0000 brk/heap
0x0000_4000_0000 mmap arena
0x0000_7f00_0000 stack region
Use a compact synthetic address space. Do not reproduce typical native Linux high-half addresses unless required by a specific binary.
9.2 Region metadata #
Maintain software metadata:
pub struct MemoryRegion {
pub start: u64,
pub end: u64,
pub permissions: Permissions,
pub kind: RegionKind,
pub backing: Backing,
pub executable_generation: u64,
}
Every memory operation must have a defined strategy:
- Fast translated accesses when the address is known valid.
- Checked accesses where faults are possible.
- Cross-region and unaligned accesses.
- Execute-permission checks on block fetch.
- Guard pages.
- Dirty tracking.
- Code invalidation.
9.3 Multiple memories #
A possible later layout:
- Memory 0: guest Memory64 address space
- Memory 1: runtime metadata in Memory32
- Memory 2: shared communication buffers
Do not require multiple memories until a concrete design demonstrates that it reduces complexity.
10. ELF and process loading #
10.1 Initial loader #
Support:
- ELF64
- Little-endian
EM_AARCH64ET_EXEC- Static
ET_DYN/PIE when practical PT_LOADPT_TLSwhen first needed- Common static relocations
- GNU stack flags
- Build ID extraction for cache identity
Reject clearly:
- Unsupported endianness
- AArch32
- Required SVE/SVE2/SME features
- Unsupported dynamic interpreter in early phases
- Overlapping or invalid segments
- Guest addresses outside configured limits
- Writable and executable mappings when policy forbids them
10.2 Initial process stack #
Construct:
argc
argv[]
NULL
envp[]
NULL
auxv entries
NULL terminator
strings
random bytes
alignment padding
Initial auxiliary-vector support should include at least:
AT_NULLAT_PHDRAT_PHENTAT_PHNUMAT_PAGESZAT_BASEAT_FLAGSAT_ENTRYAT_UIDAT_EUIDAT_GIDAT_EGIDAT_SECUREAT_RANDOMAT_EXECFNAT_HWCAPAT_HWCAP2
Report hardware capabilities conservatively. Never advertise an ISA extension unless both interpreter and translator implement it correctly.
10.3 Dynamic linking #
Defer until static programs, CPython, and the syscall runtime are stable.
Preferred path:
- Load the real AArch64 musl dynamic linker as a guest.
- Implement enough ELF/process behavior for the guest dynamic linker to do its own work.
- Avoid writing a custom full dynamic linker unless unavoidable.
The runtime still must:
- Map
PT_INTERP. - Establish
AT_BASE. - Map shared objects requested by the dynamic linker.
- Implement file and memory calls it uses.
- Support TLS.
- Handle executable-memory invalidation after relocation.
11. Linux syscall roadmap #
11.1 Phase A: static hello-world programs #
Implement approximately:
exit
exit_group
read
write
writev
close
brk
mmap
munmap
mprotect
clock_gettime
getrandom
uname
set_tid_address
set_robust_list
rt_sigaction minimal/stub as allowed
rt_sigprocmask minimal/stub as allowed
prlimit64
Exact calls depend on the chosen musl startup objects and compiler output. Build syscall-probe binaries and trace real execution rather than guessing.
11.2 Phase B: usable filesystem and CPython #
Add:
openat
newfstatat
fstat
lseek
readlinkat
getdents64
mkdirat
unlinkat
renameat/renameat2
faccessat/faccessat2
ftruncate
fsync
fdatasync
dup
dup3
fcntl
ioctl terminal subset
getcwd
chdir
fchdir
statx if required
clock_nanosleep
nanosleep
gettimeofday
times
sysinfo
11.3 Phase C: compilers and child processes #
Add:
execve
wait4
waitid
pipe2
clone/clone3 constrained process/thread modes
getpid
getppid
gettid
kill/tgkill
eventfd2 if toolchains require it
poll/ppoll
select/pselect6
epoll_create1
epoll_ctl
epoll_pwait
sched_yield
futex
Avoid claiming fork() semantics early. A practical process model may initially support spawn/exec without efficient copy-on-write.
11.4 Phase D: networking abstraction #
Browser limitations require an explicit compatibility policy.
Potential guest API mapping:
- DNS through a controlled resolver service.
- HTTP/HTTPS through
fetch. - Stream sockets through a remote WebSocket/WebTransport TCP proxy.
- Local loopback sockets inside the runtime.
- Unix-domain sockets inside the virtual environment.
- Unsupported raw sockets and arbitrary UDP with clear errors.
Do not silently pretend that browser fetch is a POSIX socket.
12. Browser host architecture #
12.1 Worker model #
Run guest execution in a dedicated Web Worker.
Benefits:
- Prevents guest computation from blocking UI.
- Enables synchronous OPFS access APIs available to workers where applicable.
- Provides a natural process-control boundary.
- Makes termination possible by stopping the worker.
- Positions the project for one-worker-per-guest-thread later.
12.2 JSPI #
Use JSPI for operations that genuinely need browser promises:
- Terminal input
- Network requests
- Permission prompts
- Toolchain package downloads
- Some OPFS operations
- UI-mediated debugging stops
The guest may issue a synchronous-looking Linux call while Wasm execution suspends and resumes after the Promise settles.
Hide JSPI behind a project abstraction so native tests do not depend on browser APIs.
12.3 OPFS #
Use OPFS for:
- User project files
- Installed toolchain images
- Compiler caches
- Translated Wasm cache
- Python standard library and packages
- Rust registry and build artifacts
- Persistent virtual disk snapshots
Use an in-memory layer for:
/tmp- Pipes
- Synthetic
/proc - Synthetic
/dev - Ephemeral files
- Test fixtures
12.4 Resource control #
Every guest should have configurable limits:
- Maximum linear memory
- Maximum virtual mappings
- CPU instruction or block budget
- Wall-clock timeout
- Maximum output
- Maximum number of open descriptors
- Maximum process count
- Maximum filesystem size
- Maximum translated-code cache
- Maximum toolchain download size
An unbounded compiler or binary must not be able to freeze the browser indefinitely.
13. AArch64 implementation roadmap #
13.1 Instruction groups for the first static programs #
The current static musl fixture covers unconditional calls/returns, conditional and internal P-code branches, compare-and-branch and test-bit-and-branch forms, integer and logical operations, shifts, move-wide immediates, address generation, byte and pair loads/stores with writeback, stack-pointer handling, TPIDR_EL0, SVC, NOP, and the architectural zero register. The remaining groups below will continue to be implemented from observed compiler output rather than as an untested bulk expansion.
- Unconditional branches and calls
- Conditional branches
- Compare-and-branch
- Test-bit-and-branch
- Integer arithmetic
- Logical instructions
- Shift and rotate
- Move-wide immediates
- Address generation
- Basic loads and stores
- Load/store pairs
- Stack-pointer handling
SVC- Common system-register reads needed by userspace
NOPand architectural hints
13.2 Required for realistic C/Rust binaries #
- Multiply/divide
- Bitfield instructions
- Count/reverse instructions
- Conditional selects
- Flag-setting arithmetic
- Carry arithmetic
- Scalar FP operations
- FP/int conversion
- Core SIMD moves, loads, stores, and lane operations
- Common vector integer operations
- Common vector floating-point operations
- Exclusive loads/stores
- Acquire/release operations
- TLS-related system-register behavior
13.3 Later compatibility groups #
- LSE atomics
- Crypto instructions
- More complete saturating/narrowing SIMD
- Complex vector permutations
- Strict FP status behavior
- Signal-frame behavior
- Optional branch-protection instructions as no-ops or enforced semantics where safe
13.4 Deliberately excluded initially #
- SVE
- SVE2
- SME
- MTE
- PAC
- AArch32
- Privileged instructions
- EL1/EL2/EL3 behavior
- MMU and system control
14. Wasm backend strategy #
14.1 Lower IR, not AArch64 directly #
Each AArch64 instruction may lower to multiple IR operations. The Wasm backend only needs to understand normalized operations such as:
integer add/subtract
bitwise operations
shifts and rotates
integer comparisons
flag calculation helpers
loads and stores
vector operations
control-flow edges
syscall/trap exits
register reads/writes
14.2 Register caching #
Within a compiled region:
- Keep frequently accessed guest registers in Wasm locals.
- Track dirty registers.
- Materialize state before syscalls, traps, interpreter fallback, or debugger suspension.
- Avoid writing all architectural state after every guest instruction.
14.3 Flags #
Use lazy NZCV representation where practical:
pub enum LazyFlags {
Materialized(Nzcv),
Add32 { lhs: u32, rhs: u32, result: u32 },
Add64 { lhs: u64, rhs: u64, result: u64 },
Sub32 { lhs: u32, rhs: u32, result: u32 },
Sub64 { lhs: u64, rhs: u64, result: u64 },
Logical32 { result: u32 },
Logical64 { result: u64 },
}
Only compute the requested condition when a consumer needs it.
14.4 SIMD #
Use Wasm v128 for AArch64 Vn registers where possible.
Some instructions can map directly. Others require:
- Multiple Wasm SIMD operations
- Scalar lane extraction
- Helper calls
- Bit-level fallback
- Strict versus relaxed semantic modes
Maintain a compatibility table per instruction and lane arrangement.
14.5 Dynamic module construction #
Do not emit one Wasm module per basic block.
Batch by:
- Hot region
- Executable page
- Function-like region when discoverable
- Fixed block count
- Fixed emitted-byte budget
Expose metrics:
- Guest blocks per module
- Wasm bytes emitted
- Browser compile time
- Cache hit rate
- Execution tier transitions
- Time spent interpreting versus translated
15. Testing strategy #
Testing is the project’s primary correctness mechanism. LOC estimates should reserve nearly as much effort for tests as production implementation.
15.1 Unit tests #
Test:
- Arithmetic edge cases
NZCV- Register 31 behavior
- 32-bit register zero extension
- Addressing modes
- Pair load/store writeback
- Unaligned memory
- FP conversions
- SIMD lane layouts
- Exclusive monitor behavior
- ELF validation
- Stack and
auxvconstruction - Path resolution
- Syscall structure serialization
15.2 Differential instruction testing #
For each generated instruction case:
- Initialize registers and memory.
- Execute on a trusted reference.
- Execute in the project interpreter.
- Execute through translated Wasm.
- Compare:
- Registers
- FP/SIMD registers
- Flags
- Memory changes
- Traps
- Next PC
Possible references:
- QEMU user mode in test infrastructure
- Native AArch64 CI hardware
- Icicle’s existing semantics tests
- Another independently implemented emulator
Avoid using a single reference as the only oracle for all tests.
15.3 Binary corpus #
Maintain small, inspectable programs:
- Handwritten assembly per instruction family
- C programs per syscall
- Rust
no_stdprograms - musl static programs
- CPython startup probes
- Clang/LLD invocation probes
- Dynamic-linker probes
- Thread/futex probes later
Every compatibility regression should gain a minimized fixture.
15.4 Fuzzing #
Fuzz:
- ELF parsing and loader validation
- IR verifier
- AArch64 decoder integration
- Interpreter versus translator
- Syscall argument decoding
- Path traversal
- Filesystem operations
- Wasm emission and validation
- Translation-cache invalidation
Use browser-independent native builds for high-throughput fuzzing.
15.5 Browser integration tests #
Use automated Chrome tests for:
- Feature detection
- Worker startup
- Memory64 module instantiation
- JSPI suspension/resume
- OPFS persistence
- Program cancellation
- Toolchain installation
- Terminal input/output
- Cache reuse
- Resource-limit termination
16. Milestone plan #
Phase 0: Feasibility and dependency spike #
Target production LOC: 1,000–3,000
Purpose: Eliminate foundational uncertainty before building product infrastructure.
Deliverables:
- Rust workspace and CI.
- Icicle dependency/fork experiment.
- Minimal AArch64 instruction decode in native Rust.
- Minimal AArch64 instruction execution through Icicle/P-code.
- Browser build experiment.
- Memory64 feature-detection page.
- JSPI proof of concept.
wasm-encodermodule emitted and compiled dynamically.- Architecture decision record.
Acceptance criteria:
- An agent can state precisely which Icicle components are reusable.
- A small
wasm32artifact can execute at least one AArch64 semantic operation. - A generated Wasm module can be compiled and called in Chrome.
- No hidden GPL dependency has entered the intended permissive core.
- The project has a documented fallback plan if Icicle is rejected.
Fallback foundation if Icicle fails:
- A permissive AArch64 decoder.
- A small project-owned normalized IR.
- Handwritten semantics added incrementally.
- Reference testing against QEMU/native AArch64.
Phase 1: Native-host interpreter for static ELF #
Cumulative production LOC: 8,000–15,000
Deliverables:
- AArch64 register model.
- ELF parser and static loader.
- Compact virtual address space.
- Interpreter.
- Initial Linux syscall layer.
- Native CLI host.
- Static
hello world. - Exit-code propagation.
- Instruction/syscall trace.
- External cross-compilation scripts.
Acceptance criteria:
clang --target=aarch64-linux-musl -static hello.c -o hello
project-run hello
prints expected output and exits correctly.
Required tests:
- Invalid ELF corpus.
argc/argv/envp.- Static C arithmetic and control flow.
- File-free Rust binary.
- Differential instruction cases.
Phase 2: Browser interpreter #
Cumulative production LOC: 15,000–25,000
Deliverables:
- Worker-hosted runtime.
- Browser host traits.
- Memory64 integration or a documented interim memory design.
- Terminal output.
- Program start/stop.
- Browser resource limits.
- In-memory filesystem.
- Chrome feature detection.
Acceptance criteria:
- The same static C and Rust fixtures run in Chrome.
- An infinite loop can be terminated.
- Errors are surfaced as structured diagnostics.
- Native and browser hosts pass the same semantic tests.
Phase 3: Persistent filesystem and CPython prerequisites #
Cumulative production LOC: 25,000–40,000
Deliverables:
- VFS.
- OPFS persistence.
/tmp,/dev, and minimal/proc.- Expanded file syscalls.
- Terminal input.
- Clock and randomness.
- JSPI suspension model.
- Toolchain/package image installer.
- Snapshot/import/export format.
Acceptance criteria:
- A guest program can create, modify, list, rename, and delete files.
- Files survive browser reload.
- Blocking terminal input resumes correctly.
- A minimal or statically linked CPython build starts and runs scripts.
Phase 4: P-code/IR-to-Wasm translation #
Cumulative production LOC: 35,000–55,000
Deliverables:
- Hot-block profiler.
- Basic-block Wasm lowering.
- Dynamic module compilation.
- Function-table dispatch.
- Interpreter fallback.
- Translation cache.
- Differential interpreter/translator suite.
- Translation metrics.
Acceptance criteria:
- Every translated test also passes in the interpreter.
- Hot loops execute from generated Wasm.
- Unsupported operations fall back safely.
- Cache invalidation is tested.
- Common integer benchmarks materially outperform interpretation.
Phase 5: Robust CPython environment #
Cumulative production LOC: 40,000–65,000
Deliverables:
- CPython runtime image.
- Python standard library.
- Import behavior.
- Filesystem-backed modules.
- Exceptions and tracebacks.
- Common pure-Python package installation.
- Compatibility documentation.
Acceptance criteria:
- Run multi-file Python programs.
- Use representative standard-library modules.
- Install and import a pure-Python package.
- Preserve project/package files in OPFS.
Native extension modules are out of scope until dynamic linking is stable.
Phase 6: In-browser C compilation #
Cumulative production LOC: 50,000–75,000
Deliverables:
- AArch64 Clang.
- LLD.
- musl sysroot.
- Headers and startup objects.
- Child process orchestration.
- Pipes and compiler diagnostics.
- Build cancellation.
- Build cache.
Acceptance criteria:
- User edits C source.
- Browser-local Clang compiles it.
- Browser-local LLD emits an ELF.
- Runtime executes the ELF.
- Compiler errors point to source locations.
Prefer LLVM/LLD over GNU Binutils for initial distribution and licensing simplicity.
Phase 7: Dynamic ELF support #
Cumulative production LOC: 60,000–85,000
Deliverables:
PT_INTERP.- Guest musl dynamic linker.
- Shared-object filesystem layout.
- TLS.
- Dynamic relocation/code invalidation.
dlopen/dlsymcompatibility as practical.- Shared-library cache and diagnostics.
Acceptance criteria:
- Dynamically linked small C programs run.
- CPython can use selected native extension modules.
- Shared objects are loaded by the guest dynamic linker rather than a project-specific linker.
Phase 8: Browser-local Rust and Cargo #
Cumulative production LOC: 70,000–110,000
Deliverables:
- AArch64 Rust host toolchain.
- Cargo.
- Rust standard libraries.
- Registry/source cache.
- Build-script process support.
- More complete process, pipe, filesystem, and futex behavior.
- Incremental build persistence.
- Crate compatibility policy.
Acceptance criteria:
- Compile and execute a no-dependency Rust program.
- Compile and execute a small dependency-based Cargo project.
- Cache dependencies and incremental artifacts.
- Cancel a build without corrupting the workspace.
- Provide clear errors for unsupported build scripts or native dependencies.
Phase 9: Educational tooling #
This can begin earlier but should not block runtime correctness.
Deliverables:
- ELF segment viewer.
- Initial stack/
auxvviewer. - Register and flag inspector.
- Instruction stepper.
- Syscall timeline.
- Guest virtual memory map.
- Source/assembly correlation through DWARF.
- AArch64 instruction → IR → Wasm view.
- Performance tier visualization.
Phase 10: Threads and broader compatibility #
Deliverables:
- Shared guest memory.
- Worker-per-thread or scheduler design.
clone/clone3.futex.- TLS per thread.
- Signals and thread interruption.
- Atomics and exclusive-monitor correctness.
- Cross-origin isolation deployment requirements.
- Thread/process resource quotas.
Do not begin this phase until single-threaded compilers and runtimes are stable.
17. LOC and effort model #
These are engineering estimates, not commitments.
| Subsystem | Likely net-new Rust LOC |
|---|---|
| Icicle port/integration or replacement glue | 6k–12k |
| IR/P-code-to-Wasm backend | 10k–20k |
| Guest memory and mappings | 4k–8k |
| ELF/process loader | 4k–8k |
| Linux syscall/process runtime | 15k–30k |
| VFS, OPFS, pipes, terminal | 8k–16k |
| Browser host and Worker integration | 4k–8k |
| Toolchain orchestration | 6k–14k |
| Debug/education protocol | 5k–12k |
| Production total | 60k–100k typical |
| Tests and fixtures | 40k–80k typical |
Potential scope bands:
| Product level | Production LOC |
|---|---|
| Static ELF proof of concept | 8k–15k |
| Useful interpreter-only learning MVP | 20k–40k |
| CPython + browser filesystem | 35k–60k |
| C/Python with Wasm translation | 45k–75k |
| Rust/C/Python with local toolchains | 60k–100k |
| Broad Linux compatibility | 120k–200k+ |
Reuse reduces source volume but does not proportionally reduce validation work.
18. Performance expectations #
Do not set aggressive benchmark targets before the interpreter and translator are measurable.
Initial performance goals:
- Interpreter startup under a perceptible UI threshold for small binaries.
- Dynamic Wasm compilation performed in batches.
- Translation only after a configurable hotness threshold.
- No UI-thread guest execution.
- Persistent caching for repeated lessons/builds.
- Instrumentation that can be disabled in performance mode.
Metrics to collect from the beginning:
- Guest instructions executed
- Blocks interpreted
- Blocks translated
- Interpreter time
- Translation time
- Browser Wasm compile time
- Cache hits/misses
- Syscall counts and latency
- OPFS read/write latency
- Peak guest memory
- Peak runtime metadata memory
- Process count
- Terminal output volume
Avoid performance work that compromises the interpreter’s role as a correctness oracle.
19. Security and isolation #
WebAssembly and the browser sandbox provide important containment, but the runtime still processes attacker-controlled binary formats and executes untrusted guest logic.
Required protections:
- Strict ELF bounds checking.
- Checked integer arithmetic in loaders.
- Memory-region validation.
- Resource limits.
- Controlled network access.
- No access to browser credentials or arbitrary origin data.
- No direct JavaScript object exposure to the guest.
- Explicit virtual filesystem roots.
- Safe cancellation.
- Fuzzed parsers.
- Cache keying that prevents code confusion across binaries.
- No
unsafewithout documented invariants and targeted tests. - Content Security Policy compatible with Wasm compilation.
- Clear threat model before public arbitrary-binary upload.
Guest mprotect(PROT_EXEC) and writable-executable mappings should be restricted or carefully tracked.
20. Licensing plan #
This is an engineering checklist, not legal advice.
Project code #
Preferred project license:
- Apache-2.0, or
- MIT OR Apache-2.0
Apache-2.0 offers an explicit patent grant and is a sensible default for low-level runtime work.
AArch64 specifications #
- Implement behavior from publicly available specifications.
- Do not copy large amounts of Arm prose, diagrams, or pseudocode into the source tree.
- Use “AArch64-compatible” descriptively.
- Do not use Arm logos or imply endorsement.
- Track specification versions in
docs/licensing.md.
Arm’s public ABI repository currently publishes its ABI documents under CC BY-SA 4.0 with an additional patent license. Architecture-reference-manual terms should be reviewed separately.
Third-party code #
Before adoption, record:
- License
- Copyright notice requirements
- Patent clauses
- Transitive dependencies
- Browser compatibility
- Whether code was copied, modified, linked, or used only as a reference
Avoid copying QEMU implementation code into a permissively licensed core unless the project intentionally chooses GPL obligations.
Toolchains #
Maintain a distribution manifest for:
- LLVM/Clang
- LLD
- musl
- CPython
- Rust
- Cargo
- Standard libraries
- Bundled packages
Include licenses and source-offer obligations where applicable.
21. Major risks and mitigations #
Risk: Icicle is too coupled to native execution #
Mitigation:
- Time-box Phase 0.
- Adopt crates selectively.
- Preserve project-defined CPU, memory, and host interfaces.
- Be prepared to retain SLEIGH/P-code while replacing Linux/memory layers.
- Reject the dependency rather than designing the entire product around a brittle fork.
Risk: P-code-to-Wasm performs poorly #
Mitigation:
- Keep interpreter.
- Batch modules.
- Add a normalized project IR between P-code and Wasm.
- Compile hot regions, not individual instructions.
- Cache register values.
- Measure browser compile time separately from execution time.
Risk: Memory64 does not model Linux virtual memory well enough #
Mitigation:
- Use compact synthetic addresses.
- Maintain software page/region metadata.
- Refuse binaries requiring incompatible address layouts.
- Add address translation only if direct guest-to-linear mapping becomes untenable.
Risk: AArch64 SIMD scope expands uncontrollably #
Mitigation:
- Define and report a conservative virtual CPU.
- Build instruction coverage from observed compiler/runtime traces.
- Use Wasm
v128. - Reject unsupported feature requirements.
- Maintain strict versus fast FP/SIMD modes if needed.
Risk: Rust/Cargo requires too much process/thread behavior #
Mitigation:
- Ship C and Python first.
- Start Rust with no-dependency programs.
- Add process and futex behavior based on traces.
- Consider a temporary server-side compiler while keeping local ELF execution.
- Publish a crate compatibility profile.
Risk: Browser async behavior leaks throughout the runtime #
Mitigation:
- Centralize suspension behind host traits.
- Use JSPI only at explicit host boundaries.
- Test all core semantics with a synchronous native host.
- Avoid making the CPU interpreter itself async.
Risk: Toolchain downloads are too large #
Mitigation:
- Compress and chunk images.
- Install languages independently.
- Use OPFS deduplication/versioning.
- Provide minimal teaching-oriented sysroots.
- Load optional components on demand.
Risk: The project becomes a generic Linux emulator #
Mitigation:
- Prioritize language-learning workflows.
- Keep a written compatibility contract.
- Add syscalls based on target toolchain needs.
- Explicitly reject features unrelated to educational use.
22. Open design questions #
Resolve these through prototypes, not prolonged speculation:
- Can useful Icicle crates compile to browser Wasm with modest changes?
- Should Icicle’s P-code become the permanent IR, or should it lower into a smaller project-owned IR?
- Can guest addresses map directly into Memory64 efficiently?
- How should translated modules access mutable CPU state?
- What block/region batching strategy minimizes Chrome compile overhead?
- Does JSPI compose cleanly with dynamically generated modules and the chosen Wasm binding approach?
- Should the VFS be fully Rust-owned or split between Rust and OPFS JavaScript?
- Can a static CPython build satisfy the initial Python milestone?
- Which AArch64 compiler flags minimize unsupported instruction use without creating unrealistic binaries?
- How much Advanced SIMD does musl and CPython exercise at startup?
- Should dynamic linking use musl exclusively before any glibc work?
- What guest process model can support compiler subprocesses before true
fork? - Can the Rust compiler host execute acceptably under interpretation plus translated hot blocks?
- What parts of DWARF need to be parsed for the initial educational UI?
- What is the minimum safe public threat model for user-uploaded ELF files?
23. Immediate implementation backlog #
The next agent should begin here.
Repository and CI #
- Create the Cargo workspace.
- Add native Linux CI.
- Add
wasm32-unknown-unknownbuild CI. - Add headless Chrome integration-test infrastructure.
- Add formatting, Clippy, dependency-license, and audit checks.
- Create
docs/decisions/.
Icicle feasibility #
- Pin the current Icicle revision in an experiment branch.
- Generate its Cargo dependency graph.
- Identify native-only dependencies and
cfgboundaries. - Build the smallest useful SLEIGH/AArch64 decoder executable.
- Build the smallest P-code interpreter test.
- Attempt a browser-target build.
- Record required patches by crate.
- Decide whether memory and Linux components should be reused.
- Write ADR-0001: Icicle adoption decision.
Browser feature probes #
- Create a page that detects Memory64, JSPI, tail calls, SIMD, multiple memories, typed function references, and exception handling.
- Emit a minimal Memory64 module using
wasm-encoder. - Dynamically compile and instantiate it in a Worker.
- Prove a Promise-bearing host call can suspend and resume Wasm through JSPI.
- Capture exact minimum Chrome behavior and failure diagnostics.
Static ELF spike #
- Cross-compile a tiny AArch64 static assembly program.
- Parse it with
object. - List and validate
PT_LOADsegments. - Construct a synthetic address space in native Rust.
- Load segments and set initial PC/SP.
- Execute until
svc. - Implement
writeandexit. - Print
hello world. - Repeat in the browser.
- Add a regression test and trace fixture.
Items 21–30 are complete. The same generated static assembly ELF fixture executes through the project-owned loader, SLEIGH interpreter, Linux runtime, and terminal host in both the native CLI and a Chromium Worker; the browser regression verifies its output, exit status, resource counters, and syscall trace.
The next compiler checkpoint is also complete: Zig 0.16.0 produces a deterministic freestanding static AArch64 C ELF from guest-tests/compiler-hello/main.c, and the same checked-in binary runs in the native runtime and Chromium Worker. Its emitted success path added stack memory, comparison flags, conditional control flow, address generation, and zero-register semantics. Because this freestanding fixture still dispatches only write and exit, no speculative syscalls were added. The next step is to trace a libc-linked static fixture and implement only the additional instructions and startup syscalls it demonstrates.
The static musl checkpoint is complete as well: guest-tests/libc-hello/main.c enters through musl _start, initializes libc and static TLS, calls main, writes through libc, and exits through exit_group in both native and Chromium hosts. Its observed path added call/return, internal P-code control flow, pair/byte memory operations, register extension and slicing, logical/shift operations, and TPIDR_EL0. The only new syscall is set_tid_address; musl's built-in TLS storage means this fixture does not reach mmap or brk. The next Phase 1 fixture should be a file-free static Rust binary.
The file-free Rust checkpoint is complete: guest-tests/rust-hello is an ordinary static std executable built for aarch64-unknown-linux-musl, not a freestanding or no_std special case. Native and Chromium hosts both print rust hello, exit 0, and report the same 4,880-instruction, 20-syscall execution. Its observed path added single-thread exclusive operations and barriers, scalar multiplication/division and signed shifts, 128-bit vector memory, NEON byte count/reduction, deterministic one-CPU affinity, signal state, and anonymous mapping/protection/unmapping. Phase 1 acceptance is therefore complete; the next checkpoint is Phase 2 Worker start/stop control with structured diagnostics for a non-terminating guest.
The Phase 2 control checkpoint is complete: the Worker accepts explicit C, Rust, and infinite-loop runs with browser-selected instruction, syscall, output, and committed-memory limits. Execution traps are returned as stable diagnostic codes with partial counters and trace state. The browser can unconditionally stop an infinite guest by terminating the active Worker and creates a fresh ready Worker afterward. The remaining Phase 2 implementation item is the initial in-memory filesystem and its shared native/browser guest regression.
Phase 2 is complete. binarrow-memory-fs supplies a bounded ephemeral /tmp regular-file store behind the project host filesystem trait, while binarrow-linux-runtime owns guest descriptors and the observed openat, close, lseek, read, and file write ABI. A freestanding C fixture creates, writes, seeks, reads, and closes /tmp/roundtrip.txt before printing the recovered bytes; native, CLI, and Chromium tests all run the identical ELF. ADR-0002 records the interpreter's interim browser memory design: project-owned sparse 64-bit guest mappings backed by bounded Wasm32 allocations, with Memory64 retained as a required feature gate for the future translator. Every Phase 2 deliverable and acceptance criterion now has an automated regression. Phase 3 begins with a mountable VFS, directory and metadata syscalls, OPFS persistence, terminal input, clock/random services, and the suspension model needed by CPython.
Phase 3 is complete. guest-tests/cpython/build.sh checksum-pins CPython 3.12.13, isolates all build and Zig caches under the repository's ignored .tmp directory, cross-compiles a static AArch64 musl interpreter, links the configured dependency-free extension modules into the executable, and packages the standard library with its empty pre-dynamic-linking lib-dynload landmark. guest-tests/cpython/verify.sh checks the version and runs the bounded -c 'print(6 * 7)' regression with a deterministic CLI random seed. The freshly built broader module profile added focused semantics for NEON table lookup, unsigned variable shifts, extraction, and 64-bit-group reversal. The browser UI and Worker accept externally built ELF bytes, argv[0], and additional arguments. guest-tests/cpython/verify-browser.sh installs the packaged standard-library image, uploads that interpreter, and verifies print(6 * 7) with exit status zero in Chromium. This completes every Phase 3 acceptance criterion; Phase 4 begins with hot-block profiling and basic-block Wasm lowering, while shared native extensions remain deferred to Phase 7.
The first Phase 4 checkpoint is implemented. The interpreter identifies basic-block boundaries from normalized external-branch and supervisor-call operations, counts successful block entries in a deterministic bounded profile, and exposes the ordered results through the Linux process boundary. Browser reports include the tracked-block count, hottest guest address, and its execution count. Focused regressions prove a tight self-loop reaches exactly the configured hotness while instruction exhaustion, syscall behavior, and ordinary fixture outputs remain unchanged. Basic-block Wasm lowering is next.
The initial basic-block lowering checkpoint is also implemented. Decoder output now crosses into the project-owned binarrow-execution-ir crate, where blocks are validated as non-empty, contiguous single-entry sequences with a final terminator and scratch storage no longer exposes Icicle types. binarrow-wasm-backend lowers its scalar Tier-1 subset against an explicit imported state-memory ABI, emits deterministic modules and translation metrics, and returns structured unsupported-operation results for interpreter fallback. A checked-in AArch64 scalar-loop fixture gives Chromium a real decoded block to compile dynamically and compare against interpreter state. Hot-dispatch integration, broader operation coverage, and the translation cache followed from this checkpoint.
Hot-dispatch integration is implemented for the initial scalar subset. The interpreter offers blocks to a backend after a deterministic execution threshold, charges translated instructions against the same process budget, applies replacement architectural state atomically, and remembers unsupported block identities for interpreter fallback. The browser backend compiles and caches core Wasm modules by guest address plus instruction encodings, installs their exported entry functions in a bounded Worker-local WebAssembly table, reuses a module-local imported state memory, and exposes translated-block, translated-instruction, fallback, compilation, cache-hit, and emitted-byte counters. The Chromium infinite-loop regression executes 64 cold iterations in the interpreter and the next 64 through one generated function-table entry with 63 cache hits. Code-identity regressions replace executable bytes at the same guest address and prove that both the fallback set and compiled-module cache select a new identity. The dynamic translation probe initializes interpreter and Wasm execution from identical state, then compares the complete scalar state ABI and next PC across a 17-instruction block covering normal and exceptional signed/unsigned division, shifts, and bitwise operations. Structured forward internal branches lower to Wasm blocks while malformed control retains interpreter fallback.
Phase 4 is complete for the planned Tier-1 scope. An opt-in Chromium benchmark runs the 17-instruction integer-mix loop through fresh interpreter-only and hot-translation sessions, warms both paths, takes five samples, and compares their medians without burdening normal CI with a timing assertion. A July 24, 2026 local run over 278,528 guest instructions measured 36.8 ms interpreted and 24.5 ms translated, a 1.50x end-to-end speedup including scalar state exchange and function-table dispatch. The benchmark requires at least a 1.2x speedup when explicitly enabled. Broader operation coverage and a larger differential corpus remain ongoing quality work, but every Phase 4 deliverable and acceptance criterion now has an implementation and regression or reproducible benchmark. Phase 5 begins with multi-file CPython programs, filesystem-backed imports, exceptions/tracebacks, pure-Python package installation, and compatibility documentation.
The first Phase 5 checkpoint implemented a checked-in multi-file project that imports phase5pkg.calculator from the guest filesystem, prints a computed result, and deliberately raises an exception so one run covers package discovery, source loading, compilation, traceback paths, exception text, and nonzero exit behavior. The standard-library snapshot and project package remain separate deterministic images; Chromium installs the package through the project-image API, while the native CLI accepts repeatable --filesystem-install overlays. CPython's ioctl(FIOCLEX/FIONCLEX) descriptor path is implemented with ENOTTY fallback for unrelated requests. Native and opt-in Chromium regressions both pass against the same CPython 3.12.13 runtime and images. Compatibility and current limitations are recorded in docs/cpython-compatibility.md.
Phase 5 is complete. guest-tests/cpython/package-packaging.sh downloads the official pure-Python packaging 26.2 wheel into the repository's ignored .tmp directory, verifies its pinned SHA-256 digest, preserves its package and distribution metadata, and emits an independent filesystem image. The shared project imports that wheel, checks its version and METADATA, and retains the multi-file result plus deliberate traceback assertions. Native and opt-in Chromium regressions install the standard library, project, and dependency as separate ordered overlays and pass under a 120-million-instruction cap. Chromium image operations now expose a pending state, disable conflicting controls, and acknowledge request identities, closing a race discovered by consecutive installs. Project and package files persist through the existing OPFS snapshot path. Guest-side dependency resolution, network indexes, native wheels, and shared extensions remain deferred. Every Phase 5 deliverable and acceptance criterion has an implementation and regression; Phase 6 begins with a packaged AArch64 Clang/LLD/musl toolchain and child-process compiler orchestration.
The first Phase 6 checkpoint implements AArch64 Linux execve as atomic process-image replacement from the guest filesystem. The runtime bounds and copies the pathname, argument vector, environment vector, and executable before loading a fresh static ELF image; it preserves credentials, the current directory, the signal mask, resource counters, trace history, and ordinary descriptors while resetting process-image state, signal dispositions, the alternate signal stack, pending input, and anonymous-memory placement. Descriptors carrying O_CLOEXEC are closed only after the replacement image loads successfully. A deterministic two-ELF fixture validates reconstructed argv/envp plus retained and close-on-exec descriptors through the native runtime, CLI, browser-runtime native host, and a real Chromium Worker using the same installable filesystem image. This establishes process replacement, not yet concurrent child processes: the next checkpoint is a minimal packaged Clang/LLD/musl toolchain and the smallest additional orchestration/syscall surface its observed execution trace requires.
The next Phase 6 compatibility checkpoint is driven by an ignored, checksum-verified official AArch64 Linux Zig 0.16.0 bundle as a compact LLVM/LLD runtime probe; it is not the product's promised Clang distribution. zig version now completes, and zig cc discovers its installed executable/library tree, creates explicit project-local caches, and advances through 20 million guest instructions without a syscall or semantic trap before reaching the configured instruction limit. The observed path added scalar binary32/binary64 conversion, NEON rev32, Linux O_PATH/O_NOCTTY, directory-descriptor-relative *at path resolution, repeatable initial environment entries in the native CLI, and correct ENOENT versus ENOTDIR handling for missing filesystem parents. This measurement makes performance—not another guessed ABI surface—the current boundary. The next implementation step is to persist intermediate compiler snapshots and exercise the same workload through the browser's translated tier while pruning the eventual Clang/LLD/musl image.
Restartable filesystem checkpoints now cover the compiler workload in both hosts. The native CLI accepts --filesystem-output and exports completed project mutations even after instruction exhaustion; a regression stops a writer after 25 instructions and successfully consumes the emitted snapshot in a second process. Browser-uploaded programs accept explicit environment entries, large image imports transfer their buffers into the Worker, and an opt-in real Chromium regression imports the ignored 330 MB Zig image, completes zig version within three million instructions with translated blocks, drives zig cc to a five-million-instruction boundary, and exports a larger post-run snapshot. The measured browser probe completes in 2.2 minutes. This is filesystem/cache continuation, not suspended process-memory continuation. The remaining Phase 6 work is to prune and package the promised Clang/LLD/musl distribution, decide whether subprocess semantics beyond execve are required by its trace, and drive a source-to-runnable-ELF compile through completion.
The promised distribution now has a reproducible, checksum-pinned producer for LLVM 22.1.8 and musl 1.2.6. It cross-builds 52 MB Clang and 32 MB LLD AArch64 executables as static musl ELFs, packages Clang resource headers plus a 3.7 MB musl sysroot and license notices, and keeps every source, cache, build, and output under the repository's ignored .tmp directory. clang --version completes inside the native runtime after adding single-threaded CLREX handling. A ten-million-instruction clang -cc1 probe reads stdio.h from the packaged sysroot and reaches only the configured instruction boundary inside a hot memset, with its filesystem snapshot exported and no further semantic or syscall trap. The next checkpoint is the higher-budget translated Chromium compile, followed by a separate LLD run over the persisted object and execution of the resulting ELF; direct child-process orchestration and pipes remain subsequent Phase 6 work.
The native source-to-ELF acceptance path is now complete and reproducible through toolchains/clang-musl/verify.sh. A bounded Clang -cc1 run compiles a minimal C main into /project/main.o, a separate bounded LLD run links that object with the packaged musl startup objects and libc.a, and a generic checked-in AArch64 launcher uses execve to run the guest-created /project/hello, which exits 42. The observed workload added scalar FNEG, a conservative DCZID_EL0 value that disables DC ZVA, pread64, ftruncate, and stable path-derived inode identities so Clang does not collapse distinct include roots. Clang completes below a 50-million-instruction ceiling and LLD completes under the same ceiling. The next checkpoint is this exact compile/link/execute sequence in the translated Chromium host; direct child-process orchestration and pipes remain subsequent Phase 6 work.
The same source-to-ELF sequence now passes in a real Chromium Worker through the opt-in toolchains/clang-musl/verify-browser.sh regression. In one persisted browser session, Clang exits zero after 32,725,151 guest instructions and 507,877 translated-block executions, LLD exits zero after 11,026,753 interpreted guest instructions, and the generic launcher executes the browser-created ELF to exit 42 after 739 instructions. The measured end-to-end browser run completes in 19.2 minutes on the reference host. This satisfies the core local compile/link/execute path with staged process orchestration; the remaining Phase 6 work is editor-to-filesystem integration, compiler source diagnostics in the product UI, direct child-process and pipe semantics, cache policy, and interactive-performance improvements.
Editor-to-filesystem integration is complete for the initial C workflow. The browser presents editable /project/main.c, transfers its exact UTF-8 bytes to the Worker, atomically replaces the file under the configured filesystem quota, and persists the resulting canonical snapshot in OPFS. A fast Chromium regression saves distinct source, reloads the page, and confirms the exported project contains those bytes. The full Clang acceptance test now installs only the toolchain image and obtains its source through this editor path before compiling, linking, and executing it. The verified editor-driven run completes in 19.4 minutes: Clang exits zero after 32,725,151 instructions and 508,211 translated-block executions, LLD exits zero after 11,026,753 instructions, and the resulting ELF exits 42 after 739 instructions. Remaining Phase 6 work is compiler source diagnostics in the product UI, direct child-process and pipe semantics, cache policy, and interactive-performance improvements.
Compiler source diagnostics now have a product UI path. Clang-format stderr records are parsed into path, line, column, severity, and message entries while the complete raw terminal stream remains available. Selecting a /project/main.c diagnostic focuses the source editor at the bounded reported position. A deterministic checked-in AArch64 fixture emits the same source-location format through the real standard-error syscall path; native and Chromium regressions verify its nonzero exit, structured display, and exact editor selection. Remaining Phase 6 work is direct child-process and pipe semantics, build cache policy, and interactive-performance improvements.
The first pipe checkpoint adds AArch64 pipe2 with process-owned descriptors and a fixed 64 KiB FIFO queue. The initial contract deliberately requires O_NONBLOCK: reads distinguish EAGAIN from EOF, writes are partial at remaining capacity and return EPIPE after the reader closes, fstat identifies FIFO descriptors, lseek returns ESPIPE, close-on-exec shares the ordinary descriptor flag path, and pipe ends count against the open-file limit. A checked-in assembly fixture round-trips bytes through the pipe and passes on the native host and in Chromium. Blocking pipe suspension/wakeup and descriptor sharing across concurrent child processes remain the next orchestration checkpoint; build cache policy and interactive-performance work also remain.
The first direct child-process checkpoint implements a deliberately constrained spawn/exec model rather than claiming fork. AArch64 clone accepts exactly CLONE_VM | CLONE_VFORK | SIGCHLD, one suspended parent, and an optional child stack; it rejects nested or unreaped children, TID/TLS modes, and other flags. PID 2 can run directly or replace itself from the guest filesystem, after which exit restores the parent CPU, memory, interpreter, signals, identity, and configuration while retaining aggregate resource counters, output, trace history, and filesystem mutations. getpid, getppid, gettid, and wait4 expose the child identity and encoded exit status. Native and Chromium fixtures cover both direct child exit and child execve followed by parent resumption. Blocking pipe wakeups, general clone modes, build cache policy, and interactive-performance work remain.
The constrained child now inherits a cloned descriptor table backed by shared host open-file descriptions and process-owned pipe queues. Shared file offsets and child writes survive parent restoration, while a child close removes only its table entry and closes the underlying host handle only when no suspended-parent reference remains. Pipe reader/writer lifetime likewise accounts for both tables, so child O_CLOEXEC cleanup cannot discard the parent's endpoints or queued data. The spawn/exec fixture now passes bytes through an inherited close-on-exec pipe before replacement, then validates the data and EOF after wait4; native runtime and Chromium-facing coverage use that same ELF and filesystem image. Blocking pipe suspension/wakeup, general clone modes, and interactive-performance work remain.
The Phase 6 build-cache policy now designates /project/.cache as the bounded persistent namespace for guest compiler artifacts. It naturally shares the project snapshot's filesystem quota and OPFS durability rather than introducing a second storage format. A new atomic snapshot transformation removes that file-or-directory tree while retaining all unrelated source and installed toolchain paths; the browser exposes it as Clear build cache and persists the canonical result. Rust regressions verify nested removal, idempotence, path confinement, and source retention, while Chromium imports a snapshot containing both cache data and source and verifies the exported result. Blocking pipe suspension/wakeup, general clone modes, and interactive-performance work remain.
Phase 6 is complete for its initial static C scope. pipe2(O_CLOEXEC) now supports the blocking-mode descriptor shape used by musl posix_spawn, with F_GETFL/F_SETFL tracking O_NONBLOCK; operations that would actually sleep still use the bounded EAGAIN boundary until general scheduler wakeups exist. The constrained clone path correctly ignores unused legacy TID/TLS pointer arguments when their flags are absent, and trace-driven faccessat provides existence and writable-namespace checks. The packaged image installs LLD under the conventional ld driver name. A single native guest Clang invocation now compiles /project/main.c in its spawned -cc1 child, links through its spawned LLD child under one 120-million-instruction ceiling, and produces /project/driver-hello, which exits 42. The staged real-Chromium compile/link/execute acceptance and fast Chromium child/exec/blocking-pipe regression cover the browser boundary. General blocking wakeups, broader clone modes, dynamic ELF support, and interactive performance remain subsequent work; Phase 7 is next.
Phase 7 is complete for the guest-musl scope. Strict PT_INTERP parsing feeds a two-image process loader with correct main-program AT_ENTRY, interpreter AT_BASE, and initial interpreter PC; the runtime resolves the interpreter from the guest filesystem for initial execution and execve. File-backed page-aligned mmap, fixed mappings, protection changes, and descriptor-offset preservation let the unmodified musl 1.2.6 dynamic linker map and relocate shared objects itself. A checked-in PIE fixture covers startup TLS, libanswer.so, late-loaded plugin TLS, and guest dlopen/dlsym in native and Chromium-facing sessions. Executable-byte identities keep translated and unsupported-block caches coherent when dynamic mappings replace code. The packaged system image provides a deterministic shared-library cache, and missing interpreters or DSOs retain precise loader/musl diagnostics. Finally, a reproducible dynamic CPython 3.12.13 checkpoint loads shared libpython3.12.so and imports _struct through CPython's ordinary dlopen path, printing 0000002a. This satisfies all Phase 7 acceptance criteria; Phase 8 begins with a packaged AArch64 Rust host toolchain and Cargo.
The first Phase 8 host-tool and source compilation checkpoint is complete. toolchains/rust-musl/build.sh checksum-pins the official Rust 1.93.0 aarch64-unknown-linux-musl rustc, Cargo, and standard-library components, strips host debug data, retains the distributed licenses, and emits an ignored approximately 540 MiB system image. Its libgcc_s.so.1 compatibility DSO combines the component's PIC LLVM libunwind archive, explicitly exported Zig compiler-rt builtins needed by LLD, and frame-registration compatibility entry points. The native verifier runs dynamically linked rustc through the guest musl loader, checks its exact version, compiles a checked-in no_std AArch64 source file with one codegen unit, invokes the packaged LLD over the persisted object, and executes the resulting static ELF to print guest rustc hello and exit 42. Reaching that path added bounded futex wait/wake scheduling for rustc's signal waiter, compiler worker, nested helpers, and coordinator/worker pair; per-thread IDs; final file-copy syscalls; unsigned and signed NEON comparisons; pairwise unsigned maximum; subtract-with-carry flags; and explicit CLI syscall/memory limits. General Cargo dependency resolution, build scripts, and reusable multi-threaded guest semantics remain the next Phase 8 work.
The offline Cargo dependency and persistent-cache checkpoint is complete. A checksum-locked pure-Rust fixture resolves itoa 1.0.18 from a minimal separately packed crates.io archive, sparse-index record, and registry configuration with guest Cargo running --offline --locked. Registry extraction uses guest utimensat; deterministic BNFS v4 snapshots persist absolute nanosecond modification times while retaining v2/v3 migration support. Cargo compiles the dependency and fixture, exports the incremental target directory, and the resulting static ELF prints guest cargo dependency hello and exits 42. A second verbose build from the exported snapshot reports both crates as Fresh and finishes in 0.00 seconds before the cached ELF passes again. The cold rustc path also added correct upper-lane clearing for scalar SIMD reductions, NEON bitwise-insert-if-false, and scalar NEON comparisons. At that checkpoint the compatibility policy limited the verified tier to locked pure-Rust crates without build scripts; build-script execution was the next Phase 8 target.
Minimal Cargo build-script execution is now complete. The locked offline fixture uses a Rust build.rs child to read OUT_DIR, write generated Rust source, and send cargo:rerun-if-changed through Cargo's captured stdout; the final package includes that source in a normal static musl std binary, prints the dependency-backed result, and exits 42. A restored-snapshot replay reports both itoa and the root package as Fresh without rerunning the script, and the cached ELF passes again. Reaching the child added lane-wise NEON integer negation and unsigned widening add-across semantics with exact compiler/startup opcode regressions. The reproducible verifier gives the cold three-target compile a 1.5-billion-instruction bound while retaining a smaller cache-replay bound. Procedural macros, native dependencies, compiler-spawning or host-probing build scripts, and browser build cancellation remain the next Phase 8 work.
Browser Cargo cancellation is now covered by a reproducible Chromium verifier. It imports the completed toolchain/project/registry snapshot, completes a guest cargo clean, starts a real cold offline build, and terminates the active Worker while that build is running. Guest filesystem changes remain private to the Worker until a completed execution atomically replaces the OPFS snapshot; after cancellation, the replacement Worker becomes ready and the committed snapshot's size, modification time, and boundary digest remain unchanged. Restarting large compiler Workers is deferred to the next browser task so Chromium can reclaim the terminated Worker's Wasm state first, while the existing infinite-loop cancellation stays immediate. Explicit compatibility diagnostics for native dependencies and compiler-spawning or host-probing build scripts were the final Phase 8 acceptance gap.
Phase 8's compatibility-policy checkpoint is complete. The Rust-only image installs small static AArch64 guard executables under the common C/C++ compiler, native discovery/build, shell, and host-probing command names. A build script that crosses the supported locked pure-Rust/minimal target-local tier now receives a stable binarrow compatibility error for either native dependencies or host probing instead of an ambiguous missing-executable failure; the verifier runs both guard classes through the same guest filesystem and execve path used by build-script children. The supported tier, minimal build scripts, offline registry cache, incremental persistence, successful source-to-ELF paths, and browser cancellation durability now satisfy every Phase 8 acceptance criterion. Procedural macros and actual native-dependency compilation remain intentionally outside this tier rather than blocking Phase 8; Phase 9 is next.
Do not begin the full web IDE before item 30 passes.
24. Suggested first pull requests #
PR 1: Workspace and browser feature probe #
- Workspace
- CI
wasm-encoder- Worker bootstrap
- Memory64 module
- Feature report
PR 2: Icicle dependency investigation #
- Experimental crate
- Dependency report
- Minimal AArch64 decode
- Minimal P-code interpretation
- ADR with adopt/partial/reject decision
PR 3: ELF inspection CLI #
objectintegration- AArch64 ELF validation
- Segment listing
- Entry-point display
- Fuzz target
PR 4: Native static process image #
- Memory regions
- ELF segment mapping
- Stack construction
argc/argv/envp/auxv- Initial instruction execution
PR 5: Minimal Linux ABI #
svchandlingwriteexitbrkmmap- Static hello-world test
PR 6: Browser execution #
- Worker runtime
- Terminal output
- Cancellation
- Resource budget
- Browser hello-world test
25. Agent handoff prompt #
The following can be pasted into a coding agent after creating the repository:
We are building a Rust-based Chrome runtime that executes AArch64 Linux ELF binaries without booting a kernel. It should behave like a userspace emulator: load ELF segments, construct a Linux process image, execute AArch64 instructions, intercept
svc, and implement Linux syscalls against browser-backed services. The long-term execution strategy is an interpreter plus hot-code translation from an architecture-neutral P-code/IR into dynamically compiled WebAssembly.Begin with Phase 0 from
AARCH64_ELF_TO_WASM_BUILD_PLAN.md. Do not build the UI or broad syscall support yet.First, create the Cargo workspace and CI. Then investigate whether the Icicle emulator’s
icicle-cpu,icicle-vm,icicle-mem,icicle-linux, andsleighcrates can be reused in awasm32-unknown-unknownbuild. Produce a dependency report, minimal AArch64 decode test, minimal P-code execution test, and an architecture decision record explaining whether to adopt Icicle fully, adopt selected components, or reject it.In parallel, create a Chrome Worker feature probe for Memory64, JSPI, tail calls, Wasm SIMD, multiple memories, typed function references, and finalized exception handling. Use
wasm-encoderto emit a tiny Memory64 module and dynamically compile it.Keep browser APIs behind explicit Rust host traits. Keep the CPU/interpreter testable on a native host. Do not introduce GPL implementation code into the permissive project core. Add tests for every behavior. End the first milestone with a concrete plan for loading and running a tiny static AArch64 assembly ELF that only invokes Linux
writeandexit.
26. Definition of the first convincing demo #
The first public demo does not need compilers in the browser.
It should:
- Open in Chrome.
- Load a supplied static AArch64 ELF.
- Display parsed ELF metadata.
- Show the synthetic memory map.
- Execute the program in a Worker.
- Print program output to a terminal.
- Show an instruction counter.
- Show a syscall trace.
- Stop an infinite loop through a resource limit.
- Run at least one C binary and one Rust binary cross-compiled outside the browser.
That demo proves the architectural core without prematurely taking on CPython, Clang, Cargo, dynamic linking, or threads.
27. References to verify during implementation #
These links reflect the architectural research used for this plan. Pin versions and re-check licenses before adoption.
- Icicle emulator: https://github.com/icicle-emu/icicle-emu
- Icicle paper: https://arxiv.org/abs/2301.13346
- Rust
objectcrate: https://github.com/gimli-rs/object - Bytecode Alliance
wasm-tools: https://github.com/bytecodealliance/wasm-tools - WebAssembly feature status: https://webassembly.org/features/
- Chrome 133 Memory64 release notes: https://developer.chrome.com/release-notes/133
- Chrome 137 JSPI release notes: https://developer.chrome.com/release-notes/137
- Arm ABI documents: https://github.com/ARM-software/abi-aa
- AAPCS64: https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst
- AArch64 ELF ABI: https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst
- AArch64 System V ABI: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
28. Final recommendation #
Build the project in this order:
Icicle and browser feasibility
→ native AArch64 interpreter
→ static ELF + minimal syscalls
→ browser Worker execution
→ persistent VFS and CPython
→ P-code/IR-to-Wasm translation
→ local Clang/LLD
→ dynamic linking
→ local Rust/Cargo
→ threads and broader compatibility
The highest-risk mistake would be starting with the WebAssembly translator or web IDE before proving the interpreter, Linux ABI boundary, and browser portability of the chosen emulator foundation.
The safest first commitment is not “we will use Icicle.” It is:
We will preserve an architecture-neutral execution IR and interpreter, reuse Icicle if its components fit, and keep the Linux/browser runtime independent enough to replace the CPU foundation without restarting the project.