GitHub Security Advisory: GHSA-q96j-3fmm-7fv4
Package: github.com/canonical/lxd (GO)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-20: Improper Input Validation
Vulnerable versions: >= 0.0.0[PII_REDACTED]3314-538ac3df036e, <= 0.0.0[PII_REDACTED]5519-736f34afb267
Patched in: unpatched

Summary: LXD: Importing a crafted backup leads to project restriction bypass

Details:
## Summary

LXD instance backup import validates project restrictions against `backup/index.yaml` embedded in the tar archive, but creates the actual instance from `backup/container/backup.yaml` extracted to the storage volume. Because these are separate, independently attacker-controlled files within the same tar archive, an attacker with instance-creation rights in a restricted project can craft a backup where `index.yaml` contains clean configuration (passing all restriction checks) while `backup.yaml` contains `security.privileged=true`, `raw.lxc` host filesystem mounts, and restricted device types. The instance is created from the unchecked `backup.yaml`, bypassing all project restriction enforcement.

## Details

LXD projects support a `restricted=true` mode that enforces security boundaries on what instances within the project can do. These restrictions include blocking `security.privileged=true` containers, `raw.lxc` / `raw.apparmor` overrides, and device passthrough (GPU, USB, PCI, unix-char). These restrictions are intended to prevent container escape vectors regardless of user privilege level within the project.

The backup import path has two distinct configuration sources within a single tar archive:

1. `backup/index.yaml` - A quick-access metadata file read by `backup.GetInfo()` at `backup/backup_info.go:68`. This is the config checked against project restrictions.
2. `backup/container/backup.yaml` - The full instance configuration extracted to the storage volume and used for actual instance creation at `api_internal.go:784`.

The vulnerability exists because:

1. `AllowInstanceCreation()` at `instances_post.go:885` validates project restrictions using only `bInfo.Config` from `index.yaml`.

2. The tar contents (including `backup/container/backup.yaml`) are extracted to the storage volume at `generic_vfs.go:952` via `unpackVolume()`.

3. `UpdateInstanceConfig()` at `backup_config_utils.go:236` reads `backup.yaml` from storage but only syncs `Name`, `Project`, pool info, and volume UUIDs - it does not overwrite `Instance.Config` or `Instance.Devices`.

4. `internalImportFromBackup()` at `api_internal.go:784` reads `backup.yaml` from the storage mount path (not `index.yaml`) to build the instance database record.

5. `instance.CreateInternal()` at `api_internal.go:946` creates the instance using the config from `backup.yaml`. `CreateInternal` calls `ValidConfig` which validates config key **format** only, not project restriction compliance.


## Proof of Concept

### Environment setup (server admin)

These steps are performed by the LXD server administrator to set up the
restricted project and grant access to the user. This represents the normal
multi-tenant configuration that the exploit targets.

```bash
# Create a restricted project
lxc project create restricted-project \
  -c features.images=false \
  -c features.profiles=true \
  -c restricted=true

# Create a default profile with a root disk in the restricted project
lxc profil