Repository navigation
v0.19.0
Added
verl-hyperlight(integrations/verl/): runs the test cases of verl's code rewards in micro-VMs on the training node, throughhluk run, withreward.sandbox_fusion.url=hyperlight://(orhyperlight://<image>, orhyperlight:///path/to/rootfs.cpio), instead of in the grader's own process or on a Sandbox Fusion service. Each case runs from a warm snapshot, with no network or view of the host, aspython solution.pywould under Sandbox Fusion, and comes back as Sandbox Fusion's reply, so verl scores it as the service's: on 200 APPS problems its scores match Sandbox Fusion's on 199 (on the agent image all 200), at about its speed. It registers as one of verl's plugins (verl.plugins), which registers it as thehyperlightsandbox runner of a verl with sandbox runners, and routeshyperlight://URLs to it in an earlier one. Linux only, withhlukv0.19.0 or later. Published to PyPI on its own version bypypi-publish.yml; CI tests it on Linux with each commit'shlukand python image, at its floor offilelockand the newest.smolagents-hyperlight(integrations/smolagents/): a smolagents code executor that runs aCodeAgent's code in a micro-VM throughhluk run, published to PyPI on its own version bypypi-publish.yml. The guest reads each step from its console and answers on it, so it needs no network, and has none unless hosts are allowed (net_allow); between steps its micro-VM is paused. It registers under thesmolagents.executorsentry points, so a smolagents that loads executors from them (huggingface/smolagents#2724, not in a release yet) takesexecutor_type="hyperlight". CI tests it with each commit'shlukandagentimage, in micro-VMs on Linux and Windows, against the oldest smolagents it allows on the oldest Python, and the newest.mcp-server-hyperlight(integrations/mcp/): an MCP server, on stdio, whoserun_pythontool runs Python in a micro-VM that keeps its variables between calls, and whosereset_sandboxtool replaces it with a fresh one from the warm snapshot. It runs its guest throughsmolagents-hyperlight's executor, so the two share the guest protocol and its bounds; the guest starts as the client starts the server, and has no network unless hosts are allowed (--net-allow). Published to PyPI on its own version, bypypi-publish.yml, which now takes the package to publish. CI tests it beside the smolagents executor, in micro-VMs on Linux and Windows: on the oldest Python with mcp, smolagents-hyperlight and smolagents at the floors it allows, from PyPI, and on the newest with the commit's smolagents-hyperlight. Each release is listed in the MCP Registry asio.github.hyperlight-dev/mcp-server-hyperlight, and gets a GitHub release with the Claude Desktop Extension thatintegrations/mcp/mcpb/packs; the server findshlukin the install scripts' directory too, for a client that starts it without that directory onPATH;--timeouttakes decimals, and--net-allowa list separated by commas, which the extension's settings pass; its README configures Claude Code, Claude Desktop, VS Code, Codex and the OpenAI Agents SDK.clickhouse-hyperlight(integrations/clickhouse/): a driver for ClickHouse's driver-based executable UDFs (ClickHouse 26.6 and later), so thatCREATE FUNCTION ... ENGINE = HyperlightPython AS '<body>'defines a function in Python, which ClickHouse runs in micro-VMs.CREATE FUNCTIONboots a guest from the python image, defines the function in it, and saves the guest's snapshot in the function's directory; a body that does not compile fails it with Python's error. Theexecutable_poolUDF it hands ClickHouse restores that snapshot before each block, in about 1.5 ms, so nothing a block leaves in the guest reaches the next. Blocks come in RowBinary, and cross into the guest as JSON through host function calls, in pieces, so a block is not bound by what one guest function call carries;Bool, integers up to 64 bits, floats,String, andNullableandArrayof them are supported. CI tests it with each commit's kernel and python image, against a pinned ClickHouse release.- CI builds, lints and unit-tests on Windows arm64 (hosted
windows-11-arm), so the aarch64-pc-windows-msvc code paths (WHP on arm64) compile and pass clippy on every PR.arm64.yml(onmain, and on pull requests labelledci/arm64) boots the guests there too, on hyperlight's arm64 runners: Linux, macOS and Windows each run what CI runs on x86, every test suite, the demos, the smolagents, mcp and verl integrations, the CPython conformance suite and the benchmarks, with a history per host (dev/bench/<os>-arm64) and ceilings inbenchmarks/limits.json, and on Linux the ClickHouse UDF tests and a guest booted by the statichluk. The Linux runner, a small VM under nested virtualization, runs the agent, Java, node, .NET JIT and PowerShell suites and the agent benchmark nightly instead of on every change. - Releases and dev builds carry a Windows arm64
hluk(aarch64-pc-windows-msvc), whichinstall.ps1installs on an arm64 host. Mount::backend: a guest mount served by the embedder'sFsBackendinstead of a host directory. hostfs hands it each operation the guest's VFS makes (stat, read, write, mkdir, remove, truncate, list, rename, and links and chmod, which it refuses and ignores by default), and itsio::Errors reach the guest as errno (a panic asEIO), so an embedder can account for every byte written (EDQUOT) or keep files anywhere, while the guest's programs use them as on any mount. A read-only backend mount is asked only to read. The directory mounts are served by the same code, and behave as before (docs/fs.md).NetworkPolicy::Filter: the embedder'sNetworkFilterdecides what the guest may reach, asked for each DNS question by name and for each connect andsendtoby address and port, with the names DNS gave the guest that address. It is asked every time, so rules the embedder changes while the guest runs apply at the next connect, with no re-boot; loopback and the metadata addresses stay refused, and the resolvers an allow list exempts take questions, as underAllowList(docs/net.md).- A guest function call (a script,
--execcode, a--callinput) carries up to 1 MiB, from about 72 KB: the host-to-guest pool is 32 buffers of 32 KiB. A larger one fails before it is sent, withError::CallTooLarge, where it failed in the transport (H2G capacity cannot provide N buffers);GUEST_CALL_MAXandCALL_PAYLOAD_MAXgive the limits. os.utimeand the like set a mounted file's times on the host (fs_set_times), from 1970 on (an earlier one fails withEINVAL); they did nothing. AnFsBackendgets them throughset_times, ignored by default, and gives a file's times withFileStat::times.
Changed
NetworkPolicyis#[non_exhaustive], and has a new variant,Filter: amatchon it needs a wildcard arm.Mounthas a private field, the backend: make one withMount::rw,Mount::roorMount::backendrather than a struct literal, and match it with..when destructuring. A backend mount'shost_pathis empty.- Snapshots saved by v0.18.0 and earlier releases are refused (the kernels changed, the snapshot contract is 7, and on arm64 the initrd's guest address moved); save them again. Rebuild images
just build-rootfsmade on an earlier tree: their drivers predate this release's console and exec changes. - The guest's console is a terminal only where its output reaches one:
isattyon standard input and output is true when the host's stdin and stdout are terminals, on standard error when the host's stderr is, in both cases only while the output is echoed, and in a library embedder only onceAppSandbox::console_terminal(true)(orSandboxBuilder::console_terminal) allows it, whichhluk rundoes for the workload. Off a terminal, terminal requests fail withENOTTYand input is not echoed. It was always a terminal. The Python driver and the C drivers' stdio keep standard output line-buffered either way. An image whose driver comes from an earlier build (a custom image builtFROMan earlier base image) does not, and can lose Python output a call does not flush: rebuild it on this release's base image. - A mounted file's mode is the host's (on Windows, from the read-only flag as before), where every file read
755(555without write permission):access(X_OK)now fails on one without an execute bit, so the exec driver refuses it (Permission denied, status 126) andshutil.whichand the like skip it, thoughexecve(and so a shell's PATH search) still runs it. A guest'schmodnever sets set-user-ID or set-group-ID on a host file. - The kernel's messages stay out of the guest's output: they are Hyperlight's guest log (
GuestLogDatamessages), which the host logs throughtracingas targethyperlight_guest, with the library, source file and line as fields, at the levels it asks for (SandboxBuilder::guest_log_level; unset, a fresh guest takes Hyperlight's default, fromRUST_LOG, else errors, and a restored one its snapshot's). They were on the guest's standard error, where a failedexecve(a shell's PATH search,subprocess.runof a missing program) added fourERR:lines to the program's own.hluk --log-level <level>shows them, and without it the CLI still prints the latest when hluk itself fails or an entry point writes nothing, so one that cannot start says why, but not for a program's own failure. A crash report is still on standard error. The exec driver says why it cannot run a program (hl: cannot execute <path>: <reason>). The driver no longer printshl_driver: dispatch callback failedafter a call that exited with a status of its own, only when the driver itself fails. - The
bash-repltemplate prints its prompt only when its input is a terminal.
Fixed
- Concurrent first runs of one guest (a pool of workers on a fresh cache, say) failed: each saved its warm snapshot into the same
<slot>.partdirectory, removing the others' half-written one (atomic write: failed to create tmp), and removed the slot before moving its own in, under runs restoring from it. Each run now writes its own part and moves it in only when the slot is empty, or held a snapshot that would not load; parts a killed run left behind go after an hour. 16 concurrent first runs all succeed, leaving one snapshot. - The python image had no
/etc/passwdor/etc/group, sogetpass.getuser()raisedKeyError, andpwdlookups, and~with noHOME, failed. It ships the base image's. link()crashed the guest kernel on its error paths (vfscore's dentry refcount assertion): linking a directory, into a missing directory, or across file systems (from/tmpto/dev). It fails withEPERM,ENOENTandEXDEV, as on Linux.- Every hluk upgrade left the warm snapshots of each guest in the cache for good: a snapshot another hluk build saved, which this one refuses, was pruned only once its rootfs was rebuilt.
hluk runnow removes those of the same rootfs when it saves its own. - CI's benchmark jobs, x86 and arm64, failed when another job's load on a shared runner put one run over a ceiling in
benchmarks/limits.json. They now measure such a timing again, and fail only if the second run is over too. - A guest given a network policy and no listen ports could bind any port:
hluk run --netwith no--port, a manifest with[run.net]and noports, orSandboxBuilder::networkwithoutlisten_portslet the guest serve, where--port's help and net.md promise a guest that cannot.bind()is refused withEACCESnow unless the port is listed, whether the guest means to listen on it or to connect from it (source_address); an ephemeral bind, port 0, is always allowed, so the guest can still serve on a port the host picks. A guest that serves on a port of its own needs--port, as the documentation said, and an embedder that served undernetworkalone addslisten_ports; the CPython conformance recipes pass--port all, since those tests bind as they please and the test host is the boundary. os.chmod(and everychmod) on a mount failed withEIO: the host took the mode as unsigned and the kernel sends it signed, so the host call was refused before it ran. It now reaches the host file, permission bits only: a guest cannot set setuid, setgid or sticky.- DNS messages are read only as far as a resolver could be asked: a name longer than 253 characters, a label with a dot or a byte that is not printable ASCII, or an extended label type makes the message no well-formed query (refused) or answer (ignored). The label
a.bread as the two labelsaandb, and a forged answer could spell out a name kilobytes long through a compression loop. - A
sendtoon a connected TCP socket was judged by the address it named, though the data goes to the socket's peer: a guest connected to a resolver on port 53 could send anything there by naming an address the policy allows, past the check of its DNS questions. Asendtoon a stream socket is asendto its peer now, checked as one. - A guest under a block list could reach the host's loopback services through the unspecified address: a connect to
0.0.0.0or::goes to loopback on Linux, and only127.0.0.0/8and::1were refused. The lists and a filter refuse the unspecified address too. - No guest booted on Windows arm64 (#166): hyperlight-host puts WHP's emulated interrupt controller at guest physical addresses just below 4 GiB, and the initrd, mapped at
0xFEF0_0000, ran into them past its first 17 MiB. The arm64 initrd is mapped at 4 GiB now; x86 keeps its address. - arm64 guests under nested Hyper-V (Windows arm64 runners) crashed at random, or read zeros where they had just written: the kernel mapped the pages it allocates at run time (heap,
mmap, file pages) Non-shareable, which the CPU does not keep coherent across processors. They are Inner Shareable now, as the boot mappings were. - An
mmap()hint inside the initrd was honoured, though the files extracted from it share its memory: the new mapping replaced their pages, and on x86 the kernel crashed writing it. The kernel reserves the initrd now, and the hint goes elsewhere; on arm64, whose initrd moved to 4 GiB, that is the first address HotSpot asks for. - Python output a call left in
sys.stdoutorsys.stderr(or the originalsys.__stdout__andsys.__stderr__) reaches the host when the call ends: text with no newline (print(x, end=''),json.dump(obj, sys.stdout)), bytes written throughsys.stdout.buffer, and what was written before an exception, now ahead of its traceback, or aSystemExit. The driver flushed only the C streams under them, so this arrived with a later call's output, or never if the sandbox ended or was restored first, for runs and guest function calls alike, in the python, python-shell and agent images; output left buffered at boot no longer waits in the warm snapshot for the first call. - An arm64 guest was killed when it touched an
mmap()larger than the address space, which the kernel accepted: its check of a virtual address passed every address. CPython'stest_decimal, which expectsMemoryErrorfrom such a request, crashed the guest. Themmap()fails withENOMEMnow, as on x86. staton a mounted file read the epoch for every time.mkdirandrmdirof a path with trailing slashes (mkdir("a/b/"), as BusyBox'smkdir -ppasses it) failed withENOENT; the working directory at a mount's root ended in/; a mount given with a trailing slash (--mount=/h:/g/) was not reached at all;umount2returned a positive error number.- Input piped to a guest was echoed into its output, and a chunk of it ending in
\rwas read as ending in\n.
Pull requests
- Add smolagents-hyperlight, a smolagents executor in a micro-VM by @danbugs in #163
- integrations/mcp: mcp-server-hyperlight, an MCP server that runs Python in a micro-VM by @danbugs in #165
- ci: build, lint and unit-test on Windows arm64 by @danbugs in #164
- drivers: flush Python's stdout and stderr when a call ends by @danbugs in #168
- integrations/clickhouse: untrusted Python UDFs for ClickHouse in micro-VMs by @danbugs in #167
- Windows arm64 guests: shareable arm64 pages, initrd clear of WHP's GIC by @danbugs in #169
- integrations/mcp: MCP Registry listing and Claude Desktop Extension (0.1.1) by @danbugs in #173
- arm64: CI parity with x86 on Linux, macOS and Windows by @danbugs in #174
- dependabot.yml: update the composite actions' pins too by @danbugs in #176
- build(deps): bump the cargo group across 2 directories with 3 updates by @dependabot[bot] in #171
- net_policy: NetworkPolicy::Filter, a destination filter the embedder decides with by @danbugs in #179
- build(deps): bump astral-sh/setup-uv from 10.2.0 to 10.3.0 in /.github/actions/arm64-host in the actions group across 1 directory by @dependabot[bot] in #177
- build(deps): bump the wasmtime group across 1 directory with 3 updates by @dependabot[bot] in #175
- build(deps): bump dtolnay/rust-toolchain from e2a55d2ffb04f378e9626c28d38b36d230d1e12f to 683b588c487a69d01f6c6ddf40f036ff682cc2c8 in /.github/actions/arm64-host by @dependabot[bot] in #178
- hostfs: FsBackend, mounts served by the embedder by @danbugs in #181
- Stop leaking warm snapshots; sweep the self-hosted Macs' disks by @danbugs in #182
- Fix guest file, console and call bugs; kernel messages as Hyperlight's guest log by @danbugs in #180
- CI flakes: re-measure benchmark breaches, MCP and smolagents test races, Dependabot majors by @danbugs in #183
- build(deps): bump wit-bindgen from 0.62.0 to 0.63.0 in /examples/wasmtime/calculator in the wasmtime group across 1 directory by @dependabot[bot] in #185
- hostnet: refuse a bind on a port no one listed by @danbugs in #184
- Fix concurrent first runs, the python image's users, and link() crashing the kernel by @danbugs in #187
- build(deps): bump the images group across 3 directories with 1 update by @dependabot[bot] in #186
- integrations/verl: verl-hyperlight, run verl's code-reward test cases in micro-VMs by @danbugs in #188
- Release v0.19.0 by @danbugs in #190
Full Changelog: v0.18.0...v0.19.0