Skip to content

Commit 8dad47a

Browse files
authored
Fix race in LFS ContentStore.Put(...) (#14895) (#14913)
Backport #14895 Continuing on from #14888 The previous implementation has race whereby an incomplete upload or hash mismatch upload can end up in the ContentStore. This PR moves the validation into the reader so that if there is a hash error or size mismatch the reader will return with an error instead of an io.EOF causing the storage to abort the storage. Signed-off-by: Andrew Thornton <[email protected]>
1 parent 8e79298 commit 8dad47a

File tree

1 file changed

+51
-11
lines changed

1 file changed

+51
-11
lines changed

modules/lfs/content_store.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/hex"
1010
"errors"
1111
"fmt"
12+
"hash"
1213
"io"
1314
"os"
1415

@@ -66,30 +67,27 @@ func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadC
6667

6768
// Put takes a Meta object and an io.Reader and writes the content to the store.
6869
func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error {
69-
hash := sha256.New()
70-
rd := io.TeeReader(r, hash)
7170
p := meta.RelativePath()
72-
written, err := s.Save(p, rd)
71+
72+
// Wrap the provided reader with an inline hashing and size checker
73+
wrappedRd := newHashingReader(meta.Size, meta.Oid, r)
74+
75+
// now pass the wrapped reader to Save - if there is a size mismatch or hash mismatch then
76+
// the errors returned by the newHashingReader should percolate up to here
77+
written, err := s.Save(p, wrappedRd)
7378
if err != nil {
7479
log.Error("Whilst putting LFS OID[%s]: Failed to copy to tmpPath: %s Error: %v", meta.Oid, p, err)
7580
return err
7681
}
7782

83+
// This shouldn't happen but it is sensible to test
7884
if written != meta.Size {
7985
if err := s.Delete(p); err != nil {
8086
log.Error("Cleaning the LFS OID[%s] failed: %v", meta.Oid, err)
8187
}
8288
return errSizeMismatch
8389
}
8490

85-
shaStr := hex.EncodeToString(hash.Sum(nil))
86-
if shaStr != meta.Oid {
87-
if err := s.Delete(p); err != nil {
88-
log.Error("Cleaning the LFS OID[%s] failed: %v", meta.Oid, err)
89-
}
90-
return errHashMismatch
91-
}
92-
9391
return nil
9492
}
9593

@@ -118,3 +116,45 @@ func (s *ContentStore) Verify(meta *models.LFSMetaObject) (bool, error) {
118116

119117
return true, nil
120118
}
119+
120+
type hashingReader struct {
121+
internal io.Reader
122+
currentSize int64
123+
expectedSize int64
124+
hash hash.Hash
125+
expectedHash string
126+
}
127+
128+
func (r *hashingReader) Read(b []byte) (int, error) {
129+
n, err := r.internal.Read(b)
130+
131+
if n > 0 {
132+
r.currentSize += int64(n)
133+
wn, werr := r.hash.Write(b[:n])
134+
if wn != n || werr != nil {
135+
return n, werr
136+
}
137+
}
138+
139+
if err != nil && err == io.EOF {
140+
if r.currentSize != r.expectedSize {
141+
return n, errSizeMismatch
142+
}
143+
144+
shaStr := hex.EncodeToString(r.hash.Sum(nil))
145+
if shaStr != r.expectedHash {
146+
return n, errHashMismatch
147+
}
148+
}
149+
150+
return n, err
151+
}
152+
153+
func newHashingReader(expectedSize int64, expectedHash string, reader io.Reader) *hashingReader {
154+
return &hashingReader{
155+
internal: reader,
156+
expectedSize: expectedSize,
157+
expectedHash: expectedHash,
158+
hash: sha256.New(),
159+
}
160+
}

0 commit comments

Comments
 (0)