Virtainer Guest Agent — a modern, vsock-native guest agent that speaks the QEMU Guest Agent protocol
The Virtainer Guest Agent is a QEMU Guest Agent (QGA, qemu-ga) compatible agent for x86_64 Linux and Windows VMs on Cloud Hypervisor, from Virtainer. The Linux build is one static binary (about 1 MB, no runtime dependencies), reached by the host over vsock, that installs itself in systemd and OpenRC guests. The Windows MVP runs as an auto-start LocalSystem service through virtio-win's viosock Winsock provider and provisions Windows guests from a raw FAT12 seed. It is the guest component of the Virtainer virtualization platform for classic VMs.
It replaces installing qemu-guest-agent in every guest, which needs internet
access at first boot, per-distro package names, unit overrides and SELinux
policy. On Cloud Hypervisor qemu-ga also needs a vsock override, because
Cloud Hypervisor's virtio-console has no named virtio-serial ports.
- Transport: AF_VSOCK, port 100, any CID. Only the host (CID 2) may
connect; local and nested-VM callers are dropped (see Security).
On the host side this is Cloud Hypervisor's hybrid vsock socket:
CONNECT 100\n→OK <n>\n, then the byte stream. - Protocol: QGA's wire format, matching qemu-ga's observable behaviour:
the JSON stream (no newline required), the
0xFFresync byte,guest-sync-delimited,idecho, QAPI error classes and texts, no reply to a successfulguest-shutdown, and QEMU's JSON spelling ({"return": {}}, ASCII-only). The end-to-end suite diffs ten read-only commands against the qemu-ga that each guest image ships (see Test); the rest is checked against the QGA documentation, not against a reference run. - Linux commands: every Linux command of QEMU 11.1's
qga/qapi-schema.json, except the threeguest-suspend-*commands, which behave badly on Cloud Hypervisor (see Differences from qemu-ga).
| Group | Commands |
|---|---|
| Session | guest-sync, guest-sync-delimited, guest-ping, guest-info |
| Snapshots | guest-fsfreeze-status, -freeze, -freeze-list, -thaw, guest-fstrim |
| Identity | guest-get-osinfo, guest-get-host-name, guest-get-timezone, guest-get-users |
| Network | guest-network-get-interfaces, guest-network-get-route |
| Storage | guest-get-fsinfo, guest-get-disks (PCI address, serial, device node, NVMe SMART) |
| Power and time | guest-shutdown, guest-get-time, guest-set-time |
| Exec and files | guest-exec, guest-exec-status, guest-file-open/read/write/seek/flush/close |
| Accounts | guest-set-user-password, guest-ssh-get/add/remove-authorized-keys |
| Hotplug | guest-get/set-vcpus, guest-get/set-memory-blocks, guest-get-memory-block-info |
| Stats | guest-get-load, guest-get-cpustats, guest-get-diskstats |
| Virtainer | __io.virtainer_shell (below) |
Beyond qemu-ga:
- Shell:
__io.virtainer_shellturns the connection into an interactive terminal: the chosen user's login shell on a pseudo-terminal (on Windows, PowerShell as SYSTEM on a pseudo console), live resize, up to 8 sessions at once, apart from the 16 QGA connections. Closing the connection hangs the shell up, like closing an SSH session. The protocol is under Shell protocol. Guest owners who don't want it add the command toblock-rpcs; blockingguest-execblocks the Shell too. - Hot-added vCPUs and memory are onlined by the agent. The kernel does not
online a hot-added CPU by itself; that is normally a udev rule's job, and
most images do not ship one for virtual machines. Of the images the e2e
suite covers, only RHEL, AlmaLinux and Rocky 9-10 (
40-redhat*.rules) and openSUSE/SLES (80-hotplug-cpu-mem.rules) ship rules that online a hot-added CPU. Ubuntu's40-vm-hotadd.rulesmatches only Hyper-V and Xen DMI vendors; Debian, Fedora, Arch and Alpine have none, so without the agent a live resize leaves the new vCPUs offline there. Memory is onlined by the kernel's default policy on Ubuntu, Debian 13, Fedora and Arch, but not on Debian 12 or Alpine. The agent listens for the kernel'sadduevents and onlines the new CPU or memory block itself; devices present at start are left alone. - Auto-thaw watchdog: if no thaw arrives within
freeze-timeout(default 10 minutes;0disables auto-thaw), the agent thaws the filesystems itself, so a guest never stays frozen forever. A host that compares the freeze and thaw counts sees them differ (thaw returns 0), so an early thaw cannot pass for a quiesced snapshot. guest-set-timewithout hwclock(8): the agent sets the clock through system calls and the RTC device, so it works on images that don't shiphwclock(Ubuntu 24.04 and Debian 13 cloud images don't). With an explicittimeit also works without an RTC; withouttimeand without an RTC it returns an error, because there is nothing to read the time from. Under Cloud Hypervisor v53 the guests we booted had no usable RTC.
The Windows build targets Windows Server 2019, 2022 and 2025 (Core and Desktop
Experience), and Windows 11 IoT Enterprise LTSC. It requires amd64 Windows,
virtio-win's viosock driver and its Winsock provider, and built-in Windows
PowerShell 5.1, CIM, LocalAccounts and NetTCPIP cmdlets. The command transport
uses Winsock AF_VSOCK (40), port 100, and rejects every peer except CID 2 before
starting a connection thread. The host uses the same Cloud Hypervisor hybrid
vsock handshake and QGA framing as on Linux.
These are implemented MVP paths. Cross-target check and Clippy do not establish Windows runtime acceptance; SCM installation, the provider, specialize, PowerShell provisioning and executable replacement must also be exercised in a real guest.
| Group | Supported Windows commands |
|---|---|
| Session | guest-sync, guest-sync-delimited, guest-ping, guest-info |
| Identity | guest-get-osinfo, guest-get-host-name |
| Time and power | guest-get-time, guest-set-time, guest-shutdown |
| Network | guest-network-get-interfaces (names, MACs, IPv4/IPv6 addresses and prefixes) |
| Exec | guest-exec, guest-exec-status (stdin, environment, separate or merged capture) |
| Files | guest-file-open, guest-file-read, guest-file-write, guest-file-seek, guest-file-flush, guest-file-close |
| Accounts | guest-set-user-password (base64 UTF-8 password, crypted: false) |
| Provisioning | __io.virtainer_provision |
| Shell | __io.virtainer_shell (Windows PowerShell as SYSTEM; see Shell on Windows) |
| Crash report | __io.virtainer_last_crash (see Last blue screen) |
guest-set-time requires an explicit Unix time in nanoseconds on Windows; this
build does not read an RTC. guest-shutdown accepts powerdown, halt and
reboot, uses the Windows shutdown API, and sends no reply on success. In this
MVP, halt also powers down. File and exec limits match the Linux limits above;
file write modes reject a reparse point in the last path component.
The known commands outside this table return the QAPI error
{"error": {"class": "GenericError", "desc": "Command <name> is not supported"}}.
This includes filesystem freeze/thaw and trim, filesystem/disk/device inventory,
vCPU and memory commands, statistics, routes, timezone/user inventory, authorized
key RPCs and suspend commands. They are absent from guest-info's supported
command list. Unknown command names return CommandNotFound. VSS is outside
this MVP; Windows snapshots are crash-consistent.
Run an elevated prompt once while preparing the image:
.\virtainer-guest-agent.exe installThis copies the executable into
%ProgramFiles%\Virtainer\GuestAgent\virtainer-guest-agent.exe, registers
virtainer-guest-agent as an auto-start LocalSystem service, and starts it.
An unchanged executable is not rewritten or restarted. Installation also
sets private SYSTEM/Administrators directory ACLs. Windows RPC filters use
%ProgramData%\Virtainer\GuestAgent\virtainer-guest-agent.conf, with the same
block-rpcs and allow-rpcs syntax shown below; Linux freeze settings are not
accepted in this file. uninstall stops and deletes the service and removes its
executable. When invoked from the installed executable, Windows can keep that
file mapped; removal is then scheduled for the next reboot using
MoveFileExW.
The command reports deferred removal; reboot before reinstalling. Provisioning
records are kept so a reinstall cannot replay the user's script.
The generalized image must include this entry in its unattend file's
specialize pass. Setup invokes it synchronously as SYSTEM before the normal
specialize reboot. It sets only the hostname, which takes effect with that
reboot. The network stack's RPC services are not running during specialize, so
network configuration waits for the service:
<settings pass="specialize">
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>1</Order>
<Description>Apply Virtainer hostname and network</Description>
<Path>"C:\Program Files\Virtainer\GuestAgent\virtainer-guest-agent.exe" specialize</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>Adjust the path if Windows uses a different Program Files location. The agent
must already be installed, and the seed must already be attached when Setup
runs this entry. An ordinary service boot leaves a new instance pending until
its specialize checkpoint exists and the active Windows hostname matches the
requested name (case-insensitive). A service restart before the specialize reboot
keeps provisioning pending and defers networking, accounts and the user script. It does not
report done for a rename that still needs a reboot. When the service finds
the requested name set but not yet active after specialize (Windows Server 2019),
it restarts Windows itself, once per provisioning: it records the restart first
(in a <instance>.restart file next to the record, so earlier agents can
still read the record),
waits until Windows setup after sysprep (HKLM\SYSTEM\Setup
SystemSetupInProgress/OOBEInProgress) has finished, logs the reason, and
restarts with a comment that Windows records in the System log (event 1074).
If the name is still not active after that restart, provisioning stays pending
until the next boot. The service is not running
during specialize, so the agent first answers on vsock after that reboot. Non-generalized images require the operator to arrange
this entry point and the appropriate reboot before using the service.
Attach an unpartitioned FAT12 disk containing:
virtainer-guest-agent.exe;virtainer-provision.json;- optionally
user-script.ps1.
Windows need not mount the volume. The agent scans \\.\PhysicalDrive0 through
\\.\PhysicalDrive255, recognizes a seed by the contract filename on a valid
FAT12 volume, decodes its long filenames, and reads it twice to establish stable
content. Multiple matching seeds, corrupt files and inconsistent persisted state
are refused. The reader accepts images up to 256 MiB and files up to 64 MiB,
checks FAT geometry, FAT copies and cluster chains, and never writes the disk.
The schema and field names are:
{
"schema": 1,
"instance_id": "00000000-0000-4000-8000-000000000001",
"seed_version": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"hostname": "WEB-01",
"admin": {"username": "Administrator", "password": "REPLACE-WITH-A-STRONG-PASSWORD"},
"ssh_authorized_keys": ["ssh-ed25519 REPLACE-WITH-A-PUBLIC-KEY"],
"timezone": "UTC",
"network": [{"mac": "52:54:00:aa:bb:cc", "addresses": ["10.0.0.5/24"],
"gateway": "10.0.0.1", "dns": ["1.1.1.1"]}],
"user_script": "user-script.ps1"
}instance_id is a canonical lowercase UUID; seed_version is the host-supplied
64-digit SHA-256 content identifier, returned unchanged. The agent checks stable
raw content; it does not independently recompute that identifier (the document
itself contains it). Hostnames use the NetBIOS form, at most 15 ASCII letters,
digits or hyphens, and cannot be all digits or start/end with a hyphen. Static
IPv4/IPv6 addresses are selected by MAC; an empty network selects DHCP on all
hardware adapters. A MAC-selected entry with empty addresses selects DHCP on
that adapter. Both DHCP paths remove manual IPv4/IPv6 addresses and static
default routes, enable DHCP for both families and IPv6 router discovery, and
reset DNS to automatic configuration.
Duplicate MACs, invalid CIDRs/IPs, unknown fields and other schema mistakes are
rejected before changes. user_script, when supplied, must be exactly
user-script.ps1.
Specialize applies the hostname. At service startup the agent updates
itself if the seed executable differs, then configures networking, creates or
enables the requested local administrator and sets its password, writes
%ProgramData%\ssh\administrators_authorized_keys with SYSTEM/Administrators-only
ACLs, sets the Windows timezone ID, and runs the optional PowerShell script as
SYSTEM (UTF-8 seed scripts receive a Unicode BOM for Windows PowerShell 5.1).
OpenSSH installation and enabling its service remain image preparation
steps. Credentials go to fixed PowerShell code on stdin, never on the command
line; records and helper error messages contain no credentials or script output.
When a PowerShell step fails, the recorded error holds that step's PowerShell
error category, error ID and message, with the administrator password removed
and the text limited to 300 characters; the same text goes to the agent log.
Records under %ProgramData%\Virtainer\GuestAgent\instances survive reboot.
Each instance's completed provisioning is retained, including when a different
instance later uses the image. A changed seed for an already completed instance
does not reapply its configuration or replay its script. Interrupted system
provisioning is failed with an unknown outcome and is not automatically retried.
The script is checkpointed before starting; failure or interruption is recorded
in errors, but does not block done and does not cause replay. Helpers have a
120-second limit; the user script has a 30-minute limit. An operator must resolve
failed/uncertain system steps before preparing a new instance.
Request provisioning status without arguments:
{"execute": "__io.virtainer_provision"}Its return object contains exactly instance_id, seed_version, state
(pending, applying, done or failed), errors and agent_version.
Before any instance is known, the first two fields are null. The last record
remains available after seed removal. Host readiness and seed removal must wait
for this instance's done and matching seed_version; ICMP is not the readiness
signal.
Boot-time updates copy the installed, working executable to virtainer-guest-agent.updater.exe
and stage the replacement separately as virtainer-guest-agent.next.exe. The
service stops after launching the updater. The updater runs the installed version's
code, waits for the service to stop, copies the installed binary to
virtainer-guest-agent.previous.exe, atomically replaces the installed
executable and restarts through SCM. The installed path remains present until
atomic replacement, including if the updater exits or Windows reboots after the
backup copy. A backup copy failure restarts the existing service. Startup is
confirmed within 60 seconds by observing two continuous seconds of SCM
RUNNING; the service publishes that state only after opening its vsock listener.
A replacement or startup failure attempts rollback. Before launching the helper,
the agent persists the instance ID, seed version and exact executable bytes in
%ProgramData%\Virtainer\GuestAgent\failed-update. This checkpoint also covers
helper crashes; it is retained on failure and written again before restarting
the fallback. The same input is suppressed across service restarts and reboots,
so the fallback can serve and provision without repeating the failed update.
Changed instance, seed version or executable bytes permit a new attempt.
Successful confirmed startup clears the checkpoint. To retry unchanged input
after correcting the failure, run these commands in an elevated PowerShell:
Stop-Service virtainer-guest-agent
& "$env:ProgramFiles\Virtainer\GuestAgent\virtainer-guest-agent.exe" retry-update
Start-Service virtainer-guest-agentretry-update requires the service to be stopped. This is trusted-seed, unsigned delivery;
attach the seed read-only and treat its executable, password and script as
privileged input. Authenticated live updates are outside the MVP.
The transport ABI follows virtio-win's public viosock interface. The Windows address layout follows its socket ABI header. Command field semantics follow the QGA protocol reference.
| Area | qemu-ga (observed with 10.2.2) | This agent |
|---|---|---|
| Concurrency | One connection at a time | Up to 16 at once |
guest-set-time |
Fails when hwclock is missing |
Works without hwclock; with an explicit time also without an RTC |
Files created by guest-file-open |
Mode 0666 | Mode 0644 |
IPv6 route metric/flags |
Sign-extension bug (-1, 18446744071564165121) |
The kernel's unsigned values |
guest-network-get-route without IPv6 |
Error | IPv4 routes only |
guest-get-users |
utmp only | utmp if present, otherwise logind session files |
guest-exec tracking |
Unbounded | Bounded (see Limits) |
guest-suspend-* |
Present | Absent |
guest-info version |
QEMU's version | This agent's version |
Why there is no guest-suspend-*. Cloud Hypervisor's ACPI tables offer
only S0 (running) and S5 (off). Observed with qemu-ga 10.2.2 on Fedora 44
under Cloud Hypervisor v53: guest-suspend-ram reported success, fell back
to suspend-to-idle and never woke; guest-suspend-disk reported success but
wrote the hibernation image to zram, which lives in the guest's own RAM, so
the next start was a cold boot; guest-suspend-hybrid failed with "unknown
guest suspend mode". These are observations on that one setup, not a claim
about every guest or hypervisor version.
| Limit | Value |
|---|---|
| Concurrent QGA connections | 16 |
| Concurrent Shell sessions (not counted against the 16) | 8 |
guest-exec processes tracked until their status is collected |
128 |
Captured output per guest-exec stream |
16 MiB |
Captured output retained across all guest-exec processes |
256 MiB |
Open guest-file-open handles |
256 |
guest-file-read count |
48 MiB |
| Maximum size of one QGA message | 1 MiB; 64 MiB for guest-file-write |
| Idle connection timeout | 600 s; none once the connection is a Shell session |
| Write timeout (a host that stops reading) | 60 s |
| Shell frame, data, host to guest | 64 KiB |
| Shell frame, data, guest to host | chunks of up to 16 KiB |
| Shell frame, control | 4 KiB |
A Shell frame larger than the limit ends the session with a protocol error. Past the limits above, the agent answers with a QGA error or closes the connection; it does not queue.
The host connects to port 100, syncs as usual, and sends:
{"execute": "__io.virtainer_shell",
"arguments": {"user": "debian", "rows": 40, "cols": 120, "term": "xterm-256color"}}user is required (root has to be asked for by name); rows/cols
(1-9999, default 24 x 80) and term (default xterm-256color) are optional.
On success the agent replies {"return": {"pid": N}}, and from the next byte
on the connection carries Shell frames in both directions instead of QGA
messages. On failure it replies with a normal QGA error and the connection
stays a QGA connection, so the host must wait for the reply before it sends a
frame.
Each frame is a 1-byte type, a 4-byte little-endian payload length, and the payload:
| Type | Payload | Direction |
|---|---|---|
| 0, data | Raw terminal bytes (at most 64 KiB host → guest; chunks of at most 16 KiB guest → host) | host → guest: keystrokes; guest → host: output |
| 1, control | One JSON object (at most 4 KiB) | host → guest: {"resize": [rows, cols]}; guest → host: {"exitcode": N} or {"signal": N}, once, when the shell has ended |
Unknown frame types and control members are ignored. The shell is the account's login shell on a new pseudo-terminal, started as a login shell in the home directory with a fresh environment. It is not a PAM login: no password, no logind session, no utmp entry. Closing the connection hangs the shell up, like closing an SSH session. At most 8 sessions run at once, apart from the 16 QGA connections, and every session is logged.
The protocol is the same. user must be SYSTEM: the shell is Windows
PowerShell 5.1 (powershell.exe -NoLogo) running as the service's own
account, LocalSystem, so whoami prints nt authority\system. It runs in
session 0 with no desktop (windows a program opens are not visible), starts
in SYSTEM's profile directory and inherits the service's environment; term
is checked but not used. Type cmd for the Command Prompt.
The terminal is a pseudo console (ConPTY), which Windows Server 2019 and
later provide. Keystrokes are read as UTF-8 and output is UTF-8 VT text,
so dir and other console programs show non-ASCII names correctly whatever
the console code page. A program that writes UTF-8 bytes to its output on
its own (Python, Git) is decoded with the console code page, as in a local
console; chcp 65001 or [Console]::OutputEncoding = [Text.Encoding]::UTF8
fixes that for the session. Ctrl-C reaches the foreground program as it
does in a local console. The shell runs in a job object: when the session
ends, because the shell exited or the connection closed, every process it
started ends with it. The exit code is reported as guest-exec-status
reports it on Windows, a signed 32-bit value.
__io.virtainer_last_crash (Windows only, no arguments) reports the bug
check that ended the previous boot, or null if the previous boot did not
end in one. It reads the System event log of the current boot: Kernel-Power
event 41, logged early in the boot, and event 1001 from
Microsoft-Windows-WER-SystemErrorReporting, logged once Windows has saved
the dump. A restart or power loss without a bug check is not reported.
{"execute": "__io.virtainer_last_crash"}
{"return": {"time": "2026-10-13T15:02:11.5000000Z",
"stop-code": "0x000000EF",
"stop-name": "CRITICAL_PROCESS_DIED",
"parameters": ["0xFFFFE40AE6D6F080", "0x0000000000000000",
"0x0000000000000000", "0x0000000000000000"],
"dump-path": "C:\\Windows\\MEMORY.DMP"}}time is when the event was logged after the restart (UTC), not the time
of the crash. stop-name is left out for a code the agent has no name for,
and dump-path until event 1001 has been logged, or when no dump was saved.
The reply stays the same for the whole boot, so asking again later can add
the dump path. Clearing the System log during the boot loses the report.
tools/build-iso.sh # → dist/virtainer-guest-agent-<version>.iso (+ .sha256)Prerequisites: rustup (it installs the toolchain and the
musl target pinned in rust-toolchain.toml), python3 and xorriso. The
script runs cargo build --release --locked for x86_64-unknown-linux-musl
(the default target, see .cargo/config.toml) and packs the binary with its
licence notices into a read-only ISO labelled VIRTGA, next to a .sha256
file. No system musl toolchain is needed: the Rust target links its bundled
musl.
The binary installs itself (virtainer-guest-agent install, as root, on
systemd and OpenRC guests). The install:
- copies itself to
/usr/local/sbin/virtainer-guest-agentatomically; - writes a systemd unit (or an OpenRC script on Alpine), then enables and
starts it with
--no-block; - runs
restoreconwhen SELinux is enabled; - disables a qemu-ga unit that earlier Virtainer releases pointed at vsock
port 100, keeping it as
.bak.
Re-running it does not rewrite files or restart the service unless the binary
or the unit changed, so the same call both installs and upgrades.
virtainer-guest-agent uninstall removes the binary, the service unit and the
agent's runtime directory; it does not restore the old qemu-ga unit.
Recommended delivery: attach the ISO read-only to every VM and give the NoCloud seed this vendor-data. It runs on every boot, so swapping the ISO upgrades the agent:
#cloud-boothook
#!/bin/sh
d=$(mktemp -d) || exit 0
if mount -o ro LABEL=VIRTGA "$d" 2>/dev/null; then
"$d/$(uname -m)/virtainer-guest-agent" install
umount "$d"
fi
rmdir "$d"Use a boothook, not a cloud-config bootcmd: a bootcmd in the user's own
user-data would replace the vendor one. Users can still opt out with
vendor_data: {enabled: false}. Without cloud-init, mount the ISO and run
the same install once by hand. To upgrade, swap the ISO (the boothook
installs the new binary at the next boot) or re-run install from the new
binary.
Optional /etc/virtainer-guest-agent.conf in the guest. A file that cannot be
parsed stops the agent instead of being half-applied.
# Same semantics as qemu-ga's --block-rpcs / --allow-rpcs.
block-rpcs = guest-exec, guest-file-open
# allow-rpcs = guest-ping, guest-info, guest-fsfreeze-freeze, ...
# Script run with "freeze"/"thaw". Default: qemu-ga's hook if one is
# installed (/etc/qemu-ga/fsfreeze-hook, /etc/qemu/fsfreeze-hook).
# Empty disables hooks.
# fsfreeze-hook = /usr/local/sbin/my-hook
# Seconds before auto-thaw; 0 disables auto-thaw.
freeze-timeout = 600Whoever can reach the host's vsock socket for this VM has root in the
guest. The agent runs as root and its command set is root-equivalent. The
Shell adds no capability beyond guest-exec. Protect the socket the way you
would protect root SSH access to the guest.
- Root-equivalent commands:
guest-exec,__io.virtainer_shell,guest-file-open,guest-set-user-password,guest-ssh-add-authorized-keys,guest-ssh-remove-authorized-keys,guest-set-timeandguest-shutdown. A guest that must not give the host this reach can list them inblock-rpcs, or useallow-rpcswith a short list, in the guest's config file. Blocking only some of them is not a boundary: for exampleguest-file-opencan rewrite/etc/passwd, andguest-set-timecan break certificate validation. - On SELinux distros the agent runs as
unconfined_service_t, so RHEL's qemu-ga restrictions (no exec, no file access, unlabeled mount points refused) do not apply. - The ISO plus boothook runs whatever carries the
VIRTGAlabel as root at every boot. Treat the ISO as trusted code: build it yourself or verify its.sha256against a checksum you obtained separately, and attach it read-only. - Connections are accepted only from the host (CID 2). If
vsock_loopbackis loaded, any unprivileged guest process could otherwise reach the root agent; nested VMs could too. Port 100 is privileged, so no unprivileged process can bind it while the agent is down. authorized_keysis read and written with the target user's credentials, so a symlink there pointing at another user's file is refused. The credential switch uses raw per-thread syscalls on a dedicated thread, so it never affects other connections.guest-file-openopens paths as root. In the write modes it does not follow a symlink in the last path component.- Files created through
guest-file-openget mode 0644, not qemu-ga's 0666. - Logging never blocks. Nothing is written while filesystems are frozen, because a journald stuck on a frozen disk must not stall the thaw.
To report a vulnerability, see SECURITY.md.
cargo test --locked # unit tests, as an unprivileged user
cargo clippy --all-targets --locked -- -D warnings
cargo build --release --locked # the e2e suite boots this binary
e2e/run.py # real VMs under Cloud HypervisorThe end-to-end suite needs cloud-hypervisor v53 and ch-remote, CLOUDHV.fd
and hypervisor-fw in $VGA_E2E_BIN (default ~/.cache/vga-e2e/bin),
qemu-img, xorriso, python3, /dev/kvm, and a tap device that root
creates once; the script's docstring has the commands.
e2e/run.py --list shows the images it boots: Ubuntu 26.04 and 24.04,
Debian 13 and 12, Fedora 44, AlmaLinux 10, Rocky 9, CentOS Stream 10, RHEL 9.7
and 10.1, openSUSE Leap 16, Alpine 3.24, Arch and Amazon Linux 2023. The image
URLs are pinned, dated builds, so they need updating as upstreams rotate them;
RHEL images require a Red Hat login and must be supplied by hand. Downloads
are not checksum-verified. No per-distro results are published in this
repository.
Each guest gets the agent through the ISO and boothook above, and the host talks to it over hybrid vsock. The suite checks:
- guest-info, OS info, host name, network interfaces, file systems and disks;
- freeze and thaw, including counts, and
guest-fstrim; guest-execand the guest-file commands;- authorized-keys changes (including a symlink to
/etc/shadow),guest-set-user-passwordandguest-set-time; - live vCPU and memory hot-add;
- the Shell, including hang-up on disconnect and the limit of 8 sessions;
guest-file-openrefusing to follow a symlink in the write modes (reads still follow it);- the message limits (a 1 MiB non-write message and a token flood get the
size error and a close, a 2 MiB
guest-file-writelands intact) and the limit of 16 connections; guest-execreturning while a background child still holds its pipe, keeping arguments out of the log, and log lines that cannot be forged with a newline in a path;- the configuration file:
freeze-timeoutauto-thaw,block-rpcs(including the Shell), and refusal to start on a group-writable file; - rejection of a connection from a local (loopback) vsock client;
- the SELinux context, where SELinux is enabled;
installidempotence and the legacy qemu-ga unit cleanup;- reboot and power-off;
- where the image ships qemu-ga, a diff of ten read-only commands against it
on port 101 (
guest-get-osinfo,-host-name,-timezone,-network-get-interfaces,-get-fsinfo,-get-disks,-get-vcpus,-get-memory-block-info,-fsfreeze-status,-network-get-route), with the known qemu-ga differences normalised.
Copyright 2026 The Virtainer authors. Licensed under the Apache License, Version 2.0.
src/autoonline.rs is adapted from Kata Containers (also Apache-2.0); see
THIRD_PARTY_NOTICES.md for the notices that ship
with the binary. No QEMU (GPL) code is included, and none may be added.
Virtainer is a self-hosted virtualization platform for hardware you own: it runs full Linux VMs and Docker images as hardware-isolated machines. Virtainer Free runs on a single host; Virtainer Pro, the multi-host edition for clusters, is in development. The Virtainer Guest Agent is one of Virtainer's open-source components.