#1140955 trixie-pu: package golang-github-containers-buildah/1.39.3+ds1-1

#1140955#5
Date:
2026-06-28 17:22:14 UTC
From:
To:
[ Reason ]

This is to fix a security vulnerability in the podman package present in Debian
Trixie. The current podman package (version 5.4.2+ds1-2) vendors and compiles
Buildah (prior to v1.43.2, probably v1.39.4) directly into its binary to handle
container builds. Upstream has recently disclosed CVE-2026-44517, a
high-severity flaw affecting buildah. Because podman statically embeds the
vulnerable Buildah (>= v1.38.1) Go modules, the podman package inherits this
vulnerability despite the flaw fundamentally existing within the buildah
codebase.

I've backported the upstream commits and want to upload both buildah, and once
accepted, podman to fix this CVE

[ Impact ]
CVE-2026-44517 remains open

[ Tests ]
automated e2e autopkgtests

[ Risks ]
Bad code change can lead to other bugs or regressions

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

<#part type="text/x-patch" filename="/tmp/buildah-1.39-3+ds1-1+deb13u1.debdiff" disposition=inline>
<#/part>

#1140955#12
Date:
2026-07-13 17:37:06 UTC
From:
To:
Hi,

The debdiff is missing, please send again. When it comes to podman is a
binary rebuild sufficient to pull in the updated buildah, or are source
changes required?

Thanks,

#1140955#19
Date:
2026-09-11 09:18:46 UTC
From:
To:
Hi Reinhard

Have you seen the question from Jonathan?

Regards,
Salvatore

#1140955#24
Date:
2026-09-27 17:14:38 UTC
From:
To:
Hi Jonathan, Salvatore,

My apologies for the long delay in following up, and for omitting the debdiff
in the initial submission. Please find the debdiff included below.

Regarding podman: A binary rebuild (binNMU) is sufficient. The security fix
does not alter any public APIs or exported interfaces in Buildah, so rebuilding
podman against the updated golang-github-containers-buildah-dev pulls in the
fix without requiring any source changes.

Thanks,
Reinhard
--- debdiff ---
diff --git golang-github-containers-buildah-1.39.3+ds1/debian/changelog golang-github-containers-buildah-1.39.3+ds1/debian/changelog
index a2ba2201a..6c25b5f98 100644
--- golang-github-containers-buildah-1.39.3+ds1/debian/changelog
+++ golang-github-containers-buildah-1.39.3+ds1/debian/changelog
@@ -1,3 +1,11 @@
+golang-github-containers-buildah (1.39.3+ds1-1+deb13u1) trixie; urgency=high
+
+  * Backport upstream fix for CVE-2026-44517: symlink-based path traversal
+    in build contexts (ADD/COPY with malicious Git repos or tar archives)
+    Closes: #1140619
+
+ -- Reinhard Tartler <siretart@tauware.de>  Sun, 28 Jun 2026 13:16:49 -0400
+
 golang-github-containers-buildah (1.39.3+ds1-1) unstable; urgency=medium

   * New upstream release
diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch
index 3587bd03f..bcd9040ae 100644
--- golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch
+++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch
@@ -11,7 +11,7 @@ Using an older version of rhel works equially fine without this requirement.
  1 file changed, 1 insertion(+), 1 deletion(-)

 diff --git a/tests/chroot.bats b/tests/chroot.bats
-index 8df3519..47fafe1 100644
+index 1826a5f..e893951 100644
 --- a/tests/chroot.bats
 +++ b/tests/chroot.bats
 @@ -22,7 +22,7 @@ load helpers
diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch
index 2733386ba..f0518c70a 100644
--- golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch
+++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/0005-Disable-TestDeviceFromPath.patch
@@ -8,7 +8,7 @@ Closes: #1072147
  1 file changed, 1 insertion(+)

 diff --git a/pkg/parse/parse_test.go b/pkg/parse/parse_test.go
-index 6cbb3af..94881b4 100644
+index aeb842f..56186d9 100644
 --- a/pkg/parse/parse_test.go
 +++ b/pkg/parse/parse_test.go
 @@ -88,6 +88,7 @@ func TestIsValidDeviceMode(t *testing.T) {
diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/0007-Prevent-symlink-based-path-traversal-in-build-contex.patch golang-github-containers-buildah-1.39.3+ds1/debian/patches/0007-Prevent-symlink-based-path-traversal-in-build-contex.patch
new file mode 100644
index 000000000..e6e72c336
--- /dev/null
+++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/0007-Prevent-symlink-based-path-traversal-in-build-contex.patch
@@ -0,0 +1,376 @@
+From: =?utf-8?q?Jan_Rod=C3=A1k?= <hony.com@seznam.cz>
+Date: Thu, 14 May 2026 17:59:47 +0200
+Subject: Prevent symlink-based path traversal in build contexts
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+
+Use securejoin.SecureJoin in TempDirForURL to prevent symlink escape in
+subdirectory resolution. Use os.OpenRoot in downloadToDirectory and
+stdinToDirectory to safely write the Dockerfile fallback without
+following symlinks left by partial tar extractions.
+
+Fixes: https://github.com/containers/buildah/security/advisories/GHSA-49p4-px3h-rq49
+Fixes: CVE-2026-44517
+
+Signed-off-by: Jan Rodák <hony.com@seznam.cz>
+(cherry picked from commit fc2003bb2efeb3ed7d5c7e1e88d04e78f370a944)
+Signed-off-by: Tom Sweeney <tsweeney@redhat.com>
+---
+ define/types.go      |  71 ++++++++++++++++++++++++++-------
+ define/types_test.go |  75 +++++++++++++++++++++++++++++++++++
+ tests/NEW-IMAGES     |   2 +
+ tests/bud.bats       | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 242 insertions(+), 15 deletions(-)
+
+diff --git a/define/types.go b/define/types.go
+index d2a820b..c45eac4 100644
+--- a/define/types.go
++++ b/define/types.go
+@@ -14,10 +14,10 @@ import (
+ 	"path/filepath"
+ 	"strings"
+
++	securejoin "github.com/cyphar/filepath-securejoin"
+ 	"github.com/containers/image/v5/manifest"
+ 	"github.com/containers/storage/pkg/archive"
+ 	"github.com/containers/storage/pkg/chrootarchive"
+-	"github.com/containers/storage/pkg/ioutils"
+ 	"github.com/containers/storage/types"
+ 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
+ 	"github.com/opencontainers/runtime-spec/specs-go"
+@@ -204,8 +204,16 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err
+ 			}
+ 			return "", "", fmt.Errorf("cloning %q to %q:\n%s: %w", url, name, string(combinedOutput), err)
+ 		}
+-		logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, gitSubDir))
+-		return name, filepath.Join(filepath.Base(downloadDir), gitSubDir), nil
++		absPath, err := securejoin.SecureJoin(downloadDir, gitSubDir)
++		if err != nil {
++			return "", "", err
++		}
++		subdir, err := filepath.Rel(name, absPath)
++		if err != nil {
++			return "", "", err
++		}
++		logrus.Debugf("Build context is at %q", subdir)
++		return name, subdir, nil
+ 	}
+ 	if strings.HasPrefix(url, "github.com/") {
+ 		ghurl := url
+@@ -221,8 +229,16 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err
+ 			}
+ 			return "", "", err
+ 		}
+-		logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir))
+-		return name, filepath.Join(filepath.Base(downloadDir), subdir), nil
++		absPath, err := securejoin.SecureJoin(downloadDir, subdir)
++		if err != nil {
++			return "", "", err
++		}
++		resultSubdir, err := filepath.Rel(name, absPath)
++		if err != nil {
++			return "", "", err
++		}
++		logrus.Debugf("Build context is at %q", resultSubdir)
++		return name, resultSubdir, nil
+ 	}
+ 	if url == "-" {
+ 		err = stdinToDirectory(downloadDir)
+@@ -232,8 +248,8 @@ func TempDirForURL(dir, prefix, url string) (name string, subdir string, err err
+ 			}
+ 			return "", "", err
+ 		}
+-		logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir))
+-		return name, filepath.Join(filepath.Base(downloadDir), subdir), nil
++		logrus.Debugf("Build context is at %q", downloadDir)
++		return name, filepath.Base(downloadDir), nil
+ 	}
+ 	logrus.Debugf("don't know how to retrieve %q", url)
+ 	if err2 := os.RemoveAll(name); err2 != nil {
+@@ -321,6 +337,8 @@ func downloadToDirectory(url, dir string) error {
+ 	if resp.ContentLength == 0 {
+ 		return fmt.Errorf("no contents in %q", url)
+ 	}
++	// Try to extract the response as a tar archive; if that fails,
++	// assume it is a raw Dockerfile and write it as such.
+ 	if err := chrootarchive.Untar(resp.Body, dir, nil); err != nil {
+ 		resp1, err := http.Get(url)
+ 		if err != nil {
+@@ -331,10 +349,8 @@ func downloadToDirectory(url, dir string) error {
+ 		if err != nil {
+ 			return err
+ 		}
+-		dockerfile := filepath.Join(dir, "Dockerfile")
+-		// Assume this is a Dockerfile
+-		if err := ioutils.AtomicWriteFile(dockerfile, body, 0o600); err != nil {
+-			return fmt.Errorf("failed to write %q to %q: %w", url, dockerfile, err)
++		if err := writeFileInRoot(dir, "Dockerfile", body, 0o600); err != nil {
++			return fmt.Errorf("failed to write %q to %q: %w", url, filepath.Join(dir, "Dockerfile"), err)
+ 		}
+ 	}
+ 	return nil
+@@ -347,13 +363,38 @@ func stdinToDirectory(dir string) error {
+ 	if err != nil {
+ 		return fmt.Errorf("failed to read from stdin: %w", err)
+ 	}
++	// Try to extract the buffered input as a tar archive; if that fails,
++	// assume it is a raw Dockerfile and write it as such.
+ 	reader := bytes.NewReader(b)
+ 	if err := chrootarchive.Untar(reader, dir, nil); err != nil {
+-		dockerfile := filepath.Join(dir, "Dockerfile")
+-		// Assume this is a Dockerfile
+-		if err := ioutils.AtomicWriteFile(dockerfile, b, 0o600); err != nil {
+-			return fmt.Errorf("failed to write bytes to %q: %w", dockerfile, err)
++		if err := writeFileInRoot(dir, "Dockerfile", b, 0o600); err != nil {
++			return fmt.Errorf("failed to write bytes to %q: %w", filepath.Join(dir, "Dockerfile"), err)
+ 		}
+ 	}
+ 	return nil
+ }
++
++// writeFileInRoot safely writes data to a file inside root, without following
++// symlinks that escape the root directory.
++func writeFileInRoot(root, name string, data []byte, perm os.FileMode) error {
++	rootHandle, err := os.OpenRoot(root)
++	if err != nil {
++		return err
++	}
++	defer rootHandle.Close()
++
++	if err := rootHandle.Remove(name); err != nil && !errors.Is(err, os.ErrNotExist) {
++		return err
++	}
++
++	fileHandle, err := rootHandle.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, perm)
++	if err != nil {
++		return err
++	}
++
++	_, err = fileHandle.Write(data)
++	if closeErr := fileHandle.Close(); closeErr != nil && err == nil {
++		err = closeErr
++	}
++	return err
++}
+diff --git a/define/types_test.go b/define/types_test.go
+index 9ca2fa6..12438e8 100644
+--- a/define/types_test.go
++++ b/define/types_test.go
+@@ -1,11 +1,86 @@
+ package define
+
+ import (
++	"os"
++	"path/filepath"
+ 	"testing"
+
+ 	"github.com/stretchr/testify/assert"
++	"github.com/stretchr/testify/require"
+ )
+
++func TestWriteFileInRoot(t *testing.T) {
++	t.Parallel()
++
++	t.Run("creates file normally", func(t *testing.T) {
++		t.Parallel()
++		root := t.TempDir()
++		err := writeFileInRoot(root, "Dockerfile", []byte("FROM scratch\n"), 0o600)
++		require.NoError(t, err)
++		content, err := os.ReadFile(filepath.Join(root, "Dockerfile"))
++		require.NoError(t, err)
++		assert.Equal(t, "FROM scratch\n", string(content))
++	})
++
++	t.Run("overwrites existing regular file", func(t *testing.T) {
++		t.Parallel()
++		root := t.TempDir()
++		err := os.WriteFile(filepath.Join(root, "Dockerfile"), []byte("old"), 0o600)
++		require.NoError(t, err)
++		err = writeFileInRoot(root, "Dockerfile", []byte("new"), 0o600)
++		require.NoError(t, err)
++		content, err := os.ReadFile(filepath.Join(root, "Dockerfile"))
++		require.NoError(t, err)
++		assert.Equal(t, "new", string(content))
++	})
++
++	t.Run("does not follow symlink escaping root", func(t *testing.T) {
++		t.Parallel()
++		root := t.TempDir()
++
++		targetDir := t.TempDir()
++		target := filepath.Join(targetDir, "pwned")
++		err := os.WriteFile(target, []byte("original"), 0o600)
++		require.NoError(t, err)
++
++		err = os.Symlink(target, filepath.Join(root, "Dockerfile"))
++		require.NoError(t, err)
++
++		err = writeFileInRoot(root, "Dockerfile", []byte("attacker content"), 0o600)
++		require.NoError(t, err)
++
++		content, err := os.ReadFile(target)
++		require.NoError(t, err)
++		assert.Equal(t, "original", string(content))
++
++		info, err := os.Lstat(filepath.Join(root, "Dockerfile"))
++		require.NoError(t, err)
++		assert.True(t, info.Mode().IsRegular())
++	})
++
++	t.Run("does not follow relative symlink escaping root", func(t *testing.T) {
++		t.Parallel()
++		// Create structure: parentDir/root/ and parentDir/secret
++		parentDir := t.TempDir()
++		root := filepath.Join(parentDir, "root")
++		err := os.Mkdir(root, 0o755)
++		require.NoError(t, err)
++		secret := filepath.Join(parentDir, "secret")
++		err = os.WriteFile(secret, []byte("sensitive data"), 0o600)
++		require.NoError(t, err)
++
++		err = os.Symlink("../secret", filepath.Join(root, "Dockerfile"))
++		require.NoError(t, err)
++
++		err = writeFileInRoot(root, "Dockerfile", []byte("attacker content"), 0o600)
++		require.NoError(t, err)
++
++		content, err := os.ReadFile(secret)
++		require.NoError(t, err)
++		assert.Equal(t, "sensitive data", string(content))
++	})
++}
++
+ func TestParseGitBuildContext(t *testing.T) {
+ 	// Tests with only repo
+ 	repo, subdir, branch := parseGitBuildContext("https://github.com/containers/repo.git")
+diff --git a/tests/NEW-IMAGES b/tests/NEW-IMAGES
+index 2096017..f6af0ca 100644
+--- a/tests/NEW-IMAGES
++++ b/tests/NEW-IMAGES
+@@ -13,3 +13,5 @@
+ #
+ # Format is one FQIN per line. Enumerate them below:
+ #
++registry.access.redhat.com/ubi10
++quay.io/hummingbird/git
+diff --git a/tests/bud.bats b/tests/bud.bats
+index 10e4f81..747a1b1 100644
+--- a/tests/bud.bats
++++ b/tests/bud.bats
+@@ -7228,6 +7228,115 @@ _EOF
+   assert "$output" = "$local_head_hash"
+ }
+
++@test "bud with ADD with git repository source escape directory" {
++  _prefetch alpine
++
++  local secretdir=${TEST_SCRATCH_DIR}/secretdir
++  mkdir -p ${secretdir}
++  echo mysecret > ${secretdir}/secretfile
++
++  local repodir=${TEST_SCRATCH_DIR}/repo
++  mkdir -p ${repodir}
++  git -C ${repodir} init -b main
++  ln -s / ${repodir}/proj
++  git -C ${repodir} add proj
++  git -C ${repodir} commit -m "initial commit"
++
++  local baredir=${TEST_SCRATCH_DIR}/repository
++  mkdir -p ${baredir}
++  git clone --bare ${repodir} ${baredir}/test-bug.git
++
++  starthttpd /git/=${baredir}:"git http-backend":GIT_HTTP_EXPORT_ALL=1:GIT_PROJECT_ROOT=${baredir} ${baredir}
++
++  local contextdir=${TEST_SCRATCH_DIR}/add-git
++  mkdir -p $contextdir
++  cat > $contextdir/Containerfile << _EOF
++FROM alpine
++ADD http://0.0.0.0:${HTTP_SERVER_PORT}/git/test-bug.git#main:proj${secretdir} /mydir
++RUN cat /mydir/secretfile
++_EOF
++
++  run_buildah 125 build -f $contextdir/Containerfile -t escape-image --no-cache $contextdir
++  assert "$output" !~ "mysecret"
++}
++
++@test "bud with http context symlinked Dockerfile does not write through symlink on fallback" {
++  local targetfile=${TEST_SCRATCH_DIR}/targetfile
++  echo "ORIGINAL_CONTENT" > ${targetfile}
++
++  # Create a tar with a symlink Dockerfile pointing to targetfile, then
++  # append garbage so chrootarchive.Untar creates the symlink but fails.
++  local tarsrc=${TEST_SCRATCH_DIR}/tarsrc
++  mkdir -p ${tarsrc}
++  ln -s ${targetfile} ${tarsrc}/Dockerfile
++  local broken_tar=${TEST_SCRATCH_DIR}/broken.tar
++  tar -cf ${broken_tar} -C ${tarsrc} Dockerfile
++  dd if=/dev/urandom bs=512 count=4 >> ${broken_tar} 2>/dev/null
++
++  # Start a Python HTTP server that serves the broken tar on the first
++  # request (triggers Untar failure + symlink creation), then a malicious
++  # Dockerfile on the second request (which would be written through the
++  # symlink without the fix).
++  local portfile=${TEST_SCRATCH_DIR}/port
++  python3 -c "
++import http.server, threading, sys
++
++class Handler(http.server.BaseHTTPRequestHandler):
++    count = 0
++    def do_GET(self):
++        Handler.count += 1
++        self.send_response(200)
++        self.end_headers()
++        if Handler.count % 2 == 1:
++            with open('${broken_tar}', 'rb') as f:
++                self.wfile.write(f.read())
++        else:
++            self.wfile.write(b'FROM scratch\nRUN echo MALICIOUS_CONTENT\n')
++    def log_message(self, *args):
++        pass
++
++srv = http.server.HTTPServer(('0.0.0.0', 0), Handler)
++with open('${portfile}', 'w') as f:
++    f.write(str(srv.server_address[1]))
++srv.serve_forever()
++" &
++  local srv_pid=$!
++  # Wait for the server to write its port.
++  local waited=0
++  while ! test -s ${portfile}; do
++    sleep 0.1
++    if test $((++waited)) -ge 50; then
++      kill $srv_pid 2>/dev/null || true
++      die "python http server did not start"
++    fi
++  done
++  local port=$(< ${portfile})
++
++  run_buildah 125 build $WITH_POLICY_JSON http://0.0.0.0:${port}/context.tar
++  kill $srv_pid 2>/dev/null || true
++
++  run cat ${targetfile}
++  assert "$output" = "ORIGINAL_CONTENT"
++}
++
++@test "bud with stdin context symlinked Dockerfile does not write through symlink on fallback" {
++  local targetfile=${TEST_SCRATCH_DIR}/targetfile
++  echo "ORIGINAL_CONTENT" > ${targetfile}
++
++  # Create a tar with a symlink Dockerfile pointing to targetfile, then
++  # append garbage so chrootarchive.Untar fails after creating the symlink.
++  local tarsrc=${TEST_SCRATCH_DIR}/tarsrc
++  mkdir -p ${tarsrc}
++  ln -s ${targetfile} ${tarsrc}/Dockerfile
++  local broken_tar=${TEST_SCRATCH_DIR}/broken.tar
++  tar -cf ${broken_tar} -C ${tarsrc} Dockerfile
++  dd if=/dev/urandom bs=512 count=4 >> ${broken_tar} 2>/dev/null
++
++  run_buildah 125 build $WITH_POLICY_JSON - < ${broken_tar}
++  run cat ${targetfile}
++  assert "$output" = "ORIGINAL_CONTENT"
++}
++
+ @test "build-validates-bind-bind-propagation" {
+   _prefetch alpine
+
diff --git golang-github-containers-buildah-1.39.3+ds1/debian/patches/series golang-github-containers-buildah-1.39.3+ds1/debian/patches/series
index 5c4c9cd23..5402e3e48 100644
--- golang-github-containers-buildah-1.39.3+ds1/debian/patches/series
+++ golang-github-containers-buildah-1.39.3+ds1/debian/patches/series
@@ -2,3 +2,4 @@ root-testfail-ignore.patch
 0005-Disable-TestDeviceFromPath.patch
 0006-tolerate-absence-of-netavark-binary-in-tests.patch
 0004-integration-tests-Prefer-ubi8-over-ubi9-image.patch
+0007-Prevent-symlink-based-path-traversal-in-build-contex.patch