From 42e4a0eba01cf74870562ed360595187a05e8472 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 24 Jul 2026 21:32:44 -0700 Subject: [PATCH] feat: compile and link C with packaged clang --- PLAN.md | 4 +- crates/aarch64/src/lib.rs | 60 ++++++ crates/execution-ir/src/lib.rs | 5 + crates/host-api/src/lib.rs | 12 ++ crates/linux-abi/src/lib.rs | 6 + crates/linux-runtime/src/lib.rs | 201 +++++++++++++++++- crates/memory-fs/src/lib.rs | 29 +++ guest-tests/exec-from-filesystem/README.md | 16 ++ guest-tests/exec-from-filesystem/build.sh | 29 +++ .../exec-from-filesystem.aarch64.elf | Bin 0 -> 648 bytes guest-tests/exec-from-filesystem/launcher.S | 21 ++ toolchains/clang-musl/README.md | 11 + toolchains/clang-musl/project/main.c | 5 +- toolchains/clang-musl/verify.sh | 90 ++++++++ 14 files changed, 478 insertions(+), 11 deletions(-) create mode 100644 guest-tests/exec-from-filesystem/README.md create mode 100755 guest-tests/exec-from-filesystem/build.sh create mode 100644 guest-tests/exec-from-filesystem/exec-from-filesystem.aarch64.elf create mode 100644 guest-tests/exec-from-filesystem/launcher.S create mode 100755 toolchains/clang-musl/verify.sh diff --git a/PLAN.md b/PLAN.md index a4462b1..cadba6e 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 5 complete; Phase 6 in progress (packaged Clang prototype) +**Status:** Phase 5 complete; Phase 6 in progress (native Clang/LLD acceptance complete) **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1879,6 +1879,8 @@ Restartable filesystem checkpoints now cover the compiler workload in both hosts The promised distribution now has a reproducible, checksum-pinned producer for LLVM 22.1.8 and musl 1.2.6. It cross-builds 52 MB Clang and 32 MB LLD AArch64 executables as static musl ELFs, packages Clang resource headers plus a 3.7 MB musl sysroot and license notices, and keeps every source, cache, build, and output under the repository's ignored `.tmp` directory. `clang --version` completes inside the native runtime after adding single-threaded `CLREX` handling. A ten-million-instruction `clang -cc1` probe reads `stdio.h` from the packaged sysroot and reaches only the configured instruction boundary inside a hot `memset`, with its filesystem snapshot exported and no further semantic or syscall trap. The next checkpoint is the higher-budget translated Chromium compile, followed by a separate LLD run over the persisted object and execution of the resulting ELF; direct child-process orchestration and pipes remain subsequent Phase 6 work. +The native source-to-ELF acceptance path is now complete and reproducible through `toolchains/clang-musl/verify.sh`. A bounded Clang `-cc1` run compiles a minimal C `main` into `/project/main.o`, a separate bounded LLD run links that object with the packaged musl startup objects and `libc.a`, and a generic checked-in AArch64 launcher uses `execve` to run the guest-created `/project/hello`, which exits 42. The observed workload added scalar `FNEG`, a conservative `DCZID_EL0` value that disables `DC ZVA`, `pread64`, `ftruncate`, and stable path-derived inode identities so Clang does not collapse distinct include roots. Clang completes below a 50-million-instruction ceiling and LLD completes under the same ceiling. The next checkpoint is this exact compile/link/execute sequence in the translated Chromium host; direct child-process orchestration and pipes remain subsequent Phase 6 work. + Do not begin the full web IDE before item 30 passes. --- diff --git a/crates/aarch64/src/lib.rs b/crates/aarch64/src/lib.rs index ac4efae..d247ae2 100644 --- a/crates/aarch64/src/lib.rs +++ b/crates/aarch64/src/lib.rs @@ -831,6 +831,10 @@ fn lower_pcode( destination: lower_place(language, instruction.output)?, source: lower_value(language, left)?, }), + PcodeOp::FloatNegate => Some(Operation::FloatNegate { + destination: lower_place(language, instruction.output)?, + source: lower_value(language, left)?, + }), operation @ (PcodeOp::FloatAdd | PcodeOp::FloatSub | PcodeOp::FloatMul @@ -1289,6 +1293,7 @@ fn architectural_storage(name: &str) -> Option { match name { "sp" => return Some(Storage::Sp), "tpidr_el0" => return Some(Storage::TpidrEl0), + "dczid_el0" => return Some(Storage::DczidEl0), "fpcr" => return Some(Storage::Fpcr), "fpsr" => return Some(Storage::Fpsr), "NG" => return Some(Storage::Flag(Flag::Negative)), @@ -1357,6 +1362,7 @@ fn execute_operations( | Operation::FloatConvert { .. } | Operation::FloatToInteger { .. } | Operation::FloatAbsolute { .. } + | Operation::FloatNegate { .. } | Operation::FloatArithmetic { .. } | Operation::FloatCompare { .. } | Operation::FloatMinimumNumber { .. } @@ -1476,6 +1482,7 @@ fn execute_value_operation( | Operation::FloatConvert { .. } | Operation::FloatToInteger { .. } | Operation::FloatAbsolute { .. } + | Operation::FloatNegate { .. } | Operation::FloatArithmetic { .. } | Operation::FloatCompare { .. } | Operation::FloatMinimumNumber { .. } @@ -1654,6 +1661,13 @@ fn execute_float_value_operation( destination, absolute_float_bits(read(source)?, source.size).ok_or_else(unsupported)?, )), + Operation::FloatNegate { + destination, + source, + } => Ok(( + destination, + negate_float_bits(read(source)?, source.size).ok_or_else(unsupported)?, + )), Operation::FloatArithmetic { destination, left, @@ -1809,6 +1823,14 @@ fn absolute_float_bits(value: u128, size: u8) -> Option { } } +fn negate_float_bits(value: u128, size: u8) -> Option { + match size { + 4 => Some(value ^ (1_u128 << 31)), + 8 => Some(value ^ (1_u128 << 63)), + _ => None, + } +} + fn minimum_number_float_bits(left: u128, right: u128, size: u8) -> Option { match size { 4 => Some(u128::from( @@ -2646,6 +2668,7 @@ fn execute_memory_operation( | Operation::FloatConvert { .. } | Operation::FloatToInteger { .. } | Operation::FloatAbsolute { .. } + | Operation::FloatNegate { .. } | Operation::FloatArithmetic { .. } | Operation::FloatCompare { .. } | Operation::FloatMinimumNumber { .. } @@ -2703,6 +2726,7 @@ fn read_value(state: &Aarch64State, scratch: &ScratchValues, value: Value) -> Op } ValueSource::Storage(Storage::Sp) => u128::from(state.sp().get()), ValueSource::Storage(Storage::TpidrEl0) => u128::from(state.tpidr_el0()), + ValueSource::Storage(Storage::DczidEl0) => 0x10, ValueSource::Storage(Storage::Fpcr) => u128::from(state.fpcr()), ValueSource::Storage(Storage::Fpsr) => u128::from(state.fpsr()), ValueSource::Storage(Storage::Flag(flag)) => u128::from(read_flag(state, flag)), @@ -2744,6 +2768,7 @@ fn write_place( } Storage::Sp => state.set_sp(GuestAddress::new(u64::try_from(value).ok()?)), Storage::TpidrEl0 => state.set_tpidr_el0(u64::try_from(value).ok()?), + Storage::DczidEl0 => return None, Storage::Fpcr => state.set_fpcr(u32::try_from(value).ok()?), Storage::Fpsr => state.set_fpsr(u32::try_from(value).ok()?), Storage::Flag(flag) => write_flag(state, flag, value != 0), @@ -3884,6 +3909,41 @@ mod tests { assert_eq!(state.vector(1), Some(u128::from(42.5_f64.to_bits()))); } + #[test] + fn executes_compiler_scalar_float_negation() { + const CODE: &[u8] = &[ + 0x00, 0x40, 0x61, 0x1e, // fneg d0, d0 + 0x21, 0x40, 0x21, 0x1e, // fneg s1, s1 + ]; + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + state.set_vector(0, u128::from(42.5_f64.to_bits())).unwrap(); + state + .set_vector(1, u128::from((-0.0_f32).to_bits())) + .unwrap(); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!(state.vector(0), Some(u128::from((-42.5_f64).to_bits()))); + assert_eq!(state.vector(1), Some(u128::from(0.0_f32.to_bits()))); + } + + #[test] + fn reports_data_cache_zero_as_prohibited() { + const CODE: &[u8] = &[ + 0xe5, 0x00, 0x3b, 0xd5, // mrs x5, dczid_el0 + ]; + let mut memory = executable_memory(CODE); + let mut state = Aarch64State::new(CODE_ADDRESS, GuestAddress::new(0x8000)); + let mut interpreter = Interpreter::new().unwrap(); + + interpreter.step(&mut state, &mut memory).unwrap(); + + assert_eq!(state.x(5), Some(0x10)); + } + #[test] fn enforces_the_instruction_budget() { let mut memory = executable_memory(SYSCALL_CODE); diff --git a/crates/execution-ir/src/lib.rs b/crates/execution-ir/src/lib.rs index 37a7ec6..c3613d3 100644 --- a/crates/execution-ir/src/lib.rs +++ b/crates/execution-ir/src/lib.rs @@ -14,6 +14,7 @@ pub enum Storage { Vector { index: u8, offset: u8 }, Sp, TpidrEl0, + DczidEl0, Fpcr, Fpsr, Flag(Flag), @@ -132,6 +133,10 @@ pub enum Operation { destination: Place, source: Value, }, + FloatNegate { + destination: Place, + source: Value, + }, FloatArithmetic { destination: Place, left: Value, diff --git a/crates/host-api/src/lib.rs b/crates/host-api/src/lib.rs index 14669d8..70e9241 100644 --- a/crates/host-api/src/lib.rs +++ b/crates/host-api/src/lib.rs @@ -256,6 +256,14 @@ pub trait HostFileSystem { from: FileSeekFrom, ) -> Result; + /// Resize an open regular file without changing its current offset. + /// + /// # Errors + /// + /// Returns a stable filesystem error for invalid handles, access, or + /// capacity exhaustion. + fn set_length(&mut self, handle: u64, length: u64) -> Result<(), FileSystemError>; + /// Close one opaque handle. /// /// # Errors @@ -331,6 +339,10 @@ impl HostFileSystem for NullFileSystem { Err(FileSystemError::BadDescriptor) } + fn set_length(&mut self, _handle: u64, _length: u64) -> Result<(), FileSystemError> { + Err(FileSystemError::BadDescriptor) + } + fn close(&mut self, _handle: u64) -> Result<(), FileSystemError> { Err(FileSystemError::BadDescriptor) } diff --git a/crates/linux-abi/src/lib.rs b/crates/linux-abi/src/lib.rs index 974b4e7..a315d48 100644 --- a/crates/linux-abi/src/lib.rs +++ b/crates/linux-abi/src/lib.rs @@ -10,6 +10,7 @@ pub enum Syscall { Mkdirat = 34, Unlinkat = 35, Renameat = 38, + Ftruncate = 46, Openat = 56, Close = 57, Getdents64 = 61, @@ -17,6 +18,7 @@ pub enum Syscall { Read = 63, Write = 64, Writev = 66, + Pread64 = 67, Readlinkat = 78, Newfstatat = 79, Fstat = 80, @@ -48,6 +50,7 @@ impl Syscall { 34 => Some(Self::Mkdirat), 35 => Some(Self::Unlinkat), 38 => Some(Self::Renameat), + 46 => Some(Self::Ftruncate), 56 => Some(Self::Openat), 57 => Some(Self::Close), 61 => Some(Self::Getdents64), @@ -56,6 +59,7 @@ impl Syscall { 73 => Some(Self::Ppoll), 64 => Some(Self::Write), 66 => Some(Self::Writev), + 67 => Some(Self::Pread64), 78 => Some(Self::Readlinkat), 79 => Some(Self::Newfstatat), 80 => Some(Self::Fstat), @@ -127,6 +131,7 @@ mod tests { assert_eq!(Syscall::from_number(34), Some(Syscall::Mkdirat)); assert_eq!(Syscall::from_number(35), Some(Syscall::Unlinkat)); assert_eq!(Syscall::from_number(38), Some(Syscall::Renameat)); + assert_eq!(Syscall::from_number(46), Some(Syscall::Ftruncate)); assert_eq!(Syscall::from_number(56), Some(Syscall::Openat)); assert_eq!(Syscall::from_number(57), Some(Syscall::Close)); assert_eq!(Syscall::from_number(61), Some(Syscall::Getdents64)); @@ -136,6 +141,7 @@ mod tests { assert_eq!(Syscall::from_number(66), Some(Syscall::Writev)); 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(80), Some(Syscall::Fstat)); assert_eq!(Syscall::from_number(73), Some(Syscall::Ppoll)); assert_eq!(Syscall::from_number(93), Some(Syscall::Exit)); diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index b9e0237..b038d8d 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -210,11 +210,24 @@ impl fmt::Display for SyscallEvent { "read(fd={}, buf={:#x}, count={})", self.arguments[0], self.arguments[1], self.arguments[2], )?, + Some(Syscall::Pread64) => write!( + formatter, + "pread64(fd={}, buf={:#x}, count={}, offset={})", + self.arguments[0], + self.arguments[1], + self.arguments[2], + self.arguments[3].cast_signed(), + )?, Some(Syscall::Fstat) => write!( formatter, "fstat(fd={}, statbuf={:#x})", self.arguments[0], self.arguments[1], )?, + Some(Syscall::Ftruncate) => write!( + formatter, + "ftruncate(fd={}, length={})", + self.arguments[0], self.arguments[1], + )?, Some(Syscall::Ppoll) => write!( formatter, "ppoll(fds={:#x}, nfds={}, timeout={:#x}, sigmask={:#x}, sigsetsize={})", @@ -735,6 +748,7 @@ impl Process { Some(Syscall::Readlinkat) => self.dispatch_readlinkat(filesystem), Some(Syscall::Newfstatat) => self.dispatch_newfstatat(filesystem), Some(Syscall::Fstat) => self.dispatch_fstat(filesystem), + Some(Syscall::Ftruncate) => self.dispatch_ftruncate(filesystem), Some(Syscall::Read) => { if self.register(0) == STANDARD_INPUT { if let Some(request) = self.dispatch_terminal_read(input, arguments) { @@ -744,6 +758,7 @@ impl Process { self.dispatch_read(filesystem); } } + Some(Syscall::Pread64) => self.dispatch_pread64(filesystem), Some(Syscall::Ppoll) => self.set_return(0), Some(Syscall::Write) => self.dispatch_write(terminal, filesystem)?, Some(Syscall::Writev) => self.dispatch_writev(terminal, filesystem)?, @@ -1024,7 +1039,7 @@ impl Process { .memory .write( GuestAddress::new(self.register(2)), - &linux_stat_bytes(metadata), + &linux_stat_bytes(metadata, &path), ) .is_err() { @@ -1051,7 +1066,7 @@ impl Process { return; }; match filesystem.metadata(path) { - Ok(metadata) => linux_stat_bytes(metadata), + Ok(metadata) => linux_stat_bytes(metadata, path), Err(error) => { self.set_return(filesystem_error_return(error)); return; @@ -1505,6 +1520,17 @@ impl Process { } } + fn dispatch_ftruncate(&mut self, filesystem: &mut F) { + let Some(handle) = self.file_handle(self.register(0)) else { + self.set_return(Errno::BadFileDescriptor.return_value()); + return; + }; + match filesystem.set_length(handle, self.register(1)) { + Ok(()) => self.set_return(0), + Err(error) => self.set_return(filesystem_error_return(error)), + } + } + fn dispatch_terminal_read( &mut self, input: &mut I, @@ -1618,6 +1644,60 @@ impl Process { self.set_return(read as u64); } + fn dispatch_pread64(&mut self, filesystem: &mut F) { + let Some(handle) = self.file_handle(self.register(0)) else { + self.set_return(Errno::BadFileDescriptor.return_value()); + return; + }; + let count = self.register(2); + let (Ok(host_count), Ok(offset)) = + (usize::try_from(count), i64::try_from(self.register(3))) + else { + self.set_return(Errno::InvalidArgument.return_value()); + return; + }; + if count > self.limits.max_memory_bytes { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + let original_position = match filesystem.seek(handle, 0, FileSeekFrom::Current) { + Ok(position) => position, + Err(error) => { + self.set_return(filesystem_error_return(error)); + return; + } + }; + if let Err(error) = filesystem.seek(handle, offset, FileSeekFrom::Start) { + self.set_return(filesystem_error_return(error)); + return; + } + let mut bytes = vec![0; host_count]; + let read = filesystem.read(handle, &mut bytes); + let restored = i64::try_from(original_position) + .ok() + .and_then(|position| filesystem.seek(handle, position, FileSeekFrom::Start).ok()); + if restored.is_none() { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + let read = match read { + Ok(read) => read, + Err(error) => { + self.set_return(filesystem_error_return(error)); + return; + } + }; + if self + .memory + .write(GuestAddress::new(self.register(1)), &bytes[..read]) + .is_err() + { + self.set_return(Errno::Fault.return_value()); + return; + } + self.set_return(read as u64); + } + fn dispatch_file_write(&mut self, filesystem: &mut F) { let Some(handle) = self.file_handle(self.register(0)) else { self.set_return(Errno::BadFileDescriptor.return_value()); @@ -2068,10 +2148,10 @@ fn directory_inode(name: &[u8]) -> u64 { }) } -fn linux_stat_bytes(metadata: FileMetadata) -> [u8; STAT_SIZE] { +fn linux_stat_bytes(metadata: FileMetadata, identity: &[u8]) -> [u8; STAT_SIZE] { let mut bytes = [0; STAT_SIZE]; bytes[..8].copy_from_slice(&1_u64.to_le_bytes()); - bytes[8..16].copy_from_slice(&1_u64.to_le_bytes()); + bytes[8..16].copy_from_slice(&directory_inode(identity).to_le_bytes()); let mode = match metadata.file_type { FileType::Regular => STAT_REGULAR_MODE, FileType::Directory => STAT_DIRECTORY_MODE, @@ -2124,8 +2204,8 @@ mod tests { use core::convert::Infallible; use binarrow_host_api::{ - DeterministicSystem, FileAccess, FileOpenFlags, FileOpenOptions, HostFileSystem, HostInput, - HostTerminal, NullFileSystem, TerminalInputRead, TerminalStream, + DeterministicSystem, FileAccess, FileOpenFlags, FileOpenOptions, FileSeekFrom, + HostFileSystem, HostInput, HostTerminal, NullFileSystem, TerminalInputRead, TerminalStream, }; use binarrow_linux_abi::Errno; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; @@ -2532,6 +2612,97 @@ mod tests { ); } + #[test] + fn pread64_reads_without_changing_the_file_offset() { + let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); + let mut process = Process::new(image).unwrap(); + let mut filesystem = MemoryFileSystem::new(1024); + let writer = filesystem + .open( + b"/tmp/pread.txt", + FileOpenOptions { + access: FileAccess::WriteOnly, + flags: FileOpenFlags::CREATE, + }, + ) + .unwrap(); + filesystem.write(writer, b"hello, world").unwrap(); + filesystem.close(writer).unwrap(); + let reader = filesystem + .open( + b"/tmp/pread.txt", + FileOpenOptions { + access: FileAccess::ReadOnly, + flags: FileOpenFlags::NONE, + }, + ) + .unwrap(); + filesystem.seek(reader, 2, FileSeekFrom::Start).unwrap(); + process.file_descriptors.insert(3, reader); + let destination = process.state.sp().checked_sub(64).unwrap(); + process.state.set_x(0, 3).unwrap(); + process.state.set_x(1, destination.get()).unwrap(); + process.state.set_x(2, 5).unwrap(); + process.state.set_x(3, 7).unwrap(); + + process.dispatch_pread64(&mut filesystem); + + assert_eq!(process.register(0), 5); + let mut bytes = [0; 5]; + process.memory.read_exact(destination, &mut bytes).unwrap(); + assert_eq!(&bytes, b"world"); + assert_eq!( + filesystem.seek(reader, 0, FileSeekFrom::Current).unwrap(), + 2 + ); + } + + #[test] + fn ftruncate_resizes_without_changing_the_file_offset() { + let image = load_hello(ProcessConfig::default(), 1, MESSAGE_ADDRESS); + let mut process = Process::new(image).unwrap(); + let mut filesystem = MemoryFileSystem::new(1024); + let handle = filesystem + .open( + b"/tmp/truncate.txt", + FileOpenOptions { + access: FileAccess::ReadWrite, + flags: FileOpenFlags::CREATE, + }, + ) + .unwrap(); + filesystem.write(handle, b"hello").unwrap(); + process.file_descriptors.insert(3, handle); + process.state.set_x(0, 3).unwrap(); + process.state.set_x(1, 2).unwrap(); + + process.dispatch_ftruncate(&mut filesystem); + + assert_eq!(process.register(0), 0); + assert_eq!( + filesystem.read_file(b"/tmp/truncate.txt"), + Some(b"he".as_slice()) + ); + assert_eq!( + filesystem.seek(handle, 0, FileSeekFrom::Current).unwrap(), + 5 + ); + + process.state.set_x(0, 3).unwrap(); + process.state.set_x(1, 6).unwrap(); + process.dispatch_ftruncate(&mut filesystem); + + assert_eq!(process.register(0), 0); + assert_eq!( + filesystem.read_file(b"/tmp/truncate.txt"), + Some(b"he\0\0\0\0".as_slice()) + ); + assert_eq!( + filesystem.seek(handle, 0, FileSeekFrom::Current).unwrap(), + 5 + ); + } + #[test] fn blocking_terminal_read_suspends_and_resumes_without_reexecuting_the_syscall() { let image = load_process( @@ -2766,6 +2937,24 @@ mod tests { ), 5 ); + let file_inode = u64::from_le_bytes(status[8..16].try_into().unwrap()); + + process + .memory + .write(path_address, b"/project/python/lib\0") + .unwrap(); + process.state.set_x(0, AT_FDCWD).unwrap(); + process.state.set_x(1, path_address.get()).unwrap(); + process.state.set_x(2, status_address.get()).unwrap(); + process.state.set_x(3, 0).unwrap(); + process.dispatch_newfstatat(&mut filesystem); + assert_eq!(process.register(0), 0); + process + .memory + .read_exact(status_address, &mut status) + .unwrap(); + let directory_inode = u64::from_le_bytes(status[8..16].try_into().unwrap()); + assert_ne!(file_inode, directory_inode); process .memory diff --git a/crates/memory-fs/src/lib.rs b/crates/memory-fs/src/lib.rs index c4db812..2f38cbc 100644 --- a/crates/memory-fs/src/lib.rs +++ b/crates/memory-fs/src/lib.rs @@ -566,6 +566,35 @@ impl HostFileSystem for MemoryFileSystem { self.seek_open_file(handle, offset, from) } + fn set_length(&mut self, handle: u64, length: u64) -> Result<(), FileSystemError> { + let (path, access) = match self + .open_handles + .get(&handle) + .ok_or(FileSystemError::BadDescriptor)? + { + OpenHandle::File { path, access, .. } => (path.clone(), *access), + OpenHandle::Directory { .. } => return Err(FileSystemError::IsDirectory), + }; + if !access.can_write() { + return Err(FileSystemError::PermissionDenied); + } + let host_length = usize::try_from(length).map_err(|_| FileSystemError::NoSpace)?; + let file = self + .files + .get_mut(&path) + .ok_or(FileSystemError::PermissionDenied)?; + let old_length = file.len() as u64; + let next_stored_bytes = self + .stored_bytes + .checked_sub(old_length) + .and_then(|stored| stored.checked_add(length)) + .filter(|stored| *stored <= self.byte_limit) + .ok_or(FileSystemError::NoSpace)?; + file.resize(host_length, 0); + self.stored_bytes = next_stored_bytes; + Ok(()) + } + fn close(&mut self, handle: u64) -> Result<(), FileSystemError> { self.open_handles .remove(&handle) diff --git a/guest-tests/exec-from-filesystem/README.md b/guest-tests/exec-from-filesystem/README.md new file mode 100644 index 0000000..4ae60c7 --- /dev/null +++ b/guest-tests/exec-from-filesystem/README.md @@ -0,0 +1,16 @@ +# Execute an ELF from the guest filesystem + +This minimal AArch64 launcher calls `execve` on the guest path supplied as +`argv[1]`. The child receives that path as its own `argv[0]`, retains any +remaining arguments, and inherits the launcher's environment. It provides a +generic acceptance bridge for running an ELF produced inside a persisted +Binarrow filesystem without adding a host-side extraction operation. + +Build the deterministic static launcher: + +```sh +guest-tests/exec-from-filesystem/build.sh +``` + +Zig caches stay under the repository's ignored `.tmp` directory. The generated +launcher is checked in so normal tests do not require Zig. diff --git a/guest-tests/exec-from-filesystem/build.sh b/guest-tests/exec-from-filesystem/build.sh new file mode 100755 index 0000000..9cd1c9e --- /dev/null +++ b/guest-tests/exec-from-filesystem/build.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +fixture_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +workspace_directory=$(CDPATH= cd -- "$fixture_directory/../.." && pwd) +cache_directory=$workspace_directory/.tmp/exec-from-filesystem-zig-cache +output=$fixture_directory/exec-from-filesystem.aarch64.elf +zig_version=$(zig version) + +if [ "$zig_version" != "0.16.0" ]; then + echo "exec-from-filesystem requires Zig 0.16.0; found $zig_version" >&2 + exit 1 +fi + +mkdir -p "$cache_directory/local" "$cache_directory/global" +env \ + ZIG_LOCAL_CACHE_DIR="$cache_directory/local" \ + ZIG_GLOBAL_CACHE_DIR="$cache_directory/global" \ + zig cc \ + -target aarch64-linux-musl \ + -nostdlib \ + -static \ + -g0 \ + -s \ + -Wl,--build-id=none \ + -Wl,-e,_start \ + "$fixture_directory/launcher.S" \ + -o "$output" +chmod 0644 "$output" diff --git a/guest-tests/exec-from-filesystem/exec-from-filesystem.aarch64.elf b/guest-tests/exec-from-filesystem/exec-from-filesystem.aarch64.elf new file mode 100644 index 0000000000000000000000000000000000000000..cddf082fa82cccd443de31ff92257a80572b0bd3 GIT binary patch literal 648 zcmb<-^>JfjWMqH=CWh?{Af5svM9={$(E$>KvK<&K7+4q_7+4sX8Q2&Y7+4q>7+~rg z7{J;=e6WrOP(3i31IhJtxD*#|#W154atEK6GZ7sKv=J@ln23_X_ETONf;F0J5(uzfP~vQvw!6g={CQ - int main(void) { - puts("browser-local clang"); - return 0; + return 42; } diff --git a/toolchains/clang-musl/verify.sh b/toolchains/clang-musl/verify.sh new file mode 100755 index 0000000..52eb320 --- /dev/null +++ b/toolchains/clang-musl/verify.sh @@ -0,0 +1,90 @@ +#!/bin/sh +set -eu + +toolchain_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +workspace_directory=$(CDPATH= cd -- "$toolchain_directory/../.." && pwd) +work_directory=$workspace_directory/.tmp/phase6-clang +cargo_home=$workspace_directory/.tmp/cargo-home +cargo_target=$workspace_directory/.tmp/cargo-target +rust_tmp=$workspace_directory/.tmp/rust-tmp +toolchain_image=$work_directory/clang-musl.bnfs +project_image=$work_directory/clang-project.bnfs +compiled_image=$work_directory/clang-compiled.bnfs +linked_image=$work_directory/clang-linked.bnfs +clang_runtime=$work_directory/package/toolchain/bin/clang +lld_runtime=$work_directory/package/toolchain/bin/lld +launcher=$workspace_directory/guest-tests/exec-from-filesystem/exec-from-filesystem.aarch64.elf + +for artifact in "$toolchain_image" "$clang_runtime" "$lld_runtime"; do + if [ ! -f "$artifact" ]; then + echo "missing toolchain artifact: $artifact; run toolchains/clang-musl/build.sh" >&2 + exit 1 + fi +done + +mkdir -p "$cargo_home" "$cargo_target" "$rust_tmp" +export CARGO_HOME=$cargo_home +export CARGO_TARGET_DIR=$cargo_target +export TMPDIR=$rust_tmp + +env CARGO_NET_OFFLINE=true cargo build \ + --manifest-path "$workspace_directory/Cargo.toml" \ + --release \ + -p binarrow-cli +runner=$cargo_target/release/binarrow +"$runner" image pack \ + --guest-root /project \ + "$toolchain_directory/project" \ + "$project_image" +"$workspace_directory/guest-tests/exec-from-filesystem/build.sh" + +"$runner" run \ + --instruction-budget 50000000 \ + --filesystem-limit 268435456 \ + --filesystem-install "$toolchain_image" \ + --filesystem-install "$project_image" \ + --filesystem-output "$compiled_image" \ + --argv0 /project/toolchain/bin/clang \ + "$clang_runtime" \ + -cc1 \ + -triple aarch64-unknown-linux-musl \ + -isysroot /project/toolchain/sysroot \ + -resource-dir /project/toolchain/lib/clang/22 \ + -internal-isystem /project/toolchain/lib/clang/22/include \ + -internal-externc-isystem /project/toolchain/sysroot/usr/include \ + -emit-obj \ + -o /project/main.o \ + /project/main.c + +"$runner" run \ + --instruction-budget 50000000 \ + --filesystem-limit 268435456 \ + --filesystem-snapshot "$compiled_image" \ + --filesystem-output "$linked_image" \ + --argv0 /project/toolchain/bin/ld.lld \ + "$lld_runtime" \ + -static \ + -o /project/hello \ + /project/toolchain/sysroot/usr/lib/crt1.o \ + /project/toolchain/sysroot/usr/lib/crti.o \ + /project/main.o \ + /project/toolchain/sysroot/usr/lib/libc.a \ + /project/toolchain/sysroot/usr/lib/crtn.o + +if "$runner" run \ + --instruction-budget 100000 \ + --filesystem-limit 268435456 \ + --filesystem-snapshot "$linked_image" \ + --argv0 /exec-from-filesystem \ + "$launcher" \ + /project/hello; then + exit_code=0 +else + exit_code=$? +fi +if [ "$exit_code" -ne 42 ]; then + echo "compiled program exited $exit_code; expected 42" >&2 + exit 1 +fi + +echo "Clang compiled, LLD linked, and /project/hello exited 42" -- 2.51.2