Repository navigation
Added
- The
javaruntime (tier 2): Java 25 on a jlinked runtime, with Java source run by JShell in the guest's JVM, so statements, expressions and declarations mix and stay for the next call, and a call that only declares amain(a compact source file, or a class) runs it. It serves guest function calls (a static method, its input converted to a record, collection or simple value, a future awaited) and makes host function calls throughhyperlight.Host.call;System.getenv()sees the host's variables, andSystem.exit()ends the call with its status; the next call starts a fresh JVM while the guest has the memory for one (two restarts at 512 MiB: the kernel keeps an exited process's memory), and fails without crashing when it doesn't. The image is 84 MiB and runs in 512 MiB; a CDS archive of the classes the dispatcher loads while it warms up comes with it.hluk init --template javastarts a project. - A
devchannel: each push tomainwhose CI passes replaces thedevpre-release (thehlukbinaries and aSHA256SUMS) and publishes the images of that commit (:dev-<sha7>,:initrd-dev-<sha7>), somaincan be installed and run between releases:HLUK_VERSION=devwithinstall.shorinstall.ps1. A devhlukpulls the images of its own commit and prints its build with--version(0.17.0+dev.44ee170); only the last two builds' images are kept. Releases and their tags are unchanged (dev.yml, docs/RELEASE.md). - A project site, https://hyperlight-dev.github.io/hyperlight-unikraft: startup times from CI, a density measurement (1,000 Python sandboxes in one process), every template, and terminal recordings of
hluk init,hluk buildand a guest function call. Its source issite/, andsite.ymlpublishes it to thegh-pagesbranch on pushes tomain, beside the CI benchmark history.just site-servepreviews it,just site-recordre-records the demos andsite/tools/density.shrepeats the density measurement. AppSandbox::drain_stderr, the guest's standard error apart from its standard output, and the host function behind it,HostWriteErr(see Changed).SandboxBuilder::output_limit(bytes): a bound, enforced on the host, on the guest output a sandbox keeps between drains, for its output (drain_output, both streams) and its standard error (drain_stderr) each. The host keeps the beginning and counts the rest, whichAppSandbox::take_outputandtake_stderrreport with the text (CapturedOutput); the guest carries on. Unlimited by default.SandboxBuilder::echo_output(false)stops the copy to the host process's stdout and stderr, so the capture is all there is.
Changed
- A guest's standard error is a stream of its own, through a new host function,
HostWriteErr:hluk runwrites it to its own stderr instead of its stdout, andAppSandbox::drain_stderrreturns it apart from standard output.drain_outputstill returns both, in the order written, and drops whatdrain_stderrhas not taken, so take standard error first. The kernel's own messages go to standard error too, out of the program's standard output, and so does the report ofhluk run --profile, which shares stderr with them. A host of your own that does not registerHostWriteErrgets standard error throughHostWrite. - Guest output keeps its newlines:
drain_output,drain_stderrandhluk runno longer have a\rbefore each\n. - The images the builds run are pinned by digest, as the CI actions are by commit: the base images of the drivers, the kernel builder, the examples, demos and
hluk inittemplates, the imagesjust build-test-binsruns, the urunc image's BuildKit frontend and the BuildKit image CI builds the kernel with (#126). - Dependabot opens grouped version updates once a month (see
.github/dependabot.yml) for the crates, the CI actions and the Dockerfiles' base images, digest refreshes and patch releases only for those (#135). Dependabot labels its version updatesci/arm64. On a pull request with that label,ci-passed(the required check) waits for the arm64 run and fails unless it passes, so merges, auto-merge included, wait for the arm64 tests. A label added afterci-passedstarts counts once that job is re-run. - The Linux release binaries are static, built with musl (
hluk-v<version>-x86_64-unknown-linux-musl.tar.gzandaarch64-unknown-linux-musl), so they run on any distribution. The glibc builds of earlier releases need the glibc of the runner that built them, 2.39, which Debian 12 and Ubuntu 22.04 do not have (#141).install.shinstalls the static one and, for a release before this, the glibc one. - Hyperlight's virtqueue transport (hyperlight v0.18): the kernel and the host talk over two VIRTIO packed rings in scratch memory instead of the PEB's I/O stacks. Host function calls are a request and its reply in one G2H chain; guest function calls arrive in buffers the kernel posts on H2G, and their results go back on G2H. Byte vectors travel after the FlatBuffer rather than inside it. The pools are sized for 64 KiB host-call payloads, rounded up to whole pages: one host call now carries 69,620 bytes, and a guest function call 73,716. hostfs reads and writes move 64 KiB per host call instead of 32 KiB: against v0.17, reading a 64 MiB file from Python is about 55% faster and writing one about 50% (KVM).
hyperlight-hostandhyperlight-commonmove to 0.18. - Snapshots saved by an earlier release or build are refused (the kernel, the host functions and the host transport changed); save them again. A native Unikraft kernel (
--kernel,SandboxBuilder::from_kernel) built against an earlierplat/hyperlightno longer boots: rebuild it against this kernel tree. - Rust 1.99 builds the project (
rust-toolchain.toml). The arm64wasmtimeimage is a static PIE now, as on x86_64: the driver and nothing else, without the glibc it carried (#129). --port(a manifest's[run.net] ports) no longer needs--net: on its own the guest serves TCP on the port and can open no connection of its own, as under an allow list that lists nothing (a UDP service's replies are outbound sends, so it still needs a policy); with a project, it replaces only the project's ports, not what the project may reach. In the library,SandboxBuilder::listen_portswithoutnetworkdoes the same, where it used to leave the guest with no networking at all.
Fixed
hluk run,hluk snapshotandhluk benchkept everything a guest printed in memory until they exited, though they never read it: a long-running guest that kept printing grew the host process without end. They keep none of it now; the output still reaches the terminal.- A path below a host mount's root longer than 1,023 bytes was cut short, and the syscall made on what was left:
mkdirreported success and created a directory with a truncated name. It now fails withENAMETOOLONG. - A path on a host mount that was not UTF-8 (
os.stat(b'/mnt/data/\xff')) failed the guest function call that used it, since the host takes paths as strings: the kernel now refuses it, and the syscall fails withEILSEQ. - Console output written in one piece longer than 4 KiB (a long line, say) reached the host cut to its first 4 KiB, with nothing to say so; and output that was not UTF-8, or a write that ended inside a character (
catof a large UTF-8 file can do it), failed the guest function call that printed it. The kernel now sends console output as bytes, in as many host calls as it takes, through newHostWriteandHostWriteErrhost functions, and the host decodes them, a character split between writes included, with invalid bytes as U+FFFD, for stdout anddrain_output()alike. Stdin, the other way, had the same flaw: a character cut at a 4 KiB read, or bytes that were not UTF-8, reached the guest as U+FFFD.ReadStdinnow hands over bytes too (the guest's terminal layer still treats them as a terminal does: it echoes them, and reads Ctrl-D as end of input). hluk init NAME --template gowrote ago.modthat Go refused when the name wasgo,toolchainor a standard-library package such asfmt: the module path is nowexample.com/NAME.- A condition variable broadcast, or several signals, woke one waiter and left the others asleep in programs linked with musl, which most images use (node, dotnet-jit, java, quickjs, bash, and programs built on Alpine): the kernel refused
FUTEX_REQUEUE, which musl's condition variables use to hand each woken waiter's successor to the mutex. It showed as rare, timing-dependent hangs, such as Java'sHttpClientstalling on its first request on Windows hosts.FUTEX_CMP_REQUEUEalso woke a waiter when asked to wake none, looped when requeuing onto the same futex, and crashed the kernel when a requeued waiter timed out. All four are generic Unikraft fixes, each reproduced and verified on QEMU/KVM. - Standard error reached the host as standard output: the kernel opened fd 2 as a copy of fd 1. It is a stream of its own now (see Changed).
- Every newline a guest printed reached the host as CR-LF (
\r\n), from a workaround for serial consoles that need a carriage return. The host gets the bytes the program wrote (see Changed). - A command that failed to execute (a mistyped command in the bash image,
subprocess.run(['/missing'])in python-shell) crashed the guest the next time the parent waited: the vfork child freed its parent's stack on its way out, as if it were its own. A subprocess started from a thread other than the main one failed withENOMEM, for want of an allocator for the new program's stack. Both are generic Unikraft fixes, verified on QEMU/KVM. - Kernel messages carried NUL bytes, and one longer than 191 bytes carried bytes past its buffer; it is cut there now, at a whole character, and keeps its line end. nolibc's
printfpast 1 KiB also read past its buffer; it is cut at 1 KiB. - A pipe, a
$(...)substitution or a redirect of an external command crashed the guest in the bash image on x86_64, and so did one in Python'ssubprocess.run(..., shell=True)in the python-shell image: a vfork child returned to userland on the kernel's TLS rather than its parent's, and musl, reading its thread pointer in the system calls the child made beforeexecve(open,dup2,close), read kernel memory as its own; so did Python'ssubprocessonce any thread had run, when libc starts taking its locks. The child now runs on its parent's TLS untilexecve, as it runs on its parent's stack; arm64 had the same code and gets the same fix. - The
agentimage got 256 MiB when booted by path (--initrd, or a project builtFROMit) rather than the 1536 MiB it is tested with, too little to unpack it into, and the guest crashed during boot. hluk sizes a guest by the runtime marker an image carries,etc/hluk-runtime, which no image wrote, and then by its driver, whichagentshares withpython-shell. Theagentimage now writes the marker, and images built from it inherit it. - A warm snapshot kept the environment of the run that saved it:
hluk run --runtime agent --env PYTHONPATH=/xleft/xonsys.pathfor every later run of that snapshot. The guest saved as the warm snapshot now boots with the project's[run] envonly, which is part of the snapshot's key, and a run's--envreaches its runtime driver after the save, as it does a restored one. An image with no driver, whose program reads its environment once as it starts, or a run with awarm_exec, which runs before the save, boots with all of it, and its snapshot is keyed by it (the newest four are kept per image). A variable a runtime reads only as it starts (NODE_OPTIONS, say) therefore belongs in the project's[run] env, or needs--cold. PYTHONPATHgiven to a restored Python guest (hluk snapshot run --env PYTHONPATH=...,set_env_vars) was inos.environbut not onsys.path, which Python reads once at startup. Thepython,python-shellandagentdrivers now put its entries where startup would have, in place of the previous call's, before each call, and a variable the host stops sending goes back to the guest's own value, or away if it had none.- Waiting for and stopping processes:
wait()for a child that was still running slept for good (the guest deadlocked) unless it waited for any child, and with two threads waiting for two children one could sleep on; a parent paused insigsuspend()for a child's SIGCHLD slept for good, so a shell'ssleep 1 & waithung the guest, a laterpause()orsigsuspend()could return at once, for a signal already handled, andsigsuspend()put the caller's mask back before the signal it waited for was delivered, so its handler never ran andwhile (!got) sigsuspend(&old)never ended; a killed process lived on until its threads next left a system call, so a timeout's kill waited out asleepand never ended acatreading a pipe (a killed process's blocked calls are now interrupted and unwind, releasing the files they hold), and a process ended while one of its threads lent its stack to avforkchild (a thread runningsubprocesswhile another calledos._exit) freed that stack under the child, and a thread blocked invfork()resumed on that stack while its child still ran on it whenever another thread or child it had made exited or exec'd (the guest crashed); an exec'd program kept the signal handlers its parent had set, so a signal it did not catch ran its parent's code; and there was one process group and one session, sosetsid()(Python'sstart_new_session) failed, andkillpg()andkill(0, ...)signalled every process in the guest, the caller's supervisor and init included. Each process now has a group and a session as on Linux,killandwait4/waitidtake groups, a signal that ends a process ends it when it is sent,execveresets caught signals, init is spared the signals it does not handle, and waiting on a group a child leaves gives ECHILD rather than a hang. pause()returned at once on arm64 hosts, where libcs make it asppoll(NULL, 0, NULL, NULL), which the kernel refused: Python'ssignal.pause()never slept, and a program paused for a child's signal ran on. Apoll(),ppoll(),select()orpselect6()with no fd to watch and no timeout now waits for a signal, as on Linux (select(0, NULL, NULL, NULL, NULL)hung on both architectures), andpoll(NULL, 0, ms)sleeps instead of failing. A generic Unikraft fix, verified on QEMU/KVM.execveof a file that does not exist failed with EINVAL, not ENOENT, so Python raisedOSErrorinstead ofFileNotFoundErrorfor a missing program;kill()of a pid past the process table (os.kill(99999, 0)) crashed the kernel, and so didpthread_kill(thread, 0), the check whether a thread exists; a signal sent to one thread was also marked pending on the process, so the same signal sent to the process later was dropped; a standard signal sent while one was pending leaked memory; a process with 32 signals queued could not be killed (SIGKILL and SIGTERM were refused with the rest), and crashed the kernel when one of its children exited, since the exit signal could not be queued; a signal to a process group stopped at the first member that refused it; and a SIGCHLD left at its default stayed queued, so a handler installed later ran for children long reaped, while one installed withSA_NOCLDWAITnever ran.- A program that ran the guest out of memory crashed the kernel with a fault; it is now killed as Linux's OOM killer would (
Out of memory: killed process N), and the guest exits with 137 when it was the last. A child process out of memory with others still running is killed too, but the guest still crashes freeing its memory. select()andpoll()called a socket readable that was not: a new listener, a listener whose last connection had been accepted, and a connection whose data had been read. Theaccept()orrecv()that followed blocked, and asocketserverloop never got back to itsshutdown().
Pull requests
- drivers: add the java runtime by @danbugs in #139
- Static Linux release binaries, built with musl by @danbugs in #144
- Keep guest output whole, stderr apart and newlines as written; fix vfork crashes by @danbugs in #145
- Bound the guest output the host keeps by @danbugs in #146
- Move to Hyperlight 0.18's virtqueue transport by @danbugs in #142
- build(deps): bump crate-ci/typos from 1.50.1 to 1.51.1 by @dependabot[bot] in #150
- build(deps): bump the wasmtime group across 1 directory with 3 updates by @dependabot[bot] in #147
- build(deps): bump the cargo group across 2 directories with 5 updates by @dependabot[bot] in #151
- build(deps): bump the actions group across 1 directory with 3 updates by @dependabot[bot] in #148
- Pin the images the builds run by digest by @danbugs in #152
- build(deps): bump dtolnay/rust-toolchain from 02cb101ec7c40f2c49e1d9714d64511d8e1b74de to e2a55d2ffb04f378e9626c28d38b36d230d1e12f by @dependabot[bot] in #149
- Make running, waiting for and killing processes work in the guest by @danbugs in #153
- kernel: fix pause() on arm64, and polls with nothing to watch by @danbugs in #156
- tests: retry the HTTP GET examples when the server fails by @danbugs in #159
- ci: make ci-passed wait for arm64 on pull requests labelled ci/arm64 by @danbugs in #154
- build(deps): bump rustls from 0.23.43 to 0.23.46 in /demos/pptx-gen in the cargo group across 1 directory by @dependabot[bot] in #160
- build(deps): bump wasmparser from 0.259.0 to 0.261.0 in /drivers/wasmtime/driver in the wasmtime group across 1 directory by @dependabot[bot] in #161
- v0.18.0: java, Hyperlight 0.18's virtqueue transport, separate stderr, static Linux binaries by @danbugs in #155
Full Changelog: v0.17.0...v0.18.0