Your message dated Sat, 16 May 2026 10:23:17 +0000
with message-id <[email protected]>
and subject line Released with 13.5
has caused the Debian Bug report #1134421,
regarding trixie-pu: package git-lfs/3.6.1-1+deb13u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1134421: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1134421
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:git-lfs
User: [email protected]
Usertags: pu

This fixes CVE-2025-26625, which hardenes symlink handling
in git-lfs. Manual tests and tests as run by debusine were
all fine. debdiff below.

Cheers,
        Moritz

diff -Nru git-lfs-3.6.1/debian/changelog git-lfs-3.6.1/debian/changelog
--- git-lfs-3.6.1/debian/changelog      2025-01-21 07:34:17.000000000 +0100
+++ git-lfs-3.6.1/debian/changelog      2026-04-10 17:17:50.000000000 +0200
@@ -1,3 +1,9 @@
+git-lfs (3.6.1-1+deb13u1) trixie; urgency=medium
+
+  * CVE-2025-26625 (Closes: #1118339)
+
+ -- Moritz Mühlenhoff <[email protected]>  Fri, 10 Apr 2026 17:17:50 +0200
+
 git-lfs (3.6.1-1) unstable; urgency=medium
 
   * New upstream release
diff -Nru git-lfs-3.6.1/debian/patches/CVE-2025-26625.patch 
git-lfs-3.6.1/debian/patches/CVE-2025-26625.patch
--- git-lfs-3.6.1/debian/patches/CVE-2025-26625.patch   1970-01-01 
01:00:00.000000000 +0100
+++ git-lfs-3.6.1/debian/patches/CVE-2025-26625.patch   2026-04-10 
10:14:08.000000000 +0200
@@ -0,0 +1,1190 @@
+Backports of:
+
+From 5c11ffce9a4f095ff356bc781e2a031abb46c1a8 Mon Sep 17 00:00:00 2001
+From: Chris Darroch <[email protected]>
+Date: Thu, 15 May 2025 23:42:40 -0700
+Subject: [PATCH] docs,lfs,t: create new files on checkout and pull
+
+From 0cffe93176b870055c9dadbb3cc9a4a440e98396 Mon Sep 17 00:00:00 2001
+From: Chris Darroch <[email protected]>
+Date: Sun, 24 Aug 2025 21:17:41 -0700
+Subject: [PATCH] check for dir/symlink conflicts on checkout/pull
+
+From d02bd13f02ef76f6807581cd6b34709069cb3615 Mon Sep 17 00:00:00 2001
+From: Chris Darroch <[email protected]>
+Date: Wed, 13 Aug 2025 00:24:02 -0700
+Subject: [PATCH] fix bare repo pull/checkout path handling bug
+
+--- git-lfs-3.6.1.orig/commands/command_checkout.go
++++ git-lfs-3.6.1/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,15 @@ var (
+ func checkoutCommand(cmd *cobra.Command, args []string) {
+       setupRepository()
+ 
++      // TODO: After suitable advance public notice, replace this block
++      // and the preceding call to setupRepository() with a single call to
++      // setupWorkingCopy(), which will perform the same check for a bare
++      // repository but will exit non-zero, as other commands already do.
++      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))
+@@ -92,6 +103,11 @@ func checkoutCommand(cmd *cobra.Command,
+ }
+ 
+ func checkoutConflict(file string, stage git.IndexStage) {
++      err := tools.MkdirAll(filepath.Dir(checkoutTo), cfg)
++      if 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."))
+--- git-lfs-3.6.1.orig/commands/pull.go
++++ git-lfs-3.6.1/commands/pull.go
+@@ -12,6 +12,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"
+ )
+@@ -33,6 +34,7 @@ func newSingleCheckout(gitEnv config.Env
+ 
+       return &singleCheckout{
+               gitIndexer:    &gitIndexer{},
++              hasWorkTree:   cfg.LocalWorkingDir() != "",
+               pathConverter: pathConverter,
+               manifest:      nil,
+               remote:        remote,
+@@ -49,6 +51,7 @@ type abstractCheckout interface {
+ 
+ type singleCheckout struct {
+       gitIndexer    *gitIndexer
++      hasWorkTree   bool
+       pathConverter lfs.PathConverter
+       manifest      tq.Manifest
+       remote        string
+@@ -66,10 +69,26 @@ 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)
++      dirWalker := tools.NewDirWalkerForFile("", p.Name, cfg)
++      err := dirWalker.Walk()
++
++      var filepointer *lfs.Pointer
++      if err != nil {
++              if !os.IsNotExist(err) {
++                      LoggedError(err, tr.Tr.Get("Checkout error trying to 
check path for %q: %s", p.Name, err))
++                      return
++              }
++      } else {
++              // Check the content - either missing or still this pointer 
(not exist is ok)
++              filepointer, err = lfs.DecodePointerFromFile(p.Name)
++      }
++
+       if err != nil {
+               if os.IsNotExist(err) {
+                       output, err := git.DiffIndexWithPaths("HEAD", true, 
[]string{p.Name})
+@@ -99,6 +118,13 @@ func (c *singleCheckout) Run(p *lfs.Wrap
+               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", p.Name, 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)
+--- git-lfs-3.6.1.orig/docs/man/git-lfs-checkout.adoc
++++ git-lfs-3.6.1/docs/man/git-lfs-checkout.adoc
+@@ -30,7 +30,12 @@ to a merge, this option checks out one o
+ Git LFS object into a separate file (which can be outside of the work
+ tree). This can make using diff tools to inspect and resolve merges
+ easier. A single Git LFS object's file path must be provided in
+-`<conflict-obj-path>`.
++`<conflict-obj-path>`. If `<file>` already exists, whether as a regular
++file, symbolic link, or directory, it will be removed and replaced, unless
++it is a non-empty directory or otherwise cannot be deleted.
++
++In a bare repository, this command has no effect.  In a future version,
++this command may exit with an error if it is run in a bare repository.
+ 
+ == OPTIONS
+ 
+--- git-lfs-3.6.1.orig/docs/man/git-lfs-pull.adoc
++++ git-lfs-3.6.1/docs/man/git-lfs-pull.adoc
+@@ -17,6 +17,16 @@ This is equivalent to running the follow
+ 
+ git lfs fetch [options] [] git lfs checkout
+ 
++In a bare repository, if the installed Git version is at least 2.42.0,
++this command will by default fetch Git LFS objects for files only if
++they are present in the Git index and if they match a Git LFS filter
++attribute from a local `gitattributes` file such as
++`$GIT_DIR/info/attributes`. Any `.gitattributes` files in `HEAD` will
++be ignored, unless the `GIT_ATTR_SOURCE` environment variable is set
++to `HEAD`, and any `.gitattributes` files in the index or current
++working tree will always be ignored. These constraints do not apply
++with prior versions of Git.
++
+ == OPTIONS
+ 
+ `-I <paths>`::
+--- git-lfs-3.6.1.orig/lfs/gitfilter_smudge.go
++++ git-lfs-3.6.1/lfs/gitfilter_smudge.go
+@@ -16,23 +16,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 {
++      // 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.
++      var mode os.FileMode = 0666
++      if stat, _ := os.Lstat(filename); stat != nil && 
stat.Mode().IsRegular() {
+               if ptr.Size == 0 && stat.Size() == 0 {
+                       return nil
+               }
+ 
+-              if 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 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)
+@@ -40,9 +34,13 @@ func (f *GitFilter) SmudgeToFile(filenam
+               return errors.New(tr.Tr.Get("could not produce absolute path 
for %q", filename))
+       }
+ 
+-      file, err := os.Create(abs)
++      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 {
+--- git-lfs-3.6.1.orig/t/t-checkout.sh
++++ git-lfs-3.6.1/t/t-checkout.sh
+@@ -114,6 +114,64 @@ begin_test "checkout"
+ )
+ end_test
+ 
++begin_test "checkout: break hard links to existing files"
++(
++  set -e
++
++  reponame="checkout-break-file-hardlinks"
++  setup_remote_repo "$reponame"
++  clone_repo "$reponame" "$reponame"
++
++  git lfs track "*.dat"
++
++  contents="a"
++  contents_oid="$(calc_oid "$contents")"
++  mkdir -p dir1/dir2/dir3
++  printf "%s" "$contents" >a.dat
++  printf "%s" "$contents" >dir1/dir2/dir3/a.dat
++
++  git add .gitattributes a.dat dir1
++  git commit -m "initial commit"
++
++  git push origin main
++  assert_server_object "$reponame" "$contents_oid"
++
++  cd ..
++  GIT_LFS_SKIP_SMUDGE=1 git clone "$GITSERVER/$reponame" "${reponame}-assert"
++
++  cd "${reponame}-assert"
++  git lfs fetch origin main
++
++  assert_local_object "$contents_oid" 1
++
++  rm -f a.dat dir1/dir2/dir3/a.dat ../link
++  pointer="$(git cat-file -p ":a.dat")"
++  echo "$pointer" >../link
++  ln ../link a.dat
++  ln ../link dir1/dir2/dir3/a.dat
++
++  git lfs checkout
++
++  [ "$contents" = "$(cat a.dat)" ]
++  [ "$contents" = "$(cat dir1/dir2/dir3/a.dat)" ]
++  [ "$pointer" = "$(cat ../link)" ]
++  assert_clean_status
++
++  rm a.dat dir1/dir2/dir3/a.dat
++  ln ../link a.dat
++  ln ../link dir1/dir2/dir3/a.dat
++
++  pushd dir1/dir2
++    git lfs checkout
++  popd
++
++  [ "$contents" = "$(cat a.dat)" ]
++  [ "$contents" = "$(cat dir1/dir2/dir3/a.dat)" ]
++  [ "$pointer" = "$(cat ../link)" ]
++  assert_clean_status
++)
++end_test
++
+ begin_test "checkout: without clean filter"
+ (
+   set -e
+@@ -249,6 +307,36 @@ begin_test "checkout: conflicts"
+     echo "abc123" | cmp - theirs.txt
+     echo "def456" | cmp - ours.txt
+ 
++    rm -f base.txt link1 ../ours.txt ../link2
++    ln -s link1 base.txt
++    ln -s link2 ../ours.txt
++
++    git lfs checkout --to base.txt --base file1.dat
++    git lfs checkout --to ../ours.txt --ours file1.dat
++
++    [ ! -L "base.txt" ]
++    [ ! -L "../ours.txt" ]
++    [ ! -e "link1" ]
++    [ ! -e "../link2" ]
++    echo "file1.dat" | cmp - base.txt
++    echo "def456" | cmp - ../ours.txt
++
++    rm -f base.txt link1 ../ours.txt ../link2
++    printf "link1" >link1
++    printf "link2" >../link2
++    ln link1 base.txt
++    ln ../link2 ../ours.txt
++
++    git lfs checkout --to base.txt --base file1.dat
++    git lfs checkout --to ../ours.txt --ours file1.dat
++
++    [ -f "link1" ]
++    [ -f "../link2" ]
++    [ "link1" = "$(cat link1)" ]
++    [ "link2" = "$(cat ../link2)" ]
++    echo "file1.dat" | cmp - base.txt
++    echo "def456" | cmp - ../ours.txt
++
+     git lfs checkout --to base.txt --ours other.txt 2>&1 | tee output.txt
+     grep 'Could not find decoder pointer for object' output.txt
+   popd > /dev/null
+@@ -281,6 +369,23 @@ begin_test "checkout: GIT_WORK_TREE"
+ )
+ end_test
+ 
++begin_test "checkout: bare repository"
++(
++  set -e
++
++  reponame="checkout-bare"
++  git init --bare "$reponame"
++  cd "$reponame"
++
++  git lfs checkout 2>&1 | tee checkout.log
++  if [ "0" -ne "${PIPESTATUS[0]}" ]; then
++    echo >&2 "fatal: expected checkout to succeed ..."
++    exit 1
++  fi
++  [ "This operation must be run in a work tree." = "$(cat checkout.log)" ]
++)
++end_test
++
+ begin_test "checkout: sparse with partial clone and sparse index"
+ (
+   set -e
+--- git-lfs-3.6.1.orig/t/t-pull.sh
++++ git-lfs-3.6.1/t/t-pull.sh
+@@ -157,6 +157,67 @@ begin_test "pull"
+ )
+ end_test
+ 
++begin_test "pull: break hard links to existing files"
++(
++  set -e
++
++  reponame="pull-break-file-hardlinks"
++  setup_remote_repo "$reponame"
++  clone_repo "$reponame" "$reponame"
++
++  git lfs track "*.dat"
++
++  contents="a"
++  contents_oid="$(calc_oid "$contents")"
++  mkdir -p dir1/dir2/dir3
++  printf "%s" "$contents" >a.dat
++  printf "%s" "$contents" >dir1/dir2/dir3/a.dat
++
++  git add .gitattributes a.dat dir1
++  git commit -m "initial commit"
++
++  git push origin main
++  assert_server_object "$reponame" "$contents_oid"
++
++  cd ..
++  GIT_LFS_SKIP_SMUDGE=1 git clone "$GITSERVER/$reponame" "${reponame}-assert"
++
++  cd "${reponame}-assert"
++  refute_local_object "$contents_oid" 1
++
++  rm -f a.dat dir1/dir2/dir3/a.dat ../link
++  pointer="$(git cat-file -p ":a.dat")"
++  echo "$pointer" >../link
++  ln ../link a.dat
++  ln ../link dir1/dir2/dir3/a.dat
++
++  git lfs pull
++  assert_local_object "$contents_oid" 1
++
++  [ "$contents" = "$(cat a.dat)" ]
++  [ "$contents" = "$(cat dir1/dir2/dir3/a.dat)" ]
++  [ "$pointer" = "$(cat ../link)" ]
++  assert_clean_status
++
++  rm a.dat dir1/dir2/dir3/a.dat
++  ln ../link a.dat
++  ln ../link dir1/dir2/dir3/a.dat
++
++  rm -rf .git/lfs/objects
++
++  pushd dir1/dir2
++    git lfs pull
++  popd
++
++  assert_local_object "$contents_oid" 1
++
++  [ "$contents" = "$(cat a.dat)" ]
++  [ "$contents" = "$(cat dir1/dir2/dir3/a.dat)" ]
++  [ "$pointer" = "$(cat ../link)" ]
++  assert_clean_status
++)
++end_test
++
+ begin_test "pull without clean filter"
+ (
+   set -e
+@@ -393,6 +454,137 @@ begin_test "pull with empty file doesn't
+ )
+ end_test
+ 
++begin_test "pull: bare repository"
++(
++  set -e
++
++  reponame="pull-bare"
++  setup_remote_repo "$reponame"
++  clone_repo "$reponame" "$reponame"
++
++  git lfs track "*.dat"
++
++  contents="a"
++  contents_oid="$(calc_oid "$contents")"
++  printf "%s" "$contents" >a.dat
++
++  # The "git lfs pull" command should never check out files in a bare
++  # repository, either into a directory within the repository or one
++  # outside it.  To verify this, we add a Git LFS pointer file whose path
++  # inside the repository is one which, if it were instead treated as an
++  # absolute filesystem path, corresponds to a writable directory.
++  # The "git lfs pull" command should not check out files into either
++  # this external directory or the bare repository.
++  external_dir="$TRASHDIR/${reponame}-external"
++  internal_dir="$(printf "%s" "$external_dir" | sed 's/^\/*//')"
++  mkdir -p "$internal_dir"
++  printf "%s" "$contents" >"$internal_dir/a.dat"
++
++  git add .gitattributes a.dat "$internal_dir/a.dat"
++  git commit -m "initial commit"
++
++  git push origin main
++  assert_server_object "$reponame" "$contents_oid"
++
++  cd ..
++  git clone --bare "$GITSERVER/$reponame" "${reponame}-assert"
++
++  cd "${reponame}-assert"
++  [ ! -e lfs ]
++  refute_local_object "$contents_oid"
++
++  git lfs pull 2>&1 | tee pull.log
++  if [ "0" -ne "${PIPESTATUS[0]}" ]; then
++    echo >&2 "fatal: expected pull to succeed ..."
++    exit 1
++  fi
++
++  # When Git version 2.42.0 or higher is available, the "git lfs pull"
++  # command will use the "git ls-files" command rather than the
++  # "git ls-tree" command to list files.  By default a bare repository
++  # lacks an index, so we expect no Git LFS objects to be fetched when
++  # "git ls-files" is used because Git v2.42.0 or higher is available.
++  gitversion="$(git version | cut -d" " -f3)"
++  set +e
++  compare_version "$gitversion" '2.42.0'
++  result=$?
++  set -e
++  if [ "$result" -eq "$VERSION_LOWER" ]; then
++    grep "Downloading LFS objects" pull.log
++
++    assert_local_object "$contents_oid" 1
++  else
++    grep -q "Downloading LFS objects" pull.log && exit 1
++
++    refute_local_object "$contents_oid"
++  fi
++
++  [ ! -e "a.dat" ]
++  [ ! -e "$internal_dir/a.dat" ]
++  [ ! -e "$external_dir/a.dat" ]
++
++  rm -rf lfs/objects
++  refute_local_object "$contents_oid"
++
++  # When Git version 2.42.0 or higher is available, the "git lfs pull"
++  # command will use the "git ls-files" command rather than the
++  # "git ls-tree" command to list files.  By default a bare repository
++  # lacks an index, so we expect no Git LFS objects to be fetched when
++  # "git ls-files" is used because Git v2.42.0 or higher is available.
++  #
++  # Therefore to verify that the "git lfs pull" command never checks out
++  # files in a bare repository, we first populate the index with Git LFS
++  # pointer files and then retry the command.
++  contents_git_oid="$(git ls-tree HEAD a.dat | awk '{ print $3 }')"
++  git update-index --add --cacheinfo 100644 "$contents_git_oid" a.dat
++  git update-index --add --cacheinfo 100644 "$contents_git_oid" 
"$internal_dir/a.dat"
++
++  # When Git version 2.42.0 or higher is available, the "git lfs pull"
++  # command will use the "git ls-files" command rather than the
++  # "git ls-tree" command to list files, and does so by passing an
++  # "attr:filter=lfs" pathspec to the "git ls-files" command so it only
++  # lists files which match that filter attribute.
++  #
++  # In a bare repository, however, the "git ls-files" command will not read
++  # attributes from ".gitattributes" files in the index, so by default it
++  # will not list any Git LFS pointer files even if those files and the
++  # corresponding ".gitattributes" files have been added to the index and
++  # the pointer files would otherwise match the "attr:filter=lfs" pathspec.
++  #
++  # Therefore, instead of adding the ".gitattributes" file to the index, we
++  # copy it to "info/attributes" so that the pathspec filter will match our
++  # pointer file index entries and they will be listed by the "git ls-files"
++  # command.  This allows us to verify that with Git v2.42.0 or higher, the
++  # "git lfs pull" command will fetch the objects for these pointer files
++  # in the index when the command is run in a bare repository.
++  #
++  # Note that with older versions of Git, the "git lfs pull" command will
++  # use the "git ls-tree" command to list the files in the tree referenced
++  # by HEAD.  The Git LFS objects for any well-formed pointer files found in
++  # that list will then be fetched (unless local copies already exist),
++  # regardless of whether the pointer files actually match a "filter=lfs"
++  # attribute in any ".gitattributes" file in the index, the tree
++  # referenced by HEAD, or the current work tree.
++  if [ "$result" -ne "$VERSION_LOWER" ]; then
++    mkdir -p info
++    git show HEAD:.gitattributes >info/attributes
++  fi
++
++  git lfs pull 2>&1 | tee pull.log
++  if [ "0" -ne "${PIPESTATUS[0]}" ]; then
++    echo >&2 "fatal: expected pull to succeed ..."
++    exit 1
++  fi
++  grep "Downloading LFS objects" pull.log
++
++  assert_local_object "$contents_oid" 1
++
++  [ ! -e "a.dat" ]
++  [ ! -e "$internal_dir/a.dat" ]
++  [ ! -e "$external_dir/a.dat" ]
++)
++end_test
++
+ begin_test "pull with partial clone and sparse checkout and index"
+ (
+   set -e
+--- /dev/null
++++ git-lfs-3.6.1/tools/dir_walker.go
+@@ -0,0 +1,142 @@
++package tools
++
++import (
++      "os"
++      "strings"
++
++      "github.com/git-lfs/git-lfs/v3/errors"
++      "github.com/git-lfs/git-lfs/v3/tr"
++)
++
++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 joinErrors(errors.New(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 joinErrors(errors.New(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)
++}
++
++type joinError struct {
++      errs []error
++}
++
++func (e *joinError) Error() string {
++      var b []byte
++      for i, err := range e.errs {
++              if i > 0 {
++                      b = append(b, '\n')
++              }
++              b = append(b, err.Error()...)
++      }
++      return string(b)
++}
++
++func (e *joinError) Unwrap() []error {
++      return e.errs
++}
++
++func joinErrors(errs ...error) error {
++      var validErrs []error
++      for _, err := range errs {
++              if err != nil {
++                      validErrs = append(validErrs, err)
++              }
++      }
++      if len(validErrs) == 0 {
++              return nil
++      }
++      if len(validErrs) == 1 {
++              return validErrs[0]
++      }
++      return &joinError{errs: validErrs}
++}
+--- /dev/null
++++ git-lfs-3.6.1/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)
++      }
++}
+--- git-lfs-3.6.1.orig/tools/filetools.go
++++ git-lfs-3.6.1/tools/filetools.go
+@@ -121,6 +121,15 @@ type repositoryPermissionFetcher interfa
+       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 -Nru git-lfs-3.6.1/debian/patches/series 
git-lfs-3.6.1/debian/patches/series
--- git-lfs-3.6.1/debian/patches/series 1970-01-01 01:00:00.000000000 +0100
+++ git-lfs-3.6.1/debian/patches/series 2026-04-10 10:13:30.000000000 +0200
@@ -0,0 +1 @@
+CVE-2025-26625.patch

--- End Message ---
--- Begin Message ---
Package: release.debian.org
Version: 13.5

This update has been released as part of Debian 13.5.

--- End Message ---

Reply via email to