Repository navigation
Releases: hyperlight-dev/hyperlight-unikraft
Release list
dev (0.17.0+dev.3df47f6)
The latest build of main that passed CI: 0.17.0+dev.3df47f6, from 3df47f6 site: stop narrow phones from scrolling sideways.
Replaced on every push to main. It pulls the images of its own commit, :initrd-dev-3df47f6 (:dev-3df47f6 to build FROM).
curl -fsSL https://raw.githubusercontent.com/hyperlight-dev/hyperlight-unikraft/main/install.sh | HLUK_VERSION=dev sh$env:HLUK_VERSION = 'dev'; irm https://raw.githubusercontent.com/hyperlight-dev/hyperlight-unikraft/main/install.ps1 | iexv0.17.0
Added
-
arm64 guests:
hlukruns on arm64 hosts, macOS on Apple silicon (Hypervisor.framework) and Linux arm64 (KVM), with an arm64 kernel,kernel/elfloader_hyperlight-arm64, embedded in place of the x86_64 one on an arm64 host.just build-kernelandjust verify-kernelbuild and check both (the arm64 one cross-compiled). Every runtime runs on it, warm snapshots included; Rust programs need Rust 1.99 or later, the first to link a static PIE for arm64, and are built on Linux. -
HLUK_ROOTFS_PLATFORM=linux/arm64 just build-rootfs <runtime>builds an arm64 rootfs on an x86_64 host (intobuild-elfloader/<runtime>-rootfs-arm64.cpio). Every runtime image builds for either architecture; powershell on arm64 is Microsoft's glibc build, since there is no musl one. -
Published images are multi-platform (linux/amd64 and linux/arm64): each architecture is built on a native runner and the tags joined into one index (
just publish ... <arch>,just publish-index). The kernel image carries both kernels. -
Releases carry
hlukfor Linux arm64 and macOS (Apple silicon) too, and aSHA256SUMS.install.shinstalls them (and signs the macOS binary for Hypervisor.framework);install.ps1installs on Windows:irm https://raw.githubusercontent.com/hyperlight-dev/hyperlight-unikraft/main/install.ps1 | iex. -
The
go,dotnet-aotandhttp-dotnettemplates build for the host's CPU, whatever it is (they name only the OS, Linux), andhluk build's Docker builds name the host's platform.dotnet-aotnow publishes in Alpine's .NET SDK image (Docker), a musl toolchain: linked by a glibc one, an arm64 binary did not load in the guest.hluk runsays so when a mounted program is a macOS or Windows binary, or one for another architecture. -
CI builds, lints and unit-tests on macOS and Linux arm64. A new
arm64workflow builds the arm64 images and test binaries and runs the integration tests on hyperlight-dev's self-hosted arm64 runners (Linux/KVM and macOS/Hypervisor.framework): onmain, on demand, and on pull requests labelledci/arm64. The Linux runner, a small VM with nested virtualization, runs only the suites with small guests; macOS runs them all. -
GetRandomByteshost function: the arm64 kernel seeds its CSPRNG from the host, at boot and on every restore, since Apple's cores have no random-number instructions. -
On macOS,
cargo runandcargo testsign binaries with the hypervisor entitlement (dev/macos-sign-and-run.sh), and an unsignedhluksays how to sign itself. Tests run one at a time there (RUST_TEST_THREADSoverrides it): Hypervisor.framework runs one sandbox of a process at a time. -
/dev/maps(LIBUKVMEM_DEVFS_MAPS, both kernels) lists the address space in/proc/self/mapsformat. The powershell image links/proc/self/mapsto it: glibc reads it to find the main thread's stack, and PowerShell's glibc build on arm64 does not start without it.
Changed
- Snapshots saved by an earlier release are refused (the kernel and the host functions changed); save them again.
- The
ctemplate's[build]command takesCC, e.g. a Linux cross compiler on macOS. - Projects made by an earlier
hlukpin amd64 (GOARCH=amd64or-r linux-musl-x64in[build], or in an http-dotnetDockerfile); drop it to build them on arm64.
Fixed
- An entry-point program that slept before anything else happened was reported deadlocked: the boot's first idle dropped its timer.
- Generic Unikraft arm64 bugs that kept Linux binaries from running, each reproduced and fixed on QEMU/KVM:
execvestarted the program at a stale address with stale registers and FP state, system calls ran with interrupts masked (so any that blocked crashed),struct statandstruct epoll_eventhad the x86_64 layout,unamereportedarm64rather thanaarch64, the build failed without SMP or withsignalfd, andchmod,chownand symlinks failed with ENOSYS (the*atsystem calls arm64 has in their place were missing, so pip could not install a package). A jump into a non-executable page raises SIGSEGV instead of crashing the kernel (arm64 took instruction faults for reads). An SVE or SME instruction raises SIGILL, as on Linux, instead of SIGFPE or a kernel crash, so Node.js (whose OpenSSL probes for SVE at startup) runs on CPUs with SVE and on Apple M4. hluk pulltakes the image for the host's architecture from an index, and refuses a single image built for another one.- A signal the CPU raises (SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP) carries the
si_codeandsi_addrLinux would report (the faulting address or instruction where Linux gives one); it looked like akill(2)from the process itself. Runtimes read these to turn a null dereference into an exception. Both architectures. - macOS: sockets are close-on-exec and don't raise SIGPIPE, host errors reach the guest as Linux errno values, a datagram socket can be disconnected and connected again, and a refused connect completes.
v0.16.0
Added
hluk run --profile(alsoHLUK_PROFILE=1andSandboxBuilder::profile) prints where a sandbox's time goes, host side: each VM entry split into guest and host function time with its VM exits, each host function, and boot and restore. Seedocs/profiling.md.
Changed
- Snapshot restore takes about 0.9 ms instead of 1.9–2.7 ms, now about the same for every runtime. After a restore the kernel's frame allocator takes its memory in 512 KiB chunks as it needs them, next to the copy-on-write pages, instead of writing its metadata over most of the scratch memory. Only the memory exception delivery writes is pre-faulted, and the exception stacks get fresh pages instead of copies. The new host function
GetResumeStatebrings the clock, mounts and resolver in one exit instead of three. - A guest function call costs one VM exit instead of five: its start and return (with the result) ride on the entry's
Yield, and the kernel refetches the environment only when the embedder changed it.CallResultis gone;CallStartedandCallDone(status, result)remain for an entry that ends without aYield. - The rootfs is no longer copied into guest memory at boot: files extracted from the initrd reference it until they are changed (
CONFIG_LIBVFSCORE_AUTOMOUNT_EXTRACT_BORROW). Python boots in 178 ms instead of 296, its snapshot is 74 MiB instead of 114, and it runs in 48 MiB of scratch instead of 128. The initrd is now mapped with 4 KiB pages, since Hyperlight's snapshot does not take large pages. - Snapshots saved by an earlier release are refused (the host functions changed); save them again.
Fixed
- The copy-on-write page fault handler no longer changes SSE registers. It copied the page with an SSE
memcpy, and exception entry doesn't save those registers, so the vectorised store that faulted could write the wrong data. - The buddy frame allocator cleared its bitmaps with an SSE
memsetwhen memory was added from a page fault, changing the registers of the code that faulted. Generic Unikraft bug, reproduced on QEMU/KVM. - A ramfs file shrunk and then grown again (by
truncateor a write past its end) read back the bytes the shrink had cut instead of zeros. Generic Unikraft bug, reproduced on QEMU/KVM.
v0.15.0
Added
- quickjs and wasmtime runtime images (tier 3), with
hluk inittemplates.- quickjs: QuickJS (quickjs-ng 0.17) with
qjs:stdandqjs:os, in a 2 MiB rootfs. Scripts and ES modules both run, and globals persist between calls. A call returns once the jobs and timers it started have run. - wasmtime: Wasmtime 49 with Cranelift. Runs WebAssembly text,
.wasmand.cwasmmodules and components under WASI 0.1, 0.2 and 0.3 (0.3 is experimental in Wasmtime).
- quickjs: QuickJS (quickjs-ng 0.17) with
- Guest function calls:
AppSandbox::call(function, input)runs a function the guest defined and returns its result, JSON in and out. Served by quickjs, node, python, python-shell, agent, dotnet-jit and wasmtime.hluk run --call FUNCTION --input JSONdoes it from the CLI. Seedocs/calls.md. - Host function calls:
SandboxBuilder::host_function(name, f)gives the guest one of your functions. It'shost.callin JavaScript (andhost:modules in quickjs),hyperlight.callin Python andHost.Callin C#. In wasmtime, any import outside WASI is linked to one, so a component can import a WIT interface you implement. - Kernel: a driver's
write()can carry a result after the status (sent asCallResult), andHLCALL_IOC_HOSTCALLcalls the embedder's functions through one host function,HostCall(name, args).hl_driver.hhashl_set_resultandhl_host_call. - Two
hluk inittemplates:python-shell, a Python script that runs shell commands throughsubprocesson the python-shell rootfs (the one runtime that had onlyhttp-python), andbash-repl, an interactive shell whose read-eval loop takes commands fromhluk run's stdin until Ctrl-D. hluk init --templatetakes a template of your own: a directory on disk, orgithub.com/OWNER/REPO[/PATH][@REF](a browser's…/tree/REF/PATHURL too), downloaded as the repository's tarball through the GitHub API with no git on the host;GITHUB_TOKENreaches a private repository and lifts the anonymous rate limit. The template'sruntimeis checked against the published ones, a template is held to 256 text files and 4 MiB, andinitprints the[build] commandand Dockerfilehluk buildwould run from a template that is not built in.tieris optional in such a template'stemplate.toml.docs/templates.mdis the guide to writing one, withexamples/templates/word-countto copy from.
Changed
Exechas a new variant,Call, so an exhaustivematchon it needs another arm.
Fixed
- Programs, most often .NET, could resume at a wild address after a snapshot restore (
dotnet_jit_snapshot_round_tripfailed about half the time). A page fault inside a page fault handler reused the top of the exception stack and overwrote the outer fault's registers. Exception handlers now stay on the stack they were entered on, so a nested fault lands below the outer frame. The fix is in the native x86_64 platform, shared with KVM. - Reading stdin past its end deadlocked the guest:
poll()andselect()waited forever for more input, so a second shellreador Python'sinput()afterEOFErrorhung. The end of input is now final on Hyperlight (CONFIG_LIBPOSIX_TTY_SERIAL_EOF_FINAL). openat()from a directory fd never crossed into a mount below that directory, so WASI programs couldn't see mounts. Generic Unikraft bug, reproduced on QEMU/KVM.- A relative path starting with a dotfile (
.profile) was glued to its directory without a/. Generic Unikraft bug, reproduced on QEMU/KVM.
v0.14.2
Changed
- A guest with no memory size asked for gets the size its runtime image is tested with, from a table the library carries (
RUNTIME_SCRATCH_MB: c and rust 64 MiB, go 128, bash, python, python-shell and dotnet-aot 256, node 512, dotnet-jit 768, powershell 1024, agent 1536), instead of a flat 256 MiB:SandboxBuilder::bootlooks the driver in the initrd up (default_scratch_mb),hluk run --scratch-mbis optional, and a manifest'sscratch_mbis an override, with the runtime read from the image's name when the rootfs is a published one. A rootfs that has grown past half of that memory is reported byhluk buildandhluk runwith a size to set.
Added
- Projects.
hluk initstarts a project from a template: it writes ahluk.tomlmanifest and starter files, and pulls the published rootfs the template runs on from GHCR into a local cache (~/.cache/hluk;HLUK_CACHE_DIRoverrides), pinned to the tag of thishlukrelease (<runtime>:initrd-v<version>) so a project keeps running under thehlukthat made it.hluk runwith no--initrdruns the project the manifest describes, its flags standing in for the manifest's keys where given;hluk buildruns its[build]command and builds its[rootfs] dockerfilewith Docker into.hluk/rootfs.cpio;hluk pullrefreshes the rootfs image;hluk templateslists the templates;hluk cachelists and removes what was pulled and snapshotted. With no argumentsinitasks for the template and the name. The pull speaks the registry API itself (anonymous token, manifest, one digest-checked layer), so a project runs on any hosthlukruns on with no Docker. Seedocs/manifest.md. hluk run --runtime <name|image>runs a published runtime image by name (python,node,agent, …) or full reference with no project and no local build: pulled into the cache when missing, warm by default.hluk snapshot save --runtimesaves one the same way, and its--warm-exec CODEruns code before the snapshot is taken.--log-level infotimings are printed to a tenth of a millisecond.- Thirteen templates, one per runtime, compiled into the binary:
python,agent,node,bash,dotnet(C# compiled in the guest by Roslyn),powershell,go,rust,c,dotnet-aot(compiled on the host with a[build]command and mounted into the guest), andhttp-python,http-node,http-dotnet(a Flask, Express or Kestrel server on a rootfs the project's Dockerfile extends with pip, npm or the .NET SDK,FROMthe published<runtime>:v<version>base). - Warm starts. A manifest with
warm = true(every template's default),hluk run --warm, orhluk run --runtimehas the first run snapshot the booted guest before the workload runs, and every later run restore it instead of booting;--coldboots fresh. Snapshots live in the cache, one per distinct guest and shared by every project that runs it; a rebuilt rootfs replaces the snapshots of its old file.warm_execis code the runtime driver runs once before that snapshot, so what it loads is in it (http-pythonsetsimport flask,http-noderequire('express')). The snapshot is stamped with the rootfs file,scratch_mb,entry,warm_execand the build's snapshot key, and is taken again when any of them changes; mounts, network policy and environment are supplied on restore and need no new snapshot. install.sh:curl -fsSL https://raw.githubusercontent.com/hyperlight-dev/hyperlight-unikraft/main/install.sh | shinstalls the latest release's Linux binary into~/.local/binwith no Rust toolchain (HLUK_VERSION,HLUK_INSTALL_DIR), verifying it against aSHA256SUMSwhen the release has one.hluk init --image-version X.Y.Z(orHLUK_IMAGE_VERSION) pins another release's images than this build's, for a build between releases.- A program the exec driver runs from a mount is checked to be position-independent before the boot (
hluk buildafter its command,hluk runbefore booting), and refused with the build flag that makes it one, instead of the guest's "Image format not recognized". hluk buildconverts a Docker image's filesystem to the initrd CPIO in Rust (docker exportstreamed through a tar-to-newc converter that lays entries out asfind . | cpio -o -H newcdoes and restores the/etc/hosts,/etc/nsswitch.confand/etc/resolv.confthatdocker exportempties), so the host needs Docker and nothing else, on Windows and macOS too.
v0.14.1
Added
--port alllets the guest bind any port and--port LOW-HIGHa range, next to single ports; the library gainsListenPorts::all()andListenPorts::with_range.allis for a container runtime, whose network namespace already scopes what the guest exposes, the waydocker run -Ppublishes every port.hluk --version.--resolv-conf FILEinstalls a resolver configuration as the guest's/etc/resolv.conf, at boot and again on a restore, so a snapshot resolves names where it now runs rather than where it was taken; the library hasSandboxBuilder::resolv_conf. Without it the rootfs's own file stands, andoptions single-requestis added unless present. Under an allow list, the file's nameservers are exempt on port 53 like the host's own. Kernel: the newGetResolvConfhost function is read once the rootfs is mounted and onresume.
Changed
- A restored guest gets the mounts the restore names: on
resumethe kernel fetches the host's mount table (the newGetMountshost function) and makes its own match, mounting what is new, unmounting what is gone and remounting an entry whose index or read-only flag changed. A warm snapshot saved without mounts serves any mount set, so an embedder restores it to run a script over host directories, andhluk snapshot run --mountmounts what it is given.hluk benchtakes--mount, and the newmountworkload measures a mounted restore. A mount kept busy by a file open across the snapshot stays until a restore finds it free, its operations failing withESTALEmeanwhile; a mount table is at most 32 mounts and 3.5 KiB of entries (MOUNTS_MAX,FSTAB_ENTRIES_MAX). - A snapshot is named by its release and a snapshot key,
<release>-k<kernel>-c<contract>: the embedded kernel's hash and a host contract number, the two things a snapshot depends on. A load matches the key, so a release that changes neither keeps every saved snapshot; one that does refuses them withError::SnapshotRelease, which names the release that saved the snapshot and says to save it again, inhluk snapshot run,hluk benchand the library alike.hluk snapshot keyprints this build's key;SNAPSHOT_KEYis the library's. Snapshots from 0.14.0, named by release alone, are refused the same way. - The
net_*host functions exist on every path, refusing everysocket()withEACCESwhen there is no network policy (before, they were absent and a guest without a policy gotEIO). So a snapshot saved under a policy restores without one, its sockets dying on resume, and a snapshot saved without a policy restores under one; before, the first failed the restore for the missing host functions. - The urunc demo image (
demos/urunc, published ashello-urunc) bakes its workload at/app/hello.pyand declares it as the image'sCMD, sodocker runneeds no command after the image name and the driver's fallback entrypoint stays out of the image contract. Its greeting now names Hyperlight.
Fixed
connect(2)withAF_UNSPECdissolves a datagram socket's association, as on Linux, instead of failing. glibc'sgetaddrinforelies on it to probe every candidate of a dual-stack answer through one IPv6 socket, so a passive lookup --socket.getaddrinfo(None, port, AF_UNSPEC, SOCK_STREAM, 0, AI_PASSIVE), what Python'shttp.serverdoes to bind -- no longer aborts the guest with a glibc assertion on the source address. Kernel:hostsockforwards it as the newnet_disconnecthost function.- A script,
--execor--guest-execon a rootfs with no runtime driver is an error that names--entry, instead of a guest that exits with status 0 having run nothing.
v0.14.0
Added
- Cooperative step model for long-running guests. The kernel hands the vCPU back to the host when the guest goes idle, reporting when its next timer is due, instead of spinning in the VM; the host waits on that timer and on the guest's sockets, then re-enters. A guest with nothing left to wake it is reported deadlocked instead of hung forever, and a guest can be snapshotted at any boundary -- even mid-call -- and resumed later, in the same process or from disk. Library:
SandboxBuilder::bootreturns anAppSandbox;rundispatches a call and waits,submitdispatches without waiting,stepadvances the guest one boundary,joindrives an entry-point workload to exit, andsnapshot/from_snapshot(orrestorein place) checkpoint and resume it, sockets included.Yieldsays why a step returned. - A plain Linux binary can be the guest's entry point (
--entry "/bin/server --flag") with no runtime driver;hluk rundrives it until it exits, and the library exposes it asAppSandbox::join. This is how a container runtime or an actor host runs a server in the guest. asyncionow works in the Python guest, whose event loop needs the Unix-domain sockets the kernel now enables (examples/python/asyncio_demo.py).- The host learns how a call and the guest ended: a failed call carries a status (
Yield::CallFailed { status }-- the program's own exit code, 1 for an uncaught exception, -1 when the driver could not run it), and a process exit carries the guest's status (Yield::Exited { status }, returned byjoin;hluk runexits with it). A guest that halts without reporting is an error, not an exit of unknown status. - A restored guest is re-seeded and reconnected: the kernel reseeds its CSPRNG on restore (two clones no longer draw the same
os.urandom, UUIDs or TLS nonces) and re-establishes its sockets (listeners rebind; connections whose peers died read as closed), so a checkpointed server keeps serving after a restore with nothing saved beside the snapshot. - Runtime drivers receive calls through
/dev/hlcall, each on its own thread. The device'sHLCALL_IOC_MAXLENioctl reports the largest call the host can send, andHLCALL_IOC_GETENVhands a driver the host's current environment to refresh before each call. AppSandbox::snapshot_to(dir),SandboxBuilder::from_snapshot_dir(dir)andAppSandbox::restore_from(dir)move a snapshot through disk. It is named by the crate version inside the directory; a load by another version fails and names the versions present.- New docs:
docs/execution.md(the step model and sandbox lifecycle),docs/driver.md(the/dev/hlcalldriver contract and how to write a driver),docs/clock.mdanddocs/random.md(where the guest's clocks and random bytes come from, and what a restore does to them).
Changed
- Network-policy hostnames are enforced at the DNS question and the destination. Under an allow list, only listed names may be looked up and only listed, resolved-at-build, or DNS-learned addresses may be reached -- the host no longer resolves names for the guest, so an address it was never given is refused. Under a block list, a blocked name is refused and re-checked at each connect (250 ms deadline; refused if the lookup fails or runs late). Malformed or non-query traffic to port 53 is refused, and the allow list's DNS exemption is UDP-only. Before, a name was enforced only through addresses the host re-resolved at every connect, so a blocked name that moved was reachable until the resolver caught up, and an allow-listed guest could query any name.
hluk runandhluk snapshot runexit with the guest's status when the guest is what failed:sys.exit(3)is exit code 3 with nothing added, as running the script directly would be. A malformed--env(no=) or--mount(no:) is now an error instead of being silently ignored, andsnapshot rundrives a restored entry-point guest to its exit likerun.- The crate has its own error type,
hyperlight_unikraft::Error, and every public function returnshyperlight_unikraft::Result, with a variant per condition (CallFailed,GuestExited,Deadlocked,NoDriver,CallInFlight, and so on) plusHyperlightfor the hypervisor layer, so an embedder matches a failed call or a deadlock instead of parsing text.set_env_vars, which cannot fail, no longer returns aResult. - A program the guest runs no longer inherits a driver's call device or pipes (both are close-on-exec).
- The .NET JIT driver sets the GC hard limit as a share of the guest's memory (
DOTNET_GCHeapHardLimitPercent) instead of a fixed 768 MB that never engaged, so an allocation the guest cannot serve is now a catchableOutOfMemoryExceptioninstead of a SIGSEGV. SandboxBuilder::bootreturns anAppSandboxinstead of a(MultiUseSandbox, GuestConfig)tuple;GuestConfigis no longer public and the freerunis nowAppSandbox::run.runfails if the guest exits before the call returns,joinrefuses a driver image with no call in flight, andbootrefuseskernel/initrd/entry/scratch_mbon afrom_snapshotbuilder or a mount path the kernel'svfs.fstabcannot carry (whitespace,:, brackets, or a relative path), instead of ignoring them.- Breaking, guest side: the driver protocol changed (see Removed), so rootfs images built for 0.13.0 must be rebuilt with
just build-rootfs. - Kernel: guest sockets are more robust -- a
recv/sendon a connection whose peer died or whose host socket is gone (after a restore, or a reset while parked) returns instead of hanging, aselect/acceptloop no longer livelocks, and a server polling more than 64 sockets no longer stops waking. - Kernel: transfer buffers are sized from the host's I/O stacks instead of hard-coded literals, so a socket send carries the full 64 KiB, a directory listing or symlink target is no longer cut at 8 KiB / 1 KiB, and an environment over 4 KiB no longer vanishes.
- Kernel: the periodic CSPRNG reseed timer is off, so an idle guest no longer wakes the host every 300 s, and a guest with nothing to wake it is reported deadlocked instead of waited on forever. The CSPRNG is still seeded at boot and on every restore.
AppSandbox::set_env_varsno longer rejects keys starting withHL_; the kernel reserves no keys now.AllowList::from_hostsandBlockList::from_hostsfail with aResolveError(naming the entry and the resolver error) instead of aString; several host-side policy internals are no longer public.
Removed
- The
net_resolvehost function (unused; it ran a blocking, unfiltered resolver lookup on the vCPU thread). - The
host_nanosleephost function (unused; it stalled the embedder's thread up to 30 s).net_pollnow refuses a non-zero timeout: waiting is the host's job between entries. - The callback-and-halt driver protocol: drivers no longer write a callback pointer and halt the VM themselves, and the
HL_*dispatch and env addresses are gone from the guest environment.HLCALL_IOC_GETENVreplaces the raw env-refresh function pointer, andhl_driver_initno longer takesenvp. SNAPSHOT_TAGand theOciTagre-export: embedders no longer name snapshots themselves.
Fixed
- Interactive programs no longer echo every character twice: the serial terminal now honors the
ECHOflag and stores thetermiosa program sets, so a shell (hluk run --entry /bin/sh) can turn echo off, while a program that leavesECHOon still has its input echoed once. - The Python drivers set
PATH=/usr/local/bin:/usr/bin:/binat startup, sosubprocess.run(["python3", ...])and other bare-name lookups find the interpreter (a host--env PATHstill overrides it). - AWS's IPv6 instance-metadata address (
fd00:ec2::254) is refused under every network policy, like the link-local metadata addresses already were. - Snapshotting a guest whose process had already exited produced an unresumable image;
AppSandbox::snapshot(andsnapshot_to,hluk snapshot save) now refuses withError::GuestExited. - Listing a mounted directory too large for one host call (about 3,000 entries) poisoned the sandbox; the host now refuses that one listing with
EOVERFLOWand the guest goes on. Other listing errors now reach the guest with the right errno instead ofEIO. - The driver FunctionCall reader (
hl_fc.h) bounds-checks every offset, so a malformed call fails instead of reading out of bounds. - Kernel: signal-handling fixes (
sigaltstack) for a runtime that manages its own alternate signal stacks, so the .NET workers no longer crash the kernel when the guest runs out of memory. - The exec driver (C, C++, Rust, Go, .NET AOT) and the PowerShell driver report the program's exit status: a non-zero exit, a signal, a C++
std::terminateorexit 3now fails the call. Before, they took the closed exit pipe for success whatever the program did. - Kernel: a program that returns from
main()while another of its threads sleeps no longer hangs the guest. - The Node driver reads the next call asynchronously, so its event loop keeps turning: an
unref()ed timer or handle left behind makes progress between calls, and the child no longer exits (and deadlocks the next call) after leaving only unref'd work. - Kernel: a guest kernel crash now ends the call with a
GuestAbortederror and the crash dump in the output, instead of the vCPU running on and the guest spinning at 100% CPU. - An exit inside a call ends that call with its status and leaves the runtime ready for the next, in every runtime: Python catches
SystemExit; Node turnsprocess.exit(),process.exitCodeand uncaught errors into the status and stops the timers, servers and sockets the call left; the .NET JIT and bash drivers take a child's exit status and respawn for the next call.sys.exit(0)/process.exit(0)succeed; a non-zero code fails the call. Before, an exit could take down the whole guest or deadlock the next call. - The Python dr...
v0.13.0
0.13.0 is a ground-up rewrite of the project since 0.12.1: a single hluk CLI plus a library, a typed guest-to-host boundary, one reproducible embedded Unikraft kernel, and a formal runtime support-tier policy. It is not CLI- or API-compatible with 0.12.x; see Removed and the behavioral notes under Changed.
Added
- Single
hlukCLI with subcommandsrun,snapshot save/snapshot run, andbench(cold/cold-snap/warm-restore/warm-stateful/parallel). Flags:--mount HOST:GUEST[:ro],--net/--net-allow/--net-block/--port,--exec,--guest-exec(urunc-style, runs a binary baked into the initrd),--entry,--env KEY=VALUE, and--scratch-mb. - Library API: a
SandboxBuilder—from_initrd(rootfs)/from_kernel/from_snapshot, then.boot()to get a running sandbox — plus therundispatch call,Mount,Exec,GuestConfig, and re-exportedSnapshot/OciTag.hlukis built on the same API. - 11 guest runtimes on a 3-tier support policy (
docs/guest-support-tiers.md), a shared C driver toolkit, and a CPython conformance suite..NETruns both ahead-of-time (AOT) and in-guest JIT (Roslyn source compilation). - Typed
fs_*/net_*host functions with new filesystem operations (rename,symlink,readlink, hardlink,chmod), multi-mount support,net_resolve(host DNS), real guest stdin (ReadStdin+ EOF), and host environment-variable passthrough (--env, re-applied across snapshot restore). --kernel <path>(advanced) to boot an external kernel instead of the embedded one, and a reproducible native-kernel test fixture (just build-native-kernel/just verify-native-kernel).- Snapshot save/restore as OCI-layout directories; macOS (
hvf) support; parallel multi-VM benchmarking; on-demand Windows surrogates.
Changed
- Guest-to-host boundary: a single JSON-RPC
__dispatchfunction plus a host-sideToolRegistrybecame many small, typed FlatBuffer host functions returning real-errno(Linux numbers, with a Win32/Winsock-to-Linux translation table). - Boot metadata: pushed as magic-tagged TLVs prepended to the initrd became pulled via host functions at boot (
GetCmdLine,GetInitrdBase/Size,GetWallClockNs,GetEnvVars, …), re-answerable after a snapshot restore. - Filesystem sandbox: a hand-rolled path resolver became
cap-stdwith kernel-enforcedopenat2(RESOLVE_BENEATH)on Linux (component-by-component resolution on Windows). Networking: blockingsocket2became non-blockingrustixwith anet_pollreadiness model. - Guest memory: a no-paging design with a bespoke
lib/cpiovfsandlib/ukmmapbecame the standard Unikraft paging/vmem/mmap stack with a ramfs initrd. - Kernel provisioning: per-example
kraftbuilds became one reproducible, Docker-built Unikraft kernel embedded in the binary viainclude_bytes!and verified in CI (kraftkit dropped). - VMM:
hyperlight-host0.16 to 0.17. On Linux the default build is KVM-only (for the fastMADV_DONTNEEDsnapshot-restore path); MSHV support is behind--features mshv. - Behavioral (breaking): in 0.12.x
--portimplied--net; now--portrequires an explicit--net(or--net-allow/--net-block) and errors on its own. Under--net, the defaultAllowAllpolicy permits the host loopback interface (needed for intra-guest server+client patterns);--net-allow/--net-blockstill block loopback. - PowerShell runtime moved from a 7.6.x tarball to the 7.4 (LTS) Alpine image.
- The Unikraft kernel is now embedded by default (0.12.x took a positional kernel path); use
--kernelto override it.
Removed
- Wasm custom host tools (
--tool,--tool-wasi-*), the publicToolRegistry/SandboxBuilder::tool()extension API, and the WASIp1 host-function sandbox. - The
pyhlPython tool (image pull from GHCR, warm-then-snapshot,--deterministic) and themultifn-test/pydriver-rundev binaries; the snapshot workflow folded intohluk snapshot. - The
--memory/--stackknobs (replaced by--scratch-mb), the reserved-mountpoint rejection list (/,/bin,/dev,/proc,/sys,/usr), and the implicit default/hostmount path (mounts now require an explicit guest path). - Kernel-internal: the bespoke
lib/cpiovfs, the/dev/hcalluserspace device, the per-syscall TSC profiler, and the trace ports. File-backedmmapand demand paging move from the customlib/ukmmap/cow.cto the standardukvmemfault handler. (mmap'smremapis currentlyENOSYS; glibc tolerates it.) - The
app-elfloaderandkraftkitforks; the platform builds against upstreamapp-elfloaderand a singleunikraftkernel fork. - Docs: the old
host_functions.md(dispatch wire format / attack surface) andpython-packages.md.
Fixed
- Host-mount path-escape resolution is now OS-enforced (
openat2RESOLVE_BENEATH/RESOLVE_NO_MAGICLINKSon Linux; component-by-component on Windows), with defense-in-depth:roenforcement on both the VFS and host sides. - Guest output is captured via a
HostPrinthost function (one VM exit per buffer instead of per byte) and exposed programmatically throughGuestConfig::drain_output.
Pull requests
- @jsturtevant made their first contribution in #110
- @cmainas made their first contribution in #113
Full Changelog: v0.12.1...v0.13.0
v0.12.1
v0.12.0
What's Changed
- ci: trigger cargo publish from release workflow by @danbugs in #103
- feat: subprocess support, networking idle-poll fix, and CI improvements by @danbugs in #108
Full Changelog: v0.11.0...v0.12.0