Repository navigation
v0.19.1
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 withjust demos php2shell. - The
phpimage, 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.phpfiles 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 orexit()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 areHyperlight\call($name, ...$args). PHP'smemory_limitis 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, andmt_rand()and the other functions on PHP's default generator draw from a new seed every call, unless code gavemt_srand()one, and OpenSSL's gets new entropy, so a warm snapshot's clones draw their own numbers. With thephptemplate 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 labelledci/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 (theci/integrationslabel adds them all).ci-report.ymlre-runs a failed run onmainonce, the nightly run or the newest push's, and files each job that fails again, a timeout included, as an issue labelledci/nightlyorci/main. - Dependabot also proposes updates for the Rust toolchain (
rust-toolchain.toml, labelledci/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()indrivers/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/arm64no longer run on the Linux arm64 runner, whichci-passedno longer waits for, and pushes tomainno longer run the arm64 tests, only refresh the arm64 guest images' cache: the nightly run tests both.ci-passedcounts only the pull request's own arm64 run, not one started by hand. Dependabot no longer labels the actions' updatesci/arm64.
Fixed
just conformancenow and then ran a module its skip list names (known_failures.toml,CONFORMANCE_SKIP), failing the job when it crashed (test_regrteston Linux arm64): the skip-list check, a pipe intogrep -q, could read an early exit ofgrepas 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, andmmap(MAP_HUGETLB)fails withENOMEM, 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'sputenv()keeps the string it replaces, SQLite cachesgetenv("TMPDIR")). A variable is now set only when it changed, withputenv()and a string that stays. - Programs did not get random
AT_RANDOMbytes of their own: the elfloader pointed the auxiliary vector at a stale stack frame. Go's runtime seeds from those bytes, somath/randdrew 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, asset | 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
randomand NumPy's legacynp.random, OpenSSL (cryptography's copy too), SQLite'srandom(), node's and Java'sMath.random, Java'sThreadLocalRandom, DRBGs and new generators' seeds, .NET'sRandom.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.mdlists 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$RANDOMwhenRANDOMis 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