RC RANDOM CHAOS

Code rides the error channel

A compromised Qubes qube can inject commands into dom0 via the filename in a qvm-copy-to-vm error message. QSB-118 patches it in 4.3.22.

· 3 min read
Code rides the error channel

If you run qvm-copy-to-vm from dom0 to a qube and that qube is already compromised, the qube can execute an arbitrary command in dom0 by controlling the filename in the error message it sends back. Qubes patched this in QSB-118 on 28 August 2026 with qubes-core-dom0-linux version 4.3.22 for Qubes 4.3. Every Qubes OS release was affected. Tim C. discovered it.

The direction of the attack is the notable part. qvm-copy-to-vm moves a file out of dom0, the trusted base, into an untrusted qube. Data is only supposed to travel that way. The tool uses the qfile protocol, a stripped-down archive format with a confirmation step at the end: the receiving qube sends back a checksum, an error code, and the name of the last file it received. When the error code is non-zero, dom0 shows a GUI dialog containing that filename so the user knows which file failed. The filename is attacker-controlled, and that return path, not the copied data, is what carries the attack back across the boundary.

The code path is short. wait_for_result() runs the returned name through sanitize_remote_filename() before passing it to the error handler. That function walks the string and replaces any byte below 0x20, above 0x7e, or equal to " with an underscore. Control characters, non-ASCII bytes and double quotes are gone. Every other printable ASCII character survives, including $, backticks, semicolons, pipes and single quotes.

The sanitized name then reaches display_error(), the dom0 error reporter, which assembles a command with asprintf and runs it through system():

asprintf(&dialog_cmd, "%s '%s: %s (error type: %s)'",
         ret==0 ? KDIALOG_CMD : ZENITY_CMD,
         program_invocation_short_name, buf, strerror(errno));
...
system(dialog_cmd);

The message is wrapped in single quotes. The sanitizer strips double quotes, which protects the inner "%s" in the message template, but the shell quoting that actually matters here is the single quotes, and those pass through untouched. A single quote in the filename closes the quoted string and lets the rest of the name run as shell. system() hands the whole line to /bin/sh, so a crafted name executes in dom0.

The VM side of the same tool is not exploitable. Its error reporter forks and calls execlp() on zenity or kdialog directly, passing the message as a plain argument. No shell, no word-splitting, nothing to break out of. The dom0 code is the one variant that routes an attacker-supplied string through a shell.

To fire this, an attacker needs a qube already under their control and a user who copies a file from dom0 into that qube. Copying out of dom0 is a deliberate and relatively uncommon action, so this is not a remote drive-by. When it does fire, it is full dom0 code execution, which on Qubes means control of the entire system.

No special action is required beyond updating. The fixed qubes-core-dom0-linux 4.3.22 moves from the security-testing repository to stable after a short community testing period; install it through the Qubes Update tool once it lands.

Share

Keep Reading

Latest on the Wire

Full wire →

New signal daily · RSS

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.