diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index 48614f5..b5a743a 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -61,6 +61,8 @@ jobs: - python-agent-driver - powershell - networking-py + - go-http + - dotnet-http steps: - uses: actions/checkout@v4 @@ -223,6 +225,14 @@ jobs: - example: networking-py args: "--port 8080 -- /echo_server_test.py" expect: "SUCCESS: bind\\+listen on port 8080 allowed" + - example: go-http + args: "--port 8080 -- /bin/server" + expect: "Hello from Hyperlight-Unikraft!" + http_port: "8080" + - example: dotnet-http + args: "--port 8080 -- /app/KestrelHyperlight" + expect: "Hello from Kestrel on Hyperlight!" + http_port: "8080" steps: - uses: actions/checkout@v4 @@ -338,6 +348,42 @@ jobs: mkdir -p "$mount_dir" mount_args="--mount $mount_dir:/host" fi + # HTTP server examples: start in background, poll, curl, kill. + http_port="${{ matrix.http_port }}" + if [ -n "$http_port" ]; then + mem_args="" + if [ -n "$memory" ]; then + mem_args="-m $memory" + fi + hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" ${{ matrix.args }} & + server_pid=$! + sleep 3 + ready=0 + for i in $(seq 1 30); do + if curl -s --max-time 10 "http://127.0.0.1:${http_port}" >/dev/null 2>&1; then + ready=1 + break + fi + sleep 1 + done + if [ "$ready" -ne 1 ]; then + echo "FAIL: server did not become ready within timeout" + kill "$server_pid" 2>/dev/null || true + exit 1 + fi + response=$(curl -s --max-time 10 "http://127.0.0.1:${http_port}") + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + echo "=== HTTP response ===" + echo "$response" + if echo "$response" | grep -qF "$expect"; then + echo "PASS: matched /$expect/" + else + echo "FAIL: did not match /$expect/" + exit 1 + fi + exit 0 + fi case "${{ matrix.driver }}" in multifn-test) cmd=(timeout 60 /home/runner/work/hyperlight-unikraft/hyperlight-unikraft/host/target/release/multifn-test "$kernel" "$cpio") @@ -403,6 +449,8 @@ jobs: - python-agent-driver - powershell - networking-py + - go-http + - dotnet-http steps: - uses: actions/checkout@v4 @@ -554,6 +602,14 @@ jobs: - example: networking-py args: "--port 8080 -- /echo_server_test.py" expect: "SUCCESS: bind\\+listen on port 8080 allowed" + - example: go-http + args: "--port 8080 -- /bin/server" + expect: "Hello from Hyperlight-Unikraft!" + http_port: "8080" + - example: dotnet-http + args: "--port 8080 -- /app/KestrelHyperlight" + expect: "Hello from Kestrel on Hyperlight!" + http_port: "8080" steps: - uses: actions/checkout@v4 @@ -622,6 +678,54 @@ jobs: $PSNativeCommandUseErrorActionPreference = $false $ErrorActionPreference = 'Continue' + # HTTP server examples: start in background, poll, curl, kill. + $httpPort = '${{ matrix.http_port }}' + if ($httpPort -ne '') { + $argList = @() + if ($runArgs -ne '') { + $argList = $runArgs.Split(' ') | Where-Object { $_ -ne '' } + } + $memArgs = @() + if ($memory -ne '') { + $memArgs = @('-m', $memory) + } + $stderrLog = Join-Path $env:RUNNER_TEMP 'hl-stderr.log' + $proc = Start-Process -FilePath 'hyperlight-unikraft' ` + -ArgumentList (@('-q') + $memArgs + @($kernel, '--initrd', $cpio) + $argList) ` + -PassThru -NoNewWindow -RedirectStandardError $stderrLog + # Give the server time to boot the unikernel and start listening + Start-Sleep -Seconds 5 + $ready = $false + for ($i = 0; $i -lt 30; $i++) { + try { + $null = Invoke-WebRequest -Uri "http://127.0.0.1:${httpPort}" -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop + $ready = $true + break + } catch { + Start-Sleep -Seconds 1 + } + } + if (-not $ready) { + Write-Host "FAIL: server did not become ready within timeout" + Write-Host "=== stderr (last 200 lines) ===" + Get-Content $stderrLog -Tail 200 -ErrorAction SilentlyContinue + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + exit 1 + } + $raw = (Invoke-WebRequest -Uri "http://127.0.0.1:${httpPort}" -UseBasicParsing -TimeoutSec 10).Content + if ($raw -is [byte[]]) { $resp = [System.Text.Encoding]::UTF8.GetString($raw) } else { $resp = $raw } + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + Write-Host "=== HTTP response ===" + Write-Host $resp + if ($resp -match [regex]::Escape($expect)) { + Write-Host "PASS: matched /$expect/" + exit 0 + } else { + Write-Host "FAIL: did not match /$expect/" + exit 1 + } + } + switch ($driver) { 'multifn-test' { $out = & multifn-test $kernel $cpio 2>&1 diff --git a/examples/dotnet-http/Dockerfile b/examples/dotnet-http/Dockerfile new file mode 100644 index 0000000..ccf6f2f --- /dev/null +++ b/examples/dotnet-http/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build +WORKDIR /app +COPY Program.cs KestrelHyperlight.csproj ./ +RUN dotnet publish -c Release -r linux-musl-x64 --self-contained \ + -p:PublishTrimmed=true \ + -p:InvariantGlobalization=true \ + -o /out + +FROM scratch AS rootfs + +# .NET app + runtime (self-contained) +COPY --from=build /out/ /app/ + +# musl dynamic linker +COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 + +# C++ runtime libraries (needed by .NET runtime) +COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 + +# --- CPIO rootfs builder (used by: docker build --target cpio) --- +FROM alpine:3.20 AS cpio +RUN apk add --no-cache cpio findutils +COPY --from=rootfs / /rootfs/ +RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null diff --git a/examples/dotnet-http/Justfile b/examples/dotnet-http/Justfile new file mode 100644 index 0000000..3750555 --- /dev/null +++ b/examples/dotnet-http/Justfile @@ -0,0 +1,49 @@ +# dotnet-http (Kestrel) on Hyperlight +# +# just run - Run the HTTP server (--port implies --net) +# just rootfs - Build rootfs via Docker (cross-platform) +# just build - Build/pull kernel +# just clean - Remove build artifacts + +set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] +export DOCKER_BUILDKIT := "0" + +kernel := ".unikraft/build/dotnet-http-hyperlight_hyperlight-x86_64" +initrd := "initrd.cpio" +memory := "128Mi" +image := "dotnet-http-hyperlight" + +# Run the HTTP server (curl http://localhost:8080 to test) +run: + hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --port 8080 -- /app/KestrelHyperlight + +# Build rootfs via Docker (cross-platform) +rootfs: + docker build --platform linux/amd64 --target cpio -t {{image}}-cpio . + - docker rm -f {{image}}-tmp + docker create --name {{image}}-tmp {{image}}-cpio /bin/true + docker cp {{image}}-tmp:/output.cpio ./{{initrd}} + docker rm -f {{image}}-tmp + +# Build kernel via kraft (Linux) +[unix] +build: + -kraft-hyperlight build --plat hyperlight --arch x86_64 + +# Pull pre-built kernel from GHCR (Windows — no kernel published yet) +[windows] +build: + Write-Host "No pre-built kernel for dotnet-http yet. Build on Linux first." + +# Clean + rebuild everything +rebuild: clean build rootfs + +# Clean build artifacts +[unix] +clean: + rm -rf .unikraft {{initrd}} + +[windows] +clean: + if (Test-Path .unikraft) { Remove-Item -Recurse -Force .unikraft } + if (Test-Path {{initrd}}) { Remove-Item -Force {{initrd}} } diff --git a/examples/dotnet-http/KestrelHyperlight.csproj b/examples/dotnet-http/KestrelHyperlight.csproj new file mode 100644 index 0000000..c770bb0 --- /dev/null +++ b/examples/dotnet-http/KestrelHyperlight.csproj @@ -0,0 +1,7 @@ + + + net9.0 + enable + true + + diff --git a/examples/dotnet-http/Program.cs b/examples/dotnet-http/Program.cs new file mode 100644 index 0000000..d584f0a --- /dev/null +++ b/examples/dotnet-http/Program.cs @@ -0,0 +1,13 @@ +var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions()); +builder.WebHost.UseKestrelCore(); + +var app = builder.Build(); +app.Urls.Add("http://0.0.0.0:8080"); + +app.Run(async context => +{ + await context.Response.WriteAsync("Hello from Kestrel on Hyperlight!"); +}); + +Console.WriteLine("Listening on :8080"); +await app.RunAsync(); diff --git a/examples/dotnet-http/kraft.yaml b/examples/dotnet-http/kraft.yaml new file mode 100644 index 0000000..37c6ed0 --- /dev/null +++ b/examples/dotnet-http/kraft.yaml @@ -0,0 +1,101 @@ +specification: '0.6' +name: dotnet-http-hyperlight + +unikraft: + source: https://github.com/unikraft/unikraft.git + version: plat-hyperlight + kconfig: + # Platform + CONFIG_PLAT_HYPERLIGHT: 'y' + CONFIG_PAGING: 'n' + CONFIG_LIBUKVMEM: 'n' + CONFIG_LIBUKINTCTLR_HYPERLIGHT: 'y' + CONFIG_HYPERLIGHT_MAX_GUEST_LOG_LEVEL: 0 + + # Suppress kernel logging for performance + CONFIG_LIBUKPRINT_KLVL_CRIT: 'y' + CONFIG_LIBUKPRINT_PRINT_TIME: 'n' + CONFIG_LIBUKPRINT_PRINT_SRCNAME: 'n' + CONFIG_LIBUKBOOT_BANNER_NONE: 'y' + + # Size optimization + CONFIG_OPTIMIZE_SIZE: 'y' + CONFIG_OPTIMIZE_PIE: 'y' + + # VFS and initrd support + CONFIG_LIBVFSCORE: 'y' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI: 'y' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI_CUSTOM: 'y' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_MP: '/' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_DRIVER: 'cpiovfs' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_UKOPTS_IFINITRD0: 'y' + CONFIG_LIBCPIOVFS: 'y' + CONFIG_LIBUKCPIO: 'y' + + # ELF loader - .NET apphost binary + CONFIG_APPELFLOADER: 'y' + CONFIG_APPELFLOADER_VFSEXEC: 'y' + CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'n' + CONFIG_APPELFLOADER_VFSEXEC_ENVPATH: 'n' + CONFIG_APPELFLOADER_VFSEXEC_PATH: '/app/KestrelHyperlight' + CONFIG_APPELFLOADER_VFSEXEC_EXECBIT_CHECK: 'n' + CONFIG_LIBPOSIX_ENVIRON_ENVP0: '"PATH=/app:/usr/bin:/bin"' + CONFIG_LIBPOSIX_ENVIRON_ENVP1: '"DOTNET_GCHeapHardLimit=0x8000000"' + CONFIG_LIBPOSIX_ENVIRON_ENVP2: '"DOTNET_gcServer=0"' + CONFIG_LIBPOSIX_ENVIRON_ENVP3: '"DOTNET_EnableWriteXorExecute=0"' + CONFIG_LIBPOSIX_ENVIRON_ENVP4: '"COMPlus_EnableDiagnostics=0"' + CONFIG_LIBPOSIX_ENVIRON_ENVP5: '"DOTNET_DefaultStackSize=0x10000"' + CONFIG_LIBPOSIX_ENVIRON_ENVP6: '"hostBuilder__reloadConfigOnChange=false"' + CONFIG_LIBELF: 'y' + + # Random number support + CONFIG_LIBUKRANDOM_CMDLINE_SEED: 'y' + CONFIG_LIBUKRANDOM_GETRANDOM: 'y' + + # Stack size (order 9 = 2^9 pages = 2MB, needed for CoreCLR's 1.5MB alloca) + CONFIG_STACK_SIZE_PAGE_ORDER: 9 + + # Threading support (required for .NET runtime) + CONFIG_LIBUKBOOT_MAINTHREAD: 'y' + CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y' + CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y' + CONFIG_LIBPOSIX_PROCESS_SIGNAL: 'y' + CONFIG_LIBPOSIX_FUTEX: 'y' + CONFIG_LIBUKSCHED: 'y' + CONFIG_LIBUKSCHEDCOOP: 'y' + CONFIG_LIBUKMPI: 'y' + CONFIG_LIBUKLOCK: 'y' + CONFIG_LIBUKLOCK_SEMAPHORE: 'y' + CONFIG_LIBUKLOCK_MUTEX: 'y' + + # mmap needed by CoreCLR memory management + CONFIG_LIBUKMMAP: 'y' + + # Event/polling support + CONFIG_LIBPOSIX_POLL: 'y' + CONFIG_LIBPOSIX_EVENTFD: 'y' + CONFIG_LIBPOSIX_FDIO: 'y' + CONFIG_LIBPOSIX_FDTAB: 'y' + CONFIG_LIBUKFILE: 'y' + + # devfs + /dev/hcall (required for host-proxied networking) + CONFIG_LIBDEVFS: 'y' + CONFIG_LIBDEVFS_AUTOMOUNT: 'y' + CONFIG_HYPERLIGHT_HCALL: 'y' + + # Networking: host-proxied sockets + CONFIG_LIBPOSIX_SOCKET: 'y' + CONFIG_LIBHOSTSOCK: 'y' + + +libraries: + app-elfloader: + source: https://github.com/unikraft/app-elfloader.git + version: plat-hyperlight + libelf: + source: https://github.com/unikraft/lib-libelf.git + version: staging + +targets: + - architecture: x86_64 + platform: hyperlight diff --git a/examples/go-http/Dockerfile b/examples/go-http/Dockerfile new file mode 100644 index 0000000..8353118 --- /dev/null +++ b/examples/go-http/Dockerfile @@ -0,0 +1,32 @@ +FROM golang:1.21.3-bookworm AS build + +WORKDIR /src + +COPY ./main.go /src/main.go + +RUN set -xe; \ + CGO_ENABLED=0 \ + GOARCH=amd64 \ + go build \ + -buildmode=pie \ + -ldflags="-linkmode=internal" \ + -o /server main.go \ + ; + +# Strip PT_INTERP from the static PIE binary: Go's internal linker adds +# /lib64/ld-linux-x86-64.so.2 as interpreter for PIE on Linux, but the +# binary is fully self-contained and does not need a dynamic linker. +# Unikraft's elfloader fails when the interpreter is listed but absent. +# Patch the program header: change PT_INTERP (type=3) -> PT_NULL (type=0). +COPY patch_interp.py /patch_interp.py +RUN python3 /patch_interp.py /server + +FROM scratch AS rootfs + +COPY --from=build /server /bin/server + +# --- CPIO rootfs builder (used by: docker build --target cpio) --- +FROM alpine:3.20 AS cpio +RUN apk add --no-cache cpio findutils +COPY --from=rootfs / /rootfs/ +RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null diff --git a/examples/go-http/Justfile b/examples/go-http/Justfile new file mode 100644 index 0000000..96b3ad0 --- /dev/null +++ b/examples/go-http/Justfile @@ -0,0 +1,49 @@ +# go-http on Hyperlight +# +# just run - Run the HTTP server (--port implies --net) +# just rootfs - Build rootfs via Docker (cross-platform) +# just build - Build/pull kernel +# just clean - Remove build artifacts + +set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] +export DOCKER_BUILDKIT := "0" + +kernel := ".unikraft/build/go-http-hyperlight_hyperlight-x86_64" +initrd := "initrd.cpio" +memory := "32Mi" +image := "go-http-hyperlight" + +# Run the HTTP server (curl http://localhost:8080 to test) +run: + hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --port 8080 -- /bin/server + +# Build rootfs via Docker (cross-platform) +rootfs: + docker build --platform linux/amd64 --target cpio -t {{image}}-cpio . + - docker rm -f {{image}}-tmp + docker create --name {{image}}-tmp {{image}}-cpio /bin/true + docker cp {{image}}-tmp:/output.cpio ./{{initrd}} + docker rm -f {{image}}-tmp + +# Build kernel via kraft (Linux) +[unix] +build: + -kraft-hyperlight build --plat hyperlight --arch x86_64 + +# Pull pre-built kernel from GHCR (Windows — no kernel published yet) +[windows] +build: + Write-Host "No pre-built kernel for go-http yet. Build on Linux first." + +# Clean + rebuild everything +rebuild: clean build rootfs + +# Clean build artifacts +[unix] +clean: + rm -rf .unikraft {{initrd}} + +[windows] +clean: + if (Test-Path .unikraft) { Remove-Item -Recurse -Force .unikraft } + if (Test-Path {{initrd}}) { Remove-Item -Force {{initrd}} } diff --git a/examples/go-http/kraft.yaml b/examples/go-http/kraft.yaml new file mode 100644 index 0000000..d06b787 --- /dev/null +++ b/examples/go-http/kraft.yaml @@ -0,0 +1,90 @@ +specification: '0.6' +name: go-http-hyperlight + +unikraft: + source: https://github.com/unikraft/unikraft.git + version: plat-hyperlight + kconfig: + # Platform + CONFIG_PLAT_HYPERLIGHT: 'y' + CONFIG_PAGING: 'n' + CONFIG_LIBUKVMEM: 'n' + CONFIG_LIBUKINTCTLR_HYPERLIGHT: 'y' + CONFIG_HYPERLIGHT_MAX_GUEST_LOG_LEVEL: 0 + + # Suppress kernel logging for performance + CONFIG_LIBUKPRINT_KLVL_CRIT: 'y' + CONFIG_LIBUKPRINT_PRINT_TIME: 'n' + CONFIG_LIBUKPRINT_PRINT_SRCNAME: 'n' + CONFIG_LIBUKBOOT_BANNER_NONE: 'y' + + # Size optimization + CONFIG_OPTIMIZE_SIZE: 'y' + CONFIG_OPTIMIZE_PIE: 'y' + + # VFS and initrd support + CONFIG_LIBVFSCORE: 'y' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI: 'y' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI_CUSTOM: 'y' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_MP: '/' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_DRIVER: 'cpiovfs' + CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_UKOPTS_IFINITRD0: 'y' + CONFIG_LIBCPIOVFS: 'y' + CONFIG_LIBUKCPIO: 'y' + + # ELF loader - Go binary location + CONFIG_APPELFLOADER: 'y' + CONFIG_APPELFLOADER_VFSEXEC: 'y' + CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'n' + CONFIG_APPELFLOADER_VFSEXEC_ENVPATH: 'n' + CONFIG_APPELFLOADER_VFSEXEC_PATH: '/bin/server' + CONFIG_LIBPOSIX_ENVIRON_ENVP0: '"PATH=/bin"' + CONFIG_LIBPOSIX_ENVIRON_ENVP1: '"GOMEMLIMIT=64MiB"' + CONFIG_LIBPOSIX_ENVIRON_ENVP2: '"GOMAXPROCS=1"' + CONFIG_LIBELF: 'y' + + # Random number support + CONFIG_LIBUKRANDOM_CMDLINE_SEED: 'y' + CONFIG_LIBUKRANDOM_GETRANDOM: 'y' + + # Threading support + CONFIG_LIBUKBOOT_MAINTHREAD: 'y' + CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y' + CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y' + + # Signal and futex support (needed for Go runtime) + CONFIG_LIBPOSIX_PROCESS_SIGNAL: 'y' + CONFIG_LIBPOSIX_FUTEX: 'y' + + # eventfd (needed by Go's epoll-based netpoller) + CONFIG_LIBPOSIX_EVENTFD: 'y' + + # mmap needed by Go runtime (heap reservation + page summaries) + CONFIG_LIBUKMPI: 'y' + CONFIG_LIBUKMMAP: 'y' + + # devfs + /dev/hcall (required for host-proxied networking) + CONFIG_LIBDEVFS: 'y' + CONFIG_LIBDEVFS_AUTOMOUNT: 'y' + CONFIG_HYPERLIGHT_HCALL: 'y' + + # Networking: host-proxied sockets + CONFIG_LIBPOSIX_SOCKET: 'y' + CONFIG_LIBHOSTSOCK: 'y' + + # FD infrastructure (required by posix-socket) + CONFIG_LIBPOSIX_FDIO: 'y' + CONFIG_LIBPOSIX_FDTAB: 'y' + CONFIG_LIBUKFILE: 'y' + +libraries: + app-elfloader: + source: https://github.com/unikraft/app-elfloader.git + version: plat-hyperlight + libelf: + source: https://github.com/unikraft/lib-libelf.git + version: staging + +targets: + - architecture: x86_64 + platform: hyperlight diff --git a/examples/go-http/main.go b/examples/go-http/main.go new file mode 100644 index 0000000..646c6ff --- /dev/null +++ b/examples/go-http/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello from Hyperlight-Unikraft!") +} + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", helloHandler) + + fmt.Println("Server is running on http://localhost:8080") + log.Fatal(http.ListenAndServe(":8080", mux)) +} diff --git a/examples/go-http/patch_interp.py b/examples/go-http/patch_interp.py new file mode 100644 index 0000000..a06c796 --- /dev/null +++ b/examples/go-http/patch_interp.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Strip PT_INTERP from a static PIE binary. + +Go's internal linker adds /lib64/ld-linux-x86-64.so.2 as PT_INTERP for PIE +binaries on Linux, but a CGO_ENABLED=0 binary is fully self-contained and +does not need a dynamic linker. Patch PT_INTERP -> PT_NULL so elfloaders +that reject missing interpreters will load the binary directly. +""" +import struct +import sys + +if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + +path = sys.argv[1] +with open(path, "rb") as f: + data = bytearray(f.read()) + +e_phoff, = struct.unpack_from(" PT_NULL at program header index {i}") + +with open(path, "wb") as f: + f.write(data) diff --git a/examples/go/patch_interp.py b/examples/go/patch_interp.py index 37e94ca..a06c796 100644 --- a/examples/go/patch_interp.py +++ b/examples/go/patch_interp.py @@ -9,6 +9,10 @@ import struct import sys +if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + sys.exit(1) + path = sys.argv[1] with open(path, "rb") as f: data = bytearray(f.read()) diff --git a/host/Cargo.toml b/host/Cargo.toml index de55d41..c312f67 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -44,5 +44,5 @@ nix = { version = "0.29", features = ["fs"] } libc = "0.2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_System_IO", "Win32_System_Ioctl", "Win32_Storage_FileSystem"] } +windows-sys = { version = "0.61", features = ["Win32_System_IO", "Win32_System_Ioctl", "Win32_Storage_FileSystem", "Win32_Networking_WinSock"] } diff --git a/host/src/lib.rs b/host/src/lib.rs index d785f34..bd1229b 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -1030,15 +1030,26 @@ fn register_internal_tools( ec.store(code, Ordering::Relaxed); Ok(serde_json::json!({})) }); + // Create socket table early so __hl_sleep can poll sockets while sleeping. + let socket_table = network.map(|_| Arc::new(Mutex::new(SocketTable::new()))); + let sc = sleep_cancel.clone(); + let st_for_sleep = socket_table.clone(); tools.register("__hl_sleep", move |args| { let ns = args["ns"].as_u64().unwrap_or(0).min(MAX_SLEEP_NS); if ns > 0 { + if let Some(ref table) = st_for_sleep { + return hl_sleep_poll_sockets(&sc, table, ns); + } sc.wait(Duration::from_nanos(ns)); } Ok(serde_json::json!({})) }); - network.map(|policy| register_net_tools(tools, policy, listen_ports)) + + if let (Some(policy), Some(ref table)) = (network, &socket_table) { + register_net_tools(tools, policy, listen_ports, table.clone()); + } + socket_table } // --------------------------------------------------------------------------- @@ -1605,14 +1616,291 @@ fn handle_net_getsockname( } } +/// Poll host sockets for readiness using the real OS `poll()` syscall. +/// +/// Input: `{"fds": [{"fd": N, "events": N}, ...], "timeout_ms": N}` +/// Output: `{"ready": [{"fd": N, "revents": N}, ...]}` +/// +/// `events`/`revents` use standard POSIX poll flags (POLLIN=1, POLLOUT=4). +#[cfg(unix)] +fn handle_net_poll( + table: &Mutex, + args: &serde_json::Value, +) -> Result { + use serde_json::json; + use std::os::unix::io::AsRawFd; + + let fds_val = args + .get("fds") + .and_then(|v| v.as_array()) + .ok_or_else(|| anyhow!("net_poll: missing 'fds' array"))?; + let timeout_ms = args + .get("timeout_ms") + .and_then(|v| v.as_i64()) + .unwrap_or(0) + .clamp(0, libc::c_int::MAX as i64) as libc::c_int; + + let tbl = table.lock().unwrap(); + let mut pollfds: Vec = Vec::new(); + let mut guest_fds: Vec = Vec::new(); + let mut ready = Vec::new(); + + for entry in fds_val { + let fd = entry + .get("fd") + .and_then(|v| v.as_u64()) + .ok_or_else(|| anyhow!("net_poll: entry missing 'fd'"))?; + let raw_events = entry.get("events").and_then(|v| v.as_i64()).unwrap_or(0); + if !(0..=i16::MAX as i64).contains(&raw_events) { + return Err(anyhow!("net_poll: events {raw_events} out of i16 range")); + } + let events = raw_events as i16; + if let Ok(sock) = tbl.get_socket(fd) { + pollfds.push(libc::pollfd { + fd: sock.as_raw_fd(), + events, + revents: 0, + }); + guest_fds.push(fd); + } else { + ready.push(json!({ "fd": fd, "revents": libc::POLLNVAL as i64 })); + } + } + drop(tbl); + + if pollfds.is_empty() { + return Ok(json!({"ready": ready})); + } + + let ret = unsafe { + libc::poll( + pollfds.as_mut_ptr(), + pollfds.len() as libc::nfds_t, + timeout_ms, + ) + }; + + if ret < 0 { + let err = std::io::Error::last_os_error(); + if err.raw_os_error() != Some(libc::EINTR) { + return Err(anyhow!("net_poll: poll() failed: {err}")); + } + } + + for (i, pfd) in pollfds.iter().enumerate() { + if pfd.revents != 0 { + ready.push(json!({ + "fd": guest_fds[i], + "revents": pfd.revents as i64, + })); + } + } + Ok(json!({"ready": ready})) +} + +#[cfg(windows)] +fn handle_net_poll( + table: &Mutex, + args: &serde_json::Value, +) -> Result { + use serde_json::json; + use std::os::windows::io::AsRawSocket; + use windows_sys::Win32::Networking::WinSock::{ + WSAPoll, POLLERR as W_POLLERR, POLLHUP as W_POLLHUP, POLLNVAL as W_POLLNVAL, POLLRDNORM, + POLLWRNORM, WSAPOLLFD, + }; + + const POSIX_POLLIN: i16 = 0x0001; + const POSIX_POLLOUT: i16 = 0x0004; + const POSIX_POLLERR: i16 = 0x0008; + const POSIX_POLLHUP: i16 = 0x0010; + const POSIX_POLLNVAL: i16 = 0x0020; + + fn posix_to_win(posix: i16) -> i16 { + let mut win: i16 = 0; + if posix & POSIX_POLLIN != 0 { + win |= POLLRDNORM; + } + if posix & POSIX_POLLOUT != 0 { + win |= POLLWRNORM; + } + win + } + + fn win_to_posix(win: i16) -> i16 { + let mut posix: i16 = 0; + if win & POLLRDNORM != 0 { + posix |= POSIX_POLLIN; + } + if win & POLLWRNORM != 0 { + posix |= POSIX_POLLOUT; + } + if win & W_POLLERR != 0 { + posix |= POSIX_POLLERR; + } + if win & W_POLLHUP != 0 { + posix |= POSIX_POLLHUP; + } + if win & W_POLLNVAL != 0 { + posix |= POSIX_POLLNVAL; + } + posix + } + + let fds_val = args + .get("fds") + .and_then(|v| v.as_array()) + .ok_or_else(|| anyhow!("net_poll: missing 'fds' array"))?; + let timeout_ms = args + .get("timeout_ms") + .and_then(|v| v.as_i64()) + .unwrap_or(0) + .clamp(0, i32::MAX as i64) as i32; + + let tbl = table.lock().unwrap(); + let mut pollfds: Vec = Vec::new(); + let mut guest_fds: Vec = Vec::new(); + let mut ready = Vec::new(); + + for entry in fds_val { + let fd = entry + .get("fd") + .and_then(|v| v.as_u64()) + .ok_or_else(|| anyhow!("net_poll: entry missing 'fd'"))?; + let raw_events = entry.get("events").and_then(|v| v.as_i64()).unwrap_or(0); + if !(0..=i16::MAX as i64).contains(&raw_events) { + return Err(anyhow!("net_poll: events {raw_events} out of i16 range")); + } + let events = posix_to_win(raw_events as i16); + if let Ok(sock) = tbl.get_socket(fd) { + pollfds.push(WSAPOLLFD { + fd: sock.as_raw_socket() as usize, + events, + revents: 0, + }); + guest_fds.push(fd); + } else { + ready.push(json!({ "fd": fd, "revents": POSIX_POLLNVAL as i64 })); + } + } + drop(tbl); + + if pollfds.is_empty() { + return Ok(json!({"ready": ready})); + } + + let ret = unsafe { WSAPoll(pollfds.as_mut_ptr(), pollfds.len() as u32, timeout_ms) }; + + if ret < 0 { + let err = std::io::Error::last_os_error(); + return Err(anyhow!("net_poll: WSAPoll() failed: {err}")); + } + + for (i, pfd) in pollfds.iter().enumerate() { + if pfd.revents != 0 { + ready.push(json!({ + "fd": guest_fds[i], + "revents": win_to_posix(pfd.revents) as i64, + })); + } + } + Ok(json!({"ready": ready})) +} + +/// `__hl_sleep` variant: poll all sockets in the table while sleeping. +/// Returns early if any socket becomes ready. +#[cfg(unix)] +fn hl_sleep_poll_sockets( + _sc: &SleepCancel, + table: &Mutex, + ns: u64, +) -> Result { + use serde_json::json; + use std::os::unix::io::AsRawFd; + + let tbl = table.lock().unwrap(); + let mut pollfds: Vec = tbl + .sockets + .values() + .map(|hs| libc::pollfd { + fd: hs.socket.as_raw_fd(), + events: libc::POLLIN | libc::POLLOUT | libc::POLLERR, + revents: 0, + }) + .collect(); + drop(tbl); + + if pollfds.is_empty() { + _sc.wait(Duration::from_nanos(ns)); + return Ok(json!({})); + } + + let timeout_ms = ((ns / 1_000_000) as libc::c_int).clamp(1, 30_000); + let ret = unsafe { + libc::poll( + pollfds.as_mut_ptr(), + pollfds.len() as libc::nfds_t, + timeout_ms, + ) + }; + + if ret < 0 { + let err = std::io::Error::last_os_error(); + if err.raw_os_error() != Some(libc::EINTR) { + return Err(anyhow!("hl_sleep poll failed: {err}")); + } + } + + Ok(json!({"socket_ready": ret > 0})) +} + +#[cfg(windows)] +fn hl_sleep_poll_sockets( + _sc: &SleepCancel, + table: &Mutex, + ns: u64, +) -> Result { + use serde_json::json; + use std::os::windows::io::AsRawSocket; + use windows_sys::Win32::Networking::WinSock::{WSAPoll, POLLRDNORM, POLLWRNORM, WSAPOLLFD}; + + let tbl = table.lock().unwrap(); + let mut pollfds: Vec = tbl + .sockets + .values() + .map(|hs| WSAPOLLFD { + fd: hs.socket.as_raw_socket() as usize, + // POLLERR is output-only on Windows; setting it in events causes WSAEINVAL + events: (POLLRDNORM | POLLWRNORM) as i16, + revents: 0, + }) + .collect(); + drop(tbl); + + if pollfds.is_empty() { + _sc.wait(Duration::from_nanos(ns)); + return Ok(json!({})); + } + + let timeout_ms = ((ns / 1_000_000) as i32).clamp(1, 30_000); + let ret = unsafe { WSAPoll(pollfds.as_mut_ptr(), pollfds.len() as u32, timeout_ms) }; + + if ret < 0 { + let err = std::io::Error::last_os_error(); + return Err(anyhow!("hl_sleep WSAPoll failed: {err}")); + } + + Ok(json!({"socket_ready": ret > 0})) +} + // --------------------------------------------------------------------------- fn register_net_tools( tools: &mut ToolRegistry, policy: &NetworkPolicy, listen_ports: Option<&ListenPorts>, -) -> Arc> { - let table = Arc::new(Mutex::new(SocketTable::new())); + table: Arc>, +) { let policy = Arc::new(policy.clone()); let t = table.clone(); @@ -1678,7 +1966,10 @@ fn register_net_tools( handle_net_getsockname(&t, &args) }); - table + { + let t = table.clone(); + tools.register("net_poll", move |args| handle_net_poll(&t, &args)); + } } /// Routes incoming fs_* tool calls to the matching `FsSandbox` by @@ -3594,7 +3885,8 @@ mod tests { fn net_getsockopt_returns_correct_type_for_dgram() { let mut reg = ToolRegistry::new(); let policy = NetworkPolicy::AllowAll; - register_net_tools(&mut reg, &policy, None); + let table = Arc::new(Mutex::new(SocketTable::new())); + register_net_tools(&mut reg, &policy, None, table); let req = br#"{"name":"net_socket","args":{"family":2,"type":2}}"#; let resp = std::str::from_utf8(®.dispatch(req)).unwrap().to_string(); @@ -3695,7 +3987,8 @@ mod tests { let mut reg = ToolRegistry::new(); let policy = NetworkPolicy::AllowAll; - register_net_tools(&mut reg, &policy, None); + let table = Arc::new(Mutex::new(SocketTable::new())); + register_net_tools(&mut reg, &policy, None, table); // Create a socket let req = br#"{"name":"net_socket","args":{"family":2,"type":2}}"#;