GitHub Security Advisory: GHSA-fm2x-c5qw-4h6f
Package: github.com/canonical/lxd (GO)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-184: Incomplete List of Disallowed Inputs
Vulnerable versions: >= 0.0.0[PII_REDACTED]3314-538ac3df036e, <= 0.0.0[PII_REDACTED]5519-736f34afb267
Patched in: unpatched

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

Details:
## Summary

The `isVMLowLevelOptionForbidden` function in `lxd/project/limits/permissions.go` is missing `raw.apparmor` and `raw.qemu.conf` from its hardcoded forbidden list. A user with `can_edit` permission on a VM instance in a restricted project can combine these two omissions to bridge the LXD unix socket into the guest VM and gain full cluster administrator access. This bypasses the `restricted.virtual-machines.lowlevel=block` project restriction, which is the security control specifically designed to prevent raw config injection.

## Details

### Affected code

The enforcement point for VM lowlevel restrictions is `isVMLowLevelOptionForbidden` at `lxd/project/limits/permissions.go:924-926`:

```go
func isVMLowLevelOptionForbidden(key string) bool {
    return slices.Contains([]string{"boot.host_shutdown_timeout", "limits.memory.hugepages", "raw.idmap", "raw.qemu"}, key)
}
```

This list is missing two security-critical config keys:

- **`raw.apparmor`** -- allows injecting arbitrary AppArmor rules into the QEMU process confinement profile
- **`raw.qemu.conf`** -- allows injecting arbitrary sections into the QEMU configuration file

The container equivalent (`isContainerLowLevelOptionForbidden` at line 916) correctly includes `raw.apparmor` in its forbidden list.

### Attack mechanism

Both `raw.apparmor` and `raw.qemu.conf` are valid VM config keys (defined in `lxd/instance/instancetype/instance.go`). When a restricted user sets them on a VM in a project with `restricted.virtual-machines.lowlevel=block`, the entity config checker at line 779 calls `isVMLowLevelOptionForbidden` for each key, which returns `false` for both. The config is accepted without error.

On VM startup:

1. `instanceProfile` (`lxd/apparmor/instance.go:150`) reads `raw.apparmor` from the expanded config and injects it verbatim into the QEMU AppArmor profile template (`lxd/apparmor/instance_qemu.go:114-118`). An attacker-supplied rule like `/var/snap/lxd/common/lxd/unix.socket rw,` grants the QEMU process read-write access to the LXD unix socket.

2. `qemuRawCfgOverride` (`lxd/instance/drivers/driver_qemu_config_override.go:242`) reads `raw.qemu.conf` and appends new sections to the generated QEMU config.  The attacker adds a `[chardev]` section with `backend = "socket"` pointing at the LXD unix socket, and a `[device]` section creating a `virtserialport` connected to it.

3. QEMU starts with `-readconfig` containing the injected drive definition. The QEMU process connects to `/var/snap/lxd/common/lxd/unix.socket` (permitted by the injected AppArmor rule) and exposes the connection as `/dev/virtio-ports/lxd.exploit` inside the VM.

The exposed socket grants full administrative access to the entire LXD cluster, which can be used to create privileged containers, mount the host root filesystem, and escape to host root.

### Affected deployments

Any LXD deployment where:

- A project has `restricted=true` and `restricted.virtual-machines.lowlevel=block` (the default when `