From 4799c2cb178a8131e1239fb9f6db43405dd2348c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 16:11:22 +0000 Subject: [PATCH] chore(deps): bump github.com/go-git/go-git/v5 from 5.16.5 to 5.19.1 Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.5 to 5.19.1. - [Release notes](https://github.com/go-git/go-git/releases) - [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md) - [Commits](https://github.com/go-git/go-git/compare/v5.16.5...v5.19.1) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.19.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 12 +- go.sum | 36 +- .../github.com/go-git/go-billy/v5/README.md | 4 + .../go-billy/v5/helper/chroot/chroot.go | 208 +++++++++- .../go-billy/v5/helper/polyfill/polyfill.go | 11 +- .../github.com/go-git/go-billy/v5/osfs/os.go | 5 + .../go-git/go-billy/v5/osfs/os_bound.go | 108 ++++- .../go-git/go-billy/v5/osfs/os_chroot.go | 10 + .../go-git/go-billy/v5/util/util.go | 43 +- .../go-git/go-git/v5/config/config.go | 31 +- .../go-git/go-git/v5/config/modules.go | 52 +++ .../go-git/go-git/v5/config/optbool.go | 82 ++++ .../go-git/v5/internal/pathutil/dotgit.go | 21 + .../go-git/go-git/v5/internal/pathutil/hfs.go | 99 +++++ .../go-git/v5/internal/pathutil/ntfs.go | 187 +++++++++ .../go-git/v5/internal/pathutil/tree.go | 66 +++ .../go-git/go-git/v5/internal/url/url.go | 37 +- .../v5/plumbing/format/idxfile/decoder.go | 174 +++++++- .../v5/plumbing/format/idxfile/idxfile.go | 29 +- .../v5/plumbing/format/index/decoder.go | 110 +++-- .../v5/plumbing/format/index/encoder.go | 39 +- .../go-git/v5/plumbing/format/index/index.go | 2 + .../v5/plumbing/format/objfile/reader.go | 18 +- .../v5/plumbing/format/packfile/diff_delta.go | 3 - .../v5/plumbing/format/packfile/fsobject.go | 8 +- .../v5/plumbing/format/packfile/packfile.go | 21 +- .../v5/plumbing/format/packfile/parser.go | 72 +++- .../plumbing/format/packfile/patch_delta.go | 113 ++++-- .../v5/plumbing/format/packfile/scanner.go | 154 ++++++- .../go-git/v5/plumbing/object/commit.go | 207 +++++----- .../v5/plumbing/object/commit_scanner.go | 377 ++++++++++++++++++ .../go-git/v5/plumbing/object/signature.go | 122 +++++- .../go-git/go-git/v5/plumbing/object/tag.go | 135 ++++--- .../go-git/v5/plumbing/object/tag_scanner.go | 237 +++++++++++ .../go-git/go-git/v5/plumbing/object/tree.go | 151 +++++-- .../v5/plumbing/transport/http/common.go | 168 +++++++- .../v5/plumbing/transport/ssh/common.go | 34 +- .../github.com/go-git/go-git/v5/repository.go | 19 +- .../go-git/go-git/v5/repository_extensions.go | 121 ++++++ .../v5/storage/filesystem/dotgit/dotgit.go | 19 +- .../v5/storage/filesystem/dotgit/writers.go | 68 +++- .../storage/filesystem/dotgit/writers_unix.go | 29 ++ .../filesystem/dotgit/writers_windows.go | 58 +++ .../go-git/v5/storage/filesystem/index.go | 5 + .../go-git/v5/storage/memory/storage.go | 4 + .../github.com/go-git/go-git/v5/submodule.go | 82 +++- .../go-git/go-git/v5/utils/binary/read.go | 15 + .../v5/utils/merkletrie/filesystem/node.go | 108 ++++- .../github.com/go-git/go-git/v5/worktree.go | 148 ++----- .../go-git/go-git/v5/worktree_fs.go | 264 ++++++++++++ .../go-git/go-git/v5/worktree_status.go | 11 +- vendor/github.com/pjbgf/sha1cd/Dockerfile.arm | 2 +- .../github.com/pjbgf/sha1cd/Dockerfile.arm64 | 2 +- vendor/github.com/pjbgf/sha1cd/sha1cd.go | 5 - .../pjbgf/sha1cd/sha1cdblock_amd64.go | 4 +- .../pjbgf/sha1cd/sha1cdblock_amd64.s | 8 +- .../pjbgf/sha1cd/sha1cdblock_arm64.go | 4 +- .../pjbgf/sha1cd/sha1cdblock_generic.go | 25 +- vendor/github.com/pjbgf/sha1cd/ubc/ubc.go | 5 +- .../golang.org/x/crypto/ssh/agent/server.go | 2 +- vendor/golang.org/x/crypto/ssh/cipher.go | 2 +- vendor/golang.org/x/crypto/ssh/client_auth.go | 10 +- .../x/sys/cpu/asm_darwin_arm64_gc.s | 12 + vendor/golang.org/x/sys/cpu/cpu_arm64.go | 9 +- .../golang.org/x/sys/cpu/cpu_darwin_arm64.go | 67 ++++ .../x/sys/cpu/cpu_darwin_arm64_other.go | 31 ++ .../golang.org/x/sys/cpu/cpu_gccgo_arm64.go | 1 + .../golang.org/x/sys/cpu/cpu_other_arm64.go | 6 +- .../golang.org/x/sys/cpu/cpu_windows_arm64.go | 42 -- .../x/sys/cpu/syscall_darwin_arm64_gc.go | 54 +++ vendor/golang.org/x/sys/unix/ztypes_linux.go | 229 ++++++----- vendor/golang.org/x/sys/windows/aliases.go | 1 + .../golang.org/x/sys/windows/dll_windows.go | 37 +- .../x/sys/windows/security_windows.go | 6 +- .../x/sys/windows/syscall_windows.go | 14 - vendor/modules.txt | 23 +- 76 files changed, 3932 insertions(+), 806 deletions(-) create mode 100644 vendor/github.com/go-git/go-git/v5/config/optbool.go create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go create mode 100644 vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go create mode 100644 vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go create mode 100644 vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go create mode 100644 vendor/github.com/go-git/go-git/v5/repository_extensions.go create mode 100644 vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go create mode 100644 vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go create mode 100644 vendor/github.com/go-git/go-git/v5/worktree_fs.go create mode 100644 vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go delete mode 100644 vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go create mode 100644 vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go diff --git a/go.mod b/go.mod index cb949fb..009668b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26 require ( github.com/alecthomas/chroma/v2 v2.23.1 github.com/davecgh/go-spew v1.1.1 - github.com/go-git/go-git/v5 v5.16.5 + github.com/go-git/go-git/v5 v5.19.1 github.com/mattn/go-isatty v0.0.20 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.10.2 @@ -21,19 +21,19 @@ require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.5.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index ad6b8e2..3d6406e 100644 --- a/go.sum +++ b/go.sum @@ -33,12 +33,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -66,8 +66,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= -github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -93,13 +93,13 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -107,14 +107,14 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/vendor/github.com/go-git/go-billy/v5/README.md b/vendor/github.com/go-git/go-billy/v5/README.md index da5c074..f260f79 100644 --- a/vendor/github.com/go-git/go-billy/v5/README.md +++ b/vendor/github.com/go-git/go-billy/v5/README.md @@ -5,6 +5,10 @@ Billy implements an interface based on the `os` standard library, allowing to de Billy was born as part of [go-git/go-git](https://github.com/go-git/go-git) project. +## Version support + +go-billy v5 is in maintenance mode. Users should upgrade to [go-billy v6](https://pkg.go.dev/github.com/go-git/go-billy/v6) where possible. + ## Installation ```go diff --git a/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go b/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go index dbdf111..299d165 100644 --- a/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go +++ b/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go @@ -3,19 +3,25 @@ package chroot import ( "errors" "os" + "path" "path/filepath" "strings" + "syscall" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/helper/polyfill" ) // ChrootHelper is a helper to implement billy.Chroot. +// It is not a security boundary, callers that need containment should use a +// filesystem implementation that enforces paths at the OS boundary instead. type ChrootHelper struct { underlying billy.Filesystem base string } +const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX + // New creates a new filesystem wrapping up the given 'fs'. // The created filesystem has its base in the given ChrootHelperectory of the // underlying filesystem. @@ -34,15 +40,184 @@ func (fs *ChrootHelper) underlyingPath(filename string) (string, error) { return fs.Join(fs.Root(), filename), nil } -func isCrossBoundaries(path string) bool { - path = filepath.ToSlash(path) - path = filepath.Clean(path) +func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) { + fullpath, err := fs.underlyingPath(filename) + if err != nil { + return "", err + } - return strings.HasPrefix(path, ".."+string(filepath.Separator)) + sl, ok := fs.underlying.(billy.Symlink) + if !ok { + return fullpath, nil + } + + rel, err := fs.relativeToRoot(fullpath) + if err != nil { + return "", err + } + + fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl) + if errors.Is(err, billy.ErrNotSupported) { + return fs.underlyingPath(filename) + } + + return fullpath, err +} + +func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) { + if rel == "" { + return fs.resolveFollowedRoot(followFinal, op, sl) + } + + parts := splitRelativePath(rel) + resolved := "" + followed := 0 + + for len(parts) > 0 { + part := parts[0] + parts = parts[1:] + + currentRel := joinRelativePath(resolved, part) + currentPath := fs.Join(fs.Root(), currentRel) + if len(parts) == 0 && !followFinal { + return currentPath, nil + } + + fi, err := sl.Lstat(currentPath) + if err != nil { + if os.IsNotExist(err) { + return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil + } + return "", err + } + + if fi.Mode()&os.ModeSymlink == 0 { + resolved = currentRel + continue + } + + followed++ + if followed > maxFollowedSymlinks { + return "", symlinkLoopError(op, currentPath) + } + + target, err := sl.Readlink(currentPath) + if err != nil { + return "", err + } + + targetRel, err := fs.linkTargetRel(currentPath, target) + if err != nil { + return "", err + } + if targetRel == currentRel { + return "", symlinkLoopError(op, currentPath) + } + + parts = append(splitRelativePath(targetRel), parts...) + resolved = "" + } + + return fs.Join(fs.Root(), resolved), nil +} + +func symlinkLoopError(op, path string) error { + return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP} +} + +func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) { + root := fs.Join(fs.Root(), "") + if !followFinal { + return root, nil + } + + fi, err := sl.Lstat(root) + if err != nil { + if os.IsNotExist(err) { + return root, nil + } + return "", err + } + + if fi.Mode()&os.ModeSymlink == 0 { + return root, nil + } + + target, err := sl.Readlink(root) + if err != nil { + return "", err + } + + targetRel, err := fs.linkTargetRel(root, target) + if err != nil { + return root, err + } + if targetRel == "" { + return "", symlinkLoopError(op, root) + } + + return fs.resolveFollowedPath(targetRel, followFinal, op, sl) +} + +func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) { + rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename)) + if err != nil || isCrossBoundaries(rel) { + return "", billy.ErrCrossedBoundary + } + + if rel == "." { + return "", nil + } + return rel, nil +} + +func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) { + target = filepath.FromSlash(target) + if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) { + return fs.relativeToRoot(target) + } + + return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target)) +} + +func splitRelativePath(filename string) []string { + filename = filepath.Clean(filename) + if filename == "" || filename == "." { + return nil + } + + return strings.Split(filepath.ToSlash(filename), "/") +} + +func joinRelativePath(elem ...string) string { + parts := make([]string, 0, len(elem)) + for _, part := range elem { + if part == "" || part == "." { + continue + } + parts = append(parts, part) + } + + if len(parts) == 0 { + return "" + } + return filepath.Join(parts...) +} + +func isCreateExclusive(flag int) bool { + return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0 +} + +func isCrossBoundaries(name string) bool { + name = filepath.ToSlash(name) + name = strings.TrimLeft(name, "/") + name = path.Clean(name) + + return name == ".." || strings.HasPrefix(name, "../") } func (fs *ChrootHelper) Create(filename string) (billy.File, error) { - fullpath, err := fs.underlyingPath(filename) + fullpath, err := fs.followedPath(filename, true, "create") if err != nil { return nil, err } @@ -56,7 +231,7 @@ func (fs *ChrootHelper) Create(filename string) (billy.File, error) { } func (fs *ChrootHelper) Open(filename string) (billy.File, error) { - fullpath, err := fs.underlyingPath(filename) + fullpath, err := fs.followedPath(filename, true, "open") if err != nil { return nil, err } @@ -70,7 +245,7 @@ func (fs *ChrootHelper) Open(filename string) (billy.File, error) { } func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) { - fullpath, err := fs.underlyingPath(filename) + fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open") if err != nil { return nil, err } @@ -84,12 +259,16 @@ func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (b } func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) { - fullpath, err := fs.underlyingPath(filename) + fullpath, err := fs.followedPath(filename, true, "stat") if err != nil { return nil, err } - return fs.underlying.Stat(fullpath) + fi, err := fs.underlying.Stat(fullpath) + if err != nil { + return nil, err + } + return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil } func (fs *ChrootHelper) Rename(from, to string) error { @@ -135,7 +314,7 @@ func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) { } func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) { - fullpath, err := fs.underlyingPath(path) + fullpath, err := fs.followedPath(path, true, "readdir") if err != nil { return nil, err } @@ -241,6 +420,11 @@ type file struct { name string } +type fileInfo struct { + os.FileInfo + name string +} + func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File { filename = fs.Join(fs.Root(), filename) filename, _ = filepath.Rel(fs.Root(), filename) @@ -254,3 +438,7 @@ func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File { func (f *file) Name() string { return f.name } + +func (fi fileInfo) Name() string { + return fi.name +} diff --git a/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go b/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go index 1efce0e..9fe131b 100644 --- a/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go +++ b/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go @@ -13,7 +13,7 @@ type Polyfill struct { c capabilities } -type capabilities struct{ tempfile, dir, symlink, chroot bool } +type capabilities struct{ tempfile, dir, symlink, chroot, chmod bool } // New creates a new filesystem wrapping up 'fs' the intercepts all the calls // made and errors if fs doesn't implement any of the billy interfaces. @@ -28,6 +28,7 @@ func New(fs billy.Basic) billy.Filesystem { _, h.c.dir = h.Basic.(billy.Dir) _, h.c.symlink = h.Basic.(billy.Symlink) _, h.c.chroot = h.Basic.(billy.Chroot) + _, h.c.chmod = h.Basic.(billy.Chmod) return h } @@ -87,6 +88,14 @@ func (h *Polyfill) Chroot(path string) (billy.Filesystem, error) { return h.Basic.(billy.Chroot).Chroot(path) } +func (h *Polyfill) Chmod(path string, mode os.FileMode) error { + if !h.c.chmod { + return billy.ErrNotSupported + } + + return h.Basic.(billy.Chmod).Chmod(path, mode) +} + func (h *Polyfill) Root() string { if !h.c.chroot { return string(filepath.Separator) diff --git a/vendor/github.com/go-git/go-billy/v5/osfs/os.go b/vendor/github.com/go-git/go-billy/v5/osfs/os.go index a7fe79f..0c240ef 100644 --- a/vendor/github.com/go-git/go-billy/v5/osfs/os.go +++ b/vendor/github.com/go-git/go-billy/v5/osfs/os.go @@ -24,6 +24,9 @@ var Default = &ChrootOS{} // New returns a new OS filesystem. // By default paths are deduplicated, but still enforced // under baseDir. For more info refer to WithDeduplicatePath. +// +// New returns ChrootOS by default for v5 compatibility. Users should prefer +// New with WithBoundOS. func New(baseDir string, opts ...Option) billy.Filesystem { o := &options{ deduplicatePath: true, @@ -47,6 +50,8 @@ func WithBoundOS() Option { } // WithChrootOS returns the option of using a Chroot filesystem OS. +// +// Deprecated: use WithBoundOS instead. func WithChrootOS() Option { return func(o *options) { o.Type = ChrootOSFS diff --git a/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go b/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go index 6f54480..70e6a72 100644 --- a/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go +++ b/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go @@ -20,6 +20,7 @@ package osfs import ( + "errors" "fmt" "os" "path/filepath" @@ -29,6 +30,31 @@ import ( "github.com/go-git/go-billy/v5" ) +var ( + // ErrBaseDirCannotBeRemoved is returned when removing the BoundOS base dir. + ErrBaseDirCannotBeRemoved = errors.New("base dir cannot be removed") + + // ErrBaseDirCannotBeRenamed is returned when renaming the BoundOS base dir. + ErrBaseDirCannotBeRenamed = errors.New("base dir cannot be renamed") + + dotPrefixes = dotPathPrefixes() + dotSeparators = dotPathSeparators() +) + +func dotPathPrefixes() []string { + if filepath.Separator == '\\' { + return []string{"./", ".\\"} + } + return []string{"./"} +} + +func dotPathSeparators() string { + if filepath.Separator == '\\' { + return `/\` + } + return `/` +} + // BoundOS is a fs implementation based on the OS filesystem which is bound to // a base dir. // Prefer this fs implementation over ChrootOS. @@ -54,6 +80,7 @@ func (fs *BoundOS) Create(filename string) (billy.File, error) { } func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { + filename = fs.expandDot(filename) fn, err := fs.abs(filename) if err != nil { return nil, err @@ -62,6 +89,7 @@ func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy. } func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) { + path = fs.expandDot(path) dir, err := fs.abs(path) if err != nil { return nil, err @@ -71,6 +99,12 @@ func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) { } func (fs *BoundOS) Rename(from, to string) error { + if fs.isBaseDir(from) { + return ErrBaseDirCannotBeRenamed + } + from = fs.expandDot(from) + to = fs.expandDot(to) + f, err := fs.abs(from) if err != nil { return err @@ -89,6 +123,7 @@ func (fs *BoundOS) Rename(from, to string) error { } func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error { + path = fs.expandDot(path) dir, err := fs.abs(path) if err != nil { return err @@ -101,6 +136,7 @@ func (fs *BoundOS) Open(filename string) (billy.File, error) { } func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) { + filename = fs.expandDot(filename) filename, err := fs.abs(filename) if err != nil { return nil, err @@ -109,6 +145,11 @@ func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) { } func (fs *BoundOS) Remove(filename string) error { + if fs.isBaseDir(filename) { + return ErrBaseDirCannotBeRemoved + } + filename = fs.expandDot(filename) + fn, err := fs.abs(filename) if err != nil { return err @@ -122,10 +163,19 @@ func (fs *BoundOS) Remove(filename string) error { func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) { if dir != "" { var err error + dir = fs.expandDot(dir) dir, err = fs.abs(dir) if err != nil { return nil, err } + + _, err = os.Stat(dir) + if err != nil && os.IsNotExist(err) { + err = os.MkdirAll(dir, defaultDirectoryMode) + if err != nil { + return nil, err + } + } } return tempFile(dir, prefix) @@ -136,6 +186,11 @@ func (fs *BoundOS) Join(elem ...string) string { } func (fs *BoundOS) RemoveAll(path string) error { + if fs.isBaseDir(path) { + return ErrBaseDirCannotBeRemoved + } + path = fs.expandDot(path) + dir, err := fs.abs(path) if err != nil { return err @@ -144,6 +199,7 @@ func (fs *BoundOS) RemoveAll(path string) error { } func (fs *BoundOS) Symlink(target, link string) error { + link = fs.expandDot(link) ln, err := fs.abs(link) if err != nil { return err @@ -156,6 +212,7 @@ func (fs *BoundOS) Symlink(target, link string) error { } func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) { + filename = fs.expandDot(filename) filename = filepath.Clean(filename) if !filepath.IsAbs(filename) { filename = filepath.Join(fs.baseDir, filename) @@ -167,6 +224,7 @@ func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) { } func (fs *BoundOS) Readlink(link string) (string, error) { + link = fs.expandDot(link) if !filepath.IsAbs(link) { link = filepath.Clean(filepath.Join(fs.baseDir, link)) } @@ -177,6 +235,7 @@ func (fs *BoundOS) Readlink(link string) (string, error) { } func (fs *BoundOS) Chmod(path string, mode os.FileMode) error { + path = fs.expandDot(path) abspath, err := fs.abs(path) if err != nil { return err @@ -191,7 +250,7 @@ func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) { if err != nil { return nil, err } - return New(joined), nil + return New(joined, WithBoundOS()), nil } // Root returns the current base dir of the billy.Filesystem. @@ -212,6 +271,37 @@ func (fs *BoundOS) createDir(fullpath string) error { return nil } +func (fs *BoundOS) expandDot(path string) string { + if path == "." { + return fs.baseDir + } + for _, prefix := range dotPrefixes { + if strings.HasPrefix(path, prefix) { + path = strings.TrimLeft(strings.TrimPrefix(path, prefix), dotSeparators) + if path == "" { + return fs.baseDir + } + return path + } + } + return path +} + +func (fs *BoundOS) isBaseDir(path string) bool { + if path == "" || filepath.Clean(path) == "." { + return true + } + path = fs.expandDot(path) + if filepath.Clean(path) == filepath.Clean(fs.baseDir) { + return true + } + abspath, err := fs.abs(path) + if err != nil { + return false + } + return filepath.Clean(abspath) == filepath.Clean(fs.baseDir) +} + // abs transforms filename to an absolute path, taking into account the base dir. // Relative paths won't be allowed to ascend the base dir, so `../file` will become // `/working-dir/file`. @@ -225,7 +315,7 @@ func (fs *BoundOS) abs(filename string) (string, error) { path, err := securejoin.SecureJoin(fs.baseDir, filename) if err != nil { - return "", nil + return "", err } if fs.deduplicatePath { @@ -238,24 +328,12 @@ func (fs *BoundOS) abs(filename string) (string, error) { return path, nil } -// insideBaseDir checks whether filename is located within -// the fs.baseDir. -func (fs *BoundOS) insideBaseDir(filename string) (bool, error) { - if filename == fs.baseDir { - return true, nil - } - if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) { - return false, fmt.Errorf("path outside base dir") - } - return true, nil -} - // insideBaseDirEval checks whether filename is contained within // a dir that is within the fs.baseDir, by first evaluating any symlinks // that either filename or fs.baseDir may contain. func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) { // "/" contains all others. - if fs.baseDir == "/" { + if fs.baseDir == "/" || fs.baseDir == filename { return true, nil } dir, err := filepath.EvalSymlinks(filepath.Dir(filename)) diff --git a/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go b/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go index 413b3b8..2fa9d8b 100644 --- a/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go +++ b/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go @@ -14,6 +14,8 @@ import ( // ChrootOS is a legacy filesystem based on a "soft chroot" of the os filesystem. // Although this is still the default os filesystem, consider using BoundOS instead. // +// Deprecated: use New with WithBoundOS instead. +// // Behaviours of note: // 1. A "soft chroot" translates the base dir to "/" for the purposes of the // fs abstraction. @@ -24,6 +26,14 @@ import ( type ChrootOS struct{} func newChrootOS(baseDir string) billy.Filesystem { + if baseDir != "" { + resolved, err := filepath.EvalSymlinks(baseDir) + if err != nil { + return chroot.New(&ChrootOS{}, baseDir) + } + baseDir = resolved + } + return chroot.New(&ChrootOS{}, baseDir) } diff --git a/vendor/github.com/go-git/go-billy/v5/util/util.go b/vendor/github.com/go-git/go-billy/v5/util/util.go index 2cdd832..cd869d6 100644 --- a/vendor/github.com/go-git/go-billy/v5/util/util.go +++ b/vendor/github.com/go-git/go-billy/v5/util/util.go @@ -16,8 +16,6 @@ import ( // can but returns the first error it encounters. If the path does not exist, // RemoveAll returns nil (no error). func RemoveAll(fs billy.Basic, path string) error { - fs, path = getUnderlyingAndPath(fs, path) - if r, ok := fs.(removerAll); ok { return r.RemoveAll(path) } @@ -39,7 +37,7 @@ func removeAll(fs billy.Basic, path string) error { } // Otherwise, is this a directory we need to recurse into? - dir, serr := fs.Stat(path) + dir, serr := lstat(fs, path) if serr != nil { if errors.Is(serr, os.ErrNotExist) { return nil @@ -48,8 +46,8 @@ func removeAll(fs billy.Basic, path string) error { return serr } - if !dir.IsDir() { - // Not a directory; return the error from Remove. + if dir.Mode()&os.ModeSymlink != 0 || !dir.IsDir() { + // Not a directory we should recurse into; return the error from Remove. return err } @@ -62,7 +60,7 @@ func removeAll(fs billy.Basic, path string) error { fis, err := dirfs.ReadDir(path) if err != nil { if errors.Is(err, os.ErrNotExist) { - // Race. It was deleted between the Lstat and Open. + // Race. It was deleted between the Lstat and ReadDir. // Return nil per RemoveAll's docs. return nil } @@ -91,7 +89,18 @@ func removeAll(fs billy.Basic, path string) error { } return err +} +func lstat(filesystem billy.Basic, path string) (os.FileInfo, error) { + if sl, ok := filesystem.(billy.Symlink); ok { + // Avoid following a symlink substituted after the initial Remove fails. + fi, err := sl.Lstat(path) + if err == nil || !errors.Is(err, billy.ErrNotSupported) { + return fi, err + } + } + + return filesystem.Stat(path) } // WriteFile writes data to a file named by filename in the given filesystem. @@ -123,8 +132,10 @@ func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) ( // We generate random temporary file names so that there's a good // chance the file doesn't exist yet - keeps the number of tries in // TempFile to a minimum. -var rand uint32 -var randmu sync.Mutex +var ( + rand uint32 + randmu sync.Mutex +) func reseed() uint32 { return uint32(time.Now().UnixNano() + int64(os.Getpid())) @@ -220,22 +231,6 @@ func getTempDir(fs billy.Basic) string { return ".tmp" } -type underlying interface { - Underlying() billy.Basic -} - -func getUnderlyingAndPath(fs billy.Basic, path string) (billy.Basic, string) { - u, ok := fs.(underlying) - if !ok { - return fs, path - } - if ch, ok := fs.(billy.Chroot); ok { - path = fs.Join(ch.Root(), path) - } - - return u.Underlying(), path -} - // ReadFile reads the named file and returns the contents from the given filesystem. // A successful call returns err == nil, not err == EOF. // Because ReadFile reads the whole file, it does not treat an EOF from Read diff --git a/vendor/github.com/go-git/go-git/v5/config/config.go b/vendor/github.com/go-git/go-git/v5/config/config.go index 33f6e37..3ae6a57 100644 --- a/vendor/github.com/go-git/go-git/v5/config/config.go +++ b/vendor/github.com/go-git/go-git/v5/config/config.go @@ -61,6 +61,16 @@ type Config struct { CommentChar string // RepositoryFormatVersion identifies the repository format and layout version. RepositoryFormatVersion format.RepositoryFormatVersion + // ProtectNTFS controls whether NTFS-specific path protections are + // applied (e.g. rejecting .git trailing spaces/periods, alternate + // data streams, 8.3 short names). When unset, defaults to true on + // Windows. + ProtectNTFS OptBool + // ProtectHFS controls whether HFS+-specific path protections are + // applied (e.g. rejecting .git with Unicode zero-width or + // directional characters that HFS+ would normalize away). + // When unset, defaults to true on macOS. + ProtectHFS OptBool } User struct { @@ -266,6 +276,8 @@ const ( repositoryFormatVersionKey = "repositoryformatversion" objectFormat = "objectformat" mirrorKey = "mirror" + protectNTFSKey = "protectNTFS" + protectHFSKey = "protectHFS" // DefaultPackWindow holds the number of previous objects used to // generate deltas. The value 10 is the same used by git command. @@ -309,6 +321,14 @@ func (c *Config) unmarshalCore() { c.Core.Worktree = s.Options.Get(worktreeKey) c.Core.CommentChar = s.Options.Get(commentCharKey) + + if parsed := parseConfigBool(s.Options.Get(protectNTFSKey)); parsed.IsSet() { + c.Core.ProtectNTFS = parsed + } + + if parsed := parseConfigBool(s.Options.Get(protectHFSKey)); parsed.IsSet() { + c.Core.ProtectHFS = parsed + } } func (c *Config) unmarshalUser() { @@ -379,7 +399,8 @@ func unmarshalSubmodules(fc *format.Config, submodules map[string]*Submodule) { m := &Submodule{} m.unmarshal(sub) - if m.Validate() == ErrModuleBadPath { + if err := m.Validate(); errors.Is(err, ErrModuleBadPath) || + errors.Is(err, ErrModuleBadName) { continue } @@ -436,6 +457,14 @@ func (c *Config) marshalCore() { if c.Core.Worktree != "" { s.SetOption(worktreeKey, c.Core.Worktree) } + + if c.Core.ProtectNTFS.IsSet() { + s.SetOption(protectNTFSKey, c.Core.ProtectNTFS.FormatBool()) + } + + if c.Core.ProtectHFS.IsSet() { + s.SetOption(protectHFSKey, c.Core.ProtectHFS.FormatBool()) + } } func (c *Config) marshalExtensions() { diff --git a/vendor/github.com/go-git/go-git/v5/config/modules.go b/vendor/github.com/go-git/go-git/v5/config/modules.go index 1c10aa3..5fdd838 100644 --- a/vendor/github.com/go-git/go-git/v5/config/modules.go +++ b/vendor/github.com/go-git/go-git/v5/config/modules.go @@ -3,8 +3,11 @@ package config import ( "bytes" "errors" + "fmt" "regexp" + "strings" + "github.com/go-git/go-git/v5/internal/pathutil" format "github.com/go-git/go-git/v5/plumbing/format/config" ) @@ -12,6 +15,7 @@ var ( ErrModuleEmptyURL = errors.New("module config: empty URL") ErrModuleEmptyPath = errors.New("module config: empty path") ErrModuleBadPath = errors.New("submodule has an invalid path") + ErrModuleBadName = errors.New("ignoring suspicious submodule name") ) var ( @@ -94,6 +98,10 @@ type Submodule struct { // Validate validates the fields and sets the default values. func (m *Submodule) Validate() error { + if err := validSubmoduleName(m.Name); err != nil { + return fmt.Errorf("%w: %q", ErrModuleBadName, m.Name) + } + if m.Path == "" { return ErrModuleEmptyPath } @@ -109,6 +117,50 @@ func (m *Submodule) Validate() error { return nil } +// validSubmoduleName mirrors canonical Git's check_submodule_name in +// submodule-config.c [1]: reject empty names and any name with a ".." +// path component, using both '/' and '\\' as separators so the rule +// is consistent across platforms. The component check is delegated to +// `pathutil.IsHFSDot` and `pathutil.IsNTFSDot` with `.` as the needle, +// which both cover the bare ".." case and reject components that +// resolve to ".." after HFS+ Unicode normalisation (ignored code +// points, e.g. `..`) or NTFS trailing-space/dot/ADS +// canonicalisation (e.g. `.. `, `..::$INDEX_ALLOCATION`). +// `.gitmodules` is attacker-controlled by definition, so both checks +// run unconditionally regardless of host OS. +// +// The additional checks (bare ".", NUL byte, leading or trailing +// separator, drive-letter prefix) close go-git-specific edge cases +// the canonical loop does not exercise: canonical Git treats names +// as opaque C strings, while Go strings carry NULs through and the +// billy filesystem layer is path-aware in ways Git's working storage +// is not. +// +// [1]: https://github.com/git/git/blob/v2.54.0/submodule-config.c#L214-L237 +func validSubmoduleName(name string) error { + if name == "" || name == "." { + return ErrModuleBadName + } + for _, seg := range strings.FieldsFunc(name, isPathSep) { + if pathutil.IsHFSDot(seg, ".") || pathutil.IsNTFSDot(seg, ".", "") { + return ErrModuleBadName + } + } + // go-git-specific defensive checks beyond canonical Git. + if strings.ContainsRune(name, 0) { + return ErrModuleBadName + } + if isPathSep(rune(name[0])) || isPathSep(rune(name[len(name)-1])) { + return ErrModuleBadName + } + if len(name) >= 2 && name[1] == ':' { + return ErrModuleBadName + } + return nil +} + +func isPathSep(r rune) bool { return r == '/' || r == '\\' } + func (m *Submodule) unmarshal(s *format.Subsection) { m.raw = s diff --git a/vendor/github.com/go-git/go-git/v5/config/optbool.go b/vendor/github.com/go-git/go-git/v5/config/optbool.go new file mode 100644 index 0000000..cb89fbf --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/config/optbool.go @@ -0,0 +1,82 @@ +package config + +import ( + "strconv" + "strings" +) + +// OptBool is a tri-state boolean: unset, explicitly false, or explicitly true. +// Its zero value (OptBoolUnset) means the setting was not specified, which +// allows merge logic based on reflect.Value.IsZero to skip unset fields while +// still letting an explicit "false" override a previously set "true". +type OptBool byte + +const ( + // OptBoolUnset indicates the setting was not specified. + OptBoolUnset OptBool = iota + // OptBoolFalse indicates the setting was explicitly set to false. + OptBoolFalse + // OptBoolTrue indicates the setting was explicitly set to true. + OptBoolTrue +) + +// NewOptBool converts a plain bool into an OptBool. +func NewOptBool(v bool) OptBool { + if v { + return OptBoolTrue + } + return OptBoolFalse +} + +// IsTrue returns whether the value is explicitly true. +func (o OptBool) IsTrue() bool { return o == OptBoolTrue } + +// IsSet returns whether the value was explicitly specified (true or false). +func (o OptBool) IsSet() bool { return o != OptBoolUnset } + +func (o OptBool) String() string { + switch o { + case OptBoolTrue: + return "true" + case OptBoolFalse: + return "false" + default: + return "unset" + } +} + +// FormatBool returns the strconv-formatted value. Only meaningful when IsSet. +func (o OptBool) FormatBool() string { + return strconv.FormatBool(o.IsTrue()) +} + +// parseConfigBool mirrors upstream Git's git_parse_maybe_bool: it +// accepts true/yes/on (→ OptBoolTrue) and false/no/off (→ +// OptBoolFalse) case-insensitively, plus any decimal integer (zero +// → OptBoolFalse, non-zero → OptBoolTrue). Empty or otherwise +// unrecognised values return OptBoolUnset, leaving the caller's +// platform default in place. The empty-string handling is the only +// intentional divergence from upstream, which returns false for +// empty: in our unmarshalCore caller, an empty value means the key +// is unset and the platform default should apply. +// +// Reference: upstream Git git_parse_maybe_bool_text at parse.c +// L157-L173 and git_parse_maybe_bool at parse.c L174-L182 in tag +// v2.54.0[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/parse.c#L157-L182 +func parseConfigBool(v string) OptBool { + switch strings.ToLower(v) { + case "true", "yes", "on": + return OptBoolTrue + case "false", "no", "off": + return OptBoolFalse + } + if i, err := strconv.Atoi(v); err == nil { + if i != 0 { + return OptBoolTrue + } + return OptBoolFalse + } + return OptBoolUnset +} diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go new file mode 100644 index 0000000..e50ee9c --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go @@ -0,0 +1,21 @@ +package pathutil + +import "strings" + +// IsDotGitName reports whether name is `.git` or its 8.3 NTFS short +// alias `git~1`, case-insensitively. Both are forbidden as path +// components (and as submodule names) because they refer to the +// repository's own metadata directory. +// +// File names that do not conform to the 8.3 format (up to eight +// characters for the basename, three for the file extension) are +// associated with a so-called "short name" on NTFS — at least on +// the `C:` drive by default — which means that `git~1/` is a valid +// way to refer to `.git/`. +func IsDotGitName(name string) bool { + switch strings.ToLower(name) { + case ".git", "git~1": + return true + } + return false +} diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go new file mode 100644 index 0000000..66fc12f --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go @@ -0,0 +1,99 @@ +package pathutil + +import "unicode" + +// hfsIgnoredCodepoints contains Unicode code points that HFS+ ignores +// during path normalization. A path component containing these +// characters between the bytes of ".git" (or ".gitmodules", etc.) +// will be treated as that name by HFS+, so they have to be filtered +// out before comparison. +// +// See upstream Git utf8.c next_hfs_char in tag v2.54.0[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L703-L740 +var hfsIgnoredCodepoints = map[rune]struct{}{ + 0x200c: {}, // ZERO WIDTH NON-JOINER + 0x200d: {}, // ZERO WIDTH JOINER + 0x200e: {}, // LEFT-TO-RIGHT MARK + 0x200f: {}, // RIGHT-TO-LEFT MARK + 0x202a: {}, // LEFT-TO-RIGHT EMBEDDING + 0x202b: {}, // RIGHT-TO-LEFT EMBEDDING + 0x202c: {}, // POP DIRECTIONAL FORMATTING + 0x202d: {}, // LEFT-TO-RIGHT OVERRIDE + 0x202e: {}, // RIGHT-TO-LEFT OVERRIDE + 0x206a: {}, // INHIBIT SYMMETRIC SWAPPING + 0x206b: {}, // ACTIVATE SYMMETRIC SWAPPING + 0x206c: {}, // INHIBIT ARABIC FORM SHAPING + 0x206d: {}, // ACTIVATE ARABIC FORM SHAPING + 0x206e: {}, // NATIONAL DIGIT SHAPES + 0x206f: {}, // NOMINAL DIGIT SHAPES + 0xfeff: {}, // ZERO WIDTH NO-BREAK SPACE +} + +// IsHFSDot reports whether part would be treated as "." on an +// HFS+ filesystem after stripping ignored Unicode code points and +// folding ASCII to lower case. The needle is the lowercase ASCII +// suffix without the leading dot (e.g. "git", "gitmodules"). It +// mirrors upstream Git's is_hfs_dot_generic and is the building +// block of IsHFSDotGit / IsHFSDotGitmodules. +// +// Reference: upstream Git utf8.c is_hfs_dot_generic at L741-L774 and +// the dotgit family at L784-L809 in tag v2.54.0[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L741-L809 +func IsHFSDot(part, needle string) bool { + runes := []rune(part) + i := 0 + + // skip ignored code points, then expect '.' + for i < len(runes) { + if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok { + break + } + i++ + } + if i >= len(runes) || runes[i] != '.' { + return false + } + i++ + + // match needle case-insensitively, skipping ignored code points + for _, expected := range needle { + for i < len(runes) { + if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok { + break + } + i++ + } + if i >= len(runes) { + return false + } + r := runes[i] + if r > 127 { + return false + } + if unicode.ToLower(r) != expected { + return false + } + i++ + } + + // skip trailing ignored code points + for i < len(runes) { + if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok { + break + } + i++ + } + + // must be at end of component + return i == len(runes) +} + +// IsHFSDotGit reports whether part is an HFS+ equivalent of ".git". +func IsHFSDotGit(part string) bool { return IsHFSDot(part, "git") } + +// IsHFSDotGitmodules reports whether part is an HFS+ equivalent of +// ".gitmodules", catching attempts to plant the file via Unicode +// code points that HFS+ would strip during normalisation. +func IsHFSDotGitmodules(part string) bool { return IsHFSDot(part, "gitmodules") } diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go new file mode 100644 index 0000000..2ca6c28 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go @@ -0,0 +1,187 @@ +package pathutil + +import "strings" + +// IsNTFSDotGit ports upstream Git's is_ntfs_dotgit. It detects path +// components that NTFS would resolve to ".git": the canonical name +// itself and its 8.3 short-name alias "git~1", each followed by any +// number of trailing spaces or periods (which NTFS silently trims) +// and an optional Alternate Data Stream suffix (":"). The +// bare strings ".git" and "git~1" also match, mirroring upstream. +// +// Reference: upstream Git path.c is_ntfs_dotgit at L1415-L1449 +// in tag v2.54.0[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1415-L1449 +func IsNTFSDotGit(part string) bool { + var i int + switch { + case len(part) >= 4 && part[0] == '.' && + asciiToLower(part[1]) == 'g' && + asciiToLower(part[2]) == 'i' && + asciiToLower(part[3]) == 't': + i = 4 + case len(part) >= 5 && + asciiToLower(part[0]) == 'g' && + asciiToLower(part[1]) == 'i' && + asciiToLower(part[2]) == 't' && + part[3] == '~' && part[4] == '1': + i = 5 + default: + return false + } + + for ; i < len(part); i++ { + c := part[i] + if c == ':' { + return true + } + if c != '.' && c != ' ' { + return false + } + } + return true +} + +// WindowsValidPath reports whether part is a valid Windows / NTFS +// path component for the worktree filesystem abstraction. It rejects +// NTFS-disguised variants of `.git` and `git~1` (trailing spaces, +// periods, Alternate Data Streams) and Windows reserved device +// names. Bare `.git` and `git~1` are allowed at this layer; the +// caller decides whether they are permissible at the current path +// position. +func WindowsValidPath(part string) bool { + if IsNTFSDotGit(part) && !IsDotGitName(part) { + return false + } + return !isWindowsReservedName(part) +} + +// windowsReservedNames lists the Windows reserved device names. +// A path component is reserved if its base name (ignoring trailing +// spaces, extensions, and NTFS Alternate Data Streams) matches one of +// these case-insensitively. +// +// See upstream Git compat/mingw.c is_valid_win32_path(). +var windowsReservedNames = []string{ + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", + "CONIN$", "CONOUT$", +} + +func isWindowsReservedName(part string) bool { + for _, name := range windowsReservedNames { + if len(part) < len(name) { + continue + } + if !strings.EqualFold(part[:len(name)], name) { + continue + } + // Exact match or followed by space, dot, colon (ADS), or separator. + if len(part) == len(name) { + return true + } + switch part[len(name)] { + case ' ', '.', ':': + return true + } + } + return false +} + +// IsNTFSDot ports upstream Git's is_ntfs_dot_generic. It detects NTFS +// path-component variants of a dotfile name that attackers can use to +// bypass case-insensitive comparisons against the canonical name on +// Windows. The dotgit parameter is the lowercase name without the +// leading dot (e.g. "gitmodules"); shortnamePrefix is the canonical +// 6-character NTFS short-name prefix used as a fall-back match +// (e.g. "gi7eba" for ".gitmodules"). +// +// Reference: upstream Git path.c is_ntfs_dot_generic at L1451-L1507 +// in tag v2.54.0[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1451-L1507 +func IsNTFSDot(name, dotgit, shortnamePrefix string) bool { + // onlySpacesAndPeriods returns true when the suffix from start + // onwards consists only of trailing spaces and periods, possibly + // terminated by a NTFS Alternate Data Stream colon. Mirrors the + // only_spaces_and_periods label in upstream's is_ntfs_dot_generic. + onlySpacesAndPeriods := func(start int) bool { + for i := start; i < len(name); i++ { + c := name[i] + if c == ':' { + return true + } + if c != ' ' && c != '.' { + return false + } + } + return true + } + + // Pattern 1: "." prefix + trailing spaces / periods / ADS. + if len(name) >= len(dotgit)+1 && name[0] == '.' && + strings.EqualFold(name[1:1+len(dotgit)], dotgit) { + if onlySpacesAndPeriods(len(dotgit) + 1) { + return true + } + } + + // Pattern 2: standard NTFS short name ~[1-4]. + if len(dotgit) >= 6 && len(name) >= 8 && + strings.EqualFold(name[:6], dotgit[:6]) && + name[6] == '~' && name[7] >= '1' && name[7] <= '4' { + if onlySpacesAndPeriods(8) { + return true + } + } + + // Pattern 3: fall-back NTFS short name keyed by shortnamePrefix. + if len(shortnamePrefix) < 6 || len(name) < 8 { + return false + } + sawTilde := false + i := 0 + for i < 8 { + c := name[i] + switch { + case sawTilde: + if c < '0' || c > '9' { + return false + } + case c == '~': + i++ + if i >= len(name) || name[i] < '1' || name[i] > '9' { + return false + } + sawTilde = true + case i >= 6: + return false + case c&0x80 != 0: + return false + default: + if asciiToLower(c) != shortnamePrefix[i] { + return false + } + } + i++ + } + return onlySpacesAndPeriods(8) +} + +// IsNTFSDotGitmodules reports whether part is an NTFS-equivalent of +// ".gitmodules" — the file name (or any of its variants that NTFS +// would resolve to it) that attackers can use to plant submodule +// configuration disguised as a symlink. The 6-character canonical +// short-name prefix "gi7eba" mirrors upstream Git's is_ntfs_dotgitmodules. +func IsNTFSDotGitmodules(part string) bool { + return IsNTFSDot(part, "gitmodules", "gi7eba") +} + +func asciiToLower(c byte) byte { + if c >= 'A' && c <= 'Z' { + return c + ('a' - 'A') + } + return c +} diff --git a/vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go b/vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go new file mode 100644 index 0000000..e610cd4 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go @@ -0,0 +1,66 @@ +package pathutil + +import ( + "fmt" + "path/filepath" + "strings" +) + +// ErrInvalidPath is returned by ValidTreePath when its argument is +// not a safe path to materialise into the worktree. +var ErrInvalidPath = fmt.Errorf("invalid path") + +// ValidTreePath rejects path strings that, if materialised into a +// worktree, would let an attacker-controlled tree entry escape the +// worktree or rewrite repository metadata. It rejects: +// +// - control characters (< 0x20, 0x7f); +// - empty paths and "." / ".." components; +// - Windows volume name prefixes (e.g. C:); +// - .git, its 8.3 NTFS short-name git~1, plus their HFS+ and NTFS +// variants — at every position, not just the root. +// +// HFS+/NTFS variants of `.git` are always rejected at this layer +// regardless of runtime config: tree paths are canonical UTF-8 with +// no zero-width characters or NTFS short-name forms, so an entry +// that looks like a disguised `.git` is suspicious anywhere. Windows +// reserved device names (CON, NUL, etc.) are not policed here — they +// are legitimate filenames on non-Windows filesystems and upstream +// Git accepts them. The wrapper layer (validPath in package git) +// rejects them at materialisation time when core.protectNTFS is on. +// +// Mirrors upstream Git's verify_path_internal at read-cache.c#L987 +// in tag v2.54.0[1] with protect_hfs / protect_ntfs treated as +// always-on for `.git`-disguise detection (tree paths are not +// application-supplied) and is_valid_win32_path left to the wrapper. +// +// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L987 +func ValidTreePath(p string) error { + for i := 0; i < len(p); i++ { + if p[i] < 0x20 || p[i] == 0x7f { + return fmt.Errorf("%w %q: contains control character", ErrInvalidPath, p) + } + } + + parts := strings.FieldsFunc(p, func(r rune) bool { return r == '\\' || r == '/' }) + if len(parts) == 0 { + return fmt.Errorf("%w: %q", ErrInvalidPath, p) + } + + // Volume names are not supported, in both formats: \\ and :. + if vol := filepath.VolumeName(p); vol != "" { + return fmt.Errorf("%w: %q", ErrInvalidPath, p) + } + + for _, part := range parts { + if part == "." || part == ".." { + return fmt.Errorf("%w %q: cannot use %q", ErrInvalidPath, p, part) + } + + if IsDotGitName(part) || IsHFSDotGit(part) || IsNTFSDotGit(part) { + return fmt.Errorf("%w component: %q", ErrInvalidPath, p) + } + } + + return nil +} diff --git a/vendor/github.com/go-git/go-git/v5/internal/url/url.go b/vendor/github.com/go-git/go-git/v5/internal/url/url.go index 2662448..e40947c 100644 --- a/vendor/github.com/go-git/go-git/v5/internal/url/url.go +++ b/vendor/github.com/go-git/go-git/v5/internal/url/url.go @@ -2,12 +2,14 @@ package url import ( "regexp" + "runtime" + "strings" ) var ( isSchemeRegExp = regexp.MustCompile(`^[^:]+://`) - // Ref: https://github.com/git/git/blob/master/Documentation/urls.txt#L37 + // Ref: https://github.com/git/git/blob/v2.54.0/Documentation/urls.adoc#L41-L48 scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P[^@]+)@)?(?P[^:\s]+):(?:(?P[0-9]{1,5}):)?(?P[^\\].*)$`) ) @@ -20,7 +22,38 @@ func MatchesScheme(url string) bool { // MatchesScpLike returns true if the given string matches an SCP-like // format scheme. func MatchesScpLike(url string) bool { - return scpLikeUrlRegExp.MatchString(url) + if !scpLikeUrlRegExp.MatchString(url) { + return false + } + // Mirror canonical Git's url_is_local_not_ssh in connect.c[1] for + // the cases the regex above cannot disambiguate by itself: a URL + // is treated as a local path (not SCP-style SSH) when a `/` + // precedes the first `:` (e.g. `./relative:path`, + // `/abs/with:colon/file`), or — on Windows only — when it has a + // DOS drive prefix like `C:foo` where the host is a single + // ASCII letter. + // + // [1]: https://github.com/git/git/blob/v2.54.0/connect.c#L710-L716 + if before, _, _ := strings.Cut(url, ":"); strings.Contains(before, "/") { + return false + } + if runtime.GOOS == "windows" && hasDosDrivePrefix(url) { + return false + } + return true +} + +// hasDosDrivePrefix reports whether s begins with `:` (a +// Windows drive prefix such as `C:` or `c:`). Mirrors canonical Git's +// win32_has_dos_drive_prefix[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/compat/win32/path-utils.c#L20-L29 +func hasDosDrivePrefix(s string) bool { + if len(s) < 2 || s[1] != ':' { + return false + } + c := s[0] + return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') } // FindScpLikeComponents returns the user, host, port and path of the diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go index 867553c..825fad9 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "github.com/go-git/go-git/v5/plumbing/hash" "github.com/go-git/go-git/v5/utils/binary" @@ -25,35 +26,88 @@ const ( objectIDLength = hash.Size ) +// Byte sizes of the idx v2 layout elements, used by the size formula +// in [validateIdxV2Size]. See [gitformat-pack] for the canonical +// layout. +// +// [gitformat-pack]: https://git-scm.com/docs/gitformat-pack +const ( + headerLen = 8 // magic + version + fanoutLen = fanout * 4 // uint32 per bucket + crc32Len = 4 // CRC32 per object + offset32Len = 4 // 32-bit offset per object + offset64Len = 8 // 64-bit overflow offset + trailerHashes = 2 // pack checksum + idx checksum, each hashsz +) + +// statInput is the optional shape the [Decoder] probes for at the +// start of [Decoder.Decode] to learn the on-disk length of the idx +// blob, which it uses to validate the canonical-Git size formula +// before any allocations driven by the fanout table. Callers that +// pass an [*os.File] or a `billy.File` backed by an `*os.File` +// (the production call sites in `storage/filesystem`) satisfy it +// directly; arbitrary [io.Reader]s do not, and decode for them +// retains the pre-existing behaviour of erroring out at the +// truncated-payload boundary instead. +// +// The interface is intentionally unexported so the public +// [NewDecoder] signature stays compatible with v5. +type statInput interface { + Stat() (fs.FileInfo, error) +} + // Decoder reads and decodes idx files from an input stream. type Decoder struct { io.Reader - h hash.Hash + src io.Reader + h hash.Hash } // NewDecoder builds a new idx stream decoder, that reads from r. func NewDecoder(r io.Reader) *Decoder { h := hash.New(crypto.SHA1) tr := io.TeeReader(r, h) - return &Decoder{tr, h} + return &Decoder{tr, r, h} } // Decode reads from the stream and decode the content into the MemoryIndex struct. func (d *Decoder) Decode(idx *MemoryIndex) error { + idxSize := int64(-1) + if in, ok := d.src.(statInput); ok { + fi, err := in.Stat() + if err != nil { + return fmt.Errorf("%w: stat input: %w", ErrMalformedIdxFile, err) + } + idxSize = fi.Size() + } + if err := validateHeader(d); err != nil { return err } - flow := []func(*MemoryIndex, io.Reader) error{ + headerFlow := []func(*MemoryIndex, io.Reader) error{ readVersion, readFanout, + } + for _, f := range headerFlow { + if err := f(idx, d); err != nil { + return err + } + } + + if idxSize >= 0 { + if err := validateIdxV2Size(idx, idxSize); err != nil { + return err + } + } + + bodyFlow := []func(*MemoryIndex, io.Reader) error{ readObjectNames, readCRC32, readOffsets, readPackChecksum, } - - for _, f := range flow { + for _, f := range bodyFlow { if err := f(idx, d); err != nil { return err } @@ -91,8 +145,8 @@ func readVersion(idx *MemoryIndex, r io.Reader) error { return err } - if v > VersionSupported { - return ErrUnsupportedVersion + if v != VersionSupported { + return fmt.Errorf("%w: v%d", ErrUnsupportedVersion, v) } idx.Version = v @@ -106,6 +160,10 @@ func readFanout(idx *MemoryIndex, r io.Reader) error { return err } + if k > 0 && n < idx.Fanout[k-1] { + return fmt.Errorf("%w: fanout table is not monotonically non-decreasing at entry %d", ErrMalformedIdxFile, k) + } + idx.Fanout[k] = n idx.FanoutMapping[k] = noMapping } @@ -155,7 +213,7 @@ func readCRC32(idx *MemoryIndex, r io.Reader) error { } func readOffsets(idx *MemoryIndex, r io.Reader) error { - var o64cnt int + var o64cnt int64 for k := 0; k < fanout; k++ { if pos := idx.FanoutMapping[k]; pos != noMapping { if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil { @@ -195,3 +253,103 @@ func readIdxChecksum(idx *MemoryIndex, r io.Reader) error { return nil } + +// validateIdxV2Size enforces the size formula used by canonical Git +// load_idx for idx v2 files: the on-disk length must lie within +// [minSize, maxSize] where +// +// perObject = hashsz + crc32Len + offset32Len +// minSize = headerLen + fanoutLen + trailerHashes*hashsz + nr*perObject +// maxSize = minSize + (nr-1)*offset64Len when nr > 0 +// +// with nr taken from the last fanout entry and hashsz fixed at +// [objectIDLength] (SHA-1 in v5). Multiplications use a self-checking +// overflow guard so inputs whose claimed object count overflows the +// formula are rejected rather than wrapping into a smaller value. +func validateIdxV2Size(idx *MemoryIndex, idxSize int64) error { + nr := int64(idx.Fanout[fanout-1]) + hashsz := int64(objectIDLength) + + minSize := minIdxV2Size(nr, hashsz) + maxSize := maxIdxV2Size(nr, hashsz) + if minSize < 0 || maxSize < 0 { + return fmt.Errorf("%w: object count %d is inconsistent with file size", ErrMalformedIdxFile, nr) + } + + if idxSize < minSize || idxSize > maxSize { + return fmt.Errorf("%w: file size %d is inconsistent with object count %d", ErrMalformedIdxFile, idxSize, nr) + } + return nil +} + +// minIdxV2Size returns the minimum on-disk size of an idx v2 file +// holding nr objects with the given hash size, mirroring the +// computation in canonical Git load_idx. Returns -1 when any +// intermediate multiplication or addition would overflow int64. +func minIdxV2Size(nr, hashsz int64) int64 { + perObject := hashsz + crc32Len + offset32Len + fixed := int64(headerLen+fanoutLen) + trailerHashes*hashsz + + objects, ok := mulInt64(nr, perObject) + if !ok { + return -1 + } + sum, ok := addInt64(fixed, objects) + if !ok { + return -1 + } + return sum +} + +// maxIdxV2Size returns the maximum on-disk size of an idx v2 file +// holding nr objects with the given hash size, mirroring the +// computation in canonical Git load_idx. Returns -1 on overflow. +func maxIdxV2Size(nr, hashsz int64) int64 { + minSize := minIdxV2Size(nr, hashsz) + if minSize < 0 { + return -1 + } + if nr == 0 { + return minSize + } + overflow, ok := mulInt64(nr-1, offset64Len) + if !ok { + return -1 + } + sum, ok := addInt64(minSize, overflow) + if !ok { + return -1 + } + return sum +} + +// mulInt64 returns a*b and whether the result fits in an int64 without +// overflow. Negative operands or overflow yield ok=false. The overflow +// check uses the standard self-inverse identity: a*b/b == a only when +// the multiplication did not wrap. +func mulInt64(a, b int64) (int64, bool) { + if a < 0 || b < 0 { + return 0, false + } + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if c/b != a { + return 0, false + } + return c, true +} + +// addInt64 returns a+b and whether the result fits in an int64 without +// overflow. Negative operands or overflow yield ok=false. +func addInt64(a, b int64) (int64, bool) { + if a < 0 || b < 0 { + return 0, false + } + c := a + b + if c < a { + return 0, false + } + return c, true +} diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go index 136c3e2..f068c25 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go @@ -2,6 +2,7 @@ package idxfile import ( "bytes" + "fmt" "io" "sort" "sync" @@ -126,7 +127,10 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) { return 0, plumbing.ErrObjectNotFound } - offset := idx.getOffset(k, i) + offset, err := idx.getOffset(k, i) + if err != nil { + return 0, err + } // Save the offset for reverse lookup idx.mu.Lock() @@ -141,17 +145,19 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) { const isO64Mask = uint64(1) << 31 -func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) uint64 { +func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) (uint64, error) { offset := secondLevel << 2 ofs := encbin.BigEndian.Uint32(idx.Offset32[firstLevel][offset : offset+4]) if (uint64(ofs) & isO64Mask) != 0 { offset := 8 * (uint64(ofs) & ^isO64Mask) - n := encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8]) - return n + if l := uint64(len(idx.Offset64)); l < 8 || offset > l-8 { + return 0, fmt.Errorf("%w: offset64 index out of range", ErrMalformedIdxFile) + } + return encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8]), nil } - return uint64(ofs) + return uint64(ofs), nil } // FindCRC32 implements the Index interface. @@ -209,8 +215,11 @@ func (idx *MemoryIndex) genOffsetHash() error { mappedFirstLevel := idx.FanoutMapping[firstLevel] for secondLevel := uint32(0); i < fanoutValue; i++ { copy(hash[:], idx.Names[mappedFirstLevel][secondLevel*objectIDLength:]) - offset := int64(idx.getOffset(mappedFirstLevel, int(secondLevel))) - offsetHash[offset] = hash + off, err := idx.getOffset(mappedFirstLevel, int(secondLevel)) + if err != nil { + return err + } + offsetHash[int64(off)] = hash secondLevel++ } } @@ -291,7 +300,11 @@ func (i *idxfileEntryIter) Next() (*Entry, error) { mappedFirstLevel := i.idx.FanoutMapping[i.firstLevel] entry := new(Entry) copy(entry.Hash[:], i.idx.Names[mappedFirstLevel][i.secondLevel*objectIDLength:]) - entry.Offset = i.idx.getOffset(mappedFirstLevel, i.secondLevel) + var err error + entry.Offset, err = i.idx.getOffset(mappedFirstLevel, i.secondLevel) + if err != nil { + return nil, err + } entry.CRC32 = i.idx.getCRC32(mappedFirstLevel, i.secondLevel) i.secondLevel++ diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go index fc25d37..a1bdf00 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go @@ -4,8 +4,8 @@ import ( "bufio" "bytes" "errors" + "fmt" "io" - "strconv" "time" @@ -26,12 +26,14 @@ var ( ErrInvalidChecksum = errors.New("invalid checksum") // ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory ErrUnknownExtension = errors.New("unknown extension") + // ErrMalformedIndexFile is returned when the index file contents are + // structurally invalid. + ErrMalformedIndexFile = errors.New("index decoder: malformed index file") ) const ( entryHeaderLength = 62 entryExtended = 0x4000 - entryValid = 0x8000 nameMask = 0xfff intentToAddMask = 1 << 13 skipWorkTreeMask = 1 << 14 @@ -140,33 +142,55 @@ func (d *Decoder) readEntry(idx *Index) (*Entry, error) { e.SkipWorktree = extended&skipWorkTreeMask != 0 } - if err := d.readEntryName(idx, e, flags); err != nil { + nameConsumed, err := d.readEntryName(idx, e, flags) + if err != nil { return nil, err } - return e, d.padEntry(idx, e, read) + return e, d.padEntry(idx, e, read, nameConsumed) } -func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) error { - var name string - var err error - +// readEntryName reads the entry path and sets e.Name. It returns the +// number of bytes consumed from the stream for the name portion. +func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) (int, error) { switch idx.Version { case 2, 3: - len := flags & nameMask - name, err = d.doReadEntryName(len) + nameLen := flags & nameMask + name, consumed, err := d.doReadEntryName(nameLen) + if err != nil { + return 0, err + } + e.Name = name + return consumed, nil case 4: - name, err = d.doReadEntryNameV4() + name, err := d.doReadEntryNameV4() + if err != nil { + return 0, err + } + e.Name = name + return 0, nil // V4 has no padding; consumed count unused default: - return ErrUnsupportedVersion + return 0, ErrUnsupportedVersion + } +} + +// doReadEntryName reads the entry path for V2/V3 indexes. It returns the +// name, the number of bytes consumed from the stream, and any error. +// When nameLen equals nameMask (0xFFF), the name was too long to fit in +// the 12-bit field and the real length is found by scanning for the NUL +// terminator — matching C Git's strlen(name) fallback in create_from_disk. +func (d *Decoder) doReadEntryName(nameLen uint16) (string, int, error) { + if nameLen == nameMask { + name, err := binary.ReadUntil(d.r, '\x00') + if err != nil { + return "", 0, err + } + return string(name), len(name) + 1, nil // +1 for the consumed NUL delimiter } - if err != nil { - return err - } - - e.Name = name - return nil + name := make([]byte, nameLen) + _, err := io.ReadFull(d.r, name) + return string(name), int(nameLen), err } func (d *Decoder) doReadEntryNameV4() (string, error) { @@ -177,7 +201,14 @@ func (d *Decoder) doReadEntryNameV4() (string, error) { var base string if d.lastEntry != nil { + if l < 0 || int(l) > len(d.lastEntry.Name) { + return "", fmt.Errorf("%w: invalid V4 entry name strip length %d (previous name length: %d)", + ErrMalformedIndexFile, l, len(d.lastEntry.Name)) + } base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)] + } else if l > 0 { + return "", fmt.Errorf("%w: non-zero strip length %d on first V4 entry", + ErrMalformedIndexFile, l) } name, err := binary.ReadUntil(d.r, '\x00') @@ -188,24 +219,23 @@ func (d *Decoder) doReadEntryNameV4() (string, error) { return base + string(name), nil } -func (d *Decoder) doReadEntryName(len uint16) (string, error) { - name := make([]byte, len) - _, err := io.ReadFull(d.r, name) - - return string(name), err -} - -// Index entries are padded out to the next 8 byte alignment -// for historical reasons related to how C Git read the files. -func (d *Decoder) padEntry(idx *Index, e *Entry, read int) error { +// padEntry discards NUL padding bytes that follow each V2/V3 entry on +// disk. nameConsumed is the number of stream bytes consumed while reading +// the entry name (which may exceed len(e.Name) when a NUL terminator was +// consumed for long names where the 12-bit length field overflowed). +func (d *Decoder) padEntry(idx *Index, e *Entry, read, nameConsumed int) error { if idx.Version == 4 { return nil } entrySize := read + len(e.Name) padLen := 8 - entrySize%8 - _, err := io.CopyN(io.Discard, d.r, int64(padLen)) - return err + padLen -= nameConsumed - len(e.Name) + if padLen > 0 { + _, err := io.CopyN(io.Discard, d.r, int64(padLen)) + return err + } + return nil } func (d *Decoder) readExtensions(idx *Index) error { @@ -312,7 +342,7 @@ func (d *Decoder) readChecksum(expected []byte) error { } func validateHeader(r io.Reader) (version uint32, err error) { - var s = make([]byte, 4) + s := make([]byte, 4) if _, err := io.ReadFull(r, s); err != nil { return 0, err } @@ -376,24 +406,26 @@ func (d *treeExtensionDecoder) readEntry() (*TreeEntry, error) { return nil, err } - // An entry can be in an invalidated state and is represented by having a - // negative number in the entry_count field. - if i == -1 { - return nil, nil - } - e.Entries = i trees, err := binary.ReadUntil(d.r, '\n') if err != nil { return nil, err } - i, err = strconv.Atoi(string(trees)) + subtrees, err := strconv.Atoi(string(trees)) if err != nil { return nil, err } - e.Trees = i + e.Trees = subtrees + + // An entry can be in an invalidated state and is represented by having a + // negative number in the entry_count field. In this case, there is no + // object name and the next entry starts immediately after the newline. + if i < 0 { + return nil, nil + } + _, err = io.ReadFull(d.r, e.Hash[:]) if err != nil { return nil, err diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go index c232e03..161bd97 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go @@ -5,9 +5,7 @@ import ( "errors" "fmt" "io" - "path" "sort" - "strings" "time" "github.com/go-git/go-git/v5/plumbing/hash" @@ -160,26 +158,39 @@ func (e *Encoder) encodeEntryName(entry *Entry) error { } func (e *Encoder) encodeEntryNameV4(entry *Entry) error { - name := entry.Name - l := 0 + // V4 prefix compression: find the longest common prefix between the + // previous entry's name and the current one. The strip length tells + // the decoder how many bytes to remove from the end of the previous + // name, and the suffix is the remainder of the current name. + prefix := 0 if e.lastEntry != nil { - dir := path.Dir(e.lastEntry.Name) + "/" - if strings.HasPrefix(entry.Name, dir) { - l = len(e.lastEntry.Name) - len(dir) - name = strings.TrimPrefix(entry.Name, dir) - } else { - l = len(e.lastEntry.Name) - } + prefix = commonPrefixLen(e.lastEntry.Name, entry.Name) + } + stripLen := 0 + if e.lastEntry != nil { + stripLen = len(e.lastEntry.Name) - prefix } e.lastEntry = entry - err := binary.WriteVariableWidthInt(e.w, int64(l)) - if err != nil { + if err := binary.WriteVariableWidthInt(e.w, int64(stripLen)); err != nil { return err } - return binary.Write(e.w, []byte(name+string('\x00'))) + suffix := entry.Name[prefix:] + return binary.Write(e.w, append([]byte(suffix), '\x00')) +} + +// commonPrefixLen returns the length of the longest common byte prefix +// between a and b. +func commonPrefixLen(a, b string) int { + n := min(len(b), len(a)) + for i := range n { + if a[i] != b[i] { + return i + } + } + return n } func (e *Encoder) encodeRawExtension(signature string, data []byte) error { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go index f4c7647..30a7e14 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go @@ -54,6 +54,8 @@ type Index struct { ResolveUndo *ResolveUndo // EndOfIndexEntry represents the 'End of Index Entry' extension EndOfIndexEntry *EndOfIndexEntry + // ModTime is the modification time of the index file + ModTime time.Time } // Add creates a new Entry and returns it. The caller should first check that diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go index 621883a..f9842ed 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go @@ -11,9 +11,10 @@ import ( ) var ( - ErrClosed = errors.New("objfile: already closed") - ErrHeader = errors.New("objfile: invalid header") - ErrNegativeSize = errors.New("objfile: negative object size") + ErrClosed = errors.New("objfile: already closed") + ErrHeader = errors.New("objfile: invalid header") + ErrHeaderNotRead = errors.New("objfile: Header must be called before Read") + ErrNegativeSize = errors.New("objfile: negative object size") ) // Reader reads and decodes compressed objfile data from a provided io.Reader. @@ -100,12 +101,23 @@ func (r *Reader) prepareForRead(t plumbing.ObjectType, size int64) { // // If Read encounters the end of the data stream it will return err == io.EOF, // either in the current call if n > 0 or in a subsequent call. +// +// Read returns ErrHeaderNotRead if Header has not been called successfully. func (r *Reader) Read(p []byte) (n int, err error) { + if r.multi == nil { + return 0, ErrHeaderNotRead + } return r.multi.Read(p) } // Hash returns the hash of the object data stream that has been read so far. +// It returns the zero plumbing.Hash if Header has not been called +// successfully — guarding against the nil hasher that prepareForRead has +// not yet allocated. func (r *Reader) Hash() plumbing.Hash { + if r.multi == nil { + return plumbing.ZeroHash + } return r.hasher.Sum() } diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go index 8898e58..a24b63b 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go @@ -19,9 +19,6 @@ const ( // https://github.com/git/git/blob/f7466e94375b3be27f229c78873f0acf8301c0a5/diff-delta.c#L428 // Max size of a copy operation (64KB). maxCopySize = 64 * 1024 - - // Min size of a copy operation. - minCopySize = 4 ) // GetDelta returns an EncodedObject of type OFSDeltaObject. Base and Target object, diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go index 238339d..93a6faf 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go @@ -78,7 +78,13 @@ func (o *FSObject) Reader() (io.ReadCloser, error) { _ = f.Close() return nil, err } - return ioutil.NewReadCloserWithCloser(r, f.Close), nil + // Cap the lazy stream at the resolved object size: well-formed + // content reaches EOF inside the bound, an inflated stream that + // runs past surfaces ErrInflatedSizeMismatch on the byte just + // past the limit. For delta-resolved objects o.size is the + // expanded size, which is what the caller is reading here. + bounded := newBoundedReadCloser(r, o.size) + return ioutil.NewReadCloserWithCloser(bounded, f.Close), nil } r, err := p.getObjectContent(o.offset) if err != nil { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go index 6852702..f7fb958 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go @@ -126,11 +126,17 @@ func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) { return h, err } -func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) int64 { +func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) (int64, error) { delta := buf.Bytes() - _, delta = decodeLEB128(delta) // skip src size - sz, _ := decodeLEB128(delta) - return int64(sz) + _, delta, err := decodeLEB128(delta) // skip src size + if err != nil { + return 0, err + } + sz, _, err := decodeLEB128(delta) + if err != nil { + return 0, err + } + return int64(sz), nil } func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) { @@ -145,7 +151,7 @@ func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) { return 0, err } - return p.getDeltaObjectSize(buf), nil + return p.getDeltaObjectSize(buf) default: return 0, ErrInvalidObject.AddDetails("type %q", h.Type) } @@ -233,7 +239,10 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing. return nil, err } - size = p.getDeltaObjectSize(buf) + size, err = p.getDeltaObjectSize(buf) + if err != nil { + return nil, err + } if size <= smallObjectThreshold { var obj = new(plumbing.MemoryObject) obj.SetSize(size) diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go index 2659c27..7774d2d 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go @@ -26,6 +26,45 @@ var ( ErrDeltaNotCached = errors.New("delta could not be found in cache") ) +// maxObjectPreallocBytes caps the up-front size hint passed to +// bytes.Buffer.Grow when staging an object's contents, so a malformed length +// cannot trigger a huge or out-of-range allocation. The buffer still grows +// dynamically as data is written; this is purely a hint cap. +const maxObjectPreallocBytes = 1 << 30 // 1 GiB + +// maxObjectsPrealloc caps the up-front capacity reserved from the pack's +// declared object count, so a header advertising an absurd quantity cannot +// trigger a multi-gigabyte allocation. The slice and maps still grow +// organically beyond this hint. +const maxObjectsPrealloc = 1 << 16 // 64 Ki entries + +// Match upstream Git's pack depth ceiling: pack-objects.h OE_DEPTH_BITS, +// enforced in builtin/pack-objects.c as (1 << OE_DEPTH_BITS) - 1. +const maxDeltaChainDepth = 4095 + +// growHint returns a non-negative int64 size, clamped to a sane upper bound, +// suitable for passing to bytes.Buffer.Grow. +func growHint(n int64) int { + switch { + case n <= 0: + return 0 + case n > maxObjectPreallocBytes: + return maxObjectPreallocBytes + default: + return int(n) + } +} + +// objectsHint returns a non-negative count, clamped to maxObjectsPrealloc, +// suitable for passing to make() as the capacity hint for slices or maps +// sized from a pack's declared object count. +func objectsHint(n uint32) int { + if n > maxObjectsPrealloc { + return maxObjectsPrealloc + } + return int(n) +} + // Observer interface is implemented by index encoders. type Observer interface { // OnHeader is called when a new packfile is opened. @@ -166,9 +205,10 @@ func (p *Parser) init() error { } p.count = c - p.oiByHash = make(map[plumbing.Hash]*objectInfo, p.count) - p.oiByOffset = make(map[int64]*objectInfo, p.count) - p.oi = make([]*objectInfo, p.count) + hint := objectsHint(p.count) + p.oiByHash = make(map[plumbing.Hash]*objectInfo, hint) + p.oiByOffset = make(map[int64]*objectInfo, hint) + p.oi = make([]*objectInfo, 0, hint) return nil } @@ -261,7 +301,7 @@ func (p *Parser) indexObjects() error { } if delta && !p.scanner.IsSeekable { buf.Reset() - buf.Grow(int(oh.Length)) + buf.Grow(growHint(oh.Length)) writers = append(writers, buf) } @@ -306,7 +346,7 @@ func (p *Parser) indexObjects() error { } p.oiByOffset[oh.Offset] = ota - p.oi[i] = ota + p.oi = append(p.oi, ota) } return nil @@ -317,8 +357,12 @@ func (p *Parser) resolveDeltas() error { defer sync.PutBytesBuffer(buf) for _, obj := range p.oi { + if err := checkDeltaChainDepth(obj); err != nil { + return err + } + buf.Reset() - buf.Grow(int(obj.Length)) + buf.Grow(growHint(obj.Length)) err := p.get(obj, buf) if err != nil { return err @@ -337,6 +381,9 @@ func (p *Parser) resolveDeltas() error { // create it once and reuse across all children. r := bytes.NewReader(buf.Bytes()) for _, child := range obj.Children { + if err := checkDeltaChainDepth(child); err != nil { + return err + } // Even though we are discarding the output, we still need to read it to // so that the scanner can advance to the next object, and the SHA1 can be // calculated. @@ -356,6 +403,17 @@ func (p *Parser) resolveDeltas() error { return nil } +func checkDeltaChainDepth(o *objectInfo) error { + var depth int + for current := o; current != nil && current.DiskType.IsDelta(); current = current.Parent { + depth++ + if depth > maxDeltaChainDepth { + return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackFile, maxDeltaChainDepth) + } + } + return nil +} + func (p *Parser) resolveExternalRef(o *objectInfo) { if ref, ok := p.oiByHash[o.SHA1]; ok && ref.ExternalRef { p.oiByHash[o.SHA1] = o @@ -405,7 +463,7 @@ func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) (err error) { if o.DiskType.IsDelta() { b := sync.GetBytesBuffer() defer sync.PutBytesBuffer(b) - buf.Grow(int(o.Length)) + buf.Grow(growHint(o.Length)) err := p.get(o.Parent, b) if err != nil { return err diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go index a9c6b9b..4bcb491 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go @@ -31,10 +31,15 @@ const ( // premptively made available for a patch operation. maxPatchPreemptionSize uint = 65536 - // minDeltaSize defines the smallest size for a delta. - minDeltaSize = 4 + // minDeltaSize is the smallest valid delta: a 1-byte srcSz LEB128 + // header followed by a 1-byte targetSz LEB128 header (the + // shortest case being targetSz=0 with no operations). + minDeltaSize = 2 ) +// uintBits is the bit width of uint on the current platform (32 or 64). +const uintBits = 32 << (^uint(0) >> 63) + type offset struct { mask byte shift uint @@ -142,7 +147,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo baseBuf := bufio.NewReader(baseRd) basePos := uint(0) - for { + for remainingTargetSz > 0 { cmd, err := deltaBuf.ReadByte() if err == io.EOF { _ = dstWr.CloseWithError(ErrInvalidDelta) @@ -166,9 +171,9 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo return } - if invalidSize(sz, targetSz) || + if invalidSize(sz, remainingTargetSz) || invalidOffsetSize(offset, sz, srcSz) { - _ = dstWr.Close() + _ = dstWr.CloseWithError(ErrInvalidDelta) return } @@ -210,7 +215,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo case isCopyFromDelta(cmd): sz := uint(cmd) // cmd is the size itself - if invalidSize(sz, targetSz) { + if invalidSize(sz, remainingTargetSz) { _ = dstWr.CloseWithError(ErrInvalidDelta) return } @@ -225,40 +230,48 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo _ = dstWr.CloseWithError(ErrDeltaCmd) return } - - if remainingTargetSz <= 0 { - _ = dstWr.Close() - return - } } + + // Mirror upstream's `data != top` post-loop check: every byte + // of the delta payload must be consumed. + if _, err := deltaBuf.ReadByte(); err == nil { + _ = dstWr.CloseWithError(ErrInvalidDelta) + return + } else if err != io.EOF { + _ = dstWr.CloseWithError(err) + return + } + + _ = dstWr.Close() }() return dstRd, nil } func patchDelta(dst *bytes.Buffer, src, delta []byte) error { - if len(delta) < minCopySize { - return ErrInvalidDelta + srcSz, delta, err := decodeLEB128(delta) + if err != nil { + return fmt.Errorf("%w: %w", ErrInvalidDelta, err) } - - srcSz, delta := decodeLEB128(delta) if srcSz != uint(len(src)) { return ErrInvalidDelta } - targetSz, delta := decodeLEB128(delta) + targetSz, delta, err := decodeLEB128(delta) + if err != nil { + return fmt.Errorf("%w: %w", ErrInvalidDelta, err) + } remainingTargetSz := targetSz - var cmd byte - growSz := min(targetSz, maxPatchPreemptionSize) dst.Grow(int(growSz)) - for { + + for remainingTargetSz > 0 { if len(delta) == 0 { return ErrInvalidDelta } - cmd = delta[0] + cmd := delta[0] delta = delta[1:] switch { @@ -275,16 +288,16 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error { return err } - if invalidSize(sz, targetSz) || + if invalidSize(sz, remainingTargetSz) || invalidOffsetSize(offset, sz, srcSz) { - break + return ErrInvalidDelta } dst.Write(src[offset : offset+sz]) remainingTargetSz -= sz case isCopyFromDelta(cmd): sz := uint(cmd) // cmd is the size itself - if invalidSize(sz, targetSz) { + if invalidSize(sz, remainingTargetSz) { return ErrInvalidDelta } @@ -299,10 +312,12 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error { default: return ErrDeltaCmd } + } - if remainingTargetSz <= 0 { - break - } + // Mirror upstream's `data != top` post-loop check: every byte of + // the delta payload must be consumed. + if len(delta) != 0 { + return ErrInvalidDelta } return nil @@ -354,7 +369,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, baselr := io.LimitReader(sr, 0).(*io.LimitedReader) deltalr := io.LimitReader(deltaBuf, 0).(*io.LimitedReader) - for { + for remainingTargetSz > 0 { buf := *bufp cmd, err := deltaBuf.ReadByte() if err == io.EOF { @@ -374,9 +389,9 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, return 0, plumbing.ZeroHash, err } - if invalidSize(sz, targetSz) || + if invalidSize(sz, remainingTargetSz) || invalidOffsetSize(offset, sz, srcSz) { - return 0, plumbing.ZeroHash, err + return 0, plumbing.ZeroHash, ErrInvalidDelta } if _, err := sr.Seek(int64(offset), io.SeekStart); err != nil { @@ -389,7 +404,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, remainingTargetSz -= sz } else if isCopyFromDelta(cmd) { sz := uint(cmd) // cmd is the size itself - if invalidSize(sz, targetSz) { + if invalidSize(sz, remainingTargetSz) { return 0, plumbing.ZeroHash, ErrInvalidDelta } deltalr.N = int64(sz) @@ -399,30 +414,41 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader, remainingTargetSz -= sz } else { - return 0, plumbing.ZeroHash, err - } - if remainingTargetSz <= 0 { - break + return 0, plumbing.ZeroHash, ErrDeltaCmd } } + // Mirror upstream's `data != top` post-loop check: every byte of + // the delta payload must be consumed. + if _, err := deltaBuf.ReadByte(); err == nil { + return 0, plumbing.ZeroHash, ErrInvalidDelta + } else if err != io.EOF { + return 0, plumbing.ZeroHash, err + } + return targetSz, hasher.Sum(), nil } // Decodes a number encoded as an unsigned LEB128 at the start of some -// binary data and returns the decoded number and the rest of the -// stream. +// binary data and returns the decoded number, the rest of the stream, +// and an error if the encoded value does not fit in a uint. // // This must be called twice on the delta data buffer, first to get the // expected source buffer size, and again to get the target buffer size. -func decodeLEB128(input []byte) (uint, []byte) { +func decodeLEB128(input []byte) (uint, []byte, error) { if len(input) == 0 { - return 0, input + return 0, input, nil } var num, sz uint var b byte for { + // A continuation byte at shift > uintBits-7 cannot contribute + // without overflowing the accumulator. + if sz*7 > uintBits-7 { + return 0, input, ErrLengthOverflow + } + b = input[sz] num |= (uint(b) & payload) << (sz * 7) // concats 7 bits chunks sz++ @@ -432,12 +458,16 @@ func decodeLEB128(input []byte) (uint, []byte) { } } - return num, input[sz:] + return num, input[sz:], nil } func decodeLEB128ByteReader(input io.ByteReader) (uint, error) { var num, sz uint for { + if sz*7 > uintBits-7 { + return 0, ErrLengthOverflow + } + b, err := input.ReadByte() if err != nil { return 0, err @@ -529,8 +559,9 @@ func decodeSize(cmd byte, delta []byte) (uint, []byte, error) { return sz, delta, nil } -func invalidSize(sz, targetSz uint) bool { - return sz > targetSz +// invalidSize reports whether sz exceeds the remaining target size. +func invalidSize(sz, remaining uint) bool { + return sz > remaining } func invalidOffsetSize(offset, sz, srcSz uint) bool { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go index 8318aae..6d2907e 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go @@ -29,8 +29,100 @@ var ( ErrSeekNotSupported = NewError("not seek support") // ErrMalformedPackFile is returned by the parser when the pack file is corrupted. ErrMalformedPackFile = errors.New("malformed PACK file") + // ErrLengthOverflow is returned when a variable-length integer would not + // fit into its accumulator because the input declares more continuation + // bytes than the type can hold. + ErrLengthOverflow = errors.New("variable-length integer overflow") + // ErrInflatedSizeMismatch is returned when a packfile object inflates to + // more bytes than the size declared in its object header. A well-formed + // packfile never produces more data than the declared size; exceeding it + // indicates a structurally invalid entry. + ErrInflatedSizeMismatch = errors.New("packfile: inflated object exceeds declared size") ) +// boundedWriter passes writes through to w up to limit bytes total, then +// returns ErrInflatedSizeMismatch. It is used to enforce that a packfile +// object's inflated length does not exceed the size declared in its header. +type boundedWriter struct { + w io.Writer + limit int64 + n int64 +} + +// Write forwards p to the underlying writer while keeping the running total +// at or below limit. On overrun it forwards the legal prefix and reports +// the number of bytes actually consumed alongside ErrInflatedSizeMismatch, +// matching the contract in io.Writer. A write error from the underlying +// writer during overrun-handling is joined with ErrInflatedSizeMismatch so +// it is not silently dropped. +func (b *boundedWriter) Write(p []byte) (int, error) { + if b.n+int64(len(p)) > b.limit { + remain := int(b.limit - b.n) + err := error(ErrInflatedSizeMismatch) + if remain > 0 { + n, werr := b.w.Write(p[:remain]) + b.n += int64(n) + if werr != nil { + err = errors.Join(ErrInflatedSizeMismatch, werr) + } + return n, err + } + return 0, err + } + n, err := b.w.Write(p) + b.n += int64(n) + return n, err +} + +// boundedReadCloser wraps a ReadCloser and reports ErrInflatedSizeMismatch +// once more than limit bytes have been read. It is used by the on-demand +// object reader returned from FSObject.Reader so that a lazy Read of a +// packfile object cannot stream past its declared inflated size. +// +// The implementation builds on io.LimitedReader with the standard +// overrun-detection trick: request limit+1 bytes from the underlying so +// that the moment the sentinel byte materializes (LimitedReader.N drops +// to zero) we know the source produced more than limit bytes. +type boundedReadCloser struct { + lr io.LimitedReader + closer io.Closer + overrun bool +} + +// newBoundedReadCloser wraps rc so that the cumulative bytes returned from +// Read never exceed limit. The first call that would have returned a byte +// past limit instead returns ErrInflatedSizeMismatch; subsequent calls +// keep returning the same error. A negative limit is treated as zero, so +// the first byte produced by rc surfaces ErrInflatedSizeMismatch. +func newBoundedReadCloser(rc io.ReadCloser, limit int64) *boundedReadCloser { + if limit < 0 { + limit = 0 + } + return &boundedReadCloser{ + lr: io.LimitedReader{R: rc, N: limit + 1}, + closer: rc, + } +} + +// Read forwards Read up to the configured byte limit. When the underlying +// stream produces the limit+1 sentinel byte, the legal prefix is returned +// alongside ErrInflatedSizeMismatch; on subsequent calls only the error +// is returned. +func (b *boundedReadCloser) Read(p []byte) (int, error) { + if b.overrun { + return 0, ErrInflatedSizeMismatch + } + n, err := b.lr.Read(p) + if b.lr.N == 0 { + b.overrun = true + return n - 1, ErrInflatedSizeMismatch + } + return n, err +} + +// Close closes the underlying ReadCloser. +func (b *boundedReadCloser) Close() error { return b.closer.Close() } + // ObjectHeader contains the information related to the object, this information // is collected from the previous bytes to the content of the object. type ObjectHeader struct { @@ -220,6 +312,13 @@ func (s *Scanner) nextObjectHeader() (*ObjectHeader, error) { return nil, err } + // An OFS-delta references a base object that appears earlier + // in the pack; the negative offset must be strictly positive + // and not larger than the current object's offset. + if no <= 0 || no > h.Offset { + return nil, fmt.Errorf("%w: invalid OFS delta offset", ErrMalformedPackFile) + } + h.OffsetReference = h.Offset - no case plumbing.REFDeltaObject: var err error @@ -303,6 +402,13 @@ func (s *Scanner) readLength(first byte) (int64, error) { shift := firstLengthBits var err error for c&maskContinue > 0 { + // Mirrors unpack_object_header_buffer in canonical Git's + // packfile.c: a continuation byte at shift > 64-7 cannot + // contribute without overflowing an int64. + if shift > 64-lengthBits { + return 0, fmt.Errorf("%w: %w", ErrMalformedPackFile, ErrLengthOverflow) + } + if c, err = s.r.ReadByte(); err != nil { return 0, err } @@ -315,10 +421,18 @@ func (s *Scanner) readLength(first byte) (int64, error) { } // NextObject writes the content of the next object into the reader, returns -// the number of bytes written, the CRC32 of the content and an error, if any +// the number of bytes written, the CRC32 of the content and an error, if any. +// +// When a prior NextObjectHeader has stashed the object header in +// pendingObject, the inflated stream is bounded by the header's declared +// length and surfaces ErrInflatedSizeMismatch on overrun. func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err error) { + declaredSize := int64(-1) + if s.pendingObject != nil { + declaredSize = s.pendingObject.Length + } s.pendingObject = nil - written, err = s.copyObject(w) + written, err = s.copyObject(w, declaredSize) s.r.Flush() crc32 = s.crc.Sum32() @@ -327,23 +441,39 @@ func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err erro return } -// ReadObject returns a reader for the object content and an error +// ReadObject returns a reader for the object content and an error. +// +// When a prior NextObjectHeader has stashed the object header in +// pendingObject, the returned reader is bounded by the header's declared +// length so callers cannot stream past the declared inflated size; an +// overrun surfaces ErrInflatedSizeMismatch on the byte just past the +// limit. func (s *Scanner) ReadObject() (io.ReadCloser, error) { + declaredSize := int64(-1) + if s.pendingObject != nil { + declaredSize = s.pendingObject.Length + } s.pendingObject = nil zr, err := sync.GetZlibReader(s.r) if err != nil { return nil, fmt.Errorf("zlib reset error: %s", err) } - return ioutil.NewReadCloserWithCloser(zr.Reader, func() error { + rc := ioutil.NewReadCloserWithCloser(zr.Reader, func() error { sync.PutZlibReader(zr) return nil - }), nil + }) + if declaredSize >= 0 { + return newBoundedReadCloser(rc, declaredSize), nil + } + return rc, nil } -// ReadRegularObject reads and write a non-deltified object -// from it zlib stream in an object entry in the packfile. -func (s *Scanner) copyObject(w io.Writer) (n int64, err error) { +// copyObject inflates a non-deltified object's zlib stream into w. When +// declaredSize is non-negative, the write sink is wrapped in a +// boundedWriter so an overrun surfaces ErrInflatedSizeMismatch instead +// of being silently appended. +func (s *Scanner) copyObject(w io.Writer, declaredSize int64) (n int64, err error) { zr, err := sync.GetZlibReader(s.r) defer sync.PutZlibReader(zr) @@ -352,8 +482,14 @@ func (s *Scanner) copyObject(w io.Writer) (n int64, err error) { } defer ioutil.CheckClose(zr.Reader, &err) + + sink := w + if declaredSize >= 0 { + sink = &boundedWriter{w: w, limit: declaredSize} + } + buf := sync.GetByteSlice() - n, err = io.CopyBuffer(w, zr.Reader, *buf) + n, err = io.CopyBuffer(sink, zr.Reader, *buf) sync.PutByteSlice(buf) return } diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go index 78627b0..07034c1 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go @@ -5,7 +5,7 @@ import ( "context" "errors" "fmt" - "io" + "slices" "strings" "github.com/ProtonMail/go-crypto/openpgp" @@ -20,6 +20,7 @@ const ( beginpgp string = "-----BEGIN PGP SIGNATURE-----" endpgp string = "-----END PGP SIGNATURE-----" headerpgp string = "gpgsig" + headerpgp256 string = "gpgsig-sha256" headerencoding string = "encoding" // https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153 @@ -41,6 +42,11 @@ type MessageEncoding string // in time, such as a timestamp, the author of the changes since the last // commit, a pointer to the previous commit(s), etc. // http://shafiulazam.com/gitbook/1_the_git_object_model.html +// +// When a Commit is populated by Decode it retains a reference to the source +// plumbing.EncodedObject so that EncodeWithoutSignature can reproduce the +// exact bytes the signature was computed over. Refer to EncodeWithoutSignature +// for more information. type Commit struct { // Hash of the commit object. Hash plumbing.Hash @@ -66,6 +72,9 @@ type Commit struct { ExtraHeaders []ExtraHeader s storer.EncodedObjectStorer + // src holds the encoded object this Commit was decoded from, used by + // EncodeWithoutSignature to recover the canonical signed bytes. + src plumbing.EncodedObject } // ExtraHeader holds any non-standard header @@ -98,8 +107,8 @@ func (h ExtraHeader) Format(f fmt.State, verb rune) { func parseExtraHeader(line []byte) (ExtraHeader, bool) { split := bytes.SplitN(line, []byte{' '}, 2) - out := ExtraHeader { - Key: string(bytes.TrimRight(split[0], "\n")), + out := ExtraHeader{ + Key: string(bytes.TrimRight(split[0], "\n")), Value: "", } @@ -181,6 +190,11 @@ func (c *Commit) NumParents() int { var ErrParentNotFound = errors.New("commit parent not found") +// ErrMalformedCommit is returned when a commit object cannot be decoded +// because its standard headers (tree, parent, author, committer) are missing, +// duplicated, or out of order. +var ErrMalformedCommit = errors.New("malformed commit") + // Parent returns the ith parent of a commit. func (c *Commit) Parent(i int) (*Commit, error) { if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 { @@ -227,14 +241,23 @@ func (c *Commit) Type() plumbing.ObjectType { return plumbing.CommitObject } +func (c *Commit) reset() { + storer := c.s + *c = Commit{ + Encoding: defaultUtf8CommitMessageEncoding, + s: storer, + } +} + // Decode transforms a plumbing.EncodedObject into a Commit struct. func (c *Commit) Decode(o plumbing.EncodedObject) (err error) { if o.Type() != plumbing.CommitObject { return ErrUnsupportedObject } + c.reset() c.Hash = o.Hash() - c.Encoding = defaultUtf8CommitMessageEncoding + c.src = o reader, err := o.Reader() if err != nil { @@ -245,97 +268,17 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) { r := sync.GetBufioReader(reader) defer sync.PutBufioReader(r) - var message bool - var mergetag bool - var pgpsig bool - var msgbuf bytes.Buffer - var extraheader *ExtraHeader = nil - for { - line, err := r.ReadBytes('\n') - if err != nil && err != io.EOF { + s := &commitScanner{r: r, c: c} + for state := scanTree; state != nil; { + state, err = state(s) + if err != nil { return err } - - if mergetag { - if len(line) > 0 && line[0] == ' ' { - line = bytes.TrimLeft(line, " ") - c.MergeTag += string(line) - continue - } else { - mergetag = false - } - } - - if pgpsig { - if len(line) > 0 && line[0] == ' ' { - line = bytes.TrimLeft(line, " ") - c.PGPSignature += string(line) - continue - } else { - pgpsig = false - } - } - - if extraheader != nil { - if len(line) > 0 && line[0] == ' ' { - extraheader.Value += string(line[1:]) - continue - } else { - extraheader.Value = strings.TrimRight(extraheader.Value, "\n") - c.ExtraHeaders = append(c.ExtraHeaders, *extraheader) - extraheader = nil - } - } - - if !message { - original_line := line - line = bytes.TrimSpace(line) - if len(line) == 0 { - message = true - continue - } - - split := bytes.SplitN(line, []byte{' '}, 2) - - var data []byte - if len(split) == 2 { - data = split[1] - } - - switch string(split[0]) { - case "tree": - c.TreeHash = plumbing.NewHash(string(data)) - case "parent": - c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data))) - case "author": - c.Author.Decode(data) - case "committer": - c.Committer.Decode(data) - case headermergetag: - c.MergeTag += string(data) + "\n" - mergetag = true - case headerencoding: - c.Encoding = MessageEncoding(data) - case headerpgp: - c.PGPSignature += string(data) + "\n" - pgpsig = true - default: - h, maybecontinued := parseExtraHeader(original_line) - if maybecontinued { - extraheader = &h - } else { - c.ExtraHeaders = append(c.ExtraHeaders, h) - } - } - } else { - msgbuf.Write(line) - } - - if err == io.EOF { - break - } } - c.Message = msgbuf.String() + if !s.sawTree { + return fmt.Errorf("%w: missing tree header", ErrMalformedCommit) + } + c.Message = s.msgbuf.String() return nil } @@ -344,11 +287,73 @@ func (c *Commit) Encode(o plumbing.EncodedObject) error { return c.encode(o, true) } -// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature). +// EncodeWithoutSignature exports a Commit into a plumbing.EncodedObject +// without any signature headers, producing the payload that PGP/GPG +// signatures are computed over. +// +// Behaviour depends on how the Commit was created: +// +// - For Commits populated by Decode whose exported fields still match the +// source object, the payload is streamed from the raw source bytes with +// gpgsig and gpgsig-sha256 headers (and their continuation lines) +// stripped verbatim. This preserves the exact bytes the signature was +// computed over, regardless of any normalization performed by Decode. +// +// - For Commits constructed in memory, or for decoded Commits whose +// exported fields have been mutated, the payload is derived from the +// current struct fields. Mutation is detected by re-decoding the source +// object and comparing exported fields; if any differ, the in-memory +// representation prevails. func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error { + if c.matchesSource() { + return stripObjectSignatures(o, c.src, plumbing.CommitObject) + } return c.encode(o, false) } +// matchesSource reports whether c.src is set and re-decoding it produces a +// Commit whose payload-affecting exported fields are identical to those of +// c. It is the auto-detection used by EncodeWithoutSignature to decide +// between the raw bytes and the struct-encoded payload. +// +// PGPSignature is intentionally excluded from the comparison: neither path +// emits it, so mutating it must not trigger a switch to struct-encode (which +// would change the byte layout the caller is trying to verify against). +func (c *Commit) matchesSource() bool { + if c.src == nil { + return false + } + fresh := &Commit{} + if err := fresh.Decode(c.src); err != nil { + return false + } + return c.Hash == fresh.Hash && + signatureEqual(c.Author, fresh.Author) && + signatureEqual(c.Committer, fresh.Committer) && + c.MergeTag == fresh.MergeTag && + c.Message == fresh.Message && + c.TreeHash == fresh.TreeHash && + c.Encoding == fresh.Encoding && + slices.Equal(c.ParentHashes, fresh.ParentHashes) && + slices.Equal(c.ExtraHeaders, fresh.ExtraHeaders) +} + +func signatureEqual(a, b Signature) bool { + return a.Name == b.Name && + a.Email == b.Email && + a.When.Unix() == b.When.Unix() && + a.When.Format("-0700") == b.When.Format("-0700") +} + +func isStandardHeader(key string) bool { + switch key { + case "tree", "parent", "author", "committer", + headerencoding, headermergetag, headerpgp, headerpgp256: + return true + } + return false +} + func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { o.SetType(plumbing.CommitObject) w, err := o.Writer() @@ -407,7 +412,9 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { } for _, header := range c.ExtraHeaders { - + if isStandardHeader(header.Key) { + continue + } if _, err = fmt.Fprintf(w, "\n%s", header); err != nil { return err } @@ -478,9 +485,21 @@ func (c *Commit) String() string { ) } +// ErrMultipleSignatures is returned by Verify when the commit carries more +// than one armored signature block. Mirrors upstream's parse_gpg_output +// rejection of GOODSIG/BADSIG status lines after the first +// (gpg-interface.c:257-269): multi-signature commits are intentionally +// unsupported because their provenance cannot be reduced to a single +// authoritative signer. +var ErrMultipleSignatures = errors.New("commit has multiple signatures") + // Verify performs PGP verification of the commit with a provided armored // keyring and returns openpgp.Entity associated with verifying key on success. func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) { + if countSignatureBlocks([]byte(c.PGPSignature)) > 1 { + return nil, ErrMultipleSignatures + } + keyRingReader := strings.NewReader(armoredKeyRing) keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) if err != nil { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go new file mode 100644 index 0000000..7e4cf54 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go @@ -0,0 +1,377 @@ +package object + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" + + "github.com/go-git/go-git/v5/plumbing" +) + +// commitScanner holds the working state of the commit decoder driven by the +// stateFn loop in (*Commit).Decode. Each commitState reads one or more lines +// from r, updates the in-progress *Commit and the scanner's bookkeeping, and +// returns the state that should run next (or nil to stop). +type commitScanner struct { + r *bufio.Reader + c *Commit + msgbuf bytes.Buffer + + // pending holds a line that was read but the current state decided to + // hand back to the next state, paired with the io.EOF flag that was + // returned when the line was originally read. + pending []byte + pendingErr error + + // First-occurrence tracking: once the corresponding field has been + // decoded, subsequent occurrences are silently dropped (matches + // upstream's find_commit_header / first-wins semantics). + // + // gpgsig is not tracked here: upstream's parse_buffer_signed_by_header + // (commit.c:1186) accumulates every occurrence into one signature buffer, + // so we do the same on the scanner side to keep verification payloads + // byte-aligned. gpgsig-sha256 is recognized and skipped without exposing a + // new field in v5. + sawTree, sawAuthor, sawCommitter bool + sawEncoding, sawMergetag bool + + // extra is the multi-line ExtraHeader currently being assembled. + extra *ExtraHeader +} + +// commitState is one step of the decoder state machine. Each function reads +// the lines it needs, mutates *Commit via s.c, and returns the next state to +// run (or nil to terminate the loop). +type commitState func(*commitScanner) (commitState, error) + +// readLine returns the next line from the buffer, transparently consuming any +// line that was previously pushed back by a state that decided not to handle +// it. +func (s *commitScanner) readLine() ([]byte, error) { + if s.pending != nil { + line, err := s.pending, s.pendingErr + s.pending, s.pendingErr = nil, nil + return line, err + } + line, err := s.r.ReadBytes('\n') + if err != nil && err != io.EOF { + return line, err + } + return line, err +} + +// pushBack stashes an unconsumed line so the next state's readLine call sees +// it. Only one line can be pushed back at a time. +func (s *commitScanner) pushBack(line []byte, err error) { + s.pending = line + s.pendingErr = err +} + +// scanTree expects the first non-empty header to be `tree HASH`. Anything +// else (or an empty buffer) is rejected with ErrMalformedCommit, matching +// upstream's `bogus commit object` check. +func scanTree(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 || isBlankLine(line) { + return nil, fmt.Errorf("%w: missing tree header", ErrMalformedCommit) + } + + key, data := splitHeader(line) + if key != "tree" { + return nil, fmt.Errorf("%w: tree header must be first", ErrMalformedCommit) + } + h, herr := parseObjectIDHex(data, ErrMalformedCommit, "tree") + if herr != nil { + return nil, herr + } + s.c.TreeHash = h + s.sawTree = true + if err == io.EOF { + return nil, nil + } + return scanParents, nil +} + +// scanParents consumes contiguous `parent HASH` lines. The first non-parent +// line ends the parent block and is handed off to scanAuthor; any later +// `parent` line is silently dropped (matches upstream's parse_commit_buffer +// exiting its parent loop at the first non-parent line and +// read_commit_extra_header_lines filtering `parent` out of extras). +func scanParents(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 { + return nil, nil + } + if isBlankLine(line) { + return scanMessage, nil + } + + key, data := splitHeader(line) + if key == "parent" { + h, herr := parseObjectIDHex(data, ErrMalformedCommit, "parent") + if herr != nil { + return nil, herr + } + s.c.ParentHashes = append(s.c.ParentHashes, h) + if err == io.EOF { + return nil, nil + } + return scanParents, nil + } + s.pushBack(line, err) + return scanAuthor, nil +} + +// scanAuthor accepts an `author` line at its canonical position immediately +// after the parent block. Any other header here is pushed back for +// scanCommitter; an out-of-place author is therefore silently dropped. +// Mirrors upstream's parse_commit_date func. +func scanAuthor(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 { + return nil, nil + } + if isBlankLine(line) { + return scanMessage, nil + } + + key, data := splitHeader(line) + if key == "author" { + s.c.Author.Decode(data) + s.sawAuthor = true + if err == io.EOF { + return nil, nil + } + return scanCommitter, nil + } + s.pushBack(line, err) + return scanCommitter, nil +} + +// scanCommitter accepts a `committer` line at its canonical position +// immediately after the author. Any other header is pushed back for +// scanHeaders. Same upstream rationale as scanAuthor. +func scanCommitter(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 { + return nil, nil + } + if isBlankLine(line) { + return scanMessage, nil + } + + key, data := splitHeader(line) + if key == "committer" { + s.c.Committer.Decode(data) + s.sawCommitter = true + if err == io.EOF { + return nil, nil + } + return scanHeaders, nil + } + s.pushBack(line, err) + return scanHeaders, nil +} + +// scanHeaders dispatches one header line. Continuation-bearing headers +// (mergetag, gpgsig, gpgsig-sha256, and unknown extras whose value is +// continued on subsequent lines) hand off to a dedicated continuation state +// that handles the `...` lines and then returns here. +func scanHeaders(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 { + return nil, nil + } + if isBlankLine(line) { + return scanMessage, nil + } + + originalLine := line + key, data := splitHeader(line) + + var next commitState = scanHeaders + switch key { + case "tree", "parent", "author", "committer": + // Anything reaching scanHeaders with one of these keys is out of + // canonical position: duplicate tree, parent past the contiguous + // block, or author/committer not at their expected slot. Drop them + // the same way upstream's standard_header_field filter excludes + // them from the extras list (read_commit_extra_header_lines, + // commit.c:1520-1522). + case headerencoding: + if !s.sawEncoding { + s.c.Encoding = MessageEncoding(data) + s.sawEncoding = true + } + case headermergetag: + if s.sawMergetag { + next = scanSkipCont + } else { + s.c.MergeTag += string(data) + "\n" + s.sawMergetag = true + next = scanMergetagCont + } + case headerpgp: + s.c.PGPSignature += string(data) + "\n" + next = scanPgpCont + case headerpgp256: + next = scanSkipCont + default: + h, multiline := parseExtraHeader(originalLine) + if multiline { + s.extra = &h + next = scanExtraCont + } else { + s.c.ExtraHeaders = append(s.c.ExtraHeaders, h) + } + } + + if err == io.EOF { + return nil, nil + } + return next, nil +} + +// scanMergetagCont accumulates continuation lines for the first mergetag +// header. Continuations strip exactly one leading space, mirroring upstream's +// `line + 1` (commit.c:1509). The first non-continuation line is pushed back +// so scanHeaders can dispatch it. +func scanMergetagCont(s *commitScanner) (commitState, error) { + return continuationCont(s, &s.c.MergeTag, scanMergetagCont) +} + +// scanPgpCont accumulates continuation lines for a signature header. +// Continuations strip exactly one leading space, mirroring upstream's +// `line + 1` (commit.c:1509). The first non-continuation line is pushed back +// so scanHeaders can dispatch it. Repeat occurrences of the same signature +// header land back here and concatenate, matching upstream's +// parse_buffer_signed_by_header (commit.c:1186). +func scanPgpCont(s *commitScanner) (commitState, error) { + return continuationCont(s, &s.c.PGPSignature, scanPgpCont) +} + +func continuationCont(s *commitScanner, dst *string, self commitState) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) > 0 && line[0] == ' ' { + *dst += string(line[1:]) + if err == io.EOF { + return nil, nil + } + return self, nil + } + if len(line) > 0 { + s.pushBack(line, err) + } + return scanHeaders, nil +} + +// scanSkipCont discards continuation lines that belong to a header scanHeaders +// chose to drop. +func scanSkipCont(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) > 0 && line[0] == ' ' { + if err == io.EOF { + return nil, nil + } + return scanSkipCont, nil + } + if len(line) > 0 { + s.pushBack(line, err) + } + return scanHeaders, nil +} + +// scanExtraCont accumulates continuation lines for an unknown ExtraHeader +// whose value spans multiple lines, then finalises the entry once the +// continuation block ends. +func scanExtraCont(s *commitScanner) (commitState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) > 0 && line[0] == ' ' { + s.extra.Value += string(line[1:]) + if err == io.EOF { + s.finaliseExtra() + return nil, nil + } + return scanExtraCont, nil + } + s.finaliseExtra() + if len(line) > 0 { + s.pushBack(line, err) + } + return scanHeaders, nil +} + +func (s *commitScanner) finaliseExtra() { + s.extra.Value = strings.TrimRight(s.extra.Value, "\n") + s.c.ExtraHeaders = append(s.c.ExtraHeaders, *s.extra) + s.extra = nil +} + +// scanMessage drains the remaining bytes into the message buffer. +func scanMessage(s *commitScanner) (commitState, error) { + for { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) > 0 { + s.msgbuf.Write(line) + } + if err == io.EOF { + return nil, nil + } + } +} + +// isBlankLine reports whether line is the canonical header/body separator: +// a single newline. Mirrors upstream's `*line == '\n'` test in +// read_commit_extra_header_lines (commit.c:1502). +func isBlankLine(line []byte) bool { + return len(line) == 1 && line[0] == '\n' +} + +// splitHeader returns the header keyword (everything before the first space) +// and the value (everything after, with the trailing newline stripped). If +// the header has no value the returned data is nil. +func splitHeader(line []byte) (string, []byte) { + trimmed := bytes.TrimRight(line, "\n") + key, value, ok := bytes.Cut(trimmed, []byte{' '}) + if !ok { + return string(trimmed), nil + } + return string(key), value +} + +func parseObjectIDHex(data []byte, malformedErr error, header string) (plumbing.Hash, error) { + id := string(data) + if !plumbing.IsHash(id) { + return plumbing.ZeroHash, fmt.Errorf("%w: bad %s hash", malformedErr, header) + } + return plumbing.NewHash(id), nil +} diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go index f9c3d30..3346e4f 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go @@ -1,6 +1,13 @@ package object -import "bytes" +import ( + "bytes" + "io" + + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/utils/ioutil" + "github.com/go-git/go-git/v5/utils/sync" +) const ( signatureTypeUnknown signatureType = iota @@ -100,3 +107,116 @@ func parseSignedBytes(b []byte) (int, signatureType) { } return match, t } + +// countSignatureBlocks reports how many distinct armored signature blocks +// start at a line boundary in b. Used by verification paths to reject +// multi-signature payloads, matching upstream's check in gpg-interface.c +// where parse_gpg_output bails out the first time it sees a second +// exclusive status line (a second GOODSIG/BADSIG/etc.). +func countSignatureBlocks(b []byte) int { + n, count := 0, 0 + for n < len(b) { + i := b[n:] + if typeForSignature(i) != signatureTypeUnknown { + count++ + } + if eol := bytes.IndexByte(i, '\n'); eol >= 0 { + n += eol + 1 + continue + } + break + } + return count +} + +// isSignatureHeader reports whether line is a canonical "gpgsig "/ +// "gpgsig-sha256 " header line. Other "gpgsig"-prefixed extra headers +// are intentionally not matched. +func isSignatureHeader(line []byte) bool { + return bytes.HasPrefix(line, []byte(headerpgp+" ")) || + bytes.HasPrefix(line, []byte(headerpgp256+" ")) +} + +// stripObjectSignatures streams src into dst, producing the byte sequence +// over which a PGP/GPG signature is computed: +// +// - Canonical "gpgsig" and "gpgsig-sha256" headers (and their +// continuation lines) are dropped, mirroring upstream's +// remove_signature in commit.c. +// - For tag objects, the inline trailing PGP signature is additionally +// truncated, mirroring upstream's parse_signature in gpg-interface.c +// used by gpg_verify_tag. +// +// The returned object's type is set to objType. Used by both +// Commit.EncodeWithoutSignature and Tag.EncodeWithoutSignature to +// reproduce the exact bytes the signature was computed over. +func stripObjectSignatures(dst, src plumbing.EncodedObject, objType plumbing.ObjectType) (err error) { + dst.SetType(objType) + + r, err := src.Reader() + if err != nil { + return err + } + defer ioutil.CheckClose(r, &err) + + var input io.Reader = r + if objType == plumbing.TagObject { + raw, err := io.ReadAll(r) + if err != nil { + return err + } + if sm, _ := parseSignedBytes(raw); sm >= 0 { + raw = raw[:sm] + } + input = bytes.NewReader(raw) + } + + w, err := dst.Writer() + if err != nil { + return err + } + defer ioutil.CheckClose(w, &err) + + return stripHeaderSignatures(w, input) +} + +// stripHeaderSignatures copies r to w, dropping canonical signature header +// lines (gpgsig and gpgsig-sha256) and their continuation lines. Lines +// past the blank line that closes the header block are copied verbatim. +func stripHeaderSignatures(w io.Writer, r io.Reader) error { + br := sync.GetBufioReader(r) + defer sync.PutBufioReader(br) + + var inBody, skipping bool + for { + line, rerr := br.ReadBytes('\n') + if rerr != nil && rerr != io.EOF { + return rerr + } + + write := true + if !inBody { + switch { + case skipping && len(line) > 0 && line[0] == ' ': + write = false + case isSignatureHeader(line): + skipping = true + write = false + case len(line) == 1 && line[0] == '\n': + skipping = false + inBody = true + default: + skipping = false + } + } + + if write && len(line) > 0 { + if _, werr := w.Write(line); werr != nil { + return werr + } + } + if rerr == io.EOF { + return nil + } + } +} diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go index cf46c08..93e56a4 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go @@ -1,9 +1,8 @@ package object import ( - "bytes" + "errors" "fmt" - "io" "strings" "github.com/ProtonMail/go-crypto/openpgp" @@ -13,6 +12,10 @@ import ( "github.com/go-git/go-git/v5/utils/sync" ) +// ErrMalformedTag is returned when a tag object cannot be decoded because +// its required headers (object, type, tag) are missing or out of order. +var ErrMalformedTag = errors.New("malformed tag") + // Tag represents an annotated tag object. It points to a single git object of // any type, but tags typically are applied to commit or blob objects. It // provides a reference that associates the target with a tag name. It also @@ -39,6 +42,9 @@ type Tag struct { Target plumbing.Hash s storer.EncodedObjectStorer + // src holds the encoded object this Tag was decoded from, used by + // EncodeWithoutSignature to recover the canonical signed bytes. + src plumbing.EncodedObject } // GetTag gets a tag from an object storer and decodes it. @@ -77,13 +83,20 @@ func (t *Tag) Type() plumbing.ObjectType { return plumbing.TagObject } +func (t *Tag) reset() { + storer := t.s + *t = Tag{s: storer} +} + // Decode transforms a plumbing.EncodedObject into a Tag struct. func (t *Tag) Decode(o plumbing.EncodedObject) (err error) { if o.Type() != plumbing.TagObject { return ErrUnsupportedObject } + t.reset() t.Hash = o.Hash() + t.src = o reader, err := o.Reader() if err != nil { @@ -94,42 +107,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) { r := sync.GetBufioReader(reader) defer sync.PutBufioReader(r) - for { - var line []byte - line, err = r.ReadBytes('\n') - if err != nil && err != io.EOF { + scanner := &tagScanner{r: r, t: t} + for state := scanTagObject; state != nil; { + state, err = state(scanner) + if err != nil { return err } - - line = bytes.TrimSpace(line) - if len(line) == 0 { - break // Start of message - } - - split := bytes.SplitN(line, []byte{' '}, 2) - switch string(split[0]) { - case "object": - t.Target = plumbing.NewHash(string(split[1])) - case "type": - t.TargetType, err = plumbing.ParseObjectType(string(split[1])) - if err != nil { - return err - } - case "tag": - t.Name = string(split[1]) - case "tagger": - t.Tagger.Decode(split[1]) - } - - if err == io.EOF { - return nil - } } - data, err := io.ReadAll(r) - if err != nil { - return err - } + data := scanner.msgbuf.Bytes() if sm, _ := parseSignedBytes(data); sm >= 0 { t.PGPSignature = string(data[sm:]) data = data[:sm] @@ -144,11 +130,54 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error { return t.encode(o, true) } -// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature). +// EncodeWithoutSignature exports a Tag into a plumbing.EncodedObject without +// any signature data, producing the payload that PGP/GPG signatures are +// computed over. +// +// Behaviour mirrors Commit.EncodeWithoutSignature: +// +// - For Tags populated by Decode whose exported fields still match the +// source object, the payload is streamed from the raw source bytes with +// the inline trailing signature truncated and gpgsig/gpgsig-sha256 +// headers (and their continuation lines) stripped verbatim. This +// preserves the exact bytes the signature was computed over, regardless +// of any normalization performed by Decode. +// +// - For Tags constructed in memory, or for decoded Tags whose exported +// fields have been mutated, the payload is derived from the current +// struct fields. Mutation is detected by re-decoding the source object +// and comparing exported fields; if any differ, the in-memory +// representation prevails. func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error { + if t.matchesSource() { + return stripObjectSignatures(o, t.src, plumbing.TagObject) + } return t.encode(o, false) } +// matchesSource reports whether t.src is set and re-decoding it produces a +// Tag whose payload-affecting exported fields are identical to those of t. +// +// PGPSignature is intentionally excluded from the comparison: neither path +// emits it as part of the verification payload, so mutating it must not +// trigger a switch to struct-encode (which would change the byte layout the +// caller is trying to verify against). +func (t *Tag) matchesSource() bool { + if t.src == nil { + return false + } + fresh := &Tag{} + if err := fresh.Decode(t.src); err != nil { + return false + } + return t.Hash == fresh.Hash && + t.Name == fresh.Name && + signatureEqual(t.Tagger, fresh.Tagger) && + t.Message == fresh.Message && + t.TargetType == fresh.TargetType && + t.Target == fresh.Target +} + func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { o.SetType(plumbing.TagObject) w, err := o.Writer() @@ -158,16 +187,26 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { defer ioutil.CheckClose(w, &err) if _, err = fmt.Fprintf(w, - "object %s\ntype %s\ntag %s\ntagger ", + "object %s\ntype %s\ntag %s\n", t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil { return err } - if err = t.Tagger.Encode(w); err != nil { - return err + if !isZeroSignature(t.Tagger) { + if _, err = fmt.Fprint(w, "tagger "); err != nil { + return err + } + + if err = t.Tagger.Encode(w); err != nil { + return err + } + + if _, err = fmt.Fprint(w, "\n"); err != nil { + return err + } } - if _, err = fmt.Fprint(w, "\n\n"); err != nil { + if _, err = fmt.Fprint(w, "\n"); err != nil { return err } @@ -175,11 +214,12 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { return err } - // Note that this is highly sensitive to what it sent along in the message. - // Message *always* needs to end with a newline, or else the message and the - // signature will be concatenated into a corrupt object. Since this is a - // lower-level method, we assume you know what you are doing and have already - // done the needful on the message in the caller. + // Note that this is highly sensitive to what is sent along in the + // message. Message *always* needs to end with a newline, or else the + // message and the trailing signature will be concatenated into a + // corrupt object. Since this is a lower-level method, we assume you + // know what you are doing and have already done the needful on the + // message in the caller. if includeSig { if _, err = fmt.Fprint(w, t.PGPSignature); err != nil { return err @@ -189,6 +229,10 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) { return err } +func isZeroSignature(s Signature) bool { + return s.Name == "" && s.Email == "" && s.When.IsZero() +} + // Commit returns the commit pointed to by the tag. If the tag points to a // different type of object ErrUnsupportedObject will be returned. func (t *Tag) Commit() (*Commit, error) { @@ -256,7 +300,8 @@ func (t *Tag) String() string { } // Verify performs PGP verification of the tag with a provided armored -// keyring and returns openpgp.Entity associated with verifying key on success. +// keyring and returns openpgp.Entity associated with verifying key on +// success. func (t *Tag) Verify(armoredKeyRing string) (*openpgp.Entity, error) { keyRingReader := strings.NewReader(armoredKeyRing) keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go new file mode 100644 index 0000000..2bfb3a1 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go @@ -0,0 +1,237 @@ +package object + +import ( + "bufio" + "bytes" + "fmt" + "io" + + "github.com/go-git/go-git/v5/plumbing" +) + +// tagScanner holds the working state of the tag decoder driven by the +// stateFn loop in (*Tag).Decode. Each tagState reads one or more lines +// from r, updates the in-progress *Tag and the scanner's bookkeeping, +// and returns the state that should run next (or nil to stop). +type tagScanner struct { + r *bufio.Reader + t *Tag + msgbuf bytes.Buffer + + // pending holds a line that was read but the current state decided to + // hand back to the next state, paired with the io.EOF flag returned + // when the line was originally read. + pending []byte + pendingErr error + + // First-occurrence tracking: once the corresponding canonical + // header has been decoded at its expected position, subsequent + // occurrences (or out-of-position lines) are silently dropped, + // matching the strict layout enforced by upstream's + // parse_tag_buffer (tag.c:130). + // + // gpgsig-sha256 is recognized and skipped without exposing a new field + // in v5. + sawObject, sawType, sawName, sawTagger bool +} + +// tagState is one step of the decoder state machine. Each function reads +// the lines it needs, mutates *Tag via s.t, and returns the next state +// to run (or nil to terminate the loop). +type tagState func(*tagScanner) (tagState, error) + +// readLine returns the next line from the buffer, transparently +// consuming any line that was previously pushed back by a state that +// decided not to handle it. +func (s *tagScanner) readLine() ([]byte, error) { + if s.pending != nil { + line, err := s.pending, s.pendingErr + s.pending, s.pendingErr = nil, nil + return line, err + } + return s.r.ReadBytes('\n') +} + +// pushBack stashes an unconsumed line so the next state's readLine call +// sees it. Only one line can be pushed back at a time. +func (s *tagScanner) pushBack(line []byte, err error) { + s.pending = line + s.pendingErr = err +} + +// scanTagObject requires the first line to be `object HASH`, mirroring +// upstream's strict parse_tag_buffer (tag.c:151-156). Anything else +// returns ErrMalformedTag. +func scanTagObject(s *tagScanner) (tagState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 || isBlankLine(line) { + return nil, fmt.Errorf("%w: missing object header", ErrMalformedTag) + } + + key, data := splitHeader(line) + if key != "object" { + return nil, fmt.Errorf("%w: object header must be first", ErrMalformedTag) + } + h, herr := parseObjectIDHex(data, ErrMalformedTag, "object") + if herr != nil { + return nil, herr + } + s.t.Target = h + s.sawObject = true + if err == io.EOF { + return nil, nil + } + return scanTagType, nil +} + +// scanTagType requires a `type` line immediately after the object header, +// mirroring upstream's parse_tag_buffer (tag.c:158-166). +func scanTagType(s *tagScanner) (tagState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 || isBlankLine(line) { + return nil, fmt.Errorf("%w: missing type header", ErrMalformedTag) + } + + key, data := splitHeader(line) + if key != "type" { + return nil, fmt.Errorf("%w: type header must follow object", ErrMalformedTag) + } + ot, perr := plumbing.ParseObjectType(string(data)) + if perr != nil { + return nil, perr + } + s.t.TargetType = ot + s.sawType = true + if err == io.EOF { + return nil, nil + } + return scanTagName, nil +} + +// scanTagName requires a `tag` line immediately after the type header, +// mirroring upstream's parse_tag_buffer (tag.c:186-194). +func scanTagName(s *tagScanner) (tagState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 || isBlankLine(line) { + return nil, fmt.Errorf("%w: missing tag header", ErrMalformedTag) + } + + key, data := splitHeader(line) + if key != "tag" { + return nil, fmt.Errorf("%w: tag header must follow type", ErrMalformedTag) + } + s.t.Name = string(data) + s.sawName = true + if err == io.EOF { + return nil, nil + } + return scanTagTagger, nil +} + +// scanTagTagger accepts a `tagger` line at its canonical position. Any +// other header is pushed back for scanTagHeaders. +func scanTagTagger(s *tagScanner) (tagState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 { + return nil, nil + } + if isBlankLine(line) { + return scanTagMessage, nil + } + + key, data := splitHeader(line) + if key == "tagger" { + s.t.Tagger.Decode(data) + s.sawTagger = true + if err == io.EOF { + return nil, nil + } + return scanTagHeaders, nil + } + s.pushBack(line, err) + return scanTagHeaders, nil +} + +// scanTagHeaders dispatches one header line. gpgsig-sha256 hands off to +// scanTagSkipCont so the continuation block can be consumed; out-of-position +// canonical fields and unknown headers are silently dropped. +func scanTagHeaders(s *tagScanner) (tagState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) == 0 { + return nil, nil + } + if isBlankLine(line) { + return scanTagMessage, nil + } + + key, _ := splitHeader(line) + next := scanTagHeaders + switch key { + case "object", "type", "tag", "tagger": + // Out-of-canonical-position duplicates are dropped, mirroring the + // strict ordering of upstream's parse_tag_buffer. + case headerpgp256: + next = scanTagSkipCont + default: + // Unknown header: silently dropped (the Tag struct does not + // expose ExtraHeaders). + } + + if err == io.EOF { + return nil, nil + } + return next, nil +} + +// scanTagSkipCont discards continuation lines for a header scanTagHeaders chose +// to drop. The first non-continuation line is pushed back so scanTagHeaders can +// dispatch it. +func scanTagSkipCont(s *tagScanner) (tagState, error) { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) > 0 && line[0] == ' ' { + if err == io.EOF { + return nil, nil + } + return scanTagSkipCont, nil + } + if len(line) > 0 { + s.pushBack(line, err) + } + return scanTagHeaders, nil +} + +// scanTagMessage drains the remaining bytes into the message buffer. +// (*Tag).Decode then runs parseSignedBytes over those bytes to peel off +// the optional inline trailing PGP signature. +func scanTagMessage(s *tagScanner) (tagState, error) { + for { + line, err := s.readLine() + if err != nil && err != io.EOF { + return nil, err + } + if len(line) > 0 { + s.msgbuf.Write(line) + } + if err == io.EOF { + return nil, nil + } + } +} diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go b/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go index 2e1b789..3c004f5 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go @@ -10,6 +10,7 @@ import ( "sort" "strings" + "github.com/go-git/go-git/v5/internal/pathutil" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/storer" @@ -29,6 +30,7 @@ var ( ErrDirectoryNotFound = errors.New("directory not found") ErrEntryNotFound = errors.New("entry not found") ErrEntriesNotSorted = errors.New("entries in tree are not sorted") + ErrMalformedTree = errors.New("malformed tree") ) // Tree is basically like a directory - it references a bunch of other trees @@ -37,9 +39,9 @@ type Tree struct { Entries []TreeEntry Hash plumbing.Hash - s storer.EncodedObjectStorer - m map[string]*TreeEntry - t map[string]*Tree // tree path cache + s storer.EncodedObjectStorer + t map[string]*Tree // tree path cache + entriesSorted bool } // GetTree gets a tree from an object storer and decodes it. @@ -117,7 +119,16 @@ func (t *Tree) Tree(path string) (*Tree, error) { } // TreeEntryFile returns the *File for a given *TreeEntry. +// +// The entry's name is validated against pathutil.ValidTreePath for +// the same reason FindEntry validates: TreeEntryFile is a boundary +// where attacker-controlled tree data leaves the trusted store as a +// *File whose Name a caller can hand to filesystem ops. func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) { + if err := pathutil.ValidTreePath(e.Name); err != nil { + return nil, err + } + blob, err := GetBlob(t.s, e.Hash) if err != nil { return nil, err @@ -127,7 +138,16 @@ func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) { } // FindEntry search a TreeEntry in this tree or any subtree. +// +// The lookup path is validated against pathutil.ValidTreePath to +// prevent attacker-controlled tree contents from leaking past this +// boundary as `.git`-shaped or path-traversal-shaped names. Callers +// that legitimately need to look up unsafe paths should walk the +// tree manually. func (t *Tree) FindEntry(path string) (*TreeEntry, error) { + if err := pathutil.ValidTreePath(path); err != nil { + return nil, err + } if t.t == nil { t.t = make(map[string]*Tree) } @@ -182,16 +202,43 @@ func (t *Tree) dir(baseName string) (*Tree, error) { } func (t *Tree) entry(baseName string) (*TreeEntry, error) { - if t.m == nil { - t.buildMap() - } - - entry, ok := t.m[baseName] - if !ok { + if t.entriesSorted { + if entry := t.searchEntry(baseName); entry != nil { + return entry, nil + } return nil, ErrEntryNotFound } - return entry, nil + pastName := baseName + "/" + for i := range t.Entries { + entry := &t.Entries[i] + if entry.Name == baseName { + return entry, nil + } + if treeEntrySortName(entry) > pastName { + break + } + } + + return nil, ErrEntryNotFound +} + +func (t *Tree) searchEntry(baseName string) *TreeEntry { + if i := t.searchEntryIndex(baseName); i < len(t.Entries) && t.Entries[i].Name == baseName { + return &t.Entries[i] + } + + if i := t.searchEntryIndex(baseName + "/"); i < len(t.Entries) && t.Entries[i].Name == baseName { + return &t.Entries[i] + } + + return nil +} + +func (t *Tree) searchEntryIndex(name string) int { + return sort.Search(len(t.Entries), func(i int) bool { + return treeEntrySortName(&t.Entries[i]) >= name + }) } // Files returns a FileIter allowing to iterate over the Tree @@ -212,20 +259,25 @@ func (t *Tree) Type() plumbing.ObjectType { return plumbing.TreeObject } +func (t *Tree) reset() { + storer := t.s + *t = Tree{s: storer} +} + // Decode transform an plumbing.EncodedObject into a Tree struct func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { if o.Type() != plumbing.TreeObject { return ErrUnsupportedObject } + t.reset() t.Hash = o.Hash() + // assume tree is sorted as a valid tree should always be sorted. + t.entriesSorted = true if o.Size() == 0 { return nil } - t.Entries = nil - t.m = nil - reader, err := o.Reader() if err != nil { return err @@ -235,10 +287,14 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { r := sync.GetBufioReader(reader) defer sync.PutBufioReader(r) + var prevSortName string for { str, err := r.ReadString(' ') if err != nil { if err == io.EOF { + if len(str) != 0 { + return fmt.Errorf("%w: missing mode terminator", ErrMalformedTree) + } break } @@ -248,25 +304,41 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { mode, err := filemode.New(str) if err != nil { - return err + return fmt.Errorf("%w: malformed mode", ErrMalformedTree) } + mode = canonicalTreeMode(mode) name, err := r.ReadString(0) - if err != nil && err != io.EOF { + if err != nil { + if err == io.EOF { + return fmt.Errorf("%w: missing filename terminator", ErrMalformedTree) + } return err } + if len(name) == 1 { + return fmt.Errorf("%w: empty filename", ErrMalformedTree) + } var hash plumbing.Hash if _, err = io.ReadFull(r, hash[:]); err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return fmt.Errorf("%w: truncated object id", ErrMalformedTree) + } return err } baseName := name[:len(name)-1] - t.Entries = append(t.Entries, TreeEntry{ + entry := TreeEntry{ Hash: hash, Mode: mode, Name: baseName, - }) + } + sortName := treeEntrySortName(&entry) + if len(t.Entries) != 0 && prevSortName > sortName { + t.entriesSorted = false + } + prevSortName = sortName + t.Entries = append(t.Entries, entry) } return nil @@ -279,21 +351,37 @@ func (s TreeEntrySorter) Len() int { } func (s TreeEntrySorter) Less(i, j int) bool { - name1 := s[i].Name - name2 := s[j].Name - if s[i].Mode == filemode.Dir { - name1 += "/" - } - if s[j].Mode == filemode.Dir { - name2 += "/" - } - return name1 < name2 + return treeEntrySortName(&s[i]) < treeEntrySortName(&s[j]) } func (s TreeEntrySorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +// Git compares tree entries as if directory names had a trailing slash. +func treeEntrySortName(e *TreeEntry) string { + if e.Mode == filemode.Dir { + return e.Name + "/" + } + return e.Name +} + +func canonicalTreeMode(mode filemode.FileMode) filemode.FileMode { + switch mode & 0o170000 { + case 0o040000: + return filemode.Dir + case 0o100000: + if mode&0o111 != 0 { + return filemode.Executable + } + return filemode.Regular + case 0o120000: + return filemode.Symlink + default: + return filemode.Submodule + } +} + // Encode transforms a Tree into a plumbing.EncodedObject. // The tree entries must be sorted by name. func (t *Tree) Encode(o plumbing.EncodedObject) (err error) { @@ -329,13 +417,6 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) { return err } -func (t *Tree) buildMap() { - t.m = make(map[string]*TreeEntry) - for i := 0; i < len(t.Entries); i++ { - t.m[t.Entries[i].Name] = &t.Entries[i] - } -} - // Diff returns a list of changes between this tree and the provided one func (t *Tree) Diff(to *Tree) (Changes, error) { return t.DiffContext(context.Background(), to) @@ -455,6 +536,10 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) { continue } + if err := pathutil.ValidTreePath(entry.Name); err != nil { + return name, entry, err + } + if entry.Mode == filemode.Dir { obj, err = GetTree(w.s, entry.Hash) } diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go index 5dd2e31..83f93f1 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go @@ -7,7 +7,6 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "net" "net/http" "net/url" "reflect" @@ -24,6 +23,33 @@ import ( "github.com/go-git/go-git/v5/utils/ioutil" ) +type contextKey int + +const initialRequestKey contextKey = iota + +// RedirectPolicy controls how the HTTP transport follows redirects. +// +// The values mirror Git's http.followRedirects config: +// "true" follows redirects for all requests, "false" treats redirects as +// errors, and "initial" follows redirects only for the initial +// /info/refs discovery request. The zero value defaults to "initial". +type RedirectPolicy string + +const ( + FollowInitialRedirects RedirectPolicy = "initial" + FollowRedirects RedirectPolicy = "true" + NoFollowRedirects RedirectPolicy = "false" +) + +func withInitialRequest(ctx context.Context) context.Context { + return context.WithValue(ctx, initialRequestKey, true) +} + +func isInitialRequest(req *http.Request) bool { + v, _ := req.Context().Value(initialRequestKey).(bool) + return v +} + // it requires a bytes.Buffer, because we need to know the length func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) { req.Header.Add("User-Agent", capability.DefaultAgent()) @@ -54,12 +80,15 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) ( s.ApplyAuthToRequest(req) applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName) - res, err := s.client.Do(req.WithContext(ctx)) + res, err := s.client.Do(req.WithContext(withInitialRequest(ctx))) if err != nil { return nil, err } - s.ModifyEndpointIfRedirect(res) + if err := s.ModifyEndpointIfRedirect(res); err != nil { + _ = res.Body.Close() + return nil, err + } defer ioutil.CheckClose(res.Body, &err) if err = NewErr(res); err != nil { @@ -96,6 +125,7 @@ type client struct { client *http.Client transports *lru.Cache mutex sync.RWMutex + follow RedirectPolicy } // ClientOptions holds user configurable options for the client. @@ -106,6 +136,11 @@ type ClientOptions struct { // size, will result in the least recently used transport getting deleted // before the provided transport is added to the cache. CacheMaxEntries int + + // RedirectPolicy controls redirect handling. Supported values are + // "true", "false", and "initial". The zero value defaults to + // "initial", matching Git's http.followRedirects default. + RedirectPolicy RedirectPolicy } var ( @@ -150,12 +185,16 @@ func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transpo } cl := &client{ client: c, + follow: FollowInitialRedirects, } if opts != nil { if opts.CacheMaxEntries > 0 { cl.transports = lru.New(opts.CacheMaxEntries) } + if opts.RedirectPolicy != "" { + cl.follow = opts.RedirectPolicy + } } return cl } @@ -289,14 +328,9 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* } } - httpClient = &http.Client{ - Transport: transport, - CheckRedirect: c.client.CheckRedirect, - Jar: c.client.Jar, - Timeout: c.client.Timeout, - } + httpClient = c.cloneHTTPClient(transport) } else { - httpClient = c.client + httpClient = c.cloneHTTPClient(c.client.Transport) } s := &session{ @@ -324,30 +358,122 @@ func (s *session) ApplyAuthToRequest(req *http.Request) { s.auth.SetAuth(req) } -func (s *session) ModifyEndpointIfRedirect(res *http.Response) { +func (s *session) ModifyEndpointIfRedirect(res *http.Response) error { if res.Request == nil { - return + return nil + } + if s.endpoint == nil { + return fmt.Errorf("http redirect: nil endpoint") } r := res.Request if !strings.HasSuffix(r.URL.Path, infoRefsPath) { - return + return fmt.Errorf("http redirect: target %q does not end with %s", r.URL.Path, infoRefsPath) + } + if r.URL.Scheme != "http" && r.URL.Scheme != "https" { + return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme) + } + if r.URL.Scheme != s.endpoint.Protocol && + !(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") { + return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme) } - h, p, err := net.SplitHostPort(r.URL.Host) + host := endpointHost(r.URL.Hostname()) + port, err := endpointPort(r.URL.Port()) if err != nil { - h = r.URL.Host + return err } - if p != "" { - port, err := strconv.Atoi(p) - if err == nil { - s.endpoint.Port = port - } + + if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) { + s.endpoint.User = "" + s.endpoint.Password = "" + s.auth = nil } - s.endpoint.Host = h + + s.endpoint.Host = host + s.endpoint.Port = port s.endpoint.Protocol = r.URL.Scheme s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)] + return nil +} + +func endpointHost(host string) string { + if strings.Contains(host, ":") { + return "[" + host + "]" + } + + return host +} + +func endpointPort(port string) (int, error) { + if port == "" { + return 0, nil + } + + parsed, err := strconv.Atoi(port) + if err != nil { + return 0, fmt.Errorf("http redirect: invalid port %q", port) + } + + return parsed, nil +} + +func effectivePort(scheme string, port int) int { + if port != 0 { + return port + } + + switch strings.ToLower(scheme) { + case "http": + return 80 + case "https": + return 443 + default: + return 0 + } +} + +func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client { + return &http.Client{ + Transport: transport, + CheckRedirect: wrapCheckRedirect(c.follow, c.client.CheckRedirect), + Jar: c.client.Jar, + Timeout: c.client.Timeout, + } +} + +func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.Request) error) func(*http.Request, []*http.Request) error { + return func(req *http.Request, via []*http.Request) error { + if err := checkRedirect(req, via, policy); err != nil { + return err + } + if next != nil { + return next(req, via) + } + return nil + } +} + +func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error { + switch policy { + case FollowRedirects: + case NoFollowRedirects: + return fmt.Errorf("http redirect: redirects disabled to %s", req.URL) + case "", FollowInitialRedirects: + if !isInitialRequest(req) { + return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL) + } + default: + return fmt.Errorf("http redirect: invalid redirect policy %q", policy) + } + if req.URL.Scheme != "http" && req.URL.Scheme != "https" { + return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme) + } + if len(via) >= 10 { + return fmt.Errorf("http redirect: too many redirects") + } + return nil } func (*session) Close() error { diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go b/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go index ae6f217..647955b 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go @@ -252,7 +252,39 @@ func (c *command) setAuthFromEndpoint() error { } func endpointToCommand(cmd string, ep *transport.Endpoint) string { - return fmt.Sprintf("%s '%s'", cmd, ep.Path) + var b strings.Builder + b.WriteString(cmd) + b.WriteByte(' ') + writeShellQuote(&b, ep.Path) + return b.String() +} + +// writeShellQuote writes s to b, wrapped in single quotes with +// embedded single quotes and exclamation marks escaped using the +// POSIX close-escape-reopen idiom: +// +// ' becomes '\'' +// ! becomes '\!' +// +// It is a direct port of canonical Git's sq_quote_buf (quote.c). +// The bang escape keeps the result safe when re-evaluated under +// csh-derived shells that perform history expansion. The output is +// safe to pass as a single argument through any POSIX shell and +// round-trips through git-shell's sq_dequote_to_argv. +func writeShellQuote(b *strings.Builder, s string) { + b.Grow(len(s) + 2) + b.WriteByte('\'') + for i := 0; i < len(s); i++ { + c := s[i] + if c == '\'' || c == '!' { + b.WriteString(`'\`) + b.WriteByte(c) + b.WriteByte('\'') + continue + } + b.WriteByte(c) + } + b.WriteByte('\'') } func overrideConfig(overrides *ssh.ClientConfig, c *ssh.ClientConfig) { diff --git a/vendor/github.com/go-git/go-git/v5/repository.go b/vendor/github.com/go-git/go-git/v5/repository.go index 4015905..12af162 100644 --- a/vendor/github.com/go-git/go-git/v5/repository.go +++ b/vendor/github.com/go-git/go-git/v5/repository.go @@ -208,6 +208,12 @@ func Open(s storage.Storer, worktree billy.Filesystem) (*Repository, error) { return nil, ErrRepositoryNotExists } + cfg, err := s.Config() + if err != nil { + return nil, err + } + + err = verifyExtensions(s, cfg) if err != nil { return nil, err } @@ -1524,7 +1530,18 @@ func (r *Repository) Worktree() (*Worktree, error) { return nil, ErrIsBareRepository } - return &Worktree{r: r, Filesystem: r.wt}, nil + protectNTFS := defaultProtectNTFS() + protectHFS := defaultProtectHFS() + if cfg, err := r.Config(); err == nil { + if cfg.Core.ProtectNTFS.IsSet() { + protectNTFS = cfg.Core.ProtectNTFS.IsTrue() + } + if cfg.Core.ProtectHFS.IsSet() { + protectHFS = cfg.Core.ProtectHFS.IsTrue() + } + } + + return &Worktree{r: r, Filesystem: newWorktreeFilesystem(r.wt, protectNTFS, protectHFS)}, nil } func expand_ref(s storer.ReferenceStorer, ref plumbing.ReferenceName) (*plumbing.Reference, error) { diff --git a/vendor/github.com/go-git/go-git/v5/repository_extensions.go b/vendor/github.com/go-git/go-git/v5/repository_extensions.go new file mode 100644 index 0000000..635d9aa --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/repository_extensions.go @@ -0,0 +1,121 @@ +package git + +import ( + "errors" + "fmt" + "strings" + + "github.com/go-git/go-git/v5/config" + cfgformat "github.com/go-git/go-git/v5/plumbing/format/config" + "github.com/go-git/go-git/v5/storage" +) + +var ( + // ErrUnsupportedExtensionRepositoryFormatVersion represents when an + // extension being used is not compatible with the repository's + // core.repositoryFormatVersion. + ErrUnsupportedExtensionRepositoryFormatVersion = errors.New("core.repositoryformatversion does not support extension") + + // ErrUnsupportedRepositoryFormatVersion represents when an repository + // is using a format version that is not supported. + ErrUnsupportedRepositoryFormatVersion = errors.New("core.repositoryformatversion not supported") + + // ErrUnknownExtension represents when a repository has an extension + // which is unknown or unsupported by go-git. + ErrUnknownExtension = errors.New("unknown extension") + + // builtinExtensions defines the Git extensions that are supported by + // the core go-git implementation. + // + // Some extensions are storage-specific, those are defined by the Storers + // themselves by implementing the ExtensionChecker interface. + builtinExtensions = map[string]struct{}{ + // noop does not change git’s behavior at all. + // It is useful only for testing format-1 compatibility. + // + // This extension is respected regardless of the + // core.repositoryFormatVersion setting. + "noop": {}, + + // noop-v1 does not change git’s behavior at all. + // It is useful only for testing format-1 compatibility. + "noop-v1": {}, + } + + // Some Git extensions were supported upstream before the introduction + // of repositoryformatversion. These are the only extensions that can be + // enabled while core.repositoryformatversion is unset or set to 0. + extensionsValidForV0 = map[string]struct{}{ + "noop": {}, + "partialClone": {}, + "preciousObjects": {}, + "worktreeConfig": {}, + } +) + +type extension struct { + name string + value string +} + +func extensions(cfg *config.Config) []extension { + if cfg == nil || cfg.Raw == nil { + return nil + } + + if !cfg.Raw.HasSection("extensions") { + return nil + } + + section := cfg.Raw.Section("extensions") + out := make([]extension, 0, len(section.Options)) + for _, opt := range section.Options { + out = append(out, extension{name: strings.ToLower(opt.Key), value: strings.ToLower(opt.Value)}) + } + + return out +} + +func verifyExtensions(st storage.Storer, cfg *config.Config) error { + needed := extensions(cfg) + + switch cfg.Core.RepositoryFormatVersion { + case "", cfgformat.Version_0, cfgformat.Version_1: + default: + return fmt.Errorf("%w: %q", + ErrUnsupportedRepositoryFormatVersion, + cfg.Core.RepositoryFormatVersion) + } + + if len(needed) > 0 { + if cfg.Core.RepositoryFormatVersion == cfgformat.Version_0 || + cfg.Core.RepositoryFormatVersion == "" { + var unsupported []string + for _, ext := range needed { + if _, ok := extensionsValidForV0[ext.name]; !ok { + unsupported = append(unsupported, ext.name) + } + } + if len(unsupported) > 0 { + return fmt.Errorf("%w: %s", + ErrUnsupportedExtensionRepositoryFormatVersion, + strings.Join(unsupported, ", ")) + } + } + + var missing []string + for _, ext := range needed { + if _, ok := builtinExtensions[ext.name]; ok { + continue + } + + missing = append(missing, ext.name) + } + + if len(missing) > 0 { + return fmt.Errorf("%w: %s", ErrUnknownExtension, strings.Join(missing, ", ")) + } + } + + return nil +} diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go index 72c9ccf..eb85a11 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go @@ -75,6 +75,10 @@ var ( // ErrEmptyRefFile is returned when a reference file is attempted to be read, // but the file is empty ErrEmptyRefFile = errors.New("ref file is empty") + // ErrModuleNameEscape is returned when a submodule name would + // resolve outside the modules/ subtree, mirroring canonical Git's + // "ignoring suspicious submodule name" defence. + ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory") ) // Options holds configuration for the storage. @@ -1127,9 +1131,20 @@ func (d *DotGit) PackRefs() (err error) { return nil } -// Module return a billy.Filesystem pointing to the module folder +// Module returns a billy.Filesystem pointing to the module folder. +// +// As a defence in depth against submodule name path traversal, +// refuse names whose joined path leaves the modules/ subtree once +// cleaned. The config-layer parser also validates submodule names, +// but Module may be reached from any caller that constructs a +// Submodule struct programmatically and so bypasses the parser. func (d *DotGit) Module(name string) (billy.Filesystem, error) { - return d.fs.Chroot(d.fs.Join(modulePath, name)) + p := d.fs.Join(modulePath, name) + cleaned := path.Clean(filepath.ToSlash(p)) + if cleaned != modulePath && !strings.HasPrefix(cleaned, modulePath+"/") { + return nil, ErrModuleNameEscape + } + return d.fs.Chroot(p) } func (d *DotGit) AddAlternate(remote string) error { diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go index 849b7a1..e9be5bc 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go @@ -3,6 +3,7 @@ package dotgit import ( "fmt" "io" + "os" "sync/atomic" "github.com/go-git/go-git/v5/plumbing" @@ -131,20 +132,62 @@ func (w *PackWriter) clean() error { func (w *PackWriter) save() error { base := w.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s", w.checksum)) - idx, err := w.fs.Create(fmt.Sprintf("%s.idx", base)) + + // Pack files are content addressable. Each file is checked + // individually — if it already exists on disk, skip creating it. + idxPath := fmt.Sprintf("%s.idx", base) + exists, err := fileExists(w.fs, idxPath) if err != nil { return err } + if !exists { + idx, err := w.fs.Create(idxPath) + if err != nil { + return err + } - if err := w.encodeIdx(idx); err != nil { - return err + if err := w.encodeIdx(idx); err != nil { + _ = idx.Close() + return err + } + + if err := idx.Close(); err != nil { + return err + } + fixPermissions(w.fs, idxPath) } - if err := idx.Close(); err != nil { + packPath := fmt.Sprintf("%s.pack", base) + exists, err = fileExists(w.fs, packPath) + if err != nil { return err } + if !exists { + if err := w.fs.Rename(w.fw.Name(), packPath); err != nil { + return err + } + fixPermissions(w.fs, packPath) + } else { + // Pack already exists, clean up the temp file. + return w.clean() + } - return w.fs.Rename(w.fw.Name(), fmt.Sprintf("%s.pack", base)) + return nil +} + +// fileExists checks whether path already exists as a regular file. +// It returns (true, nil) for an existing regular file, (false, nil) when the +// path does not exist, and (false, err) if the path exists but is not a +// regular file (e.g. a directory or symlink). +func fileExists(fs billy.Filesystem, path string) (bool, error) { + fi, err := fs.Lstat(path) + if err != nil { + return false, nil + } + if !fi.Mode().IsRegular() { + return false, fmt.Errorf("unexpected file type for %q: %s", path, fi.Mode().Type()) + } + return true, nil } func (w *PackWriter) encodeIdx(writer io.Writer) error { @@ -226,7 +269,6 @@ func (s *syncedReader) sleep() { atomic.StoreUint32(&s.blocked, 1) <-s.news } - } func (s *syncedReader) Seek(offset int64, whence int) (int64, error) { @@ -281,5 +323,17 @@ func (w *ObjectWriter) save() error { hex := w.Hash().String() file := w.fs.Join(objectsPath, hex[0:2], hex[2:hash.HexSize]) - return w.fs.Rename(w.f.Name(), file) + // Loose objects are content addressable, if they already exist + // we can safely delete the temporary file and short-circuit the + // operation. + if _, err := w.fs.Lstat(file); err == nil || os.IsExist(err) { + return w.fs.Remove(w.f.Name()) + } + + if err := w.fs.Rename(w.f.Name(), file); err != nil { + return err + } + fixPermissions(w.fs, file) + + return nil } diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go new file mode 100644 index 0000000..134a258 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go @@ -0,0 +1,29 @@ +//go:build !windows + +package dotgit + +import ( + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/utils/trace" +) + +func fixPermissions(fs billy.Filesystem, path string) { + if chmodFS, ok := fs.(billy.Chmod); ok { + if err := chmodFS.Chmod(path, 0o444); err != nil { + trace.General.Printf("failed to chmod %s: %v", path, err) + } + } +} + +func isReadOnly(fs billy.Filesystem, path string) (bool, error) { + fi, err := fs.Stat(path) + if err != nil { + return false, err + } + + if fi.Mode().Perm() == 0o444 { + return true, nil + } + + return false, nil +} diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go new file mode 100644 index 0000000..c22abcc --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go @@ -0,0 +1,58 @@ +//go:build windows + +package dotgit + +import ( + "fmt" + "path/filepath" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/utils/trace" + "golang.org/x/sys/windows" +) + +func fixPermissions(fs billy.Filesystem, path string) { + fullpath := filepath.Join(fs.Root(), path) + p, err := windows.UTF16PtrFromString(fullpath) + if err != nil { + trace.General.Printf("failed to chmod %s: %v", fullpath, err) + return + } + + attrs, err := windows.GetFileAttributes(p) + if err != nil { + trace.General.Printf("failed to chmod %s: %v", fullpath, err) + return + } + + if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 { + return + } + + err = windows.SetFileAttributes(p, + attrs|windows.FILE_ATTRIBUTE_READONLY, + ) + + if err != nil { + trace.General.Printf("failed to chmod %s: %v", fullpath, err) + } +} + +func isReadOnly(fs billy.Filesystem, path string) (bool, error) { + fullpath := filepath.Join(fs.Root(), path) + p, err := windows.UTF16PtrFromString(fullpath) + if err != nil { + return false, fmt.Errorf("%w: %q", err, fullpath) + } + + attrs, err := windows.GetFileAttributes(p) + if err != nil { + return false, fmt.Errorf("%w: %q", err, fullpath) + } + + if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 { + return true, nil + } + + return false, nil +} diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go index a86ef3e..b5b9f95 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go @@ -48,6 +48,11 @@ func (s *IndexStorage) Index() (i *index.Index, err error) { defer ioutil.CheckClose(f, &err) + fi, statErr := s.dir.Fs().Stat(f.Name()) + if statErr == nil { + idx.ModTime = fi.ModTime() + } + d := index.NewDecoder(f) err = d.Decode(idx) return idx, err diff --git a/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go b/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go index 79211c7..b5d0aa7 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go +++ b/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go @@ -69,7 +69,11 @@ type IndexStorage struct { index *index.Index } +// SetIndex stores the given index. +// Note: this method sets idx.ModTime to simulate filesystem storage behavior. func (c *IndexStorage) SetIndex(idx *index.Index) error { + // Set ModTime to enable racy git detection in the metadata optimization. + idx.ModTime = time.Now() c.index = idx return nil } diff --git a/vendor/github.com/go-git/go-git/v5/submodule.go b/vendor/github.com/go-git/go-git/v5/submodule.go index afabb6a..2fe4ca2 100644 --- a/vendor/github.com/go-git/go-git/v5/submodule.go +++ b/vendor/github.com/go-git/go-git/v5/submodule.go @@ -6,9 +6,12 @@ import ( "errors" "fmt" "path" + "path/filepath" "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/internal/pathutil" + giturl "github.com/go-git/go-git/v5/internal/url" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/format/index" "github.com/go-git/go-git/v5/plumbing/transport" @@ -119,6 +122,16 @@ func (s *Submodule) Repository() (*Repository, error) { exists = true } + // s.c.Path is sourced from the worktree's .gitmodules and is + // therefore tree-controlled. Apply the strict tree-path validator + // before chroot — the wrapper's tolerant validPath would let a + // final-position .git component through (e.g. "submodule/.git"), + // which a malicious .gitmodules could use to chroot the submodule + // worktree into the repository's actual .git directory. + if err := pathutil.ValidTreePath(s.c.Path); err != nil { + return nil, err + } + var worktree billy.Filesystem if worktree, err = s.w.Filesystem.Chroot(s.c.Path); err != nil { return nil, err @@ -138,18 +151,25 @@ func (s *Submodule) Repository() (*Repository, error) { return nil, err } - if !path.IsAbs(moduleEndpoint.Path) && moduleEndpoint.Protocol == "file" { - remotes, err := s.w.r.Remotes() + // A relative submodule URL such as "../X.git" must resolve against + // the parent repository's remote URL, not against the process CWD. + // Detect relativity from the raw configured URL because + // transport.NewEndpoint normalizes local paths to absolute form via + // filepath.Abs, which would otherwise mask the relative form here. + if giturl.IsLocalEndpoint(s.c.URL) && + !path.IsAbs(s.c.URL) && !filepath.IsAbs(s.c.URL) { + + base, err := defaultRemote(s.w.r) + if err != nil { + return nil, fmt.Errorf("resolving relative submodule URL: %w", err) + } + + rootEndpoint, err := transport.NewEndpoint(base.URLs[0]) if err != nil { return nil, err } - rootEndpoint, err := transport.NewEndpoint(remotes[0].c.URLs[0]) - if err != nil { - return nil, err - } - - rootEndpoint.Path = path.Join(rootEndpoint.Path, moduleEndpoint.Path) + rootEndpoint.Path = path.Join(rootEndpoint.Path, s.c.URL) *moduleEndpoint = *rootEndpoint } @@ -161,6 +181,52 @@ func (s *Submodule) Repository() (*Repository, error) { return r, err } +// defaultRemote returns the remote that relative submodule URLs are +// resolved against, mirroring canonical Git's repo_default_remote +// (remote.c) and resolve_relative_url (builtin/submodule--helper.c): +// +// 1. if HEAD is on a branch with branch..remote configured, +// use that remote; +// 2. else if exactly one remote is configured, use it; +// 3. otherwise fall back to DefaultRemoteName ("origin"). +// +// Each rule falls through unconditionally: a branch lookup that +// finds the branch but with an empty Remote does not short-circuit +// rule (2). Returns an error when the chosen remote is not configured. +func defaultRemote(r *Repository) (*config.RemoteConfig, error) { + cfg, err := r.Config() + if err != nil { + return nil, err + } + + if ref, err := r.Reference(plumbing.HEAD, false); err == nil && + ref.Type() == plumbing.SymbolicReference && + ref.Target().IsBranch() { + if b, ok := cfg.Branches[ref.Target().Short()]; ok && b.Remote != "" { + return lookupRemote(cfg, b.Remote) + } + } + + if len(cfg.Remotes) == 1 { + for name := range cfg.Remotes { + return lookupRemote(cfg, name) + } + } + + return lookupRemote(cfg, DefaultRemoteName) +} + +func lookupRemote(cfg *config.Config, name string) (*config.RemoteConfig, error) { + rc, ok := cfg.Remotes[name] + if !ok { + return nil, fmt.Errorf("remote %q not found", name) + } + if len(rc.URLs) == 0 { + return nil, fmt.Errorf("remote %q has no configured URL", name) + } + return rc, nil +} + // Update the registered submodule to match what the superproject expects, the // submodule should be initialized first calling the Init method or setting in // the options SubmoduleUpdateOptions.Init equals true diff --git a/vendor/github.com/go-git/go-git/v5/utils/binary/read.go b/vendor/github.com/go-git/go-git/v5/utils/binary/read.go index b8f9df1..71d9ad6 100644 --- a/vendor/github.com/go-git/go-git/v5/utils/binary/read.go +++ b/vendor/github.com/go-git/go-git/v5/utils/binary/read.go @@ -5,11 +5,18 @@ package binary import ( "bufio" "encoding/binary" + "errors" "io" + "math" "github.com/go-git/go-git/v5/plumbing" ) +// ErrIntegerOverflow is returned when a Git-format variable-width integer +// would not fit into an int64 because the input declares more continuation +// bytes than the type can hold. +var ErrIntegerOverflow = errors.New("variable-width integer overflow") + // Read reads structured binary data from r into data. Bytes are read and // decoded in BigEndian order // https://golang.org/pkg/encoding/binary/#Read @@ -92,6 +99,14 @@ func ReadVariableWidthInt(r io.Reader) (int64, error) { var v = int64(c & maskLength) for c&maskContinue > 0 { + // Reject input that, after the v++ and shift below, would + // not fit in an int64. With v < (MaxInt64-127)>>7, the + // post-increment v is at most (MaxInt64-127)>>7 and the + // final (v << 7) + (c & 0x7F) stays within int64. + if v >= (math.MaxInt64-int64(maskLength))>>lengthBits { + return 0, ErrIntegerOverflow + } + v++ if err := Read(r, &c); err != nil { return 0, err diff --git a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go index 3380062..83df4dd 100644 --- a/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go +++ b/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go @@ -4,9 +4,11 @@ import ( "io" "os" "path" + "time" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/format/index" "github.com/go-git/go-git/v5/utils/merkletrie/noder" "github.com/go-git/go-billy/v5" @@ -16,6 +18,14 @@ var ignore = map[string]bool{ ".git": true, } +// Options contains configuration for the filesystem node. +type Options struct { + // Index is used to enable the metadata-first comparison optimization while + // correctly handling the "racy git" condition. If no index is provided, + // the function works without the optimization. + Index *index.Index +} + // The node represents a file or a directory in a billy.Filesystem. It // implements the interface noder.Noder of merkletrie package. // @@ -24,6 +34,8 @@ var ignore = map[string]bool{ type node struct { fs billy.Filesystem submodules map[string]plumbing.Hash + idx *index.Index + idxMap map[string]*index.Entry path string hash []byte @@ -31,6 +43,7 @@ type node struct { isDir bool mode os.FileMode size int64 + modTime time.Time } // NewRootNode returns the root node based on a given billy.Filesystem. @@ -42,7 +55,41 @@ func NewRootNode( fs billy.Filesystem, submodules map[string]plumbing.Hash, ) noder.Noder { - return &node{fs: fs, submodules: submodules, isDir: true} + return NewRootNodeWithOptions(fs, submodules, Options{}) +} + +// NewRootNodeWithOptions returns the root node based on a given billy.Filesystem +// with options to set an index. Providing an index enables the metadata-first +// comparison optimization while correctly handling the "racy git" condition. If +// no index is provided, the function works without the optimization. +// +// The index's ModTime field is used to detect the racy git condition. When a file's +// mtime equals or is newer than the index ModTime, we must hash the file content +// even if other metadata matches, because the file may have been modified in the +// same second that the index was written. +// +// Reference: https://git-scm.com/docs/racy-git +func NewRootNodeWithOptions( + fs billy.Filesystem, + submodules map[string]plumbing.Hash, + options Options, +) noder.Noder { + var idxMap map[string]*index.Entry + + if options.Index != nil { + idxMap = make(map[string]*index.Entry, len(options.Index.Entries)) + for _, entry := range options.Index.Entries { + idxMap[entry.Name] = entry + } + } + + return &node{ + fs: fs, + submodules: submodules, + idx: options.Index, + idxMap: idxMap, + isDir: true, + } } // Hash the hash of a filesystem is the result of concatenating the computed @@ -133,11 +180,14 @@ func (n *node) newChildNode(file os.FileInfo) (*node, error) { node := &node{ fs: n.fs, submodules: n.submodules, + idx: n.idx, + idxMap: n.idxMap, - path: path, - isDir: file.IsDir(), - size: file.Size(), - mode: file.Mode(), + path: path, + isDir: file.IsDir(), + size: file.Size(), + mode: file.Mode(), + modTime: file.ModTime(), } if _, isSubmodule := n.submodules[path]; isSubmodule { @@ -161,6 +211,16 @@ func (n *node) calculateHash() { n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...) return } + + if n.idxMap != nil { + if entry, ok := n.idxMap[n.path]; ok { + if n.metadataMatches(entry) { + n.hash = append(entry.Hash[:], mode.Bytes()...) + return + } + } + } + var hash plumbing.Hash if n.mode&os.ModeSymlink != 0 { hash = n.doCalculateHashForSymlink() @@ -170,6 +230,44 @@ func (n *node) calculateHash() { n.hash = append(hash[:], mode.Bytes()...) } +func (n *node) metadataMatches(entry *index.Entry) bool { + if entry == nil { + return false + } + + if uint32(n.size) != entry.Size { + return false + } + + if !n.modTime.IsZero() && !n.modTime.Equal(entry.ModifiedAt) { + return false + } + + mode, err := filemode.NewFromOSFileMode(n.mode) + if err != nil { + return false + } + + if mode != entry.Mode { + return false + } + + if n.idx != nil && !n.idx.ModTime.IsZero() && !n.modTime.IsZero() { + if !n.modTime.Before(n.idx.ModTime) { + return false + } + } + + // If we couldn't perform the racy git check (idx is nil or idx.ModTime is zero), + // we cannot safely rely on metadata alone — force content hashing. + // This can occur with in-memory storage where the index file timestamp is unavailable. + if n.idx == nil || n.idx.ModTime.IsZero() { + return false + } + + return true +} + func (n *node) doCalculateHashForRegular() plumbing.Hash { f, err := n.fs.Open(n.path) if err != nil { diff --git a/vendor/github.com/go-git/go-git/v5/worktree.go b/vendor/github.com/go-git/go-git/v5/worktree.go index 5e9cd7b..d8ee9fd 100644 --- a/vendor/github.com/go-git/go-git/v5/worktree.go +++ b/vendor/github.com/go-git/go-git/v5/worktree.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "runtime" "strings" "github.com/go-git/go-billy/v5" @@ -385,7 +384,8 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([] return nil, err } - var removedFiles []string + removedFiles := make([]string, 0, len(changes)) + filesMap := buildFilePathMap(files) for _, ch := range changes { a, err := ch.Action() if err != nil { @@ -407,7 +407,7 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([] } if len(files) > 0 { - contains := inFiles(files, name) + contains := inFiles(filesMap, name) if !contains { continue } @@ -436,15 +436,11 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([] return removedFiles, w.r.Storer.SetIndex(idx) } -func inFiles(files []string, v string) bool { +// inFiles checks if the given file is in the list of files. The incoming filepaths in files should be cleaned before calling this function. +func inFiles(files map[string]struct{}, v string) bool { v = filepath.Clean(v) - for _, s := range files { - if filepath.Clean(s) == v { - return true - } - } - - return false + _, exists := files[v] + return exists } func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { @@ -459,11 +455,8 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } b := newIndexBuilder(idx) + filesMap := buildFilePathMap(files) for _, ch := range changes { - if err := w.validChange(ch); err != nil { - return err - } - if len(files) > 0 { file := "" if ch.From != nil { @@ -476,7 +469,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { continue } - contains := inFiles(files, file) + contains := inFiles(filesMap, file) if !contains { continue } @@ -491,108 +484,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { return w.r.Storer.SetIndex(idx) } -// worktreeDeny is a list of paths that are not allowed -// to be used when resetting the worktree. -var worktreeDeny = map[string]struct{}{ - // .git - GitDirName: {}, - - // For other historical reasons, file names that do not conform to the 8.3 - // format (up to eight characters for the basename, three for the file - // extension, certain characters not allowed such as `+`, etc) are associated - // with a so-called "short name", at least on the `C:` drive by default. - // Which means that `git~1/` is a valid way to refer to `.git/`. - "git~1": {}, -} - -// validPath checks whether paths are valid. -// The rules around invalid paths could differ from upstream based on how -// filesystems are managed within go-git, but they are largely the same. -// -// For upstream rules: -// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/read-cache.c#L946 -// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/path.c#L1383 -func validPath(paths ...string) error { - for _, p := range paths { - parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') }) - if len(parts) == 0 { - return fmt.Errorf("invalid path: %q", p) - } - - if _, denied := worktreeDeny[strings.ToLower(parts[0])]; denied { - return fmt.Errorf("invalid path prefix: %q", p) - } - - if runtime.GOOS == "windows" { - // Volume names are not supported, in both formats: \\ and :. - if vol := filepath.VolumeName(p); vol != "" { - return fmt.Errorf("invalid path: %q", p) - } - - if !windowsValidPath(parts[0]) { - return fmt.Errorf("invalid path: %q", p) - } - } - - for _, part := range parts { - if part == ".." { - return fmt.Errorf("invalid path %q: cannot use '..'", p) - } - } - } - return nil -} - -// windowsPathReplacer defines the chars that need to be replaced -// as part of windowsValidPath. -var windowsPathReplacer *strings.Replacer - -func init() { - windowsPathReplacer = strings.NewReplacer(" ", "", ".", "") -} - -func windowsValidPath(part string) bool { - if len(part) > 3 && strings.EqualFold(part[:4], GitDirName) { - // For historical reasons, file names that end in spaces or periods are - // automatically trimmed. Therefore, `.git . . ./` is a valid way to refer - // to `.git/`. - if windowsPathReplacer.Replace(part[4:]) == "" { - return false - } - - // For yet other historical reasons, NTFS supports so-called "Alternate Data - // Streams", i.e. metadata associated with a given file, referred to via - // `::`. There exists a default stream - // type for directories, allowing `.git/` to be accessed via - // `.git::$INDEX_ALLOCATION/`. - // - // For performance reasons, _all_ Alternate Data Streams of `.git/` are - // forbidden, not just `::$INDEX_ALLOCATION`. - if len(part) > 4 && part[4:5] == ":" { - return false - } - } - return true -} - -func (w *Worktree) validChange(ch merkletrie.Change) error { - action, err := ch.Action() - if err != nil { - return nil - } - - switch action { - case merkletrie.Delete: - return validPath(ch.From.String()) - case merkletrie.Insert: - return validPath(ch.To.String()) - case merkletrie.Modify: - return validPath(ch.From.String(), ch.To.String()) - } - - return nil -} - func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error { a, err := ch.Action() if err != nil { @@ -765,10 +656,10 @@ func (w *Worktree) checkoutFile(f *object.File) (err error) { } func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) { - // https://github.com/git/git/commit/10ecfa76491e4923988337b2e2243b05376b40de - if strings.EqualFold(f.Name, gitmodulesFile) { - return ErrGitModulesSymlink - } + // .gitmodules symlink rejection (and its NTFS / HFS variants) is + // enforced by the worktreeFilesystem wrapper's Symlink method via + // validSymlinkName. See https://github.com/git/git/commit/10ecfa7 + // for the upstream rationale. from, err := f.Reader() if err != nil { @@ -1206,3 +1097,16 @@ func (b *indexBuilder) Add(e *index.Entry) { func (b *indexBuilder) Remove(name string) { delete(b.entries, filepath.ToSlash(name)) } + +// buildFilePathMap creates a map of cleaned file paths for efficient lookup. +// Returns nil if the input slice is empty. +func buildFilePathMap(files []string) map[string]struct{} { + if len(files) == 0 { + return nil + } + filesMap := make(map[string]struct{}, len(files)) + for _, f := range files { + filesMap[filepath.Clean(f)] = struct{}{} + } + return filesMap +} diff --git a/vendor/github.com/go-git/go-git/v5/worktree_fs.go b/vendor/github.com/go-git/go-git/v5/worktree_fs.go new file mode 100644 index 0000000..9bc2fd9 --- /dev/null +++ b/vendor/github.com/go-git/go-git/v5/worktree_fs.go @@ -0,0 +1,264 @@ +package git + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/go-git/go-billy/v5" + + "github.com/go-git/go-git/v5/internal/pathutil" +) + +// defaultProtectHFS returns the default value for core.protectHFS +// when not explicitly configured. Matches upstream Git's +// PROTECT_HFS_DEFAULT[1], which the Makefile sets to 1 on Darwin +// and leaves at 0 on every other platform. +// +// [1]: https://github.com/git/git/blob/v2.54.0/config.mak.uname#L146 +func defaultProtectHFS() bool { + return runtime.GOOS == "darwin" +} + +// defaultProtectNTFS returns the default value for core.protectNTFS +// when not explicitly configured. Matches upstream Git's +// PROTECT_NTFS_DEFAULT, which has been 1 on every platform since +// 9102f958ee5 (CVE-2019-1353)[1]: WSL allows Linux processes to +// reach NTFS-mounted worktrees on Windows hosts, so the +// is_ntfs_dotgit guard cannot safely be gated on the runtime OS. +// +// [1]: https://github.com/git/git/commit/9102f958ee5 +func defaultProtectNTFS() bool { + return true +} + +// worktreeFilesystem wraps a billy.Filesystem and validates every path passed +// to a mutating operation. This prevents writing to, or deleting from, +// dangerous locations (e.g. .git/*, ../) regardless of which worktree +// code path triggers the operation. +type worktreeFilesystem struct { + billy.Filesystem + protectNTFS bool + protectHFS bool +} + +func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *worktreeFilesystem { + return &worktreeFilesystem{Filesystem: fs, protectNTFS: protectNTFS, protectHFS: protectHFS} +} + +func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) { + if err := sfs.validPath(filename); err != nil { + return nil, fmt.Errorf("create: %w", err) + } + return sfs.Filesystem.Create(filename) +} + +func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) { + if err := sfs.validReadPath(filename); err != nil { + return nil, fmt.Errorf("open: %w", err) + } + return sfs.Filesystem.Open(filename) +} + +func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { + if err := sfs.validPath(filename); err != nil { + return nil, fmt.Errorf("openfile: %w", err) + } + return sfs.Filesystem.OpenFile(filename, flag, perm) +} + +func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) { + if err := sfs.validReadPath(filename); err != nil { + return nil, fmt.Errorf("stat: %w", err) + } + return sfs.Filesystem.Stat(filename) +} + +func (sfs *worktreeFilesystem) Remove(filename string) error { + if err := sfs.validPath(filename); err != nil { + return fmt.Errorf("remove: %w", err) + } + return sfs.Filesystem.Remove(filename) +} + +func (sfs *worktreeFilesystem) Rename(from, to string) error { + if err := sfs.validPath(from, to); err != nil { + return fmt.Errorf("rename: %w", err) + } + return sfs.Filesystem.Rename(from, to) +} + +func (sfs *worktreeFilesystem) ReadDir(path string) ([]os.FileInfo, error) { + if err := sfs.validReadPath(path); err != nil { + return nil, fmt.Errorf("readdir: %w", err) + } + return sfs.Filesystem.ReadDir(path) +} + +func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) { + if err := sfs.validReadPath(filename); err != nil { + return nil, fmt.Errorf("lstat: %w", err) + } + return sfs.Filesystem.Lstat(filename) +} + +func (sfs *worktreeFilesystem) Symlink(target, link string) error { + if err := sfs.validPath(link); err != nil { + return fmt.Errorf("symlink: %w", err) + } + if err := sfs.validSymlinkName(link); err != nil { + return fmt.Errorf("symlink: %w", err) + } + return sfs.Filesystem.Symlink(target, link) +} + +func (sfs *worktreeFilesystem) Readlink(link string) (string, error) { + if err := sfs.validReadPath(link); err != nil { + return "", fmt.Errorf("readlink: %w", err) + } + return sfs.Filesystem.Readlink(link) +} + +func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error { + // MkdirAll on the worktree root is a no-op: the root always exists, + // so there is nothing to materialise. Mirroring the tolerance that + // validReadPath gives to read-side operations avoids breaking callers + // that walk a directory tree and pass the relative-to-root prefix + // ("") through to the worktree FS. + if path == "" || path == "." || path == "/" { + return nil + } + if err := sfs.validPath(path); err != nil { + return fmt.Errorf("mkdirall: %w", err) + } + return sfs.Filesystem.MkdirAll(path, perm) +} + +func (sfs *worktreeFilesystem) TempFile(_, _ string) (billy.File, error) { + return nil, fmt.Errorf("tempfile: %w", errUnsupportedOperation) +} + +func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) { + if err := sfs.validReadPath(path); err != nil { + return nil, fmt.Errorf("chroot: %w", err) + } + return sfs.Filesystem.Chroot(path) +} + +// validReadPath is like validPath but treats the empty string and "." as +// valid references to the worktree root. Read-side operations on the root +// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself +// is not, so write-side operations continue to use validPath directly. +func (sfs *worktreeFilesystem) validReadPath(p string) error { + if p == "" || p == "." || p == "/" { + return nil + } + return sfs.validPath(p) +} + +var errUnsupportedOperation = errors.New("unsupported operation") + +// isDotGitVariant reports whether part is .git, git~1, or an HFS+ +// equivalent of .git (when protectHFS is true). NTFS variants of .git +// (e.g. ".git " with trailing space, ".git::$INDEX_ALLOCATION") are +// detected separately by pathutil.WindowsValidPath, which applies +// regardless of position in the path. Both validators reuse this +// helper. +func isDotGitVariant(part string, protectHFS bool) bool { + if pathutil.IsDotGitName(part) { + return true + } + if protectHFS && pathutil.IsHFSDotGit(part) { + return true + } + return false +} + +// validPath checks whether paths are valid for the worktree +// filesystem abstraction. It is intentionally tolerant of .git as +// the final path component of a multi-component path +// (e.g. "submodule/.git"), so that legitimate gitlink pointer files +// can still be Stat'd, Read, and Removed via the wrapper during +// submodule cleanup. Attacker-controlled tree-entry paths are +// validated separately by pathutil.ValidTreePath at the boundaries +// where data leaves the trusted store (Tree.FindEntry, the explicit +// callers in CherryPick and Submodule.Repository). +// +// For upstream rules: +// https://github.com/git/git/blob/v2.54.0/read-cache.c#L987 +// https://github.com/git/git/blob/v2.54.0/path.c#L1419 +func (sfs *worktreeFilesystem) validPath(paths ...string) error { + for _, p := range paths { + for i := 0; i < len(p); i++ { + if p[i] < 0x20 || p[i] == 0x7f { + return fmt.Errorf("invalid path %q: contains control character", p) + } + } + + parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') }) + if len(parts) == 0 { + return fmt.Errorf("invalid path: %q", p) + } + + if sfs.protectNTFS { + // Volume names are not supported, in both formats: \\ and :. + if vol := filepath.VolumeName(p); vol != "" { + return fmt.Errorf("invalid path: %q", p) + } + } + + for i, part := range parts { + if part == "." || part == ".." { + return fmt.Errorf("invalid path %q: cannot use %q", p, part) + } + + // Reject .git (and equivalents) as a path component when it is + // either the first component (root-level .git) or a non-final + // component (traversal into a .git directory, e.g. "a/.git/config"). + // A final non-first .git component (e.g. "submodule/.git") is + // allowed because submodule worktrees contain a .git pointer file. + if isDotGitVariant(part, sfs.protectHFS) && (i == 0 || i < len(parts)-1) { + return fmt.Errorf("invalid path component: %q", p) + } + + if sfs.protectNTFS && !pathutil.WindowsValidPath(part) { + return fmt.Errorf("invalid path: %q", p) + } + } + } + return nil +} + +// validSymlinkName checks the per-component name of a symlink for +// dotfile names that attackers can use to trick a checkout into +// writing a dangerous symlink. Each path component is compared +// against .gitmodules case-insensitively, against its NTFS variants +// (e.g. ".gitmodules .", ".gitmodules::$INDEX_ALLOCATION", or 8.3 +// short-name forms) when protectNTFS is on, and against its HFS+ +// variants (Unicode ignored code points folded into ".gitmodules") +// when protectHFS is on. +// +// Reference: upstream Git verify_path_internal at read-cache.c#L1004-L1024 +// in tag v2.54.0[1]. +// +// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L1004-L1024 +func (sfs *worktreeFilesystem) validSymlinkName(name string) error { + parts := strings.FieldsFunc(name, func(r rune) bool { + return r == '/' || r == '\\' + }) + for _, part := range parts { + if strings.EqualFold(part, gitmodulesFile) { + return ErrGitModulesSymlink + } + if sfs.protectNTFS && pathutil.IsNTFSDotGitmodules(part) { + return ErrGitModulesSymlink + } + if sfs.protectHFS && pathutil.IsHFSDotGitmodules(part) { + return ErrGitModulesSymlink + } + } + return nil +} diff --git a/vendor/github.com/go-git/go-git/v5/worktree_status.go b/vendor/github.com/go-git/go-git/v5/worktree_status.go index 7870d13..ecc3d7a 100644 --- a/vendor/github.com/go-git/go-git/v5/worktree_status.go +++ b/vendor/github.com/go-git/go-git/v5/worktree_status.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/go-git/go-billy/v5/util" + "github.com/go-git/go-git/v5/internal/pathutil" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/format/gitignore" @@ -141,7 +142,7 @@ func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool) return nil, err } - to := filesystem.NewRootNode(w.Filesystem, submodules) + to := filesystem.NewRootNodeWithOptions(w.Filesystem, submodules, filesystem.Options{Index: idx}) var c merkletrie.Changes if reverse { @@ -545,6 +546,14 @@ func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h p } func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error { + // Mirror upstream's Index.Add gate at the v5 caller boundary: the + // index feeds future trees, so a name that the tree-side + // pathutil.ValidTreePath gate would reject must not enter the + // index in the first place. v5 keeps Index.Add's existing signature + // for API compatibility, so the validation happens here. + if err := pathutil.ValidTreePath(filename); err != nil { + return err + } return w.doUpdateFileToIndex(idx.Add(filename), filename, h) } diff --git a/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm b/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm index cbbb007..1fe43ec 100644 --- a/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm +++ b/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm @@ -1,4 +1,4 @@ -FROM golang:1.24@sha256:14fd8a55e59a560704e5fc44970b301d00d344e45d6b914dda228e09f359a088 +FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a ENV GOOS=linux ENV GOARCH=arm diff --git a/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm64 b/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm64 index 93b2bb1..af7f2e2 100644 --- a/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm64 +++ b/vendor/github.com/pjbgf/sha1cd/Dockerfile.arm64 @@ -1,4 +1,4 @@ -FROM golang:1.24@sha256:14fd8a55e59a560704e5fc44970b301d00d344e45d6b914dda228e09f359a088 +FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a ENV GOOS=linux ENV GOARCH=arm64 diff --git a/vendor/github.com/pjbgf/sha1cd/sha1cd.go b/vendor/github.com/pjbgf/sha1cd/sha1cd.go index b8d2890..865a995 100644 --- a/vendor/github.com/pjbgf/sha1cd/sha1cd.go +++ b/vendor/github.com/pjbgf/sha1cd/sha1cd.go @@ -12,7 +12,6 @@ package sha1cd // Original: https://github.com/golang/go/blob/master/src/crypto/sha1/sha1.go import ( - "crypto" "encoding/binary" "errors" "hash" @@ -20,10 +19,6 @@ import ( shared "github.com/pjbgf/sha1cd/internal" ) -func init() { - crypto.RegisterHash(crypto.SHA1, New) -} - // The size of a SHA-1 checksum in bytes. const Size = shared.Size diff --git a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.go b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.go index 7b3ad25..6b716ab 100644 --- a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.go +++ b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.go @@ -37,9 +37,9 @@ func block(dig *digest, p []byte) { chunk := p[:shared.Chunk] blockAMD64(dig.h[:], chunk, m1[:], cs[:]) - rectifyCompressionState(m1, &cs) + rectifyCompressionState(&m1, &cs) - col := checkCollision(m1, cs, dig.h) + col := checkCollision(&m1, &cs, &dig.h) if col { dig.col = true diff --git a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.s b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.s index e2725f3..061906a 100644 --- a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.s +++ b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.s @@ -11,11 +11,11 @@ // Reference implementations: // - https://github.com/golang/go/blob/master/src/crypto/sha1/sha1block_amd64.s +// Reverse the dword order in abcd via PSHUFD then store the 16 bytes in one +// move, instead of issuing four VPEXTRD's that each go through the store port. #define LOADCS(abcd, e, index, target) \ - VPEXTRD $3, abcd, ((index*20)+0)(target); \ - VPEXTRD $2, abcd, ((index*20)+4)(target); \ - VPEXTRD $1, abcd, ((index*20)+8)(target); \ - VPEXTRD $0, abcd, ((index*20)+12)(target); \ + VPSHUFD $0x1B, abcd, X8; \ + VMOVDQU X8, ((index*20)+0)(target); \ MOVL e, ((index*20)+16)(target); #define LOADM1(m1, index, target) \ diff --git a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_arm64.go b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_arm64.go index e641c95..f44f22d 100644 --- a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_arm64.go +++ b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_arm64.go @@ -34,8 +34,8 @@ func block(dig *digest, p []byte) { blockARM64(dig.h[:], chunk, m1[:], cs[:]) - rectifyCompressionState(m1, &cs) - col := checkCollision(m1, cs, dig.h) + rectifyCompressionState(&m1, &cs) + col := checkCollision(&m1, &cs, &dig.h) if col { dig.col = true diff --git a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_generic.go b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_generic.go index 0569a1f..a80148e 100644 --- a/vendor/github.com/pjbgf/sha1cd/sha1cdblock_generic.go +++ b/vendor/github.com/pjbgf/sha1cd/sha1cdblock_generic.go @@ -127,7 +127,8 @@ func blockGeneric(dig *digest, p []byte) { } if hi == 1 { - col := checkCollision(m1, cs, [shared.WordBuffers]uint32{h0, h1, h2, h3, h4}) + h := [shared.WordBuffers]uint32{h0, h1, h2, h3, h4} + col := checkCollision(&m1, &cs, &h) if col { dig.col = true hi++ @@ -143,23 +144,23 @@ func blockGeneric(dig *digest, p []byte) { //go:noinline func checkCollision( - m1 [shared.Rounds]uint32, - cs [shared.PreStepState][shared.WordBuffers]uint32, - h [shared.WordBuffers]uint32, + m1 *[shared.Rounds]uint32, + cs *[shared.PreStepState][shared.WordBuffers]uint32, + h *[shared.WordBuffers]uint32, ) bool { if mask := ubc.CalculateDvMask(m1); mask != 0 { dvs := ubc.SHA1_dvs() for i := 0; dvs[i].DvType != 0; i++ { if (mask & ((uint32)(1) << uint32(dvs[i].MaskB))) != 0 { - var csState [shared.WordBuffers]uint32 + var csState *[shared.WordBuffers]uint32 switch dvs[i].TestT { case 58: - csState = cs[1] + csState = &cs[1] case 65: - csState = cs[2] + csState = &cs[2] case 0: - csState = cs[0] + csState = &cs[0] default: panic(fmt.Sprintf("dvs data is trying to use a testT that isn't available: %d", dvs[i].TestT)) } @@ -168,7 +169,7 @@ func checkCollision( dvs[i].TestT, // testT is the step number // m2 is a secondary message created XORing with // ubc's DM prior to the SHA recompression step. - m1, dvs[i].Dm, + m1, &dvs[i].Dm, csState, h) @@ -182,8 +183,8 @@ func checkCollision( } //go:nosplit -func hasCollided(step uint32, m1, dm [shared.Rounds]uint32, - state [shared.WordBuffers]uint32, h [shared.WordBuffers]uint32) bool { +func hasCollided(step uint32, m1, dm *[shared.Rounds]uint32, + state *[shared.WordBuffers]uint32, h *[shared.WordBuffers]uint32) bool { // Intermediary Hash Value. ihv := [shared.WordBuffers]uint32{} @@ -282,7 +283,7 @@ func hasCollided(step uint32, m1, dm [shared.Rounds]uint32, // //go:nosplit func rectifyCompressionState( - m1 [shared.Rounds]uint32, + m1 *[shared.Rounds]uint32, cs *[shared.PreStepState][shared.WordBuffers]uint32, ) { if cs == nil { diff --git a/vendor/github.com/pjbgf/sha1cd/ubc/ubc.go b/vendor/github.com/pjbgf/sha1cd/ubc/ubc.go index 0da55c0..bd93205 100644 --- a/vendor/github.com/pjbgf/sha1cd/ubc/ubc.go +++ b/vendor/github.com/pjbgf/sha1cd/ubc/ubc.go @@ -29,7 +29,10 @@ type DvInfo struct { // bitconditions for that DV have been met. // //go:nosplit -func CalculateDvMask(W [80]uint32) uint32 { +func CalculateDvMask(W *[80]uint32) uint32 { + if W == nil { + return 0 + } mask := uint32(0xFFFFFFFF) mask &= (((((W[44] ^ W[45]) >> 29) & 1) - 1) | ^(DV_I_48_0_bit | DV_I_51_0_bit | DV_I_52_0_bit | DV_II_45_0_bit | DV_II_46_0_bit | DV_II_50_0_bit | DV_II_51_0_bit)) mask &= (((((W[49] ^ W[50]) >> 29) & 1) - 1) | ^(DV_I_46_0_bit | DV_II_45_0_bit | DV_II_50_0_bit | DV_II_51_0_bit | DV_II_55_0_bit | DV_II_56_0_bit)) diff --git a/vendor/golang.org/x/crypto/ssh/agent/server.go b/vendor/golang.org/x/crypto/ssh/agent/server.go index 4e8ff86..2a7658c 100644 --- a/vendor/golang.org/x/crypto/ssh/agent/server.go +++ b/vendor/golang.org/x/crypto/ssh/agent/server.go @@ -36,7 +36,7 @@ func (s *server) processRequestBytes(reqData []byte) []byte { return []byte{agentFailure} } - if err == nil && rep == nil { + if rep == nil { return []byte{agentSuccess} } diff --git a/vendor/golang.org/x/crypto/ssh/cipher.go b/vendor/golang.org/x/crypto/ssh/cipher.go index 7554ed5..ad2b370 100644 --- a/vendor/golang.org/x/crypto/ssh/cipher.go +++ b/vendor/golang.org/x/crypto/ssh/cipher.go @@ -586,7 +586,7 @@ func (c *cbcCipher) writeCipherPacket(seqNum uint32, w io.Writer, rand io.Reader // Length of encrypted portion of the packet (header, payload, padding). // Enforce minimum padding and packet size. - encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize) + encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPacketSize) // Enforce block size. encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize diff --git a/vendor/golang.org/x/crypto/ssh/client_auth.go b/vendor/golang.org/x/crypto/ssh/client_auth.go index 3127e49..4f2f75c 100644 --- a/vendor/golang.org/x/crypto/ssh/client_auth.go +++ b/vendor/golang.org/x/crypto/ssh/client_auth.go @@ -274,10 +274,14 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA } // Filter algorithms based on those supported by MultiAlgorithmSigner. + // Iterate over the signer's algorithms first to preserve its preference order. + supportedKeyAlgos := algorithmsForKeyFormat(keyFormat) var keyAlgos []string - for _, algo := range algorithmsForKeyFormat(keyFormat) { - if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) { - keyAlgos = append(keyAlgos, algo) + for _, signerAlgo := range as.Algorithms() { + if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool { + return underlyingAlgo(algo) == signerAlgo + }); idx >= 0 { + keyAlgos = append(keyAlgos, supportedKeyAlgos[idx]) } } diff --git a/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s new file mode 100644 index 0000000..e07fa75 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s @@ -0,0 +1,12 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +#include "textflag.h" + +TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctlbyname(SB) +GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB) diff --git a/vendor/golang.org/x/sys/cpu/cpu_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_arm64.go index 6d8eb78..5fc09e2 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_arm64.go @@ -44,14 +44,11 @@ func initOptions() { } func archInit() { - switch runtime.GOOS { - case "freebsd": + if runtime.GOOS == "freebsd" { readARM64Registers() - case "linux", "netbsd", "openbsd", "windows": + } else { + // Most platforms don't seem to allow directly reading these registers. doinit() - default: - // Many platforms don't seem to allow reading these registers. - setMinimalFeatures() } } diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go new file mode 100644 index 0000000..0b47074 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go @@ -0,0 +1,67 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +package cpu + +func doinit() { + setMinimalFeatures() + + // The feature flags are explained in [Instruction Set Detection]. + // There are some differences between MacOS versions: + // + // MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features. + // + // MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual. + // For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512". + // It currently checks both to stay compatible with MacOS 11 and 12. + // The old names also work with MacOS 13, however it's not clear whether + // they will continue working with future OS releases. + // + // Once MacOS 12 is no longer supported the old names can be removed. + // + // [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics + + // Encryption, hashing and checksum capabilities + + // For the following flags there are no MacOS 11 sysctl flags. + ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00")) + ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00")) + ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00")) + ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00")) + + ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00")) + ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00")) + + ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00")) + + // Atomic and memory ordering + ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00")) + ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00")) + + // SIMD and floating point capabilities + ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00")) + ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00")) + ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00")) + ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00")) + ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00")) + ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00")) + + ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00")) + ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00")) + + // Miscellaneous + ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00")) + ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00")) + ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00")) + + // Not supported, but added for completeness + ARM64.HasCPUID = false + + ARM64.HasSM3 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00")) + ARM64.HasSM4 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00")) + ARM64.HasSVE = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00")) + ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00")) +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go new file mode 100644 index 0000000..37ecc66 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && !gc + +package cpu + +import "runtime" + +func doinit() { + setMinimalFeatures() + + ARM64.HasASIMD = true + ARM64.HasFP = true + + // Go already assumes these to be available because they were on the M1 + // and these are supported on all Apple arm64 chips. + ARM64.HasAES = true + ARM64.HasPMULL = true + ARM64.HasSHA1 = true + ARM64.HasSHA2 = true + + if runtime.GOOS != "ios" { + // Apple A7 processors do not support these, however + // M-series SoCs are at least armv8.4-a + ARM64.HasCRC32 = true // armv8.1 + ARM64.HasATOMICS = true // armv8.2 + ARM64.HasJSCVT = true // armv8.3, if HasFP + } +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go index 7f19467..0591308 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go @@ -9,3 +9,4 @@ package cpu func getisar0() uint64 { return 0 } func getisar1() uint64 { return 0 } func getpfr0() uint64 { return 0 } +func getzfr0() uint64 { return 0 } diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go index ff74d7a..53f814d 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !linux && !netbsd && !openbsd && !windows && arm64 +//go:build !darwin && !linux && !netbsd && !openbsd && arm64 package cpu -func doinit() {} +func doinit() { + setMinimalFeatures() +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go deleted file mode 100644 index d09e85a..0000000 --- a/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2026 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cpu - -import ( - "golang.org/x/sys/windows" -) - -func doinit() { - // set HasASIMD and HasFP to true as per - // https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements - // - // The ARM64 version of Windows always presupposes that it's running on an ARMv8 or later architecture. - // Both floating-point and NEON support are presumed to be present in hardware. - // - ARM64.HasASIMD = true - ARM64.HasFP = true - - if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) { - ARM64.HasAES = true - ARM64.HasPMULL = true - ARM64.HasSHA1 = true - ARM64.HasSHA2 = true - } - ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE) - ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE) - ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE) - ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE) - if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) { - ARM64.HasASIMDDP = true - ARM64.HasASIMDRDM = true - } - if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) { - ARM64.HasLRCPC = true - ARM64.HasSM3 = true - } - ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE) - ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE) - ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE) -} diff --git a/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go b/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go new file mode 100644 index 0000000..7b4e67f --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go @@ -0,0 +1,54 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Minimal copy from internal/cpu and runtime to make sysctl calls. + +//go:build darwin && arm64 && gc + +package cpu + +import ( + "syscall" + "unsafe" +) + +type Errno = syscall.Errno + +// adapted from internal/cpu/cpu_arm64_darwin.go +func darwinSysctlEnabled(name []byte) bool { + out := int32(0) + nout := unsafe.Sizeof(out) + if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil { + return false + } + return out > 0 +} + +//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib" + +var libc_sysctlbyname_trampoline_addr uintptr + +// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix +func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error { + if _, _, err := syscall_syscall6( + libc_sysctlbyname_trampoline_addr, + uintptr(unsafe.Pointer(name)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen), + 0, + ); err != 0 { + return err + } + + return nil +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" + +// Implemented in the runtime package (runtime/sys_darwin.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index c1a4670..45476a7 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -593,110 +593,115 @@ const ( ) const ( - NDA_UNSPEC = 0x0 - NDA_DST = 0x1 - NDA_LLADDR = 0x2 - NDA_CACHEINFO = 0x3 - NDA_PROBES = 0x4 - NDA_VLAN = 0x5 - NDA_PORT = 0x6 - NDA_VNI = 0x7 - NDA_IFINDEX = 0x8 - NDA_MASTER = 0x9 - NDA_LINK_NETNSID = 0xa - NDA_SRC_VNI = 0xb - NTF_USE = 0x1 - NTF_SELF = 0x2 - NTF_MASTER = 0x4 - NTF_PROXY = 0x8 - NTF_EXT_LEARNED = 0x10 - NTF_OFFLOADED = 0x20 - NTF_ROUTER = 0x80 - NUD_INCOMPLETE = 0x1 - NUD_REACHABLE = 0x2 - NUD_STALE = 0x4 - NUD_DELAY = 0x8 - NUD_PROBE = 0x10 - NUD_FAILED = 0x20 - NUD_NOARP = 0x40 - NUD_PERMANENT = 0x80 - NUD_NONE = 0x0 - IFA_UNSPEC = 0x0 - IFA_ADDRESS = 0x1 - IFA_LOCAL = 0x2 - IFA_LABEL = 0x3 - IFA_BROADCAST = 0x4 - IFA_ANYCAST = 0x5 - IFA_CACHEINFO = 0x6 - IFA_MULTICAST = 0x7 - IFA_FLAGS = 0x8 - IFA_RT_PRIORITY = 0x9 - IFA_TARGET_NETNSID = 0xa - IFAL_LABEL = 0x2 - IFAL_ADDRESS = 0x1 - RT_SCOPE_UNIVERSE = 0x0 - RT_SCOPE_SITE = 0xc8 - RT_SCOPE_LINK = 0xfd - RT_SCOPE_HOST = 0xfe - RT_SCOPE_NOWHERE = 0xff - RT_TABLE_UNSPEC = 0x0 - RT_TABLE_COMPAT = 0xfc - RT_TABLE_DEFAULT = 0xfd - RT_TABLE_MAIN = 0xfe - RT_TABLE_LOCAL = 0xff - RT_TABLE_MAX = 0xffffffff - RTA_UNSPEC = 0x0 - RTA_DST = 0x1 - RTA_SRC = 0x2 - RTA_IIF = 0x3 - RTA_OIF = 0x4 - RTA_GATEWAY = 0x5 - RTA_PRIORITY = 0x6 - RTA_PREFSRC = 0x7 - RTA_METRICS = 0x8 - RTA_MULTIPATH = 0x9 - RTA_FLOW = 0xb - RTA_CACHEINFO = 0xc - RTA_TABLE = 0xf - RTA_MARK = 0x10 - RTA_MFC_STATS = 0x11 - RTA_VIA = 0x12 - RTA_NEWDST = 0x13 - RTA_PREF = 0x14 - RTA_ENCAP_TYPE = 0x15 - RTA_ENCAP = 0x16 - RTA_EXPIRES = 0x17 - RTA_PAD = 0x18 - RTA_UID = 0x19 - RTA_TTL_PROPAGATE = 0x1a - RTA_IP_PROTO = 0x1b - RTA_SPORT = 0x1c - RTA_DPORT = 0x1d - RTN_UNSPEC = 0x0 - RTN_UNICAST = 0x1 - RTN_LOCAL = 0x2 - RTN_BROADCAST = 0x3 - RTN_ANYCAST = 0x4 - RTN_MULTICAST = 0x5 - RTN_BLACKHOLE = 0x6 - RTN_UNREACHABLE = 0x7 - RTN_PROHIBIT = 0x8 - RTN_THROW = 0x9 - RTN_NAT = 0xa - RTN_XRESOLVE = 0xb - SizeofNlMsghdr = 0x10 - SizeofNlMsgerr = 0x14 - SizeofRtGenmsg = 0x1 - SizeofNlAttr = 0x4 - SizeofRtAttr = 0x4 - SizeofIfInfomsg = 0x10 - SizeofIfAddrmsg = 0x8 - SizeofIfAddrlblmsg = 0xc - SizeofIfaCacheinfo = 0x10 - SizeofRtMsg = 0xc - SizeofRtNexthop = 0x8 - SizeofNdUseroptmsg = 0x10 - SizeofNdMsg = 0xc + NDA_UNSPEC = 0x0 + NDA_DST = 0x1 + NDA_LLADDR = 0x2 + NDA_CACHEINFO = 0x3 + NDA_PROBES = 0x4 + NDA_VLAN = 0x5 + NDA_PORT = 0x6 + NDA_VNI = 0x7 + NDA_IFINDEX = 0x8 + NDA_MASTER = 0x9 + NDA_LINK_NETNSID = 0xa + NDA_SRC_VNI = 0xb + NTF_USE = 0x1 + NTF_SELF = 0x2 + NTF_MASTER = 0x4 + NTF_PROXY = 0x8 + NTF_EXT_LEARNED = 0x10 + NTF_OFFLOADED = 0x20 + NTF_ROUTER = 0x80 + NUD_INCOMPLETE = 0x1 + NUD_REACHABLE = 0x2 + NUD_STALE = 0x4 + NUD_DELAY = 0x8 + NUD_PROBE = 0x10 + NUD_FAILED = 0x20 + NUD_NOARP = 0x40 + NUD_PERMANENT = 0x80 + NUD_NONE = 0x0 + IFA_UNSPEC = 0x0 + IFA_ADDRESS = 0x1 + IFA_LOCAL = 0x2 + IFA_LABEL = 0x3 + IFA_BROADCAST = 0x4 + IFA_ANYCAST = 0x5 + IFA_CACHEINFO = 0x6 + IFA_MULTICAST = 0x7 + IFA_FLAGS = 0x8 + IFA_RT_PRIORITY = 0x9 + IFA_TARGET_NETNSID = 0xa + IFAL_LABEL = 0x2 + IFAL_ADDRESS = 0x1 + RT_SCOPE_UNIVERSE = 0x0 + RT_SCOPE_SITE = 0xc8 + RT_SCOPE_LINK = 0xfd + RT_SCOPE_HOST = 0xfe + RT_SCOPE_NOWHERE = 0xff + RT_TABLE_UNSPEC = 0x0 + RT_TABLE_COMPAT = 0xfc + RT_TABLE_DEFAULT = 0xfd + RT_TABLE_MAIN = 0xfe + RT_TABLE_LOCAL = 0xff + RT_TABLE_MAX = 0xffffffff + RTA_UNSPEC = 0x0 + RTA_DST = 0x1 + RTA_SRC = 0x2 + RTA_IIF = 0x3 + RTA_OIF = 0x4 + RTA_GATEWAY = 0x5 + RTA_PRIORITY = 0x6 + RTA_PREFSRC = 0x7 + RTA_METRICS = 0x8 + RTA_MULTIPATH = 0x9 + RTA_FLOW = 0xb + RTA_CACHEINFO = 0xc + RTA_TABLE = 0xf + RTA_MARK = 0x10 + RTA_MFC_STATS = 0x11 + RTA_VIA = 0x12 + RTA_NEWDST = 0x13 + RTA_PREF = 0x14 + RTA_ENCAP_TYPE = 0x15 + RTA_ENCAP = 0x16 + RTA_EXPIRES = 0x17 + RTA_PAD = 0x18 + RTA_UID = 0x19 + RTA_TTL_PROPAGATE = 0x1a + RTA_IP_PROTO = 0x1b + RTA_SPORT = 0x1c + RTA_DPORT = 0x1d + RTN_UNSPEC = 0x0 + RTN_UNICAST = 0x1 + RTN_LOCAL = 0x2 + RTN_BROADCAST = 0x3 + RTN_ANYCAST = 0x4 + RTN_MULTICAST = 0x5 + RTN_BLACKHOLE = 0x6 + RTN_UNREACHABLE = 0x7 + RTN_PROHIBIT = 0x8 + RTN_THROW = 0x9 + RTN_NAT = 0xa + RTN_XRESOLVE = 0xb + PREFIX_UNSPEC = 0x0 + PREFIX_ADDRESS = 0x1 + PREFIX_CACHEINFO = 0x2 + SizeofNlMsghdr = 0x10 + SizeofNlMsgerr = 0x14 + SizeofRtGenmsg = 0x1 + SizeofNlAttr = 0x4 + SizeofRtAttr = 0x4 + SizeofIfInfomsg = 0x10 + SizeofPrefixmsg = 0xc + SizeofPrefixCacheinfo = 0x8 + SizeofIfAddrmsg = 0x8 + SizeofIfAddrlblmsg = 0xc + SizeofIfaCacheinfo = 0x10 + SizeofRtMsg = 0xc + SizeofRtNexthop = 0x8 + SizeofNdUseroptmsg = 0x10 + SizeofNdMsg = 0xc ) type NlMsghdr struct { @@ -735,6 +740,22 @@ type IfInfomsg struct { Change uint32 } +type Prefixmsg struct { + Family uint8 + Pad1 uint8 + Pad2 uint16 + Ifindex int32 + Type uint8 + Len uint8 + Flags uint8 + Pad3 uint8 +} + +type PrefixCacheinfo struct { + Preferred_time uint32 + Valid_time uint32 +} + type IfAddrmsg struct { Family uint8 Prefixlen uint8 diff --git a/vendor/golang.org/x/sys/windows/aliases.go b/vendor/golang.org/x/sys/windows/aliases.go index 16f9056..9631796 100644 --- a/vendor/golang.org/x/sys/windows/aliases.go +++ b/vendor/golang.org/x/sys/windows/aliases.go @@ -8,5 +8,6 @@ package windows import "syscall" +type Signal = syscall.Signal type Errno = syscall.Errno type SysProcAttr = syscall.SysProcAttr diff --git a/vendor/golang.org/x/sys/windows/dll_windows.go b/vendor/golang.org/x/sys/windows/dll_windows.go index 3ca814f..1157b06 100644 --- a/vendor/golang.org/x/sys/windows/dll_windows.go +++ b/vendor/golang.org/x/sys/windows/dll_windows.go @@ -163,42 +163,7 @@ func (p *Proc) Addr() uintptr { // (according to the semantics of the specific function being called) before consulting // the error. The error will be guaranteed to contain windows.Errno. func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) { - switch len(a) { - case 0: - return syscall.Syscall(p.Addr(), uintptr(len(a)), 0, 0, 0) - case 1: - return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], 0, 0) - case 2: - return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], 0) - case 3: - return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], a[2]) - case 4: - return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], 0, 0) - case 5: - return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], 0) - case 6: - return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5]) - case 7: - return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], 0, 0) - case 8: - return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], 0) - case 9: - return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]) - case 10: - return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], 0, 0) - case 11: - return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], 0) - case 12: - return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11]) - case 13: - return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], 0, 0) - case 14: - return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], 0) - case 15: - return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14]) - default: - panic("Call " + p.Name + " with too many arguments " + itoa(len(a)) + ".") - } + return syscall.SyscallN(p.Addr(), a...) } // A LazyDLL implements access to a single DLL. diff --git a/vendor/golang.org/x/sys/windows/security_windows.go b/vendor/golang.org/x/sys/windows/security_windows.go index a8b0364..6c955ce 100644 --- a/vendor/golang.org/x/sys/windows/security_windows.go +++ b/vendor/golang.org/x/sys/windows/security_windows.go @@ -1438,13 +1438,17 @@ func GetSecurityInfo(handle Handle, objectType SE_OBJECT_TYPE, securityInformati } // GetNamedSecurityInfo queries the security information for a given named object and returns the self-relative security -// descriptor result on the Go heap. +// descriptor result on the Go heap. The security descriptor might be nil, even when err is nil, if the object exists +// but has no security descriptor. func GetNamedSecurityInfo(objectName string, objectType SE_OBJECT_TYPE, securityInformation SECURITY_INFORMATION) (sd *SECURITY_DESCRIPTOR, err error) { var winHeapSD *SECURITY_DESCRIPTOR err = getNamedSecurityInfo(objectName, objectType, securityInformation, nil, nil, nil, nil, &winHeapSD) if err != nil { return } + if winHeapSD == nil { + return nil, nil + } defer LocalFree(Handle(unsafe.Pointer(winHeapSD))) return winHeapSD.copySelfRelativeSecurityDescriptor(), nil } diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 738a9f2..d766436 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -1490,20 +1490,6 @@ func Getgid() (gid int) { return -1 } func Getegid() (egid int) { return -1 } func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS } -type Signal int - -func (s Signal) Signal() {} - -func (s Signal) String() string { - if 0 <= s && int(s) < len(signals) { - str := signals[s] - if str != "" { - return str - } - } - return "signal " + itoa(int(s)) -} - func LoadCreateSymbolicLink() error { return procCreateSymbolicLinkW.Find() } diff --git a/vendor/modules.txt b/vendor/modules.txt index 1a1e4b4..de6f93a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -80,19 +80,20 @@ github.com/go-git/gcfg github.com/go-git/gcfg/scanner github.com/go-git/gcfg/token github.com/go-git/gcfg/types -# github.com/go-git/go-billy/v5 v5.7.0 -## explicit; go 1.23.0 +# github.com/go-git/go-billy/v5 v5.9.0 +## explicit; go 1.25.0 github.com/go-git/go-billy/v5 github.com/go-git/go-billy/v5/helper/chroot github.com/go-git/go-billy/v5/helper/polyfill github.com/go-git/go-billy/v5/memfs github.com/go-git/go-billy/v5/osfs github.com/go-git/go-billy/v5/util -# github.com/go-git/go-git/v5 v5.16.5 -## explicit; go 1.24.0 +# github.com/go-git/go-git/v5 v5.19.1 +## explicit; go 1.25.0 github.com/go-git/go-git/v5 github.com/go-git/go-git/v5/config github.com/go-git/go-git/v5/internal/path_util +github.com/go-git/go-git/v5/internal/pathutil github.com/go-git/go-git/v5/internal/revision github.com/go-git/go-git/v5/internal/url github.com/go-git/go-git/v5/plumbing @@ -157,7 +158,7 @@ github.com/mattn/go-isatty # github.com/mitchellh/go-homedir v1.1.0 ## explicit github.com/mitchellh/go-homedir -# github.com/pjbgf/sha1cd v0.5.0 +# github.com/pjbgf/sha1cd v0.6.0 ## explicit; go 1.22 github.com/pjbgf/sha1cd github.com/pjbgf/sha1cd/internal @@ -177,8 +178,8 @@ github.com/spf13/pflag # github.com/xanzy/ssh-agent v0.3.3 ## explicit; go 1.16 github.com/xanzy/ssh-agent -# golang.org/x/crypto v0.48.0 -## explicit; go 1.24.0 +# golang.org/x/crypto v0.50.0 +## explicit; go 1.25.0 golang.org/x/crypto/argon2 golang.org/x/crypto/blake2b golang.org/x/crypto/blowfish @@ -195,13 +196,13 @@ golang.org/x/crypto/ssh golang.org/x/crypto/ssh/agent golang.org/x/crypto/ssh/internal/bcrypt_pbkdf golang.org/x/crypto/ssh/knownhosts -# golang.org/x/net v0.50.0 -## explicit; go 1.24.0 +# golang.org/x/net v0.53.0 +## explicit; go 1.25.0 golang.org/x/net/context golang.org/x/net/internal/socks golang.org/x/net/proxy -# golang.org/x/sys v0.41.0 -## explicit; go 1.24.0 +# golang.org/x/sys v0.43.0 +## explicit; go 1.25.0 golang.org/x/sys/cpu golang.org/x/sys/execabs golang.org/x/sys/unix