Introduction
A validation discrepancy in Canonical LXD's backup import path allowed authenticated users to smuggle privileged container configurations past project restriction enforcement, achieving full host compromise from within a supposedly locked down project. For any organization running multi tenant LXD environments, this vulnerability (scored CVSS 9.1) effectively dissolved the security boundary that project restrictions were designed to enforce.
LXD is a system container and virtual machine manager written in Go, built on top of LXC, and maintained by Canonical. It is widely used in cloud infrastructure, development environments, and multi tenant hosting scenarios where project level isolation is a core security control. Canonical took full ownership of the LXD project in July 2023, and it ships as a snap package across multiple supported release tracks.
Technical Information
Root Cause: Dual Configuration Sources with No Integrity Linkage
LXD projects support a "restricted" mode designed to enforce security boundaries. In restricted projects, operations like creating privileged containers or passing through host devices are blocked regardless of the user's privilege level within that project. CVE-2026-34178 completely bypasses these protections due to a configuration synchronization failure during the backup import path.
A single LXD backup tar archive contains two distinct configuration sources:
backup/index.yaml: A metadata file read bybackup.GetInfo()during import validation.backup/container/backup.yaml: The full instance configuration that gets extracted to the storage volume and is used to actually build the instance.
The AllowInstanceCreation() function validates project restrictions using only the configuration parsed from index.yaml. However, the internalImportFromBackup function reads backup.yaml from the storage mount path to construct the instance database record and create the instance. Critically, this creation function only validates the format of configuration keys, not their compliance with project restrictions.
Because both files live inside the same attacker controlled tar archive with no integrity linkage between them, an attacker can construct an archive where index.yaml carries clean, restriction compliant configuration while backup.yaml smuggles in dangerous settings.
Attack Flow
Exploitation requires the attacker to hold can_view_instances, can_create_instances, and can_operate_instances permissions on the target project. These are standard permissions for any user expected to manage instances within a restricted project. The attack proceeds as follows:
- The attacker locally constructs a backup directory structure containing a minimal root filesystem.
- The attacker creates a benign
backup/index.yamlthat contains no restricted settings and will pass all project restriction checks. - The attacker creates a malicious
backup/container/backup.yamlcontainingsecurity.privileged: "true"andraw.lxcdirectives that bind mount the host LXD unix socket (/var/snap/lxd/common/lxd/unix.socket) into the container. - The attacker packages these files into a tar archive and imports it to the target LXD server, specifying the restricted project.
- The
AllowInstanceCreation()check passes because it only inspectsindex.yaml. - The instance is created from the unchecked
backup.yaml, resulting in a privileged container with a host socket mount. - The attacker starts the container and accesses the bind mounted LXD Unix socket. Local connections over this socket are trusted as full admin, granting the attacker unrestricted access across all projects on the host and full cluster admin privileges.
CVSS v3.1 Breakdown
The 9.1 Critical score reflects the following base metrics:
| Metric | Value | Operational Implication |
|---|---|---|
| Attack Vector | Network | Exploit delivered remotely via the LXD API during backup import |
| Attack Complexity | Low | No advanced timing or race conditions required |
| Privileges Required | High | Attacker must have instance creation rights in a project |
| User Interaction | None | No action required from the server administrator |
| Scope | Changed | Escape from the restricted project to the host system |
| Confidentiality | High | Full host compromise exposes all data on the cluster |
| Integrity | High | Full administrative control to modify any instance |
| Availability | High | Ability to disrupt or destroy any service on the host |
The combination of a network attack vector with changed scope makes this particularly severe for shared hosting and multi tenant deployments.
Patch Information
The fix for CVE-2026-34178 was merged via Pull Request #17921 ("Import: Create backup config from index") into the canonical/lxd repository on March 24, 2026, with merge commit 06637bfd. The patch was authored by contributor roosterfish and merged by tomponline (Tom Parrott, Canonical).
The patch eliminates the dual source inconsistency through a single source of truth strategy, making index.yaml the only configuration source, across 8 changed files with 199 additions and 116 deletions. Here is how each piece fits together:
The Critical Pivot: lxd/api_internal.go
The internalImportFromBackup function previously read instance config from the on disk backup.yaml file extracted to the storage volume. The patch changes its signature to accept a *backup.Info struct (populated from index.yaml) and directly uses it:
// Before: read untrusted backup.yaml from storage mount backupYamlPath := filepath.Join(instanceMountPoint, "backup.yaml") backupConf, err := backup.ParseConfigYamlFile(backupYamlPath) // After: use the already-validated index config backupConf := bInfo.Config
By never reading the separate backup.yaml for instance creation, the attacker controlled secondary file is completely cut out of the trust chain.
Eliminating the Disk Round Trip: lxd/backup/backup_config_utils.go
The old UpdateInstanceConfig function read backup.yaml from disk, selectively merged a few fields (Name, Project, pool info), then wrote it back. Critically, it did not overwrite Instance.Config or Instance.Devices, which are exactly the fields that carried the attacker's payload. The replacement function UpdateInstanceConfigInPlace operates entirely on the in memory Info struct and never touches backup.yaml on disk. Roughly 50 lines of file I/O and partial merge logic were removed.
Wiring Up the New Flow: lxd/instances_post.go
The createFromBackup function now calls UpdateInstanceConfigInPlace on the bInfo object before handing it to internalImportFromBackup. The full bInfo struct is passed instead of individual projectName and instName strings:
err = backup.UpdateInstanceConfigInPlace(s.DB.Cluster, bInfo) if err != nil { return response.SmartError(fmt.Errorf("Failed updating backup index file in place: %w", err)) } // ... err = internalImportFromBackup(ctx, s, bInfo, instanceName != "", devices)
Defense in Depth: Excluding backup.yaml from Exports
In lxd/storage/drivers/generic_vfs.go, new exports now explicitly skip backup.yaml from the tar archive. This means that going forward, backup tarballs will only contain index.yaml, eliminating the secondary config file as an attack surface entirely:
alwaysExcludedPaths := []string{ filepath.Join(mountPath, "backup.yaml"), }
Additional Changes
The backupWriteIndex function in lxd/backup.go was updated to include custom storage volume configuration in the index (previously omitted), so that index.yaml now carries enough information to fully reconstruct instance config without needing backup.yaml. The old update path in lxd/storage/backend_lxd.go that mounted the volume and called UpdateInstanceConfig on the on disk backup.yaml was deleted entirely.
A new integration test test_backup_inconsistent_config explicitly verifies that importing an archive where the index contains security.privileged: "true" in a restricted project is rejected, that a clean index succeeds, and that exported backups no longer contain backup/container/backup.yaml.
Patched Versions
Interim snap releases delivering the fix were published on March 30, 2026:
| LXD Series | Patched Version | Interim Snap Release |
|---|---|---|
| 6.x | 6.8 | 6.7-d814d89 |
| 5.21.x LTS | 5.21.5 | 5.21.4-aee7e08 |
| 5.0.x LTS | 5.0.7 | 5.0.6-7fc3b36 |
| 4.0.x LTS | N/A | 4.0.10-e92d947 |
Administrators should immediately refresh their snap installations to the interim releases or upgrade to the final patched versions.
Affected Systems and Versions
According to the GitHub Security Advisory, CVE-2026-34178 affects all LXD versions from 4.12 onward. Specifically:
- LXD 6.x series: all versions prior to 6.8
- LXD 5.21.x LTS series: all versions prior to 5.21.5
- LXD 5.0.x LTS series: all versions prior to 5.0.7
- LXD 4.x series: versions from 4.12 onward
The vulnerability is only exploitable in environments where LXD projects are configured with restricted mode enabled and where users have been granted instance creation permissions within those restricted projects. Single tenant deployments where all users already have full admin access are not meaningfully impacted, as those users already possess the privileges this vulnerability grants.
Vendor Security History
Canonical maintains a robust security tracking infrastructure, tracking all Common Vulnerabilities and Exposures affecting Ubuntu and its official packages. When security issues are resolved, Canonical developers issue Ubuntu Security Notices to inform the public. The prompt response to CVE-2026-34178, including the simultaneous release of interim snap builds across four different release tracks on March 30, 2026, demonstrates a mature security incident response capability. The detailed security advisory published on GitHub, including full CVSS scoring and remediation guidance, reflects Canonical's transparent approach to vulnerability disclosure.
References
- GitHub Security Advisory GHSA-q96j-3fmm-7fv4
- Pull Request #17921: Import: Create backup config from index
- Merge Commit 06637bfd3158e2457128703afcf60e286a6d6a61
- LXD 6.7 Interim Snap Release 6.7-d814d89
- LXD 5.21.4 LTS Interim Snap Release 5.21.4-aee7e08
- LXD 5.0.6 LTS Interim Snap Release 5.0.6-7fc3b36
- Ubuntu CVE Tracker
- LXD on ArchWiki



