Version in base suite: 5.0.2+git20231211.1364ae4-9+deb13u6 Base version: lxd_5.0.2+git20231211.1364ae4-9+deb13u6 Target version: lxd_5.0.2+git20231211.1364ae4-9+deb13u7 Base file: /srv/ftp-master.debian.org/ftp/pool/main/l/lxd/lxd_5.0.2+git20231211.1364ae4-9+deb13u6.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/l/lxd/lxd_5.0.2+git20231211.1364ae4-9+deb13u7.dsc changelog | 16 patches/118-CVE-2026-9639.patch | 37 ++ patches/119-CVE-2026-9640.patch | 706 +++++++++++++++++++++++++++++++++++++++ patches/120-CVE-2026-48749.patch | 43 ++ patches/121-CVE-2026-48750.patch | 36 + patches/122-CVE-2026-48751.patch | 48 ++ patches/123-CVE-2026-48752.patch | 187 ++++++++++ patches/124-CVE-2026-48755.patch | 38 ++ patches/125-CVE-2026-48769.patch | 69 +++ patches/126-CVE-2026-55621.patch | 41 ++ patches/127-CVE-2026-55622.patch | 40 ++ patches/series | 10 12 files changed, 1271 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp2f5hhppw/lxd_5.0.2+git20231211.1364ae4-9+deb13u6.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp2f5hhppw/lxd_5.0.2+git20231211.1364ae4-9+deb13u7.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-05-01 23:29:36.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/changelog 2026-06-27 17:07:59.000000000 +0000 @@ -1,3 +1,19 @@ +lxd (5.0.2+git20231211.1364ae4-9+deb13u7) trixie-security; urgency=high + + * Cherry-pick fixes for the following security issues: + - CVE-2026-9639 / GHSA-j93m-3j9p-m5m8 + - CVE-2026-9640 / GHSA-ppq7-4492-5552 + - CVE-2026-48749 / GHSA-vghh-5rfx-xhq8 + - CVE-2026-48750 / GHSA-9j25-mm2h-2f76 + - CVE-2026-48751 / GHSA-47w9-6r3f-938g + - CVE-2026-48752 / GHSA-jpf8-86f3-wp38 + - CVE-2026-48755 / GHSA-fmc8-p6q7-75cc + - CVE-2026-48769 / GHSA-pjff-c2wc-f6jm + - CVE-2026-55621 / GHSA-7mr3-28h5-m5vx + - CVE-2026-55622 / GHSA-qx75-2p3r-pwm5 + + -- Mathias Gibbens Sat, 27 Jun 2026 17:07:59 +0000 + lxd (5.0.2+git20231211.1364ae4-9+deb13u6) trixie-security; urgency=high * Cherry-pick fixes for the following security issues (from Incus): diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/118-CVE-2026-9639.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/118-CVE-2026-9639.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/118-CVE-2026-9639.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/118-CVE-2026-9639.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,37 @@ +From ab6b7dff0c770044875d9d26a6254a7075b4d00b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Thu, 28 May 2026 17:37:33 -0400 +Subject: [PATCH] incusd/storage: Guard nil ExpiresAt in + CreateCustomVolumeFromBackup +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48756 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/storage/backend_lxd.go | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/lxd/storage/backend_lxd.go b/lxd/storage/backend_lxd.go +index 92ae9462f..6d7812dc8 100644 +--- a/lxd/storage/backend_lxd.go ++++ b/lxd/storage/backend_lxd.go +@@ -5745,9 +5745,14 @@ func (b *lxdBackend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData + snapVolStorageName := project.StorageVolume(srcBackup.Project, fullSnapName) + snapVol := b.GetVolume(drivers.VolumeTypeCustom, drivers.ContentType(srcBackup.Config.Volume.ContentType), snapVolStorageName, snapshot.Config) + ++ var snapExpiryDate time.Time ++ if snapshot.ExpiresAt != nil { ++ snapExpiryDate = *snapshot.ExpiresAt ++ } ++ + // 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.ExpiresAt, snapVol.ContentType(), true, true) ++ err = VolumeDBCreate(b, srcBackup.Project, fullSnapName, snapshot.Description, snapVol.Type(), true, snapVol.Config(), snapExpiryDate, snapVol.ContentType(), true, true) + if err != nil { + return err + } diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/119-CVE-2026-9640.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/119-CVE-2026-9640.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/119-CVE-2026-9640.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/119-CVE-2026-9640.patch 2026-06-27 16:42:44.000000000 +0000 @@ -0,0 +1,706 @@ +From 7e6d93a6ccb183ec4bf3a0b5af5aaf77da98ecff Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Tue, 19 May 2026 15:39:53 +0100 +Subject: [PATCH 1/9] lxd/project/limits/permissions: Fix typo in description + of fetchProject + +Signed-off-by: Thomas Parrott +(cherry picked from commit 1000cf6b86cd3472f214699ea4c92f00d6b12e9d) +--- + lxd/project/permissions.go | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index f3d92b8d3436..7b4580cb01b6 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -1149,7 +1149,7 @@ type projectInfo struct { + // and possibly custom volumes. + // + // If the skipIfNoLimits flag is true, then profiles, instances and volumes +-// won't be loaded if the profile has no limits set on it, and nil will be ++// won't be loaded if the project has no limits set on it, and nil will be + // returned. + func fetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*projectInfo, error) { + ctx := context.Background() + +From df80fdf4c0d0988d8820b3ba059e146e0ad48d88 Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Tue, 19 May 2026 13:11:17 +0100 +Subject: [PATCH 2/9] lxd/project/limits: Export FetchProject and ProjectInfo + +Signed-off-by: Thomas Parrott +(cherry picked from commit edf1b94038a8432882f174412f0a0bd0e50770f4) +Rebased-by: Mathias Gibbens +--- + lxd/project/permissions.go | 36 ++++++++++++++++++------------------ + lxd/project/state.go | 2 +- + 2 files changed, 19 insertions(+), 19 deletions(-) + +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index 7b4580cb01b6..75c8f031fcdd 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -24,7 +24,7 @@ import ( + // AllowInstanceCreation returns an error if any project-specific limit or + // restriction is violated when creating a new instance. + func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.InstancesPost) error { +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return err + } +@@ -88,7 +88,7 @@ func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.Instanc + } + + // Check that we have not exceeded the maximum total allotted number of instances for both containers and vms. +-func checkTotalInstanceCountLimit(info *projectInfo) error { ++func checkTotalInstanceCountLimit(info *ProjectInfo) error { + count, limit, err := getTotalInstanceCountLimit(info) + if err != nil { + return err +@@ -101,7 +101,7 @@ func checkTotalInstanceCountLimit(info *projectInfo) error { + return nil + } + +-func getTotalInstanceCountLimit(info *projectInfo) (int, int, error) { ++func getTotalInstanceCountLimit(info *ProjectInfo) (int, int, error) { + overallValue, ok := info.Project.Config["limits.instances"] + if ok { + limit, err := strconv.Atoi(overallValue) +@@ -116,7 +116,7 @@ func getTotalInstanceCountLimit(info *projectInfo) (instanceCount int, limit int + } + + // Check that we have not reached the maximum number of instances for this type. +-func checkInstanceCountLimit(info *projectInfo, instanceType instancetype.Type) error { ++func checkInstanceCountLimit(info *ProjectInfo, instanceType instancetype.Type) error { + count, limit, err := getInstanceCountLimit(info, instanceType) + if err != nil { + return err +@@ -129,7 +129,7 @@ func checkInstanceCountLimit(info *projectInfo, instanceType instancetype.Type) + return nil + } + +-func getInstanceCountLimit(info *projectInfo, instanceType instancetype.Type) (int, int, error) { ++func getInstanceCountLimit(info *ProjectInfo, instanceType instancetype.Type) (int, int, error) { + var key string + switch instanceType { + case instancetype.Container: +@@ -227,7 +227,7 @@ func checkRestrictionsOnVolatileConfig(project api.Project, instanceType instanc + // AllowVolumeCreation returns an error if any project-specific limit or + // restriction is violated when creating a new custom volume in a project. + func AllowVolumeCreation(tx *db.ClusterTx, projectName string, req api.StorageVolumesPost) error { +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return err + } +@@ -260,7 +260,7 @@ func AllowVolumeCreation(tx *db.ClusterTx, projectName string, req api.StorageVo + // + // If no limit is in place, return -1. + func GetImageSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return -1, err + } +@@ -306,7 +306,7 @@ func GetImageSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { + + // Check that we would not violate the project limits or restrictions if we + // were to commit the given instances and profiles. +-func checkRestrictionsAndAggregateLimits(tx *db.ClusterTx, info *projectInfo) error { ++func checkRestrictionsAndAggregateLimits(tx *db.ClusterTx, info *ProjectInfo) error { + // List of config keys for which we need to check aggregate values + // across all project instances. + aggregateKeys := []string{} +@@ -349,7 +349,7 @@ func checkRestrictionsAndAggregateLimits(tx *db.ClusterTx, info *projectInfo) er + return nil + } + +-func getAggregateLimits(info *projectInfo, aggregateKeys []string) (map[string]api.ProjectStateResource, error) { ++func getAggregateLimits(info *ProjectInfo, aggregateKeys []string) (map[string]api.ProjectStateResource, error) { + result := map[string]api.ProjectStateResource{} + + if len(aggregateKeys) == 0 { +@@ -383,7 +383,7 @@ func getAggregateLimits(info *projectInfo, aggregateKeys []string) (map[string]a + return result, nil + } + +-func checkAggregateLimits(info *projectInfo, aggregateKeys []string) error { ++func checkAggregateLimits(info *ProjectInfo, aggregateKeys []string) error { + if len(aggregateKeys) == 0 { + return nil + } +@@ -859,7 +859,7 @@ func isVMLowLevelOptionForbidden(key string) bool { + // restriction is violated when updating an existing instance. + func AllowInstanceUpdate(tx *db.ClusterTx, projectName, instanceName string, req api.InstancePut, currentConfig map[string]string) error { + var updatedInstance *api.Instance +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return err + } +@@ -904,7 +904,7 @@ func AllowInstanceUpdate(tx *db.ClusterTx, projectName, instanceName string, req + // AllowVolumeUpdate returns an error if any project-specific limit or + // restriction is violated when updating an existing custom volume. + func AllowVolumeUpdate(tx *db.ClusterTx, projectName, volumeName string, req api.StorageVolumePut, currentConfig map[string]string) error { +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return err + } +@@ -938,7 +938,7 @@ func AllowVolumeUpdate(tx *db.ClusterTx, projectName, volumeName string, req api + // AllowProfileUpdate checks that project limits and restrictions are not + // violated when changing a profile. + func AllowProfileUpdate(tx *db.ClusterTx, projectName, profileName string, req api.ProfilePut) error { +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return err + } +@@ -967,7 +967,7 @@ func AllowProfileUpdate(tx *db.ClusterTx, projectName, profileName string, req a + + // AllowProjectUpdate checks the new config to be set on a project is valid. + func AllowProjectUpdate(tx *db.ClusterTx, projectName string, config map[string]string, changed []string) error { +- info, err := fetchProject(tx, projectName, false) ++ info, err := FetchProject(tx, projectName, false) + if err != nil { + return err + } +@@ -1138,7 +1138,7 @@ func projectHasLimitsOrRestrictions(project api.Project) bool { + + // Hold information associated with the project, such as profiles and + // instances. +-type projectInfo struct { ++type ProjectInfo struct { + Project api.Project + Profiles []api.Profile + Instances []api.Instance +@@ -1151,7 +1151,7 @@ type projectInfo struct { + // If the skipIfNoLimits flag is true, then profiles, instances and volumes + // won't be loaded if the project has no limits set on it, and nil will be + // returned. +-func fetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*projectInfo, error) { ++func FetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*ProjectInfo, error) { + ctx := context.Background() + dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName) + if err != nil { +@@ -1214,7 +1214,7 @@ func fetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*p + return nil, fmt.Errorf("Fetch project custom volumes from database: %w", err) + } + +- info := &projectInfo{ ++ info := &ProjectInfo{ + Project: *project, + Profiles: profiles, + Instances: instances, +@@ -1254,7 +1254,7 @@ func expandInstancesConfigAndDevices(instances []api.Instance, profiles []api.Pr + + // Sum of the effective values for the given limits across all project + // enties (instances and custom volumes). +-func getTotalsAcrossProjectEntities(info *projectInfo, keys []string, skipUnset bool) (map[string]int64, error) { ++func getTotalsAcrossProjectEntities(info *ProjectInfo, keys []string, skipUnset bool) (map[string]int64, error) { + totals := map[string]int64{} + + for _, key := range keys { +diff --git a/lxd/project/state.go b/lxd/project/state.go +index c92a214ad4cf..5362246c1026 100644 +--- a/lxd/project/state.go ++++ b/lxd/project/state.go +@@ -15,7 +15,7 @@ func GetCurrentAllocations(ctx context.Context, tx *db.ClusterTx, projectName st + result := map[string]api.ProjectStateResource{} + + // Get the project. +- info, err := fetchProject(tx, projectName, false) ++ info, err := FetchProject(tx, projectName, false) + if err != nil { + return nil, err + } + +From c3f89456a48e0d2f83f536bc3b1ee0af679b0320 Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Tue, 19 May 2026 13:15:19 +0100 +Subject: [PATCH 3/9] lxd/project/limits/permissions: Update + AllowInstanceCreation to accept a ProjectInfo argument + +Rather than loading the project info itself. + +Signed-off-by: Thomas Parrott +(cherry picked from commit 74f06ba1e4cc8ad595043e35d7fae8958f0f35be) +--- + lxd/instances_post.go | 25 ++++++++++++++++++++++-- + lxd/project/permissions.go | 19 +++++------------- + lxd/project/permissions_test.go | 34 +++++++++++++++++++++++++++------ + 3 files changed, 56 insertions(+), 22 deletions(-) + +diff --git a/lxd/instances_post.go b/lxd/instances_post.go +index deeed31ab8ac..58b677ba68ff 100644 +--- a/lxd/instances_post.go ++++ b/lxd/instances_post.go +@@ -655,7 +655,20 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data + Type: api.InstanceType(bInfo.Config.Container.Type), + } + +- return project.AllowInstanceCreation(tx, projectName, req) ++ restrictions, err := project.FetchProject(tx, projectName, true) ++ if err != nil { ++ return err ++ } ++ ++ // Check restrictions/limits if defined on project. ++ if restrictions != nil { ++ err = project.AllowInstanceCreation(tx, *restrictions, req) ++ if err != nil { ++ return err ++ } ++ } ++ ++ return nil + }) + if err != nil { + return response.SmartError(err) +@@ -1078,10 +1091,18 @@ func instancesPost(d *Daemon, r *http.Request) response.Response { + if !clusterNotification { + // Check that the project's limits are not violated. Note this check is performed after + // automatically generated config values (such as ones from an InstanceType) have been set. +- err = project.AllowInstanceCreation(tx, targetProjectName, req) ++ restrictions, err := project.FetchProject(tx, targetProjectName, true) + if err != nil { + return err + } ++ ++ // Check restrictions/limits if defined on project. ++ if restrictions != nil { ++ err = project.AllowInstanceCreation(tx, *restrictions, req) ++ if err != nil { ++ return err ++ } ++ } + } + + return nil +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index 75c8f031fcdd..1d7e363da73c 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -23,16 +23,7 @@ import ( + + // AllowInstanceCreation returns an error if any project-specific limit or + // restriction is violated when creating a new instance. +-func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.InstancesPost) error { +- info, err := FetchProject(tx, projectName, true) +- if err != nil { +- return err +- } +- +- if info == nil { +- return nil +- } +- ++func AllowInstanceCreation(tx *db.ClusterTx, info ProjectInfo, req api.InstancesPost) error { + var instanceType instancetype.Type + switch req.Type { + case api.InstanceTypeContainer: +@@ -47,12 +38,12 @@ func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.Instanc + req.Profiles = []string{"default"} + } + +- err = checkInstanceCountLimit(info, instanceType) ++ err := checkInstanceCountLimit(&info, instanceType) + if err != nil { + return err + } + +- err = checkTotalInstanceCountLimit(info) ++ err = checkTotalInstanceCountLimit(&info) + if err != nil { + return err + } +@@ -60,7 +51,7 @@ func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.Instanc + // Add the instance being created. + info.Instances = append(info.Instances, api.Instance{ + Name: req.Name, +- Project: projectName, ++ Project: info.Project.Name, + Type: string(req.Type), + InstancePut: req.InstancePut, + }) +@@ -79,7 +70,7 @@ func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.Instanc + return err + } + +- err = checkRestrictionsAndAggregateLimits(tx, info) ++ err = checkRestrictionsAndAggregateLimits(tx, &info) + if err != nil { + return fmt.Errorf("Failed checking if instance creation allowed: %w", err) + } +diff --git a/lxd/project/permissions_test.go b/lxd/project/permissions_test.go +index 3c1d0c6ea1d6..2a962bac46aa 100644 +--- a/lxd/project/permissions_test.go ++++ b/lxd/project/permissions_test.go +@@ -21,12 +21,18 @@ func TestAllowInstanceCreation_NotConfigured(t *testing.T) { + tx, cleanup := db.NewTestClusterTx(t) + defer cleanup() + ++ info, err := limits.FetchProject(context.Background(), tx, "default", true) ++ require.NoError(t, err) ++ require.Nil(t, info) ++ ++ info, err = limits.FetchProject(context.Background(), tx, "default", false) ++ require.NoError(t, err) ++ + req := api.InstancesPost{ + Name: "c1", + Type: api.InstanceTypeContainer, + } +- +- err := project.AllowInstanceCreation(tx, "default", req) ++ err = project.AllowInstanceCreation(tx, *info, req) + assert.NoError(t, err) + } + +@@ -56,7 +62,11 @@ func TestAllowInstanceCreation_Below(t *testing.T) { + Type: api.InstanceTypeContainer, + } + +- err = project.AllowInstanceCreation(tx, "p1", req) ++ info, err := project.FetchProject(tx, "p1", true) ++ require.NoError(t, err) ++ require.NotNil(t, info) ++ ++ err = project.AllowInstanceCreation(tx, *info, req) + assert.NoError(t, err) + } + +@@ -87,7 +97,11 @@ func TestAllowInstanceCreation_Above(t *testing.T) { + Type: api.InstanceTypeContainer, + } + +- err = project.AllowInstanceCreation(tx, "p1", req) ++ info, err := project.FetchProject(tx, "p1", true) ++ require.NoError(t, err) ++ require.NotNil(t, info) ++ ++ err = project.AllowInstanceCreation(tx, *info, req) + assert.EqualError(t, err, `Reached maximum number of instances of type "container" in project "p1"`) + } + +@@ -118,7 +132,11 @@ func TestAllowInstanceCreation_DifferentType(t *testing.T) { + Type: api.InstanceTypeContainer, + } + +- err = project.AllowInstanceCreation(tx, "p1", req) ++ info, err := project.FetchProject(tx, "p1", true) ++ require.NoError(t, err) ++ require.NotNil(t, info) ++ ++ err = project.AllowInstanceCreation(tx, *info, req) + assert.NoError(t, err) + } + +@@ -149,7 +167,11 @@ func TestAllowInstanceCreation_AboveInstances(t *testing.T) { + Type: api.InstanceTypeContainer, + } + +- err = project.AllowInstanceCreation(tx, "p1", req) ++ info, err := project.FetchProject(tx, "p1", true) ++ require.NoError(t, err) ++ require.NotNil(t, info) ++ ++ err = project.AllowInstanceCreation(tx, *info, req) + assert.EqualError(t, err, `Reached maximum number of instances in project "p1"`) + } + + +From 6484e1e0d675ee0b581c983c35c4b926dae7b393 Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Tue, 19 May 2026 13:19:13 +0100 +Subject: [PATCH 4/9] lxd/project/limits/permissions: Removes unused tx + argument from checkInstanceRestrictionsAndAggregateLimits and + AllowInstanceCreation + +Signed-off-by: Thomas Parrott +(cherry picked from commit 83af713afa50cacaf11a045b79b8a10ee10bf227) +--- + lxd/instances_post.go | 4 ++-- + lxd/project/permissions.go | 14 +++++++------- + lxd/project/permissions_test.go | 15 ++++++++------- + 3 files changed, 17 insertions(+), 16 deletions(-) + +diff --git a/lxd/instances_post.go b/lxd/instances_post.go +index 58b677ba68ff..6e7fe98288ac 100644 +--- a/lxd/instances_post.go ++++ b/lxd/instances_post.go +@@ -662,7 +662,7 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data + + // Check restrictions/limits if defined on project. + if restrictions != nil { +- err = project.AllowInstanceCreation(tx, *restrictions, req) ++ err = project.AllowInstanceCreation(*restrictions, req) + if err != nil { + return err + } +@@ -1098,7 +1098,7 @@ func instancesPost(d *Daemon, r *http.Request) response.Response { + + // Check restrictions/limits if defined on project. + if restrictions != nil { +- err = project.AllowInstanceCreation(tx, *restrictions, req) ++ err = project.AllowInstanceCreation(*restrictions, req) + if err != nil { + return err + } +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index 1d7e363da73c..8f5fc85f41d6 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -23,7 +23,7 @@ import ( + + // AllowInstanceCreation returns an error if any project-specific limit or + // restriction is violated when creating a new instance. +-func AllowInstanceCreation(tx *db.ClusterTx, info ProjectInfo, req api.InstancesPost) error { ++func AllowInstanceCreation(info ProjectInfo, req api.InstancesPost) error { + var instanceType instancetype.Type + switch req.Type { + case api.InstanceTypeContainer: +@@ -70,7 +70,7 @@ func AllowInstanceCreation(tx *db.ClusterTx, info ProjectInfo, req api.Instances + return err + } + +- err = checkRestrictionsAndAggregateLimits(tx, &info) ++ err = checkRestrictionsAndAggregateLimits(&info) + if err != nil { + return fmt.Errorf("Failed checking if instance creation allowed: %w", err) + } +@@ -238,7 +238,7 @@ func AllowVolumeCreation(tx *db.ClusterTx, projectName string, req api.StorageVo + Config: req.Config, + }) + +- err = checkRestrictionsAndAggregateLimits(tx, info) ++ err = checkRestrictionsAndAggregateLimits(info) + if err != nil { + return fmt.Errorf("Failed checking if volume creation allowed: %w", err) + } +@@ -297,7 +297,7 @@ func GetImageSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { + + // Check that we would not violate the project limits or restrictions if we + // were to commit the given instances and profiles. +-func checkRestrictionsAndAggregateLimits(tx *db.ClusterTx, info *ProjectInfo) error { ++func checkRestrictionsAndAggregateLimits(info *ProjectInfo) error { + // List of config keys for which we need to check aggregate values + // across all project instances. + aggregateKeys := []string{} +@@ -884,7 +884,7 @@ func AllowInstanceUpdate(tx *db.ClusterTx, projectName, instanceName string, req + return err + } + +- err = checkRestrictionsAndAggregateLimits(tx, info) ++ err = checkRestrictionsAndAggregateLimits(info) + if err != nil { + return fmt.Errorf("Failed checking if instance update allowed: %w", err) + } +@@ -918,7 +918,7 @@ func AllowVolumeUpdate(tx *db.ClusterTx, projectName, volumeName string, req api + info.Volumes[i].Config = req.Config + } + +- err = checkRestrictionsAndAggregateLimits(tx, info) ++ err = checkRestrictionsAndAggregateLimits(info) + if err != nil { + return fmt.Errorf("Failed checking if volume update allowed: %w", err) + } +@@ -948,7 +948,7 @@ func AllowProfileUpdate(tx *db.ClusterTx, projectName, profileName string, req a + info.Profiles[i].Devices = req.Devices + } + +- err = checkRestrictionsAndAggregateLimits(tx, info) ++ err = checkRestrictionsAndAggregateLimits(info) + if err != nil { + return fmt.Errorf("Failed checking if profile update allowed: %w", err) + } +diff --git a/lxd/project/permissions_test.go b/lxd/project/permissions_test.go +index 2a962bac46aa..fe321da096bc 100644 +--- a/lxd/project/permissions_test.go ++++ b/lxd/project/permissions_test.go +@@ -21,18 +21,19 @@ func TestAllowInstanceCreation_NotConfigured(t *testing.T) { + tx, cleanup := db.NewTestClusterTx(t) + defer cleanup() + +- info, err := limits.FetchProject(context.Background(), tx, "default", true) ++ info, err := project.FetchProject(tx, "default", true) + require.NoError(t, err) + require.Nil(t, info) + +- info, err = limits.FetchProject(context.Background(), tx, "default", false) ++ info, err = project.FetchProject(tx, "default", false) + require.NoError(t, err) + + req := api.InstancesPost{ + Name: "c1", + Type: api.InstanceTypeContainer, + } +- err = project.AllowInstanceCreation(tx, *info, req) ++ ++ err = project.AllowInstanceCreation(*info, req) + assert.NoError(t, err) + } + +@@ -66,7 +67,7 @@ func TestAllowInstanceCreation_Below(t *testing.T) { + require.NoError(t, err) + require.NotNil(t, info) + +- err = project.AllowInstanceCreation(tx, *info, req) ++ err = project.AllowInstanceCreation(*info, req) + assert.NoError(t, err) + } + +@@ -101,7 +102,7 @@ func TestAllowInstanceCreation_Above(t *testing.T) { + require.NoError(t, err) + require.NotNil(t, info) + +- err = project.AllowInstanceCreation(tx, *info, req) ++ err = project.AllowInstanceCreation(*info, req) + assert.EqualError(t, err, `Reached maximum number of instances of type "container" in project "p1"`) + } + +@@ -136,7 +137,7 @@ func TestAllowInstanceCreation_DifferentType(t *testing.T) { + require.NoError(t, err) + require.NotNil(t, info) + +- err = project.AllowInstanceCreation(tx, *info, req) ++ err = project.AllowInstanceCreation(*info, req) + assert.NoError(t, err) + } + +@@ -171,7 +172,7 @@ func TestAllowInstanceCreation_AboveInstances(t *testing.T) { + require.NoError(t, err) + require.NotNil(t, info) + +- err = project.AllowInstanceCreation(tx, *info, req) ++ err = project.AllowInstanceCreation(*info, req) + assert.EqualError(t, err, `Reached maximum number of instances in project "p1"`) + } + + +From 3a4349ebdcdb6b7d6f82e1ae4fd17be8046c7b7c Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Tue, 19 May 2026 14:42:55 +0100 +Subject: [PATCH 5/9] shared/osarch: Quote architecture name supplied + +Signed-off-by: Thomas Parrott +(cherry picked from commit 94eb67bb81101073e8fecf18790e7d4967806ee7) +--- + shared/osarch/architectures.go | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/shared/osarch/architectures.go b/shared/osarch/architectures.go +index 341e2dcc567b..c1604fe04110 100644 +--- a/shared/osarch/architectures.go ++++ b/shared/osarch/architectures.go +@@ -120,7 +120,7 @@ func ArchitectureId(arch string) (int, error) { + } + } + +- return ARCH_UNKNOWN, fmt.Errorf("Architecture isn't supported: %s", arch) ++ return ARCH_UNKNOWN, fmt.Errorf("Architecture isn't supported: %q", arch) + } + + func ArchitecturePersonality(arch int) (string, error) { + +From 6560b03a15a539d85486796944afd74d5aead3b3 Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Tue, 19 May 2026 13:54:05 +0100 +Subject: [PATCH 6/9] lxd/instances/post: Validate whether instance snapshot + can be created in createFromBackup + +Signed-off-by: Thomas Parrott +(cherry picked from commit 5aac658c888d52e3a2480a8f879799ac643fdc49) +--- + lxd/instances_post.go | 25 +++++++++++++++++++++++++ + 1 file changed, 25 insertions(+) + +diff --git a/lxd/instances_post.go b/lxd/instances_post.go +index 6e7fe98288ac..ba3e92725d39 100644 +--- a/lxd/instances_post.go ++++ b/lxd/instances_post.go +@@ -666,6 +666,31 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data + if err != nil { + return err + } ++ ++ for i, snapshot := range bInfo.Config.Snapshots { ++ if snapshot == nil { ++ return fmt.Errorf("Nil instance snapshot definition found at index %d", i) ++ } ++ ++ snapshotReq := api.InstancesPost{ ++ InstancePut: api.InstancePut{ ++ Architecture: snapshot.Architecture, ++ Config: snapshot.Config, ++ Devices: snapshot.Devices, ++ Ephemeral: snapshot.Ephemeral, ++ Profiles: snapshot.Profiles, ++ Stateful: snapshot.Stateful, ++ }, ++ Name: bInfo.Name + "/" + snapshot.Name, ++ Source: api.InstanceSource{}, // Only relevant for "copy" or "migration", but may not be nil. ++ Type: api.InstanceType(bInfo.Config.Container.Type), ++ } ++ ++ err = project.AllowInstanceCreation(*restrictions, snapshotReq) ++ if err != nil { ++ return err ++ } ++ } + } + + return nil + +From 4d25d45e5f5a2bc70284f2accc9ba2ab0a3b75cd Mon Sep 17 00:00:00 2001 +From: Thomas Parrott +Date: Wed, 20 May 2026 09:51:18 +0100 +Subject: [PATCH 8/9] lxd/project/permissions: Clarify comment on FetchProject + +Signed-off-by: Thomas Parrott +--- + lxd/project/permissions.go | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index 8f5fc85f41d6..19d4e2c993c2 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -1140,7 +1140,7 @@ type ProjectInfo struct { + // and possibly custom volumes. + // + // If the skipIfNoLimits flag is true, then profiles, instances and volumes +-// won't be loaded if the project has no limits set on it, and nil will be ++// won't be loaded if the project has no restrictions or limits set on it, and nil will be + // returned. + func FetchProject(tx *db.ClusterTx, projectName string, skipIfNoLimits bool) (*ProjectInfo, error) { + ctx := context.Background() + +From: Mathias Gibbens +Subject: Fixup renames +Date: Sat, 27 June 2026 04:42:00 +0000 +--- +diff --git a/lxd/project/permissions.go b/lxd/project/permissions.go +index d8973a484..4e3d45c3d 100644 +--- a/lxd/project/permissions.go ++++ b/lxd/project/permissions.go +@@ -273,7 +273,7 @@ func GetImageSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { + // + // If no limit is in place, return -1. + func GetSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { +- info, err := fetchProject(tx, projectName, true) ++ info, err := FetchProject(tx, projectName, true) + if err != nil { + return -1, err + } +@@ -285,7 +285,7 @@ func GetSpaceBudget(tx *db.ClusterTx, projectName string) (int64, error) { + return getSpaceBudget(info) + } + +-func getSpaceBudget(info *projectInfo) (int64, error) { ++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 -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/120-CVE-2026-48749.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/120-CVE-2026-48749.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/120-CVE-2026-48749.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/120-CVE-2026-48749.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,43 @@ +From 873c8892cefdccf629f911253cfed3099ba76bb6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 22 Jun 2026 14:12:43 -0400 +Subject: [PATCH 1/8] incusd: Reject rootfs symlink for instances +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48749 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/storage/utils.go | 9 ++++++++- + 2 files changed, 16 insertions(+), 1 deletion(-) + +diff --git a/lxd/storage/utils.go b/lxd/storage/utils.go +index b30e69a1e..c1bb6a684 100644 +--- a/lxd/storage/utils.go ++++ b/lxd/storage/utils.go +@@ -438,6 +438,12 @@ func ImageUnpack(imageFile string, vol drivers.Volume, destBlockFile string, sys + return -1, err + } + ++ // Reject a rootfs symlink which could redirect writes to the host filesystem. ++ rootfsInfo, err := os.Lstat(rootfsPath) ++ if err == nil && !rootfsInfo.IsDir() { ++ return -1, fmt.Errorf("Image rootfs isn't a regular directory: %s", imageFile) ++ } ++ + // Check for separate root file. + if shared.PathExists(imageRootfsFile) { + err = os.MkdirAll(rootfsPath, 0755) +@@ -452,7 +458,8 @@ func ImageUnpack(imageFile string, vol drivers.Volume, destBlockFile string, sys + } + + // Check that the container image unpack has resulted in a rootfs dir. +- if !shared.PathExists(rootfsPath) { ++ rootfsInfo, err = os.Lstat(rootfsPath) ++ if err != nil || !rootfsInfo.IsDir() { + return -1, fmt.Errorf("Image is missing a rootfs: %s", imageFile) + } + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/121-CVE-2026-48750.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/121-CVE-2026-48750.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/121-CVE-2026-48750.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/121-CVE-2026-48750.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,36 @@ +From dd38c2364b72ca55a7afd9692a7d2002d210596a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 22 Jun 2026 14:12:43 -0400 +Subject: [PATCH 2/8] incusd/exec: Reject exec-output symlink +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48750 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/instance_exec.go | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/lxd/instance_exec.go b/lxd/instance_exec.go +index 340a26aa7..e497d46cd 100644 +--- a/lxd/instance_exec.go ++++ b/lxd/instance_exec.go +@@ -692,6 +692,12 @@ func instanceExecPost(d *Daemon, r *http.Request) response.Response { + var stdout, stderr *os.File + + if post.RecordOutput { ++ // Reject a symlink which could redirect writes to the host filesystem. ++ logPathInfo, err := os.Lstat(inst.LogPath()) ++ if err != nil || !logPathInfo.IsDir() { ++ return fmt.Errorf("Log path directory isn't a regular directory") ++ } ++ + // Prepare stdout and stderr recording. + stdout, err = os.OpenFile(filepath.Join(inst.LogPath(), fmt.Sprintf("exec_%s.stdout", op.ID())), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { +-- +2.47.3 + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/122-CVE-2026-48751.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/122-CVE-2026-48751.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/122-CVE-2026-48751.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/122-CVE-2026-48751.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,48 @@ +From a6c444cc113192a8263502a34844c383c68e2145 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 22 Jun 2026 14:12:43 -0400 +Subject: [PATCH 3/8] incusd/instance: Enforce project restrictions on snapshot + restore +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48751 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/instance_put.go | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/lxd/instance_put.go b/lxd/instance_put.go +index f3bfad57c..6202610af 100644 +--- a/lxd/instance_put.go ++++ b/lxd/instance_put.go +@@ -224,6 +224,23 @@ func instanceSnapRestore(s *state.State, projectName string, name string, snap s + } + } + ++ // Ensure restoring the snapshot's config doesn't violate project restrictions. ++ profiles := make([]string, 0, len(source.Profiles())) ++ for _, profile := range source.Profiles() { ++ profiles = append(profiles, profile.Name) ++ } ++ ++ err = s.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error { ++ return projecthelpers.AllowInstanceUpdate(tx, projectName, name, api.InstancePut{ ++ Config: source.LocalConfig(), ++ Devices: source.LocalDevices().CloneNative(), ++ Profiles: profiles, ++ }, inst.LocalConfig()) ++ }) ++ if err != nil { ++ return err ++ } ++ + // Generate a new `volatile.uuid.generation` to differentiate this instance restored from a snapshot from the original instance. + source.LocalConfig()["volatile.uuid.generation"] = uuid.New() + +-- +2.47.3 + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/123-CVE-2026-48752.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/123-CVE-2026-48752.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/123-CVE-2026-48752.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/123-CVE-2026-48752.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,187 @@ +From 5fb6ff4758083f4de3ccd69e7e08155a01d6ac72 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 22 Jun 2026 14:12:43 -0400 +Subject: [PATCH 4/8] incusd/instance: Confine template access to instance root +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48752 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/instance_metadata.go | 77 +++++++++++++++++++++------------ + 1 file changed, 50 insertions(+), 27 deletions(-) + +diff --git a/lxd/instance_metadata.go b/lxd/instance_metadata.go +index 708b83700..05484ea4b 100644 +--- a/lxd/instance_metadata.go ++++ b/lxd/instance_metadata.go +@@ -2,8 +2,10 @@ package main + + import ( + "encoding/json" ++ "errors" + "fmt" + "io" ++ "io/fs" + "net/http" + "net/url" + "os" +@@ -468,18 +470,26 @@ func instanceMetadataTemplatesGet(d *Daemon, r *http.Request) response.Response + + defer func() { _ = storagePools.InstanceUnmount(pool, c, nil) }() + ++ // Confine all template access to the instance directory. ++ root, err := os.OpenRoot(c.Path()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ + // Look at the request + templateName := r.FormValue("path") + if templateName == "" { + templates := []string{} +- if !shared.PathExists(filepath.Join(c.Path(), "templates")) { +- return response.SyncResponse(true, templates) +- } + + // List templates +- templatesPath := filepath.Join(c.Path(), "templates") +- entries, err := os.ReadDir(templatesPath) ++ entries, err := fs.ReadDir(root.FS(), "templates") + if err != nil { ++ if errors.Is(err, fs.ErrNotExist) { ++ return response.SyncResponse(true, templates) ++ } ++ + return response.InternalError(err) + } + +@@ -493,19 +503,19 @@ func instanceMetadataTemplatesGet(d *Daemon, r *http.Request) response.Response + } + + // Check if the template exists +- templatePath, err := getContainerTemplatePath(c, templateName) ++ templatePath, err := getContainerTemplatePath(templateName) + if err != nil { + return response.SmartError(err) + } + +- if !shared.PathExists(templatePath) { +- return response.NotFound(fmt.Errorf("Template %q not found", templateName)) +- } +- + // Create a temporary file with the template content (since the container + // storage might not be available when the file is read from FileResponse) +- template, err := os.Open(templatePath) ++ template, err := root.Open(templatePath) + if err != nil { ++ if errors.Is(err, fs.ErrNotExist) { ++ return response.NotFound(fmt.Errorf("Template %q not found", templateName)) ++ } ++ + return response.SmartError(err) + } + +@@ -620,27 +630,33 @@ func instanceMetadataTemplatesPost(d *Daemon, r *http.Request) response.Response + + defer func() { _ = storagePools.InstanceUnmount(pool, c, nil) }() + ++ // Confine all template access to the instance directory. ++ root, err := os.OpenRoot(c.Path()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ + // Look at the request + templateName := r.FormValue("path") + if templateName == "" { + return response.BadRequest(fmt.Errorf("missing path argument")) + } + +- if !shared.PathExists(filepath.Join(c.Path(), "templates")) { +- err := os.MkdirAll(filepath.Join(c.Path(), "templates"), 0711) +- if err != nil { +- return response.SmartError(err) +- } ++ err = root.Mkdir("templates", 0o711) ++ if err != nil && !errors.Is(err, fs.ErrExist) { ++ return response.SmartError(err) + } + + // Check if the template already exists +- templatePath, err := getContainerTemplatePath(c, templateName) ++ templatePath, err := getContainerTemplatePath(templateName) + if err != nil { + return response.SmartError(err) + } + + // Write the new template +- template, err := os.OpenFile(templatePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) ++ template, err := root.OpenFile(templatePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return response.SmartError(err) + } +@@ -739,24 +755,32 @@ func instanceMetadataTemplatesDelete(d *Daemon, r *http.Request) response.Respon + + defer func() { _ = storagePools.InstanceUnmount(pool, c, nil) }() + ++ // Confine all template access to the instance directory. ++ root, err := os.OpenRoot(c.Path()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ + // Look at the request + templateName := r.FormValue("path") + if templateName == "" { + return response.BadRequest(fmt.Errorf("missing path argument")) + } + +- templatePath, err := getContainerTemplatePath(c, templateName) ++ templatePath, err := getContainerTemplatePath(templateName) + if err != nil { + return response.SmartError(err) + } + +- if !shared.PathExists(templatePath) { +- return response.NotFound(fmt.Errorf("Template %q not found", templateName)) +- } +- + // Delete the template +- err = os.Remove(templatePath) ++ err = root.Remove(templatePath) + if err != nil { ++ if errors.Is(err, fs.ErrNotExist) { ++ return response.NotFound(fmt.Errorf("Template %q not found", templateName)) ++ } ++ + return response.InternalError(err) + } + +@@ -765,11 +789,11 @@ func instanceMetadataTemplatesDelete(d *Daemon, r *http.Request) response.Respon + return response.EmptySyncResponse + } + +-// Return the full path of a container template. +-func getContainerTemplatePath(c instance.Instance, filename string) (string, error) { ++// Return the template path relative to the instance root. ++func getContainerTemplatePath(filename string) (string, error) { + if strings.Contains(filename, "/") { + return "", fmt.Errorf("Invalid template filename") + } + +- return filepath.Join(c.Path(), "templates", filename), nil ++ return filepath.Join("templates", filename), nil + } +-- +2.47.3 + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/124-CVE-2026-48755.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/124-CVE-2026-48755.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/124-CVE-2026-48755.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/124-CVE-2026-48755.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,38 @@ +From 0b7b8c94a7c460649601ca97ad6520c757b2fc36 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 22 Jun 2026 14:12:43 -0400 +Subject: [PATCH 5/8] shared/validate: Reject compression algorithm arguments +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48755 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + shared/validate/validate.go | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/shared/validate/validate.go b/shared/validate/validate.go +index 882a6b9fb..16dc88821 100644 +--- a/shared/validate/validate.go ++++ b/shared/validate/validate.go +@@ -576,6 +576,14 @@ func IsCompressionAlgorithm(value string) error { + return errors.New("No compression algorithm specified") + } + ++ // Only allow known-safe arguments (compression levels) to avoid argument injection. ++ allowedArgs := []string{"-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "--rsyncable"} ++ for _, arg := range fields[1:] { ++ if !slices.Contains(allowedArgs, arg) { ++ return fmt.Errorf("Compression algorithm argument %q isn't allowed", arg) ++ } ++ } ++ + // Trim arguments and just look at the command name. + cmd := fields[0] + +-- +2.47.3 + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/125-CVE-2026-48769.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/125-CVE-2026-48769.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/125-CVE-2026-48769.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/125-CVE-2026-48769.patch 2026-06-27 14:07:35.000000000 +0000 @@ -0,0 +1,69 @@ +From 73641782901264148c46e8efe540f75fe0a17495 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 22 Jun 2026 14:12:43 -0400 +Subject: [PATCH 6/8] incusd/images: Validate fingerprint on direct download +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-48769 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/daemon_images.go | 10 ++++++++++ + shared/validate/validate.go | 10 ++++++++++ + 2 files changed, 20 insertions(+) + +diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go +index dda6120e7..6eeb0e78d 100644 +--- a/lxd/daemon_images.go ++++ b/lxd/daemon_images.go +@@ -26,6 +26,7 @@ import ( + "github.com/canonical/lxd/shared/ioprogress" + "github.com/canonical/lxd/shared/logger" + "github.com/canonical/lxd/shared/units" ++ "github.com/canonical/lxd/shared/validate" + "github.com/canonical/lxd/shared/version" + ) + +@@ -276,6 +277,15 @@ func ImageDownload(r *http.Request, s *state.State, op *operations.Operation, ar + + logger.Info("Downloading image", ctxMap) + ++ // For the direct protocol the fingerprint is caller-controlled, so validate ++ // it to avoid path traversal when used as a file name. ++ if protocol == "direct" { ++ err = validate.IsSHA256(fp) ++ if err != nil { ++ return nil, fmt.Errorf("Invalid image fingerprint") ++ } ++ } ++ + // Cleanup any leftover from a past attempt + destDir := shared.VarPath("images") + destName := filepath.Join(destDir, fp) +diff --git a/shared/validate/validate.go b/shared/validate/validate.go +index 16dc88821..78f382af5 100644 +--- a/shared/validate/validate.go ++++ b/shared/validate/validate.go +@@ -538,6 +538,16 @@ func IsUUID(value string) error { + return nil + } + ++// IsSHA256 validates whether a value is a SHA-256 hash in hex form. ++func IsSHA256(value string) error { ++ match, _ := regexp.MatchString(`^[0-9a-f]{64}$`, value) ++ if !match { ++ return fmt.Errorf("Invalid SHA-256 hash") ++ } ++ ++ return nil ++} ++ + // IsPCIAddress validates whether a value is a PCI address. + func IsPCIAddress(value string) error { + match, _ := regexp.MatchString(`^(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$`, value) +-- +2.47.3 + diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/126-CVE-2026-55621.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/126-CVE-2026-55621.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/126-CVE-2026-55621.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/126-CVE-2026-55621.patch 2026-06-27 17:04:42.000000000 +0000 @@ -0,0 +1,41 @@ +From eec1cf9504c18ad7c3c36ae0b62d21c05c25702c Mon Sep 17 00:00:00 2001 +From: Nikita Mezhenskyi +Date: Tue, 23 Jun 2026 16:10:54 -0400 +Subject: [PATCH] lxd: check for view permission on source project when copying + volume + +This effectively fixes the https://github.com/canonical/lxd/security/advisories/GHSA-7mr3-28h5-m5vx + +Inspired-by: https://github.com/canonical/lxd/commit/e2635a26957737ec4a77dc28dd7b84bd380881d6 +Signed-off-by: Nikita Mezhenskyi +Rebased-by: Mathias Gibbens +--- + lxd/storage_volumes.go | 15 +++++++++++++++ + 1 file changed, 15 insertions(+) + +diff --git a/lxd/storage_volumes.go b/lxd/storage_volumes.go +index cf5ada0a2..cf01d8093 100644 +--- a/lxd/storage_volumes.go ++++ b/lxd/storage_volumes.go +@@ -593,6 +593,21 @@ func storagePoolVolumesTypePost(d *Daemon, r *http.Request) response.Response { + return response.BadRequest(fmt.Errorf("Currently not allowed to create storage volumes of type %q", req.Type)) + } + ++ if req.Source.Type == "copy" { ++ // Determine the effective source project for the permission check without mutating ++ // req.Source.Project. The cluster-internal copy path forwards req.Source.Project to the ++ // source member, where an empty value means "same as the request project". Setting it to an ++ // explicit value here would make that path treat the copy as a cross-project move and fail. ++ sourceProject := req.Source.Project ++ if sourceProject == "" { ++ sourceProject = projectParam(r) ++ } ++ ++ if !s.Authorizer.UserHasPermission(r, sourceProject, "view") { ++ return response.Forbidden(nil) ++ } ++ } ++ + poolID, err := s.DB.Cluster.GetStoragePoolID(poolName) + if err != nil { + return response.SmartError(err) diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/127-CVE-2026-55622.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/127-CVE-2026-55622.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/127-CVE-2026-55622.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/127-CVE-2026-55622.patch 2026-06-27 15:10:48.000000000 +0000 @@ -0,0 +1,40 @@ +From bc1e20ea32dff6ae2df08803eee82832959ec80e Mon Sep 17 00:00:00 2001 +From: Nikita Mezhenskyi +Date: Tue, 23 Jun 2026 16:01:07 -0400 +Subject: [PATCH] lxd: check for view permission on source project when copying + instance + +This effectively fixes the https://github.com/canonical/lxd/security/advisories/GHSA-qx75-2p3r-pwm5 + +Inspired-by: https://github.com/canonical/lxd/commit/97cb037794cb6b21c5a184eb5f3292207adbf6b2 +Signed-off-by: Nikita Mezhenskyi +Rebased-by: Mathias Gibbens +--- + lxd/instances_post.go | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/lxd/instances_post.go b/lxd/instances_post.go +index ad92a6331..7049162bb 100644 +--- a/lxd/instances_post.go ++++ b/lxd/instances_post.go +@@ -908,6 +908,20 @@ func instancesPost(d *Daemon, r *http.Request) response.Response { + return response.InternalError(fmt.Errorf("Failed to check for cluster state: %w", err)) + } + ++ if req.Source.Type == "copy" { ++ if req.Source.Source == "" { ++ return response.BadRequest(fmt.Errorf("Must specify a source instance")) ++ } ++ ++ if req.Source.Project == "" { ++ req.Source.Project = targetProjectName ++ } ++ ++ if !s.Authorizer.UserHasPermission(r, req.Source.Project, "view") { ++ return response.Forbidden(nil) ++ } ++ } ++ + var targetProject *api.Project + var profiles []api.Profile + var sourceInst *dbCluster.Instance 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-05-01 23:29:36.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/series 2026-06-27 15:07:16.000000000 +0000 @@ -29,3 +29,13 @@ 115-CVE-2026-41648.patch 116-CVE-2026-41684.patch 117-CVE-2026-41685.patch +118-CVE-2026-9639.patch +119-CVE-2026-9640.patch +120-CVE-2026-48749.patch +121-CVE-2026-48750.patch +122-CVE-2026-48751.patch +123-CVE-2026-48752.patch +124-CVE-2026-48755.patch +125-CVE-2026-48769.patch +126-CVE-2026-55621.patch +127-CVE-2026-55622.patch