From a7920d09fa6ab0901f140ddc931a967ff61b4eba Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sat, 25 Jul 2026 13:23:30 -0700 Subject: [PATCH] feat: compile Rust sources with guest rustc --- PLAN.md | 4 +- README.md | 19 +- crates/aarch64/src/lib.rs | 349 +++++++++- crates/cli/src/main.rs | 26 +- crates/cli/tests/run.rs | 31 + crates/execution-ir/src/lib.rs | 29 + crates/linux-abi/src/lib.rs | 12 + crates/linux-runtime/src/lib.rs | 657 +++++++++++++++++-- docs/architecture.md | 2 +- toolchains/rust-musl/README.md | 31 +- toolchains/rust-musl/build.sh | 33 +- toolchains/rust-musl/compiler-rt-exports.txt | 11 + toolchains/rust-musl/libgcc-compat.c | 9 + toolchains/rust-musl/project/linker.rsp | 7 + toolchains/rust-musl/project/main.rs | 28 + toolchains/rust-musl/verify.sh | 65 +- 16 files changed, 1203 insertions(+), 110 deletions(-) create mode 100644 toolchains/rust-musl/compiler-rt-exports.txt create mode 100644 toolchains/rust-musl/libgcc-compat.c create mode 100644 toolchains/rust-musl/project/linker.rsp create mode 100644 toolchains/rust-musl/project/main.rs diff --git a/PLAN.md b/PLAN.md index 6815119..d4cb3a4 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 8 in progress +**Status:** Phase 8 in progress; no-dependency Rust source-to-ELF checkpoint complete **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1899,7 +1899,7 @@ Phase 6 is complete for its initial static C scope. `pipe2(O_CLOEXEC)` now suppo 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 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 580 MiB system image. A `libgcc_s.so.1` compatibility DSO is linked from the component's own PIC LLVM libunwind archive. The native verifier runs the dynamically linked rustc and its driver/proc-macro DSOs through the guest musl loader and checks its exact version after 34.7 million guest instructions. Rustc's detached Ctrl-C waiter introduced one tightly bounded musl pthread clone shape: the runtime records a dormant helper TID without scheduling it and still rejects a second or general worker thread. The next checkpoint is a no-dependency source-to-static-ELF compile using one codegen unit and the packaged Clang/musl linker. +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. Do not begin the full web IDE before item 30 passes. diff --git a/README.md b/README.md index d3b3e91..e6295c0 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Pass repeatable `--env NAME=VALUE` options before the executable when a guest runtime needs an explicit environment; no host environment variables are inherited implicitly. +`--instruction-budget`, `--syscall-budget`, `--memory-limit`, +`--filesystem-limit`, and the existing output/open-file limits bound guest +work before it crosses a host boundary. + Use `--filesystem-output ` to persist the final `/project` state. The CLI writes this snapshot after execution stops even when a resource limit produces a diagnostic, so bounded compiler runs can retain their cache and @@ -106,13 +110,14 @@ shared `libpython3.12.so` and `_struct`; its bounded native verifier imports the extension through CPython's normal `dlopen` path. Both reproducible builds keep their sources, tools, caches, and generated artifacts under `.tmp`. -Phase 8 has started with a checksum-pinned official Rust 1.93.0 AArch64 musl -host distribution. `toolchains/rust-musl/build.sh` packages rustc, Cargo, the -musl standard library, and the required unwind compatibility DSO entirely -under `.tmp`; `toolchains/rust-musl/verify.sh` runs the real dynamically linked -host compiler through the guest loader. Source compilation, Cargo registry -caching, build scripts, and general worker-thread scheduling remain the next -checkpoints. +Phase 8 now has a checksum-pinned official Rust 1.93.0 AArch64 musl host +distribution and a source-to-static-ELF checkpoint. +`toolchains/rust-musl/build.sh` packages rustc, Cargo, the musl standard +library, LLD, and the required unwind/compiler-runtime compatibility DSO +entirely under `.tmp`. `toolchains/rust-musl/verify.sh` checks the dynamically +linked host compiler, compiles checked-in `no_std` Rust source, links its +persisted object, and executes the result. Cargo registry caching, build +scripts, and general-purpose guest threading remain the next checkpoints. The native verifier now invokes the Clang driver once rather than manually staging `-cc1` and LLD. Its musl `posix_spawn` path uses an inherited diff --git a/crates/aarch64/src/lib.rs b/crates/aarch64/src/lib.rs index d247ae2..6f20818 100644 --- a/crates/aarch64/src/lib.rs +++ b/crates/aarch64/src/lib.rs @@ -41,9 +41,13 @@ const NEON_COUNT_USER_OP: &str = "NEON_cnt"; const NEON_ADD_ACROSS_USER_OP: &str = "NEON_addv"; const NEON_COMPARE_EQUAL_USER_OP: &str = "NEON_cmeq"; const NEON_UNSIGNED_MAXIMUM_ACROSS_USER_OP: &str = "NEON_umaxv"; +const NEON_UNSIGNED_MAXIMUM_PAIRWISE_USER_OP: &str = "NEON_umaxp"; const NEON_BITWISE_SELECT_USER_OP: &str = "NEON_bsl"; const NEON_UNSIGNED_MINIMUM_ACROSS_USER_OP: &str = "NEON_uminv"; const NEON_COMPARE_LESS_THAN_USER_OP: &str = "NEON_cmlt"; +const NEON_COMPARE_UNSIGNED_HIGHER_USER_OP: &str = "NEON_cmhi"; +const NEON_COMPARE_GREATER_THAN_OR_EQUAL_USER_OP: &str = "NEON_cmge"; +const NEON_COMPARE_GREATER_THAN_USER_OP: &str = "NEON_cmgt"; const NEON_BITWISE_INSERT_USER_OP: &str = "NEON_bit"; const NEON_MINIMUM_NUMBER_USER_OP: &str = "NEON_fminnm"; const NEON_FUSED_MULTIPLY_ADD_USER_OP: &str = "NEON_fmadd"; @@ -1070,6 +1074,11 @@ fn lower_control_pcode( arguments: [Option; 2], ) -> Option { match operation { + PcodeOp::BoolXor => Some(Operation::BooleanXor { + destination: lower_place(language, output)?, + left: lower_value(language, left)?, + right: lower_value(language, right)?, + }), PcodeOp::Branch(_) => Some(Operation::Branch { condition: lower_value(language, left)?, target: lower_value(language, right)?, @@ -1172,6 +1181,10 @@ fn is_extended_user_op(user_op: &str) -> bool { | NEON_EXTRACT_USER_OP | NEON_REVERSE_64_USER_OP | NEON_REVERSE_32_USER_OP + | NEON_COMPARE_UNSIGNED_HIGHER_USER_OP + | NEON_COMPARE_GREATER_THAN_OR_EQUAL_USER_OP + | NEON_COMPARE_GREATER_THAN_USER_OP + | NEON_UNSIGNED_MAXIMUM_PAIRWISE_USER_OP | NEON_MINIMUM_NUMBER_USER_OP | NEON_FUSED_MULTIPLY_ADD_USER_OP | NEON_ROUND_INTEGRAL_USER_OP @@ -1187,6 +1200,32 @@ fn lower_extended_user_pcode( arguments: [Option; 2], ) -> Option { match user_op { + NEON_UNSIGNED_MAXIMUM_PAIRWISE_USER_OP => Some(Operation::NeonUnsignedMaximumPairwise { + destination: lower_place(language, output)?, + left: lower_value(language, left)?, + right: lower_value(language, right)?, + element_size: arguments[0]?, + }), + NEON_COMPARE_UNSIGNED_HIGHER_USER_OP => Some(Operation::NeonCompareUnsignedHigher { + destination: lower_place(language, output)?, + left: lower_value(language, left)?, + right: lower_value(language, right)?, + element_size: arguments[0]?, + }), + NEON_COMPARE_GREATER_THAN_OR_EQUAL_USER_OP => { + Some(Operation::NeonCompareGreaterThanOrEqual { + destination: lower_place(language, output)?, + left: lower_value(language, left)?, + right: lower_value(language, right)?, + element_size: arguments[0]?, + }) + } + NEON_COMPARE_GREATER_THAN_USER_OP => Some(Operation::NeonCompareGreaterThan { + destination: lower_place(language, output)?, + left: lower_value(language, left)?, + right: lower_value(language, right)?, + element_size: arguments[0]?, + }), NEON_MINIMUM_NUMBER_USER_OP if arguments[0].is_none() => { Some(Operation::FloatMinimumNumber { destination: lower_place(language, output)?, @@ -1337,14 +1376,7 @@ fn execute_operations( let mut scratch = BTreeMap::new(); let mut outcome = SemanticOutcome::Advanced; let unsupported = || Trap::UnsupportedInstruction { pc, encoding }; - let labels = operations - .iter() - .enumerate() - .filter_map(|(index, operation)| match operation { - Operation::Label(label) => Some((*label, index)), - _ => None, - }) - .collect::>(); + let labels = operation_labels(operations); let mut operation_index = 0; while let Some(operation) = operations.get(operation_index) { operation_index += 1; @@ -1385,15 +1417,20 @@ fn execute_operations( | Operation::BooleanNot { .. } | Operation::BooleanAnd { .. } | Operation::BooleanOr { .. } + | Operation::BooleanXor { .. } | Operation::ExclusiveMonitorPass { .. } | Operation::ExclusiveMonitorsStatus { .. } | Operation::NeonCount { .. } | Operation::NeonAddAcross { .. } | Operation::NeonCompareEqual { .. } | Operation::NeonUnsignedMaximumAcross { .. } + | Operation::NeonUnsignedMaximumPairwise { .. } | Operation::NeonBitwiseSelect { .. } | Operation::NeonUnsignedMinimumAcross { .. } | Operation::NeonCompareLessThan { .. } + | Operation::NeonCompareUnsignedHigher { .. } + | Operation::NeonCompareGreaterThanOrEqual { .. } + | Operation::NeonCompareGreaterThan { .. } | Operation::NeonBitwiseInsert { .. } | Operation::NeonUnsignedMultiplyLong { .. } | Operation::NeonTableLookup { .. } @@ -1435,6 +1472,17 @@ fn execute_operations( Ok(outcome) } +fn operation_labels(operations: &[Operation]) -> BTreeMap { + operations + .iter() + .enumerate() + .filter_map(|(index, operation)| match operation { + Operation::Label(label) => Some((*label, index)), + _ => None, + }) + .collect() +} + fn execute_value_operation( state: &mut Aarch64State, scratch: &mut ScratchValues, @@ -1456,10 +1504,7 @@ fn execute_value_operation( Operation::SignExtend { destination, source, - } => { - let value = signed_value(read(source)?, source.size).ok_or_else(unsupported)?; - (destination, u128::from_ne_bytes(value.to_ne_bytes())) - } + } => execute_sign_extend(state, scratch, destination, source, pc, encoding)?, Operation::Negate { destination, source, @@ -1506,7 +1551,8 @@ fn execute_value_operation( | Operation::NotEqual { .. } | Operation::BooleanNot { .. } | Operation::BooleanAnd { .. } - | Operation::BooleanOr { .. }) => { + | Operation::BooleanOr { .. } + | Operation::BooleanXor { .. }) => { execute_boolean_value_operation(state, scratch, &operation, pc, encoding)? } operation @ (Operation::ExclusiveMonitorPass { .. } @@ -1517,9 +1563,13 @@ fn execute_value_operation( | Operation::NeonAddAcross { .. } | Operation::NeonCompareEqual { .. } | Operation::NeonUnsignedMaximumAcross { .. } + | Operation::NeonUnsignedMaximumPairwise { .. } | Operation::NeonBitwiseSelect { .. } | Operation::NeonUnsignedMinimumAcross { .. } | Operation::NeonCompareLessThan { .. } + | Operation::NeonCompareUnsignedHigher { .. } + | Operation::NeonCompareGreaterThanOrEqual { .. } + | Operation::NeonCompareGreaterThan { .. } | Operation::NeonBitwiseInsert { .. } | Operation::NeonUnsignedMultiplyLong { .. } | Operation::NeonTableLookup { .. } @@ -1540,6 +1590,20 @@ fn execute_value_operation( write_place(state, scratch, destination, value).ok_or_else(unsupported) } +fn execute_sign_extend( + state: &Aarch64State, + scratch: &ScratchValues, + destination: Place, + source: Value, + pc: GuestAddress, + encoding: u32, +) -> Result<(Place, u128), Trap> { + let unsupported = || Trap::UnsupportedInstruction { pc, encoding }; + let value = read_value(state, scratch, source).ok_or_else(unsupported)?; + let value = signed_value(value, source.size).ok_or_else(unsupported)?; + Ok((destination, u128::from_ne_bytes(value.to_ne_bytes()))) +} + fn execute_integer_value_operation( state: &Aarch64State, scratch: &ScratchValues, @@ -2057,26 +2121,12 @@ fn execute_neon_value_operation( (pc, encoding), ) } - Operation::NeonCompareEqual { - destination, - left, - right, - element_size, - } - | Operation::NeonCompareLessThan { - destination, - left, - right, - element_size, - } => { - let signed_less_than = matches!(operation, Operation::NeonCompareLessThan { .. }); - execute_neon_comparison( - state, - scratch, - (destination, left, right, element_size), - signed_less_than, - (pc, encoding), - ) + operation @ (Operation::NeonCompareEqual { .. } + | Operation::NeonCompareLessThan { .. } + | Operation::NeonCompareUnsignedHigher { .. } + | Operation::NeonCompareGreaterThanOrEqual { .. } + | Operation::NeonCompareGreaterThan { .. }) => { + execute_neon_comparison_operation(state, scratch, &operation, pc, encoding) } Operation::NeonUnsignedMaximumAcross { destination, @@ -2094,6 +2144,17 @@ fn execute_neon_value_operation( .ok_or_else(unsupported)?; Ok((destination, result)) } + Operation::NeonUnsignedMaximumPairwise { + destination, + left, + right, + element_size, + } => execute_neon_unsigned_pairwise_maximum( + state, + scratch, + (destination, left, right, element_size), + (pc, encoding), + ), Operation::NeonUnsignedMultiplyLong { destination, left, @@ -2109,6 +2170,72 @@ fn execute_neon_value_operation( } } +fn execute_neon_comparison_operation( + state: &Aarch64State, + scratch: &ScratchValues, + operation: &Operation, + pc: GuestAddress, + encoding: u32, +) -> Result<(Place, u128), Trap> { + let operands = match *operation { + Operation::NeonCompareEqual { + destination, + left, + right, + element_size, + } + | Operation::NeonCompareLessThan { + destination, + left, + right, + element_size, + } + | Operation::NeonCompareUnsignedHigher { + destination, + left, + right, + element_size, + } + | Operation::NeonCompareGreaterThanOrEqual { + destination, + left, + right, + element_size, + } + | Operation::NeonCompareGreaterThan { + destination, + left, + right, + element_size, + } => (destination, left, right, element_size), + _ => return Err(Trap::UnsupportedInstruction { pc, encoding }), + }; + let comparison = ( + matches!(operation, Operation::NeonCompareLessThan { .. }), + matches!(operation, Operation::NeonCompareUnsignedHigher { .. }), + matches!(operation, Operation::NeonCompareGreaterThanOrEqual { .. }), + matches!(operation, Operation::NeonCompareGreaterThan { .. }), + ); + execute_neon_comparison(state, scratch, operands, comparison, (pc, encoding)) +} + +fn execute_neon_unsigned_pairwise_maximum( + state: &Aarch64State, + scratch: &ScratchValues, + operands: (Place, Value, Value, Value), + location: (GuestAddress, u32), +) -> Result<(Place, u128), Trap> { + let (destination, left, right, element_size) = operands; + let (pc, encoding) = location; + let unsupported = || Trap::UnsupportedInstruction { pc, encoding }; + let read = |value| read_value(state, scratch, value).ok_or_else(unsupported); + let element_size = u8::try_from(read(element_size)?).map_err(|_| unsupported())?; + let result = unsigned_lane_pairwise_maximum(read(left)?, read(right)?, left.size, element_size) + .filter(|_| destination.size == left.size && left.size == right.size) + .ok_or_else(unsupported)?; + Ok((destination, result)) +} + fn execute_neon_permutation_operation( state: &Aarch64State, scratch: &ScratchValues, @@ -2436,6 +2563,33 @@ fn unsigned_lane_extreme( Some(result) } +fn unsigned_lane_pairwise_maximum( + left: u128, + right: u128, + source_size: u8, + element_size: u8, +) -> Option { + if element_size == 0 || !source_size.is_multiple_of(element_size.checked_mul(2)?) { + return None; + } + let lane_mask = value_mask(element_size)?; + let lane_count = source_size / element_size; + let pair_count = lane_count / 2; + let mut result = 0; + for (source_index, source) in [left, right].into_iter().enumerate() { + for pair in 0..pair_count { + let first_shift = u32::from(pair) * u32::from(element_size) * 16; + let second_shift = first_shift + u32::from(element_size) * 8; + let maximum = + ((source >> first_shift) & lane_mask).max((source >> second_shift) & lane_mask); + let output_lane = + u32::try_from(source_index).ok()? * u32::from(pair_count) + u32::from(pair); + result |= maximum << (output_lane * u32::from(element_size) * 8); + } + } + Some(result) +} + fn unsigned_multiply_long_lanes( left: u128, right: u128, @@ -2468,7 +2622,7 @@ fn execute_neon_comparison( state: &Aarch64State, scratch: &ScratchValues, operands: (Place, Value, Value, Value), - signed_less_than: bool, + comparison: (bool, bool, bool, bool), location: (GuestAddress, u32), ) -> Result<(Place, u128), Trap> { let (destination, left, right, element_size) = operands; @@ -2500,9 +2654,17 @@ fn execute_neon_comparison( } else { (right >> shift) & lane_mask }; - let matches = if signed_less_than { + let matches = if comparison.0 { signed_value(left_lane, element_size).ok_or_else(unsupported)? < signed_value(right_lane, element_size).ok_or_else(unsupported)? + } else if comparison.1 { + left_lane > right_lane + } else if comparison.2 { + signed_value(left_lane, element_size).ok_or_else(unsupported)? + >= signed_value(right_lane, element_size).ok_or_else(unsupported)? + } else if comparison.3 { + signed_value(left_lane, element_size).ok_or_else(unsupported)? + > signed_value(right_lane, element_size).ok_or_else(unsupported)? } else { left_lane == right_lane }; @@ -2558,6 +2720,14 @@ fn execute_boolean_value_operation( destination, u128::from(read(left)? != 0 || read(right)? != 0), )), + Operation::BooleanXor { + destination, + left, + right, + } => Ok(( + destination, + u128::from((read(left)? != 0) ^ (read(right)? != 0)), + )), _ => Err(unsupported()), } } @@ -2691,6 +2861,7 @@ fn execute_memory_operation( | Operation::BooleanNot { .. } | Operation::BooleanAnd { .. } | Operation::BooleanOr { .. } + | Operation::BooleanXor { .. } | Operation::Branch { .. } | Operation::InternalBranch { .. } | Operation::Label(_) @@ -2701,9 +2872,13 @@ fn execute_memory_operation( | Operation::NeonAddAcross { .. } | Operation::NeonCompareEqual { .. } | Operation::NeonUnsignedMaximumAcross { .. } + | Operation::NeonUnsignedMaximumPairwise { .. } | Operation::NeonBitwiseSelect { .. } | Operation::NeonUnsignedMinimumAcross { .. } | Operation::NeonCompareLessThan { .. } + | Operation::NeonCompareUnsignedHigher { .. } + | Operation::NeonCompareGreaterThanOrEqual { .. } + | Operation::NeonCompareGreaterThan { .. } | Operation::NeonBitwiseInsert { .. } | Operation::NeonUnsignedMultiplyLong { .. } | Operation::NeonTableLookup { .. } @@ -3733,6 +3908,110 @@ mod tests { assert_eq!(state.vector(29), Some(u128::from_le_bytes(expected))); } + #[test] + fn executes_compiler_neon_unsigned_higher_comparison() { + const CODE: &[u8] = &[ + 0x41, 0x34, 0xa1, 0x6e, // cmhi v1.4s, v2.4s, v1.4s + ]; + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state + .set_vector(1, 9_u128 << 96 | 7_u128 << 64 | 5_u128 << 32 | 3) + .unwrap(); + state + .set_vector(2, 8_u128 << 96 | 8_u128 << 64 | 5_u128 << 32 | 4) + .unwrap(); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!( + state.vector(1), + Some(u128::from(u32::MAX) << 64 | u128::from(u32::MAX)) + ); + } + + #[test] + fn executes_compiler_neon_signed_greater_equal_zero_comparison() { + const CODE: &[u8] = &[ + 0x00, 0x88, 0x20, 0x2e, // cmge v0.8b, v0.8b, #0 + ]; + let source = [0x80, 0xff, 0, 1, 0x7f, 0xfe, 2, 0]; + let expected = [0, 0, 0xff, 0xff, 0xff, 0, 0xff, 0xff]; + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state + .set_vector(0, u128::from(u64::from_le_bytes(source))) + .unwrap(); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!( + state.vector(0), + Some(u128::from(u64::from_le_bytes(expected))) + ); + } + + #[test] + fn executes_compiler_neon_signed_greater_than_comparison() { + const CODE: &[u8] = &[ + 0xc6, 0x34, 0x22, 0x0e, // cmgt v6.8b, v6.8b, v2.8b + ]; + let left = [0x80, 0xff, 0, 1, 0x7f, 0xfe, 2, 0]; + let right = [0x80, 0xfe, 1, 0, 0xff, 0xff, 2, 0xff]; + let expected = [0, 0xff, 0, 0xff, 0xff, 0, 0, 0xff]; + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state + .set_vector(6, u128::from(u64::from_le_bytes(left))) + .unwrap(); + state + .set_vector(2, u128::from(u64::from_le_bytes(right))) + .unwrap(); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!( + state.vector(6), + Some(u128::from(u64::from_le_bytes(expected))) + ); + } + + #[test] + fn executes_compiler_neon_unsigned_pairwise_maximum() { + const CODE: &[u8] = &[ + 0xa5, 0xa4, 0x25, 0x6e, // umaxp v5.16b, v5.16b, v5.16b + ]; + let source = [1, 9, 7, 3, 0, 2, 8, 8, 255, 1, 4, 5, 6, 0, 10, 9]; + let half = [9, 7, 2, 8, 255, 5, 6, 10]; + let expected = core::array::from_fn(|index| half[index % half.len()]); + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state.set_vector(5, u128::from_le_bytes(source)).unwrap(); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!(state.vector(5), Some(u128::from_le_bytes(expected))); + } + + #[test] + fn executes_compiler_subtract_with_carry_flags() { + const CODE: &[u8] = &[0x5f, 0x02, 0x02, 0xfa]; // sbcs xzr, x18, x2 + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state.set_x(18, 1_u64 << 63).unwrap(); + state.set_x(2, 1).unwrap(); + state.set_nzcv(1 << 29); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!(state.nzcv(), 0x3000_0000); + } + #[test] fn executes_musl_time_conversion_vector_semantics() { const CODE: &[u8] = &[ diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 3e5c518..8dc4513 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -12,10 +12,12 @@ use binarrow_linux_runtime::{Process, load_process_with_guest_filesystem}; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters}; use binarrow_memory_fs::MemoryFileSystem; -const USAGE: &str = "usage: binarrow inspect \n binarrow run [run options] [guest arguments...]\n binarrow trace [run options] [guest arguments...]\n binarrow image pack [--guest-root ] \n\nrun options:\n --instruction-budget \n --filesystem-limit \n --filesystem-snapshot \n --filesystem-output \n --filesystem-install (repeatable)\n --random-seed \n --argv0 \n --env (repeatable)"; +const USAGE: &str = "usage: binarrow inspect \n binarrow run [run options] [guest arguments...]\n binarrow trace [run options] [guest arguments...]\n binarrow image pack [--guest-root ] \n\nrun options:\n --instruction-budget \n --syscall-budget \n --memory-limit \n --filesystem-limit \n --filesystem-snapshot \n --filesystem-output \n --filesystem-install (repeatable)\n --random-seed \n --argv0 \n --env (repeatable)"; struct RunOptions { instruction_budget: Option, + syscall_budget: Option, + memory_limit: Option, filesystem_limit: Option, filesystem_snapshot: Option, filesystem_output: Option, @@ -65,6 +67,8 @@ fn run_command(arguments: impl Iterator, print_trace: bool) -> fn parse_run_options(arguments: impl Iterator) -> Result { let mut arguments = arguments.peekable(); let mut instruction_budget = None; + let mut syscall_budget = None; + let mut memory_limit = None; let mut filesystem_limit = None; let mut filesystem_snapshot = None; let mut filesystem_output = None; @@ -81,6 +85,18 @@ fn parse_run_options(arguments: impl Iterator) -> Result { + syscall_budget = Some(parse_positive_u64( + &arguments.next().ok_or(USAGE)?, + "syscall budget", + )?); + } + Some("--memory-limit") => { + memory_limit = Some(parse_positive_u64( + &arguments.next().ok_or(USAGE)?, + "memory limit", + )?); + } Some("--filesystem-limit") => { filesystem_limit = Some(parse_positive_u64( &arguments.next().ok_or(USAGE)?, @@ -123,6 +139,8 @@ fn parse_run_options(arguments: impl Iterator) -> Result Result { if let Some(instruction_budget) = options.instruction_budget { config.limits.instruction_budget = instruction_budget; } + if let Some(syscall_budget) = options.syscall_budget { + config.limits.syscall_budget = syscall_budget; + } + if let Some(memory_limit) = options.memory_limit { + config.limits.max_memory_bytes = memory_limit; + } if let Some(filesystem_limit) = options.filesystem_limit { config.limits.max_filesystem_bytes = filesystem_limit; } diff --git a/crates/cli/tests/run.rs b/crates/cli/tests/run.rs index 96780c2..a223f3e 100644 --- a/crates/cli/tests/run.rs +++ b/crates/cli/tests/run.rs @@ -50,6 +50,37 @@ fn run_command_accepts_guest_environment_entries() { assert!(output.stderr.is_empty()); } +#[test] +fn run_command_accepts_an_explicit_memory_limit() { + let fixture = TempFixture::new("memory-limit", &hello_aarch64_elf(0)); + let output = Command::new(env!("CARGO_BIN_EXE_binarrow")) + .arg("run") + .arg("--memory-limit") + .arg("2097152") + .arg(&fixture.path) + .output() + .expect("binarrow should start"); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, MESSAGE); + assert!(output.stderr.is_empty()); +} + +#[test] +fn run_command_enforces_an_explicit_syscall_budget() { + let fixture = TempFixture::new("syscall-budget", &hello_aarch64_elf(0)); + let output = Command::new(env!("CARGO_BIN_EXE_binarrow")) + .arg("run") + .arg("--syscall-budget") + .arg("1") + .arg(&fixture.path) + .output() + .expect("binarrow should start"); + + assert_eq!(output.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&output.stderr).contains("ResourceLimit(Syscalls)")); +} + #[test] fn run_command_supplies_the_ephemeral_filesystem() { let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/crates/execution-ir/src/lib.rs b/crates/execution-ir/src/lib.rs index c3613d3..29d3834 100644 --- a/crates/execution-ir/src/lib.rs +++ b/crates/execution-ir/src/lib.rs @@ -245,6 +245,11 @@ pub enum Operation { left: Value, right: Value, }, + BooleanXor { + destination: Place, + left: Value, + right: Value, + }, Load { destination: Place, address: Value, @@ -292,6 +297,12 @@ pub enum Operation { source: Value, element_size: Value, }, + NeonUnsignedMaximumPairwise { + destination: Place, + left: Value, + right: Value, + element_size: Value, + }, NeonBitwiseSelect { destination: Place, selector: Value, @@ -310,6 +321,24 @@ pub enum Operation { right: Value, element_size: Value, }, + NeonCompareUnsignedHigher { + destination: Place, + left: Value, + right: Value, + element_size: Value, + }, + NeonCompareGreaterThanOrEqual { + destination: Place, + left: Value, + right: Value, + element_size: Value, + }, + NeonCompareGreaterThan { + destination: Place, + left: Value, + right: Value, + element_size: Value, + }, NeonBitwiseInsert { destination: Place, original: Value, diff --git a/crates/linux-abi/src/lib.rs b/crates/linux-abi/src/lib.rs index 07f0530..c0cc736 100644 --- a/crates/linux-abi/src/lib.rs +++ b/crates/linux-abi/src/lib.rs @@ -12,6 +12,7 @@ pub enum Syscall { Renameat = 38, Ftruncate = 46, Faccessat = 48, + Fchmod = 52, Openat = 56, Close = 57, Pipe2 = 59, @@ -21,6 +22,7 @@ pub enum Syscall { Write = 64, Writev = 66, Pread64 = 67, + Sendfile = 71, Readlinkat = 78, Newfstatat = 79, Fstat = 80, @@ -28,6 +30,7 @@ pub enum Syscall { Exit = 93, ExitGroup = 94, SetTidAddress = 96, + Futex = 98, ClockGettime = 113, SchedGetaffinity = 123, Sigaltstack = 132, @@ -42,6 +45,7 @@ pub enum Syscall { Mmap = 222, Mprotect = 226, Getrandom = 278, + CopyFileRange = 285, Wait4 = 260, } @@ -58,6 +62,7 @@ impl Syscall { 38 => Some(Self::Renameat), 46 => Some(Self::Ftruncate), 48 => Some(Self::Faccessat), + 52 => Some(Self::Fchmod), 56 => Some(Self::Openat), 57 => Some(Self::Close), 59 => Some(Self::Pipe2), @@ -68,12 +73,14 @@ impl Syscall { 64 => Some(Self::Write), 66 => Some(Self::Writev), 67 => Some(Self::Pread64), + 71 => Some(Self::Sendfile), 78 => Some(Self::Readlinkat), 79 => Some(Self::Newfstatat), 80 => Some(Self::Fstat), 93 => Some(Self::Exit), 94 => Some(Self::ExitGroup), 96 => Some(Self::SetTidAddress), + 98 => Some(Self::Futex), 113 => Some(Self::ClockGettime), 123 => Some(Self::SchedGetaffinity), 132 => Some(Self::Sigaltstack), @@ -88,6 +95,7 @@ impl Syscall { 222 => Some(Self::Mmap), 226 => Some(Self::Mprotect), 278 => Some(Self::Getrandom), + 285 => Some(Self::CopyFileRange), 260 => Some(Self::Wait4), _ => None, } @@ -149,6 +157,7 @@ mod tests { assert_eq!(Syscall::from_number(38), Some(Syscall::Renameat)); assert_eq!(Syscall::from_number(46), Some(Syscall::Ftruncate)); assert_eq!(Syscall::from_number(48), Some(Syscall::Faccessat)); + assert_eq!(Syscall::from_number(52), Some(Syscall::Fchmod)); assert_eq!(Syscall::from_number(56), Some(Syscall::Openat)); assert_eq!(Syscall::from_number(57), Some(Syscall::Close)); assert_eq!(Syscall::from_number(59), Some(Syscall::Pipe2)); @@ -160,11 +169,13 @@ mod tests { assert_eq!(Syscall::from_number(78), Some(Syscall::Readlinkat)); assert_eq!(Syscall::from_number(79), Some(Syscall::Newfstatat)); assert_eq!(Syscall::from_number(67), Some(Syscall::Pread64)); + assert_eq!(Syscall::from_number(71), Some(Syscall::Sendfile)); assert_eq!(Syscall::from_number(80), Some(Syscall::Fstat)); assert_eq!(Syscall::from_number(73), Some(Syscall::Ppoll)); 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(98), Some(Syscall::Futex)); assert_eq!(Syscall::from_number(113), Some(Syscall::ClockGettime)); assert_eq!(Syscall::from_number(123), Some(Syscall::SchedGetaffinity)); assert_eq!(Syscall::from_number(132), Some(Syscall::Sigaltstack)); @@ -179,6 +190,7 @@ mod tests { assert_eq!(Syscall::from_number(222), Some(Syscall::Mmap)); assert_eq!(Syscall::from_number(226), Some(Syscall::Mprotect)); assert_eq!(Syscall::from_number(278), Some(Syscall::Getrandom)); + assert_eq!(Syscall::from_number(285), Some(Syscall::CopyFileRange)); assert_eq!(Syscall::from_number(260), Some(Syscall::Wait4)); assert_eq!(Syscall::from_number(55), None); } diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index 4a11469..f7cd7dc 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -25,6 +25,10 @@ const STANDARD_OUTPUT: u64 = 1; const STANDARD_ERROR: u64 = 2; const INITIAL_PROCESS_ID: u64 = 1; const CHILD_PROCESS_ID: u64 = 2; +const DORMANT_THREAD_ID: u64 = 10_001; +const ACTIVE_THREAD_ID: u64 = 10_002; +const NESTED_HELPER_THREAD_ID: u64 = 10_003; +const MAX_NESTED_HELPERS: usize = 4; const FIRST_FILE_DESCRIPTOR: u32 = 3; const AT_FDCWD: u64 = (-100_i64).cast_unsigned(); const MAX_PATH_BYTES: usize = 4096; @@ -102,12 +106,18 @@ const SIGSTOP_BIT: u64 = 1 << (19 - 1); const CPU_AFFINITY_BYTES: u64 = 8; const INITIAL_CURRENT_DIRECTORY: &[u8] = b"/project"; const PIPE_CAPACITY_BYTES: usize = 64 * 1024; +const FILE_COPY_CHUNK_BYTES: usize = 1024 * 1024; const SUPPORTED_PIPE_FLAGS: u64 = OPEN_CLOEXEC | OPEN_NONBLOCK; const CLONE_VM: u64 = 0x100; const CLONE_VFORK: u64 = 0x4000; const SIGCHLD: u64 = 17; const SUPPORTED_CLONE_FLAGS: u64 = CLONE_VM | CLONE_VFORK | SIGCHLD; const DORMANT_THREAD_CLONE_FLAGS: u64 = 0x7d_0f00; +const FUTEX_COMMAND_MASK: u64 = 0x7f; +const FUTEX_WAIT: u64 = 0; +const FUTEX_WAKE: u64 = 1; +const FUTEX_WAIT_BITSET: u64 = 9; +const FUTEX_WAKE_BITSET: u64 = 10; const RUSAGE_SIZE: usize = 144; /// Failure while resolving an executable's optional guest-side interpreter. @@ -329,6 +339,12 @@ struct SuspendedParent { credentials: Credentials, pending_input: Option, dormant_thread: Option, + parked_helpers: Vec, + compiler_thread: Option, + worker_parent: Option, + worker_thread: Option, + next_helper_thread_id: u64, + current_thread_id: u64, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -337,11 +353,18 @@ struct ExitedChild { exit_code: u8, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] struct DormantThread { clear_child_tid: GuestAddress, } +struct SuspendedThread { + state: Aarch64State, + interpreter: Interpreter, + clear_child_tid: Option, + current_thread_id: u64, + peer: Option>, +} + struct DisabledBlockExecutor; impl BlockExecutor for DisabledBlockExecutor { @@ -424,6 +447,21 @@ impl fmt::Display for SyscallEvent { self.arguments[2], self.arguments[3].cast_signed(), )?, + Some(Syscall::Sendfile) => write!( + formatter, + "sendfile(out_fd={}, in_fd={}, offset={:#x}, count={})", + self.arguments[0], self.arguments[1], self.arguments[2], self.arguments[3], + )?, + Some(Syscall::CopyFileRange) => write!( + formatter, + "copy_file_range(in_fd={}, in_offset={:#x}, out_fd={}, out_offset={:#x}, count={}, flags={:#x})", + self.arguments[0], + self.arguments[1], + self.arguments[2], + self.arguments[3], + self.arguments[4], + self.arguments[5], + )?, Some(Syscall::Fstat) => write!( formatter, "fstat(fd={}, statbuf={:#x})", @@ -434,6 +472,11 @@ impl fmt::Display for SyscallEvent { "ftruncate(fd={}, length={})", self.arguments[0], self.arguments[1], )?, + Some(Syscall::Fchmod) => write!( + formatter, + "fchmod(fd={}, mode={:#o})", + self.arguments[0], self.arguments[1], + )?, Some(Syscall::Ppoll) => write!( formatter, "ppoll(fds={:#x}, nfds={}, timeout={:#x}, sigmask={:#x}, sigsetsize={})", @@ -457,6 +500,16 @@ impl fmt::Display for SyscallEvent { self.arguments[0] )?; } + Some(Syscall::Futex) => write!( + formatter, + "futex(address={:#x}, operation={:#x}, value={}, timeout={:#x}, address2={:#x}, value3={})", + self.arguments[0], + self.arguments[1], + self.arguments[2], + self.arguments[3], + self.arguments[4], + self.arguments[5], + )?, Some(Syscall::Getpid) => write!(formatter, "getpid()")?, Some(Syscall::Getppid) => write!(formatter, "getppid()")?, Some(Syscall::Gettid) => write!(formatter, "gettid()")?, @@ -708,10 +761,17 @@ pub struct Process { trace: Vec, pending_input: Option, current_process_id: u64, + current_thread_id: u64, parent_process_id: u64, suspended_parent: Option, + suspended_thread: Option, exited_child: Option, dormant_thread: Option, + parked_helpers: Vec, + compiler_thread: Option, + worker_parent: Option, + worker_thread: Option, + next_helper_thread_id: u64, } impl Process { @@ -749,10 +809,17 @@ impl Process { trace: Vec::new(), pending_input: None, current_process_id: INITIAL_PROCESS_ID, + current_thread_id: INITIAL_PROCESS_ID, parent_process_id: 0, suspended_parent: None, + suspended_thread: None, exited_child: None, dormant_thread: None, + parked_helpers: Vec::new(), + compiler_thread: None, + worker_parent: None, + worker_thread: None, + next_helper_thread_id: NESTED_HELPER_THREAD_ID, }) } @@ -970,25 +1037,8 @@ impl Process { arguments: [u64; 6], ) -> Result, ExecutionError> { match Syscall::from_number(number) { - Some(Syscall::Exit | Syscall::ExitGroup) => { - let exit_code = self.register(0).to_le_bytes()[0]; - self.clear_child_tid(); - self.clear_dormant_thread_tid(); - self.trace.push(SyscallEvent { - number, - arguments, - outcome: SyscallOutcome::Exited(exit_code), - }); - if self.suspended_parent.is_some() { - self.finish_child(filesystem, exit_code); - return Ok(None); - } - return Ok(Some(ExecutionEvent::Exited(ExecutionResult { - exit_code, - executed_instructions: self.executed_instructions, - dispatched_syscalls: self.dispatched_syscalls, - output_bytes: self.output_bytes, - }))); + Some(syscall @ (Syscall::Exit | Syscall::ExitGroup)) => { + return Ok(self.dispatch_exit(filesystem, syscall, number, arguments)); } Some(Syscall::Getcwd) => self.dispatch_getcwd(), Some(Syscall::Fcntl) => self.dispatch_fcntl(), @@ -1006,6 +1056,13 @@ impl Process { Some(Syscall::Newfstatat) => self.dispatch_newfstatat(filesystem), Some(Syscall::Fstat) => self.dispatch_fstat(filesystem), Some(Syscall::Ftruncate) => self.dispatch_ftruncate(filesystem), + Some(Syscall::Fchmod) => { + self.set_return(if self.file_handle(self.register(0)).is_some() { + 0 + } else { + Errno::BadFileDescriptor.return_value() + }); + } Some(Syscall::Read) => { if self.register(0) == STANDARD_INPUT { if let Some(request) = self.dispatch_terminal_read(input, arguments) { @@ -1016,16 +1073,20 @@ impl Process { } } Some(Syscall::Pread64) => self.dispatch_pread64(filesystem), + Some(Syscall::Sendfile) => self.dispatch_sendfile(filesystem), + Some(Syscall::CopyFileRange) => self.dispatch_copy_file_range(filesystem), Some(Syscall::Ppoll) => self.set_return(0), Some(Syscall::Write) => self.dispatch_write(terminal, filesystem)?, Some(Syscall::Writev) => self.dispatch_writev(terminal, filesystem)?, Some(Syscall::SetTidAddress) => { self.clear_child_tid = Some(GuestAddress::new(self.register(0))); - self.set_return(self.current_process_id); + self.set_return(self.current_thread_id); } - Some(Syscall::Getpid | Syscall::Gettid) => { + Some(Syscall::Futex) => self.dispatch_futex(), + Some(Syscall::Getpid) => { self.set_return(self.current_process_id); } + Some(Syscall::Gettid) => self.set_return(self.current_thread_id), Some(Syscall::Getppid) => self.set_return(self.parent_process_id), Some(Syscall::Clone) => self.dispatch_clone(), Some(Syscall::Execve) => self.dispatch_execve(filesystem, system), @@ -1049,6 +1110,55 @@ impl Process { Ok(None) } + fn dispatch_exit( + &mut self, + filesystem: &mut F, + syscall: Syscall, + number: u64, + arguments: [u64; 6], + ) -> Option { + let exit_code = self.register(0).to_le_bytes()[0]; + self.clear_child_tid(); + self.trace.push(SyscallEvent { + number, + arguments, + outcome: SyscallOutcome::Exited(exit_code), + }); + if self.suspended_parent.is_some() { + self.finish_child(filesystem, exit_code); + return None; + } + if syscall == Syscall::Exit && self.worker_parent.is_some() { + self.finish_worker_thread(); + return None; + } + if syscall == Syscall::Exit && self.current_thread_id >= NESTED_HELPER_THREAD_ID { + if self.compiler_thread.is_some() { + self.finish_running_helper(); + } else { + self.finish_thread(); + } + return None; + } + if syscall == Syscall::Exit && self.suspended_thread.is_some() { + self.clear_parked_helper_tids(); + self.parked_helpers.clear(); + self.compiler_thread = None; + self.worker_parent = None; + self.worker_thread = None; + self.finish_thread(); + return None; + } + self.clear_dormant_thread_tid(); + self.clear_parked_helper_tids(); + Some(ExecutionEvent::Exited(ExecutionResult { + exit_code, + executed_instructions: self.executed_instructions, + dispatched_syscalls: self.dispatched_syscalls, + output_bytes: self.output_bytes, + })) + } + fn dispatch_clone(&mut self) { if self.register(0) == DORMANT_THREAD_CLONE_FLAGS { self.dispatch_dormant_thread_clone(); @@ -1086,14 +1196,26 @@ impl Process { config: self.config, credentials: self.credentials, pending_input: self.pending_input, - dormant_thread: self.dormant_thread, + dormant_thread: self.dormant_thread.take(), + parked_helpers: core::mem::take(&mut self.parked_helpers), + compiler_thread: self.compiler_thread.take(), + worker_parent: self.worker_parent.take(), + worker_thread: self.worker_thread.take(), + next_helper_thread_id: self.next_helper_thread_id, + current_thread_id: self.current_thread_id, }; self.suspended_parent = Some(parent); self.current_process_id = CHILD_PROCESS_ID; + self.current_thread_id = CHILD_PROCESS_ID; self.parent_process_id = INITIAL_PROCESS_ID; self.clear_child_tid = None; self.pending_input = None; self.dormant_thread = None; + self.parked_helpers.clear(); + self.compiler_thread = None; + self.worker_parent = None; + self.worker_thread = None; + self.next_helper_thread_id = NESTED_HELPER_THREAD_ID; if child_stack != 0 { self.state.set_sp(GuestAddress::new(child_stack)); } @@ -1101,10 +1223,19 @@ impl Process { } fn dispatch_dormant_thread_clone(&mut self) { - if self.suspended_parent.is_some() - || self.exited_child.is_some() - || self.dormant_thread.is_some() - { + let nested_helper = self.current_thread_id == ACTIVE_THREAD_ID + || self.current_thread_id >= NESTED_HELPER_THREAD_ID; + let worker_clone = self.current_thread_id >= NESTED_HELPER_THREAD_ID; + let invalid_thread_state = if worker_clone { + self.compiler_thread.is_none() + || self.worker_parent.is_some() + || self.worker_thread.is_some() + } else if nested_helper { + self.compiler_thread.is_some() || self.parked_helpers.len() >= MAX_NESTED_HELPERS + } else { + self.suspended_thread.is_some() + }; + if self.suspended_parent.is_some() || self.exited_child.is_some() || invalid_thread_state { self.set_return(Errno::TryAgain.return_value()); return; } @@ -1120,16 +1251,205 @@ impl Process { self.set_return(Errno::InvalidArgument.return_value()); return; } - let process_id = CHILD_PROCESS_ID; - let process_id_bytes = u32::try_from(process_id) + let thread_id = if nested_helper { + let thread_id = self.next_helper_thread_id; + self.next_helper_thread_id = self + .next_helper_thread_id + .checked_add(1) + .expect("the bounded helper thread ID does not overflow"); + thread_id + } else if self.dormant_thread.is_some() { + ACTIVE_THREAD_ID + } else { + DORMANT_THREAD_ID + }; + let thread_id_bytes = u32::try_from(thread_id) .expect("the bounded helper thread ID fits u32") .to_le_bytes(); - if self.memory.write(parent_tid, &process_id_bytes).is_err() { + if self.memory.write(parent_tid, &thread_id_bytes).is_err() { self.set_return(Errno::Fault.return_value()); return; } + let Ok(child_interpreter) = Interpreter::new() else { + self.set_return(Errno::OutOfMemory.return_value()); + return; + }; + self.set_return(thread_id); + let peer = if worker_clone { + None + } else { + self.suspended_thread.take().map(Box::new) + }; + let parent = SuspendedThread { + state: self.state.clone(), + interpreter: core::mem::replace(&mut self.interpreter, child_interpreter), + clear_child_tid: self.clear_child_tid, + current_thread_id: self.current_thread_id, + peer, + }; + if worker_clone { + self.worker_parent = Some(parent); + } else { + self.suspended_thread = Some(parent); + } + self.clear_child_tid = Some(clear_child_tid); + self.current_thread_id = thread_id; + self.state.set_sp(GuestAddress::new(stack)); + self.state.set_tpidr_el0(tls); + self.set_return(0); + } + + fn finish_thread(&mut self) { + let parent = self + .suspended_thread + .take() + .expect("a thread exit has a suspended parent thread"); + self.state = parent.state; + self.interpreter = parent.interpreter; + self.clear_child_tid = parent.clear_child_tid; + self.current_thread_id = parent.current_thread_id; + self.suspended_thread = parent.peer.map(|peer| *peer); + } + + fn switch_thread(&mut self) { + let mut thread = self + .suspended_thread + .take() + .expect("a cooperative thread switch has a suspended peer"); + self.swap_thread_context(&mut thread); + self.suspended_thread = Some(thread); + } + + fn swap_thread_context(&mut self, thread: &mut SuspendedThread) { + core::mem::swap(&mut self.state, &mut thread.state); + core::mem::swap(&mut self.interpreter, &mut thread.interpreter); + core::mem::swap(&mut self.clear_child_tid, &mut thread.clear_child_tid); + core::mem::swap(&mut self.current_thread_id, &mut thread.current_thread_id); + } + + fn park_new_helper(&mut self) { + let mut parent = self + .suspended_thread + .take() + .expect("the nested helper has a suspended compiler thread"); + let peer = parent.peer.take(); + self.swap_thread_context(&mut parent); + self.suspended_thread = peer.map(|peer| *peer); + self.parked_helpers.push(parent); + } + + fn schedule_parked_helper(&mut self) { + let mut thread = self.parked_helpers.remove(0); + self.swap_thread_context(&mut thread); + self.compiler_thread = Some(thread); + } + + fn park_running_helper(&mut self) { + let mut thread = self + .compiler_thread + .take() + .expect("a running helper has a suspended compiler thread"); + self.swap_thread_context(&mut thread); + self.parked_helpers.push(thread); + } + + fn finish_running_helper(&mut self) { + let mut thread = self + .compiler_thread + .take() + .expect("a helper exit has a suspended compiler thread"); + self.swap_thread_context(&mut thread); + } + + fn park_worker_thread(&mut self) { + let mut parent = self + .worker_parent + .take() + .expect("a worker thread has a suspended coordinator"); + self.swap_thread_context(&mut parent); + self.worker_thread = Some(parent); + } + + fn schedule_worker_thread(&mut self) { + let mut worker = self + .worker_thread + .take() + .expect("a coordinator has a parked worker thread"); + self.swap_thread_context(&mut worker); + self.worker_parent = Some(worker); + } + + fn finish_worker_thread(&mut self) { + let mut parent = self + .worker_parent + .take() + .expect("a worker exit has a suspended coordinator"); + self.swap_thread_context(&mut parent); + } + + fn park_dormant_thread(&mut self) { + let clear_child_tid = self + .clear_child_tid + .expect("the musl helper clone requested child TID clearing"); + let parent = self + .suspended_thread + .take() + .expect("the dormant helper has a suspended parent thread"); + self.state = parent.state; + self.interpreter = parent.interpreter; + self.clear_child_tid = parent.clear_child_tid; + self.current_thread_id = parent.current_thread_id; + let _ = self.memory.write(clear_child_tid, &0_u32.to_le_bytes()); self.dormant_thread = Some(DormantThread { clear_child_tid }); - self.set_return(process_id); + } + + fn dispatch_futex(&mut self) { + let operation = self.register(1) & FUTEX_COMMAND_MASK; + let wait = operation == FUTEX_WAIT || operation == FUTEX_WAIT_BITSET; + let wake = operation == FUTEX_WAKE || operation == FUTEX_WAKE_BITSET; + if self.current_thread_id == DORMANT_THREAD_ID && wait { + self.park_dormant_thread(); + return; + } + if wake { + self.set_return(0); + return; + } + if !wait { + self.set_return(Errno::NoSystemCall.return_value()); + return; + } + let address = GuestAddress::new(self.register(0)); + if address.get() & 3 != 0 { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + let mut bytes = [0; 4]; + if self.memory.read_exact(address, &mut bytes).is_err() { + self.set_return(Errno::Fault.return_value()); + return; + } + let expected = self.register(2).to_le_bytes(); + if bytes != expected[..4] { + self.set_return(Errno::TryAgain.return_value()); + return; + } + self.set_return(0); + if self.worker_parent.is_some() { + self.park_worker_thread(); + } else if self.worker_thread.is_some() { + self.schedule_worker_thread(); + } else if self.current_thread_id >= NESTED_HELPER_THREAD_ID { + if self.compiler_thread.is_some() { + self.park_running_helper(); + } else { + self.park_new_helper(); + } + } else if self.current_thread_id == ACTIVE_THREAD_ID && !self.parked_helpers.is_empty() { + self.schedule_parked_helper(); + } else if self.suspended_thread.is_some() { + self.switch_thread(); + } } fn finish_child(&mut self, filesystem: &mut F, exit_code: u8) { @@ -1158,7 +1478,13 @@ impl Process { self.credentials = parent.credentials; self.pending_input = parent.pending_input; self.dormant_thread = parent.dormant_thread; + self.parked_helpers = parent.parked_helpers; + self.compiler_thread = parent.compiler_thread; + self.worker_parent = parent.worker_parent; + self.worker_thread = parent.worker_thread; + self.next_helper_thread_id = parent.next_helper_thread_id; self.current_process_id = INITIAL_PROCESS_ID; + self.current_thread_id = parent.current_thread_id; self.parent_process_id = 0; self.exited_child = Some(ExitedChild { process_id: CHILD_PROCESS_ID, @@ -1280,6 +1606,11 @@ impl Process { self.credentials = image.credentials; self.pending_input = None; self.dormant_thread = None; + self.parked_helpers.clear(); + self.compiler_thread = None; + self.worker_parent = None; + self.worker_thread = None; + self.next_helper_thread_id = NESTED_HELPER_THREAD_ID; } fn read_exec_vector(&self, address: GuestAddress) -> Result>, Errno> { @@ -1966,10 +2297,7 @@ impl Process { .get(&file_descriptor) .unwrap_or(&0) }), - FCNTL_SET_STATUS_FLAGS - if self.pipe_descriptors.contains_key(&file_descriptor) - && self.register(2) & !OPEN_NONBLOCK == 0 => - { + FCNTL_SET_STATUS_FLAGS if self.pipe_descriptors.contains_key(&file_descriptor) => { let access = self .descriptor_status_flags .get(&file_descriptor) @@ -1977,7 +2305,7 @@ impl Process { .unwrap_or(0) & OPEN_ACCESS_MASK; self.descriptor_status_flags - .insert(file_descriptor, access | self.register(2)); + .insert(file_descriptor, access | (self.register(2) & OPEN_NONBLOCK)); self.set_return(0); } FCNTL_SET_STATUS_FLAGS @@ -2272,6 +2600,79 @@ impl Process { self.set_return(read as u64); } + fn dispatch_sendfile(&mut self, filesystem: &mut F) { + if self.register(2) != 0 { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + self.copy_file_bytes( + filesystem, + self.register(1), + self.register(0), + self.register(3), + ); + } + + fn dispatch_copy_file_range(&mut self, filesystem: &mut F) { + if self.register(1) != 0 || self.register(3) != 0 || self.register(5) != 0 { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + self.copy_file_bytes( + filesystem, + self.register(0), + self.register(2), + self.register(4), + ); + } + + fn copy_file_bytes( + &mut self, + filesystem: &mut F, + input_descriptor: u64, + output_descriptor: u64, + count: u64, + ) { + let (Some(input), Some(output)) = ( + self.file_handle(input_descriptor), + self.file_handle(output_descriptor), + ) else { + self.set_return(Errno::BadFileDescriptor.return_value()); + return; + }; + if input == output { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + let requested = usize::try_from(count) + .unwrap_or(usize::MAX) + .min(FILE_COPY_CHUNK_BYTES); + let mut bytes = vec![0; requested]; + let read = match filesystem.read(input, &mut bytes) { + Ok(read) => read, + Err(error) => { + self.set_return(filesystem_error_return(error)); + return; + } + }; + let written = match filesystem.write(output, &bytes[..read]) { + Ok(written) => written, + Err(error) => { + if let Ok(rewind) = i64::try_from(read) { + let _ = filesystem.seek(input, -rewind, FileSeekFrom::Current); + } + self.set_return(filesystem_error_return(error)); + return; + } + }; + if written < read + && let Ok(rewind) = i64::try_from(read - written) + { + let _ = filesystem.seek(input, -rewind, FileSeekFrom::Current); + } + self.set_return(written as u64); + } + fn dispatch_file_write(&mut self, filesystem: &mut F) { let file_descriptor = self.register(0); let count = self.register(2); @@ -2898,6 +3299,18 @@ impl Process { .memory .write(thread.clear_child_tid, &0_u32.to_le_bytes()); } + + fn clear_parked_helper_tids(&mut self) { + for address in self + .parked_helpers + .iter() + .chain(self.worker_thread.iter()) + .filter_map(|thread| thread.clear_child_tid) + .filter(|address| address.get() != 0) + { + let _ = self.memory.write(address, &0_u32.to_le_bytes()); + } + } } fn directory_inode(name: &[u8]) -> u64 { @@ -3028,10 +3441,11 @@ mod tests { use binarrow_runtime_core::{GuestAddress, ResourceLimit, Trap}; use super::{ - AT_FDCWD, AnonymousPipe, CHILD_PROCESS_ID, DESCRIPTOR_CLOEXEC, DORMANT_THREAD_CLONE_FLAGS, - ExecutionError, ExecutionEvent, FCNTL_GET_DESCRIPTOR_FLAGS, FCNTL_GET_STATUS_FLAGS, - FCNTL_SET_STATUS_FLAGS, INITIAL_PROCESS_ID, IOCTL_CLEAR_CLOSE_ON_EXEC, - IOCTL_SET_CLOSE_ON_EXEC, MAP_FIXED, MAP_PRIVATE, OPEN_CLOEXEC, OPEN_DIRECTORY, OPEN_NOCTTY, + ACTIVE_THREAD_ID, AT_FDCWD, AnonymousPipe, CHILD_PROCESS_ID, DESCRIPTOR_CLOEXEC, + DORMANT_THREAD_CLONE_FLAGS, DORMANT_THREAD_ID, ExecutionError, ExecutionEvent, + FCNTL_GET_DESCRIPTOR_FLAGS, FCNTL_GET_STATUS_FLAGS, FCNTL_SET_STATUS_FLAGS, FUTEX_WAIT, + INITIAL_PROCESS_ID, IOCTL_CLEAR_CLOSE_ON_EXEC, IOCTL_SET_CLOSE_ON_EXEC, Interpreter, + MAP_FIXED, MAP_PRIVATE, NESTED_HELPER_THREAD_ID, OPEN_CLOEXEC, OPEN_DIRECTORY, OPEN_NOCTTY, OPEN_NOFOLLOW, OPEN_NONBLOCK, OPEN_PATH, PAGE_SIZE, PIPE_CAPACITY_BYTES, PROT_EXECUTE, PROT_READ, PipeEnd, Process, STANDARD_OUTPUT, STAT_CHARACTER_MODE, STAT_FIFO_MODE, STAT_FILE_SIZE_OFFSET, STAT_MODE_OFFSET, STAT_REGULAR_MODE, STAT_SIZE, @@ -3475,6 +3889,68 @@ mod tests { ); } + #[test] + fn copy_file_range_copies_between_descriptor_offsets() { + let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); + let mut process = Process::new(image).unwrap(); + let mut filesystem = MemoryFileSystem::new(1024); + let source_writer = filesystem + .open( + b"/tmp/source.txt", + FileOpenOptions { + access: FileAccess::WriteOnly, + flags: FileOpenFlags::CREATE, + }, + ) + .unwrap(); + filesystem.write(source_writer, b"copy me").unwrap(); + filesystem.close(source_writer).unwrap(); + let source = filesystem + .open( + b"/tmp/source.txt", + FileOpenOptions { + access: FileAccess::ReadOnly, + flags: FileOpenFlags::NONE, + }, + ) + .unwrap(); + let destination = filesystem + .open( + b"/tmp/destination.txt", + FileOpenOptions { + access: FileAccess::WriteOnly, + flags: FileOpenFlags::CREATE, + }, + ) + .unwrap(); + process.file_descriptors.insert(3, source); + process.file_descriptors.insert(4, destination); + process.state.set_x(0, 3).unwrap(); + process.state.set_x(1, 0).unwrap(); + process.state.set_x(2, 4).unwrap(); + process.state.set_x(3, 0).unwrap(); + process.state.set_x(4, 7).unwrap(); + process.state.set_x(5, 0).unwrap(); + + process.dispatch_copy_file_range(&mut filesystem); + + assert_eq!(process.register(0), 7); + assert_eq!( + filesystem.read_file(b"/tmp/destination.txt"), + Some(b"copy me".as_slice()) + ); + assert_eq!( + filesystem.seek(source, 0, FileSeekFrom::Current).unwrap(), + 7 + ); + assert_eq!( + filesystem + .seek(destination, 0, FileSeekFrom::Current) + .unwrap(), + 7 + ); + } + #[test] fn ftruncate_resizes_without_changing_the_file_offset() { let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); @@ -4082,7 +4558,10 @@ mod tests { assert_eq!(process.register(0), 0); process.state.set_x(0, u64::from(reader)).unwrap(); process.state.set_x(1, FCNTL_SET_STATUS_FLAGS).unwrap(); - process.state.set_x(2, OPEN_NONBLOCK).unwrap(); + process + .state + .set_x(2, OPEN_NONBLOCK | OPEN_NOFOLLOW) + .unwrap(); process.dispatch_fcntl(); assert_eq!(process.register(0), 0); process.state.set_x(0, u64::from(reader)).unwrap(); @@ -4196,14 +4675,14 @@ mod tests { } #[test] - fn accepts_one_dormant_musl_signal_helper_thread() { + fn serializes_one_active_thread_beside_the_dormant_signal_helper() { let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); let mut process = Process::new(image).unwrap(); let parent_tid = process.state.sp().checked_sub(32).unwrap(); let clear_child_tid = process.state.sp().checked_sub(28).unwrap(); process .memory - .write(clear_child_tid, &CHILD_PROCESS_ID.to_le_bytes()[..4]) + .write(clear_child_tid, &DORMANT_THREAD_ID.to_le_bytes()[..4]) .unwrap(); process.state.set_x(0, DORMANT_THREAD_CLONE_FLAGS).unwrap(); process @@ -4214,18 +4693,61 @@ mod tests { process.state.set_x(3, 0x1234).unwrap(); process.state.set_x(4, clear_child_tid.get()).unwrap(); process.dispatch_clone(); - assert_eq!(process.register(0), CHILD_PROCESS_ID); + assert_eq!(process.register(0), 0); + assert_eq!(process.current_thread_id, DORMANT_THREAD_ID); + assert!(process.suspended_thread.is_some()); + process.state.set_x(0, clear_child_tid.get()).unwrap(); + process.state.set_x(1, FUTEX_WAIT).unwrap(); + process.dispatch_futex(); + assert_eq!(process.register(0), DORMANT_THREAD_ID); + assert_eq!(process.current_thread_id, INITIAL_PROCESS_ID); + assert!(process.dormant_thread.is_some()); let mut tid = [0; 4]; process.memory.read_exact(parent_tid, &mut tid).unwrap(); assert_eq!( u32::from_le_bytes(tid), - u32::try_from(CHILD_PROCESS_ID).unwrap() + u32::try_from(DORMANT_THREAD_ID).unwrap() ); + process + .memory + .read_exact(clear_child_tid, &mut tid) + .unwrap(); + assert_eq!(u32::from_le_bytes(tid), 0); + + process.state.set_x(0, DORMANT_THREAD_CLONE_FLAGS).unwrap(); + process.dispatch_clone(); + assert_eq!(process.register(0), 0); + assert_eq!(process.current_thread_id, ACTIVE_THREAD_ID); + assert!(process.suspended_thread.is_some()); + process.memory.read_exact(parent_tid, &mut tid).unwrap(); + assert_eq!( + u32::from_le_bytes(tid), + u32::try_from(ACTIVE_THREAD_ID).unwrap() + ); + + process.state.set_x(0, clear_child_tid.get()).unwrap(); + process.state.set_x(1, FUTEX_WAIT).unwrap(); + process.state.set_x(2, 0).unwrap(); + process.dispatch_futex(); + assert_eq!(process.current_thread_id, INITIAL_PROCESS_ID); + assert_eq!(process.register(0), ACTIVE_THREAD_ID); + assert!(process.suspended_thread.is_some()); process.state.set_x(0, DORMANT_THREAD_CLONE_FLAGS).unwrap(); process.dispatch_clone(); assert_eq!(process.register(0), Errno::TryAgain.return_value()); + process.state.set_x(0, clear_child_tid.get()).unwrap(); + process.state.set_x(1, FUTEX_WAIT).unwrap(); + process.state.set_x(2, 0).unwrap(); + process.dispatch_futex(); + assert_eq!(process.current_thread_id, ACTIVE_THREAD_ID); + assert_eq!(process.register(0), 0); + + process.clear_child_tid(); + process.finish_thread(); + assert_eq!(process.current_thread_id, INITIAL_PROCESS_ID); + assert_eq!(process.register(0), 0); process.clear_dormant_thread_tid(); process .memory @@ -4234,6 +4756,51 @@ mod tests { assert_eq!(u32::from_le_bytes(tid), 0); } + #[test] + fn cooperatively_schedules_a_nested_helper_with_the_active_thread() { + let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); + let mut process = Process::new(image).unwrap(); + let parent_tid = process.state.sp().checked_sub(32).unwrap(); + let clear_child_tid = process.state.sp().checked_sub(28).unwrap(); + process.current_thread_id = ACTIVE_THREAD_ID; + process.suspended_thread = Some(super::SuspendedThread { + state: process.state.clone(), + interpreter: Interpreter::new().unwrap(), + clear_child_tid: None, + current_thread_id: INITIAL_PROCESS_ID, + peer: None, + }); + process.state.set_x(0, DORMANT_THREAD_CLONE_FLAGS).unwrap(); + process + .state + .set_x(1, process.state.sp().checked_sub(4096).unwrap().get()) + .unwrap(); + process.state.set_x(2, parent_tid.get()).unwrap(); + process.state.set_x(3, 0x5678).unwrap(); + process.state.set_x(4, clear_child_tid.get()).unwrap(); + process.dispatch_clone(); + assert_eq!(process.current_thread_id, NESTED_HELPER_THREAD_ID); + + for expected_thread in [ACTIVE_THREAD_ID, NESTED_HELPER_THREAD_ID, ACTIVE_THREAD_ID] { + process.state.set_x(0, clear_child_tid.get()).unwrap(); + process.state.set_x(1, FUTEX_WAIT).unwrap(); + process.state.set_x(2, 0).unwrap(); + process.dispatch_futex(); + assert_eq!(process.current_thread_id, expected_thread); + } + assert_eq!(process.parked_helpers.len(), 1); + assert!(process.compiler_thread.is_none()); + assert!(process.suspended_thread.is_some()); + + process.schedule_parked_helper(); + process.clear_child_tid(); + process.finish_running_helper(); + assert_eq!(process.current_thread_id, ACTIVE_THREAD_ID); + assert!(process.parked_helpers.is_empty()); + assert!(process.compiler_thread.is_none()); + assert!(process.suspended_thread.is_some()); + } + #[test] fn wait4_preserves_unreaped_status_on_fault() { let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); diff --git a/docs/architecture.md b/docs/architecture.md index 14aa129..0484c10 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -72,7 +72,7 @@ Implements the Phase 2 ephemeral filesystem behind `binarrow-host-api`. It norma ### `binarrow-linux-runtime` -Consumes a loaded process image, owns its architectural execution state and descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture plus `openat`, `faccessat`, `close`, `lseek`, regular-file `read`/`write`, bounded `pipe2`, constrained `clone`/`wait4`, PID queries, and static-ELF `execve`. Anonymous pipes are process-owned rather than host filesystem objects: each has a fixed 64 KiB queue, reader/writer lifetime, FIFO `fstat` identity, close-on-exec flags, mutable `O_NONBLOCK` status, EOF and broken-pipe behavior, and deterministic `EAGAIN` at empty/full boundaries. Blocking-mode descriptors support spawn paths whose reads and writes complete immediately; actual suspension and wakeup at a would-block boundary remain deferred to the general scheduler. The first child-process mode accepts exactly `CLONE_VM | CLONE_VFORK | SIGCHLD`, supports an optional child stack, ignores unused legacy TID/TLS pointer registers, suspends one parent while PID 2 runs, permits the child to replace itself, restores the parent's CPU/memory/interpreter state at child exit, and exposes the encoded status through `wait4`. The cloned descriptor table shares host open-file descriptions and anonymous pipe queues with the parent, so offsets and bytes cross the child boundary while child `close` and `O_CLOEXEC` affect only its copies. It rejects nested/unreaped children, TID/TLS features, and other clone modes; this is a spawn/exec stepping stone, not general fork semantics. `*at` calls resolve absolute paths independently of their directory descriptor and relative paths against `AT_FDCWD` or the retained guest path of an open directory descriptor; `O_PATH`, no-follow, no-controlling-terminal, directory, and close-on-exec flags cover toolchain traversal without exposing host paths. Successful process replacement reads a bounded pathname and argument/environment vectors from the old address space, loads the new executable through `HostFileSystem`, rebuilds the process image, closes `O_CLOEXEC` descriptors, and retains the current directory, credentials, signal mask, ordinary descriptors, resource counters, and trace history. Failed replacement leaves the old process image and descriptors intact. File contents remain behind `HostFileSystem`; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return `ENOSYS`, invalid arguments return Linux errno values, and terminal 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 descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture and guest compilers, including `openat`, `faccessat`, `close`, `lseek`, regular-file `read`/`write`/`copy_file_range`, bounded `pipe2`, constrained `clone`/`wait4`, futex wait/wake, thread/PID queries, and static-ELF `execve`. Anonymous pipes are process-owned rather than host filesystem objects: each has a fixed 64 KiB queue, reader/writer lifetime, FIFO `fstat` identity, close-on-exec flags, mutable `O_NONBLOCK` status, EOF and broken-pipe behavior, and deterministic `EAGAIN` at empty/full boundaries. Blocking-mode descriptors support spawn paths whose reads and writes complete immediately. A bounded cooperative thread topology schedules rustc's signal waiter, compiler worker, nested helpers, and coordinator/worker pair at futex boundaries while retaining one host thread; it is a compiler compatibility model rather than general pthread semantics. The child-process mode accepts exactly `CLONE_VM | CLONE_VFORK | SIGCHLD`, supports an optional child stack, suspends one parent while PID 2 runs, permits the child to replace itself, restores the parent's CPU/memory/interpreter state at child exit, and exposes the encoded status through `wait4`. The cloned descriptor table shares host open-file descriptions and anonymous pipe queues with the parent, so offsets and bytes cross the child boundary while child `close` and `O_CLOEXEC` affect only its copies. It rejects nested/unreaped children and other clone modes; this is a spawn/exec stepping stone, not general fork semantics. `*at` calls resolve absolute paths independently of their directory descriptor and relative paths against `AT_FDCWD` or the retained guest path of an open directory descriptor; `O_PATH`, no-follow, no-controlling-terminal, directory, and close-on-exec flags cover toolchain traversal without exposing host paths. Successful process replacement reads a bounded pathname and argument/environment vectors from the old address space, loads the new executable through `HostFileSystem`, rebuilds the process image, closes `O_CLOEXEC` descriptors, and retains the current directory, credentials, signal mask, ordinary descriptors, resource counters, and trace history. Failed replacement leaves the old process image and descriptors intact. File contents remain behind `HostFileSystem`; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return `ENOSYS`, invalid arguments return Linux errno values, and terminal 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` diff --git a/toolchains/rust-musl/README.md b/toolchains/rust-musl/README.md index 090ede9..7d4306a 100644 --- a/toolchains/rust-musl/README.md +++ b/toolchains/rust-musl/README.md @@ -14,21 +14,28 @@ Prerequisites: - the repository Rust toolchain - `curl`, `make`, `tar`, and `shasum` -Build and run the initial host-tool verification: +Build and run the host-tool and source-to-ELF verification: ```sh toolchains/rust-musl/build.sh toolchains/rust-musl/verify.sh ``` -Rust's host driver expects `libgcc_s.so.1`; the package produces that -compatibility DSO from the PIC LLVM `libunwind.a` already distributed in the -official musl standard-library component. The guest musl loader resolves the -driver, compiler proc-macro DSOs, libc, and unwind runtime. - -The current process checkpoint admits exactly one dormant detached pthread -shape used by rustc's Ctrl-C signal waiter. It writes the parent TID and clears -the child TID at process teardown, but deliberately does not schedule the -helper. General worker threads and futex scheduling remain unsupported. The -next checkpoint is a no-dependency source-to-static-ELF compile with a -single-codegen-unit policy and the packaged Clang/musl linker. +Rust's host tools expect `libgcc_s.so.1`; the package produces that +compatibility DSO from the PIC LLVM `libunwind.a` distributed in the official +musl standard-library component, selected Zig compiler-rt builtins, and the +frame-registration compatibility entry points required by the bundled LLD. +The guest musl loader resolves the driver, compiler proc-macro DSOs, libc, +unwind runtime, and linker. + +The verifier first checks rustc's exact version, then compiles a checked-in +`no_std` source file with one codegen unit and panic abort. It invokes the +packaged LLD over the persisted object and finally runs the static ELF, which +prints `guest rustc hello` and exits 42. All object, linker, and filesystem +snapshots remain under `.tmp`. + +Rustc's bounded cooperative thread topology includes its signal waiter, +compiler worker, nested helpers, and coordinator/worker pair. Futex wait/wake, +thread IDs, final artifact copying, and compiler/linker memory requirements are +covered without exposing host threads. General-purpose guest threading remains +outside this checkpoint. diff --git a/toolchains/rust-musl/build.sh b/toolchains/rust-musl/build.sh index 123b6ae..05e07ef 100755 --- a/toolchains/rust-musl/build.sh +++ b/toolchains/rust-musl/build.sh @@ -15,6 +15,9 @@ zig_cache_directory=$work_directory/zig-cache cargo_home=$workspace_directory/.tmp/cargo-home cargo_target=$workspace_directory/.tmp/cargo-target rust_tmp=$workspace_directory/.tmp/rust-tmp +compiler_rt=$work_directory/compiler-rt.a +exported_compiler_rt=$work_directory/compiler-rt-exported.a +libgcc_compat=$work_directory/libgcc-compat.o dist_url=https://static.rust-lang.org/dist/2026-01-22 rustc_archive=rustc-1.93.0-aarch64-unknown-linux-musl.tar.xz rustc_sha256=5371915850179d910d3eca32cb8f9240c336a1fdc830286242daf4b26227295f @@ -117,22 +120,42 @@ if [ ! -f "$installation_marker" ]; then touch "$installation_marker" fi -# Rust's musl host driver expects libgcc_s, while its target standard library -# ships a PIC LLVM libunwind archive. Export that archive under the expected -# compatibility SONAME instead of adding a second C runtime distribution. +# Rust's musl host tools expect libgcc_s. Its target standard library ships a +# PIC LLVM libunwind archive, while Zig supplies the compiler-rt builtins used +# by rust-lld. Export both under the expected compatibility SONAME. libunwind=$image_root/usr/local/lib/rustlib/aarch64-unknown-linux-musl/lib/self-contained/libunwind.a +"$zig" build-lib \ + -target aarch64-linux-musl \ + -fPIC \ + -fcompiler-rt \ + -femit-bin="$compiler_rt" \ + "$workspace_directory/guest-tests/dynamic-musl/compiler-rt-root.zig" +cp "$compiler_rt" "$exported_compiler_rt" +"$llvm_objcopy" \ + --set-symbols-visibility="$toolchain_directory/compiler-rt-exports.txt=default" \ + "$exported_compiler_rt" +"$zig" cc \ + -target aarch64-linux-musl \ + -fPIC \ + -fno-stack-protector \ + -fno-sanitize=undefined \ + -c "$toolchain_directory/libgcc-compat.c" \ + -o "$libgcc_compat" "$zig" ld.lld \ -shared \ -soname libgcc_s.so.1 \ - --whole-archive "$libunwind" \ + --whole-archive "$exported_compiler_rt" "$libunwind" \ --no-whole-archive \ + "$libgcc_compat" \ -o "$image_root/usr/local/lib/libgcc_s.so.1" -find "$image_root/usr/local/lib" -maxdepth 1 -type f -name '*.so' \ +find "$image_root/usr/local/lib" -type f -name '*.so' \ -exec "$llvm_objcopy" --strip-all {} \; "$llvm_objcopy" --strip-all "$image_root/usr/local/bin/cargo" "$llvm_objcopy" --strip-all "$image_root/usr/local/bin/rustc" "$llvm_objcopy" --strip-all "$image_root/usr/local/lib/libgcc_s.so.1" +"$llvm_objcopy" --strip-all \ + "$image_root/usr/local/lib/rustlib/aarch64-unknown-linux-musl/bin/rust-lld" rm -f \ "$image_root/usr/local/bin/rustdoc" \ "$image_root/usr/local/bin/rust-gdb" \ diff --git a/toolchains/rust-musl/compiler-rt-exports.txt b/toolchains/rust-musl/compiler-rt-exports.txt new file mode 100644 index 0000000..c38e2f2 --- /dev/null +++ b/toolchains/rust-musl/compiler-rt-exports.txt @@ -0,0 +1,11 @@ +__clear_cache +__divtf3 +__extendsftf2 +__floatsitf +__floatunditf +__getf2 +__gttf2 +__letf2 +__multf3 +__trunctfdf2 +__unordtf2 diff --git a/toolchains/rust-musl/libgcc-compat.c b/toolchains/rust-musl/libgcc-compat.c new file mode 100644 index 0000000..587c8db --- /dev/null +++ b/toolchains/rust-musl/libgcc-compat.c @@ -0,0 +1,9 @@ +void __register_frame_info(const void *begin, void *object) { + (void)begin; + (void)object; +} + +void *__deregister_frame_info(const void *begin) { + (void)begin; + return 0; +} diff --git a/toolchains/rust-musl/project/linker.rsp b/toolchains/rust-musl/project/linker.rsp new file mode 100644 index 0000000..6c39f76 --- /dev/null +++ b/toolchains/rust-musl/project/linker.rsp @@ -0,0 +1,7 @@ +--threads=1 +-static +-e +_start +-o +/project/rustc-hello +/project/rustc-hello.o diff --git a/toolchains/rust-musl/project/main.rs b/toolchains/rust-musl/project/main.rs new file mode 100644 index 0000000..f53f38b --- /dev/null +++ b/toolchains/rust-musl/project/main.rs @@ -0,0 +1,28 @@ +#![no_main] +#![no_std] + +use core::arch::asm; +use core::panic::PanicInfo; + +static MESSAGE: &[u8] = b"guest rustc hello\n"; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + unsafe { + asm!( + "svc 0", + in("x8") 64_usize, + in("x0") 1_usize, + in("x1") MESSAGE.as_ptr(), + in("x2") MESSAGE.len(), + ); + asm!("svc 0", in("x8") 93_usize, in("x0") 42_usize, options(noreturn)); + } +} + +#[panic_handler] +fn panic(_info: &PanicInfo<'_>) -> ! { + loop { + core::hint::spin_loop(); + } +} diff --git a/toolchains/rust-musl/verify.sh b/toolchains/rust-musl/verify.sh index 9a006c0..27770da 100755 --- a/toolchains/rust-musl/verify.sh +++ b/toolchains/rust-musl/verify.sh @@ -7,12 +7,17 @@ work_directory=$workspace_directory/.tmp/rust-1.93.0-dist filesystem_image=$work_directory/rust-toolchain.bnfs rustc=$work_directory/image-root/usr/local/bin/rustc system_image=$workspace_directory/guest-tests/dynamic-musl/dynamic-musl.bnfs +project_directory=$toolchain_directory/project +project_image=$work_directory/rust-project.bnfs +object_snapshot=$work_directory/rust-object-output.bnfs +linked_snapshot=$work_directory/rust-linked-output.bnfs +exec_helper=$workspace_directory/guest-tests/exec-from-filesystem/exec-from-filesystem.aarch64.elf cargo_home=$workspace_directory/.tmp/cargo-home cargo_target=$workspace_directory/.tmp/cargo-target rust_tmp=$workspace_directory/.tmp/rust-tmp runner=$cargo_target/release/binarrow -for artifact in "$filesystem_image" "$rustc" "$system_image"; do +for artifact in "$filesystem_image" "$rustc" "$system_image" "$exec_helper"; do if [ ! -f "$artifact" ]; then echo "build the Rust toolchain first with toolchains/rust-musl/build.sh" >&2 exit 1 @@ -45,4 +50,60 @@ if [ "$version" != "rustc 1.93.0 (254b59607 2026-01-19)" ]; then exit 1 fi -echo "AArch64 musl rustc host fixture passed" +"$runner" image pack --guest-root /project "$project_directory" "$project_image" +"$runner" run \ + --instruction-budget 150000000 \ + --syscall-budget 10000 \ + --memory-limit 1073741824 \ + --filesystem-limit 838860800 \ + --filesystem-install "$system_image" \ + --filesystem-install "$filesystem_image" \ + --filesystem-install "$project_image" \ + --filesystem-output "$object_snapshot" \ + --random-seed 1 \ + --argv0 /usr/local/bin/rustc \ + --env PATH=/usr/local/bin:/usr/bin:/bin \ + --env LD_LIBRARY_PATH=/usr/local/lib \ + --env RUSTC_BOOTSTRAP=1 \ + "$rustc" \ + -Zno-parallel-backend \ + --edition=2024 \ + -Ccodegen-units=1 \ + -Cdebuginfo=0 \ + -Copt-level=0 \ + -Cpanic=abort \ + --emit=obj \ + -o /project/rustc-hello.o \ + /project/main.rs + +"$runner" run \ + --instruction-budget 50000000 \ + --syscall-budget 5000 \ + --memory-limit 1073741824 \ + --filesystem-limit 838860800 \ + --filesystem-snapshot "$object_snapshot" \ + --filesystem-output "$linked_snapshot" \ + --argv0 /exec-from-filesystem \ + "$exec_helper" \ + /usr/local/lib/rustlib/aarch64-unknown-linux-musl/bin/gcc-ld/ld.lld \ + @/project/linker.rsp + +set +e +program_output=$( + "$runner" run \ + --instruction-budget 1000000 \ + --syscall-budget 100 \ + --memory-limit 67108864 \ + --filesystem-limit 838860800 \ + --filesystem-snapshot "$linked_snapshot" \ + --argv0 /exec-from-filesystem \ + "$exec_helper" /project/rustc-hello +) +program_status=$? +set -e +if [ "$program_status" -ne 42 ] || [ "$program_output" != "guest rustc hello" ]; then + echo "unexpected compiled guest result: status=$program_status output=$program_output" >&2 + exit 1 +fi + +echo "AArch64 musl rustc source-to-ELF fixture passed" -- 2.51.2