Version in base suite: 6.0.4-2+deb13u4 Version in overlay suite: 6.0.4-2+deb13u6 Base version: incus_6.0.4-2+deb13u6 Target version: incus_6.0.4-2+deb13u7 Base file: /srv/ftp-master.debian.org/ftp/pool/main/i/incus/incus_6.0.4-2+deb13u6.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/i/incus/incus_6.0.4-2+deb13u7.dsc changelog | 14 ++ patches/117-CVE-2026-40195.patch | 62 +++++++++++ patches/118-CVE-2026-40197.patch | 35 ++++++ patches/119-CVE-2026-40243.patch | 163 +++++++++++++++++++++++++++++ patches/120-CVE-2026-40251.patch | 54 +++++++++ patches/121-CVE-2026-41647.patch | 51 +++++++++ patches/122-CVE-2026-41648.patch | 161 +++++++++++++++++++++++++++++ patches/123-CVE-2026-41684.patch | 56 ++++++++++ patches/124-CVE-2026-41685.patch | 213 +++++++++++++++++++++++++++++++++++++++ patches/series | 8 + 10 files changed, 817 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpbj0qf303/incus_6.0.4-2+deb13u6.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpbj0qf303/incus_6.0.4-2+deb13u7.dsc: no acceptable signature found diff -Nru incus-6.0.4/debian/changelog incus-6.0.4/debian/changelog --- incus-6.0.4/debian/changelog 2026-04-14 16:44:29.000000000 +0000 +++ incus-6.0.4/debian/changelog 2026-05-01 01:13:25.000000000 +0000 @@ -1,3 +1,17 @@ +incus (6.0.4-2+deb13u7) trixie-security; urgency=high + + * Cherry-pick fixes for the following security issues: + - CVE-2026-40195 / GHSA-gc7j-g665-rxr9 + - CVE-2026-40197 / GHSA-r7w7-mmxr-47r9 + - CVE-2026-40243 / GHSA-c839-4qxr-j4x3 + - CVE-2026-40251 / GHSA-4m88-wxj4-9qj6 + - CVE-2026-41647 / GHSA-fwj8-62r8-8p8m + - 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 01:13:25 +0000 + incus (6.0.4-2+deb13u6) trixie-security; urgency=high * Cherry-pick fixes for the following security issues: diff -Nru incus-6.0.4/debian/patches/117-CVE-2026-40195.patch incus-6.0.4/debian/patches/117-CVE-2026-40195.patch --- incus-6.0.4/debian/patches/117-CVE-2026-40195.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/117-CVE-2026-40195.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,62 @@ +From 030ced1b54064583f400501cb567642e2493f1ef Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Thu, 9 Apr 2026 21:51:18 -0400 +Subject: [PATCH] incusd/storage/bucket: Validate expected metadata on import +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This adds missing validation for bucket metadata when processing a +bucket import. The missing logic would allow a malformed backup to crash +the Incus daemon, potentially allowing for a DoS by a user with access +to the Incus storage bucket functionality. + +This addresses CVE-2026-40195 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/instances_post.go | 2 +- + internal/server/storage/backend.go | 8 ++++++++ + 2 files changed, 9 insertions(+), 1 deletion(-) + +diff --git a/cmd/incusd/instances_post.go b/cmd/incusd/instances_post.go +index 6559ebd179a..98d288f9d2e 100644 +--- a/cmd/incusd/instances_post.go ++++ b/cmd/incusd/instances_post.go +@@ -835,7 +835,7 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data + } + + // Detect broken legacy backups. +- if bInfo.Config == nil { ++ if bInfo.Config == nil || bInfo.Config.Container == nil { + return response.BadRequest(fmt.Errorf("Backup file is missing required information")) + } + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 12baf0ee59a..1c4c624c307 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -7841,6 +7841,10 @@ func (b *backend) CreateBucketFromBackup(srcBackup backup.Info, srcData io.ReadS + return fmt.Errorf("Storage pool does not support buckets") + } + ++ if srcBackup.Config == nil || srcBackup.Config.Bucket == nil { ++ return errors.New("Valid bucket config not found in index") ++ } ++ + reverter := revert.New() + defer reverter.Fail() + +@@ -7859,6 +7863,10 @@ func (b *backend) CreateBucketFromBackup(srcBackup backup.Info, srcData io.ReadS + + // Upload all keys from the backup. + for _, bucketKey := range srcBackup.Config.BucketKeys { ++ if bucketKey == nil { ++ return errors.New("Bad bucket key found in index") ++ } ++ + bucketKeyRequest := api.StorageBucketKeysPost{ + Name: bucketKey.Name, + StorageBucketKeyPut: bucketKey.StorageBucketKeyPut, diff -Nru incus-6.0.4/debian/patches/118-CVE-2026-40197.patch incus-6.0.4/debian/patches/118-CVE-2026-40197.patch --- incus-6.0.4/debian/patches/118-CVE-2026-40197.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/118-CVE-2026-40197.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,35 @@ +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 +--- + internal/server/storage/backend.go | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 1c4c624c307..c1126ae1129 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -7709,6 +7709,10 @@ func (b *backend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData io + + // Create database entries for new storage volume snapshots. + for _, s := range srcBackup.Config.VolumeSnapshots { ++ if s == nil { ++ return errors.New("Bad snapshot definition found in index") ++ } ++ + snapshot := s // Local var for revert. + snapName := snapshot.Name + diff -Nru incus-6.0.4/debian/patches/119-CVE-2026-40243.patch incus-6.0.4/debian/patches/119-CVE-2026-40243.patch --- incus-6.0.4/debian/patches/119-CVE-2026-40243.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/119-CVE-2026-40243.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,163 @@ +From 2b7d83a0178022a70091d43e6a208d3256af574f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Thu, 9 Apr 2026 23:23:07 -0400 +Subject: [PATCH] incusd/network/ovn: Fix TLS validation logic +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +All 4 of the OVN connection handlers (NB, SB, IC-NB, IC-SB) were using a +rather broken validation logic which would effectively treat any remote +certificate as valid. + +The updated logic now correctly uses the configured CA as the only valid +TLS root certificate and then loads the provided remote certificate and +intermediates before validating that a path exists between the root CA +and the server certificate. + +Custom logic is used as OVN certificates and connection URLs don't +typically contain valid server name information. + +This addresses CVE-2026-40243 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +--- + internal/server/network/ovn/ovn_icnb.go | 15 +++++++++------ + internal/server/network/ovn/ovn_icsb.go | 15 +++++++++------ + internal/server/network/ovn/ovn_nb.go | 15 +++++++++------ + internal/server/network/ovn/ovn_sb.go | 15 +++++++++------ + 4 files changed, 36 insertions(+), 24 deletions(-) + +diff --git a/internal/server/network/ovn/ovn_icnb.go b/internal/server/network/ovn/ovn_icnb.go +index a85b62bea98..1814c1be956 100644 +--- a/internal/server/network/ovn/ovn_icnb.go ++++ b/internal/server/network/ovn/ovn_icnb.go +@@ -87,11 +87,13 @@ func NewICNB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey + } + + // Load the chain. +- roots := x509.NewCertPool() +- for _, rawCert := range rawCerts { +- cert, _ := x509.ParseCertificate(rawCert) +- if cert != nil { +- roots.AddCert(cert) ++ intermediates := x509.NewCertPool() ++ if len(rawCerts) > 1 { ++ for _, rawCert := range rawCerts[1:] { ++ cert, _ := x509.ParseCertificate(rawCert) ++ if cert != nil { ++ intermediates.AddCert(cert) ++ } + } + } + +@@ -103,7 +105,8 @@ func NewICNB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey + + // Validate. + opts := x509.VerifyOptions{ +- Roots: roots, ++ Roots: clientCAPool, ++ Intermediates: intermediates, + } + + _, err := cert.Verify(opts) +diff --git a/internal/server/network/ovn/ovn_icsb.go b/internal/server/network/ovn/ovn_icsb.go +index b32caf67275..5be42d21ea0 100644 +--- a/internal/server/network/ovn/ovn_icsb.go ++++ b/internal/server/network/ovn/ovn_icsb.go +@@ -93,11 +93,13 @@ func NewICSB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey + } + + // Load the chain. +- roots := x509.NewCertPool() +- for _, rawCert := range rawCerts { +- cert, _ := x509.ParseCertificate(rawCert) +- if cert != nil { +- roots.AddCert(cert) ++ intermediates := x509.NewCertPool() ++ if len(rawCerts) > 1 { ++ for _, rawCert := range rawCerts[1:] { ++ cert, _ := x509.ParseCertificate(rawCert) ++ if cert != nil { ++ intermediates.AddCert(cert) ++ } + } + } + +@@ -109,7 +111,8 @@ func NewICSB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey + + // Validate. + opts := x509.VerifyOptions{ +- Roots: roots, ++ Roots: clientCAPool, ++ Intermediates: intermediates, + } + + _, err := cert.Verify(opts) +diff --git a/internal/server/network/ovn/ovn_nb.go b/internal/server/network/ovn/ovn_nb.go +index 35522926d34..42d8babbdd4 100644 +--- a/internal/server/network/ovn/ovn_nb.go ++++ b/internal/server/network/ovn/ovn_nb.go +@@ -103,11 +103,13 @@ func NewNB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey s + } + + // Load the chain. +- roots := x509.NewCertPool() +- for _, rawCert := range rawCerts { +- cert, _ := x509.ParseCertificate(rawCert) +- if cert != nil { +- roots.AddCert(cert) ++ intermediates := x509.NewCertPool() ++ if len(rawCerts) > 1 { ++ for _, rawCert := range rawCerts[1:] { ++ cert, _ := x509.ParseCertificate(rawCert) ++ if cert != nil { ++ intermediates.AddCert(cert) ++ } + } + } + +@@ -119,7 +121,8 @@ func NewNB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey s + + // Validate. + opts := x509.VerifyOptions{ +- Roots: roots, ++ Roots: clientCAPool, ++ Intermediates: intermediates, + } + + _, err := cert.Verify(opts) +diff --git a/internal/server/network/ovn/ovn_sb.go b/internal/server/network/ovn/ovn_sb.go +index 8dc57668c17..461dc0be7e3 100644 +--- a/internal/server/network/ovn/ovn_sb.go ++++ b/internal/server/network/ovn/ovn_sb.go +@@ -87,11 +87,13 @@ func NewSB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey s + } + + // Load the chain. +- roots := x509.NewCertPool() +- for _, rawCert := range rawCerts { +- cert, _ := x509.ParseCertificate(rawCert) +- if cert != nil { +- roots.AddCert(cert) ++ intermediates := x509.NewCertPool() ++ if len(rawCerts) > 1 { ++ for _, rawCert := range rawCerts[1:] { ++ cert, _ := x509.ParseCertificate(rawCert) ++ if cert != nil { ++ intermediates.AddCert(cert) ++ } + } + } + +@@ -103,7 +105,8 @@ func NewSB(dbAddr string, sslCACert string, sslClientCert string, sslClientKey s + + // Validate. + opts := x509.VerifyOptions{ +- Roots: roots, ++ Roots: clientCAPool, ++ Intermediates: intermediates, + } + + _, err := cert.Verify(opts) diff -Nru incus-6.0.4/debian/patches/120-CVE-2026-40251.patch incus-6.0.4/debian/patches/120-CVE-2026-40251.patch --- incus-6.0.4/debian/patches/120-CVE-2026-40251.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/120-CVE-2026-40251.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,54 @@ +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 +--- + internal/server/storage/backend.go | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 62e6615faea..bbb7117d259 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -895,12 +895,12 @@ func (b *backend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.Rea + + // 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 { ++ if len(srcBackup.Config.Snapshots) > i && 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 len(srcBackup.Config.VolumeSnapshots) > i && 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 +@@ -2117,12 +2117,12 @@ func (b *backend) CreateInstanceFromMigration(inst instance.Instance, conn io.Re + + // 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 { ++ if len(srcInfo.Config.Snapshots) > i && 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 { ++ 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 incus-6.0.4/debian/patches/121-CVE-2026-41647.patch incus-6.0.4/debian/patches/121-CVE-2026-41647.patch --- incus-6.0.4/debian/patches/121-CVE-2026-41647.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/121-CVE-2026-41647.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,51 @@ +From ec65c0015df77bb0436fca894861963e38a2b989 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 17 Apr 2026 21:03:12 -0400 +Subject: [PATCH] incusd/storage/s3: Fix nil pointer dereference on truncated + input +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Bad error checking could lead a truncated s3 backup archive to trigger a +nil pointer dereference during tar parsing. + +This would lead to a daemon crash and with repeated use, a DoS of Incus. + +This addresses CVE-2026-41647 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/storage/s3/transfer_manager.go | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/internal/server/storage/s3/transfer_manager.go b/internal/server/storage/s3/transfer_manager.go +index a0cacdf57..971edae86 100644 +--- a/internal/server/storage/s3/transfer_manager.go ++++ b/internal/server/storage/s3/transfer_manager.go +@@ -3,6 +3,7 @@ package s3 + import ( + "context" + "crypto/tls" ++ "errors" + "fmt" + "io" + "net/http" +@@ -127,8 +128,13 @@ func (t TransferManager) UploadAllFiles(bucketName string, srcData io.ReadSeeker + + for { + hdr, err := tr.Next() +- if err == io.EOF { +- break // End of archive. ++ if err != nil { ++ if errors.Is(err, io.EOF) { ++ // End of archive. ++ break ++ } ++ ++ return err + } + + // Skip anything that's not in the bucket itself. diff -Nru incus-6.0.4/debian/patches/122-CVE-2026-41648.patch incus-6.0.4/debian/patches/122-CVE-2026-41648.patch --- incus-6.0.4/debian/patches/122-CVE-2026-41648.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/122-CVE-2026-41648.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,161 @@ +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 +--- + cmd/incusd/images.go | 2 +- + internal/server/backup/backup_info.go | 5 +- + .../storage/drivers/driver_btrfs_utils.go | 3 +- + internal/server/util/reader.go | 63 +++++++++++++++++++ + 4 files changed, 69 insertions(+), 4 deletions(-) + create mode 100644 internal/server/util/reader.go + +diff --git a/cmd/incusd/images.go b/cmd/incusd/images.go +index 96bedc3a654..eedf0ec3deb 100644 +--- a/cmd/incusd/images.go ++++ b/cmd/incusd/images.go +@@ -1490,7 +1490,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(localUtil.MaxBytesReader(tr, 1024*1024)).Decode(&result) + if err != nil { + return nil, "unknown", err + } +diff --git a/internal/server/backup/backup_info.go b/internal/server/backup/backup_info.go +index 78e763b58ac..47e0d761d1a 100644 +--- a/internal/server/backup/backup_info.go ++++ b/internal/server/backup/backup_info.go +@@ -8,6 +8,7 @@ import ( + + "github.com/lxc/incus/v6/internal/server/backup/config" + "github.com/lxc/incus/v6/internal/server/sys" ++ localUtil "github.com/lxc/incus/v6/internal/server/util" + "github.com/lxc/incus/v6/shared/api" + ) + +@@ -88,7 +89,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 + } +@@ -121,7 +122,7 @@ func GetInfo(r io.ReadSeeker, sysOS *sys.OS, outputPath string) (*Info, error) { + + // Load old backup data. + if result.Config == nil && hdr.Name == "backup/container/backup.yaml" { +- err = yaml.NewDecoder(tr).Decode(&result.Config) ++ err = yaml.NewDecoder(localUtil.MaxBytesReader(tr, 1024*1024)).Decode(&result.Config) + if err != nil { + return nil, err + } +diff --git a/internal/server/storage/drivers/driver_btrfs_utils.go b/internal/server/storage/drivers/driver_btrfs_utils.go +index df8bee9e987..4d218f60db5 100644 +--- a/internal/server/storage/drivers/driver_btrfs_utils.go ++++ b/internal/server/storage/drivers/driver_btrfs_utils.go +@@ -22,6 +22,7 @@ import ( + + "github.com/lxc/incus/v6/internal/linux" + "github.com/lxc/incus/v6/internal/server/backup" ++ localUtil "github.com/lxc/incus/v6/internal/server/util" + "github.com/lxc/incus/v6/shared/api" + "github.com/lxc/incus/v6/shared/ioprogress" + "github.com/lxc/incus/v6/shared/logger" +@@ -591,7 +592,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/internal/server/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 incus-6.0.4/debian/patches/123-CVE-2026-41684.patch incus-6.0.4/debian/patches/123-CVE-2026-41684.patch --- incus-6.0.4/debian/patches/123-CVE-2026-41684.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/123-CVE-2026-41684.patch 2026-05-01 01:13:25.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 +--- + cmd/incusd/api_internal.go | 4 ++++ + internal/server/backup/backup_config_utils.go | 4 ++-- + 2 files changed, 6 insertions(+), 2 deletions(-) + +diff --git a/cmd/incusd/api_internal.go b/cmd/incusd/api_internal.go +index f8980d1b76c..c4e0d59c835 100644 +--- a/cmd/incusd/api_internal.go ++++ b/cmd/incusd/api_internal.go +@@ -755,6 +755,10 @@ func internalImportFromBackup(ctx context.Context, s *state.State, projectName s + return err + } + ++ if backupConf.Container == nil { ++ return fmt.Errorf("No instance configuration found in backup file.") ++ } ++ + if allowNameOverride && instName != "" { + backupConf.Container.Name = instName + } +diff --git a/internal/server/backup/backup_config_utils.go b/internal/server/backup/backup_config_utils.go +index 9aa5cc5ead4..d1c2b088f4e 100644 +--- a/internal/server/backup/backup_config_utils.go ++++ b/internal/server/backup/backup_config_utils.go +@@ -156,11 +156,11 @@ func UpdateInstanceConfig(c *db.Cluster, b Info, mountPath string) error { + // Change the pool in the backup.yaml. + backup.Pool = pool + +- if updateRootDevicePool(backup.Container.Devices, pool.Name) { ++ if backup.Container != nil && updateRootDevicePool(backup.Container.Devices, pool.Name) { + rootDiskDeviceFound = true + } + +- if updateRootDevicePool(backup.Container.ExpandedDevices, pool.Name) { ++ if backup.Container != nil && updateRootDevicePool(backup.Container.ExpandedDevices, pool.Name) { + rootDiskDeviceFound = true + } + diff -Nru incus-6.0.4/debian/patches/124-CVE-2026-41685.patch incus-6.0.4/debian/patches/124-CVE-2026-41685.patch --- incus-6.0.4/debian/patches/124-CVE-2026-41685.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/124-CVE-2026-41685.patch 2026-05-01 01:13:25.000000000 +0000 @@ -0,0 +1,213 @@ +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 +--- + cmd/incusd/instances_post.go | 18 +++++++++++++- + cmd/incusd/storage_buckets.go | 18 +++++++++++++- + cmd/incusd/storage_volumes.go | 34 ++++++++++++++++++++++++-- + internal/server/project/permissions.go | 20 +++++++++++++++ + 4 files changed, 86 insertions(+), 4 deletions(-) + +diff --git a/cmd/incusd/instances_post.go b/cmd/incusd/instances_post.go +index 98d288f9d2e..9962ca6f5b9 100644 +--- a/cmd/incusd/instances_post.go ++++ b/cmd/incusd/instances_post.go +@@ -15,6 +15,7 @@ import ( + "github.com/gorilla/websocket" + + internalInstance "github.com/lxc/incus/v6/internal/instance" ++ internalIO "github.com/lxc/incus/v6/internal/io" + "github.com/lxc/incus/v6/internal/server/backup" + "github.com/lxc/incus/v6/internal/server/cluster" + "github.com/lxc/incus/v6/internal/server/db" +@@ -645,8 +646,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(internalIO.NewQuotaWriter(backupFile, budget), data) + if err != nil { + return response.InternalError(err) + } +diff --git a/cmd/incusd/storage_buckets.go b/cmd/incusd/storage_buckets.go +index e7a4050294e..672a31deb55 100644 +--- a/cmd/incusd/storage_buckets.go ++++ b/cmd/incusd/storage_buckets.go +@@ -15,6 +15,7 @@ import ( + "github.com/gorilla/mux" + + "github.com/lxc/incus/v6/internal/filter" ++ internalIO "github.com/lxc/incus/v6/internal/io" + "github.com/lxc/incus/v6/internal/server/auth" + "github.com/lxc/incus/v6/internal/server/backup" + "github.com/lxc/incus/v6/internal/server/db" +@@ -1233,8 +1234,23 @@ func createStoragePoolBucketFromBackup(s *state.State, r *http.Request, requestP + defer func() { _ = os.Remove(backupFile.Name()) }() + reverter.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(internalIO.NewQuotaWriter(backupFile, budget), data) + if err != nil { + return response.InternalError(err) + } +diff --git a/cmd/incusd/storage_volumes.go b/cmd/incusd/storage_volumes.go +index e0dc8d62a99..2becb8024b4 100644 +--- a/cmd/incusd/storage_volumes.go ++++ b/cmd/incusd/storage_volumes.go +@@ -2238,8 +2238,23 @@ func createStoragePoolVolumeFromISO(s *state.State, r *http.Request, requestProj + defer func() { _ = os.Remove(isoFile.Name()) }() + revert.Add(func() { _ = isoFile.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 ISO data into temporary file. +- size, err := io.Copy(isoFile, data) ++ size, err := util.SafeCopy(internalIO.NewQuotaWriter(isoFile, budget), data) + if err != nil { + return response.InternalError(err) + } +@@ -2291,8 +2306,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(internalIO.NewQuotaWriter(backupFile, budget), data) + if err != nil { + return response.InternalError(err) + } +diff --git a/internal/server/project/permissions.go b/internal/server/project/permissions.go +index 6d7a8f79593..2be4ede5836 100644 +--- a/internal/server/project/permissions.go ++++ b/internal/server/project/permissions.go +@@ -347,6 +347,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/shared/util/io.go b/shared/util/io.go +new file mode 100644 +index 000000000..4ef334b0f +--- /dev/null ++++ b/shared/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 incus-6.0.4/debian/patches/series incus-6.0.4/debian/patches/series --- incus-6.0.4/debian/patches/series 2026-04-14 16:03:20.000000000 +0000 +++ incus-6.0.4/debian/patches/series 2026-05-01 01:13:25.000000000 +0000 @@ -23,3 +23,11 @@ 112-CVE-2026-33897.patch 115-CVE-2026-34178.patch 116-CVE-2026-34179.patch +117-CVE-2026-40195.patch +118-CVE-2026-40197.patch +119-CVE-2026-40243.patch +120-CVE-2026-40251.patch +121-CVE-2026-41647.patch +122-CVE-2026-41648.patch +123-CVE-2026-41684.patch +124-CVE-2026-41685.patch