Version in base suite: 6.0.4-2+deb13u8 Base version: incus_6.0.4-2+deb13u8 Target version: incus_6.0.4-2+deb13u9 Base file: /srv/ftp-master.debian.org/ftp/pool/main/i/incus/incus_6.0.4-2+deb13u8.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/i/incus/incus_6.0.4-2+deb13u9.dsc changelog | 19 + patches/134-cherry-pick-fix-large-nft-ruleset.patch | 29 ++ patches/135-CVE-2026-62313.patch | 56 +++ patches/136-CVE-2026-62867.patch | 47 +++ patches/137-CVE-2026-62940.patch | 43 +++ patches/138-CVE-2026-62941.patch | 42 ++ patches/139-CVE-2026-63125.patch | 40 ++ patches/140-CVE-2026-63343.patch | 119 ++++++++ patches/141-GHSA-26gp-p5fw-3r2h.patch | 42 ++ patches/142-GHSA-4qxq-p5hm-3q3p.patch | 55 +++ patches/143a-GHSA-67qw-68v3-36h6.patch | 80 +++++ patches/143b-GHSA-67qw-68v3-36h6.patch | 63 ++++ patches/144-GHSA-m3j6-p3v3-qmjv.patch | 66 ++++ patches/145-GHSA-p2v3-6wvc-cv3p.patch | 68 ++++ patches/146-incus-7.3-fixes.patch | 286 ++++++++++++++++++++ patches/series | 14 16 files changed, 1069 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp7d15l3il/incus_6.0.4-2+deb13u8.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp7d15l3il/incus_6.0.4-2+deb13u9.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-06-25 00:53:11.000000000 +0000 +++ incus-6.0.4/debian/changelog 2026-07-30 22:57:51.000000000 +0000 @@ -1,3 +1,22 @@ +incus (6.0.4-2+deb13u9) trixie-security; urgency=high + + * Cherry-pick upstream fix for large nft ruleset performance + * Cherry-pick fixes for the following security issues: + - CVE-2026-62313 / GHSA-53cg-qvg7-m8vg + - CVE-2026-62867 / GHSA-q7xw-r4w2-2wcm + - CVE-2026-62940 / GHSA-qw5c-v953-38gw + - CVE-2026-62941 / GHSA-mq9x-prm8-3vpw + - CVE-2026-63125 / GHSA-6rqx-22hc-qm36 + - CVE-2026-63343 / GHSA-fmjx-5j3g-997p + - GHSA-26gp-p5fw-3r2h + - GHSA-4qxq-p5hm-3q3p + - GHSA-67qw-68v3-36h6 + - GHSA-m3j6-p3v3-qmjv + - GHSA-p2v3-6wvc-cv3p + * Cherry-pick four additional security fixes not assigned CVEs + + -- Mathias Gibbens Thu, 30 Jul 2026 22:57:51 +0000 + incus (6.0.4-2+deb13u8) trixie-security; urgency=high * Cherry-pick fixes for the following security issues: diff -Nru incus-6.0.4/debian/patches/134-cherry-pick-fix-large-nft-ruleset.patch incus-6.0.4/debian/patches/134-cherry-pick-fix-large-nft-ruleset.patch --- incus-6.0.4/debian/patches/134-cherry-pick-fix-large-nft-ruleset.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/134-cherry-pick-fix-large-nft-ruleset.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,29 @@ +From c0b8278d3bf0db1651f3b885d831873275641cdb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Thu, 21 May 2026 11:52:39 +0000 +Subject: [PATCH] incusd/firewall/nftables: Use terse mode to improve + performance +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Closes #3322 + +Signed-off-by: Stéphane Graber +--- + internal/server/firewall/drivers/drivers_nftables.go | 2 +- + 1 file changed, 1 insertions(+), 1 deletions(-) + +diff --git a/internal/server/firewall/drivers/drivers_nftables.go b/internal/server/firewall/drivers/drivers_nftables.go +index 38ec76b806d..37a6a1592ba 100644 +--- a/internal/server/firewall/drivers/drivers_nftables.go ++++ b/internal/server/firewall/drivers/drivers_nftables.go +@@ -86,7 +86,7 @@ type nftGenericItem struct { + // nftParseRuleset parses the ruleset and returns the generic parts as a slice of items. + func (d Nftables) nftParseRuleset() ([]nftGenericItem, error) { + // Dump ruleset as JSON. Use -nn flags to avoid doing DNS lookups of IPs mentioned in any rules. +- cmd := exec.Command("nft", "--json", "-nn", "list", "ruleset") ++ cmd := exec.Command("nft", "--terse", "--json", "-nn", "list", "ruleset") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err diff -Nru incus-6.0.4/debian/patches/135-CVE-2026-62313.patch incus-6.0.4/debian/patches/135-CVE-2026-62313.patch --- incus-6.0.4/debian/patches/135-CVE-2026-62313.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/135-CVE-2026-62313.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,56 @@ +From 9db983f4420adae714458586f3f3de9c41c6221b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:56:00 -0400 +Subject: [PATCH] incusd/project: Enforce isolated restriction when idmap key + omitted +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +restricted.containers.privilege=isolated only rejected an explicit +security.idmap.isolated=false, but the key defaults to non-isolated when +omitted. Require containers to explicitly enable isolation. + +This addresses CVE-2026-62313 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/project/permissions.go | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/internal/server/project/permissions.go b/internal/server/project/permissions.go +index 798d29ca6..6878ecdbf 100644 +--- a/internal/server/project/permissions.go ++++ b/internal/server/project/permissions.go +@@ -584,6 +584,7 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [ + + allowContainerLowLevel := false + allowVMLowLevel := false ++ requireIsolated := false + var allowedIDMapHostUIDs, allowedIDMapHostGIDs []idmap.Entry + + for i := range allRestrictions { +@@ -632,6 +633,8 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [ + return nil + } + ++ requireIsolated = restrictionValue == "isolated" ++ + containerConfigChecks["security.idmap.isolated"] = func(instanceValue string) error { + if restrictionValue == "isolated" && util.IsFalseOrEmpty(instanceValue) { + return fmt.Errorf("Non-isolated containers are forbidden") +@@ -816,6 +819,11 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [ + isContainerOrProfile := instType == instancetype.Container || instType == instancetype.Any + isVMOrProfile := instType == instancetype.VM || instType == instancetype.Any + ++ // Non-isolation is the default, so when isolation is required, the container must explicitly enable it. ++ if requireIsolated && instType == instancetype.Container && util.IsFalseOrEmpty(config["security.idmap.isolated"]) { ++ return fmt.Errorf(`Non-isolated containers are forbidden on %s %q of project %q ("security.idmap.isolated" must be set to "true")`, entityTypeLabel, entityName, project.Name) ++ } ++ + for key, value := range config { + if ((isContainerOrProfile && !allowContainerLowLevel) || (isVMOrProfile && !allowVMLowLevel)) && key == "raw.idmap" { + // If the low-level raw.idmap is used check whether the raw.idmap host IDs +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/136-CVE-2026-62867.patch incus-6.0.4/debian/patches/136-CVE-2026-62867.patch --- incus-6.0.4/debian/patches/136-CVE-2026-62867.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/136-CVE-2026-62867.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,47 @@ +From af593e4c677dd404e82c7a4bc38bc23b8a5ff93d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:37:25 -0400 +Subject: [PATCH] incusd/project: Restrict volume creation options in + restricted projects +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +A user in a restricted project could set block.create_options to inject +arguments into the filesystem creation command run as root. Restrict such +projects to the pool's configured default for that option. + +This addresses CVE-2026-62867 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/project/permissions.go | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/internal/server/project/permissions.go b/internal/server/project/permissions.go +index 7e963017a..82f573882 100644 +--- a/internal/server/project/permissions.go ++++ b/internal/server/project/permissions.go +@@ -268,6 +268,19 @@ func AllowVolumeCreation(tx *db.ClusterTx, projectName string, poolName string, + return nil + } + ++ // Restricted projects can't override low-level volume options that are passed to ++ // filesystem tooling running as root; they may only use the pool's configured default. ++ if util.IsTrue(info.Project.Config["restricted"]) { ++ _, pool, _, err := tx.GetStoragePool(context.Background(), poolName) ++ if err != nil { ++ return err ++ } ++ ++ if req.Config["block.create_options"] != "" && req.Config["block.create_options"] != pool.Config["volume.block.create_options"] { ++ return fmt.Errorf(`Storage volume option "block.create_options" cannot be set in a restricted project`) ++ } ++ } ++ + // Add the volume being created. + info.Volumes = append(info.Volumes, db.StorageVolumeArgs{ + Name: req.Name, +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/137-CVE-2026-62940.patch incus-6.0.4/debian/patches/137-CVE-2026-62940.patch --- incus-6.0.4/debian/patches/137-CVE-2026-62940.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/137-CVE-2026-62940.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,43 @@ +From 55134c93343071bc1df672983e97beb7c135ec48 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:54:08 -0400 +Subject: [PATCH] incusd/instance: Enforce project restrictions on migration + overrides +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The migration handler applied user-supplied config, device and profile +overrides without any project restriction check, letting a restricted +project set keys like security.privileged or raw.lxc. + +This addresses CVE-2026-62940 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/instance_post.go | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/cmd/incusd/instance_post.go b/cmd/incusd/instance_post.go +index 6706d1d1e..c91711b7f 100644 +--- a/cmd/incusd/instance_post.go ++++ b/cmd/incusd/instance_post.go +@@ -634,6 +634,15 @@ func migrateInstance(ctx context.Context, s *state.State, inst instance.Instance + targetInstInfo.Profiles = req.Profiles + } + ++ // Enforce project restrictions against the overridden config, as the migration ++ // request can otherwise set restricted keys (e.g. security.privileged, raw.lxc). ++ err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { ++ return project.AllowInstanceUpdate(tx, inst.Project().Name, inst.Name(), targetInstInfo.Writable(), inst.LocalConfig()) ++ }) ++ if err != nil { ++ return err ++ } ++ + // Handle storage pool override. + if req.Pool != "" { + rootDevKey, rootDev, err := internalInstance.GetRootDiskDevice(inst.ExpandedDevices().CloneNative()) +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/138-CVE-2026-62941.patch incus-6.0.4/debian/patches/138-CVE-2026-62941.patch --- incus-6.0.4/debian/patches/138-CVE-2026-62941.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/138-CVE-2026-62941.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,42 @@ +From 855ba3d951fe4dc669f216f6ebae0edcc17529ff Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:52:19 -0400 +Subject: [PATCH] incusd/instances: Re-check restrictions after copy config + merge +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The source instance's config is merged into the request only after the +initial project restriction check, letting a cross-project copy carry +restricted keys into the target project. Re-check the merged config. + +This addresses CVE-2026-62941 + +Signed-off-by: Stéphane Graber +--- + cmd/incusd/instances_post.go | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/cmd/incusd/instances_post.go b/cmd/incusd/instances_post.go +index a2cf3857d..9b0ec68ec 100644 +--- a/cmd/incusd/instances_post.go ++++ b/cmd/incusd/instances_post.go +@@ -707,6 +707,15 @@ func createFromCopy(ctx context.Context, s *state.State, r *http.Request, projec + req.Devices[key] = value + } + ++ // Re-check project restrictions against the fully merged config, as the source ++ // instance's config and devices (including restricted keys) are only merged in above. ++ err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { ++ return project.AllowInstanceCreation(tx, targetProject, *req) ++ }) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + if req.Stateful { + sourceName, _, _ := api.GetParentAndSnapshotName(source.Name()) + if sourceName != req.Name { +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/139-CVE-2026-63125.patch incus-6.0.4/debian/patches/139-CVE-2026-63125.patch --- incus-6.0.4/debian/patches/139-CVE-2026-63125.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/139-CVE-2026-63125.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,40 @@ +From bd7e4d99762367c6429e457112ad21822f58c42d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:43:20 -0400 +Subject: [PATCH] incusd/storage: Confine backup.yaml write to instance root +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-63125 + +Signed-off-by: Stéphane Graber +--- + internal/server/storage/backend.go | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 872ae3a44..9eeec321d 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -7206,9 +7206,17 @@ func (b *backend) UpdateInstanceBackupFile(inst instance.Instance, snapshots boo + + // Update pool information in the backup.yaml file. + err = vol.MountTask(func(mountPath string, op *operations.Operation) error { ++ // Confine the write to the instance directory to avoid following image-planted symlinks. ++ root, err := os.OpenRoot(inst.Path()) ++ if err != nil { ++ return err ++ } ++ ++ defer func() { _ = root.Close() }() ++ + // Write the YAML + path := filepath.Join(inst.Path(), "backup.yaml") +- f, err := os.Create(path) ++ f, err := root.OpenFile("backup.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o400) + if err != nil { + return fmt.Errorf("Failed to create file %q: %w", path, err) + } +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/140-CVE-2026-63343.patch incus-6.0.4/debian/patches/140-CVE-2026-63343.patch --- incus-6.0.4/debian/patches/140-CVE-2026-63343.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/140-CVE-2026-63343.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,119 @@ +From c479c1cbcc838fc830323291061ad9f9d30559f8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:44:35 -0400 +Subject: [PATCH] incusd/instance: Confine metadata.yaml access to instance + root +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses CVE-2026-63343 + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/instance_metadata.go | 49 +++++++++++++++++++++++---------- + 1 file changed, 34 insertions(+), 15 deletions(-) + +diff --git a/cmd/incusd/instance_metadata.go b/cmd/incusd/instance_metadata.go +index 15987850d..e8cec9d4d 100644 +--- a/cmd/incusd/instance_metadata.go ++++ b/cmd/incusd/instance_metadata.go +@@ -25,7 +25,6 @@ import ( + localUtil "github.com/lxc/incus/v6/internal/server/util" + "github.com/lxc/incus/v6/shared/api" + "github.com/lxc/incus/v6/shared/logger" +- "github.com/lxc/incus/v6/shared/util" + ) + + // swagger:operation GET /1.0/instances/{name}/metadata instances instance_metadata_get +@@ -115,15 +114,21 @@ func instanceMetadataGet(d *Daemon, r *http.Request) response.Response { + + defer func() { _ = storagePools.InstanceUnmount(pool, c, nil) }() + +- // If missing, just return empty result +- metadataPath := filepath.Join(c.Path(), "metadata.yaml") +- if !util.PathExists(metadataPath) { +- return response.SyncResponse(true, api.ImageMetadata{}) ++ // Confine metadata access to the instance directory. ++ root, err := os.OpenRoot(c.Path()) ++ if err != nil { ++ return response.SmartError(err) + } + +- // Read the metadata +- metadataFile, err := os.Open(metadataPath) ++ defer func() { _ = root.Close() }() ++ ++ // Read the metadata (if missing, just return empty result). ++ metadataFile, err := root.Open("metadata.yaml") + if err != nil { ++ if errors.Is(err, fs.ErrNotExist) { ++ return response.SyncResponse(true, api.ImageMetadata{}) ++ } ++ + return response.InternalError(err) + } + +@@ -227,15 +232,22 @@ func instanceMetadataPatch(d *Daemon, r *http.Request) response.Response { + + defer func() { _ = storagePools.InstanceUnmount(pool, inst, nil) }() + ++ // Confine metadata access to the instance directory. ++ root, err := os.OpenRoot(inst.Path()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ + // Read the existing data. +- metadataPath := filepath.Join(inst.Path(), "metadata.yaml") + metadata := api.ImageMetadata{} +- if util.PathExists(metadataPath) { +- metadataFile, err := os.Open(metadataPath) +- if err != nil { +- return response.InternalError(err) +- } ++ metadataFile, err := root.Open("metadata.yaml") ++ if err != nil && !errors.Is(err, fs.ErrNotExist) { ++ return response.InternalError(err) ++ } + ++ if err == nil { + defer func() { _ = metadataFile.Close() }() + + data, err := io.ReadAll(metadataFile) +@@ -364,9 +376,27 @@ func doInstanceMetadataUpdate(s *state.State, inst instance.Instance, metadata a + return response.BadRequest(err) + } + +- // Update the metadata. +- metadataPath := filepath.Join(inst.Path(), "metadata.yaml") +- err = os.WriteFile(metadataPath, data, 0o644) ++ // Update the metadata (confined to the instance directory). ++ root, err := os.OpenRoot(inst.Path()) ++ if err != nil { ++ return response.InternalError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ ++ f, err := root.Create("metadata.yaml") ++ if err != nil { ++ return response.InternalError(err) ++ } ++ ++ defer func() { _ = f.Close() }() ++ ++ err = f.Chmod(0o644) ++ if err != nil { ++ return response.InternalError(err) ++ } ++ ++ _, err = f.Write(data) + if err != nil { + return response.InternalError(err) + } +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/141-GHSA-26gp-p5fw-3r2h.patch incus-6.0.4/debian/patches/141-GHSA-26gp-p5fw-3r2h.patch --- incus-6.0.4/debian/patches/141-GHSA-26gp-p5fw-3r2h.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/141-GHSA-26gp-p5fw-3r2h.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,42 @@ +From 17b94377b1f7e9ee07c762f98fbd126f98410a58 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:50:44 -0400 +Subject: [PATCH] incusd/instances: Validate instance name on backup import +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-26gp-p5fw-3r2h (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/instances_post.go | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/cmd/incusd/instances_post.go b/cmd/incusd/instances_post.go +index 945946225..5379e9329 100644 +--- a/cmd/incusd/instances_post.go ++++ b/cmd/incusd/instances_post.go +@@ -732,6 +732,19 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data + bInfo.Name = instanceName + } + ++ // Validate the instance and snapshot names to avoid path traversal when used as path segments. ++ err = instance.ValidName(bInfo.Name, false) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ ++ for _, snapName := range bInfo.Snapshots { ++ err = instance.ValidName(bInfo.Name+internalInstance.SnapshotDelimiter+snapName, true) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ } ++ + logger.Debug("Backup file info loaded", logger.Ctx{ + "type": bInfo.Type, + "name": bInfo.Name, +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/142-GHSA-4qxq-p5hm-3q3p.patch incus-6.0.4/debian/patches/142-GHSA-4qxq-p5hm-3q3p.patch --- incus-6.0.4/debian/patches/142-GHSA-4qxq-p5hm-3q3p.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/142-GHSA-4qxq-p5hm-3q3p.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,55 @@ +From 6bdfa37c05908c901f0842393258e3b2aa6d7111 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:45:40 -0400 +Subject: [PATCH] incusd/instance/qemu: Confine template access to instance + root +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The template traversal guard from CVE-2026-48752 was only applied to the +LXC driver. Apply the same checks to the QEMU driver. + +This addresses GHSA-4qxq-p5hm-3q3p (CVE pending) + +Signed-off-by: Stéphane Graber +--- + .../server/instance/drivers/driver_qemu.go | 23 +++++++++++++++++++ + 1 file changed, 23 insertions(+) + +diff --git a/internal/server/instance/drivers/driver_qemu.go b/internal/server/instance/drivers/driver_qemu.go +index 7758ba91d..f5c9aa026 100644 +--- a/internal/server/instance/drivers/driver_qemu.go ++++ b/internal/server/instance/drivers/driver_qemu.go +@@ -3778,6 +3778,29 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e + return nil + } + ++ // Perform some early security checks on the template itself. ++ if filepath.Base(tpl.Template) != tpl.Template { ++ return errors.New("Template path is attempting to read outside of template directory") ++ } ++ ++ tplDirStat, err := os.Lstat(d.TemplatesPath()) ++ if err != nil { ++ return fmt.Errorf("Couldn't access template directory: %w", err) ++ } ++ ++ if !tplDirStat.IsDir() { ++ return errors.New("Template directory isn't a regular directory") ++ } ++ ++ tplFileStat, err := os.Lstat(filepath.Join(d.TemplatesPath(), tpl.Template)) ++ if err != nil { ++ return fmt.Errorf("Couldn't access template file: %w", err) ++ } ++ ++ if tplFileStat.Mode()&os.ModeSymlink == os.ModeSymlink { ++ return errors.New("Template file is a symlink") ++ } ++ + // Create the file itself. + w, err = os.Create(filepath.Join(path, fmt.Sprintf("%s.out", tpl.Template))) + if err != nil { +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/143a-GHSA-67qw-68v3-36h6.patch incus-6.0.4/debian/patches/143a-GHSA-67qw-68v3-36h6.patch --- incus-6.0.4/debian/patches/143a-GHSA-67qw-68v3-36h6.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/143a-GHSA-67qw-68v3-36h6.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,80 @@ +From 3ae603a22d886b1d5d04a265fff791ac797e4edf Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 8 Aug 2025 01:21:46 -0400 +Subject: [PATCH] shared/validate: Implement IsAPIName +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + shared/validate/validate.go | 37 +++++++++++++++++++++++++++++++++---- + 1 file changed, 33 insertions(+), 4 deletions(-) + +diff --git a/shared/validate/validate.go b/shared/validate/validate.go +index 63f4fbfe9..759146770 100644 +--- a/shared/validate/validate.go ++++ b/shared/validate/validate.go +@@ -2,6 +2,7 @@ package validate + + import ( + "bytes" ++ "errors" + "fmt" + "net" + "net/url" +@@ -12,6 +13,7 @@ import ( + "strconv" + "strings" + "time" ++ "unicode" + + "github.com/google/uuid" + "github.com/kballard/go-shellquote" +@@ -548,6 +550,45 @@ func IsURLSegmentSafe(value string) error { + return nil + } + ++// IsAPIName checks whether the provided value is a suitable name for an API object. ++func IsAPIName(value string, allowSlashes bool) error { ++ // Limit length to 64 characters. ++ if len(value) > 64 { ++ return errors.New("Maximum name length is 64 characters") ++ } ++ ++ // Check for unicode characters. ++ for _, r := range value { ++ if unicode.IsSpace(r) { ++ return errors.New("Name cannot contain white space") ++ } ++ } ++ ++ // Check for special URL characters. ++ reservedChars := []string{"?", "&", "+", "\"", "'", "`", "*"} ++ if !allowSlashes { ++ reservedChars = append(reservedChars, "/") ++ } ++ ++ for _, char := range reservedChars { ++ if strings.Contains(value, char) { ++ return fmt.Errorf("Name contains invalid character %q", char) ++ } ++ } ++ ++ // Check beginning and end. ++ match, err := regexp.MatchString(`^[a-zA-Z0-9]+.*[a-zA-Z0-9]+$`, value) ++ if err != nil { ++ return err ++ } ++ ++ if !match { ++ return errors.New("Names must start and end with an alphanumeric character") ++ } ++ ++ return nil ++} ++ + // IsUUID validates whether a value is a UUID. + func IsUUID(value string) error { + _, err := uuid.Parse(value) diff -Nru incus-6.0.4/debian/patches/143b-GHSA-67qw-68v3-36h6.patch incus-6.0.4/debian/patches/143b-GHSA-67qw-68v3-36h6.patch --- incus-6.0.4/debian/patches/143b-GHSA-67qw-68v3-36h6.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/143b-GHSA-67qw-68v3-36h6.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,63 @@ +From a74fc0a23d5364f66654ae74e77140686ed35895 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:49:01 -0400 +Subject: [PATCH] incusd/storage: Validate volume name on ISO and backup import +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-67qw-68v3-36h6 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/storage_volumes.go | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + +diff --git a/cmd/incusd/storage_volumes.go b/cmd/incusd/storage_volumes.go +index 54c2e5254..88e8ef0e7 100644 +--- a/cmd/incusd/storage_volumes.go ++++ b/cmd/incusd/storage_volumes.go +@@ -45,6 +45,7 @@ import ( + "github.com/lxc/incus/v6/shared/revert" + localtls "github.com/lxc/incus/v6/shared/tls" + "github.com/lxc/incus/v6/shared/util" ++ "github.com/lxc/incus/v6/shared/validate" + ) + + var storagePoolVolumesCmd = APIEndpoint{ +@@ -2221,6 +2222,12 @@ func createStoragePoolVolumeFromISO(s *state.State, r *http.Request, requestProj + return response.BadRequest(fmt.Errorf("Missing volume name")) + } + ++ // Validate the volume name to avoid path traversal when used as a path segment. ++ err := validate.IsAPIName(volName, false) ++ if err != nil { ++ return response.BadRequest(fmt.Errorf("Invalid volume name: %w", err)) ++ } ++ + // Create isos directory if needed. + if !util.PathExists(internalUtil.VarPath("isos")) { + err := os.MkdirAll(internalUtil.VarPath("isos"), 0o644) +@@ -2358,6 +2365,19 @@ func createStoragePoolVolumeFromBackup(s *state.State, r *http.Request, requestP + bInfo.Name = volName + } + ++ // Validate the volume and snapshot names to avoid path traversal when used as path segments. ++ err = validate.IsAPIName(bInfo.Name, false) ++ if err != nil { ++ return response.BadRequest(fmt.Errorf("Invalid volume name: %w", err)) ++ } ++ ++ for _, snapName := range bInfo.Snapshots { ++ err = validate.IsAPIName(snapName, false) ++ if err != nil { ++ return response.BadRequest(fmt.Errorf("Invalid snapshot name: %w", err)) ++ } ++ } ++ + logger.Debug("Backup file info loaded", logger.Ctx{ + "type": bInfo.Type, + "name": bInfo.Name, +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/144-GHSA-m3j6-p3v3-qmjv.patch incus-6.0.4/debian/patches/144-GHSA-m3j6-p3v3-qmjv.patch --- incus-6.0.4/debian/patches/144-GHSA-m3j6-p3v3-qmjv.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/144-GHSA-m3j6-p3v3-qmjv.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,66 @@ +From 67392b4f8bdfb465a366dcd62f8fd23cd4481066 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:39:16 -0400 +Subject: [PATCH] internal/instance: Prevent line breaks in NVIDIA config + values +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-m3j6-p3v3-qmjv (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/instance/config.go | 15 ++++++++++++--- + 1 file changed, 12 insertions(+), 3 deletions(-) + +diff --git a/internal/instance/config.go b/internal/instance/config.go +index 98e732b82..0a3ce8572 100644 +--- a/internal/instance/config.go ++++ b/internal/instance/config.go +@@ -18,6 +18,15 @@ func IsUserConfig(key string) bool { + return strings.HasPrefix(key, "user.") + } + ++// isNvidiaConfigValue rejects line breaks that would allow injecting arbitrary directives into the generated LXC configuration. ++func isNvidiaConfigValue(value string) error { ++ if strings.ContainsAny(value, "\r\n") { ++ return errors.New("NVIDIA configuration values cannot contain line breaks") ++ } ++ ++ return nil ++} ++ + // ConfigVolatilePrefix indicates the prefix used for volatile config keys. + const ConfigVolatilePrefix = "volatile." + +@@ -632,7 +641,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{ + // liveupdate: no + // condition: container + // shortdesc: What driver capabilities the instance needs +- "nvidia.driver.capabilities": validate.IsAny, ++ "nvidia.driver.capabilities": isNvidiaConfigValue, + + // gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.cuda) + // The specified version expression is used to set `libnvidia-container NVIDIA_REQUIRE_CUDA`. +@@ -641,7 +650,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{ + // liveupdate: no + // condition: container + // shortdesc: Required CUDA version +- "nvidia.require.cuda": validate.IsAny, ++ "nvidia.require.cuda": isNvidiaConfigValue, + + // gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.driver) + // The specified version expression is used to set `libnvidia-container NVIDIA_REQUIRE_DRIVER`. +@@ -650,7 +659,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{ + // liveupdate: no + // condition: container + // shortdesc: Required driver version +- "nvidia.require.driver": validate.IsAny, ++ "nvidia.require.driver": isNvidiaConfigValue, + + // Caller is responsible for full validation of any raw.* value. + +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/145-GHSA-p2v3-6wvc-cv3p.patch incus-6.0.4/debian/patches/145-GHSA-p2v3-6wvc-cv3p.patch --- incus-6.0.4/debian/patches/145-GHSA-p2v3-6wvc-cv3p.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/145-GHSA-p2v3-6wvc-cv3p.patch 2026-07-30 22:23:09.000000000 +0000 @@ -0,0 +1,68 @@ +From 1f3da3ae6f8afaa58d03a180a0eabb81739dd184 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 11:47:49 -0400 +Subject: [PATCH] incusd/images: Validate image fingerprint for all protocols +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The fingerprint validation added for CVE-2026-48769 was only applied to +the direct protocol. Validate it for all protocols and re-check after it +is taken from the remote server's response. + +This addresses GHSA-p2v3-6wvc-cv3p (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/daemon_images.go | 21 ++++++++++++++------- + 1 file changed, 14 insertions(+), 7 deletions(-) + +diff --git a/cmd/incusd/daemon_images.go b/cmd/incusd/daemon_images.go +index 5f7db90c8..0d3087715 100644 +--- a/cmd/incusd/daemon_images.go ++++ b/cmd/incusd/daemon_images.go +@@ -8,6 +8,7 @@ import ( + "net/http" + "os" + "path/filepath" ++ "regexp" + "slices" + "time" + +@@ -308,13 +309,12 @@ func ImageDownload(ctx context.Context, r *http.Request, s *state.State, op *ope + + 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, false, fmt.Errorf("Invalid image fingerprint") +- } ++ // The fingerprint is used as a file name, so reject anything that isn't a ++ // partial or full hex fingerprint to avoid path traversal. It's re-validated ++ // as a full SHA-256 once expanded from the remote below. ++ match, _ := regexp.MatchString("^[0-9a-f]{1,64}$", fp) ++ if !match { ++ return nil, false, fmt.Errorf("Invalid image fingerprint") + } + + // Cleanup any leftover from a past attempt +@@ -569,6 +569,13 @@ func ImageDownload(ctx context.Context, r *http.Request, s *state.State, op *ope + // Image is in the DB now, don't wipe on-disk files on failure + failure = false + ++ // Re-validate the fingerprint as it may have been updated from the remote ++ // server's response, and it is used as a file name below. ++ err = validate.IsSHA256(fp) ++ if err != nil { ++ return nil, false, fmt.Errorf("Invalid image fingerprint") ++ } ++ + // Check if the image path changed (private images) + newDestName := filepath.Join(destDir, fp) + if newDestName != destName { +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/146-incus-7.3-fixes.patch incus-6.0.4/debian/patches/146-incus-7.3-fixes.patch --- incus-6.0.4/debian/patches/146-incus-7.3-fixes.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/146-incus-7.3-fixes.patch 2026-07-30 22:53:34.000000000 +0000 @@ -0,0 +1,286 @@ +From 23066254f5dcc16b9e9022e938142ba512098858 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 12:20:05 -0400 +Subject: [PATCH 14/17] incusd/instance: Fix NVIDIA require.cuda and + require.driver handling +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The condition was inverted, so a configured value was silently dropped +while an empty value wrote an empty environment variable. Only write the +variable when a value is set. + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/instance/drivers/driver_lxc.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/internal/server/instance/drivers/driver_lxc.go b/internal/server/instance/drivers/driver_lxc.go +index 8b26df30d70..5e8ac77954c 100644 +--- a/internal/server/instance/drivers/driver_lxc.go ++++ b/internal/server/instance/drivers/driver_lxc.go +@@ -1108,7 +1108,7 @@ func (d *lxc) initLXC(config bool) (*liblxc.Container, error) { + } + + nvidiaRequireCuda := d.expandedConfig["nvidia.require.cuda"] +- if nvidiaRequireCuda == "" { ++ if nvidiaRequireCuda != "" { + err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("NVIDIA_REQUIRE_CUDA=%s", nvidiaRequireCuda)) + if err != nil { + return nil, err +@@ -1116,7 +1116,7 @@ func (d *lxc) initLXC(config bool) (*liblxc.Container, error) { + } + + nvidiaRequireDriver := d.expandedConfig["nvidia.require.driver"] +- if nvidiaRequireDriver == "" { ++ if nvidiaRequireDriver != "" { + err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("NVIDIA_REQUIRE_DRIVER=%s", nvidiaRequireDriver)) + if err != nil { + return nil, err + +From b76f970a63b12a0e711af2dc529b558aa7865555 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 15:47:06 -0400 +Subject: [PATCH 15/17] incusd/instance: Confine exec-output access to its + directory +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Serve, list and delete exec output files through an os.OpenRoot handle so +a symlink in the exec-output directory can't be followed outside of it. + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/instance_logs.go | 54 +++++++++++++++++++++++++++++++++---- + 1 file changed, 49 insertions(+), 5 deletions(-) + +diff --git a/cmd/incusd/instance_logs.go b/cmd/incusd/instance_logs.go +index 6f8c54b9607..1d49dbddbda 100644 +--- a/cmd/incusd/instance_logs.go ++++ b/cmd/incusd/instance_logs.go +@@ -1,7 +1,9 @@ + package main + + import ( ++ "errors" + "fmt" ++ "io/fs" + "net/http" + "net/url" + "os" +@@ -439,8 +440,16 @@ func instanceExecOutputsGet(d *Daemon, r *http.Request) response.Response { + + defer func() { _ = pool.UnmountInstance(inst, nil) }() + ++ // Confine access to the exec output directory to avoid following symlinks. ++ root, err := os.OpenRoot(inst.ExecOutputPath()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ + // Read exec record-output files +- dents, err := os.ReadDir(inst.ExecOutputPath()) ++ dents, err := fs.ReadDir(root.FS(), ".") + if err != nil { + return response.SmartError(err) + } +@@ -554,13 +563,40 @@ func instanceExecOutputGet(d *Daemon, r *http.Request) response.Response { + } + + revert.Add(func() { _ = pool.UnmountInstance(inst, nil) }) ++ ++ // Confine access to the exec output directory to avoid following symlinks. ++ root, err := os.OpenRoot(inst.ExecOutputPath()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ revert.Add(func() { _ = root.Close() }) ++ ++ f, err := root.Open(file) ++ if err != nil { ++ if errors.Is(err, os.ErrNotExist) { ++ return response.NotFound(fmt.Errorf("Exec record-output file %q not found", file)) ++ } ++ ++ return response.SmartError(err) ++ } ++ ++ revert.Add(func() { _ = f.Close() }) ++ ++ fi, err := f.Stat() ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + cleanup := revert.Clone() + revert.Success() + + ent := response.FileResponseEntry{ +- Path: filepath.Join(inst.ExecOutputPath(), file), +- Filename: file, +- Cleanup: cleanup.Fail, ++ File: f, ++ FileSize: fi.Size(), ++ FileModified: fi.ModTime(), ++ Filename: file, ++ Cleanup: cleanup.Fail, + } + + s.Events.SendLifecycle(projectName, lifecycle.InstanceLogRetrieved.Event(file, inst, request.CreateRequestor(r), nil)) +@@ -657,7 +693,15 @@ func instanceExecOutputDelete(d *Daemon, r *http.Request) response.Response { + + defer func() { _ = pool.UnmountInstance(inst, nil) }() + +- err = os.Remove(filepath.Join(inst.ExecOutputPath(), file)) ++ // Confine access to the exec output directory to avoid following symlinks. ++ root, err := os.OpenRoot(inst.ExecOutputPath()) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ ++ defer func() { _ = root.Close() }() ++ ++ err = root.Remove(file) + if err != nil { + return response.SmartError(err) + } + +From 8e2eab60d8f62f7aaeb252b4b68537b808c28d0f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 15:48:47 -0400 +Subject: [PATCH 16/17] incusd: Fail closed on unknown authorization project + expansion +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The project expansion used for authorization silently returned the +requested project for any object type without an explicit branch, so a +new project-scoped API missing from the switch would bypass project +confinement. Handle every object type explicitly and error out otherwise. + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/daemon.go | 24 ++++++++++++++++++------ + 1 file changed, 18 insertions(+), 6 deletions(-) + +diff --git a/cmd/incusd/daemon.go b/cmd/incusd/daemon.go +index 09c3c258e0f..a6f6e11b19f 100644 +--- a/cmd/incusd/daemon.go ++++ b/cmd/incusd/daemon.go +@@ -326,12 +326,18 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m + } + + // Expansion function to deal with project inheritance. ++ var expandProjectErr error + expandProject := func(projectName string) string { + // Object types that aren't part of projects. + if slices.Contains([]auth.ObjectType{auth.ObjectTypeUser, auth.ObjectTypeServer, auth.ObjectTypeCertificate, auth.ObjectTypeStoragePool, auth.ObjectTypeNetworkIntegration}, objectType) { + return projectName + } + ++ // Object types that are always addressed in the requested project. ++ if slices.Contains([]auth.ObjectType{auth.ObjectTypeProject, auth.ObjectTypeInstance}, objectType) { ++ return projectName ++ } ++ + // Load the project. + var p *api.Project + err := d.db.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { +@@ -329,24 +335,26 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m + } + + if objectType == auth.ObjectTypeProfile { +- projectName = project.ProfileProjectFromRecord(p) ++ return project.ProfileProjectFromRecord(p) + } else if objectType == auth.ObjectTypeStorageBucket { +- projectName = project.StorageBucketProjectFromRecord(p) ++ return project.StorageBucketProjectFromRecord(p) + } else if objectType == auth.ObjectTypeStorageVolume { + dbVolType, err := storagePools.VolumeTypeNameToDBType(muxVars[1]) + if err != nil { + return projectName + } + +- projectName = project.StorageVolumeProjectFromRecord(p, dbVolType) ++ return project.StorageVolumeProjectFromRecord(p, dbVolType) + } else if objectType == auth.ObjectTypeNetworkZone { +- projectName = project.NetworkZoneProjectFromRecord(p) ++ return project.NetworkZoneProjectFromRecord(p) + } else if slices.Contains([]auth.ObjectType{auth.ObjectTypeImage, auth.ObjectTypeImageAlias}, objectType) { +- projectName = project.ImageProjectFromRecord(p) ++ return project.ImageProjectFromRecord(p) + } else if slices.Contains([]auth.ObjectType{auth.ObjectTypeNetwork, auth.ObjectTypeNetworkACL}, objectType) { +- projectName = project.NetworkProjectFromRecord(p) ++ return project.NetworkProjectFromRecord(p) + } + ++ // Fail closed rather than defaulting to the requested project, which could bypass confinement. ++ expandProjectErr = fmt.Errorf("No project expansion defined for object type %q", objectType) + return projectName + } + +@@ -450,6 +458,10 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m + return response.InternalError(fmt.Errorf("Failed to create authentication object: %w", err)) + } + ++ if expandProjectErr != nil { ++ return response.InternalError(fmt.Errorf("Failed to expand project for authorization: %w", expandProjectErr)) ++ } ++ + s := d.State() + + // Validate whether the user has the needed permission + +From 154089f398534cbcbc2ba62ced1e9ce4245be059 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 28 Jul 2026 12:31:15 -0400 +Subject: [PATCH 17/17] incusd/instance/qemu: Use os.Root for template output +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Matches the LXC driver, the existing checks already prevent escaping. + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/instance/drivers/driver_qemu.go | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/internal/server/instance/drivers/driver_qemu.go b/internal/server/instance/drivers/driver_qemu.go +index 3e3a53e1ff8..319c1c7d727 100644 +--- a/internal/server/instance/drivers/driver_qemu.go ++++ b/internal/server/instance/drivers/driver_qemu.go +@@ -3777,6 +3777,14 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e + instanceMeta["ephemeral"] = "false" + } + ++ // Open the output directory as an os.Root so all template writes stay confined to it. ++ outputRoot, err := os.OpenRoot(path) ++ if err != nil { ++ return fmt.Errorf("Failed to open template output path: %w", err) ++ } ++ ++ defer func() { _ = outputRoot.Close() }() ++ + // Go through the templates. + for tplPath, tpl := range metadata.Templates { + err = func(tplPath string, tpl *api.ImageMetadataTemplate) error { +@@ -3813,7 +3821,7 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e + } + + // Create the file itself. +- w, err = os.Create(filepath.Join(path, fmt.Sprintf("%s.out", tpl.Template))) ++ w, err = outputRoot.Create(fmt.Sprintf("%s.out", tpl.Template)) + if err != nil { + return 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-06-25 00:53:11.000000000 +0000 +++ incus-6.0.4/debian/patches/series 2026-07-30 22:23:09.000000000 +0000 @@ -40,3 +40,17 @@ 131-CVE-2026-48769.patch 132-CVE-2026-55621.patch 133-CVE-2026-55622.patch +134-cherry-pick-fix-large-nft-ruleset.patch +135-CVE-2026-62313.patch +136-CVE-2026-62867.patch +137-CVE-2026-62940.patch +138-CVE-2026-62941.patch +139-CVE-2026-63125.patch +140-CVE-2026-63343.patch +141-GHSA-26gp-p5fw-3r2h.patch +142-GHSA-4qxq-p5hm-3q3p.patch +143a-GHSA-67qw-68v3-36h6.patch +143b-GHSA-67qw-68v3-36h6.patch +144-GHSA-m3j6-p3v3-qmjv.patch +145-GHSA-p2v3-6wvc-cv3p.patch +146-incus-7.3-fixes.patch