Package: release.debian.org
Severity: normal
Tags: trixie
User: release.debian.org@packages.debian.org
Usertags: pu
X-Debbugs-Cc: gibmat@debian.org, team@security.debian.org
Control: affects -1 + src:lxd
[ Reason ]
While investigating CVE-2025-64507, I discovered that filesystem ID
mapping is broken for LXD in trixie. This renders the attack
unexploitable (yay!) but is a regression for anyone who wants/needs to
setup filesystem ID mapping.
After discussion with the Security Team, I have prepared this updated
version of LXD that fixes both the ID mapping and relevant CVE. Please
note that is NOT being treated as a security update for trixie and it's
fine to be included in the 13.3 point release.
[ Impact ]
Users cannot currently configure filesystem ID mapping for their
containers/VMs.
[ Tests ]
I have manually tested creating containers with an ID-shifted storage
volume and verified it works as expected and that the mitigation for
CVE-2025-64507 prevents the attack.
[ Risks ]
Minor/none -- the regression fix has been in the stable-5.0 branch for
a year and a half, and the CVE has been applied on all upstream
supported LXD/Incus branches.
[ Checklist ]
[*] *all* changes are documented in the d/changelog
[*] I reviewed all changes and I approve them
[*] attach debdiff against the package in (old)stable
[ ] the issue is verified as fixed in unstable
NOTE: lxd was RM'ed from unstable, so there is no fix to
apply there
[ Changes ]
Cherry-pick relevant fixes Canonical's stable-5.0 branch.
[ Other info ]
The source debdiff is attached.
diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/changelog lxd-5.0.2+git20231211.1364ae4/debian/changelog
--- lxd-5.0.2+git20231211.1364ae4/debian/changelog 2025-10-02 16:23:38.000000000 +0000
+++ lxd-5.0.2+git20231211.1364ae4/debian/changelog 2025-11-11 15:25:08.000000000 +0000
@@ -1,3 +1,10 @@
+lxd (5.0.2+git20231211.1364ae4-9+deb13u2) trixie; urgency=medium
+
+ * Cherry-pick upstream fix for broken idmapping with kernel 6.9+
+ * Cherry-pick upstream fix for CVE-2025-64507 / GHSA-56mx-8g9f-5crf
+
+ -- Mathias Gibbens <gibmat@debian.org> Tue, 11 Nov 2025 15:25:08 +0000
+
lxd (5.0.2+git20231211.1364ae4-9+deb13u1) trixie-security; urgency=high
* Backport fixes for the following security issues that are unfixed by
diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/013-cherry-pick-fix-idmapping.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/013-cherry-pick-fix-idmapping.patch
--- lxd-5.0.2+git20231211.1364ae4/debian/patches/013-cherry-pick-fix-idmapping.patch 1970-01-01 00:00:00.000000000 +0000
+++ lxd-5.0.2+git20231211.1364ae4/debian/patches/013-cherry-pick-fix-idmapping.patch 2025-11-11 15:25:08.000000000 +0000
@@ -0,0 +1,111 @@
+From 891b129ab6dde6c63eb7cdadbbd746419d2f6d26 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber@stgraber.org>
+Date: Wed, 22 May 2024 13:29:18 -0400
+Subject: [PATCH] shared/idmap: Make get_userns_fd configure the userns
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
+(cherry picked from commit ec223f75e7056f271fb84be56980243cd68e67b3)
+Signed-off-by: Alexander Mikhalitsyn <aleksandr.mikhalitsyn@canonical.com>
+License: Apache-2.0
+---
+ lxd/idmap/shift_linux.go | 68 +++++++++++++++++++++++++++++++++++++---
+ 1 file changed, 64 insertions(+), 4 deletions(-)
+
+diff --git a/shared/idmap/shift_linux.go b/shared/idmap/shift_linux.go
+index 024c55fa819b..d8fbff31101e 100644
+--- a/shared/idmap/shift_linux.go
++++ b/shared/idmap/shift_linux.go
+@@ -279,19 +279,78 @@ static int get_userns_fd_cb(void *data)
+
+ static int get_userns_fd(void)
+ {
+- int ret;
++ int userns_fd = -EBADF;
++ int file_fd = -EBADF;
+ pid_t pid;
+ char path[256];
+
++ // Create the namespace.
+ pid = do_clone(get_userns_fd_cb, NULL, CLONE_NEWUSER);
+ if (pid < 0)
+- return -errno;
++ goto err;
+
++ // Fetch a reference.
+ snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
+- ret = open(path, O_RDONLY | O_CLOEXEC);
++ userns_fd = open(path, O_RDONLY | O_CLOEXEC);
++ if (userns_fd < 0)
++ goto err_process;
++
++ // Setup uid_map
++ snprintf(path, sizeof(path), "/proc/%d/uid_map", pid);
++ file_fd = openat(AT_FDCWD, path, O_WRONLY);
++ if (file_fd < 0)
++ goto err_process;
++
++ if (write(file_fd, "0 0 1", 5) != 5)
++ goto err_process;
++
++ if (close(file_fd) < 0) {
++ file_fd = -EBADF;
++ goto err_process;
++ }
++
++ // Setup setgroups
++ snprintf(path, sizeof(path), "/proc/%d/setgroups", pid);
++ file_fd = openat(AT_FDCWD, path, O_WRONLY);
++ if (file_fd < 0)
++ goto err_process;
++
++ if (write(file_fd, "deny", 4) != 4)
++ goto err_process;
++
++ if (close(file_fd) < 0) {
++ file_fd = -EBADF;
++ goto err_process;
++ }
++
++ // Setup gid_map
++ snprintf(path, sizeof(path), "/proc/%d/gid_map", pid);
++ file_fd = openat(AT_FDCWD, path, O_WRONLY);
++ if (file_fd < 0)
++ goto err_process;
++
++ if (write(file_fd, "0 0 1", 5) != 5)
++ goto err_process;
++
++ if (close(file_fd) < 0) {
++ file_fd = -EBADF;
++ goto err_process;
++ }
++
++ // Kill the temporary process.
++ kill(pid, SIGKILL);
++ wait_for_pid(pid);
++
++ return userns_fd;
++
++err_process:
+ kill(pid, SIGKILL);
+ wait_for_pid(pid);
+- return ret;
++
++err:
++ close(userns_fd);
++ close(file_fd);
++ return -1;
+ }
+
+ static int create_detached_idmapped_mount(const char *path, const char *fstype)
+@@ -335,6 +394,7 @@ static int create_detached_idmapped_mount(const char *path, const char *fstype)
+ if (ret < 0)
+ return -errno;
+
++ close(fd_userns);
+ return 0;
+ }
+ */
diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/104-GHSA-56mx-8g9f-5crf.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/104-GHSA-56mx-8g9f-5crf.patch
--- lxd-5.0.2+git20231211.1364ae4/debian/patches/104-GHSA-56mx-8g9f-5crf.patch 1970-01-01 00:00:00.000000000 +0000
+++ lxd-5.0.2+git20231211.1364ae4/debian/patches/104-GHSA-56mx-8g9f-5crf.patch 2025-11-11 15:25:08.000000000 +0000
@@ -0,0 +1,245 @@
+From 2236fb7001bec8a4dd6a9070eb59d2cc818f833c Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber@stgraber.org>
+Date: Sun, 9 Nov 2025 18:41:24 -0500
+Subject: [PATCH 1/5] lxd/storage: Tighten storage pool volume permissions
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Related to https://github.com/lxc/incus/issues/2641
+
+Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
+(cherry picked from commit b0c6c0bac42c6ac27d536984cc043a6ec02b9e7c)
+Signed-off-by: Thomas Parrott <thomas.parrott@canonical.com>
+License: Apache-2.0
+(cherry picked from commit 7598d5ab710e05829c7bc4a6e30106a022f376c1)
+(cherry picked from commit 049d86def7c26e8736bb991e4223ec89dab0b05e)
+---
+ lxd/storage/backend_lxd.go | 4 ++--
+ lxd/storage/drivers/driver_btrfs.go | 2 +-
+ lxd/storage/drivers/driver_zfs_utils.go | 3 +--
+ lxd/storage/drivers/generic_vfs.go | 4 ++--
+ lxd/storage/drivers/volume.go | 17 +++++++++++------
+ 5 files changed, 17 insertions(+), 13 deletions(-)
+
+diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go
+index 76be2843343b..7da6aaf631df 100644
+--- a/lxd/storage/backend_lxd.go
++++ b/lxd/storage/backend_lxd.go
+@@ -5116,9 +5116,9 @@ func (b *lxdBackend) RestoreCustomVolume(projectName, volName string, snapshotNa
+
+ func (b *lxdBackend) createStorageStructure(path string) error {
+ for _, volType := range b.driver.Info().VolumeTypes {
+- for _, name := range drivers.BaseDirectories[volType] {
++ for _, name := range drivers.BaseDirectories[volType].Paths {
+ path := filepath.Join(path, name)
+- err := os.MkdirAll(path, 0711)
++ err := os.MkdirAll(path, drivers.BaseDirectories[volType].Mode)
+ if err != nil && !os.IsExist(err) {
+ return fmt.Errorf("Failed to create directory %q: %w", path, err)
+ }
+diff --git a/lxd/storage/drivers/driver_btrfs.go b/lxd/storage/drivers/driver_btrfs.go
+index 89310350d3bb..1e8577043cc5 100644
+--- a/lxd/storage/drivers/driver_btrfs.go
++++ b/lxd/storage/drivers/driver_btrfs.go
+@@ -283,7 +283,7 @@ func (d *btrfs) Delete(op *operations.Operation) error {
+
+ // Delete potential intermediate btrfs subvolumes.
+ for _, volType := range d.Info().VolumeTypes {
+- for _, dir := range BaseDirectories[volType] {
++ for _, dir := range BaseDirectories[volType].Paths {
+ path := filepath.Join(GetPoolMountPath(d.name), dir)
+ if !shared.PathExists(path) {
+ continue
+diff --git a/lxd/storage/drivers/driver_zfs_utils.go b/lxd/storage/drivers/driver_zfs_utils.go
+index 0381c98be034..d72f5add2527 100644
+--- a/lxd/storage/drivers/driver_zfs_utils.go
++++ b/lxd/storage/drivers/driver_zfs_utils.go
+@@ -286,8 +286,7 @@ func (d *zfs) initialDatasets() []string {
+
+ // Iterate over the listed supported volume types.
+ for _, volType := range d.Info().VolumeTypes {
+- entries = append(entries, BaseDirectories[volType][0])
+- entries = append(entries, filepath.Join("deleted", BaseDirectories[volType][0]))
++ entries = append(entries, BaseDirectories[volType].Paths[0], "deleted/"+BaseDirectories[volType].Paths[0])
+ }
+
+ return entries
+diff --git a/lxd/storage/drivers/generic_vfs.go b/lxd/storage/drivers/generic_vfs.go
+index 03e729b4092f..7327a5b43487 100644
+--- a/lxd/storage/drivers/generic_vfs.go
++++ b/lxd/storage/drivers/generic_vfs.go
+@@ -1153,11 +1153,11 @@ func genericVFSListVolumes(d Driver) ([]Volume, error) {
+ poolMountPath := GetPoolMountPath(poolName)
+
+ for _, volType := range d.Info().VolumeTypes {
+- if len(BaseDirectories[volType]) < 1 {
++ if len(BaseDirectories[volType].Paths) < 1 {
+ return nil, fmt.Errorf("Cannot get base directory name for volume type %q", volType)
+ }
+
+- volTypePath := filepath.Join(poolMountPath, BaseDirectories[volType][0])
++ volTypePath := filepath.Join(poolMountPath, BaseDirectories[volType].Paths[0])
+ ents, err := os.ReadDir(volTypePath)
+ if err != nil {
+ return nil, fmt.Errorf("Failed to list directory %q for volume type %q: %w", volTypePath, volType, err)
+diff --git a/lxd/storage/drivers/volume.go b/lxd/storage/drivers/volume.go
+index 96ee6ffcfa11..c2188b826004 100644
+--- a/lxd/storage/drivers/volume.go
++++ b/lxd/storage/drivers/volume.go
+@@ -76,13 +76,18 @@ const ContentTypeISO = ContentType("iso")
+ // VolumePostHook function returned from a storage action that should be run later to complete the action.
+ type VolumePostHook func(vol Volume) error
+
++type baseDirectory struct {
++ Paths []string
++ Mode os.FileMode
++}
++
+ // BaseDirectories maps volume types to the expected directories.
+-var BaseDirectories = map[VolumeType][]string{
+- VolumeTypeBucket: {"buckets"},
+- VolumeTypeContainer: {"containers", "containers-snapshots"},
+- VolumeTypeCustom: {"custom", "custom-snapshots"},
+- VolumeTypeImage: {"images"},
+- VolumeTypeVM: {"virtual-machines", "virtual-machines-snapshots"},
++var BaseDirectories = map[VolumeType]baseDirectory{
++ VolumeTypeBucket: {Paths: []string{"buckets"}, Mode: 0o711},
++ VolumeTypeContainer: {Paths: []string{"containers", "containers-snapshots"}, Mode: 0o711},
++ VolumeTypeCustom: {Paths: []string{"custom", "custom-snapshots"}, Mode: 0o700},
++ VolumeTypeImage: {Paths: []string{"images"}, Mode: 0o700},
++ VolumeTypeVM: {Paths: []string{"virtual-machines", "virtual-machines-snapshots"}, Mode: 0o700},
+ }
+
+ // Volume represents a storage volume, and provides functions to mount and unmount it.
+
+From 54eca5752e47bd9443f43850ca8241074de49609 Mon Sep 17 00:00:00 2001
+From: Thomas Parrott <thomas.parrott@canonical.com>
+Date: Mon, 10 Nov 2025 10:49:26 +0000
+Subject: [PATCH 2/5] lxd/storage/drivers/volume: Add comments explaining
+ differences in BaseDirectories permissions
+
+Signed-off-by: Thomas Parrott <thomas.parrott@canonical.com>
+(cherry picked from commit 52a551c8f0452eda858ac7ba27dc250d84ce72bf)
+(cherry picked from commit 10ad9d7419b4127cd127d2850e79aee3e1dc07e1)
+---
+ lxd/storage/drivers/volume.go | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/lxd/storage/drivers/volume.go b/lxd/storage/drivers/volume.go
+index c2188b826004..3ef75f98c671 100644
+--- a/lxd/storage/drivers/volume.go
++++ b/lxd/storage/drivers/volume.go
+@@ -83,8 +83,8 @@ type baseDirectory struct {
+
+ // BaseDirectories maps volume types to the expected directories.
+ var BaseDirectories = map[VolumeType]baseDirectory{
+- VolumeTypeBucket: {Paths: []string{"buckets"}, Mode: 0o711},
+- VolumeTypeContainer: {Paths: []string{"containers", "containers-snapshots"}, Mode: 0o711},
++ VolumeTypeBucket: {Paths: []string{"buckets"}, Mode: 0o711}, // MinIO is run as non-root, so 0700 won't work, however as S3 interface doesn't allow creation of setuid binaries this is OK.
++ VolumeTypeContainer: {Paths: []string{"containers", "containers-snapshots"}, Mode: 0o711}, // Containers may be run as non-root, so 0700 won't work, however as containers have their own sub-directory with correct ownership that is 0100 this is OK.
+ VolumeTypeCustom: {Paths: []string{"custom", "custom-snapshots"}, Mode: 0o700},
+ VolumeTypeImage: {Paths: []string{"images"}, Mode: 0o700},
+ VolumeTypeVM: {Paths: []string{"virtual-machines", "virtual-machines-snapshots"}, Mode: 0o700},
+
+From 693c65f114b7ab94f896e38f3deaa3b53a48c6e1 Mon Sep 17 00:00:00 2001
+From: Thomas Parrott <thomas.parrott@canonical.com>
+Date: Mon, 10 Nov 2025 09:35:28 +0000
+Subject: [PATCH 3/5] lxd/storage/backend/lxd: Replace deprecated os.IsExist
+
+Signed-off-by: Thomas Parrott <thomas.parrott@canonical.com>
+(cherry picked from commit 89c0906e5107fa059263490d020b99dfa1c9c26b)
+(cherry picked from commit 97219271d4c67e388e584944349a87f86647fcdb)
+---
+ lxd/storage/backend_lxd.go | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go
+index 7da6aaf631df..f76847380c6e 100644
+--- a/lxd/storage/backend_lxd.go
++++ b/lxd/storage/backend_lxd.go
+@@ -5119,7 +5119,7 @@ func (b *lxdBackend) createStorageStructure(path string) error {
+ for _, name := range drivers.BaseDirectories[volType].Paths {
+ path := filepath.Join(path, name)
+ err := os.MkdirAll(path, drivers.BaseDirectories[volType].Mode)
+- if err != nil && !os.IsExist(err) {
++ if err != nil && !errors.Is(err, fs.ErrExist) {
+ return fmt.Errorf("Failed to create directory %q: %w", path, err)
+ }
+ }
+
+From 849ed20a5fbb5229efec24c98ca3caee608fd87d Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber@stgraber.org>
+Date: Sun, 9 Nov 2025 18:41:39 -0500
+Subject: [PATCH 4/5] lxd/patches: Re-apply storage permissions on update
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Related to https://github.com/lxc/incus/issues/2641
+
+Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
+(cherry picked from commit 3abdc12cf6a8dce391d28d340a32c137125357dd)
+Signed-off-by: Thomas Parrott <thomas.parrott@canonical.com>
+License: Apache-2.0
+(cherry picked from commit 87a34bbe6bd3f918081db431ac1a6ee22346f172)
+(cherry picked from commit b2d38e82bda982e337a3cbdc173ec4b372ab8b24)
+---
+ lxd/patches.go | 32 ++++++++++++++++++++++++++++++++
+ 1 file changed, 32 insertions(+)
+
+diff --git a/lxd/patches.go b/lxd/patches.go
+index 01ad6926aed1..b4559b13fde0 100644
+--- a/lxd/patches.go
++++ b/lxd/patches.go
+@@ -4,6 +4,7 @@ import (
+ "context"
+ "errors"
+ "fmt"
++ "io/fs"
+ "net/http"
+ "os"
+ "path/filepath"
+@@ -83,6 +84,7 @@ var patches = []patch{
+ {name: "zfs_set_content_type_user_property", stage: patchPostDaemonStorage, run: patchZfsSetContentTypeUserProperty},
+ {name: "storage_zfs_unset_invalid_block_settings", stage: patchPostDaemonStorage, run: patchStorageZfsUnsetInvalidBlockSettings},
+ {name: "storage_zfs_unset_invalid_block_settings_v2", stage: patchPostDaemonStorage, run: patchStorageZfsUnsetInvalidBlockSettingsV2},
++ {name: "pool_fix_default_permissions", stage: patchPostDaemonStorage, run: patchDefaultStoragePermissions},
+ }
+
+ type patch struct {
+@@ -1148,4 +1149,34 @@ func patchStorageZfsUnsetInvalidBlockSettingsV2(_ string, d *Daemon) error {
+ return nil
+ }
+
++// patchDefaultStoragePermissions re-applies the default modes to all storage pools.
++func patchDefaultStoragePermissions(_ string, d *Daemon) error {
++ s := d.State()
++
++ pools, err := s.DB.Cluster.GetStoragePoolNames()
++ if err != nil {
++ // Skip the rest of the patch if no storage pools were found.
++ if api.StatusErrorCheck(err, http.StatusNotFound) {
++ return nil
++ }
++
++ return fmt.Errorf("Failed getting storage pool names: %w", err)
++ }
++
++ for _, pool := range pools {
++ for _, volEntry := range storageDrivers.BaseDirectories {
++ for _, volDir := range volEntry.Paths {
++ path := storageDrivers.GetPoolMountPath(pool) + "/" + volDir
++
++ err := os.Chmod(path, volEntry.Mode)
++ if err != nil && !errors.Is(err, fs.ErrNotExist) {
++ return fmt.Errorf("Failed to set directory mode %q: %w", path, err)
++ }
++ }
++ }
++ }
++
++ return nil
++}
++
+ // Patches end here
diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/series lxd-5.0.2+git20231211.1364ae4/debian/patches/series
--- lxd-5.0.2+git20231211.1364ae4/debian/patches/series 2025-10-02 16:23:38.000000000 +0000
+++ lxd-5.0.2+git20231211.1364ae4/debian/patches/series 2025-11-11 15:25:08.000000000 +0000
@@ -9,8 +9,10 @@
010-cherry-pick-update-test-cert.patch
011-newer-qemu-fixes.patch
012-fix-issues-with-old-nvram.patch
+013-cherry-pick-fix-idmapping.patch
100-CVE-2025-54293.patch
101-CVE-2025-54287.patch
102-CVE-2025-54288.patch
103a-CVE-2025-54286.patch
103b-CVE-2025-54286.patch
+104-GHSA-56mx-8g9f-5crf.patch
Attachment:
signature.asc
Description: This is a digitally signed message part