Version in base suite: 5.0.2+git20231211.1364ae4-9+deb13u3 Base version: lxd_5.0.2+git20231211.1364ae4-9+deb13u3 Target version: lxd_5.0.2+git20231211.1364ae4-9+deb13u4 Base file: /srv/ftp-master.debian.org/ftp/pool/main/l/lxd/lxd_5.0.2+git20231211.1364ae4-9+deb13u3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/l/lxd/lxd_5.0.2+git20231211.1364ae4-9+deb13u4.dsc changelog | 9 + patches/107-CVE-2026-28384.patch | 265 +++++++++++++++++++++++++++++++++++++++ patches/108-CVE-2026-33542.patch | 210 ++++++++++++++++++++++++++++++ patches/109-CVE-2026-33897.patch | 238 +++++++++++++++++++++++++++++++++++ patches/series | 3 5 files changed, 725 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp30rdkhxp/lxd_5.0.2+git20231211.1364ae4-9+deb13u3.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp30rdkhxp/lxd_5.0.2+git20231211.1364ae4-9+deb13u4.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-02-27 23:42:15.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/changelog 2026-03-24 23:41:46.000000000 +0000 @@ -1,3 +1,12 @@ +lxd (5.0.2+git20231211.1364ae4-9+deb13u4) trixie-security; urgency=high + + * Cherry-pick fixes for the following security issues: + - CVE-2026-28384 / GHSA-4rmf-rcp8-2r9g + - CVE-2026-33542 / GHSA-p8mm-23gg-jc9r + - CVE-2026-33897 / GHSA-83xr-5xxr-mh92 + + -- Mathias Gibbens Tue, 24 Mar 2026 23:41:46 +0000 + lxd (5.0.2+git20231211.1364ae4-9+deb13u3) trixie-security; urgency=high * Cherry-pick fixes for the following security issues: diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/107-CVE-2026-28384.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/107-CVE-2026-28384.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/107-CVE-2026-28384.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/107-CVE-2026-28384.patch 2026-03-24 23:41:46.000000000 +0000 @@ -0,0 +1,265 @@ +From 157f3aba15d47ade91ab5b94dcd9addcc9eb3472 Mon Sep 17 00:00:00 2001 +From: Din Music +Date: Tue, 24 Feb 2026 16:19:58 +0000 +Subject: [PATCH 1/6] shared/validate: Tighten IsCompressionAlgorithm validator + to only allow supported values + +Signed-off-by: Din Music +(cherry picked from commit c0f6e4548620ebf6bb80c6d7374c5521969fcce1) +(cherry picked from commit 4a7736f60173e641e96a3ff631074b9d13e42690) +--- + shared/validate/validate.go | 37 +++++++++++++++++++++++++++++++------ + 1 file changed, 31 insertions(+), 6 deletions(-) + +diff --git a/shared/validate/validate.go b/shared/validate/validate.go +index b4f3b03dc5eb..06940a80360a 100644 +--- a/shared/validate/validate.go ++++ b/shared/validate/validate.go +@@ -4,12 +4,14 @@ import ( + "bytes" + "crypto/x509" + "encoding/pem" ++ "errors" + "fmt" + "net" + "net/url" + "os/exec" + "path/filepath" + "regexp" ++ "slices" + "strconv" + "strings" + +@@ -547,24 +549,47 @@ func IsPCIAddress(value string) error { + return nil + } + ++// validCompressionCommands is a list of compression command names supported by LXD. ++var validCompressionCommands = []string{ ++ "bzip2", ++ "gzip", ++ "lzma", ++ "pigz", ++ "pzstd", ++ "squashfs", ++ "xz", ++ "zstd", ++} ++ + // IsCompressionAlgorithm validates whether a value is a valid compression algorithm and is available on the system. + func IsCompressionAlgorithm(value string) error { + if value == "none" { + return nil + } + +- // Going to look up tar2sqfs executable binary +- if value == "squashfs" { +- value = "tar2sqfs" +- } +- + // Parse the command. + fields, err := shellquote.Split(value) + if err != nil { + return err + } + +- _, err = exec.LookPath(fields[0]) ++ if len(fields) == 0 { ++ return errors.New("No compression algorithm specified") ++ } ++ ++ // Trim arguments and just look at the command name. ++ cmd := fields[0] ++ ++ if !slices.Contains(validCompressionCommands, cmd) { ++ return fmt.Errorf("Invalid compression algorithm %q", cmd) ++ } ++ ++ // Going to look up tar2sqfs executable binary ++ if cmd == "squashfs" { ++ cmd = "tar2sqfs" ++ } ++ ++ _, err = exec.LookPath(cmd) + return err + } + + +From 178adaba95428c58629589f6e56e7ed3797945f6 Mon Sep 17 00:00:00 2001 +From: Din Music +Date: Tue, 24 Feb 2026 16:28:15 +0000 +Subject: [PATCH 2/6] lxd/images: Validate compression algorithm in + compressFile + +Signed-off-by: Din Music +(cherry picked from commit 417241c8d44ca422bbff3f0036e80427488ff665) +(cherry picked from commit 07f560b652ce1ff281f303e29c57040e5734613e) +--- + lxd/images.go | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/lxd/images.go b/lxd/images.go +index 5ab80f0818d6..07e072a9a060 100644 +--- a/lxd/images.go ++++ b/lxd/images.go +@@ -50,6 +50,7 @@ import ( + "github.com/canonical/lxd/shared/ioprogress" + "github.com/canonical/lxd/shared/logger" + "github.com/canonical/lxd/shared/osarch" ++ "github.com/canonical/lxd/shared/validate" + "github.com/canonical/lxd/shared/version" + ) + +@@ -131,6 +132,11 @@ func compressFile(compress string, infile io.Reader, outfile io.Writer) error { + return err + } + ++ err = validate.IsCompressionAlgorithm(fields[0]) ++ if err != nil { ++ return err ++ } ++ + if fields[0] == "squashfs" { + // 'tar2sqfs' do not support writing to stdout. So write to a temporary + // file first and then replay the compressed content to outfile. + +From 001d325b967629fb371d3020bcf6fc84793f0374 Mon Sep 17 00:00:00 2001 +From: Din Music +Date: Tue, 24 Feb 2026 16:39:00 +0000 +Subject: [PATCH 3/6] lxd/images: Fail early if compression algorithm is + unsupported + +Signed-off-by: Din Music +(cherry picked from commit 91aac3f86afc5302f9c30d41bc279f246d156c8e) +(cherry picked from commit 083c0cc8936b0c8f568d655bfd37f06a03e82fd7) +--- + lxd/images.go | 9 ++++++++- + 1 file changed, 8 insertions(+), 1 deletion(-) + +diff --git a/lxd/images.go b/lxd/images.go +index 07e072a9a060..dbe459421c63 100644 +--- a/lxd/images.go ++++ b/lxd/images.go +@@ -995,7 +995,14 @@ func imagesPost(d *Daemon, r *http.Request) response.Response { + return response.InternalError(fmt.Errorf("Invalid images JSON")) + } + +- /* Forward requests for containers on other nodes */ ++ if req.CompressionAlgorithm != "" { ++ err = validate.IsCompressionAlgorithm(req.CompressionAlgorithm) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ } ++ ++ // Forward requests for containers on other nodes. + if !imageUpload && shared.StringInSlice(req.Source.Type, []string{"container", "instance", "virtual-machine", "snapshot"}) { + name := req.Source.Name + if name != "" { + +From 0b90dfadee4c8dcd6352d89af8fb52ae3d2a5988 Mon Sep 17 00:00:00 2001 +From: Din Music +Date: Tue, 24 Feb 2026 16:44:17 +0000 +Subject: [PATCH 4/6] lxd/instance_backup: Fail fast if compression algorithm + is unsupported + +Signed-off-by: Din Music +(cherry picked from commit 7de3f4cf4269fc898bc61b35f31cf72395f98ed6) +(cherry picked from commit e9f550df27d33d4cea8dccc843cfa84f26169602) +--- + lxd/instance_backup.go | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/lxd/instance_backup.go b/lxd/instance_backup.go +index 0e5c17fc2926..0954b3662427 100644 +--- a/lxd/instance_backup.go ++++ b/lxd/instance_backup.go +@@ -23,6 +23,7 @@ import ( + "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" ++ "github.com/canonical/lxd/shared/validate" + "github.com/canonical/lxd/shared/version" + ) + +@@ -277,6 +278,13 @@ func instanceBackupsPost(d *Daemon, r *http.Request) response.Response { + return response.BadRequest(err) + } + ++ if req.CompressionAlgorithm != "" { ++ err = validate.IsCompressionAlgorithm(req.CompressionAlgorithm) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ } ++ + if req.Name == "" { + // come up with a name. + backups, err := inst.Backups() + +From 11104c5bb61b2c7d3fc6a371d0525aca938ac19d Mon Sep 17 00:00:00 2001 +From: Din Music +Date: Tue, 24 Feb 2026 16:45:14 +0000 +Subject: [PATCH 5/6] lxd/storage_volumes_backup: Fail fast if compression + algorith is unsupported + +Signed-off-by: Din Music +(cherry picked from commit db35b646f6ac140f2237c049c1fb36aa181d0c0c) +(cherry picked from commit ed9d8a3d7900089f7cf22a96fa3a5d8e5a3aeab0) +--- + lxd/storage_volumes_backup.go | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/lxd/storage_volumes_backup.go b/lxd/storage_volumes_backup.go +index ef3f2f7dd9c1..56c4568534d6 100644 +--- a/lxd/storage_volumes_backup.go ++++ b/lxd/storage_volumes_backup.go +@@ -24,6 +24,7 @@ import ( + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" + "github.com/canonical/lxd/shared/logger" ++ "github.com/canonical/lxd/shared/validate" + "github.com/canonical/lxd/shared/version" + ) + +@@ -364,6 +365,13 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response + return response.BadRequest(err) + } + ++ if req.CompressionAlgorithm != "" { ++ err = validate.IsCompressionAlgorithm(req.CompressionAlgorithm) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ } ++ + if req.Name == "" { + // come up with a name. + backups, err := s.DB.Cluster.GetStoragePoolVolumeBackupsNames(projectName, volumeName, poolID) + +From aa625cc451b3f61abdd9f94574a554935be0f43f Mon Sep 17 00:00:00 2001 +From: Din Music +Date: Tue, 24 Feb 2026 16:48:21 +0000 +Subject: [PATCH 6/6] test/suites/basic: Test invalid compression algorithm + +Signed-off-by: Din Music +(cherry picked from commit 94cf6dd2c04bdb8f7462f5bc9bac8180b72a710d) +(cherry picked from commit 5a5b7dd6c1f5d35666c27b25f895f5cc6abd1dd9) +Signed-off-by: Thomas Parrott +--- + test/suites/basic.sh | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/test/suites/basic.sh b/test/suites/basic.sh +index 4bd7f40d2d59..bf9df57d926e 100644 +--- a/test/suites/basic.sh ++++ b/test/suites/basic.sh +@@ -186,6 +186,8 @@ test_basic_usage() { + false + fi + ++ # Ensure invalid compression algorithm is rejected. ++ ! lxc publish bar --compression=ls || false + + # Test image compression on publish + lxc publish bar --alias=foo-image-compressed --compression=bzip2 prop=val1 diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/108-CVE-2026-33542.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/108-CVE-2026-33542.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/108-CVE-2026-33542.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/108-CVE-2026-33542.patch 2026-03-24 23:41:46.000000000 +0000 @@ -0,0 +1,210 @@ +From 97900d0b493828a07967364cc9e5ad4c24c6a5cd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:36:00 -0400 +Subject: [PATCH 1/4] client: Make ImageFileRequest require a ReadWriteSeeker +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is a small Go API break which is needed to address a security issue +where we need the ability to re-hash the final image files. + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +--- + client/interfaces.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/client/interfaces.go b/client/interfaces.go +index 9b3d6bceb8..8b1e9e79a4 100644 +--- a/client/interfaces.go ++++ b/client/interfaces.go +@@ -502,10 +502,10 @@ type ImageCreateArgs struct { + // The ImageFileRequest struct is used for an image download request. + type ImageFileRequest struct { + // Writer for the metadata file +- MetaFile io.WriteSeeker ++ MetaFile io.ReadWriteSeeker + + // Writer for the rootfs file +- RootfsFile io.WriteSeeker ++ RootfsFile io.ReadWriteSeeker + + // Progress handler (called whenever some progress is made) + ProgressHandler func(progress ioprogress.ProgressData) +-- +2.47.3 + +From 168b4e7432a44c5d142c27ac01002c6d704f2996 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:42:43 -0400 +Subject: [PATCH 2/4] incus: Update for changes to incus.ImageFileRequest +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxc/image.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/lxc/image.go b/lxc/image.go +index 0ae7265e6f..4f72801f80 100644 +--- a/lxc/image.go ++++ b/lxc/image.go +@@ -557,8 +557,8 @@ func (c *cmdImageExport) Run(cmd *cobra.Command, args []string) error { + } + + req := lxd.ImageFileRequest{ +- MetaFile: io.WriteSeeker(dest), +- RootfsFile: io.WriteSeeker(destRootfs), ++ MetaFile: io.ReadWriteSeeker(dest), ++ RootfsFile: io.ReadWriteSeeker(destRootfs), + ProgressHandler: progress.UpdateProgress, + } + +-- +2.47.3 + +From 2601db4e4710b11aadea01f88b5b14f484a7525f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:42:48 -0400 +Subject: [PATCH 3/4] incusd: Update for changes to incus.ImageFileRequest +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + lxd/daemon_images.go | 4 ++-- + lxd/images.go | 4 ++-- + 2 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/lxd/daemon_images.go b/lxd/daemon_images.go +index 501523c2a4..1c75b06a90 100644 +--- a/lxd/daemon_images.go ++++ b/lxd/daemon_images.go +@@ -359,8 +359,8 @@ func ImageDownload(r *http.Request, s *state.State, op *operations.Operation, ar + // Download the image + var resp *lxd.ImageFileResponse + request := lxd.ImageFileRequest{ +- MetaFile: io.WriteSeeker(dest), +- RootfsFile: io.WriteSeeker(destRootfs), ++ MetaFile: io.ReadWriteSeeker(dest), ++ RootfsFile: io.ReadWriteSeeker(destRootfs), + ProgressHandler: progress, + Canceler: canceler, + DeltaSourceRetriever: func(fingerprint string, file string) string { +diff --git a/lxd/images.go b/lxd/images.go +index 91a89bb978..f3c320db87 100644 +--- a/lxd/images.go ++++ b/lxd/images.go +@@ -3982,8 +3982,8 @@ func imageImportFromNode(imagesDir string, client lxd.InstanceServer, fingerprin + defer func() { _ = rootfsFile.Close() }() + + getReq := lxd.ImageFileRequest{ +- MetaFile: io.WriteSeeker(metaFile), +- RootfsFile: io.WriteSeeker(rootfsFile), ++ MetaFile: io.ReadWriteSeeker(metaFile), ++ RootfsFile: io.ReadWriteSeeker(rootfsFile), + } + + getResp, err := client.GetImageFile(fingerprint, getReq) +-- +2.47.3 + +From 93a88451fda06672390dfddd9434b460f4fbfe18 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:48:12 -0400 +Subject: [PATCH 4/4] client/simplestreams: Validate the full image hash +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Following download of the files/deltas, compute a full hash to make sure +we have the expected image. + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +--- + client/simplestreams_images.go | 46 ++++++++++++++++++++++++++++++++++ + 1 file changed, 46 insertions(+) + +diff --git a/client/simplestreams_images.go b/client/simplestreams_images.go +index 68b9357fc4..a2fdfad2c2 100644 +--- a/client/simplestreams_images.go ++++ b/client/simplestreams_images.go +@@ -97,6 +97,14 @@ func (r *ProtocolSimpleStreams) GetImageFile(fingerprint string, req ImageFileRe + httpTransport.ResponseHeaderTimeout = 30 * time.Second + httpClient.Transport = httpTransport + ++ // Get the image and expand the fingerprint. ++ image, err := r.ssClient.GetImage(fingerprint) ++ if err != nil { ++ return nil, err ++ } ++ ++ fingerprint = image.Fingerprint ++ + // Get the file list + files, err := r.ssClient.GetFiles(fingerprint) + if err != nil { +@@ -240,6 +248,44 @@ func (r *ProtocolSimpleStreams) GetImageFile(fingerprint string, req ImageFileRe + } + } + ++ // Validate the full image hash. ++ // ++ // Normally we'd do that as we download the image to avoid having to ++ // re-read the data, but because the simplestreams allows retries (HTTP to HTTPS), ++ // we don't have a clean reader that can be used for that. ++ // ++ // Another situation where we couldn't do a streaming hash anyway is when processing delta images. ++ hash256 := sha256.New() ++ ++ if resp.MetaSize > 0 && req.MetaFile != nil { ++ _, err = req.MetaFile.Seek(0, io.SeekStart) ++ if err != nil { ++ return nil, err ++ } ++ ++ _, err := io.Copy(hash256, req.MetaFile) ++ if err != nil { ++ return nil, err ++ } ++ } ++ ++ if resp.RootfsSize > 0 && req.RootfsFile != nil { ++ _, err = req.RootfsFile.Seek(0, io.SeekStart) ++ if err != nil { ++ return nil, err ++ } ++ ++ _, err := io.Copy(hash256, req.RootfsFile) ++ if err != nil { ++ return nil, err ++ } ++ } ++ ++ hash := fmt.Sprintf("%x", hash256.Sum(nil)) ++ if hash != fingerprint { ++ return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint) ++ } ++ + return &resp, nil + } + +-- +2.47.3 diff -Nru lxd-5.0.2+git20231211.1364ae4/debian/patches/109-CVE-2026-33897.patch lxd-5.0.2+git20231211.1364ae4/debian/patches/109-CVE-2026-33897.patch --- lxd-5.0.2+git20231211.1364ae4/debian/patches/109-CVE-2026-33897.patch 1970-01-01 00:00:00.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/109-CVE-2026-33897.patch 2026-03-24 23:41:46.000000000 +0000 @@ -0,0 +1,238 @@ +From 932f363a68709ea2e129f8df911c0a48b5dcdd80 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 24 Mar 2026 16:10:25 -0400 +Subject: [PATCH] incusd/instance: Use restricted pongo2 parser +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The chroot logic in pongo2 doesn't work and therefore allows all +templates to read and write to arbitrary paths on the host filesystem. + +Given the logic seemingly never worked properly, no template out there +should be dependent on the file related functions being functional. + +Transition to our standard RenderTemplate logic which specifically block +all file related functions. Introduces a new RenderTemplateFile to +handle cases where we want to directly write to a file (useful for +write quotas). + +This addresses CVE-2026-33897 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- +diff --git a/lxd/instance/drivers/driver_lxc.go b/lxd/instance/drivers/driver_lxc.go +index 9f8499b2a..5b5eb32e7 100644 +--- a/lxd/instance/drivers/driver_lxc.go ++++ b/lxd/instance/drivers/driver_lxc.go +@@ -61,7 +61,6 @@ import ( + storagePools "github.com/canonical/lxd/lxd/storage" + storageDrivers "github.com/canonical/lxd/lxd/storage/drivers" + "github.com/canonical/lxd/lxd/storage/filesystem" +- "github.com/canonical/lxd/lxd/template" + "github.com/canonical/lxd/lxd/util" + "github.com/canonical/lxd/shared" + "github.com/canonical/lxd/shared/api" +@@ -6665,14 +6664,6 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error { + return fmt.Errorf("Failed to read template file: %w", err) + } + +- // Restrict filesystem access to within the container's rootfs +- tplSet := pongo2.NewSet(fmt.Sprintf("%s-%s", d.name, tpl.Template), template.ChrootLoader{Path: d.RootfsPath()}) +- +- tplRender, err := tplSet.FromString("{% autoescape off %}" + string(tplString) + "{% endautoescape %}") +- if err != nil { +- return fmt.Errorf("Failed to render template: %w", err) +- } +- + configGet := func(confKey, confDefault *pongo2.Value) *pongo2.Value { + val, ok := d.expandedConfig[confKey.String()] + if !ok { +@@ -6682,17 +6673,18 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error { + return pongo2.AsValue(strings.TrimRight(val, "\r\n")) + } + +- // Render the template +- err = tplRender.ExecuteWriter(pongo2.Context{"trigger": trigger, ++ err = shared.RenderTemplateFile(w, string(tplString), pongo2.Context{ ++ "trigger": trigger, + "path": tplPath, + "container": containerMeta, + "instance": containerMeta, + "config": d.expandedConfig, + "devices": d.expandedDevices, + "properties": tpl.Properties, +- "config_get": configGet}, w) ++ "config_get": configGet, ++ }) + if err != nil { +- return err ++ return fmt.Errorf("Failed to render template: %w", err) + } + + return w.Close() +diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go +index dd61b6636..f57b542d4 100644 +--- a/lxd/instance/drivers/driver_qemu.go ++++ b/lxd/instance/drivers/driver_qemu.go +@@ -64,7 +64,6 @@ import ( + storagePools "github.com/canonical/lxd/lxd/storage" + storageDrivers "github.com/canonical/lxd/lxd/storage/drivers" + "github.com/canonical/lxd/lxd/storage/filesystem" +- pongoTemplate "github.com/canonical/lxd/lxd/template" + "github.com/canonical/lxd/lxd/util" + lxdvsock "github.com/canonical/lxd/lxd/vsock" + "github.com/canonical/lxd/lxd/warnings" +@@ -2671,13 +2670,6 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e + return fmt.Errorf("Failed to read template file: %w", err) + } + +- // Restrict filesystem access to within the instance's rootfs. +- tplSet := pongo2.NewSet(fmt.Sprintf("%s-%s", d.name, tpl.Template), pongoTemplate.ChrootLoader{Path: d.TemplatesPath()}) +- tplRender, err := tplSet.FromString("{% autoescape off %}" + string(tplString) + "{% endautoescape %}") +- if err != nil { +- return fmt.Errorf("Failed to render template: %w", err) +- } +- + configGet := func(confKey, confDefault *pongo2.Value) *pongo2.Value { + val, ok := d.expandedConfig[confKey.String()] + if !ok { +@@ -2688,16 +2680,18 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e + } + + // Render the template. +- err = tplRender.ExecuteWriter(pongo2.Context{"trigger": trigger, ++ err = shared.RenderTemplateFile(w, string(tplString), pongo2.Context{ ++ "trigger": trigger, + "path": tplPath, +- "instance": instanceMeta, + "container": instanceMeta, // FIXME: remove once most images have moved away. ++ "instance": instanceMeta, + "config": d.expandedConfig, + "devices": d.expandedDevices, + "properties": tpl.Properties, +- "config_get": configGet}, w) ++ "config_get": configGet, ++ }) + if err != nil { +- return err ++ return fmt.Errorf("Failed to render template: %w", err) + } + + return w.Close() +diff --git a/lxd/template/chroot.go b/lxd/template/chroot.go +deleted file mode 100644 +index 94da31861..000000000 +--- a/lxd/template/chroot.go ++++ /dev/null +@@ -1,51 +0,0 @@ +-package template +- +-import ( +- "bytes" +- "fmt" +- "io" +- "os" +- "path/filepath" +- "strings" +-) +- +-// ChrootLoader is a pong2 compatible file loader which restricts all accesses to a directory. +-type ChrootLoader struct { +- Path string +-} +- +-// Abs resolves a filename relative to the base directory. Absolute paths are allowed. +-// When there's no base dir set, the absolute path to the filename +-// will be calculated based on either the provided base directory (which +-// might be a path of a template which includes another template) or +-// the current working directory. +-func (l ChrootLoader) Abs(base string, name string) string { +- return filepath.Clean(fmt.Sprintf("%s/%s", l.Path, name)) +-} +- +-// Get reads the path's content from your local filesystem. +-func (l ChrootLoader) Get(path string) (io.Reader, error) { +- // Get the full path +- path, err := filepath.EvalSymlinks(path) +- if err != nil { +- return nil, err +- } +- +- basePath, err := filepath.EvalSymlinks(l.Path) +- if err != nil { +- return nil, err +- } +- +- // Validate that we're under the expected prefix +- if !strings.HasPrefix(path, basePath) { +- return nil, fmt.Errorf("Attempting to access a file outside the instance") +- } +- +- // Open and read the file +- buf, err := os.ReadFile(path) +- if err != nil { +- return nil, err +- } +- +- return bytes.NewReader(buf), nil +-} +diff --git a/shared/util.go b/shared/util.go +index 47be6439b..128b17cc1 100644 +--- a/shared/util.go ++++ b/shared/util.go +@@ -1225,13 +1225,15 @@ func (r *ReadSeeker) Seek(offset int64, whence int) (int64, error) { + return r.Seeker.Seek(offset, whence) + } + ++var bannedTemplateTags = []string{"extends", "import", "include", "ssi"} ++ + // RenderTemplate renders a pongo2 template. + func RenderTemplate(template string, ctx pongo2.Context) (string, error) { + // Create custom TemplateSet + set := pongo2.NewSet("restricted", pongo2.DefaultLoader) + + // Ban tags that could be used to access the host's filesystem. +- for _, tag := range []string{"extends", "import", "include", "ssi"} { ++ for _, tag := range bannedTemplateTags { + err := set.BanTag(tag) + if err != nil { + return "", fmt.Errorf("Failed to ban tag %q: %w", tag, err) +@@ -1258,6 +1260,35 @@ func RenderTemplate(template string, ctx pongo2.Context) (string, error) { + return ret, err + } + ++// RenderTemplateFile renders a pongo2 template to a file. ++// No nesting is supported in this scenario. ++func RenderTemplateFile(w io.Writer, template string, ctx pongo2.Context) error { ++ // Prepare a custom set. ++ custom := pongo2.NewSet("render-template", pongo2.DefaultLoader) ++ ++ // Block the use of some tags. ++ for _, tag := range bannedTemplateTags { ++ err := custom.BanTag(tag) ++ if err != nil { ++ return fmt.Errorf("Failed to configure custom pongo2 parser: Failed to block tag tag %q: %w", tag, err) ++ } ++ } ++ ++ // Load template from string ++ tpl, err := custom.FromString("{% autoescape off %}" + template + "{% endautoescape %}") ++ if err != nil { ++ return err ++ } ++ ++ // Get rendered template ++ err = tpl.ExecuteWriter(ctx, w) ++ if err != nil { ++ return err ++ } ++ ++ return nil ++} ++ + // GetExpiry returns the expiry date based on the reference date and a length of time. + // The length of time format is "(S|M|H|d|w|m|y)", and can contain multiple such fields, e.g. + // "1d 3H" (1 day and 3 hours). 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-02-27 23:42:15.000000000 +0000 +++ lxd-5.0.2+git20231211.1364ae4/debian/patches/series 2026-03-24 23:41:46.000000000 +0000 @@ -18,3 +18,6 @@ 104-GHSA-56mx-8g9f-5crf.patch 105-CVE-2026-23953.patch 106-CVE-2026-23954.patch +107-CVE-2026-28384.patch +108-CVE-2026-33542.patch +109-CVE-2026-33897.patch