You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
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::boot returns an AppSandbox; run dispatches a call and waits, submit dispatches without waiting, step advances the guest one boundary, join drives an entry-point workload to exit, and snapshot / from_snapshot (or restore in place) checkpoint and resume it, sockets included. Yield says why a step returned.
A plain Linux binary can be the guest's entry point (--entry "/bin/server --flag") with no runtime driver; hluk run drives it until it exits, and the library exposes it as AppSandbox::join. This is how a container runtime or an actor host runs a server in the guest.
asyncio now 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 by join; hluk run exits 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's HLCALL_IOC_MAXLEN ioctl reports the largest call the host can send, and HLCALL_IOC_GETENV hands a driver the host's current environment to refresh before each call.
AppSandbox::snapshot_to(dir), SandboxBuilder::from_snapshot_dir(dir) and AppSandbox::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/hlcall driver contract and how to write a driver), docs/clock.md and docs/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 run and hluk snapshot run exit 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, and snapshot run drives a restored entry-point guest to its exit like run.
The crate has its own error type, hyperlight_unikraft::Error, and every public function returns hyperlight_unikraft::Result, with a variant per condition (CallFailed, GuestExited, Deadlocked, NoDriver, CallInFlight, and so on) plus Hyperlight for 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 a Result.
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 catchable OutOfMemoryException instead of a SIGSEGV.
SandboxBuilder::boot returns an AppSandbox instead of a (MultiUseSandbox, GuestConfig) tuple; GuestConfig is no longer public and the free run is now AppSandbox::run. run fails if the guest exits before the call returns, join refuses a driver image with no call in flight, and boot refuses kernel/initrd/entry/scratch_mb on a from_snapshot builder or a mount path the kernel's vfs.fstab cannot 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/send on a connection whose peer died or whose host socket is gone (after a restore, or a reset while parked) returns instead of hanging, a select/accept loop 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_vars no longer rejects keys starting with HL_; the kernel reserves no keys now.
AllowList::from_hosts and BlockList::from_hosts fail with a ResolveError (naming the entry and the resolver error) instead of a String; several host-side policy internals are no longer public.
Removed
The net_resolve host function (unused; it ran a blocking, unfiltered resolver lookup on the vCPU thread).
The host_nanosleep host function (unused; it stalled the embedder's thread up to 30 s). net_poll now 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_GETENV replaces the raw env-refresh function pointer, and hl_driver_init no longer takes envp.
SNAPSHOT_TAG and the OciTag re-export: embedders no longer name snapshots themselves.
Fixed
Interactive programs no longer echo every character twice: the serial terminal now honors the ECHO flag and stores the termios a program sets, so a shell (hluk run --entry /bin/sh) can turn echo off, while a program that leaves ECHO on still has its input echoed once.
The Python drivers set PATH=/usr/local/bin:/usr/bin:/bin at startup, so subprocess.run(["python3", ...]) and other bare-name lookups find the interpreter (a host --env PATH still 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 (and snapshot_to, hluk snapshot save) now refuses with Error::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 EOVERFLOW and the guest goes on. Other listing errors now reach the guest with the right errno instead of EIO.
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::terminate or exit 3 now 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 GuestAborted error 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 turns process.exit(), process.exitCode and 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 drivers release the interpreter lock while parked between calls, so a thread or server a call left behind makes progress between calls instead of starving and waking the guest every 5 ms.
A host environment value with a quote, backslash, newline or shell syntax no longer breaks or alters the call: the Python drivers set os.environ through the C API, and the bash, Node and .NET drivers quote values as literals of their language. The 4 KiB cap that silently dropped variables is gone.
A connect() to a peer that never answers no longer freezes the vCPU for the OS's roughly two-minute SYN timeout: the host returns EINPROGRESS, the guest parks on writability, and a guest-side timeout (socket.settimeout) is honored.
The network policy read an IPv4-mapped IPv6 address (::ffff:169.254.169.254) as an unrelated address, so a dual-stack socket could bypass the loopback, link-local and BlockList rules. Addresses are now checked in their IPv4 form.
Data arriving on a socket no guest thread was reading made the host spin a full core per idle guest; the host now watches a socket for readability only while the guest is parked on it.
The guest clock no longer runs 12% fast: the kernel asks the host for the TSC frequency (GetTscHz) instead of assuming 2.5 GHz.
A guest restored from a snapshot reports the current time: the kernel re-anchors its wall clock on the host's during restore.
SandboxBuilder::env variables now reach a program that is the guest's entry point, not only driver-served calls.
Kernel: clock_nanosleep is implemented for the monotonic and realtime clocks (relative and TIMER_ABSTIME), so CPython's time.sleep() no longer returns immediately.
A dispatched call is no longer capped at 16 KiB ("FunctionCall too large"); the call buffer is sized from the host's I/O buffer.