Skip to content

Commit f94a0d3

Browse files
committed
Fast-fail on incorrect MaxSpanID
If a MaxSpanID is incorrect on a malformed zTOC, we will not find any error until the spans start getting pulled and verified from verifySpanContents. This will usually happen after the FUSE setup is finished. We should fail much faster since we already have the info we need to make this determination beforehand. This should cover any failures in accessing the data in verifySpanContents, but figured it can't hurt to add an extra sanity check in the func to be extra sure. This change required reworking the SpanManager's New function to handle fast fails, so the commit also covers all the changes needed for that. Signed-off-by: David Son <davbson@amazon.com>
1 parent 1619a75 commit f94a0d3

7 files changed

Lines changed: 97 additions & 23 deletions

File tree

fs/backgroundfetcher/background_fetcher_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/awslabs/soci-snapshotter/util/testutil"
2929
"github.com/awslabs/soci-snapshotter/ztoc"
3030
"github.com/opencontainers/go-digest"
31+
"github.com/stretchr/testify/assert"
3132
)
3233

3334
func withPauser(p pauser) Option {
@@ -117,7 +118,8 @@ func TestBackgroundFetcherRun(t *testing.T) {
117118
t.Fatalf("error building span manager and section reader: %v", err)
118119
}
119120
cache := &countingCache{}
120-
sm := spanmanager.New(ztoc, sr, cache, 0)
121+
sm, err := spanmanager.New(ztoc, sr, cache, 0)
122+
assert.Nil(t, err)
121123
infos = append(infos, testInfo{sm, cache, ztoc})
122124
}
123125

fs/backgroundfetcher/resolver_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/awslabs/soci-snapshotter/util/testutil"
2727
"github.com/awslabs/soci-snapshotter/ztoc"
2828
"github.com/opencontainers/go-digest"
29+
"github.com/stretchr/testify/assert"
2930
)
3031

3132
func TestSequentialResolver(t *testing.T) {
@@ -50,7 +51,8 @@ func TestSequentialResolver(t *testing.T) {
5051
if err != nil {
5152
t.Fatalf("error build ztoc and section reader: %v", err)
5253
}
53-
sm := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
54+
sm, err := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
55+
assert.Nil(t, err)
5456
sequentialResolver := NewSequentialResolver(digest.FromString("test"), sm)
5557

5658
var resolvedSpans []int

fs/layer/layer.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,10 @@ func (r *Resolver) Resolve(ctx context.Context, hosts []docker.RegistryHost, ref
348348
ztoc.TOC.FileMetadata = nil
349349
log.G(ctx).Debugf("[Resolver.Resolve]Initialized metadata store for layer sha=%v", desc.Digest)
350350

351-
spanManager := spanmanager.New(ztoc, sr, spanCache, r.config.BlobConfig.MaxSpanVerificationRetries, cache.Direct())
351+
spanManager, err := spanmanager.New(ztoc, sr, spanCache, r.config.BlobConfig.MaxSpanVerificationRetries, cache.Direct())
352+
if err != nil {
353+
return nil, fmt.Errorf("error creating span manager: %v", err)
354+
}
352355
var bgLayerResolver backgroundfetcher.Resolver
353356
if r.bgFetcher != nil {
354357
bgLayerResolver = backgroundfetcher.NewSequentialResolver(desc.Digest, spanManager)

fs/layer/util_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import (
6767
"github.com/hanwen/go-fuse/v2/fuse"
6868
digest "github.com/opencontainers/go-digest"
6969
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
70+
"github.com/stretchr/testify/assert"
7071
"golang.org/x/sys/unix"
7172
)
7273

@@ -172,7 +173,8 @@ func makeNodeReader(t *testing.T, contents []byte, spanSize int64, factory metad
172173
if err != nil {
173174
t.Fatalf("failed to create reader: %v", err)
174175
}
175-
spanManager := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
176+
spanManager, err := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
177+
assert.Nil(t, err)
176178
r, err := reader.NewReader(mr, digest.FromString(""), spanManager, false)
177179
if err != nil {
178180
mr.Close()
@@ -333,7 +335,8 @@ func testExistenceWithOpaque(t *testing.T, factory metadata.Store, opaque Overla
333335
t.Fatalf("failed to create reader: %v", err)
334336
}
335337
defer mr.Close()
336-
spanManager := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
338+
spanManager, err := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
339+
assert.Nil(t, err)
337340
r, err := reader.NewReader(mr, digest.FromString(""), spanManager, false)
338341
if err != nil {
339342
t.Fatalf("failed to make new reader: %v", err)
@@ -595,7 +598,7 @@ func hasEntry(t *testing.T, name string, ents fusefs.DirStream) (fuse.DirEntry,
595598
return fuse.DirEntry{}, false
596599
}
597600

598-
func hasStateFile(t *testing.T, id string) check {
601+
func hasStateFile(_ *testing.T, id string) check {
599602
return func(t *testing.T, root *node) {
600603
ctx, cancel := context.WithCancel(context.Background())
601604
defer cancel()
@@ -774,7 +777,8 @@ func testStatfs(t *testing.T, factory metadata.Store) {
774777
}
775778
defer mr.Close()
776779

777-
spanManager := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
780+
spanManager, err := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
781+
assert.Nil(t, err)
778782
r, err := reader.NewReader(mr, digest.FromString(""), spanManager, false)
779783
if err != nil {
780784
t.Fatalf("failed to create reader: %v", err)

fs/reader/reader_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import (
5252
"github.com/awslabs/soci-snapshotter/util/testutil"
5353
"github.com/awslabs/soci-snapshotter/ztoc"
5454
digest "github.com/opencontainers/go-digest"
55+
"github.com/stretchr/testify/assert"
5556
)
5657

5758
const (
@@ -161,7 +162,8 @@ func makeFile(t *testing.T, contents []byte, prefix string, factory metadata.Sto
161162
if err != nil {
162163
t.Fatalf("failed to create reader: %v", err)
163164
}
164-
spanManager := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
165+
spanManager, err := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
166+
assert.Nil(t, err)
165167
r, err := NewReader(mr, digest.FromString(""), spanManager, false)
166168
if err != nil {
167169
mr.Close()
@@ -216,7 +218,8 @@ func testFailReader(t *testing.T, factory metadata.Store) {
216218
if !found {
217219
t.Fatalf("free ID not found")
218220
}
219-
spanManager := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
221+
spanManager, err := spanmanager.New(ztoc, sr, cache.NewMemoryCache(), 0)
222+
assert.Nil(t, err)
220223
r, err := NewReader(mr, digest.FromString(""), spanManager, false)
221224
if err != nil {
222225
mr.Close()

fs/span-manager/span_manager.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
ErrSpanNotAvailable = errors.New("span not available in cache")
4040
ErrIncorrectSpanDigest = errors.New("span digests do not match")
4141
ErrExceedMaxSpan = errors.New("span id larger than max span id")
42+
ErrIncorrectMaxSpanID = errors.New("given max span ID differs from calculated max span ID")
4243
)
4344

4445
// SpanManager fetches and caches spans of a given layer.
@@ -69,22 +70,30 @@ type spanInfo struct {
6970
// New creates a SpanManager with given ztoc and content reader, and builds all
7071
// spans based on the ztoc.
7172

72-
// TODO: return errors/nil objects on failure
73-
func New(ztoc *ztoc.Ztoc, r *io.SectionReader, cache cache.BlobCache, retries int, cacheOpt ...cache.Option) *SpanManager {
73+
func New(ztoc *ztoc.Ztoc, r *io.SectionReader, cache cache.BlobCache, retries int, cacheOpt ...cache.Option) (*SpanManager, error) {
7474
index, err := ztoc.Zinfo()
7575
if err != nil {
76-
return nil
76+
return nil, err
7777
}
7878
ztoc.Checkpoints = nil
7979

80+
// Check zTOC contents against generated Zinfo
81+
maxSpanID := index.MaxSpanID()
82+
if ztoc.MaxSpanID != maxSpanID {
83+
return nil, fmt.Errorf("%w (zTOC: %d, checkpoints: %d)", ErrIncorrectMaxSpanID, maxSpanID, ztoc.MaxSpanID)
84+
}
85+
if int32(len(ztoc.SpanDigests)) != int32(maxSpanID+1) {
86+
return nil, fmt.Errorf("%w (len span digests: %d, MaxSpanID+1: %d", ErrIncorrectMaxSpanID, len(ztoc.SpanDigests), maxSpanID+1)
87+
}
88+
8089
// We don't need the header anywhere else, but we want to ensure we read every byte from the layer.
8190
// This also serves as a sanity check to make sure the file isn't corrupted.
8291
b := make([]byte, index.StartCompressedOffset(0))
8392
if _, err := r.Read(b); err != nil {
84-
log.L.Errorf("unable to read ztoc header: %v", err)
93+
return nil, fmt.Errorf("unable to read ztoc header: %w", err)
8594
}
8695
if err := index.VerifyHeader(bytes.NewReader(b)); err != nil {
87-
log.L.Errorf("unable to verify %v header: %v", ztoc.CompressionAlgorithm, err)
96+
return nil, fmt.Errorf("unable to verify %v header: %w", ztoc.CompressionAlgorithm, err)
8897
}
8998

9099
spans := make([]*span, ztoc.MaxSpanID+1)
@@ -105,7 +114,7 @@ func New(ztoc *ztoc.Ztoc, r *io.SectionReader, cache cache.BlobCache, retries in
105114
m.Close()
106115
})
107116

108-
return m
117+
return m, nil
109118
}
110119

111120
func (m *SpanManager) buildAllSpans() {
@@ -452,6 +461,10 @@ func (m *SpanManager) getSpanFromCache(spanID compression.SpanID, offset, size c
452461
// verifySpanContents calculates span digest from its compressed bytes, and compare
453462
// with the digest stored in ztoc.
454463
func (m *SpanManager) verifySpanContents(compressedData []byte, spanID compression.SpanID) error {
464+
if int32(spanID) >= int32(len(m.ztoc.SpanDigests)) {
465+
return ErrExceedMaxSpan
466+
}
467+
455468
actual := digest.FromBytes(compressedData)
456469
expected := m.ztoc.SpanDigests[spanID]
457470
if actual != expected {

fs/span-manager/span_manager_test.go

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/awslabs/soci-snapshotter/util/testutil"
2929
"github.com/awslabs/soci-snapshotter/ztoc"
3030
"github.com/awslabs/soci-snapshotter/ztoc/compression"
31+
"github.com/stretchr/testify/assert"
3132
)
3233

3334
func TestSpanManager(t *testing.T) {
@@ -48,14 +49,38 @@ func TestSpanManager(t *testing.T) {
4849
maxSpans: 100,
4950
},
5051
{
51-
name: "span digest verification fails",
52+
name: "bad MaxSpanID",
53+
maxSpans: 5,
54+
expectedError: ErrIncorrectMaxSpanID,
55+
},
56+
{
57+
name: "header verification fails",
5258
maxSpans: 100,
5359
sectionReader: io.NewSectionReader(readerFn(func(b []byte, _ int64) (int, error) {
5460
sz := compression.Offset(len(b))
5561
r := testutil.NewTestRand(t)
5662
copy(b, r.RandomByteData(int64(sz))) // populate with garbage data
5763
return len(b), nil
5864
}), 0, 10000000),
65+
expectedError: gzip.ErrHeader,
66+
},
67+
{
68+
name: "span digest verification fails",
69+
maxSpans: 10,
70+
sectionReader: io.NewSectionReader(readerFn(func(b []byte, _ int64) (int, error) {
71+
var r bytes.Buffer
72+
w := gzip.NewWriter(&r)
73+
w.Write([]byte("failing digest verification"))
74+
w.Close()
75+
76+
gz, err := io.ReadAll(&r)
77+
if err != nil {
78+
t.Fatalf("error creating SectionReader: %v", err)
79+
}
80+
81+
copy(b, gz)
82+
return len(b), nil
83+
}), 0, int64(spanSize)*10),
5984
expectedError: ErrIncorrectSpanDigest,
6085
},
6186
}
@@ -84,13 +109,20 @@ func TestSpanManager(t *testing.T) {
84109
return
85110
}
86111

112+
if tc.expectedError == ErrIncorrectMaxSpanID {
113+
toc.MaxSpanID += 1
114+
}
115+
87116
if tc.sectionReader != nil {
88117
r = tc.sectionReader
89118
}
90119

91120
cache := cache.NewMemoryCache()
92121
defer cache.Close()
93-
m := New(toc, r, cache, 0)
122+
m, err := New(toc, r, cache, 0)
123+
if err != nil {
124+
return
125+
}
94126

95127
// Test GetContent
96128
fileContentFromSpans, err := getFileContentFromSpans(m, toc, fileName)
@@ -133,7 +165,8 @@ func TestSpanManagerCache(t *testing.T) {
133165
}
134166
cache := cache.NewMemoryCache()
135167
defer cache.Close()
136-
m := New(toc, r, cache, 0)
168+
m, err := New(toc, r, cache, 0)
169+
assert.Nil(t, err)
137170
spanID := 0
138171
err = m.resolveSpan(compression.SpanID(spanID))
139172
if err != nil {
@@ -188,7 +221,8 @@ func TestStateTransition(t *testing.T) {
188221
}
189222
cache := cache.NewMemoryCache()
190223
defer cache.Close()
191-
m := New(toc, r, cache, 0)
224+
m, err := New(toc, r, cache, 0)
225+
assert.Nil(t, err)
192226

193227
// check initial span states
194228
for i := uint32(0); i <= uint32(toc.MaxSpanID); i++ {
@@ -347,16 +381,19 @@ func TestSpanManagerRetries(t *testing.T) {
347381
for _, tc := range testCases {
348382
t.Run(tc.name, func(t *testing.T) {
349383
r := testutil.NewTestRand(t)
384+
randStr := string(r.RandomByteData(10000000))
385+
350386
entries := []testutil.TarEntry{
351-
testutil.File("test", string(r.RandomByteData(10000000))),
387+
testutil.File("test", randStr),
352388
}
353389
ztoc, sr, err := ztoc.BuildZtocReader(t, entries, gzip.DefaultCompression, 100000)
354390
if err != nil {
355391
t.Fatal(err)
356392
}
357-
rdr := &retryableReaderAt{inner: sr, maxErrors: tc.readerErrors}
393+
rdr := newRetryableReaderAt(sr, tc.readerErrors)
358394
sr = io.NewSectionReader(rdr, 0, 10000000)
359-
sm := New(ztoc, sr, cache.NewMemoryCache(), tc.spanManagerRetries)
395+
sm, err := New(ztoc, sr, cache.NewMemoryCache(), tc.spanManagerRetries)
396+
assert.Nil(t, err)
360397

361398
for i := 0; i < int(ztoc.MaxSpanID); i++ {
362399
rdr.errCount = 0
@@ -381,14 +418,24 @@ type retryableReaderAt struct {
381418
maxErrors int
382419
}
383420

421+
func newRetryableReaderAt(inner *io.SectionReader, maxErrors int) *retryableReaderAt {
422+
return &retryableReaderAt{
423+
inner: inner,
424+
maxErrors: maxErrors,
425+
errCount: -1, // First read needs to succeed to create spanmanager
426+
}
427+
}
428+
384429
func (r *retryableReaderAt) ReadAt(buf []byte, off int64) (int, error) {
385430
n, err := r.inner.ReadAt(buf, off)
386431
if (err != nil && err != io.EOF) || n != len(buf) {
387432
return n, err
388433
}
389434
if r.errCount < r.maxErrors {
390435
r.errCount++
391-
buf[0] = buf[0] ^ 0xff
436+
if r.errCount > 0 {
437+
buf[0] = buf[0] ^ 0xff
438+
}
392439
}
393440
return n, err
394441
}

0 commit comments

Comments
 (0)