Qrexec: secure communication across domains
(This page is about qrexec v3. For qrexec v2, see here.)
The qrexec framework is used by core Qubes components to implement communication between domains. Qubes domains are strictly isolated by design. However, the OS needs a mechanism to allow the administrative domain (dom0) to force command execution in another domain (VM). For instance, when a user selects an application from the KDE menu, it should start in the selected VM. Also, it is often useful to be able to pass stdin/stdout/stderr from an application running in a VM to dom0 (and the other way around). (For example, so that a VM can notify dom0 that there are updates available for it). By default, Qubes allows VMs initiate such communications in specific circumstances. The qrexec framework generalizes this process by providing a remote procedure call (RPC) protocol for the Qubes architecture. It allows users and developers to use and design secure inter-VM tools.
Qrexec basics: architecture and examples
Qrexec is built on top of vchan, a Xen library providing data links between VMs.
During domain startup , a process named
qrexec-daemon is started in dom0, and a process named
qrexec-agent is started in the VM.
They are connected over a vchan channel.
qrexec-daemon listens for connections from a dom0 utility named
Let’s say we want to start a process (call it
VMprocess) in a VM (
Typically, the first thing that a
qrexec-client instance does is to send a request to the
qrexec-daemon, which in turn relays it to
qrexec-agent running in
qrexec-daemon assigns unique vchan connection details and sends them to both
qrexec-client (in dom0) and
qrexec-client starts a vchan server, which
qrexec-agent then connects to.
Once this channel is established, stdin/stdout/stderr from the VMprocess is passed between
qrexec-agent and the
qrexec-client command is used to make connections to VMs from dom0.
For example, the following command creates an empty file called
hello-world.txt in the home folder of
$ qrexec-client -e -d someVM user:'touch hello-world.txt'
The string before the colon specifies what user to run the command as.
-e flag tells
qrexec-client to exit immediately after sending the execution request and receiving a status code from
qrexec-agent (whether the process creation succeeded).
With this option, no further data is passed between the domains.
By contrast, the following command demonstrates an open channel between dom0 and someVM (in this case, a remote shell):
$ qrexec-client -d someVM user:bash
qvm-run command is heavily based on
It also takes care of additional activities, e.g. starting the domain if it is not up yet and starting the GUI daemon.
Thus, it is usually more convenient to use
There can be an almost arbitrary number of
qrexec-client processes for a given domain.
The limiting factor is the number of available vchan channels, which depends on the underlying hypervisor, as well the domain’s OS.
For more details on the qrexec framework and protocol, see “Qubes RPC internals.”
Qubes RPC services
Some common tasks (like copying files between VMs) have an RPC-like structure: a process in one VM (say, the file sender) needs to invoke and send/receive data to some process in other VM (say, the file receiver). The Qubes RPC framework was created to securely facilitate a range of such actions.
Obviously, inter-VM communication must be tightly controlled to prevent one VM from taking control of another, possibly more privileged, VM. Therefore the design decision was made to pass all control communication via dom0, that can enforce proper authorization. Then, it is natural to reuse the already-existing qrexec framework.
Also, note that bare qrexec provides
VM <-> dom0 connectivity, but the command execution is always initiated by dom0.
There are cases when VM needs to invoke and send data to a command in dom0 (e.g. to pass information on newly installed
Thus, the framework allows dom0 to be the RPC target as well.
Thanks to the framework, RPC programs are very simple – both RPC client and server just use their stdin/stdout to pass data.
The framework does all the inner work to connect these processes to each other via
Additionally, disposable VMs are tightly integrated – RPC to a DisposableVM is identical to RPC to a normal domain, all one needs is to pass
@dispvm as the remote domain name.
Qubes RPC administration
The dom0 directory
/etc/qubes-rpc/policy/ contains a file for each available RPC action that a VM might call.
Together the contents of these files make up the RPC access policy database.
Policies are defined in lines with the following format:
srcvm destvm (allow|deny|ask[,default_target=default_target_VM])[,user=user_to_run_as][,target=VM_to_redirect_to]
You can specify srcvm and destvm by name or by one of the reserved keywords such as
(Of these three, only
@anyvm keyword makes sense in the srcvm field.
Service calls from dom0 are currently always allowed, and
@dispvm means “new VM created for this particular request,” so it is never a source of request.)
Other methods using tags and types are also available (and discussed below).
Whenever a RPC request for an action is received, the domain checks the first matching line of the relevant file in
/etc/qubes-rpc/policy/ to determine access:
whether to allow the request, what VM to redirect the execution to, and what user account the program should run under.
Note that if the request is redirected (
target= parameter), policy action remains the same – even if there is another rule which would otherwise deny such request.
If no policy rule is matched, the action is denied.
If the policy file does not exist, the user is prompted to create one.
If there is still no policy file after prompting, the action is denied.
In the target VM, a file in either of the following locations must exist, containing the file name of the program that will be invoked, or being that program itself – in which case it must have executable permission set (
/etc/qubes-rpc/RPC_ACTION_NAMEwhen you make it in the template qube;
/usr/local/etc/qubes-rpc/RPC_ACTION_NAMEfor making it only in an app qube.
Making an RPC call
From outside of dom0, RPC calls take the following form:
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME rpc_client_path client arguments
$ qrexec-client-vm work qubes.StartApp+firefox
Note that only stdin/stdout is passed between RPC server and client – notably, no command line arguments are passed. By default, stderr of client and server is logged in the syslog/journald of the VM where the process is running.
It is also possible to call service without specific client program – in which case server stdin/out will be connected with the terminal:
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME
Specifying VMs: tags, types, targets, etc.
There are severals methods for specifying source/target VMs in RPC policies.
@tag:some-tag- meaning a VM with tag
@type:type- meaning a VM of
Target VM can be also specified as
@default, which matches the case when calling VM didn’t specified any particular target (either by using
@default target, or empty target).
@dispvm:DISP_VM is very similar to
@dispvm but forces using a particular VM (
DISP_VM) as a base VM to be started as DisposableVM.
anon-whonix @dispvm:anon-whonix-dvm allow
Adding such policy itself will not force usage of this particular
DISP_VM - it will only allow it when specified by the caller.
@dispvm:DISP_VM can also be used as target in request redirection, so it is possible to force particular
DISP_VM usage, when caller didn’t specify it:
anon-whonix @dispvm allow,target=@dispvm:anon-whonix-dvm
Note that without redirection, this rule would allow using default Disposable VM (
default_dispvm VM property, which itself defaults to global
Also note that the request will be allowed (
allow action) even if there is no second rule allowing calls to
@dispvm:anon-whonix-dvm, or even if there is a rule explicitly denying it.
This is because the redirection happens after considering the action.
The policy confirmation dialog (
ask action) allows the user to specify target VM.
User can choose from VMs that, according to policy, would lead to
It is not possible to select VM that policy would deny.
By default no VM is selected, even if the caller provided some, but policy can specify default value using
work-mail work-archive allow work-mail @tag:work ask,default_target=work-files work-mail @default ask,default_target=work-files
The first rule allow call from
work-archive, without any confirmation.
The second rule will ask the user about calls from
work-mail VM to any VM with tag
And the confirmation dialog will have
work-files VM chosen by default, regardless of the VM specified by the caller (
The third rule allow the caller to not specify target VM at all and let the user choose, still - from VMs with tag
work-archive, regardless of tag), and with
work-files as default.
RPC services and security
Be very careful when coding and adding a new RPC service.
Unless the offered functionality equals full control over the target (it is the case with e.g.
qubes.VMShell action), any vulnerability in an RPC server can be fatal to Qubes security.
On the other hand, this mechanism allows to delegate processing of untrusted input to less privileged (or disposable) AppVMs, thus wise usage of it increases security.
For example, this command will run the
firefox command in a DisposableVM based on
$ qvm-run --dispvm=work firefox
By contrast, consider this command:
$ qvm-run --dispvm=work --service qubes.StartApp+firefox
This will look for a
firefox.desktop file in a standard location in a DisposableVM based on
work, then launch the application described by that file.
The practical difference is that the bare
qvm-run command uses the
qubes.VMShell service, which allows you to run an arbitrary command with arbitrary arguments, essentially providing full control over the target VM.
By contrast, the
qubes.StartApp service allows you to run only applications that are advertised in
/usr/share/applications (or other standard locations) without control over the arguments, so giving a VM access to
qubes.StartApp is much safer.
While there isn’t much practical difference between the two commands above when starting an application from dom0 in Qubes 4.0, there is a significant security risk when launching applications from a domU (e.g., from a separate GUI domain).
This is why
qubes.StartApp uses our standard
qrexec argument grammar to strictly filter the permissible grammar of the
Exec= lines in
.desktop files that are passed from untrusted domUs to dom0, thereby protecting dom0 from command injection by maliciously-crafted
Service policies with arguments
Sometimes a service name alone isn’t enough to make reasonable qrexec policy. One example of such a situation is qrexec-based USB passthrough. Using just a service name would make it difficult to express the policy “allow access to devices X and Y, but deny to all others.” It isn’t feasible to create a separate service for every device: we would need to change the code in multiple files any time we wanted to update the service.
For this reason it is possible to specify a service argument, which will be subject to a policy. A service argument can make service policies more fine-grained. With arguments, it is easier to write more precise policies using the “allow” and “deny” actions, instead of relying on the “ask” method. (Writing too many “ask” policies offloads additional decisions to the user. Generally, the fewer choices the user must make, the lower the chance to make a mistake.)
Each specific argument that we want to use needs its own policy in dom0 at a path like
So for instance, we might have policies called
If the policy for the specific argument is not set (that is, if no file exists for
RPC_ACTION_NAME+ARGUMENT), then dom0 uses the default policy with no argument for this service.
When calling a service that takes an argument, just add the argument to the service name separated with
$ qrexec-client-vm target_vm_name RPC_ACTION_NAME+ARGUMENT
The script will receive
ARGUMENT as its argument.
The argument will also become available as the
QREXEC_SERVICE_ARGUMENT environment variable.
This means it is possible to install a different script for a particular service argument.
See below for an example of an RPC service using an argument.
Qubes RPC examples
To demonstrate some of the possibilities afforded by the qrexec framework, here are two examples of custom RPC services.
Simple RPC service (addition)
We can create an RPC service that adds two integers in a target domain (the server, call it “anotherVM”) and returns back the result to the invoker (the client, “someVM”).
In someVM, create a file with the following contents and save it with the path
#!/bin/sh echo $1 $2 # pass data to RPC server exec cat >&$SAVED_FD_1 # print result to the original stdout, not to the other RPC endpoint
Our server will be anotherVM at
The code for this file is:
#!/bin/sh read arg1 arg2 # read from stdin, which is received from the RPC client echo $(($arg1+$arg2)) # print to stdout, which is passed to the RPC client
We’ll need to create a service called
test.Add with its own definition and policy file in dom0.
Now we need to define what the service does.
In this case, it should call our addition script.
We define the service with a symlink at
/etc/qubes-rpc/test.Add pointing to our server script (the script can be also placed directly in
/etc/qubes-rpc/test.Add - make sure the file has executable bit set!):
ln -s /usr/bin/our_test_add_server /etc/qubes-rpc/test.Add
The administrative domain will direct traffic based on the current RPC policies.
In dom0, create a file at
/etc/qubes-rpc/policy/test.Add containing the following:
@anyvm @anyvm ask
This will allow our client and server to communicate.
Before we make the call, ensure that the client and server scripts have executable permissions. Finally, invoke the RPC service.
$ qrexec-client-vm anotherVM test.Add /usr/bin/our_test_add_client 1 2
We should get “3” as answer. (dom0 will ask for confirmation first.)
Note: For a real world example of writing a qrexec service, see this blog post.
RPC service with argument (file reader)
Here we create an RPC call that reads a specific file from a predefined directory on the target. This example uses an argument to the policy. In this example a simplified workflow will be used. The service code is placed directly in the service definition file on the target VM. No separate client script will be needed.
First, on your target VM, create two files in the home directory:
Have them contain two different “Hello world!” lines.
Next, we define the RPC service.
On the target VM, place the code below at
#!/bin/sh argument="$1" # service argument, also available as $QREXEC_SERVICE_ARGUMENT if [ -z "$argument" ]; then echo "ERROR: No argument given!" exit 1 fi cat "/home/user/$argument"
Make sure the file is executable! (The service argument is already sanitized by qrexec framework. It is guaranteed to not contain any spaces or slashes, so there should be no need for additional path sanitization.)
Now we create three policy files in dom0. See the table below for details. Replace “source_vm1” and others with the names of your own chosen domains.
|Path to file in dom0||Policy contents|
|/etc/qubes-rpc/policy/test.File||@anyvm @anyvm deny|
|/etc/qubes-rpc/policy/test.File+testfile1||source_vm1 target_vm allow|
|/etc/qubes-rpc/policy/test.File+testfile2||source_vm2 target_vm allow|
With this done, we can run some tests.
Invoke RPC from
[user@source_vm1] $ qrexec-client-vm target_vm test.File+testfile1
We should get the contents of
/home/user/testfile1 printed to the terminal.
Invoking the service from
source_vm2 should work the same, and
testfile2 should also work.
[user@source_vm2] $ qrexec-client-vm target_vm test.File+testfile1 [user@source_vm2] $ qrexec-client-vm target_vm test.File+testfile2
But when invoked with other arguments or from a different VM, it should be denied.