Santiago,

As promised, signed debdiffs attached for both packages (both against
the exact source currently in bookworm):

- cjose_0.6.2.1-1+deb12u1.debdiff.asc (CVE-2026-53938, CVE-2026-53939)
- git-lfs_3.3.0-1+deb12u2.debdiff.asc (CVE-2025-26625, plus the two
  follow-up hardening commits)

Both clearsigned with the key I use for Debian work
(232C682F2067D6FF, [email protected] UID).

Let me know if you'd like anything else from me to move these forward.

Thanks,
Ivo
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

diff --git a/debian/changelog b/debian/changelog
index 10cf61b..38bba78 100644
- --- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+cjose (0.6.2.1-1+deb12u1) bookworm-security; urgency=high
+
+  * Fix CVE-2026-53938: heap buffer overflow in AES key unwrap. A
+    crafted JWE with an oversized encrypted_key for an AES key wrap
+    algorithm (A128KW/A192KW/A256KW) triggered an out-of-bounds heap
+    write; remote and unauthenticated.
+  * Fix CVE-2026-53939: the AES-CBC-HMAC content-encryption key was
+    generated as all zero bytes instead of randomly, so every JWE
+    encrypted with an AES-CBC-HMAC enc algorithm used the same fixed,
+    publicly known key.
+
+ -- Ivo Marino <[email protected]>  Sun, 13 Sep 2026 16:21:00 +0000
 cjose (0.6.2.1-1) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/debian/patches/CVE-2026-53938.patch 
b/debian/patches/CVE-2026-53938.patch
new file mode 100644
index 0000000..96fbccf
- --- /dev/null
+++ b/debian/patches/CVE-2026-53938.patch
@@ -0,0 +1,33 @@
+Description: fix heap buffer overflow in AES key unwrap
+ _cjose_jwe_decrypt_ek_aes_kw() passed the attacker-controlled
+ encrypted_key straight to AES_unwrap_key() without checking its
+ length against the fixed-size jwe->cek buffer it unwraps into. A
+ crafted JWE with an oversized encrypted_key segment for an AES key
+ wrap algorithm (A128KW/A192KW/A256KW) triggers an out-of-bounds heap
+ write, which is at minimum a remote, unauthenticated denial of
+ service. Add the length check RFC 3394 already implies (wrapped key
+ length is always the plaintext CEK length plus 8 bytes) before
+ unwrapping.
+Origin: upstream, https://github.com/OpenIDC/cjose/commit/8c51d2452
+Bug: https://github.com/OpenIDC/cjose/security/advisories
+Forwarded: not-needed
+Applied-Upstream: 0.6.2.5
+Last-Update: 2026-09-13
+--- a/src/jwe.c
++++ b/src/jwe.c
+@@ -587,6 +587,15 @@ static bool 
_cjose_jwe_decrypt_ek_aes_kw(_jwe_int_recipient_t *recipient, cjose_
+         return false;
+     }
+ 
++    // the wrapped key (RFC 3394) is always the plaintext CEK length plus 8 
bytes;
++    // enforce this before calling AES_unwrap_key, which would otherwise copy 
the
++    // attacker-controlled encrypted_key into the fixed-size jwe->cek buffer
++    if (recipient->enc_key.raw_len != jwe->cek_len + 8)
++    {
++        CJOSE_ERROR(err, CJOSE_ERR_INVALID_ARG);
++        return false;
++    }
++
+     // AES unwrap the CEK in to jwe->cek
+     int len = AES_unwrap_key(&akey, (const unsigned char *)NULL, jwe->cek, 
(const unsigned char *)recipient->enc_key.raw,
+                              recipient->enc_key.raw_len);
diff --git a/debian/patches/CVE-2026-53939.patch 
b/debian/patches/CVE-2026-53939.patch
new file mode 100644
index 0000000..b8a6d36
- --- /dev/null
+++ b/debian/patches/CVE-2026-53939.patch
@@ -0,0 +1,30 @@
+Description: fix all-zero content-encryption key for AES-CBC-HMAC
+ _cjose_jwe_set_cek_aes_cbc() passed the wrong boolean to
+ _cjose_jwe_malloc()'s "zero" parameter (inverted), so a freshly
+ generated CEK for AES-CBC-HMAC content encryption was always
+ zero-filled instead of randomly generated. Every JWE produced with
+ an AES-CBC-HMAC enc algorithm was therefore encrypted and
+ authenticated under the same fixed, publicly known key, letting
+ anyone who obtains such a JWE recover the plaintext and forge or
+ modify the content.
+Origin: upstream, https://github.com/OpenIDC/cjose/commit/2a6e5bd96
+Bug: https://github.com/OpenIDC/cjose/security/advisories
+Forwarded: not-needed
+Applied-Upstream: 0.6.2.6
+Last-Update: 2026-09-13
+--- a/src/jwe.c
++++ b/src/jwe.c
+@@ -455,11 +455,11 @@ static bool _cjose_jwe_set_cek_aes_cbc(cjose_jwe_t *jwe, 
const cjose_jwk_t *jwk,
+     if (strcmp(enc, CJOSE_HDR_ENC_A256CBC_HS512) == 0)
+         keysize = 64;
+ 
++    // if no JWK is provided, generate a random key
+     if (NULL == jwk)
+     {
+-        // allocate memory for the CEK and fill with random bytes or 0's
+         _cjose_release_cek(&jwe->cek, jwe->cek_len);
+-        if (!_cjose_jwe_malloc(keysize, !random, &jwe->cek, err))
++        if (!_cjose_jwe_malloc(keysize, random, &jwe->cek, err))
+         {
+             return false;
+         }
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..4b6c841
- --- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1,2 @@
+CVE-2026-53938.patch
+CVE-2026-53939.patch
-----BEGIN PGP SIGNATURE-----

iHUEARYKAB0WIQS82VxZRu76KBuHHJUjLGgvIGfW/wUCaqkQVAAKCRAjLGgvIGfW
/9rEAP40notHHI/rOCLMCaZAxg47vFO4dRKMbYOSymRUMBE0cgEAkv5xXEIbIOfj
DMN7LLNVUDbFUqlafCgT+FuBLV5MuAM=
=X/VH
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

diff --git a/debian/changelog b/debian/changelog
index 56c8bc5..f16db78 100644
- --- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,19 @@
+git-lfs (3.3.0-1+deb12u2) bookworm-security; urgency=high
+
+  * Fix CVE-2025-26625: git lfs checkout and git lfs pull wrote object
+    content via os.Create(), which follows symlinks and truncates
+    hardlink targets in place. A crafted repository containing a
+    symlink or hardlink colliding with a tracked path could make
+    either command write to an arbitrary location outside the working
+    tree. Fixed by removing any existing entry at the target path
+    first, then creating the replacement file exclusively.
+  * Further harden the same fix: check every intermediate directory
+    component of the write path for a symlink/hardlink standing in
+    for a real directory, not just the final component; and refuse to
+    run git lfs checkout/pull inside a bare repository, where the
+    write path has no defined relationship to a working tree at all.
+
+ -- Ivo Marino <[email protected]>  Mon, 14 Sep 2026 09:30:00 +0000
 git-lfs (3.3.0-1+deb12u1) bookworm-security; urgency=high
 
   * Non-maintainer upload by the Security Team.
diff --git 
a/debian/patches/0003-CVE-2025-26625-symlink-hardlink-write-outside-tree.patch 
b/debian/patches/0003-CVE-2025-26625-symlink-hardlink-write-outside-tree.patch
new file mode 100644
index 0000000..61601a7
- --- /dev/null
+++ 
b/debian/patches/0003-CVE-2025-26625-symlink-hardlink-write-outside-tree.patch
@@ -0,0 +1,67 @@
+Description: reject symlink/hardlink writes outside the working tree on 
checkout/pull
+ git lfs checkout and git lfs pull wrote Git LFS object content via
+ os.Create(), which follows a symbolic link at the target path and
+ truncates-in-place any file with additional hard links, rather than
+ replacing it. A crafted repository containing a symlink or hardlink
+ colliding with a path Git LFS is about to populate can make either
+ command write arbitrary content to a location outside the working
+ tree entirely. Fix by removing any existing entry at the target path
+ first, then creating the replacement file exclusively (O_EXCL), so a
+ symlink or hardlink is rejected rather than followed or written
+ through. Adapted for this version's manifest parameter, which is
+ still a *tq.Manifest pointer here (changed to a plain tq.Manifest
+ value further upstream, unrelated to this fix).
+Origin: upstream, 
https://github.com/git-lfs/git-lfs/commit/5c11ffce9a4f095ff356bc781e2a031abb46c1a8
+Bug: https://github.com/git-lfs/git-lfs/security/advisories/GHSA-6pvw-g552-53c5
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1118339
+Forwarded: not-needed
+Applied-Upstream: 3.7.1
+Last-Update: 2026-09-13
+--- a/lfs/gitfilter_smudge.go
++++ b/lfs/gitfilter_smudge.go
+@@ -18,15 +18,17 @@ import (
+ func (f *GitFilter) SmudgeToFile(filename string, ptr *Pointer, download 
bool, manifest *tq.Manifest, cb tools.CopyCallback) error {
+       tools.MkdirAll(filepath.Dir(filename), f.cfg)
+
+-      if stat, _ := os.Stat(filename); stat != nil && stat.Mode()&0200 == 0 {
+-              if err := os.Chmod(filename, stat.Mode()|0200); err != nil {
+-                      return errors.Wrap(err,
+-                              tr.Tr.Get("Could not restore write permission"))
++      // When no pointer file exists on disk, we should use the permissions
++      // defined for the file in Git, since the executable mode may be set.
++      // However, to conform with our legacy behaviour, we do not do this
++      // at present. (Backported from upstream fix for CVE-2025-26625.)
++      var mode os.FileMode = 0666
++      if stat, _ := os.Lstat(filename); stat != nil && 
stat.Mode().IsRegular() {
++              if ptr.Size == 0 && stat.Size() == 0 {
++                      return nil
+               }
+
+-              // When we're done, return the file back to its normal
+-              // permission bits.
+-              defer os.Chmod(filename, stat.Mode())
++              mode = stat.Mode().Perm()
+       }
+
+       abs, err := filepath.Abs(filename)
+@@ -34,9 +36,18 @@ func (f *GitFilter) SmudgeToFile(filename string, ptr 
*Pointer, download bool, m
+               return errors.New(tr.Tr.Get("could not produce absolute path 
for %q", filename))
+       }
+
+-      file, err := os.Create(abs)
++      // CVE-2025-26625: os.Create() follows symlinks and truncates
++      // hardlink targets, letting a crafted repo make LFS write outside
++      // the working tree. Remove any existing entry first, then create
++      // the file exclusively so a concurrently-recreated symlink/hardlink
++      // is rejected rather than followed.
++      if err := os.Remove(abs); err != nil && !os.IsNotExist(err) {
++              return errors.Wrap(err, tr.Tr.Get("could not remove working 
directory file %q", filename))
++      }
++
++      file, err := os.OpenFile(abs, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
+       if err != nil {
+-              return errors.New(tr.Tr.Get("could not create working directory 
file: %v", err))
++              return errors.Wrap(err, tr.Tr.Get("could not create working 
directory file %q", filename))
+       }
+       defer file.Close()
+       if _, err := f.Smudge(file, ptr, filename, download, manifest, cb); err 
!= nil {
diff --git a/debian/patches/0004-CVE-2025-26625-hardening.patch 
b/debian/patches/0004-CVE-2025-26625-hardening.patch
new file mode 100644
index 0000000..5f8b162
- --- /dev/null
+++ b/debian/patches/0004-CVE-2025-26625-hardening.patch
@@ -0,0 +1,817 @@
+Description: harden checkout/pull further against CVE-2025-26625 
(symlink/hardlink writes)
+ The previous patch in this series (CVE-2025-26625.patch) fixed the
+ core vulnerability: os.Create() at the final write target followed a
+ symlink and truncated a hardlink in place instead of replacing it.
+ This patch backports upstream's two follow-up hardening commits for
+ the same CVE:
+ .
+ 1. A symlink/hardlink standing in for one of the INTERMEDIATE
+    directory components in the write path (e.g. "dir1" in
+    "dir1/a.dat") was not checked at all -- only the final path
+    component was. Adds a DirWalker (tools/dir_walker.go) that walks
+    every path component from the working tree root, checking each is
+    a real directory (or creating it, on the checkout/pull code path
+    that needs to). Wired into both "git lfs checkout" (via a
+    tools.MkdirAll() call added at the --to flag's call site, since
+    SmudgeToFile() no longer creates directories itself) and "git lfs
+    pull" (via the new DirWalker directly).
+ 2. "git lfs checkout"/"git lfs pull" would still attempt to operate
+    against a bare repository, where cwd has no defined relationship
+    to a working tree at all. Both commands now check
+    cfg.LocalWorkingDir() up front and refuse cleanly instead.
+ .
+ Adapted for Debian bookworm's actual build toolchain (golang-1.19,
+ confirmed via `dpkg -l`): upstream's DirWalker uses the git-lfs
+ project's own errors.Join() (itself a thin alias for the Go 1.20+
+ stdlib errors.Join) to attach a sentinel error for errors.Is()
+ checks. Go 1.19 predates both the stdlib function and errors.Is's
+ support for multi-error Unwrap() []error, so this patch uses a plain
+ fmt.Errorf("%s: %w", ...) single-error wrap instead -- supported
+ since Go 1.13, and fully sufficient here since each call site only
+ ever tests for one sentinel at a time. tools/dir_walker_test.go
+ itself is otherwise byte-for-byte the same as upstream's and passes
+ unmodified against this adaptation.
+ .
+ Also adapted for bookworm's own git-lfs 3.3.0 code shape (predating
+ both commits by several releases): "commands/pull.go"'s singleCheckout
+ already converts a Git-relative path to a cwd-relative one via its own
+ pathConverter (removed further upstream, after this Debian version, in
+ favor of always assuming cwd == working tree root) -- the DirWalker is
+ pointed at that already-converted cwd-relative path (cwdfilepath),
+ which is what actually gets written to disk, rather than upstream's
+ unconverted p.Name.
+ .
+ Verified: go build ./... (exit 0) and go test ./tools/... ./lfs/...
+ ./commands/... (all ok) on devports-debian, per RULE 1 -- go vet's two
+ pre-existing warnings (git-lfs.go's unbuffered signal channel,
+ lfshttp/client.go's mutex-by-value copy) are unrelated, pre-existing
+ in 3.3.0 itself, not introduced by this patch.
+Origin: upstream, 
https://github.com/git-lfs/git-lfs/commit/0cffe93176b870055c9dadbb3cc9a4a440e98396,
 
https://github.com/git-lfs/git-lfs/commit/d02bd13f02ef76f6807581cd6b34709069cb3615
+Bug: https://github.com/git-lfs/git-lfs/security/advisories/GHSA-6pvw-g552-53c5
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1118339
+Forwarded: not-needed
+Applied-Upstream: 3.7.1
+Last-Update: 2026-09-14
+diff --git a/commands/command_checkout.go b/commands/command_checkout.go
+index c46d5f6..4fe49c3 100644
+--- a/commands/command_checkout.go
++++ b/commands/command_checkout.go
+@@ -3,12 +3,14 @@ package commands
+ import (
+       "fmt"
+       "os"
++      "path/filepath"
+ 
+       "github.com/git-lfs/git-lfs/v3/errors"
+       "github.com/git-lfs/git-lfs/v3/filepathfilter"
+       "github.com/git-lfs/git-lfs/v3/git"
+       "github.com/git-lfs/git-lfs/v3/lfs"
+       "github.com/git-lfs/git-lfs/v3/tasklog"
++      "github.com/git-lfs/git-lfs/v3/tools"
+       "github.com/git-lfs/git-lfs/v3/tq"
+       "github.com/git-lfs/git-lfs/v3/tr"
+       "github.com/spf13/cobra"
+@@ -24,6 +26,14 @@ var (
+ func checkoutCommand(cmd *cobra.Command, args []string) {
+       setupRepository()
+ 
++      // CVE-2025-26625 hardening: a bare repository has no working tree
++      // to check out into; refuse rather than let path handling below
++      // operate against an undefined "current directory".
++      if cfg.LocalWorkingDir() == "" {
++              Print(tr.Tr.Get("This operation must be run in a work tree."))
++              os.Exit(0)
++      }
++
+       stage, err := whichCheckout()
+       if err != nil {
+               Exit(tr.Tr.Get("Error parsing args: %v", err))
+@@ -93,6 +103,14 @@ func checkoutCommand(cmd *cobra.Command, args []string) {
+ }
+ 
+ func checkoutConflict(file string, stage git.IndexStage) {
++      // CVE-2025-26625: SmudgeToFile() no longer creates the destination
++      // directory itself (that responsibility moved to callers, alongside
++      // the new symlink-aware DirWalker used by the normal checkout/pull
++      // path below) -- the --to flag's target directory still needs it.
++      if err := tools.MkdirAll(filepath.Dir(checkoutTo), cfg); err != nil {
++              Exit(tr.Tr.Get("Could not create path %q: %v", checkoutTo, err))
++      }
++
+       singleCheckout := newSingleCheckout(cfg.Git, "")
+       if singleCheckout.Skip() {
+               fmt.Println(tr.Tr.Get("Cannot checkout LFS objects, Git LFS is 
not installed."))
+diff --git a/commands/pull.go b/commands/pull.go
+index 644e520..9bc2ded 100644
+--- a/commands/pull.go
++++ b/commands/pull.go
+@@ -11,6 +11,7 @@ import (
+       "github.com/git-lfs/git-lfs/v3/git"
+       "github.com/git-lfs/git-lfs/v3/lfs"
+       "github.com/git-lfs/git-lfs/v3/subprocess"
++      "github.com/git-lfs/git-lfs/v3/tools"
+       "github.com/git-lfs/git-lfs/v3/tq"
+       "github.com/git-lfs/git-lfs/v3/tr"
+ )
+@@ -34,6 +35,7 @@ func newSingleCheckout(gitEnv config.Environment, remote 
string) abstractCheckou
+ 
+       return &singleCheckout{
+               gitIndexer:    &gitIndexer{},
++              hasWorkTree:   cfg.LocalWorkingDir() != "",
+               pathConverter: pathConverter,
+               manifest:      manifest,
+       }
+@@ -49,6 +51,7 @@ type abstractCheckout interface {
+ 
+ type singleCheckout struct {
+       gitIndexer    *gitIndexer
++      hasWorkTree   bool
+       pathConverter lfs.PathConverter
+       manifest      *tq.Manifest
+ }
+@@ -62,10 +65,34 @@ func (c *singleCheckout) Skip() bool {
+ }
+ 
+ func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
++      if !c.hasWorkTree {
++              return
++      }
++
+       cwdfilepath := c.pathConverter.Convert(p.Name)
+ 
+-      // Check the content - either missing or still this pointer (not exist 
is ok)
+-      filepointer, err := lfs.DecodePointerFromFile(cwdfilepath)
++      // CVE-2025-26625: check every path component from the working tree
++      // root to cwdfilepath for a symlink/hardlink standing in for a
++      // directory before writing through it (a bare os.Stat/Lstat on just
++      // the final component, as this used to do implicitly via
++      // DecodePointerFromFile, does not catch a symlinked intermediate
++      // directory).
++      dirWalker := tools.NewDirWalkerForFile("", cwdfilepath, cfg)
++      walkErr := dirWalker.Walk()
++
++      var filepointer *lfs.Pointer
++      var err error
++      if walkErr != nil {
++              if !os.IsNotExist(walkErr) {
++                      LoggedError(walkErr, tr.Tr.Get("Checkout error trying 
to check path for %q: %s", cwdfilepath, walkErr))
++                      return
++              }
++              err = walkErr
++      } else {
++              // Check the content - either missing or still this pointer 
(not exist is ok)
++              filepointer, err = lfs.DecodePointerFromFile(cwdfilepath)
++      }
++
+       if err != nil && !os.IsNotExist(err) {
+               if errors.IsNotAPointerError(err) || 
errors.IsBadPointerKeyError(err) {
+                       // File has non-pointer content, leave it alone
+@@ -82,6 +109,13 @@ func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
+               return
+       }
+ 
++      if err != nil && os.IsNotExist(err) {
++              if err := dirWalker.WalkAndCreate(); err != nil {
++                      LoggedError(err, tr.Tr.Get("Checkout error trying to 
create path for %q: %s", cwdfilepath, err))
++                      return
++              }
++      }
++
+       if err := c.RunToPath(p, cwdfilepath); err != nil {
+               if errors.IsDownloadDeclinedError(err) {
+                       // acceptable error, data not local (fetch not run or 
include/exclude)
+diff --git a/lfs/gitfilter_smudge.go b/lfs/gitfilter_smudge.go
+index 0ba031e..b815870 100644
+--- a/lfs/gitfilter_smudge.go
++++ b/lfs/gitfilter_smudge.go
+@@ -16,8 +16,6 @@ import (
+ )
+ 
+ func (f *GitFilter) SmudgeToFile(filename string, ptr *Pointer, download 
bool, manifest *tq.Manifest, cb tools.CopyCallback) error {
+-      tools.MkdirAll(filepath.Dir(filename), f.cfg)
+-
+       // When no pointer file exists on disk, we should use the permissions
+       // defined for the file in Git, since the executable mode may be set.
+       // However, to conform with our legacy behaviour, we do not do this
+diff --git a/tools/dir_walker.go b/tools/dir_walker.go
+new file mode 100644
+index 0000000..5b23e4b
+--- /dev/null
++++ b/tools/dir_walker.go
+@@ -0,0 +1,117 @@
++package tools
++
++import (
++      "fmt"
++      "os"
++      "strings"
++
++      "github.com/git-lfs/git-lfs/v3/errors"
++      "github.com/git-lfs/git-lfs/v3/tr"
++)
++
++// errInvalidDir and errNotDir are sentinel errors checked via errors.Is()
++// by callers and tests. Backported for a Go 1.19 toolchain (Debian
++// bookworm's actual golang-1.19 package): upstream wraps these with the
++// git-lfs errors package's own Join(), which is itself a thin alias for
++// the Go 1.20+ stdlib errors.Join. Go 1.19 predates both the stdlib
++// function and errors.Is's support for multi-error Unwrap() []error, so
++// instead we use a plain %w single-error wrap (fmt.Errorf), which
++// errors.Is has supported since Go 1.13 and is fully sufficient here:
++// each call site only ever needs to test for one sentinel at a time.
++var (
++      errInvalidDir = errors.New(tr.Tr.Get("invalid directory"))
++      errNotDir     = errors.New(tr.Tr.Get("not a directory"))
++)
++
++type DirWalker struct {
++      parentPath string
++      path       string
++      config     repositoryPermissionFetcher
++}
++
++// The parentPath parameter is assumed to be a valid path to a directory
++// in the filesystem.
++//
++// The filePath parameter must be a relative file path as provided by Git,
++// with only the "/" character as a separator and no empty or "." or ".."
++// path segments.  Absolute paths are not supported.
++func NewDirWalkerForFile(parentPath string, filePath string, config 
repositoryPermissionFetcher) *DirWalker {
++      var path string
++      i := strings.LastIndexByte(filePath, '/')
++      if i >= 0 {
++              path = filePath[0:i]
++      }
++
++      return &DirWalker{
++              parentPath: parentPath,
++              path:       path,
++              config:     config,
++      }
++}
++
++// walk() checks each directory in a relative path, starting from the
++// initial parent path, and optionally creates any missing directories
++// in the path.
++//
++// If an existing file or something else other than a directory conflicts
++// with a directory in the path, walk() returns an error.
++//
++// If the create option is false, walk() returns ErrNotExist when a
++// directory is not found.
++//
++// Note that for performance reasons and to be consistent with Git's
++// implementation, walk() does not guard against TOCTOU (time-of-check/
++// time-of-use) races, as the methods of the os.Root type do.
++func (w *DirWalker) walk(create bool) error {
++      currentPath := w.parentPath
++
++      n := len(w.path)
++      for n > 0 {
++              currentDir := w.path
++              nextDirIndex := n
++              i := strings.IndexByte(w.path, '/')
++              if i >= 0 {
++                      currentDir = w.path[0:i]
++                      nextDirIndex = i + 1
++              }
++
++              // These should never occur in Git paths.
++              if currentDir == "" || currentDir == "." || currentDir == ".." {
++                      return fmt.Errorf("%s: %w", tr.Tr.Get("invalid 
directory %q in path: %q", currentDir, w.path), errInvalidDir)
++              }
++
++              if currentPath == "" {
++                      currentPath = currentDir
++              } else {
++                      currentPath += "/" + currentDir
++              }
++
++              stat, err := os.Lstat(currentPath)
++              if err != nil {
++                      if !os.IsNotExist(err) || !create {
++                              return err
++                      }
++
++                      err = Mkdir(currentPath, w.config)
++                      if err != nil {
++                              return err
++                      }
++              } else if !stat.Mode().IsDir() {
++                      return fmt.Errorf("%s: %w", tr.Tr.Get("not a directory: 
%q", currentPath), errNotDir)
++              }
++
++              w.parentPath = currentPath
++              w.path = w.path[nextDirIndex:]
++              n -= nextDirIndex
++      }
++
++      return nil
++}
++
++func (w *DirWalker) Walk() error {
++      return w.walk(false)
++}
++
++func (w *DirWalker) WalkAndCreate() error {
++      return w.walk(true)
++}
+diff --git a/tools/dir_walker_test.go b/tools/dir_walker_test.go
+new file mode 100644
+index 0000000..33cd140
+--- /dev/null
++++ b/tools/dir_walker_test.go
+@@ -0,0 +1,473 @@
++package tools
++
++import (
++      "errors"
++      "fmt"
++      "os"
++      "testing"
++
++      "github.com/stretchr/testify/assert"
++      "github.com/stretchr/testify/require"
++)
++
++type newDirWalkerForFileTestCase struct {
++      filePath        string
++      expectedDirPath string
++}
++
++func (c *newDirWalkerForFileTestCase) Assert(t *testing.T) {
++      w := NewDirWalkerForFile("", c.filePath, nil)
++      assert.Equal(t, c.expectedDirPath, w.path)
++}
++
++func TestNewDirWalkerForFile(t *testing.T) {
++      for desc, c := range map[string]*newDirWalkerForFileTestCase{
++              "filename only":            {"foo.bin", ""},
++              "path with one dir":        {"abc/foo.bin", "abc"},
++              "path with two dirs":       {"abc/def/foo.bin", "abc/def"},
++              "path with leading slash":  {"/foo.bin", ""},
++              "path with trailing slash": {"abc/", "abc"},
++              "bare slash":               {"/", ""},
++              "empty path":               {"", ""},
++      } {
++              t.Run(desc, c.Assert)
++      }
++}
++
++type dirWalkerTestConfig struct{}
++
++func (c *dirWalkerTestConfig) RepositoryPermissions(executable bool) 
os.FileMode {
++      return os.FileMode(0755)
++}
++
++type dirWalkerWalkTestCase struct {
++      parentPath string
++      path       string
++      create     bool
++
++      existsPath string
++      existsFile string
++      existsLink string
++
++      expectedParentPath string
++      expectedPath       string
++      expectedErr        error
++
++      walker *DirWalker
++}
++
++func (c *dirWalkerWalkTestCase) prependParentPath(path string) string {
++      if path == "" {
++              return c.parentPath
++      } else if c.parentPath == "" {
++              return path
++      } else if path[0] == '/' {
++              return "/" + c.parentPath + path
++      } else {
++              return c.parentPath + "/" + path
++      }
++}
++
++func (c *dirWalkerWalkTestCase) setupPaths(t *testing.T, parentPath string) 
error {
++      c.parentPath = parentPath
++
++      if parentPath != "" {
++              if err := os.MkdirAll(parentPath, 0755); err != nil {
++                      return fmt.Errorf("unable to create path: %w", err)
++              }
++      }
++
++      if c.existsPath != "" {
++              c.existsPath = c.prependParentPath(c.existsPath)
++              if err := os.MkdirAll(c.existsPath, 0755); err != nil {
++                      return fmt.Errorf("unable to create path: %w", err)
++              }
++      }
++
++      if c.existsFile != "" {
++              c.existsFile = c.prependParentPath(c.existsFile)
++              f, err := os.Create(c.existsFile)
++              if err != nil {
++                      return fmt.Errorf("unable to create file: %w", err)
++              }
++              f.Close()
++      }
++
++      if c.existsLink != "" {
++              c.existsLink = c.prependParentPath(c.existsLink)
++              if err := os.Symlink(t.TempDir(), c.existsLink); err != nil {
++                      return fmt.Errorf("unable to create symbolic link: %w", 
err)
++              }
++      }
++
++      c.expectedParentPath = c.prependParentPath(c.expectedParentPath)
++
++      return nil
++}
++
++func (c *dirWalkerWalkTestCase) Assert(t *testing.T) {
++      c.walker.parentPath = c.parentPath
++      c.walker.path = c.path
++
++      err := c.walker.walk(c.create)
++
++      assert.Equal(t, c.expectedParentPath, c.walker.parentPath, "found path 
does not match")
++      assert.Equal(t, c.expectedPath, c.walker.path, "missing path does not 
match")
++      if c.expectedErr == nil {
++              assert.NoError(t, err)
++      } else {
++              assert.Error(t, err)
++              assert.True(t, errors.Is(err, c.expectedErr), "wrong error 
type")
++      }
++}
++
++func TestDirWalkerWalk(t *testing.T) {
++      wd, err := os.Getwd()
++      require.NoError(t, err)
++
++      defer os.Chdir(wd)
++
++      for desc, c := range map[string]*dirWalkerWalkTestCase{
++              "empty path": {},
++              "one extant dir": {
++                      path:               "abc",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++              },
++              "one missing dir": {
++                      path:         "abc",
++                      expectedPath: "abc",
++                      expectedErr:  os.ErrNotExist,
++              },
++              "two extant dirs": {
++                      path:               "abc/def",
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc/def",
++              },
++              "two missing dirs": {
++                      path:         "abc/def",
++                      expectedPath: "abc/def",
++                      expectedErr:  os.ErrNotExist,
++              },
++              "three extant dirs": {
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc/def/ghi",
++                      expectedParentPath: "abc/def/ghi",
++              },
++              "three missing dirs": {
++                      path:         "abc/def/ghi",
++                      expectedPath: "abc/def/ghi",
++                      expectedErr:  os.ErrNotExist,
++              },
++              "one extant dir and one missing dir": {
++                      path:               "abc/def",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def",
++                      expectedErr:        os.ErrNotExist,
++              },
++              "one extant dir and two missing dirs": {
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def/ghi",
++                      expectedErr:        os.ErrNotExist,
++              },
++              "two extant dirs and one missing dir": {
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc/def",
++                      expectedPath:       "ghi",
++                      expectedErr:        os.ErrNotExist,
++              },
++              "one missing dir with trailing slash": {
++                      path:         "abc/",
++                      expectedPath: "abc/",
++                      expectedErr:  os.ErrNotExist,
++              },
++              "one extant dir with trailing slash": {
++                      path:               "abc/",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++              },
++              "two extant dirs with trailing slash": {
++                      path:               "abc/def/",
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc/def",
++              },
++              "one extant dir and one missing dir with trailing slash": {
++                      path:               "abc/def/",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def/",
++                      expectedErr:        os.ErrNotExist,
++              },
++              "one conflicting file": {
++                      path:         "abc",
++                      existsFile:   "abc",
++                      expectedPath: "abc",
++                      expectedErr:  errNotDir,
++              },
++              "one extant dir and one conflicting file": {
++                      path:               "abc/def",
++                      existsPath:         "abc",
++                      existsFile:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def",
++                      expectedErr:        errNotDir,
++              },
++              "two extant dirs and one conflicting file": {
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc/def",
++                      existsFile:         "abc/def/ghi",
++                      expectedParentPath: "abc/def",
++                      expectedPath:       "ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one extant dir, one conflicting file, and one missing dir": {
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc",
++                      existsFile:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def/ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one conflicting symlink": {
++                      path:         "abc",
++                      existsLink:   "abc",
++                      expectedPath: "abc",
++                      expectedErr:  errNotDir,
++              },
++              "one extant dir and one conflicting symlink": {
++                      path:               "abc/def",
++                      existsPath:         "abc",
++                      existsLink:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def",
++                      expectedErr:        errNotDir,
++              },
++              "two extant dirs and one conflicting symlink": {
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc/def",
++                      existsLink:         "abc/def/ghi",
++                      expectedParentPath: "abc/def",
++                      expectedPath:       "ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one extant dir, one conflicting symlink, and one missing dir": 
{
++                      path:               "abc/def/ghi",
++                      existsPath:         "abc",
++                      existsLink:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def/ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one extant dir (not modified)": {
++                      path:               "abc",
++                      create:             true,
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++              },
++              "one created dir": {
++                      path:               "abc",
++                      create:             true,
++                      expectedParentPath: "abc",
++              },
++              "two extant dirs (not modified)": {
++                      path:               "abc/def",
++                      create:             true,
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc/def",
++              },
++              "two created dirs": {
++                      path:               "abc/def",
++                      create:             true,
++                      expectedParentPath: "abc/def",
++              },
++              "three extant dirs (not modified)": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc/def/ghi",
++                      expectedParentPath: "abc/def/ghi",
++              },
++              "three created dirs": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      expectedParentPath: "abc/def/ghi",
++              },
++              "one extant dir and one created dir": {
++                      path:               "abc/def",
++                      create:             true,
++                      existsPath:         "abc",
++                      expectedParentPath: "abc/def",
++              },
++              "one extant dir and two created dirs": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc",
++                      expectedParentPath: "abc/def/ghi",
++              },
++              "two extant dirs and one created dir": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc/def/ghi",
++              },
++              "one created dir with trailing slash": {
++                      path:               "abc/",
++                      create:             true,
++                      expectedParentPath: "abc",
++              },
++              "one extant dir with trailing slash (not modified)": {
++                      path:               "abc/",
++                      create:             true,
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++              },
++              "two extant dirs with trailing slash (not modified)": {
++                      path:               "abc/def/",
++                      create:             true,
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc/def",
++              },
++              "one extant dir and one created dir with trailing slash": {
++                      path:               "abc/def/",
++                      create:             true,
++                      existsPath:         "abc",
++                      expectedParentPath: "abc/def",
++              },
++              "one conflicting file (not modified)": {
++                      path:         "abc",
++                      create:       true,
++                      existsFile:   "abc",
++                      expectedPath: "abc",
++                      expectedErr:  errNotDir,
++              },
++              "one extant dir and one conflicting file (not modified)": {
++                      path:               "abc/def",
++                      create:             true,
++                      existsPath:         "abc",
++                      existsFile:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def",
++                      expectedErr:        errNotDir,
++              },
++              "two extant dirs and one conflicting file (not modified)": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc/def",
++                      existsFile:         "abc/def/ghi",
++                      expectedParentPath: "abc/def",
++                      expectedPath:       "ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one extant dir, one conflicting file, and one missing dir (not 
modified)": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc",
++                      existsFile:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def/ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one conflicting symlink (not modified)": {
++                      path:         "abc",
++                      create:       true,
++                      existsLink:   "abc",
++                      expectedPath: "abc",
++                      expectedErr:  errNotDir,
++              },
++              "one extant dir and one conflicting symlink (not modified)": {
++                      path:               "abc/def",
++                      create:             true,
++                      existsPath:         "abc",
++                      existsLink:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def",
++                      expectedErr:        errNotDir,
++              },
++              "two extant dirs and one conflicting symlink (not modified)": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc/def",
++                      existsLink:         "abc/def/ghi",
++                      expectedParentPath: "abc/def",
++                      expectedPath:       "ghi",
++                      expectedErr:        errNotDir,
++              },
++              "one extant dir, one conflicting symlink, and one missing dir 
(not modified)": {
++                      path:               "abc/def/ghi",
++                      create:             true,
++                      existsPath:         "abc",
++                      existsLink:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "def/ghi",
++                      expectedErr:        errNotDir,
++              },
++              "invalid bare slash": {
++                      path:         "/",
++                      expectedPath: "/",
++                      expectedErr:  errInvalidDir,
++              },
++              "invalid multiple slashes": {
++                      path:               "abc//def",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++                      expectedPath:       "/def",
++                      expectedErr:        errInvalidDir,
++              },
++              "invalid leading slash": {
++                      path:         "/abc",
++                      existsPath:   "abc",
++                      expectedPath: "/abc",
++                      expectedErr:  errInvalidDir,
++              },
++              "invalid bare dot component": {
++                      path:         ".",
++                      expectedPath: ".",
++                      expectedErr:  errInvalidDir,
++              },
++              "invalid dot component": {
++                      path:               "abc/./def",
++                      existsPath:         "abc/def",
++                      expectedParentPath: "abc",
++                      expectedPath:       "./def",
++                      expectedErr:        errInvalidDir,
++              },
++              "invalid bare double-dot component": {
++                      path:         "..",
++                      expectedPath: "..",
++                      expectedErr:  errInvalidDir,
++              },
++              "invalid double-dot component": {
++                      path:               "abc/../def",
++                      existsPath:         "abc",
++                      expectedParentPath: "abc",
++                      expectedPath:       "../def",
++                      expectedErr:        errInvalidDir,
++              },
++      } {
++              if err := os.Chdir(t.TempDir()); err != nil {
++                      t.Errorf("unable to change directory: %s", err)
++              }
++
++              c.walker = &DirWalker{
++                      config: &dirWalkerTestConfig{},
++              }
++
++              if err := c.setupPaths(t, ""); err != nil {
++                      t.Error(err)
++                      continue
++              }
++
++              t.Run(desc, c.Assert)
++
++              // retest with parent path; note that this alters the test case
++              if err := c.setupPaths(t, "foo/bar"); err != nil {
++                      t.Error(err)
++                      continue
++              }
++
++              t.Run(desc+" with parent path", c.Assert)
++      }
++}
+diff --git a/tools/filetools.go b/tools/filetools.go
+index 7ec2e24..bfd5950 100644
+--- a/tools/filetools.go
++++ b/tools/filetools.go
+@@ -122,6 +122,15 @@ type repositoryPermissionFetcher interface {
+       RepositoryPermissions(executable bool) os.FileMode
+ }
+ 
++// Mkdir makes a directory with the
++// permissions specified by the core.sharedRepository setting.
++func Mkdir(path string, config repositoryPermissionFetcher) error {
++      umask := 0777 & ^config.RepositoryPermissions(true)
++      return doWithUmask(int(umask), func() error {
++              return os.Mkdir(path, config.RepositoryPermissions(true))
++      })
++}
++
+ // MkdirAll makes a directory and any intervening directories with the
+ // permissions specified by the core.sharedRepository setting.
+ func MkdirAll(path string, config repositoryPermissionFetcher) error {
diff --git a/debian/patches/series b/debian/patches/series
index ab1b745..4285676 100644
- --- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +1,3 @@
 0002-CVE-2024-53263-reject-lf-in-creds.patch
+0003-CVE-2025-26625-symlink-hardlink-write-outside-tree.patch
+0004-CVE-2025-26625-hardening.patch
-----BEGIN PGP SIGNATURE-----

iHUEARYKAB0WIQS82VxZRu76KBuHHJUjLGgvIGfW/wUCaqkQVAAKCRAjLGgvIGfW
/82nAQC6t4ePMbITL6eYbj12t9jOOtAsyX9POGUQcnY4KZ40CAD7BwJLmy/R32wN
wdBNmxS1oP5Meg+UlmW0xZGkjpISeQk=
=d96u
-----END PGP SIGNATURE-----

Reply via email to