The file bytes traveled away from Qubes OS's most trusted domain. The dangerous data traveled back.
In a bulletin dated August 28 and published August 29, 2026, the Qubes Security Team disclosed QSB-118, an arbitrary-command-injection vulnerability in qvm-copy-to-vm. If a user copied a file from dom0 to a qube an attacker had already compromised, that destination could return a crafted filename in the transfer's error confirmation. Dom0 then placed the name inside a command string and handed the string to a shell. The attacker had not copied a program into dom0; a field intended for an error dialog had become one.[1]
The impact was severe because dom0 administers the Qubes system. The trigger was narrower than the headline: an attacker first needed control of a qube, and the user then had to initiate qvm-copy-to-vm from dom0 to that same qube. The bulletin does not report exploitation in the wild, and this article is a postmortem of a disclosed code path, not a report that any user's machine was breached.[1]
That distinction is essential. So is the architectural lesson. Qubes is designed so that compromise of one security domain should leave other domains safe.[3] QSB-118 did not disprove compartmentalization. It exposed a privileged protocol endpoint that had been reasoned about as an outgoing file operation even though the protocol was bidirectional.
The cover photograph shows Qubes OS founder Joanna Rutkowska speaking at LinuxCon Europe in 2014.[8] It is historical context, not incident evidence. A decade later, QSB-118 is a useful example of the standard such a project sets for itself: publish the prerequisite, the exact trusted path, the affected range, and the repair. That precision lets defenders respond without either minimizing the flaw or turning a possible route to dom0 into an unsupported claim of compromise.
The return path hid inside a confirmation
Qubes keeps dom0 deliberately spare. Its documentation calls it a “thin trusted terminal,” advises against copying material into it, and provides qvm-copy-to-vm <target_vm> <file> as the simple way to send a file in the safer-looking direction—from dom0 to a qube. The destination stores it under /home/user/QubesIncoming/dom0/.[4]
That direction describes the payload, not the whole conversation. qvm-copy-to-vm uses qfile, a small archive protocol. After receiving the data, the target sends the source a confirmation containing a checksum, an error code, and the name of the last file received. If the target reports an error, dom0 incorporates that returned name into a GUI message.[1]
For an honest peer, the field explains which file failed. For a compromised peer, it is attacker-controlled input crossing from a qube into dom0.
The vulnerable path had two stages:
wait_for_result()passed the returned name throughsanitize_remote_filename(). The function replaced control or non-ASCII characters and double quotation marks, but it retained other printable shell metacharacters.- On an error,
call_error_handler()led throughgui_fatal()to dom0'sdisplay_error(). That function constructed a command forkdialogorzenity, interpolated the message into it, and invokedsystem().[1]
The first stage made the string suitable for display, not safe for shell interpretation. In particular, the command surrounded the message with single quotes while the sanitizer did not remove single quotes. More generally, printable ASCII contains characters with grammar inside a shell. Once system() parsed the assembled text, the target's filename was no longer guaranteed to remain one inert argument.[1][2]
This was not a failure of the file archive parser accepting a complex object. It was a failure in the reporting path after that parser had something bad to report. Negative paths often receive less scrutiny than successful data handling, yet they execute at exactly the moment a hostile peer has supplied malformed state.
Copy direction was the wrong trust model
There is a sensible history behind qfile. Qubes once moved files using a block device with a filesystem on it. That forced the receiving VM's kernel to parse partition-table and filesystem metadata controlled by a potentially untrusted sender. The project replaced that design with a small qrexec-based protocol and a constrained unpacker; modern transfers stream through standard input and output instead of attaching an attacker-shaped disk image.[5]
QSB-118 sits one layer beyond that improvement. The content channel was narrower, but the exchange still needed control messages. A checksum can tell the source whether the parties saw the same bytes. It cannot make a filename supplied by the destination trustworthy. A user gesture can authorize a transfer. It cannot convert every reply from the selected qube into dom0 data.
The useful boundary is therefore not “files can leave dom0 but cannot enter.” It is:
- file content flows from the source to the destination;
- acknowledgements, checksums, status codes, and diagnostic text flow from the destination to the source;
- every field is untrusted at the endpoint with more authority, regardless of which side initiated the operation.
That last rule catches what a one-arrow data-flow sketch misses. Error details, log labels, progress messages, notification titles, and peer-reported resource names are all inputs. Their benign purpose does not lower the sender's privilege.
The Qubes security goal is strong isolation between domains, not security isolation between applications inside one domain.[3] Once a qube is compromised, the attacker can speak as that qube. The job of the dom0 endpoint is to treat every byte on that side of the boundary accordingly. QSB-118 required a user to open a particular channel, but the user's intent was “copy this file out,” not “let the target compose a dom0 shell program.”
Version 4.3.22 removed the interpreter
The repair did not attempt a longer blacklist of shell punctuation. Commit 1a7840a removed system() from file-copy-vm/qfile-dom0-agent.c. The new code formats the human-readable message into a buffer, calls fork(), and launches kdialog or zenity with execlp(). The dialog text is passed as one argument rather than concatenated into source code for a shell.[2]
That is the important semantic change. With an argument-vector API, punctuation inside the message remains punctuation inside one argument. There is no shell grammar in which it can acquire a second meaning. Display sanitization may still make diagnostics legible, but it no longer carries the impossible burden of proving that arbitrary text is safe in every shell context.
The project already had a comparison point: the error-reporting implementation used inside a VM launched its dialog with fork() and execlp() rather than system(), so the bulletin says that variant was not affected. The dom0 patch adopted the same safer process boundary.[1][2]
The bulletin marked all Qubes OS releases as affected and named qubes-core-dom0-linux 4.3.22 as the fixed package for Qubes 4.3. Its user instruction was to continue updating normally; when the bulletin was issued, the package was scheduled to move from security-testing to the stable repository after a short community test period.[1] That timing matters: the bulletin establishes the fixed version, not permission to install an unverified artifact or to assume that an end-of-life release will receive an equivalent package.
High consequence, narrow preconditions
An accurate response keeps impact and reachability separate.
If the chain completed, command execution occurred in dom0 and could hand the attacker control of Qubes OS.[1] That is a system-level consequence, not merely a broken copy dialog. But a vulnerable installation was not enough on its own. The documented route required all of the following:
- a qube under attacker control;
- a user-initiated copy from dom0 to that qube;
- a malicious error confirmation reaching the vulnerable dom0 reporter;
- an affected version of the dom0 package.[1]
Routine qube-to-qube transfers did not inherit this exact system() path, according to the advisory.[1] Nor does the bulletin say an attacker could start the dom0 copy remotely without the user's action. Those constraints should drive triage: update every supported system, then investigate especially where people have used qvm-copy-to-vm toward qubes that may have been hostile.
For a suspicious historical transfer, patching prevents a repeat; it does not answer whether the older event succeeded. Preserve the relevant dom0 and qube logs before routine retention removes them, record package versions, identify the selected destination and file, and treat unexplained dom0-side effects as an incident. At the same time, do not infer compromise merely because the vulnerable package was present. Publicly documented capability is evidence of risk, not evidence that a specific machine was used.
Audit the replies, then audit the launch sites
The first follow-up audit is protocol-shaped. For every operation that crosses from a less-trusted component to a privileged one, enumerate both directions, including the messages that occur only on rejection, timeout, partial completion, or cleanup. Mark the origin of each filename, error string, identifier, count, and status object at the point of use—not at the point where the overall operation began.
The second audit is execution-shaped. Search privileged components for system(), popen(), sh -c, and constructed command strings. The question is not whether developers remembered the right escape function. It is whether a shell is necessary at all. When a program and its options are known, use an execution API that accepts a fixed executable and a distinct argument vector. If a shell really is the product requirement, isolate the fixed script and strictly type or allowlist every variable that crosses into it.
Then test the failures. A destination harness should return filenames containing whitespace, quotes, expansion syntax, leading hyphens, invalid encodings, maximum-length text, and terminal control characters across every error code. The assertion is not just “the dialog appeared.” It is that exactly one expected executable started, each option stayed in its intended argument slot, the message was displayed as data, and no other effect occurred. Such tests would exercise the path that ordinary successful-copy coverage never reaches.
Finally, keep the larger record in proportion. A July 2026 analysis counted 109 Qubes security bulletins through 2025 and attributed 87—79.8 percent—primarily to Xen, CPUs or microarchitecture, and other upstream components rather than Qubes-core logic. The author also cautions that the public bulletin record measures disclosed advisories, not latent vulnerability incidence.[7] QSB-118 is notable precisely because it is a Qubes-core boundary error; it should not be generalized into a claim that every isolation mechanism failed.
Independent operational accounts show why the distinction matters. The Guardian's engineering team described using Qubes to build an offline, resettable environment for handling sensitive document workflows, while explicitly presenting its setup as one organization's experience rather than universal guidance.[6] The article predates QSB-118 and does not establish exposure to it. It does show the kind of high-consequence work in which clear preconditions, prompt updates, and honest trust-boundary analysis are more useful than either security theater or panic.
QSB-118's durable lesson fits in one sentence: data-flow direction is not authority-flow direction. A file went out of dom0. A diagnostic came back. The vulnerability lived in the moment privileged code forgot that the second trip crossed the boundary too.
Sources
- Qubes Security Team, “QSB-118: Dom0 arbitrary code execution in qvm-copy-to-vm error reporting” — prerequisites, qfile confirmation fields, vulnerable call path, affected releases, patching, and credit.
- Marek Marczykowski-Górecki, “Do not use system() with untrusted parts of the command,” QubesOS/qubes-core-admin-linux commit
1a7840a— the dom0 error-dialog patch from command strings tofork()andexeclp(). - Qubes OS documentation, “Security design goals” — compartmentalization objective, domain boundary, and inter-domain features.
- Qubes OS documentation, “How to copy from dom0” —
qvm-copy-to-vm, destination path, and dom0's thin-trusted-terminal boundary. - Qubes OS documentation, “Inter-qube file copying (qfilecopy)” — the move from block devices to qrexec, parser rationale, and constrained destination.
- The Guardian Engineering Blog, “When security matters: working with Qubes OS at the Guardian” — an independent operational account of an offline, resettable Qubes workflow.
- Alfonso De Gregorio, “Qubes OS Security in the Public Record,” arXiv:2607.14587 — independent analysis of 2011–2025 QSB attribution and explicit limits of advisory-count evidence.
- Krd, “LinuxCon Europe Joanna Rutkowska 03,” Wikimedia Commons — creator, date, event description, original dimensions, and CC BY-SA 4.0 license for the archival photograph.