Skip to content

v0.19.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 01:28
· 97 commits to main since this release
02fd279

Added

  • verl-hyperlight (integrations/verl/): runs the test cases of verl's code rewards in micro-VMs on the training node, through hluk run, with reward.sandbox_fusion.url=hyperlight:// (or hyperlight://<image>, or hyperlight:///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, as python solution.py would 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 the hyperlight sandbox runner of a verl with sandbox runners, and routes hyperlight:// URLs to it in an earlier one. Linux only, with hluk v0.19.0 or later. Published to PyPI on its own version by pypi-publish.yml; CI tests it on Linux with each commit's hluk and python image, at its floor of filelock and the newest.
  • smolagents-hyperlight (integrations/smolagents/): a smolagents code executor that runs a CodeAgent's code in a micro-VM through hluk run, published to PyPI on its own version by pypi-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 the smolagents.executors entry points, so a smolagents that loads executors from them (huggingface/smolagents#2724, not in a release yet) takes executor_type="hyperlight". CI tests it with each commit's hluk and agent image, 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, whose run_python tool runs Python in a micro-VM that keeps its variables between calls, and whose reset_sandbox tool replaces it with a fresh one from the warm snapshot. It runs its guest through smolagents-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, by pypi-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 as io.github.hyperlight-dev/mcp-server-hyperlight, and gets a GitHub release with the Claude Desktop Extension that integrations/mcp/mcpb/ packs; the server finds hluk in the install scripts' directory too, for a client that starts it without that directory on PATH; --timeout takes decimals, and --net-allow a 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 that CREATE FUNCTION ... ENGINE = HyperlightPython AS '<body>' defines a function in Python, which ClickHouse runs in micro-VMs. CREATE FUNCTION boots 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. The executable_pool UDF 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, and Nullable and Array of 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 (on main, and on pull requests labelled ci/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 in benchmarks/limits.json, and on Linux the ClickHouse UDF tests and a guest booted by the static hluk. 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), which install.ps1 installs on an arm64 host.
  • Mount::backend: a guest mount served by the embedder's FsBackend instead 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 its io::Errors reach the guest as errno (a panic as EIO), 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's NetworkFilter decides what the guest may reach, asked for each DNS question by name and for each connect and sendto by 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 under AllowList (docs/net.md).
  • A guest function call (a script, --exec code, a --call input) 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, with Error::CallTooLarge, where it failed in the transport (H2G capacity cannot provide N buffers); GUEST_CALL_MAX and CALL_PAYLOAD_MAX give the limits.
  • os.utime and the like set a mounted file's times on the host (fs_set_times), from 1970 on (an earlier one fails with EINVAL); they did nothing. An FsBackend gets them through set_times, ignored by default, and gives a file's times with FileStat::times.

Changed

  • NetworkPolicy is #[non_exhaustive], and has a new variant, Filter: a match on it needs a wildcard arm.
  • Mount has a private field, the backend: make one with Mount::rw, Mount::ro or Mount::backend rather than a struct literal, and match it with .. when destructuring. A backend mount's host_path is 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-rootfs made 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: isatty on 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 once AppSandbox::console_terminal(true) (or SandboxBuilder::console_terminal) allows it, which hluk run does for the workload. Off a terminal, terminal requests fail with ENOTTY and 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 built FROM an 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 (555 without write permission): access(X_OK) now fails on one without an execute bit, so the exec driver refuses it (Permission denied, status 126) and shutil.which and the like skip it, though execve (and so a shell's PATH search) still runs it. A guest's chmod never 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 (GuestLogData messages), which the host logs through tracing as target hyperlight_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, from RUST_LOG, else errors, and a restored one its snapshot's). They were on the guest's standard error, where a failed execve (a shell's PATH search, subprocess.run of a missing program) added four ERR: 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 prints hl_driver: dispatch callback failed after a call that exited with a status of its own, only when the driver itself fails.
  • The bash-repl template 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>.part directory, 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/passwd or /etc/group, so getpass.getuser() raised KeyError, and pwd lookups, and ~ with no HOME, 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 /tmp to /dev). It fails with EPERM, ENOENT and EXDEV, 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 run now 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 --net with no --port, a manifest with [run.net] and no ports, or SandboxBuilder::network without listen_ports let the guest serve, where --port's help and net.md promise a guest that cannot. bind() is refused with EACCES now 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 under network alone adds listen_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 every chmod) on a mount failed with EIO: 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.b read as the two labels a and b, and a forged answer could spell out a name kilobytes long through a compression loop.
  • A sendto on 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. A sendto on a stream socket is a send to 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.0 or :: goes to loopback on Linux, and only 127.0.0.0/8 and ::1 were 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.stdout or sys.stderr (or the original sys.__stdout__ and sys.__stderr__) reaches the host when the call ends: text with no newline (print(x, end=''), json.dump(obj, sys.stdout)), bytes written through sys.stdout.buffer, and what was written before an exception, now ahead of its traceback, or a SystemExit. 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's test_decimal, which expects MemoryError from such a request, crashed the guest. The mmap() fails with ENOMEM now, as on x86.
  • stat on a mounted file read the epoch for every time.
  • mkdir and rmdir of a path with trailing slashes (mkdir("a/b/"), as BusyBox's mkdir -p passes it) failed with ENOENT; 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; umount2 returned a positive error number.
  • Input piped to a guest was echoed into its output, and a chunk of it ending in \r was 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