Skip to content

v0.19.1

Choose a tag to compare

@github-actions github-actions released this 24 Oct 00:36
· 62 commits to main since this release
5f14d1e

Added

  • A PHP RCE containment demo (demos/php2shell): a payload that runs as if a PHP engine flaw had handed an attacker code execution reaches host secrets, a C2 and a persistent webshell on bare metal, and is contained on every phase inside an hluk micro-VM (read-only mount, no network). Run with just demos php2shell.
  • The php image, a tier 2 runtime: PHP 8.5 embedded in the guest, with SQLite (PDO and SQLite3), mbstring, DOM and the XML extensions, OpenSSL, sodium, zlib, bcmath, sockets and Phar, in a 23 MB rootfs that runs in 128 MiB. Code and .php files run in one PHP request kept for the guest's life, so what a call declares is there for the next; an uncaught exception, a fatal error or exit() ends the call with the status the CLI would exit with (255 for the errors), and the next call finds the engine as it was, except after a fatal error, which ends the request as it ends a script: the next call starts a new one. Guest function calls name a PHP callable (greet, Handlers::greet), JSON in and out; host function calls are Hyperlight\call($name, ...$args). PHP's memory_limit is two thirds of the memory free at boot, and PHP 8.5's hard limit three quarters, so a script reaches them before the guest runs out, and mt_rand() and the other functions on PHP's default generator draw from a new seed every call, unless code gave mt_srand() one, and OpenSSL's gets new entropy, so a warm snapshot's clones draw their own numbers. With the php template and examples.
  • CI runs in three tiers, described in docs/ci.md: Linux and Windows on x86_64 on every pull request, macOS and Windows arm64 on pull requests labelled ci/arm64, and Linux arm64 too nightly, blocking nothing. A pull request runs the tier-1 jobs its files call for: documentation alone gets the spell check, integrations/<name>/ alone gets lint, the Rust tests and that integration, smolagents bringing MCP's (the ci/integrations label adds them all). ci-report.yml re-runs a failed run on main once, the nightly run or the newest push's, and files each job that fails again, a timeout included, as an issue labelled ci/nightly or ci/main.
  • Dependabot also proposes updates for the Rust toolchain (rust-toolchain.toml, labelled ci/arm64) and for the libraries of the Python integrations (smolagents, MCP, verl), whose caps it widens when a release past one is out (a minor, for a 0.x cap), keeping the floors CI tests.
  • A driver can ask how many times the guest was restored from a snapshot (HLCALL_IOC_RESTORES, hl_driver_restored() in drivers/hl_driver.h), to reseed what its runtime seeded before the snapshot (docs/driver.md).

Changed

  • Snapshots saved by v0.19.0 and earlier releases are refused (the kernel changed); save them again.
  • CI: pull requests labelled ci/arm64 no longer run on the Linux arm64 runner, which ci-passed no longer waits for, and pushes to main no longer run the arm64 tests, only refresh the arm64 guest images' cache: the nightly run tests both. ci-passed counts only the pull request's own arm64 run, not one started by hand. Dependabot no longer labels the actions' updates ci/arm64.

Fixed

  • just conformance now and then ran a module its skip list names (known_failures.toml, CONFORMANCE_SKIP), failing the job when it crashed (test_regrtest on Linux arm64): the skip-list check, a pipe into grep -q, could read an early exit of grep as a miss.
  • A snapshot could leave out part of a guest's memory: the kernel mapped each aligned 2 MiB of an mmaped range (an ELF segment the elfloader maps, say) with one large page, which Hyperlight's snapshot drops, so a guest restored from it could fault there. The kernel now maps 4 KiB pages only, and mmap(MAP_HUGETLB) fails with ENOMEM, as on Linux without a hugepage pool.
  • The drivers set every host environment variable again at the top of every call, with setenv(), which on musl frees the string the variable had: code still holding it read freed memory (PHP's putenv() keeps the string it replaces, SQLite caches getenv("TMPDIR")). A variable is now set only when it changed, with putenv() and a string that stays.
  • Programs did not get random AT_RANDOM bytes of their own: the elfloader pointed the auxiliary vector at a stale stack frame. Go's runtime seeds from those bytes, so math/rand drew the same numbers in every clone of a snapshot. The bytes are now copied onto the program's stack, as Linux does.
  • A program started with an empty argument or environment string (sh -c 'cmd' '', a BusyBox hush pipeline led by a builtin, as set | grep) crashed the guest's kernel (Assertion failure: len). Empty strings now reach the program, as on Linux.
  • Generators a runtime seeded before a snapshot kept their state in every guest restored from it, though the kernel reseeds its own: Python's random and NumPy's legacy np.random, OpenSSL (cryptography's copy too), SQLite's random(), node's and Java's Math.random, Java's ThreadLocalRandom, DRBGs and new generators' seeds, .NET's Random.Shared, and bash's $RANDOM. The kernel now counts restores (HLCALL_IOC_RESTORES), and each driver reseeds them from the kernel on the first call after one; docs/random.md lists what each image covers and what no driver can, such as a generator the code made before the snapshot. The bash image's BusyBox seeds $RANDOM when RANDOM is assigned, as bash does, and from the kernel rather than the pid and the clock.

Pull requests

  • CI tiers: path-filtered x86 on every PR, macOS/Windows arm64 on ci/arm64, Linux arm64 nightly by @danbugs in #191
  • justfile: a module the conformance skip list names is always skipped by @danbugs in #192
  • Reseed userspace RNGs after snapshot restore, plus kernel fixes by @danbugs in #195
  • Add a PHP runtime (tier 2) by @danbugs in #197
  • Add a PHP RCE containment demo by @danbugs in #199
  • Release v0.19.1 by @danbugs in #201

Full Changelog: verl-hyperlight-v0.1.0...v0.19.1