diff --git a/PLAN.md b/PLAN.md index 9b69d78..d3241d6 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # AArch64 ELF-to-WebAssembly Browser Runtime ## Engineering Build Plan and Agent Handoff -**Status:** Phase 1 in progress; a compiler-produced freestanding AArch64 C ELF executes through Linux `write` and `exit` in native and browser Workers +**Status:** Phase 1 in progress; a static musl AArch64 C ELF executes through libc startup in native and browser Workers **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1052,7 +1052,7 @@ An unbounded compiler or binary must not be able to freeze the browser indefinit ### 13.1 Instruction groups for the first static programs -The current compiler-produced fixture covers conditional branching, comparison flags, basic 64-bit loads/stores, stack-pointer arithmetic, move-wide immediates, address generation, `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. +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 @@ -1849,6 +1849,8 @@ Items 21–30 are complete. The same generated static assembly ELF fixture execu 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. + Do not begin the full web IDE before item 30 passes. --- diff --git a/README.md b/README.md index e9a0442..8540cb6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The project has completed the Phase 0 feasibility stage described in [PLAN.md](P - `crates/loader`: static ELF segment mapping and an LP64 Linux initial stack with `argc`, `argv`, `envp`, and `auxv`. - `crates/linux-abi`: minimal AArch64 Linux syscall numbers and errno return encoding. - `crates/host-api`: browser-independent terminal service traits. -- `crates/linux-runtime`: bounded `write`/`exit` dispatch and the static-process execution loop. +- `crates/linux-runtime`: bounded `set_tid_address`/`write`/`exit` dispatch and the static-process execution loop. - `crates/browser-runtime`: wasm-bindgen bridge that runs the same static process inside a Worker. - `crates/cli`: the native `binarrow inspect ` and `binarrow run ` commands. - `crates/wasm-probe`: generation of the Memory64 WebAssembly module used by browser tests. @@ -22,7 +22,7 @@ The project has completed the Phase 0 feasibility stage described in [PLAN.md](P - `tools/wasm-bindgen`: pinned, repository-local generation of browser bindings. - `docs/decisions`: architecture decision records, including the selective Icicle adoption decision. -The browser gate dynamically instantiates Memory64, proves that a Promise-bearing JSPI import suspends and resumes Wasm, and runs a Zig-produced freestanding AArch64 C ELF through the production Rust loader, SLEIGH interpreter, Linux runtime, and terminal boundary in a Worker. See [the browser feasibility report](docs/research/browser-feasibility-report.md) for the original capability environment and diagnostics. +The browser gate dynamically instantiates Memory64, proves that a Promise-bearing JSPI import suspends and resumes Wasm, and runs a Zig-linked static AArch64 musl program through libc startup, the production Rust loader, SLEIGH interpreter, Linux runtime, and terminal boundary in a Worker. See [the browser feasibility report](docs/research/browser-feasibility-report.md) for the original capability environment and diagnostics. ## Prerequisites @@ -59,13 +59,13 @@ Run an ELF through the native interpreter and propagate its guest exit status: cargo run -p binarrow-cli -- run path/to/aarch64-static.elf [guest arguments...] ``` -The runnable instruction/syscall profile is deliberately narrow: the current compiler fixture exercises stack-pointer arithmetic, basic loads/stores, comparison flags, conditional branching, move-wide immediates, address generation, `nop`, the zero register, and `svc`. It writes only to stdout or stderr and exits. Invalid terminal descriptors and guest buffers return Linux errno values; instruction, syscall, memory, and combined-output limits are enforced before host side effects. +The runnable instruction/syscall profile remains fixture-driven. The current static musl program exercises libc startup, calls/returns, conditional and internal P-code branches, byte and pair loads/stores, register slicing and extension, integer logical/shift operations, comparison flags, `TPIDR_EL0`, and `svc`. The runtime implements `set_tid_address`, terminal `write`, `exit`, and `exit_group`. Invalid terminal descriptors and guest buffers return Linux errno values; instruction, syscall, memory, and combined-output limits are enforced before host side effects. The browser build generates its Memory64, JSPI, and P-code `.wasm` probes before starting Vite. Generated artifacts are not committed. ## Scope -The native-and-browser compiler checkpoint is complete: both hosts run the same checked-in ELF, project-owned loader, SLEIGH interpreter, Linux `write`/`exit` dispatcher, resource limits, and syscall trace. Chromium verifies the guest output, exit status, instruction count, syscall count, and output-byte count end to end. The normalized instruction subset remains intentionally small and traps cleanly on unsupported semantics. The next checkpoint is a libc-linked static program whose trace will drive the next instruction and startup-syscall additions. See [PLAN.md](PLAN.md) for the roadmap, [the fixture documentation](guest-tests/compiler-hello/README.md) for reproduction details, and [docs/architecture.md](docs/architecture.md) for the current boundaries. +The native-and-browser static musl checkpoint is complete: both hosts run the same checked-in libc-linked ELF, project-owned loader, SLEIGH interpreter, Linux dispatcher, resource limits, and syscall trace. Chromium verifies `libc hello`, exit 0, 1,449 guest instructions, three syscalls, and 11 output bytes end to end. The normalized instruction subset remains intentionally small and traps cleanly on unsupported semantics. The next Phase 1 checkpoint is a file-free static Rust binary. See [PLAN.md](PLAN.md) for the roadmap, [the musl fixture documentation](guest-tests/libc-hello/README.md) for reproduction details, and [docs/architecture.md](docs/architecture.md) for the current boundaries. ## License diff --git a/crates/aarch64/src/lib.rs b/crates/aarch64/src/lib.rs index 64d8db6..fd3438b 100644 --- a/crates/aarch64/src/lib.rs +++ b/crates/aarch64/src/lib.rs @@ -56,6 +56,8 @@ const SLEIGH_SOURCES: [(&str, &str); 7] = [ static SLEIGH_DATA: OnceLock> = OnceLock::new(); +type ScratchValues = BTreeMap; + /// Invalid architectural register index supplied through the public API. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct InvalidRegister { @@ -392,6 +394,7 @@ impl Interpreter { enum Storage { X(u8), Sp, + TpidrEl0, Flag(Flag), Scratch(VarNode), } @@ -438,6 +441,26 @@ enum Operation { left: Value, right: Value, }, + BitwiseAnd { + destination: Place, + left: Value, + right: Value, + }, + BitwiseOr { + destination: Place, + left: Value, + right: Value, + }, + ShiftLeft { + destination: Place, + left: Value, + right: Value, + }, + ShiftRight { + destination: Place, + left: Value, + right: Value, + }, Carry { destination: Place, left: Value, @@ -468,10 +491,41 @@ enum Operation { left: Value, right: Value, }, + NotEqual { + destination: Place, + left: Value, + right: Value, + }, + ZeroExtend { + destination: Place, + source: Value, + }, + SignExtend { + destination: Place, + source: Value, + }, + Negate { + destination: Place, + source: Value, + }, + BitwiseNot { + destination: Place, + source: Value, + }, BooleanNot { destination: Place, source: Value, }, + BooleanAnd { + destination: Place, + left: Value, + right: Value, + }, + BooleanOr { + destination: Place, + left: Value, + right: Value, + }, Load { destination: Place, address: Value, @@ -484,6 +538,11 @@ enum Operation { condition: Value, target: Value, }, + InternalBranch { + condition: Value, + label: u16, + }, + Label(u16), SupervisorCall { immediate: Value, }, @@ -534,10 +593,12 @@ fn lower_pcode(language: &SleighData, instruction: PcodeInstruction) -> Option Some(Operation::Copy { - destination: lower_place(language, instruction.output)?, - source: lower_value(language, left)?, - }), + operation @ (PcodeOp::Copy + | PcodeOp::ZeroExtend + | PcodeOp::SignExtend + | PcodeOp::IntNegate + | PcodeOp::IntNot + | PcodeOp::BoolNot) => lower_unary_pcode(language, operation, instruction.output, left), PcodeOp::IntAdd => binary(|destination, left, right| Operation::Add { destination, left, @@ -548,6 +609,26 @@ fn lower_pcode(language: &SleighData, instruction: PcodeInstruction) -> Option binary(|destination, left, right| Operation::BitwiseAnd { + destination, + left, + right, + }), + PcodeOp::IntOr => binary(|destination, left, right| Operation::BitwiseOr { + destination, + left, + right, + }), + PcodeOp::IntLeft => binary(|destination, left, right| Operation::ShiftLeft { + destination, + left, + right, + }), + PcodeOp::IntRight => binary(|destination, left, right| Operation::ShiftRight { + destination, + left, + right, + }), PcodeOp::IntCarry => binary(|destination, left, right| Operation::Carry { destination, left, @@ -578,9 +659,20 @@ fn lower_pcode(language: &SleighData, instruction: PcodeInstruction) -> Option Some(Operation::BooleanNot { - destination: lower_place(language, instruction.output)?, - source: lower_value(language, left)?, + PcodeOp::IntNotEqual => binary(|destination, left, right| Operation::NotEqual { + destination, + left, + right, + }), + PcodeOp::BoolAnd => binary(|destination, left, right| Operation::BooleanAnd { + destination, + left, + right, + }), + PcodeOp::BoolOr => binary(|destination, left, right| Operation::BooleanOr { + destination, + left, + right, }), PcodeOp::Load(memory) if memory == pcode::RAM_SPACE => Some(Operation::Load { destination: lower_place(language, instruction.output)?, @@ -590,10 +682,63 @@ fn lower_pcode(language: &SleighData, instruction: PcodeInstruction) -> Option lower_control_pcode(language, operation, left, right), + } +} + +fn lower_unary_pcode( + language: &SleighData, + operation: PcodeOp, + output: VarNode, + source: PcodeValue, +) -> Option { + let destination = lower_place(language, output)?; + let source = lower_value(language, source)?; + match operation { + PcodeOp::Copy => Some(Operation::Copy { + destination, + source, + }), + PcodeOp::ZeroExtend => Some(Operation::ZeroExtend { + destination, + source, + }), + PcodeOp::SignExtend => Some(Operation::SignExtend { + destination, + source, + }), + PcodeOp::IntNegate => Some(Operation::Negate { + destination, + source, + }), + PcodeOp::IntNot => Some(Operation::BitwiseNot { + destination, + source, + }), + PcodeOp::BoolNot => Some(Operation::BooleanNot { + destination, + source, + }), + _ => None, + } +} + +fn lower_control_pcode( + language: &SleighData, + operation: PcodeOp, + left: PcodeValue, + right: PcodeValue, +) -> Option { + match operation { PcodeOp::Branch(_) => Some(Operation::Branch { condition: lower_value(language, left)?, target: lower_value(language, right)?, }), + PcodeOp::PcodeBranch(label) => Some(Operation::InternalBranch { + condition: lower_value(language, left)?, + label, + }), + PcodeOp::PcodeLabel(label) => Some(Operation::Label(label)), PcodeOp::PcodeOp(id) if language.get_user_ops().nth(usize::from(id)) == Some(SUPERVISOR_USER_OP) => { @@ -622,13 +767,18 @@ fn lower_place(language: &SleighData, variable: VarNode) -> Option { fn architectural_storage(name: &str) -> Option { match name { "sp" => return Some(Storage::Sp), + "tpidr_el0" => return Some(Storage::TpidrEl0), "NG" => return Some(Storage::Flag(Flag::Negative)), "ZR" => return Some(Storage::Flag(Flag::Zero)), "CY" => return Some(Storage::Flag(Flag::Carry)), "OV" => return Some(Storage::Flag(Flag::Overflow)), _ => {} } - let index = name.strip_prefix('x')?.parse::().ok()?; + let index = name + .strip_prefix('x') + .or_else(|| name.strip_prefix('w'))? + .parse::() + .ok()?; (index < 31).then_some(Storage::X(index)) } @@ -659,18 +809,39 @@ fn execute_operations( let mut scratch = BTreeMap::new(); let mut outcome = SemanticOutcome::Advanced; let unsupported = || Trap::UnsupportedInstruction { pc, encoding }; - for operation in operations { + let labels = operations + .iter() + .enumerate() + .filter_map(|(index, operation)| match operation { + Operation::Label(label) => Some((*label, index)), + _ => None, + }) + .collect::>(); + let mut operation_index = 0; + while let Some(operation) = operations.get(operation_index) { + operation_index += 1; match *operation { Operation::Copy { .. } | Operation::Add { .. } | Operation::Subtract { .. } + | Operation::BitwiseAnd { .. } + | Operation::BitwiseOr { .. } + | Operation::ShiftLeft { .. } + | Operation::ShiftRight { .. } | Operation::Carry { .. } | Operation::SignedCarry { .. } | Operation::SignedBorrow { .. } | Operation::SignedLess { .. } | Operation::LessEqual { .. } | Operation::Equal { .. } - | Operation::BooleanNot { .. } => { + | Operation::NotEqual { .. } + | Operation::ZeroExtend { .. } + | Operation::SignExtend { .. } + | Operation::Negate { .. } + | Operation::BitwiseNot { .. } + | Operation::BooleanNot { .. } + | Operation::BooleanAnd { .. } + | Operation::BooleanOr { .. } => { execute_value_operation(state, &mut scratch, operation, pc, encoding)?; } Operation::Load { .. } | Operation::Store { .. } => { @@ -683,6 +854,16 @@ fn execute_operations( )); } } + Operation::InternalBranch { condition, label } => { + if read_value(state, &scratch, condition).ok_or_else(unsupported)? != 0 { + operation_index = labels + .get(&label) + .copied() + .and_then(|index| index.checked_add(1)) + .ok_or_else(unsupported)?; + } + } + Operation::Label(_) => {} Operation::SupervisorCall { immediate } => { let value = read_value(state, &scratch, immediate).ok_or_else(unsupported)?; outcome = SemanticOutcome::SupervisorCall( @@ -696,7 +877,7 @@ fn execute_operations( fn execute_value_operation( state: &mut Aarch64State, - scratch: &mut BTreeMap, + scratch: &mut ScratchValues, operation: &Operation, pc: GuestAddress, encoding: u32, @@ -707,7 +888,26 @@ fn execute_value_operation( Operation::Copy { destination, source, + } + | Operation::ZeroExtend { + destination, + source, } => (destination, read(source)?), + Operation::SignExtend { + destination, + source, + } => { + let value = signed_value(read(source)?, source.size).ok_or_else(unsupported)?; + (destination, u64::from_ne_bytes(value.to_ne_bytes())) + } + Operation::Negate { + destination, + source, + } => (destination, 0_u64.wrapping_sub(read(source)?)), + Operation::BitwiseNot { + destination, + source, + } => (destination, !read(source)?), Operation::Add { destination, left, @@ -718,6 +918,115 @@ fn execute_value_operation( left, right, } => (destination, read(left)?.wrapping_sub(read(right)?)), + Operation::BitwiseAnd { + destination, + left, + right, + } => (destination, read(left)? & read(right)?), + Operation::BitwiseOr { + destination, + left, + right, + } => (destination, read(left)? | read(right)?), + Operation::ShiftLeft { + destination, + left, + right, + } => { + let shift = u32::try_from(read(right)?).unwrap_or(u32::MAX); + (destination, read(left)?.checked_shl(shift).unwrap_or(0)) + } + Operation::ShiftRight { + destination, + left, + right, + } => { + let shift = u32::try_from(read(right)?).unwrap_or(u32::MAX); + (destination, read(left)?.checked_shr(shift).unwrap_or(0)) + } + operation @ (Operation::Carry { .. } + | Operation::SignedCarry { .. } + | Operation::SignedBorrow { .. } + | Operation::SignedLess { .. }) => { + execute_flag_value_operation(state, scratch, &operation, pc, encoding)? + } + operation @ (Operation::LessEqual { .. } + | Operation::Equal { .. } + | Operation::NotEqual { .. } + | Operation::BooleanNot { .. } + | Operation::BooleanAnd { .. } + | Operation::BooleanOr { .. }) => { + execute_boolean_value_operation(state, scratch, &operation, pc, encoding)? + } + Operation::Load { .. } + | Operation::Store { .. } + | Operation::Branch { .. } + | Operation::InternalBranch { .. } + | Operation::Label(_) + | Operation::SupervisorCall { .. } => return Err(unsupported()), + }; + write_place(state, scratch, destination, value).ok_or_else(unsupported) +} + +fn execute_boolean_value_operation( + state: &Aarch64State, + scratch: &ScratchValues, + operation: &Operation, + pc: GuestAddress, + encoding: u32, +) -> Result<(Place, u64), Trap> { + let unsupported = || Trap::UnsupportedInstruction { pc, encoding }; + let read = |value| read_value(state, scratch, value).ok_or_else(unsupported); + match *operation { + Operation::LessEqual { + destination, + left, + right, + } => Ok((destination, u64::from(read(left)? <= read(right)?))), + Operation::Equal { + destination, + left, + right, + } => Ok((destination, u64::from(read(left)? == read(right)?))), + Operation::NotEqual { + destination, + left, + right, + } => Ok((destination, u64::from(read(left)? != read(right)?))), + Operation::BooleanNot { + destination, + source, + } => Ok((destination, u64::from(read(source)? == 0))), + Operation::BooleanAnd { + destination, + left, + right, + } => Ok(( + destination, + u64::from(read(left)? != 0 && read(right)? != 0), + )), + Operation::BooleanOr { + destination, + left, + right, + } => Ok(( + destination, + u64::from(read(left)? != 0 || read(right)? != 0), + )), + _ => Err(unsupported()), + } +} + +fn execute_flag_value_operation( + state: &Aarch64State, + scratch: &ScratchValues, + operation: &Operation, + pc: GuestAddress, + encoding: u32, +) -> Result<(Place, u64), Trap> { + let unsupported = || Trap::UnsupportedInstruction { pc, encoding }; + let read = |value| read_value(state, scratch, value).ok_or_else(unsupported); + match *operation { Operation::Carry { destination, left, @@ -725,7 +1034,7 @@ fn execute_value_operation( } => { let maximum = u128::from(value_mask(left.size).ok_or_else(unsupported)?); let sum = u128::from(read(left)?) + u128::from(read(right)?); - (destination, u64::from(sum > maximum)) + Ok((destination, u64::from(sum > maximum))) } Operation::SignedCarry { destination, @@ -738,7 +1047,7 @@ fn execute_value_operation( .ok_or_else(unsupported)?; let sign = sign_bit(left.size).ok_or_else(unsupported)?; let overflow = (!(left_value ^ right_value) & (left_value ^ result) & sign) != 0; - (destination, u64::from(overflow)) + Ok((destination, u64::from(overflow))) } Operation::SignedBorrow { destination, @@ -751,7 +1060,7 @@ fn execute_value_operation( .ok_or_else(unsupported)?; let sign = sign_bit(left.size).ok_or_else(unsupported)?; let overflow = ((left_value ^ right_value) & (left_value ^ result) & sign) != 0; - (destination, u64::from(overflow)) + Ok((destination, u64::from(overflow))) } Operation::SignedLess { destination, @@ -760,34 +1069,16 @@ fn execute_value_operation( } => { let left = signed_value(read(left)?, left.size).ok_or_else(unsupported)?; let right = signed_value(read(right)?, right.size).ok_or_else(unsupported)?; - (destination, u64::from(left < right)) + Ok((destination, u64::from(left < right))) } - Operation::LessEqual { - destination, - left, - right, - } => (destination, u64::from(read(left)? <= read(right)?)), - Operation::Equal { - destination, - left, - right, - } => (destination, u64::from(read(left)? == read(right)?)), - Operation::BooleanNot { - destination, - source, - } => (destination, u64::from(read(source)? == 0)), - Operation::Load { .. } - | Operation::Store { .. } - | Operation::Branch { .. } - | Operation::SupervisorCall { .. } => return Err(unsupported()), - }; - write_place(state, scratch, destination, value).ok_or_else(unsupported) + _ => Err(unsupported()), + } } fn execute_memory_operation( state: &mut Aarch64State, memory: &mut AddressSpace, - scratch: &mut BTreeMap, + scratch: &mut ScratchValues, operation: &Operation, pc: GuestAddress, encoding: u32, @@ -818,32 +1109,46 @@ fn execute_memory_operation( Operation::Copy { .. } | Operation::Add { .. } | Operation::Subtract { .. } + | Operation::BitwiseAnd { .. } + | Operation::BitwiseOr { .. } + | Operation::ShiftLeft { .. } + | Operation::ShiftRight { .. } | Operation::Carry { .. } | Operation::SignedCarry { .. } | Operation::SignedBorrow { .. } | Operation::SignedLess { .. } | Operation::LessEqual { .. } | Operation::Equal { .. } + | Operation::NotEqual { .. } + | Operation::ZeroExtend { .. } + | Operation::SignExtend { .. } + | Operation::Negate { .. } + | Operation::BitwiseNot { .. } | Operation::BooleanNot { .. } + | Operation::BooleanAnd { .. } + | Operation::BooleanOr { .. } | Operation::Branch { .. } + | Operation::InternalBranch { .. } + | Operation::Label(_) | Operation::SupervisorCall { .. } => Err(unsupported()), } } -fn read_value(state: &Aarch64State, scratch: &BTreeMap, value: Value) -> Option { +fn read_value(state: &Aarch64State, scratch: &ScratchValues, value: Value) -> Option { let raw = match value.source { ValueSource::Constant(value) => value, ValueSource::Storage(Storage::X(index)) => state.x(index)?, ValueSource::Storage(Storage::Sp) => state.sp().get(), + ValueSource::Storage(Storage::TpidrEl0) => state.tpidr_el0(), ValueSource::Storage(Storage::Flag(flag)) => u64::from(read_flag(state, flag)), - ValueSource::Storage(Storage::Scratch(variable)) => *scratch.get(&variable)?, + ValueSource::Storage(Storage::Scratch(variable)) => read_scratch(scratch, variable)?, }; truncate(raw, value.size) } fn write_place( state: &mut Aarch64State, - scratch: &mut BTreeMap, + scratch: &mut ScratchValues, destination: Place, value: u64, ) -> Option<()> { @@ -851,14 +1156,33 @@ fn write_place( match destination.storage { Storage::X(index) => state.set_x(index, value).ok()?, Storage::Sp => state.set_sp(GuestAddress::new(value)), + Storage::TpidrEl0 => state.set_tpidr_el0(value), Storage::Flag(flag) => write_flag(state, flag, value != 0), - Storage::Scratch(variable) => { - scratch.insert(variable, value); - } + Storage::Scratch(variable) => write_scratch(scratch, variable, value)?, } Some(()) } +fn read_scratch(scratch: &ScratchValues, variable: VarNode) -> Option { + let start = usize::from(variable.offset); + let end = start.checked_add(usize::from(variable.size))?; + let mut value = [0; 8]; + value + .get_mut(..usize::from(variable.size))? + .copy_from_slice(scratch.get(&variable.id)?.get(start..end)?); + Some(u64::from_le_bytes(value)) +} + +fn write_scratch(scratch: &mut ScratchValues, variable: VarNode, value: u64) -> Option<()> { + let start = usize::from(variable.offset); + let end = start.checked_add(usize::from(variable.size))?; + let destination = scratch.entry(variable.id).or_insert([0; 16]); + destination + .get_mut(start..end)? + .copy_from_slice(value.to_le_bytes().get(..usize::from(variable.size))?); + Some(()) +} + fn read_flag(state: &Aarch64State, flag: Flag) -> bool { state.nzcv() & (1 << flag_bit(flag)) != 0 } @@ -964,6 +1288,12 @@ mod tests { const STORE_CODE: &[u8] = &[ 0xe8, 0x07, 0x00, 0xf9, // str x8, [sp, #8] ]; + const LIBC_SEMANTICS_CODE: &[u8] = &[ + 0x40, 0xd0, 0x1b, 0xd5, // msr tpidr_el0, x0 + 0x41, 0xd0, 0x3b, 0xd5, // mrs x1, tpidr_el0 + 0x08, 0x01, 0x93, 0x9a, // csel x8, x8, x19, eq + 0x01, 0x15, 0x00, 0x38, // strb w1, [x8], #1 + ]; #[test] fn state_models_integer_vector_and_userspace_control_registers() { @@ -1084,6 +1414,32 @@ mod tests { assert_eq!(state, original); } + #[test] + fn executes_libc_system_register_internal_branch_and_byte_store_semantics() { + for (nzcv, selected_address) in [(0x4000_0000, 0x7000), (0, 0x7010)] { + let mut memory = executable_memory_with_stack(LIBC_SEMANTICS_CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state.set_x(0, 0x1234_567a).unwrap(); + state.set_x(8, 0x7000).unwrap(); + state.set_x(19, 0x7010).unwrap(); + state.set_nzcv(nzcv); + let mut interpreter = Interpreter::new().unwrap(); + + for _ in 0..LIBC_SEMANTICS_CODE.len() / 4 { + interpreter.step(&mut state, &mut memory).unwrap(); + } + + assert_eq!(state.tpidr_el0(), 0x1234_567a); + assert_eq!(state.x(1), Some(0x1234_567a)); + assert_eq!(state.x(8), Some(selected_address + 1)); + let mut byte = [0]; + memory + .read_exact(GuestAddress::new(selected_address), &mut byte) + .unwrap(); + assert_eq!(byte, [0x7a]); + } + } + #[test] fn enforces_the_instruction_budget() { let mut memory = executable_memory(SYSCALL_CODE); diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index b8544a2..b112231 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -7,8 +7,8 @@ use binarrow_linux_runtime::Process; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; use wasm_bindgen::prelude::*; -const COMPILER_HELLO_ELF: &[u8] = - include_bytes!("../../../guest-tests/compiler-hello/compiler-hello.aarch64.elf"); +const LIBC_HELLO_ELF: &[u8] = + include_bytes!("../../../guest-tests/libc-hello/libc-hello.aarch64.elf"); /// Browser-safe result returned after the embedded static ELF terminates. #[wasm_bindgen] @@ -80,9 +80,9 @@ pub fn run_hello_world() -> Result { fn execute_hello_world() -> Result { let image = load_process( - COMPILER_HELLO_ELF, + LIBC_HELLO_ELF, &ProcessParameters { - argv: vec![b"/hello".to_vec()], + argv: vec![b"/libc-hello".to_vec()], envp: Vec::new(), random_bytes: [0x42; 16], credentials: Credentials::default(), @@ -135,7 +135,7 @@ impl HostTerminal for CapturedTerminal { mod tests { use super::execute_hello_world; - const MESSAGE: &[u8] = b"compiled hello\n"; + const MESSAGE: &[u8] = b"libc hello\n"; #[test] fn executes_the_browser_fixture_on_the_native_test_host() { @@ -144,12 +144,12 @@ mod tests { assert_eq!(result.stdout.as_bytes(), MESSAGE); assert!(result.stderr.is_empty()); assert_eq!(result.exit_code, 0); - assert_eq!(result.executed_instructions, 15); - assert_eq!(result.dispatched_syscalls, 2); + assert_eq!(result.executed_instructions, 1_449); + assert_eq!(result.dispatched_syscalls, 3); assert_eq!(result.output_bytes, MESSAGE.len() as u64); assert_eq!( result.trace, - "write(fd=1, buf=0x1000158, count=15) = 15\nexit(status=0) -> exit 0" + "set_tid_address(tidptr=0x1030af0) = 1\nwrite(fd=1, buf=0x1000200, count=11) = 11\nexit_group(status=0) -> exit 0" ); } } diff --git a/crates/linux-abi/src/lib.rs b/crates/linux-abi/src/lib.rs index 9ad5847..b5af192 100644 --- a/crates/linux-abi/src/lib.rs +++ b/crates/linux-abi/src/lib.rs @@ -7,6 +7,7 @@ pub enum Syscall { Write = 64, Exit = 93, ExitGroup = 94, + SetTidAddress = 96, } impl Syscall { @@ -17,6 +18,7 @@ impl Syscall { 64 => Some(Self::Write), 93 => Some(Self::Exit), 94 => Some(Self::ExitGroup), + 96 => Some(Self::SetTidAddress), _ => None, } } @@ -55,6 +57,7 @@ mod tests { assert_eq!(Syscall::from_number(64), Some(Syscall::Write)); assert_eq!(Syscall::from_number(93), Some(Syscall::Exit)); assert_eq!(Syscall::from_number(94), Some(Syscall::ExitGroup)); + assert_eq!(Syscall::from_number(96), Some(Syscall::SetTidAddress)); assert_eq!(Syscall::from_number(63), None); } diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index b39298a..90d8cd8 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -11,6 +11,7 @@ use binarrow_runtime_core::{GuestAddress, ResourceLimit, ResourceLimits, Trap}; const STANDARD_OUTPUT: u64 = 1; const STANDARD_ERROR: u64 = 2; +const MAIN_THREAD_ID: u64 = 1; /// Successful termination of one guest process. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -48,6 +49,13 @@ impl fmt::Display for SyscallEvent { Some(Syscall::ExitGroup) => { write!(formatter, "exit_group(status={})", self.arguments[0])?; } + Some(Syscall::SetTidAddress) => { + write!( + formatter, + "set_tid_address(tidptr={:#x})", + self.arguments[0] + )?; + } None => write!(formatter, "syscall({})", self.number)?, } match self.outcome { @@ -89,6 +97,7 @@ pub struct Process { executed_instructions: u64, dispatched_syscalls: u64, output_bytes: u64, + clear_child_tid: Option, trace: Vec, } @@ -108,6 +117,7 @@ impl Process { executed_instructions: 0, dispatched_syscalls: 0, output_bytes: 0, + clear_child_tid: None, trace: Vec::new(), }) } @@ -177,6 +187,7 @@ impl Process { } Some(Syscall::Exit | Syscall::ExitGroup) => { let exit_code = self.register(0).to_le_bytes()[0]; + self.clear_child_tid(); self.trace.push(SyscallEvent { number, arguments, @@ -189,6 +200,15 @@ impl Process { output_bytes: self.output_bytes, }); } + Some(Syscall::SetTidAddress) => { + self.clear_child_tid = Some(GuestAddress::new(self.register(0))); + self.set_return(MAIN_THREAD_ID); + self.trace.push(SyscallEvent { + number, + arguments, + outcome: SyscallOutcome::Returned(MAIN_THREAD_ID), + }); + } None => { self.set_return(Errno::NoSystemCall.return_value()); self.trace.push(SyscallEvent { @@ -256,6 +276,13 @@ impl Process { self.register(u8::try_from(index).expect("six syscall arguments fit u8")) }) } + + fn clear_child_tid(&mut self) { + let Some(address) = self.clear_child_tid.filter(|address| address.get() != 0) else { + return; + }; + let _ = self.memory.write(address, &0_u32.to_le_bytes()); + } } #[cfg(test)] @@ -265,9 +292,9 @@ mod tests { use binarrow_host_api::{HostTerminal, TerminalStream}; use binarrow_linux_abi::Errno; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; - use binarrow_runtime_core::{ResourceLimit, Trap}; + use binarrow_runtime_core::{GuestAddress, ResourceLimit, Trap}; - use super::{ExecutionError, Process, SyscallEvent, SyscallOutcome}; + use super::{ExecutionError, MAIN_THREAD_ID, Process, SyscallEvent, SyscallOutcome}; const MESSAGE: &[u8] = b"hello, world\n"; const MESSAGE_LENGTH: u16 = 13; @@ -276,6 +303,11 @@ mod tests { const COMPILER_MESSAGE_ADDRESS: u64 = 0x100_0158; const COMPILER_HELLO_ELF: &[u8] = include_bytes!("../../../guest-tests/compiler-hello/compiler-hello.aarch64.elf"); + const LIBC_MESSAGE: &[u8] = b"libc hello\n"; + const LIBC_MESSAGE_ADDRESS: u64 = 0x100_0200; + const LIBC_CLEAR_CHILD_TID_ADDRESS: u64 = 0x103_0af0; + const LIBC_HELLO_ELF: &[u8] = + include_bytes!("../../../guest-tests/libc-hello/libc-hello.aarch64.elf"); #[derive(Default)] struct RecordingTerminal { @@ -367,6 +399,63 @@ mod tests { ); } + #[test] + fn static_musl_elf_writes_hello_world_and_exits() { + let image = load_process( + LIBC_HELLO_ELF, + &ProcessParameters { + argv: vec![b"/libc-hello".to_vec()], + envp: Vec::new(), + random_bytes: [0x42; 16], + credentials: Credentials::default(), + }, + ProcessConfig::default(), + ) + .unwrap(); + let mut process = Process::new(image).unwrap(); + let mut terminal = RecordingTerminal::default(); + + let result = process.run(&mut terminal).unwrap(); + + assert_eq!(result.exit_code, 0); + assert_eq!(result.executed_instructions, 1_449); + assert_eq!(result.dispatched_syscalls, 3); + assert_eq!(result.output_bytes, LIBC_MESSAGE.len() as u64); + assert_eq!(terminal.standard_output, LIBC_MESSAGE); + assert!(terminal.standard_error.is_empty()); + assert_eq!( + process.clear_child_tid, + Some(GuestAddress::new(LIBC_CLEAR_CHILD_TID_ADDRESS)) + ); + assert_eq!( + process.trace(), + [ + SyscallEvent { + number: 96, + arguments: [ + LIBC_CLEAR_CHILD_TID_ADDRESS, + 0xe0, + 0, + 0x101_035c, + 0x101_0870, + 0, + ], + outcome: SyscallOutcome::Returned(MAIN_THREAD_ID), + }, + SyscallEvent { + number: 64, + arguments: [1, LIBC_MESSAGE_ADDRESS, 11, 0, 0, 0], + outcome: SyscallOutcome::Returned(11), + }, + SyscallEvent { + number: 94, + arguments: [0, LIBC_MESSAGE_ADDRESS, 11, 0, 0, 0], + outcome: SyscallOutcome::Exited(0), + }, + ] + ); + } + #[test] fn output_limit_stops_before_host_side_effects() { let mut config = ProcessConfig::default(); diff --git a/docs/architecture.md b/docs/architecture.md index 592be86..6eecfaf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -This document captures the boundaries established by the completed Phase 0 feasibility work and the Phase 1 compiler-produced ELF checkpoint. It is intentionally smaller than the target workspace in `PLAN.md`; crates should be split only when a dependency or portability boundary becomes useful. +This document captures the boundaries established by the completed Phase 0 feasibility work and the Phase 1 static-musl ELF checkpoint. It is intentionally smaller than the target workspace in `PLAN.md`; crates should be split only when a dependency or portability boundary becomes useful. ## Dependency direction @@ -40,7 +40,7 @@ Owns small architecture-neutral vocabulary shared by future execution and host c ### `binarrow-aarch64` -Owns userspace architectural state (`x0..x30`, SP, PC, SIMD registers, NZCV, FPCR/FPSR, and `TPIDR_EL0`) and the checked interpreter boundary. Its public API contains only project types. The pinned Ghidra AArch64 specification is compiled once through Icicle's filesystem-free SLEIGH compiler, decoded instructions are lifted to Icicle P-code, and the supported P-code subset is lowered into project-owned operations before execution. The current operations cover the compiler fixture's copies, integer addition/subtraction and comparisons, NZCV flag access, Boolean inversion, checked little-endian loads/stores, and conditional branches. Single-step and instruction-budgeted run APIs stop structurally at `CallSupervisor`, exposing the `svc` immediate and Linux syscall number from `x8`. Decode, semantic, alignment, memory, and budget failures remain project-owned traps. +Owns userspace architectural state (`x0..x30`, SP, PC, SIMD registers, NZCV, FPCR/FPSR, and `TPIDR_EL0`) and the checked interpreter boundary. Its public API contains only project types. The pinned Ghidra AArch64 specification is compiled once through Icicle's filesystem-free SLEIGH compiler, decoded instructions are lifted to Icicle P-code, and the supported P-code subset is lowered into project-owned operations before execution. The current operations cover scalar copies and extension, integer arithmetic/logical/shift/comparison operations, NZCV flags, persistent `TPIDR_EL0`, checked little-endian loads/stores, external branches, and label-resolved internal P-code branches. Scratch values preserve P-code byte slices so byte stores and widened loads remain coherent. Single-step and instruction-budgeted run APIs stop structurally at `CallSupervisor`, exposing the `svc` immediate and Linux syscall number from `x8`. Decode, semantic, alignment, memory, and budget failures remain project-owned traps. ### `binarrow-elf` @@ -64,11 +64,11 @@ Defines host services without choosing a native or browser implementation. The f ### `binarrow-linux-runtime` -Consumes a loaded process image, owns its architectural execution state, and repeatedly runs the interpreter to structured supervisor-call stops. The first dispatcher implements AArch64 Linux `write`, `exit`, and `exit_group`; unsupported calls return `ENOSYS`, invalid write arguments return `EBADF`/`EFAULT`, and output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. +Consumes a loaded process image, owns its architectural execution state, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements AArch64 Linux `set_tid_address`, `write`, `exit`, and `exit_group`; the single-threaded process uses deterministic TID 1 and clears the registered child-TID word on exit. Unsupported calls return `ENOSYS`, invalid write arguments return `EBADF`/`EFAULT`, and output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. ### `binarrow-browser-runtime` -Provides the narrow wasm-bindgen boundary used by the Worker. Its current checkpoint embeds the same deterministic Zig-produced freestanding C ELF used by native tests, invokes the production loader/runtime, captures the terminal host trait, and exposes output, exit status, counters, and formatted syscall events. No AArch64 or Linux behavior is reimplemented in TypeScript. +Provides the narrow wasm-bindgen boundary used by the Worker. Its current checkpoint embeds the same deterministic Zig-linked static musl C ELF used by native tests, invokes the production loader/runtime, captures the terminal host trait, and exposes output, exit status, counters, and formatted syscall events. No AArch64 or Linux behavior is reimplemented in TypeScript. ### `binarrow-cli` @@ -88,4 +88,4 @@ The Icicle feasibility spike is isolated under `experiments/icicle`; it is not a `experiments/icicle-wasm` separately validates Icicle's lightweight `pcode` crate in raw `wasm32-unknown-unknown`. The production `binarrow-aarch64` crate pins `pcode`, `sleigh-runtime`, and the filesystem-free portion of `sleigh-compile` at that same revision and keeps their types private. The required AHash/getrandom browser backend is selected in the workspace's target configuration; `icicle-cpu` and its native VM/JIT remain outside the production graph. -The next production boundary is a libc-linked static C program. Its execution trace will determine the next normalized AArch64 operations and Linux startup syscalls; likely candidates such as `brk` and anonymous `mmap` will be added only when the fixture demonstrates them. +The next production boundary is a file-free static Rust program. Its execution trace will determine the next normalized AArch64 operations and Linux calls; candidates such as `brk` and anonymous `mmap` will still be added only when a fixture demonstrates them. diff --git a/guest-tests/libc-hello/README.md b/guest-tests/libc-hello/README.md new file mode 100644 index 0000000..fb31191 --- /dev/null +++ b/guest-tests/libc-hello/README.md @@ -0,0 +1,19 @@ +# Static musl AArch64 hello-world fixture + +This fixture is a normal C program linked against Zig's static AArch64 musl libc. Unlike the freestanding compiler fixture, its ELF enters through musl's `_start`, initializes libc and thread-local state, calls `main`, invokes the libc `write` wrapper, and exits through libc. + +Build from any working directory with Zig 0.16.0: + +```sh +guest-tests/libc-hello/build.sh +``` + +The output is stripped and checked in so native and browser tests execute identical bytes without requiring Zig at test time. + +Expected SHA-256: + +```text +5911a7094f8607b81f3ab70ca9c9f26db64bcc18a467366a9bd06cabb7b81fdf +``` + +The observed execution path uses musl's built-in static TLS storage, so it does not require `mmap` or `brk`. Its dispatched syscalls are `set_tid_address`, `write`, and `exit_group`. Additional Linux calls will continue to be implemented only when a concrete fixture reaches them. diff --git a/guest-tests/libc-hello/build.sh b/guest-tests/libc-hello/build.sh new file mode 100755 index 0000000..196b290 --- /dev/null +++ b/guest-tests/libc-hello/build.sh @@ -0,0 +1,28 @@ +#!/bin/sh +set -eu + +fixture_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +cache_directory=${TMPDIR:-/tmp}/binarrow-zig-cache +output=$fixture_directory/libc-hello.aarch64.elf +zig_version=$(zig version) + +if [ "$zig_version" != "0.16.0" ]; then + echo "libc-hello requires Zig 0.16.0; found $zig_version" >&2 + exit 1 +fi + +env \ + ZIG_LOCAL_CACHE_DIR="$cache_directory/local" \ + ZIG_GLOBAL_CACHE_DIR="$cache_directory/global" \ + zig cc \ + -target aarch64-linux-musl \ + -static \ + -fno-stack-protector \ + -O1 \ + -g0 \ + -s \ + -Wl,--build-id=none \ + "$fixture_directory/main.c" \ + -o "$output" + +chmod 0644 "$output" diff --git a/guest-tests/libc-hello/libc-hello.aarch64.elf b/guest-tests/libc-hello/libc-hello.aarch64.elf new file mode 100644 index 0000000000000000000000000000000000000000..ab1783635d7b0d853120a1cce46b76c44fee91c2 GIT binary patch literal 3640 zcmb<-^>JfjWMqH=CWh?{Al@6OpaWE*f*Z(6fgy&Sks*VDfuVt& zkzoQzoPmKs07^4f1u-zz$T3Ru$TMg#Ffd3kFfi<4U}Vq%$-D3gxbVmd%ww9%62l`e z;KIjn?=DE30pdRwP&~6SLj3B1P#b`x7UUL?dtb0IGW-DPWmel7T^%q2ZDo zL&K#9%nTD7>;F$sWMG&m|E6XVBLjoTBW8wG%?u0^S{N86IJP+KNRDv**%IUWQ(l4j zY61g;z*A=VRhO(|W5+Czo=#V`pPY_tKQ&nyCTe|*w}{`%P}uPQe@NH= z|E3^6tYBc6@}S<~=kfplr@vV4u~MFq^YuXnhL9J_J;Cfm5WC#r=L!~vEg-ux-hLPO zU(c|Tf$@N?2SY>9180VbFN7I>zI0}o_=uTd;sf>uTMf>JphSj-pvTPet5_Htf*2VX zt|T!0Fnhr)ze=8w!SWPCLr~-Y{}U7qI3{XjW>s)7YM3SPG)&@PZwPwS%x?r$zYV6| zOOat>d?mxjh5!DCfc&J%&=ADwX=M4uKYZoi>admnnb{_S;^`m1+>bB*j4LPq|37^) z1H*(428Id6Nv=ORQeA(_GYY?c#J~_Tg@Iwhlr&dx95FE3Uz^V){Ce^K{~=%e9asKi zclrS~Cw$*Zk28whAhW(OGio_9IQ;y|%&_V+Gt(*tMTUvB|Nl=1g~ekA27!mnB3h4_ zg;zab7F`84BmB^c1P1wQ|M+Epd<$n>8Fogo8>IJ(KhsK(evo==H6#ckbw7PB$@ELLJ%$;7}Q z!pYDe^njUD>z_X)o&Ss9yAqW4yc&%rzL98{(7_W@~c2;WkTPYB0+%# zL7$j;Ryi;^?39;aC~Rb42=U}{_}R(Gd0LQxLF7CG!xniCg!oHF&ePls3?lL}422#{ z9kb*a1hOA8FoZaWIsBAo;C_oSQ$PBG4XXX{{0{Ff{|eh zH>bl-UO@+N8Pg*zAi%NGX=NrOXSX&(Lr~{dhLyaG3=`Hi8cpmHWcaz7m*M9cMuwlP zpzvp8n4rn1Gf_j+aiu1s!%uET&Tb!8hKrhx3>RNBbD!2^pmGM3who912-N)l zKRxFE|LMs2_8+?=IDdfi2rRF`@>!PaPmWwjK6?qvXL+taJNn{(!1CD&q0C~#T_n4Uu9ru z2!gmp{s%{m!{o=zOsgI-Gp=IbXb5`3EWZlmuZPSWtDZ5-uVMz}FLuWtptybkD!-W- zS3Lx)XI}M~nPt@zX4X|tnc2X0fz15=|G&8WAN!5Wj0_=AcY3oiFiZjIWngHSB+SUL z6P9;8E?8e>n8qV_VGFE&0jYIjVfe@>%3yKu|NjtJof2=u@NoqrLr6Ri!$%Kh zh7cxZhAkQl4wjyO94~vZFkIBm$g<#IV6fE4$h6?d(6r#r(6RuT9sh^nV+J!rh{qqt z%L)t!Y&{(qE-GfcwBcZ^nZ&@*V5`N*aatkcg$+Z-3LAxtS2he8t85rER@#8{g7j!I zIPCNiV7SP^ST~82fnld7Bgbj&jEo)J85%n@GBS5)XJqZ*$k5!uouRb@qzC2?4IYO{ zoJJcrb9C=49aN)?(o5=J@M!8DzHu>p>e1R)&uXtc^Ac|Njs1aA3Fya|_Jh zpsF2&W0)b0)dCI(%>Zj~;iQ=uJo0l>lZsNy6_Rrj^U@W{Qj3Z+^Yavp4D}54EEs$; z^RiQmtQ6p?e0*GBatwM!`6-Ddi41zF8S!aFiMgrq87V~w9)n&{}fq?;LH + +int main(void) { + static const char message[] = "libc hello\n"; + ssize_t written = write(STDOUT_FILENO, message, sizeof(message) - 1); + return written == (ssize_t)(sizeof(message) - 1) ? 0 : 1; +} diff --git a/web/tests/probe.spec.ts b/web/tests/probe.spec.ts index dfa7656..492e56d 100644 --- a/web/tests/probe.spec.ts +++ b/web/tests/probe.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; -test("executes a compiler-produced AArch64 ELF in a Worker", async ({ +test("executes a static musl AArch64 ELF in a Worker", async ({ browser, page, }) => { @@ -8,14 +8,14 @@ test("executes a compiler-produced AArch64 ELF in a Worker", async ({ await expect(page.getByRole("status")).toHaveText("Browser runtime ready"); await expect(page.getByLabel("Guest terminal output")).toHaveText( - "compiled hello", + "libc hello", ); await expect(page.locator("#exit-code")).toHaveText("0"); - await expect(page.locator("#instruction-count")).toHaveText("15"); - await expect(page.locator("#syscall-count")).toHaveText("2"); - await expect(page.locator("#output-count")).toHaveText("15"); + await expect(page.locator("#instruction-count")).toHaveText("1449"); + await expect(page.locator("#syscall-count")).toHaveText("3"); + await expect(page.locator("#output-count")).toHaveText("11"); await expect(page.getByLabel("System call trace")).toHaveText( - "write(fd=1, buf=0x1000158, count=15) = 15\nexit(status=0) -> exit 0", + "set_tid_address(tidptr=0x1030af0) = 1\nwrite(fd=1, buf=0x1000200, count=11) = 11\nexit_group(status=0) -> exit 0", ); for (const requiredFeature of [ "Memory64",