Executing arbitrary commands in your libvirt/Qemu virtual machine through Qemu guest agent

Many of us are used to installing additional Virtualization software-specific tools in virtual machines in order to get seamless Copy & Paste, higher screen resolutions — think “VMWare tools” or the “VirtualBox guest extensions”. So does Qemu, the working horse behind most libvirt-powered virtual machines these days, with the “Qemu guest agent”. But did you know that it also allows you to execute arbitrary commands in the guest system from the virtualization host?

Usually the communication channels between the virtualization host and its guests, the virtual machines or “domains” as libvirt call them, are rather limited: sure, you can always “ssh” in to execute commands but that requires not only a working network and ssh setup but also knowledge of IP addresses. Apart from that, you can ask the guest to shutdown through the emulation of a ACPI power button press but even that already requires cooperation from the guest’s operating system (OS), i.e. it can choose to ignore that. And you can try sending the guest the keyboard combination Ctrl-Alt-Del, it may or may not reboot then. Anything beyond that requires active cooperation with special software inside the guest system that uses some separate communication channel with the host.

Qemu logoMeet Qemu’s guest agent. Modern Linux distributions automatically install for you it when they detect running inside a Qemu virtual machine, otherwise you might want to install it manually. Given proper VM configuration, i.e. defining the qemu-ga channel as virt-install and virt-manager usually do for you, Qemu’s guest agent does not only give you safe reboots and shutdowns but can also be used to tell VMs to suspend or wakeup, get or set the VM’s time, set a user’s password, assist in creating snapshots of a VM and most of all retrieve lots of information.

Most of this functionality is either automatically used or abstracted by libvirt commands. For example, given the virsh shutdown command, “by default the hypervisor will try to pick a suitable shutdown method” and libvirt’s qemu will prefer communication with the guest agent to initiate shutdown over the ACPI method. Here are some other, probably lesser known virsh examples that do actually require a running guest agent in the domain:

  • virsh domfsinfo domainname returns a list of mounted filesystems within the running domain:Example output for the "virsh domfsinfo" command
  • virsh domifaddr domainname --source agent returns a list of interfaces, their virtual MAC addresses and their assigned IP addresses where the same command with --source lease may return no and with --source arp only limited information.
  • virsh dompmsuspend domainname disk sends a VM into suspend-to-disk mode (ACPI S3). Your mileage may vary.
  • virsh domtime domainname –now syncs the host’s system time to the guest’s system time.
  • virsh guestinfo domainname returns comprehensive information about almost any aspect on the VM, including the number of active users, operating system identification, disk and filesystem information, in an easily parseable format.
  • virsh set-user-password domainname username password can be used to (re)set user‘s password.

But the real powerhorse is the virsh qemu-agent-command command which allows sending commands to the guest agent directly as we will see below. It is not to be confused with virsh qemu-monitor-command which could be used to send low-level commands to the qemu monitor, a process libvirt itself already communicates with for management of the VM, just like with the guest agent.

A word of warning quoted from libvirt’s Qemu guest agent page: “Libvirt does not guarantee any support of direct use of the guest agent. […] any changes you make to the agent that change state behind libvirt’s back may cause libvirt to misbehave.” Only use virsh qemu-agent-command if there’s no other alternative and do not use it to modify the VM’s state, e.g. do not use it to send a domain to suspend mode. And do not use virsh qemu-monitor-command at all unless you really know what you’re doing.

As mentioned, virsh qemu-agent-command allows direct communication with the guest agent. The guest agent, not a shell! You can thus not pass a command to be executed inside the VM directly because there is a protocol to implement when speaking to the guest agent called QMP (Qemu Machine Protocol).

Communication with the guest agent happens in form of requests and responses. Both requests and responses are in JSON. Here’s an example request, in pretty-printed form:

{
    "execute": "guest-exec",
    "arguments": {
        "path": "/usr/bin/ls",
        "arg": [
            "/"
        ],
        "capture-output": true
    }
}

execute is the command sent to the agent itself, in our case this will always be guest-exec. arguments encapsulates the argument to the guest-exec command, here being path the path to the executable to call and arg being a list of arguments to be passed to that executable. capture-output needs to be specified as True if we’re interested in capturing stdout of the program run, which, in the case of ls, we are, of course.

If we want to type this in the Shell, we won’t be using the pretty-printed form, of course, but rather something like this (change mydomain to the name of your VM):

virsh -c qemu:///system qemu-agent-command mydomain \
  '{"execute": "guest-exec", "arguments": { "path": "/usr/bin/ls", "arg": [ "/" ], "capture-output": true }}'

This will return something like this:

{"return":{"pid":14925}}

You might wonder why we didn’t get the command’s output directly. The reason is that programs executed this way may need time to complete and we don’t want the guest agent to be blocked by that. Using the return command’s process ID (PID) we can however retrieve its results in a second step. First in pretty form again:

{
    "execute": "guest-exec-status",
    "arguments": {
        "pid": 14925
    }
}

And again for copy, paste and adapt:

virsh -c qemu:///system qemu-agent-command mydomain \
  '{"execute": "guest-exec-status", "arguments": { "pid": 14925 }}'

This will return something like this:

{"return":{"exitcode":0,"out-data":"YmluCmJvb3QKZGVhZC5sZXR0ZXIKZGV2CmV0Ywpob21lCmxpYgpsaWI2NApsb3N0K2ZvdW5kCm1lZGlhCm1udApvcHQKcHJvYwpyb290CnJ1bgpzYmluCnNlbGludXgKc3J2CnN5cwp0bXAKdXNyCnZhcgo=","exited":true}}

or pretty-printed:

{
    "return": {
        "exitcode": 0,
        "out-data": "YmluCmJvb3QKZGVhZC5sZXR0ZXIKZGV2CmV0Ywpob21lCmxpYgpsaWI2NApsb3N0K2ZvdW5kCm1lZGlhCm1udApvcHQKcHJvYwpyb290CnJ1bgpzYmluCnNlbGludXgKc3J2CnN5cwp0bXAKdXNyCnZhcgo=",
        "exited": true
    }
}

exited being true tells us the program is not running anymore, exitcode being 0 tells us it exited without an error, which is also why there is no err-data in the output which otherwise would have been the captured stderr. out-data contains the captured output to stdout. Can you guess in which format? Correct, this is Base64-encoded. If we run

echo "YmluCmJvb3QKZGVhZC5sZXR0ZXIKZGV2CmV0Ywpob21lCmxpYgpsaWI2NApsb3N0K2ZvdW5kCm1lZGlhCm1udApvcHQKcHJvYwpyb290CnJ1bgpzYmluCnNlbGludXgKc3J2CnN5cwp0bXAKdXNyCnZhcgo=" | base64 --decode

we get the expected output:

bin
boot
dev
[...]
usr
var

And that’s it! To close, a remark to those paranoid realizing that running the Qemu guest agent in their VM would also allow the VM hoster as a 3rd party execute arbitrary commands: yes, Sherlock, you got that right. But if you can’t trust your VM hoster this far, you should probably rather change the hoster…