QubesOS in Emacs Org Mode: Link to files of other qubes

Table of Contents

1. Introduction

I use Emacs Org Mode to maintain a personal wiki for pretty much everything. It includes my appointments and todos, university lecture notes, flash cards for spaced repetition learning, diary entries and more. Since much of the information is quite sensitive and since all of the pages are created by myself, they reside in a network-isolated, highly trusted qube.

Sometimes, though, I want to link to a file residing in another qube (for example, an untrusted PDF of an academic article residing in my work qube. When I click the link, it should open the file in the corresponding qube. Additionally, I want auto completion of the file path when creating the link. This blog post details how I achieved both of these requirements.

2. Qrexec services

2.1. Opening files of a qube

Surprisingly, qubes does not ship with a qrexec service with which qube A can instruct qube B to open one of qube B's files. qvm-open-in-vm does something slightly different: It enables you to copy a file from qube A to qube B and then open it in qube B.

qvm-open-in-vm also allows opening a URL in the target qube. My initial thought was: "Great, I can just pass a file:// URL to qvm-open-in-vm (for example, file:///home/user/Documents/article.pdf) and that should lead to the target qube opening the corresponding file". This almost works, but qvm-open-in-vm restricts the types of URLs that can be used with it, and file:// URLs aren't one of them. So, we need to copy and slightly modify the service.

First, in my template qube, I created a copy of /etc/qubes-rpc/qubes.OpenURL to /etc/qubes-rpc/my.OpenURL. The original lists three different allowed URL schemes (http://, https://, ftp://). These simply needed to be extended with the additional file:// scheme. The result looks like this.


read -r url

case "$url" in
        exec qubes-open "$url"
        echo "Invalid URL" >&2
        exit 1

Next, still in my template qube, I created a copy of /usr/bin/qvm-open-in-vm to /usr/bin/my-open-in-vm. In that program, I simply needed to replace every occurrence of qubes.OpenURL with my.OpenURL and every occurrence of qvm-open-in-vm with my-open-in-vm.

Finally, I created a policy file in dom0, at /etc/qubes-rpc/policy/my.OpenURL. Mine looks like this.

personal work allow
@anyvm @anyvm ask

This means that my personal qube is automatically allowed to call the service on my work qube. Any qube can ask for permission to call the service on any other qube.

After shutting down your template qube and restarting your qubes that depend on it, you can test the service by running

$ my-open-in-vm targetvm file:///path/to/file

in a qube of your choice.

2.2. Autocompletion of file paths

The following qrexec service returns a list of possible auto completions for a given path. It consists of the following parts.



read args
output="$(ls -d -p $args*)"
echo "$status"
echo "$output"



echo $@
read status
if [ "$status" -ne 0 ]; then
    exit $status
    exec cat >&$SAVED_FD_1




qrexec-client-vm "$qube" my.Ls /usr/bin/my-ls-client $args

You can test the service by running

$ my-ls targetvm /path/to/compl

in a qube of your choice. Don't forget that you need to create a qrexec policy in dom0, as described in the previous section.

3. Emacs Configuration

Finally, we need to teach Emacs to use these qrexec services. The following function simply calls the completion qrexec service.

(defun my-qubes-list-files (qube &optional dir)
  (condition-case nil
      (process-lines "/usr/bin/my-ls" qube dir)
    (error nil)))

Next, I created a completion function as required by completing-read (see the Emacs lisp documentation on completion in minibuffers if you're interested).

(defun my-qubes-completion-function (qube str &optional predicate flag)
  (let ((files (or (my-qubes-list-files
                    qube str)
                    qube (replace-regexp-in-string "[^/]*$" "" str)))))
    (cond ((eq flag nil)
           (try-completion str files predicate))
          ((eq flag t)
           (all-completions str files predicate))
          ((eq flag 'lambda)
           (test-completion str files predicate))
          ((eq flag 'metadata)
           '(metadata . nil))
          ((listp flag)
           (completion-boundaries str files predicate (cdr flag)))

Next, org-link-parameters needs a function that takes care of calling completing-read (obtaining the input from the user) and returning the link. The following function first asks the user for the target qube and then prompts them for the file, with autocompletion enabled.

(defun my-qubes-link-completion ()
  (let ((qube (read-from-minibuffer "Qube: ")))
    (concat "qubes:" qube ":"
             "File: "
             (lambda (str predicate flag)
               (my-qubes-completion-function qube str predicate flag))
             nil nil "/home/user/"))))

Finally, org-link-parameters also needs a function that specifies what happens when following (i. e. clicking on) the link. This simply calls our my-open-in-vm service.

(defun my-qubes-follow-link (path _)
  (let ((args (split-string path ":"))
        (async-shell-command-buffer "rename-buffer")
        (display-buffer-alist '((t display-buffer-no-window))))
    (async-shell-command (concat "my-open-in-vm " (car args)
                                 " file://" (cadr args))

Now that I had everything I need, I could specify the new qubes org link type.

(org-link-set-parameters "qubes"
                         :follow #'my-qubes-follow-link
                         :complete #'my-qubes-link-completion)

Links take the form qubes:targetvm:/path/to/file. Everything works as intended as far as I can tell. The only annoyance is that the qrexec service for opening the file only terminates when you exit the program with which the file is being viewed. Thus, upon trying to close Emacs, it may warn you that there is still an active process running. If anyone knows a solution for this, feel free to contact me and I will update this blog post accordingly.