diff --git a/.github/workflows/dep_build_wasm_examples.yml b/.github/workflows/dep_build_wasm_examples.yml index 31ebd609..604da776 100644 --- a/.github/workflows/dep_build_wasm_examples.yml +++ b/.github/workflows/dep_build_wasm_examples.yml @@ -23,7 +23,14 @@ permissions: jobs: build-docker-image: if: ${{ inputs.docs_only == 'false' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} - runs-on: ubuntu-latest + strategy: + matrix: + arch: + - name: X64 + runner: ubuntu-latest + - name: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.arch.runner }} steps: - uses: actions/checkout@v5 with: @@ -73,7 +80,6 @@ jobs: build-wasm-examples: needs: build-docker-image - runs-on: ubuntu-latest strategy: matrix: wasmtime_version: @@ -81,6 +87,12 @@ jobs: features: "" - name: latest features: "wasmtime_latest" + arch: + - name: X64 + runner: ubuntu-latest + - name: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.arch.runner }} steps: - uses: actions/checkout@v5 with: @@ -110,6 +122,6 @@ jobs: - name: Upload Wasm Modules (wasmtime ${{ matrix.wasmtime_version.name }}) uses: actions/upload-artifact@v4 with: - name: guest-modules-${{ matrix.wasmtime_version.name }} + name: guest-modules-${{ matrix.arch }}-${{ matrix.wasmtime_version.name }} path: | x64/release/*.aot diff --git a/.github/workflows/dep_rust.yml b/.github/workflows/dep_rust.yml index 98bc3db5..164c314a 100644 --- a/.github/workflows/dep_rust.yml +++ b/.github/workflows/dep_rust.yml @@ -39,25 +39,54 @@ jobs: # failed-job rerun does not need to repeat the entire matrix. fail-fast: false matrix: - hypervisor: [hyperv, mshv3, kvm] # hyperv is windows, mshv and kvm are linux - cpu: [amd, intel] + hypervisor: [hyperv, mshv3, kvm, hvf] # hyperv is windows, mshv and kvm are linux + cpu: [amd, intel, apple, microsoft] + arch: [X64, arm64] config: [debug, release] wasmtime: [latest, lts] exclude: # Latest testing: skip debug builds to reduce CI load - wasmtime: latest config: debug - - runs-on: ${{ fromJson( - format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=build-{3}-{4}-{5}-{6}-{7}"]', - matrix.hypervisor == 'hyperv' && 'Windows' || 'Linux', - matrix.hypervisor == 'hyperv' && 'win2025' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor, - matrix.cpu, - matrix.config, - matrix.wasmtime, - github.run_id, - github.run_number, - github.run_attempt)) }} + - cpu: apple + hypervisor: hyperv + - cpu: apple + hypervisor: mshv3 + - cpu: microsoft + hypervisor: mshv3 + - cpu: microsoft + hypervisor: kvm + - cpu: microsoft + hypervisor: hvf + - arch: X64 + hypervisor: hvf + - cpu: amd + arch: arm64 + - cpu: intel + arch: arm64 + - cpu: apple + arch: X64 + - cpu: microsoft + arch: X64 + runs-on: ${{ fromJson(matrix.arch == 'X64' + && format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=build-{3}-{4}-{5}-{6}-{7}"]', + matrix.hypervisor == 'hyperv' && 'Windows' || 'Linux', + matrix.hypervisor == 'hyperv' && 'win2025' || matrix.hypervisor == 'mshv3' && 'azlinux3-mshv' || matrix.hypervisor, + matrix.cpu, + matrix.config, + matrix.wasmtime, + github.run_id, + github.run_number, + github.run_attempt) + || matrix.hypervisor == 'hyperv' + && format('["self-hosted", "Windows", "ARM64", "1ES.Pool=hl-windows-arm", "JobId=build-{0}-{1}-{2}-{3}"]', + matrix.config, + matrix.run_id, + matrix.run_number, + matrix.run_attempt) + || format('["self-hosted", "{0}", "arm64", "{1}"]', + matrix.hypervisor == 'hvf' && 'macos' || 'Linux', + matrix.hypervisor)) }} env: # Features to pass to cargo. # On Windows LTS builds, do not pass any features. @@ -69,7 +98,7 @@ jobs: - uses: actions/checkout@v5 - name: Hyperlight setup - uses: hyperlight-dev/ci-setup-workflow@v1.8.0 + uses: hyperlight-dev/ci-setup-workflow@v2.1.0 with: rust-toolchain: "1.94" @@ -86,10 +115,29 @@ jobs: if: runner.os == 'Windows' run: rustup component add --toolchain nightly-x86_64-pc-windows-msvc rustfmt + - name: Configure ARM64 libclang + if: runner.os == 'Windows' && inputs.arch == 'arm64' + shell: pwsh + run: | + if (-not $env:VSINSTALLDIR) { + throw "Visual Studio developer environment did not set VSINSTALLDIR" + } + $libclang = Join-Path $env:VSINSTALLDIR "VC\Tools\Llvm\ARM64\bin\libclang.dll" + if (-not (Test-Path $libclang)) { + throw "ARM64 libclang not found at '$libclang'" + } + $llvmPath = Split-Path $libclang + "LIBCLANG_PATH=$llvmPath" >> $env:GITHUB_ENV + $llvmPath >> $env:GITHUB_PATH + + - name: Install clang tools (macOS) + if: ${{ runner.os == 'macOS' }} + run: brew install llvm + - name: Download Wasm Modules uses: actions/download-artifact@v5 with: - name: guest-modules-${{ matrix.wasmtime }} + name: guest-modules-${{matrix.arch}}-${{ matrix.wasmtime }} path: ./x64/${{ matrix.config }} - name: Build Rust component model examples diff --git a/Justfile b/Justfile index 360bf132..10d57e8a 100644 --- a/Justfile +++ b/Justfile @@ -90,14 +90,17 @@ fmt: cd src/hyperlight_wasm_runtime && cargo +nightly fmt -v --all cd src/hyperlight_wasm_macro && cargo +nightly fmt -v --all +clippy-excluded-features := if arch() == "aarch64" { "gdb,trace_guest,crashdump" } else { "" } +clippy-runtime-features := if arch() == "aarch64" { "" } else { ",gdb,trace_guest" } + clippy target=default-target: (check target) - cargo clippy --workspace --exclude hyperlight-wasm-runtime --exclude hyperlight-wasm-macro --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --all-features -- -D warnings + cargo clippy --workspace --exclude hyperlight-wasm-runtime --exclude hyperlight-wasm-macro --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --features "$(cargo metadata --format-version 1 --no-deps | jq -r '[.packages[]|.name as$pkg|.features|keys.[]|select(.!="default" and (IN($exclude|split(",")[])|not))|$pkg+"/"+.]|join(",")' --arg exclude {{clippy-excluded-features}})" -- -D warnings cd src/tests/rust_guests/rust_wasm_samples && cargo clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --all-features -- -D warnings cd src/tests/rust_guests/component_sample && cargo clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --all-features -- -D warnings cd src/tests/rust_guests/greeter_sample && cargo clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --all-features -- -D warnings # hyperlight_wasm_runtime has mutually exclusive wasmtime features, so we run clippy for each separately with all other features - cd src/hyperlight_wasm_runtime && cargo hyperlight clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --no-default-features --features wasmtime_latest,gdb,trace_guest,pulley -- -D warnings - cd src/hyperlight_wasm_runtime && cargo hyperlight clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --no-default-features --features wasmtime_lts,gdb,trace_guest -- -D warnings + cd src/hyperlight_wasm_runtime && cargo hyperlight clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --no-default-features --features wasmtime_latest{{clippy-runtime-features}},pulley -- -D warnings + cd src/hyperlight_wasm_runtime && cargo hyperlight clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --no-default-features --features wasmtime_lts{{clippy-runtime-features}} -- -D warnings cd src/hyperlight_wasm_macro && cargo clippy --profile={{ if target == "debug" {"dev"} else { target } }} --all-targets --all-features -- -D warnings # TESTING diff --git a/src/hyperlight_wasm/Cargo.toml b/src/hyperlight_wasm/Cargo.toml index 70100f95..e5dfe646 100644 --- a/src/hyperlight_wasm/Cargo.toml +++ b/src/hyperlight_wasm/Cargo.toml @@ -112,13 +112,14 @@ serde_json = "1.0" junction = "2" [features] -default = ["function_call_metrics", "kvm", "mshv3"] +default = ["function_call_metrics", "kvm", "mshv3", "hvf"] function_call_metrics = ["hyperlight-host/function_call_metrics"] print_debug = ["hyperlight-host/print_debug"] crashdump = ["hyperlight-host/crashdump"] gdb = ["hyperlight-host/gdb"] kvm = ["hyperlight-host/kvm"] mshv3 = ["hyperlight-host/mshv3"] +hvf = ["hyperlight-host/hvf"] pulley = [] trace_guest = ["hyperlight-host/trace_guest"] # Use latest wasmtime instead of the default LTS version in wasm_runtime diff --git a/src/hyperlight_wasm/build.rs b/src/hyperlight_wasm/build.rs index 66b41f63..b81e6150 100644 --- a/src/hyperlight_wasm/build.rs +++ b/src/hyperlight_wasm/build.rs @@ -167,8 +167,12 @@ fn build_wasm_runtime() -> PathBuf { cmd.status() .unwrap_or_else(|e| panic!("could not run cargo build hyperlight-wasm-runtime: {e:?}")); + let target_arch = format!( + "{}-hyperlight-none", + std::env::var("CARGO_CFG_TARGET_ARCH").unwrap() + ); let resource = target_dir - .join("x86_64-hyperlight-none") + .join(target_arch) .join(profile) .join("hyperlight-wasm-runtime"); diff --git a/src/hyperlight_wasm/scripts/build-wasm-examples.sh b/src/hyperlight_wasm/scripts/build-wasm-examples.sh index 150644b9..e47f03b7 100755 --- a/src/hyperlight_wasm/scripts/build-wasm-examples.sh +++ b/src/hyperlight_wasm/scripts/build-wasm-examples.sh @@ -68,13 +68,16 @@ if [ -f "/.dockerenv" ] || grep -q docker /proc/1/cgroup; then cargo run ${AOT_FEATURES} -p hyperlight-wasm-aot compile ${AOT_DEBUG_FLAGS} ${AOT_VERSION_FLAGS} --component ${OUTPUT_DIR}/${COMPONENT_NAME}-p2.wasm ${OUTPUT_DIR}/${COMPONENT_NAME}.aot done -else +else # not running in a container so use the docker image to build the wasm files echo Building docker image that has Wasm sdk. Should be quick if preivoulsy built and no changes to dockerfile. echo This will take a while if it is the first time you are building the docker image. echo Log in ${OUTPUT_DIR}/dockerbuild.log - docker pull ghcr.io/hyperlight-dev/wasm-clang-builder:latest + arch="$(uname -m)" + if [[ "$arch" == "x86_64" ]]; then + docker pull ghcr.io/hyperlight-dev/wasm-clang-builder:latest + fi docker build --build-arg GCC_VERSION=12 --build-arg WASI_SDK_VERSION_FULL=25.0 --cache-from ghcr.io/hyperlight-dev/wasm-clang-builder:latest -t wasm-clang-builder:latest . 2> ${OUTPUT_DIR}/dockerbuild.log diff --git a/src/hyperlight_wasm_aot/src/main.rs b/src/hyperlight_wasm_aot/src/main.rs index cb0ac8f9..0b39d404 100644 --- a/src/hyperlight_wasm_aot/src/main.rs +++ b/src/hyperlight_wasm_aot/src/main.rs @@ -27,6 +27,7 @@ use wasmtime::{Config, Engine, Module, OptLevel, Precompiled}; #[derive(Debug)] enum SupportedTarget { X86_64UnknownNone, + AArch64UnknownNone, WasmtimePulley64, } @@ -34,6 +35,7 @@ impl Display for SupportedTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SupportedTarget::X86_64UnknownNone => write!(f, "x86_64-unknown-none"), + SupportedTarget::AArch64UnknownNone => write!(f, "aarch64-unknown-none"), SupportedTarget::WasmtimePulley64 => write!(f, "pulley64"), } } @@ -112,7 +114,10 @@ fn precompile_bytes_lts( if pulley { config.target("pulley64").unwrap(); } else { + #[cfg(target_arch = "x86_64")] config.target("x86_64-unknown-none").unwrap(); + #[cfg(target_arch = "aarch64")] + config.target("aarch64-unknown-none").unwrap(); } if debug { config.debug_info(true); @@ -133,7 +138,10 @@ fn precompile_bytes_lts( /// Detect and deserialize using the LTS wasmtime version fn detect_and_deserialize_lts(bytes: &[u8], debug: bool, file: &str) { let mut config = wasmtime_lts::Config::new(); + #[cfg(target_arch = "x86_64")] config.target("x86_64-unknown-none").unwrap(); + #[cfg(target_arch = "aarch64")] + config.target("aarch64-unknown-none").unwrap(); if debug { config.debug_info(true); config.cranelift_opt_level(wasmtime_lts::OptLevel::None); @@ -189,7 +197,11 @@ fn main() { let target = if pulley { SupportedTarget::WasmtimePulley64 } else { - SupportedTarget::X86_64UnknownNone + #[cfg(target_arch = "x86_64")] + let target = SupportedTarget::X86_64UnknownNone; + #[cfg(target_arch = "aarch64")] + let target = SupportedTarget::AArch64UnknownNone; + target }; if debug { println!( @@ -216,7 +228,14 @@ fn main() { let target_name = if pulley { "pulley64" } else { - "x86_64-unknown-none" + #[cfg(target_arch = "x86_64")] + { + "x86_64-unknown-none" + } + #[cfg(target_arch = "aarch64")] + { + "aarch64-unknown-none" + } }; if debug { println!( @@ -356,6 +375,9 @@ fn get_config(debug: bool, minimal: bool, target: &SupportedTarget) -> Config { // See https://github.com/bytecodealliance/wasmtime/pull/11553 unsafe { config.x86_float_abi_ok(true) }; } + SupportedTarget::AArch64UnknownNone => { + config.target("aarch64-unknown-none").unwrap(); + } SupportedTarget::WasmtimePulley64 => { config.target("pulley64").unwrap(); } @@ -390,9 +412,7 @@ fn get_aot_target(bytes: &[u8]) -> Result { if let Ok(elf) = ElfFile64::::parse(bytes) { match elf.architecture() { Architecture::X86_64 => Ok(SupportedTarget::X86_64UnknownNone), - Architecture::Aarch64 => { - Err("Unsupported architecture Aarch64 in AOT compiled file".to_string()) - } + Architecture::Aarch64 => Ok(SupportedTarget::AArch64UnknownNone), Architecture::S390x => { Err("Unsupported architecture S390x in AOT compiled file".to_string()) } diff --git a/src/hyperlight_wasm_runtime/src/component.rs b/src/hyperlight_wasm_runtime/src/component.rs index 3c050ef2..affe8573 100644 --- a/src/hyperlight_wasm_runtime/src/component.rs +++ b/src/hyperlight_wasm_runtime/src/component.rs @@ -108,8 +108,6 @@ fn load_wasm_module_phys(function_call: FunctionCall) -> Result { #[no_mangle] #[instrument(skip_all, level = "Info")] pub extern "C" fn hyperlight_main() { - platform::register_page_fault_handler(); - let mut config = Config::new(); // Enable x86_float_abi_ok only for the latest Wasmtime native x86 target. // Safety: diff --git a/src/hyperlight_wasm_runtime/src/module.rs b/src/hyperlight_wasm_runtime/src/module.rs index c28fae78..331eecf7 100644 --- a/src/hyperlight_wasm_runtime/src/module.rs +++ b/src/hyperlight_wasm_runtime/src/module.rs @@ -248,8 +248,6 @@ fn load_wasm_module_phys(function_call: FunctionCall) -> Result { #[no_mangle] #[instrument(skip_all, level = "Info")] pub extern "C" fn hyperlight_main() { - platform::register_page_fault_handler(); - register_function(GuestFunctionDefinition::new( "PrintOutput".to_string(), vec![ParameterType::String], diff --git a/src/hyperlight_wasm_runtime/src/platform.rs b/src/hyperlight_wasm_runtime/src/platform.rs index 74f3ea90..3b5de48d 100644 --- a/src/hyperlight_wasm_runtime/src/platform.rs +++ b/src/hyperlight_wasm_runtime/src/platform.rs @@ -19,87 +19,76 @@ use core::ptr::NonNull; use core::sync::atomic::{AtomicPtr, AtomicU64, Ordering}; use hyperlight_common::vmem; -#[allow(deprecated)] -use hyperlight_guest_bin::exception::arch; -use hyperlight_guest_bin::paging; +use hyperlight_guest_bin::{exception, paging}; -// Extremely stupid virtual address allocator -// 0x1_0000_0000 is where the module is -// we start at -// 0x100_0000_0000 and go up from there -static FIRST_VADDR: AtomicU64 = AtomicU64::new(0x100_0000_0000u64); -fn page_fault_handler( - _exception_number: u64, - info: *mut arch::ExceptionInfo, - _ctx: *mut arch::Context, - page_fault_address: u64, -) -> bool { - let error_code = unsafe { (&raw const (*info).error_code).read_volatile() }; - // TODO: check if this is a guard-region trap (which can't happen - // right now since we don't actually set the permissions properly - // in mprotect) +// Wasmtime Embedding Interface - // TODO: replace this with some generic virtual memory area data - // structure in hyperlight core - if (error_code & 0x1) == 0x0 && page_fault_address >= 0x100_0000_0000u64 { - unsafe { - let phys_page = hyperlight_guest::prim_alloc::alloc_phys_pages(1); - let virt_base = (page_fault_address & !0xFFF) as *mut u8; - paging::map_region( - phys_page, - virt_base, - hyperlight_guest_bin::OS_PAGE_SIZE as u64, - vmem::MappingKind::Basic(vmem::BasicMapping { - readable: true, - writable: true, - executable: true, - }), - ); - virt_base.write_bytes(0u8, hyperlight_guest_bin::OS_PAGE_SIZE as usize); - } - return true; // Try again! +const WASMTIME_PROT_READ: u32 = 1 << 0; +const WASMTIME_PROT_WRITE: u32 = 1 << 1; +const WASMTIME_PROT_EXEC: u32 = 1 << 2; + +#[allow(non_camel_case_types)] // we didn't choose the name! +type wasmtime_trap_handler_t = + extern "C" fn(ip: usize, fp: usize, has_faulting_addr: bool, faulting_addr: usize); +static WASMTIME_REQUESTED_TRAP_HANDLER: spin::Mutex> = + spin::Mutex::new(None); + +extern "C" fn page_fault_handler(pc: u64, fp: u64, va: u64) -> ! { + if let Some(requested_handler) = *WASMTIME_REQUESTED_TRAP_HANDLER.lock() { + (requested_handler as wasmtime_trap_handler_t)(pc as usize, fp as usize, true, va as usize); } - false + panic!("wasmtime did not handle page fault"); } -pub(crate) fn register_page_fault_handler() { - // On amd64, vector 14 is #PF - // See AMD64 Architecture Programmer's Manual, Volume 2 - // §8.2 Vectors, p. 245 - // Table 8-1: Interrupt Vector Source and Cause - arch::HANDLERS[14].store( - page_fault_handler as *const () as usize as u64, - Ordering::Release, - ); +extern "C" fn undef_handler(pc: u64, fp: u64) -> ! { + if let Some(requested_handler) = *WASMTIME_REQUESTED_TRAP_HANDLER.lock() { + (requested_handler as wasmtime_trap_handler_t)(pc as usize, fp as usize, false, 0); + } + panic!("wasmtime did not handle undefined instruction"); } -// Wasmtime Embedding Interface +// Extremely stupid virtual address allocator +// 0x1_0000_0000 is where the module is +// we start at +// 0x100_0000_0000 and go up from there +static FIRST_VADDR: AtomicU64 = AtomicU64::new(0x100_0000_0000u64); /* We don't actually have any sensible virtual memory areas, so * we just give out virtual addresses very coarsely with - * probably-more-than-enough space between them, and take over - * page-fault handling to hardcoded check if memory is in this region - * (see above) */ + * probably-more-than-enough space between them */ #[no_mangle] -pub extern "C" fn wasmtime_mmap_new(_size: usize, _prot_flags: u32, ret: &mut *mut u8) -> i32 { - if _size > 0x100_0000_0000 { - panic!("wasmtime_mmap_{:x} {:x}", _size, _prot_flags); +pub extern "C" fn wasmtime_mmap_new(size: usize, prot_flags: u32, ret: &mut *mut u8) -> i32 { + if size > 0x100_0000_0000 { + panic!("wasmtime_mmap_{:x} {:x}", size, prot_flags); } - *ret = FIRST_VADDR.fetch_add(0x100_0000_0000, Ordering::Relaxed) as *mut u8; + let va = FIRST_VADDR.fetch_add(0x100_0000_0000, Ordering::Relaxed) as *mut u8; + wasmtime_mmap_remap(va, size, prot_flags); + *ret = va; 0 } -/* Remap is only used for changing the region size (which is presently - * a no-op, since we just hand out very large regions and treat them all - * the same), or possibly for changing permissions, which will be a no-op - * as we don't properly implement permissions at the moment. */ #[no_mangle] pub extern "C" fn wasmtime_mmap_remap(addr: *mut u8, size: usize, prot_flags: u32) -> i32 { - if size > 0x100_0000_0000 { - panic!( - "wasmtime_mmap_remap {:x} {:x} {:x}", - addr as usize, size, prot_flags - ); + // Since wasmtime needs a huge buffer region (4GB) and we don't + // have hugepages yet, don't actually record anything unless the + // protection flags tell us we need to. One of + // wasmtime_mmap_remap, wasmtime_mprotect, or + // WasmtimeCodeMemory::publish will have a chance to change this + // later. + if prot_flags != 0 { + unsafe { + paging::map_region( + 0, + addr, + size as u64, + vmem::MappingKind::ZeroInit(vmem::BasicMapping { + readable: prot_flags & WASMTIME_PROT_READ != 0, + writable: prot_flags & WASMTIME_PROT_WRITE != 0, + executable: prot_flags & WASMTIME_PROT_EXEC != 0, + }), + ); + } } + paging::barrier::first_valid_same_ctx(); 0 } @@ -108,15 +97,84 @@ pub extern "C" fn wasmtime_munmap(_ptr: *mut u8, _size: usize) -> i32 { 0 } +// On most architectures, "upgrades" including the first time that +// a a valid translation exists for a VA, need a lighter barrier +// than full downgrades, and don't even need that if another +// barrier has been used. +struct BarrierTracker { + downgrade_start: Option, + has_ops_since_last_downgrade: bool, +} +impl BarrierTracker { + fn new() -> Self { + Self { + downgrade_start: None, + has_ops_since_last_downgrade: false, + } + } + fn track(&mut self, here: u64, is_downgrade: bool) { + if is_downgrade && self.downgrade_start.is_none() { + self.downgrade_start = Some(here); + } else if !is_downgrade { + if let Some(start) = self.downgrade_start { + paging::barrier::downgrade_in_place(start..here); + self.downgrade_start = None; + self.has_ops_since_last_downgrade = false; + } else { + self.has_ops_since_last_downgrade = true; + } + } + } +} + /* TODO: implement permissions properly */ #[no_mangle] -pub extern "C" fn wasmtime_mprotect(_ptr: *mut u8, _size: usize, prot_flags: u32) -> i32 { - /* currently all memory is allocated RWX; we assume that - * restricting to R or RX can be ignored */ - if prot_flags == 1 || prot_flags == 3 || prot_flags == 5 { - return 0; +pub extern "C" fn wasmtime_mprotect(ptr: *mut u8, size: usize, prot_flags: u32) -> i32 { + let base_u = ptr as u64; + let mut bt = BarrierTracker::new(); + let readable = prot_flags & WASMTIME_PROT_READ != 0; + let writable = prot_flags & WASMTIME_PROT_WRITE != 0; + let executable = prot_flags & WASMTIME_PROT_EXEC != 0; + unsafe { + paging::modify_mapping(ptr, size as u64, |va, mapping| { + bt.track(va, true); + match mapping.kind { + // The main reason an unmapped region would show up in + // an mprotect'd region is if it was left unmapped by + // wasmtime_mmap_new above, so just treat it + // more-or-less the same as ZeroInit. + vmem::MappingKind::Unmapped => vmem::MappingKind::ZeroInit(vmem::BasicMapping { + readable, + writable, + executable, + }), + vmem::MappingKind::ZeroInit(_) => vmem::MappingKind::ZeroInit(vmem::BasicMapping { + readable, + writable, + executable, + }), + vmem::MappingKind::Basic(_) => vmem::MappingKind::Basic(vmem::BasicMapping { + readable, + writable, + executable, + }), + vmem::MappingKind::Cow(_) if !writable => { + vmem::MappingKind::Basic(vmem::BasicMapping { + readable, + writable, + executable, + }) + } + vmem::MappingKind::Cow(_) => vmem::MappingKind::Cow(vmem::CowMapping { + readable, + executable, + }), + } + }); } - -1 + bt.track(base_u + size as u64, false); + + 0 } #[no_mangle] @@ -124,147 +182,83 @@ pub extern "C" fn wasmtime_page_size() -> usize { unsafe { hyperlight_guest_bin::OS_PAGE_SIZE as usize } } -#[allow(non_camel_case_types)] // we didn't choose the name! -type wasmtime_trap_handler_t = - extern "C" fn(ip: usize, fp: usize, has_faulting_addr: bool, faulting_addr: usize); -static WASMTIME_REQUESTED_TRAP_HANDLER: AtomicU64 = AtomicU64::new(0); -fn wasmtime_trap_handler( - exception_number: u64, - info: *mut arch::ExceptionInfo, - ctx: *mut arch::Context, - _page_fault_address: u64, -) -> bool { - let requested_handler = WASMTIME_REQUESTED_TRAP_HANDLER.load(Ordering::Relaxed); - if requested_handler != 0 { - #[allow(clippy::collapsible_if)] // We will add more cases - if exception_number == 6 { - // #UD - // we assume that handle_trap always longjmp's away, so don't bother - // setting up a terribly proper stack frame - unsafe { - let orig_rip = (&raw mut (*info).rip).read_volatile(); - (&raw mut (*info).rip).write_volatile(requested_handler); - // TODO: This only works on amd64 sysv - (&raw mut (*ctx).gprs[9]).write_volatile(orig_rip); - let orig_rbp = (&raw mut (*ctx).gprs[8]).read_volatile(); - (&raw mut (*ctx).gprs[10]).write_volatile(orig_rbp); - (&raw mut (*ctx).gprs[11]).write_volatile(0); - (&raw mut (*ctx).gprs[12]).write_volatile(0); - } - return true; - } - // TODO: Add handlers for any other traps that wasmtime needs - } - false -} - #[no_mangle] pub extern "C" fn wasmtime_init_traps(handler: wasmtime_trap_handler_t) -> i32 { - WASMTIME_REQUESTED_TRAP_HANDLER.store(handler as usize as u64, Ordering::Relaxed); - // On amd64, vector 6 is #UD - // See AMD64 Architecture Programmer's Manual, Volume 2 - // §8.2 Vectors, p. 245 - // Table 8-1: Interrupt Vector Source and Cause - arch::HANDLERS[6].store( - wasmtime_trap_handler as *const () as usize as u64, - Ordering::Release, - ); + *WASMTIME_REQUESTED_TRAP_HANDLER.lock() = Some(handler); + exception::register_page_fault_handler(page_fault_handler); + exception::register_undefined_instruction_handler(undef_handler); // TODO: Add handlers for any other traps that wasmtime needs, // probably including at least some floating-point // exceptions - // TODO: Ensure that invalid accesses to mprotect()'d regions also - // need to trap, although those will need to go through the - // page fault handler instead of using this handler that - // takes over the exception. 0 } // Copy a VA range to a new VA. Old and new VA, and len, must be // page-aligned. -fn copy_va_mapping(base: *const u8, len: usize, to_va: *mut u8, remap_original: bool) { +fn copy_va_mapping( + base: *const u8, + len: usize, + to_va: *mut u8, + remap_original: bool, + force_writable: bool, +) { debug_assert!((base as usize).is_multiple_of(vmem::PAGE_SIZE)); debug_assert!(len.is_multiple_of(vmem::PAGE_SIZE)); - // TODO: all this barrier code is amd64 specific. It should be - // refactored to use some better architecture-independent APIs. - // - // On amd64, "upgrades" including the first time that a a valid - // translation exists for a VA, only need a light (serialising - // instruction) barrier. Since invlpg is also a barrier, we don't - // even need that, if we did do a downgrade remap just before. - let mut needs_first_valid_exposure_barrier = false; - - // TODO: make this more efficient by directly exposing the ability - // to traverse an entire VA range in - // hyperlight_guest_bin::paging::virt_to_phys, and coalescing - // continuous ranges there. let base_u = base as u64; - let va_page_bases = (base_u..(base_u + len as u64)).step_by(vmem::PAGE_SIZE); - let mappings = va_page_bases.flat_map(paging::virt_to_phys); - for mapping in mappings { - // TODO: Deduplicate with identical logic in hyperlight_host snapshot. - let (new_kind, was_writable) = match mapping.kind { - // Skip unmapped pages, since they will be unmapped in - // both the original and the new copy - vmem::MappingKind::Unmapped => continue, - vmem::MappingKind::ZeroInit(bm) => (vmem::MappingKind::ZeroInit(bm), false), - vmem::MappingKind::Basic(bm) if bm.writable => ( - vmem::MappingKind::Cow(vmem::CowMapping { - readable: bm.readable, - executable: bm.executable, - }), - true, - ), - vmem::MappingKind::Basic(bm) => ( - vmem::MappingKind::Basic(vmem::BasicMapping { - readable: bm.readable, - writable: false, - executable: bm.executable, - }), - false, - ), - vmem::MappingKind::Cow(cm) => (vmem::MappingKind::Cow(cm), false), - }; - let do_downgrade = remap_original && was_writable; - if do_downgrade { - // If necessary, remap the original page as Cow, instead - // of whatever it is now, to ensure that any more writes to - // that region do not change the image base. - // - // TODO: could the table traversal needed for this be fused - // with the table traversal that got the original mapping, - // above? - unsafe { - paging::map_region( - mapping.phys_base, - mapping.virt_base as *mut u8, - vmem::PAGE_SIZE as u64, - new_kind, - ); - } - } - // map the same pages to the new VA - unsafe { + + let mut bt = BarrierTracker::new(); + + unsafe { + paging::modify_mapping(base as *mut u8, len as u64, |va, mapping| { + // TODO: Deduplicate with identical logic in hyperlight_host snapshot. + let (new_kind, was_writable) = match mapping.kind { + // Skip unmapped pages, since they will be unmapped in + // both the original and the new copy + vmem::MappingKind::Unmapped => return vmem::MappingKind::Unmapped, + vmem::MappingKind::ZeroInit(bm) => (vmem::MappingKind::ZeroInit(bm), false), + vmem::MappingKind::Basic(bm) if bm.writable => ( + vmem::MappingKind::Cow(vmem::CowMapping { + readable: bm.readable, + executable: bm.executable, + }), + true, + ), + vmem::MappingKind::Basic(bm) if force_writable => ( + vmem::MappingKind::Cow(vmem::CowMapping { + readable: bm.readable, + executable: bm.executable, + }), + false, + ), + vmem::MappingKind::Basic(bm) => ( + vmem::MappingKind::Basic(vmem::BasicMapping { + readable: bm.readable, + writable: false, + executable: bm.executable, + }), + false, + ), + vmem::MappingKind::Cow(cm) => (vmem::MappingKind::Cow(cm), false), + }; + // map the same pages to the new VA paging::map_region( - mapping.phys_base, - to_va.wrapping_add((mapping.virt_base - base_u) as usize), - vmem::PAGE_SIZE as u64, + mapping.phys_base + (va - mapping.virt_base), + to_va.wrapping_add((va - base_u) as usize), + mapping.len, new_kind, ); - } - if do_downgrade { - // Since we have downgraded a page from writable to CoW we - // need to do an invlpg on it. Because invlpg is a - // serialising instruction, we don't need the other - // barrier for the new mapping. - unsafe { - core::arch::asm!("invlpg [{}]", in(reg) mapping.virt_base, options(readonly, nostack, preserves_flags)); + let do_downgrade = remap_original && was_writable; + bt.track(va, do_downgrade); + if do_downgrade { + new_kind + } else { + mapping.kind } - needs_first_valid_exposure_barrier = false; - } else { - needs_first_valid_exposure_barrier = true; - } + }); } - if needs_first_valid_exposure_barrier { + + bt.track(base_u + len as u64, false); + if bt.has_ops_since_last_downgrade { paging::barrier::first_valid_same_ctx(); } } @@ -283,7 +277,7 @@ pub extern "C" fn wasmtime_memory_image_new( // the original VA range here, making the original copy CoW as we // go. let new_virt = FIRST_VADDR.fetch_add(0x100_0000_0000, Ordering::Relaxed) as *mut u8; - copy_va_mapping(ptr, len, new_virt, true); + copy_va_mapping(ptr, len, new_virt, true, false); *ret = new_virt as *mut c_void; 0 } @@ -294,7 +288,7 @@ pub extern "C" fn wasmtime_memory_image_map_at( addr: *mut u8, len: usize, ) -> i32 { - copy_va_mapping(image as *mut u8, len, addr, false); + copy_va_mapping(image as *mut u8, len, addr, false, true); 0 } @@ -340,16 +334,22 @@ impl wasmtime::CustomCodeMemory for WasmtimeCodeMemory { } fn publish_executable( &self, - _ptr: *const u8, - _len: usize, + ptr: *const u8, + len: usize, ) -> core::result::Result<(), wasmtime::Error> { + wasmtime_mprotect(ptr as *mut u8, len, WASMTIME_PROT_READ | WASMTIME_PROT_EXEC); Ok(()) } fn unpublish_executable( &self, - _ptr: *const u8, - _len: usize, + ptr: *const u8, + len: usize, ) -> core::result::Result<(), wasmtime::Error> { + wasmtime_mprotect( + ptr as *mut u8, + len, + WASMTIME_PROT_READ | WASMTIME_PROT_WRITE, + ); Ok(()) } } diff --git a/src/tests/c_guests/wasmsamples/dockerfile b/src/tests/c_guests/wasmsamples/dockerfile index 1e3fde02..b1fe04be 100644 --- a/src/tests/c_guests/wasmsamples/dockerfile +++ b/src/tests/c_guests/wasmsamples/dockerfile @@ -3,21 +3,25 @@ FROM ubuntu ENV DEBIAN_FRONTEND=noninteractive LABEL org.opencontainers.image.source=https://github.com/hyperlight-dev/hyperlight-wasm +ARG TARGETPLATFORM ARG GCC_VERSION=12 RUN apt-get update \ && apt-get upgrade -y RUN apt-get install -y wget \ && apt-get install -y build-essential \ - && apt-get install -y g++-multilib \ && apt-get install -y libgcc-${GCC_VERSION}-dev \ - && apt-get install -y lib32gcc-${GCC_VERSION}-dev + && ([[ $TARGETPLATFORM == "linux/amd64" ]] \ + && (apt-get install -y g++-multilib \ + && apt-get install -y libgcc32-${GCC_VERSION}-dev) \ + || true) ARG WASI_SDK_VERSION_FULL=25.0 ARG WASI_SDK_VERSION_MAJOR=${WASI_SDK_VERSION_FULL%%.*} -RUN wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION_MAJOR}/wasi-sdk-${WASI_SDK_VERSION_FULL}-x86_64-linux.tar.gz \ - && tar xvf wasi-sdk-${WASI_SDK_VERSION_FULL}-x86_64-linux.tar.gz \ - && rm wasi-sdk-${WASI_SDK_VERSION_FULL}-x86_64-linux.tar.gz \ - && mv /wasi-sdk-${WASI_SDK_VERSION_FULL}-x86_64-linux /opt/wasi-sdk +RUN WASI_SDK_ARCH=$(echo "${TARGETPLATFORM#*/}" | sed 's/amd64/x86_64/'); \ + wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION_MAJOR}/wasi-sdk-${WASI_SDK_VERSION_FULL}-${WASI_SDK_ARCH}-linux.tar.gz \ + && tar xvf wasi-sdk-${WASI_SDK_VERSION_FULL}-${WASI_SDK_ARCH}-linux.tar.gz \ + && rm wasi-sdk-${WASI_SDK_VERSION_FULL}-${WASI_SDK_ARCH}-linux.tar.gz \ + && mv /wasi-sdk-${WASI_SDK_VERSION_FULL}-${WASI_SDK_ARCH}-linux /opt/wasi-sdk CMD ["/bin/sh"]