Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
- python-agent-driver
- powershell
- networking-py
- go-http
- dotnet-http
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -403,6 +449,8 @@ jobs:
- python-agent-driver
- powershell
- networking-py
- go-http
- dotnet-http
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions examples/dotnet-http/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions examples/dotnet-http/Justfile
Original file line number Diff line number Diff line change
@@ -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}} }
7 changes: 7 additions & 0 deletions examples/dotnet-http/KestrelHyperlight.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
13 changes: 13 additions & 0 deletions examples/dotnet-http/Program.cs
Original file line number Diff line number Diff line change
@@ -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();
101 changes: 101 additions & 0 deletions examples/dotnet-http/kraft.yaml
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions examples/go-http/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading