Version in base suite: 5.0.2+git20231211.1364ae4-9+deb13u3 Version in overlay suite: 5.0.2+git20231211.1364ae4-9+deb13u5 Base version: lxd_5.0.2+git20231211.1364ae4-9+deb13u5 Target version: lxd_5.0.2+git20231211.1364ae4-9+deb13u6 Base file: /srv/ftp-master.debian.org/ftp/pool/main/l/lxd/lxd_5.0.2+git20231211.1364ae4-9+deb13u5.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/l/lxd/lxd_5.0.2+git20231211.1364ae4-9+deb13u6.dsc changelog | 11 ++ patches/113-CVE-2026-40197.patch | 36 +++++++++ patches/114-CVE-2026-40251.patch | 43 +++++++++++ patches/115-CVE-2026-41648.patch | 152 +++++++++++++++++++++++++++++++++++++++ patches/116-CVE-2026-41684.patch | 56 ++++++++++++++ patches/117-CVE-2026-41685.patch | 150 ++++++++++++++++++++++++++++++++++++++ patches/series | 5 + 7 files changed, 453 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpjbfx297s/lxd_5.0.2+git20231211.1364ae4-9+deb13u5.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpjbfx297s/lxd_5.0.2+git20231211.1364ae4-9+deb13u6.dsc: no acceptable signature found 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 2026-04-14 18:30:26.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/changelog 2026-05-01 23:29:36.000000000 +0000 @@ -1,3 +1,14 @@ +lxd (5.0.2+git20231211.1364ae4-9+deb13u6) trixie-security; urgency=high + + * Cherry-pick fixes for the following security issues (from Incus): + - CVE-2026-40197 / GHSA-r7w7-mmxr-47r9 + - CVE-2026-40251 / GHSA-4m88-wxj4-9qj6 + - CVE-2026-41648 / GHSA-67wx-r9xr-x75x + - CVE-2026-41684 / GHSA-x5r6-jr56-89pv + - CVE-2026-41685 / GHSA-98vh-x9cx-9cfp + + -- Mathias Gibbens Fri, 01 May 2026 23:29:36 +0000 + lxd (5.0.2+git20231211.1364ae4-9+deb13u5) trixie-security; urgency=high * Cherry-pick fixes for the following security issues: diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/113-CVE-2026-40197.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/113-CVE-2026-40197.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/113-CVE-2026-40197.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/113-CVE-2026-40197.patch 2026-05-01 23:01:04.000000000 +0000 @@ -0,0 +1,36 @@ +From 985a1dedf9f3e7ba729c93b654905ed510de25c2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Thu, 9 Apr 2026 22:16:35 -0400 +Subject: [PATCH] incusd/storage/volume: Validate snapshot entries on import +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The volume import logic was missing some validation of the snapshot data +provided through the index. A nil snapshot entry would trigger a nil +pointer dereference causing the Incus daemon to crash. + +This addresses CVE-2026-40197 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/storage/backend_lxd.go | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go +index 92ae9462f..e9785373c 100644 +--- a/lxd/storage/backend_lxd.go ++++ b/lxd/storage/backend_lxd.go +@@ -5732,6 +5732,10 @@ func (b *lxdBackend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData + + // Create database entries fro new storage volume snapshots. + for _, s := range srcBackup.Config.VolumeSnapshots { ++ if s == nil { ++ return fmt.Errorf("Bad snapshot definition found in index") ++ } ++ + snapshot := s // Local var for revert. + snapName := snapshot.Name + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/114-CVE-2026-40251.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/114-CVE-2026-40251.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/114-CVE-2026-40251.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/114-CVE-2026-40251.patch 2026-05-01 23:29:36.000000000 +0000 @@ -0,0 +1,43 @@ +From afcf707e0e9dfcfcf0f644069801cfeb439565a8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 10 Apr 2026 01:37:45 -0400 +Subject: [PATCH] incusd/storage/instance: Fix bad snapshot index calculation +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The logic used to check if we have additional volume configuration +matching a backup was doing incorrect index math which could lead to +crashes when fed a bad index file. + +This addresses CVE-2026-40251 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/storage/backend_lxd.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go +index 92ae9462f..67c897029 100644 +--- a/lxd/storage/backend_lxd.go ++++ b/lxd/storage/backend_lxd.go +@@ -779,7 +779,7 @@ func (b *lxdBackend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io. + var volumeSnapExpiryDate time.Time + + // Check if snapshot volume config is available for restore and matches snapshot name. +- if srcBackup.Config != nil && len(srcBackup.Config.VolumeSnapshots) >= i-1 && srcBackup.Config.VolumeSnapshots[i] != nil && srcBackup.Config.VolumeSnapshots[i].Name == backupFileSnap { ++ if srcBackup.Config != nil && len(srcBackup.Config.VolumeSnapshots) > i && srcBackup.Config.VolumeSnapshots[i] != nil && srcBackup.Config.VolumeSnapshots[i].Name == backupFileSnap { + volumeSnapDescription = srcBackup.Config.VolumeSnapshots[i].Description + volumeSnapConfig = srcBackup.Config.VolumeSnapshots[i].Config + +@@ -1834,7 +1834,7 @@ func (b *lxdBackend) CreateInstanceFromMigration(inst instance.Instance, conn io + + // If the source snapshot config is available, use that. + if srcInfo != nil && srcInfo.Config != nil { +- if len(srcInfo.Config.VolumeSnapshots) >= i-1 && srcInfo.Config.VolumeSnapshots[i] != nil && srcInfo.Config.VolumeSnapshots[i].Name == snapName { ++ if len(srcInfo.Config.VolumeSnapshots) > i && 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 diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/115-CVE-2026-41648.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/115-CVE-2026-41648.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/115-CVE-2026-41648.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/115-CVE-2026-41648.patch 2026-05-01 23:29:36.000000000 +0000 @@ -0,0 +1,152 @@ +From da44e2d05550e8c9ff1fe835c0f95ee0819c2e0e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 17 Apr 2026 21:50:29 -0400 +Subject: [PATCH] incusd: Limit tarball YAML reads to 1MiB +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +User provided image and backup tarballs would be unpacked and YAML files +parsed without any size restrictions. This was making it easy for an +authenticated user to provide a crafted image or backup tarball that +when parsed by Incus would lead to a very large YAML document being +loaded into memory, potentially causing the entire server to run out of +memory. + +This addresses CVE-2026-41648 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/images.go | 2 +- + lxd/backup/backup_info.go | 3 +- + lxd/storage/drivers/driver_btrfs_utils.go | 3 +- + lxd/util/reader.go | 63 +++++++++++++++++++ + 4 files changed, 68 insertions(+), 3 deletions(-) + create mode 100644 internal/server/util/reader.go + +diff --git a/lxd/backup/backup_info.go b/lxd/backup/backup_info.go +index b96d1f677..68a991ee5 100644 +--- a/lxd/backup/backup_info.go ++++ b/lxd/backup/backup_info.go +@@ -8,6 +8,7 @@ import ( + + "github.com/canonical/lxd/lxd/backup/config" + "github.com/canonical/lxd/lxd/sys" ++ localUtil "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared/api" + ) + +@@ -81,7 +82,7 @@ func GetInfo(r io.ReadSeeker, sysOS *sys.OS, outputPath string) (*Info, error) { + } + + if hdr.Name == backupIndexPath { +- err = yaml.NewDecoder(tr).Decode(&result) ++ err = yaml.NewDecoder(localUtil.MaxBytesReader(tr, 1024*1024)).Decode(&result) + if err != nil { + return nil, err + } +diff --git a/lxd/images.go b/lxd/images.go +index 6ae5b32c5..c6a60d73f 100644 +--- a/lxd/images.go ++++ b/lxd/images.go +@@ -1201,7 +1201,7 @@ func getImageMetadata(fname string) (*api.ImageMetadata, string, error) { + } + + if hdr.Name == "metadata.yaml" || hdr.Name == "./metadata.yaml" { +- err = yaml.NewDecoder(tr).Decode(&result) ++ err = yaml.NewDecoder(util.MaxBytesReader(tr, 1024*1024)).Decode(&result) + if err != nil { + return nil, "unknown", err + } +diff --git a/lxd/storage/drivers/driver_btrfs_utils.go b/lxd/storage/drivers/driver_btrfs_utils.go +index 2573742a3..3965aee10 100644 +--- a/lxd/storage/drivers/driver_btrfs_utils.go ++++ b/lxd/storage/drivers/driver_btrfs_utils.go +@@ -25,6 +25,7 @@ import ( + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/ioprogress" + "github.com/canonical/lxd/shared/logger" ++ localUtil "github.com/canonical/lxd/lxd/util" + ) + + // Errors. +@@ -584,7 +585,7 @@ func (d *btrfs) loadOptimizedBackupHeader(r io.ReadSeeker, mountPath string) (*B + } + + if hdr.Name == "backup/optimized_header.yaml" { +- err = yaml.NewDecoder(tr).Decode(&header) ++ err = yaml.NewDecoder(localUtil.MaxBytesReader(tr, 1024*1024)).Decode(&header) + if err != nil { + return nil, fmt.Errorf("Error parsing optimized backup header file: %w", err) + } +diff --git a/internal/server/util/reader.go b/internal/server/util/reader.go +new file mode 100644 +index 00000000000..3b49694d45a +--- /dev/null ++++ b/lxd/util/reader.go +@@ -0,0 +1,63 @@ ++package util ++ ++import ( ++ "io" ++) ++ ++// MaxBytesReader provides a ReadCloser wrapper which returns an error when reading past a set limit. ++// This is based on http.MaxBytesReader but adapted for use outside of http. ++func MaxBytesReader(r io.Reader, n int64) io.Reader { ++ if n < 0 { // Treat negative limits as equivalent to 0. ++ n = 0 ++ } ++ ++ return &maxBytesReader{r: r, i: n, n: n} ++} ++ ++// MaxBytesError is returned by [MaxBytesReader] when its read limit is exceeded. ++type MaxBytesError struct { ++ Limit int64 ++} ++ ++func (e *MaxBytesError) Error() string { ++ return "input data too large" ++} ++ ++type maxBytesReader struct { ++ r io.Reader // underlying reader ++ i int64 // max bytes initially, for MaxBytesError ++ n int64 // max bytes remaining ++ err error // sticky error ++} ++ ++func (l *maxBytesReader) Read(p []byte) (n int, err error) { ++ if l.err != nil { ++ return 0, l.err ++ } ++ ++ if len(p) == 0 { ++ return 0, nil ++ } ++ ++ // If they asked for a 32KB byte read but only 5 bytes are ++ // remaining, no need to read 32KB. 6 bytes will answer the ++ // question of the whether we hit the limit or go past it. ++ // 0 < len(p) < 2^63 ++ if int64(len(p))-1 > l.n { ++ p = p[:l.n+1] ++ } ++ ++ n, err = l.r.Read(p) ++ if int64(n) <= l.n { ++ l.n -= int64(n) ++ l.err = err ++ ++ return n, err ++ } ++ ++ n = int(l.n) ++ l.n = 0 ++ ++ l.err = &MaxBytesError{l.i} ++ return n, l.err ++} diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/116-CVE-2026-41684.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/116-CVE-2026-41684.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/116-CVE-2026-41684.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/116-CVE-2026-41684.patch 2026-05-01 23:29:36.000000000 +0000 @@ -0,0 +1,56 @@ +From 5ceb17630b459137ade4d4f6aad3c2b764df220b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 17 Apr 2026 22:45:27 -0400 +Subject: [PATCH] incusd: Fix nil pointer dereference in instance backup + restore +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Both the user triggered and admin triggered instance backup restore +mechanism could lead to a daemon crash by being fed partial backup +metadata. + +This addresses CVE-2026-41684 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/api_internal.go | 4 ++++ + lxd/backup/backup_config_utils.go | 4 ++-- + 2 files changed, 6 insertions(+), 2 deletions(-) + +diff --git a/lxd/api_internal.go b/lxd/api_internal.go +index 821c7e517..4cd642ddf 100644 +--- a/lxd/api_internal.go ++++ b/lxd/api_internal.go +@@ -644,6 +644,10 @@ func internalImportFromBackup(s *state.State, bInfo *backup.Info, allowNameOverr + // The backup config later gets persisted to disk too. + backupConf := bInfo.Config + ++ if backupConf.Container == nil { ++ return fmt.Errorf("No instance configuration found in backup file.") ++ } ++ + if allowNameOverride && instName != "" { + backupConf.Container.Name = instName + } +diff --git a/lxd/backup/backup_config_utils.go b/lxd/backup/backup_config_utils.go +index a2ea30e33..ed03fac4c 100644 +--- a/lxd/backup/backup_config_utils.go ++++ b/lxd/backup/backup_config_utils.go +@@ -117,11 +117,11 @@ func UpdateInstanceConfigInPlace(c *db.Cluster, b *Info) error { + // Change the pool in case it doesn't match the one of the original instance. + b.Config.Pool = pool + +- if updateRootDevicePool(b.Config.Container.Devices, pool.Name) { ++ if b.Config.Container != nil && updateRootDevicePool(b.Config.Container.Devices, pool.Name) { + rootDiskDeviceFound = true + } + +- if updateRootDevicePool(b.Config.Container.ExpandedDevices, pool.Name) { ++ if b.Config.Container != nil && updateRootDevicePool(b.Config.Container.ExpandedDevices, pool.Name) { + rootDiskDeviceFound = true + } + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/117-CVE-2026-41685.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/117-CVE-2026-41685.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/117-CVE-2026-41685.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/117-CVE-2026-41685.patch 2026-05-01 23:29:36.000000000 +0000 @@ -0,0 +1,150 @@ +From 236711d0f240e8bf4ad5acafbf3db9501eba0d60 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 20 Apr 2026 18:57:00 -0400 +Subject: [PATCH] incusd: Use QuotaWriter for backup and ISO uploads +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Incus should always respect project disk limits when they are in place +as those are a very useful safety net to prevent users from causing a +denial of service by filling up the server's disk. + +This addresses CVE-2026-41685 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/instances_post.go | 18 +++++++++++++- + lxd/storage_volumes.go | 17 ++++++++++++++++++++++++-- + lxd/project/permissions.go | 20 +++++++++++++++ + 4 files changed, 86 insertions(+), 4 deletions(-) + +diff --git a/lxd/instances_post.go b/lxd/instances_post.go +index d03746dc5..1e98a1973 100644 +--- a/lxd/instances_post.go ++++ b/lxd/instances_post.go +@@ -29,6 +29,7 @@ import ( + "github.com/canonical/lxd/lxd/revert" + "github.com/canonical/lxd/lxd/state" + storagePools "github.com/canonical/lxd/lxd/storage" ++ "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/logger" +@@ -570,8 +571,23 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data + defer func() { _ = os.Remove(backupFile.Name()) }() + revert.Add(func() { _ = backupFile.Close() }) + ++ // Get disk budget for the project if any. ++ var budget int64 ++ ++ err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { ++ budget, err = project.GetSpaceBudget(tx, projectName) ++ if err != nil { ++ return err ++ } ++ ++ return nil ++ }) ++ if err != nil { ++ return response.InternalError(err) ++ } ++ + // Stream uploaded backup data into temporary file. +- _, err = io.Copy(backupFile, data) ++ _, err = util.SafeCopy(shared.NewQuotaWriter(backupFile, budget), data) + if err != nil { + return response.InternalError(err) + } +diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go +index cf5ada0a2..26b1a8547 100644 +--- a/lxd/storage_volumes.go ++++ b/lxd/storage_volumes.go +@@ -1869,8 +1869,23 @@ func createStoragePoolVolumeFromBackup(s *state.State, r *http.Request, requestP + defer func() { _ = os.Remove(backupFile.Name()) }() + revert.Add(func() { _ = backupFile.Close() }) + ++ // Get disk budget for the project if any. ++ var budget int64 ++ ++ err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { ++ budget, err = project.GetSpaceBudget(tx, projectName) ++ if err != nil { ++ return err ++ } ++ ++ return nil ++ }) ++ if err != nil { ++ return response.InternalError(err) ++ } ++ + // Stream uploaded backup data into temporary file. +- _, err = io.Copy(backupFile, data) ++ _, err = util.SafeCopy(shared.NewQuotaWriter(backupFile, budget), data) + if err != nil { + return response.InternalError(err) + } +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index 2d04a697f..9ca757bb6 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -274,6 +274,26 @@ func GetImageSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { + return -1, nil + } + ++ return getSpaceBudget(info) ++} ++ ++// GetSpaceBudget returns how much disk space is left in the given project. ++// ++// If no limit is in place, return -1. ++func GetSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { ++ info, err := fetchProject(tx, projectName, true) ++ if err != nil { ++ return -1, err ++ } ++ ++ if info == nil { ++ return -1, nil ++ } ++ ++ return getSpaceBudget(info) ++} ++ ++func getSpaceBudget(info *projectInfo) (int64, error) { + // If "limits.disk" is not set, the budget is unlimited. + if info.Project.Config["limits.disk"] == "" { + return -1, nil +diff --git a/lxd/util/io.go b/lxd/util/io.go +new file mode 100644 +index 000000000..4ef334b0f +--- /dev/null ++++ b/lxd/util/io.go +@@ -0,0 +1,24 @@ ++package util ++ ++import ( ++ "io" ++) ++ ++// SafeCopy behaves like io.Copy but performs the copy through a loop of ++// io.CopyN calls using a fixed 4MiB chunk size. ++func SafeCopy(dst io.Writer, src io.Reader) (int64, error) { ++ const chunkSize = 4 * 1024 * 1024 ++ ++ var written int64 ++ for { ++ n, err := io.CopyN(dst, src, chunkSize) ++ written += n ++ if err != nil { ++ if err == io.EOF { ++ return written, nil ++ } ++ ++ return written, err ++ } ++ } ++} 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 2026-04-14 18:30:26.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/series 2026-05-01 23:29:36.000000000 +0000 @@ -24,3 +24,8 @@ 110-CVE-2026-34177.patch 111-CVE-2026-34178.patch 112-CVE-2026-34179.patch +113-CVE-2026-40197.patch +114-CVE-2026-40251.patch +115-CVE-2026-41648.patch +116-CVE-2026-41684.patch +117-CVE-2026-41685.patch