1// Copyright 2016 Keybase Inc. All rights reserved.
2// Use of this source code is governed by a BSD
3// license that can be found in the LICENSE file.
4
5package libkbfs
6
7import (
8	"errors"
9	"fmt"
10	"reflect"
11	"time"
12
13	"github.com/keybase/client/go/kbfs/data"
14	"github.com/keybase/client/go/kbfs/kbfsblock"
15	"github.com/keybase/client/go/kbfs/kbfsmd"
16	"github.com/keybase/client/go/kbfs/kbfssync"
17	"github.com/keybase/client/go/kbfs/libkey"
18	"github.com/keybase/client/go/kbfs/tlf"
19	"github.com/keybase/client/go/logger"
20	"github.com/keybase/client/go/protocol/keybase1"
21	"golang.org/x/net/context"
22)
23
24// StateChecker verifies that the server-side state for KBFS is
25// consistent.  Useful mostly for testing because it isn't scalable
26// and loads all the state in memory.
27type StateChecker struct {
28	config Config
29	log    logger.Logger
30}
31
32// NewStateChecker returns a new StateChecker instance.
33func NewStateChecker(config Config) *StateChecker {
34	return &StateChecker{config, config.MakeLogger("")}
35}
36
37// findAllFileBlocks adds all file blocks found under this block to
38// the blockSizes map, if the given path represents an indirect block.
39func (sc *StateChecker) findAllFileBlocks(ctx context.Context,
40	lState *kbfssync.LockState, ops *folderBranchOps, kmd libkey.KeyMetadata,
41	file data.Path, blockSizes map[data.BlockPointer]uint32) error {
42	infos, err := ops.blocks.GetIndirectFileBlockInfos(ctx, lState, kmd, file)
43	if err != nil {
44		return err
45	}
46
47	for _, info := range infos {
48		blockSizes[info.BlockPointer] = info.EncodedSize
49	}
50	return nil
51}
52
53// findAllDirBlocks adds all dir blocks found under this block to the
54// blockSizes map, if the given path represents an indirect block.
55func (sc *StateChecker) findAllDirBlocks(ctx context.Context,
56	lState *kbfssync.LockState, ops *folderBranchOps, kmd libkey.KeyMetadata,
57	dir data.Path, blockSizes map[data.BlockPointer]uint32) error {
58	infos, err := ops.blocks.GetIndirectDirBlockInfos(ctx, lState, kmd, dir)
59	if err != nil {
60		return err
61	}
62
63	for _, info := range infos {
64		blockSizes[info.BlockPointer] = info.EncodedSize
65	}
66	return nil
67}
68
69// findAllBlocksInPath adds all blocks found within this directory to
70// the blockSizes map, and then recursively checks all
71// subdirectories.
72func (sc *StateChecker) findAllBlocksInPath(ctx context.Context,
73	lState *kbfssync.LockState, ops *folderBranchOps, kmd libkey.KeyMetadata,
74	dir data.Path, blockSizes map[data.BlockPointer]uint32) error {
75	children, err := ops.blocks.GetEntries(ctx, lState, kmd, dir)
76	if err != nil {
77		return err
78	}
79
80	err = sc.findAllDirBlocks(ctx, lState, ops, kmd, dir, blockSizes)
81	if err != nil {
82		return err
83	}
84
85	for name, de := range children {
86		if de.Type == data.Sym {
87			continue
88		}
89
90		blockSizes[de.BlockPointer] = de.EncodedSize
91		p := dir.ChildPath(name, de.BlockPointer, ops.makeObfuscator())
92
93		if de.Type == data.Dir {
94			err := sc.findAllBlocksInPath(ctx, lState, ops, kmd, p, blockSizes)
95			if err != nil {
96				return err
97			}
98		} else {
99			// If it's a file, check to see if it's indirect.
100			err := sc.findAllFileBlocks(ctx, lState, ops, kmd, p, blockSizes)
101			if err != nil {
102				return err
103			}
104		}
105	}
106	return nil
107}
108
109func (sc *StateChecker) getLastGCData(ctx context.Context,
110	tlfID tlf.ID) (time.Time, kbfsmd.Revision) {
111	config, ok := sc.config.(*ConfigLocal)
112	if !ok {
113		return time.Time{}, kbfsmd.RevisionUninitialized
114	}
115
116	var latestTime time.Time
117	var latestRev kbfsmd.Revision
118	for _, c := range *config.allKnownConfigsForTesting {
119		ops := c.KBFSOps().(*KBFSOpsStandard).getOpsIfExists(
120			context.Background(),
121			data.FolderBranch{Tlf: tlfID, Branch: data.MasterBranch})
122		if ops == nil {
123			continue
124		}
125		rt, rev := ops.fbm.getLastQRData()
126		if rt.After(latestTime) && rev > latestRev {
127			latestTime = rt
128			latestRev = rev
129		}
130	}
131	if latestTime.IsZero() {
132		return latestTime, latestRev
133	}
134
135	sc.log.CDebugf(ctx, "Last qr data for TLF %s: revTime=%s, rev=%d",
136		tlfID, latestTime, latestRev)
137	return latestTime.Add(
138		-sc.config.Mode().QuotaReclamationMinUnrefAge()), latestRev
139}
140
141// CheckMergedState verifies that the state for the given tlf is
142// consistent.
143func (sc *StateChecker) CheckMergedState(ctx context.Context, tlfID tlf.ID) error {
144	// Blow away MD cache so we don't have any lingering re-embedded
145	// block changes (otherwise we won't be able to learn their sizes).
146	sc.config.SetMDCache(NewMDCacheStandard(defaultMDCacheCapacity))
147
148	// Fetch all the MD updates for this folder, and use the block
149	// change lists to build up the set of currently referenced blocks.
150	rmds, err := getMergedMDUpdates(ctx, sc.config, tlfID,
151		kbfsmd.RevisionInitial, nil)
152	if err != nil {
153		return err
154	}
155	if len(rmds) == 0 {
156		sc.log.CDebugf(ctx, "No state to check for folder %s", tlfID)
157		return nil
158	}
159
160	lState := makeFBOLockState()
161
162	// Re-embed block changes.
163	kbfsOps, ok := sc.config.KBFSOps().(*KBFSOpsStandard)
164	if !ok {
165		return errors.New("Unexpected KBFSOps type")
166	}
167
168	fb := data.FolderBranch{Tlf: tlfID, Branch: data.MasterBranch}
169	ops := kbfsOps.getOps(context.Background(), fb, FavoritesOpNoChange)
170	lastGCRevisionTime, lastGCRev := sc.getLastGCData(ctx, tlfID)
171
172	// Build the expected block list.
173	expectedLiveBlocks := make(map[data.BlockPointer]bool)
174	expectedRef := uint64(0)
175	expectedMDRef := uint64(0)
176	archivedBlocks := make(map[data.BlockPointer]bool)
177	actualLiveBlocks := make(map[data.BlockPointer]uint32)
178
179	// See what the last GC op revision is.  All unref'd pointers from
180	// that revision or earlier should be deleted from the block
181	// server.
182	gcRevision := kbfsmd.RevisionUninitialized
183	for _, rmd := range rmds {
184		// Don't process copies.
185		if rmd.IsWriterMetadataCopiedSet() {
186			continue
187		}
188
189		for _, op := range rmd.data.Changes.Ops {
190			GCOp, ok := op.(*GCOp)
191			if !ok {
192				continue
193			}
194			gcRevision = GCOp.LatestRev
195		}
196	}
197
198	for _, rmd := range rmds {
199		// Don't process copies.
200		if rmd.IsWriterMetadataCopiedSet() {
201			continue
202		}
203		// Unembedded block changes count towards the MD size.
204		if info := rmd.data.cachedChanges.Info; info.BlockPointer != data.ZeroPtr {
205			sc.log.CDebugf(ctx, "Unembedded block change: %v, %d",
206				info.BlockPointer, info.EncodedSize)
207			actualLiveBlocks[info.BlockPointer] = info.EncodedSize
208
209			// Any child block change pointers?
210			file := data.Path{
211				FolderBranch: data.FolderBranch{
212					Tlf: tlfID, Branch: data.MasterBranch},
213				Path: []data.PathNode{{
214					BlockPointer: info.BlockPointer,
215					Name: data.NewPathPartString(fmt.Sprintf(
216						"<MD with revision %d>", rmd.Revision()), nil),
217				}}}
218			err := sc.findAllFileBlocks(ctx, lState, ops, rmd.ReadOnly(),
219				file, actualLiveBlocks)
220			if err != nil {
221				return err
222			}
223		}
224
225		var hasGCOp bool
226		updated := make(map[data.BlockPointer]bool)
227		for _, op := range rmd.data.Changes.Ops {
228			_, isGCOp := op.(*GCOp)
229			hasGCOp = hasGCOp || isGCOp
230
231			opRefs := make(map[data.BlockPointer]bool)
232			for _, ptr := range op.Refs() {
233				if ptr != data.ZeroPtr {
234					expectedLiveBlocks[ptr] = true
235					opRefs[ptr] = true
236				}
237			}
238			if !isGCOp {
239				for _, ptr := range op.Unrefs() {
240					if updated[ptr] {
241						return fmt.Errorf(
242							"%s already updated in this revision %d",
243							ptr, rmd.Revision())
244					}
245					delete(expectedLiveBlocks, ptr)
246					if ptr != data.ZeroPtr {
247						// If the revision has been garbage-collected,
248						// or if the pointer has been referenced and
249						// unreferenced within the same op (which
250						// indicates a failed and retried sync), the
251						// corresponding block should already be
252						// cleaned up.
253						if rmd.Revision() <= gcRevision || opRefs[ptr] {
254							delete(archivedBlocks, ptr)
255						} else {
256							archivedBlocks[ptr] = true
257						}
258					}
259				}
260			}
261			for _, update := range op.allUpdates() {
262				if update.Ref != update.Unref {
263					updated[update.Unref] = true
264					delete(expectedLiveBlocks, update.Unref)
265				}
266				if update.Unref != data.ZeroPtr && update.Ref != update.Unref {
267					if rmd.Revision() <= gcRevision {
268						delete(archivedBlocks, update.Unref)
269					} else {
270						archivedBlocks[update.Unref] = true
271					}
272				}
273				if update.Ref != data.ZeroPtr && update.Ref != update.Unref {
274					expectedLiveBlocks[update.Ref] = true
275				}
276			}
277		}
278		expectedRef += rmd.RefBytes()
279		expectedRef -= rmd.UnrefBytes()
280		expectedMDRef += rmd.MDRefBytes()
281
282		if len(rmd.data.Changes.Ops) == 1 && hasGCOp {
283			// Don't check GC status for GC revisions
284			continue
285		}
286
287		// Make sure that if this revision should be covered by a GC
288		// op, it is.  Note that this assumes that if QR is ever run,
289		// it will be run completely and not left partially done due
290		// to there being too many pointers to collect in one sweep.
291		mtime := time.Unix(0, rmd.data.Dir.Mtime)
292		if !lastGCRevisionTime.Before(mtime) && rmd.Revision() <= lastGCRev &&
293			rmd.Revision() > gcRevision {
294			return fmt.Errorf("Revision %d happened on or before the last "+
295				"gc time %s rev %d, but was not included in the latest "+
296				"gc op revision %d", rmd.Revision(), lastGCRevisionTime,
297				lastGCRev, gcRevision)
298		}
299	}
300	sc.log.CDebugf(ctx, "Folder %v has %d expected live blocks, "+
301		"total %d bytes (%d MD bytes)", tlfID, len(expectedLiveBlocks),
302		expectedRef, expectedMDRef)
303
304	currMD := rmds[len(rmds)-1]
305	expectedUsage := currMD.DiskUsage()
306	if expectedUsage != expectedRef {
307		return fmt.Errorf("Expected ref bytes %d doesn't match latest disk "+
308			"usage %d", expectedRef, expectedUsage)
309	}
310	expectedMDUsage := currMD.MDDiskUsage()
311	if expectedMDUsage != expectedMDRef {
312		return fmt.Errorf("Expected MD ref bytes %d doesn't match latest disk "+
313			"MD usage %d", expectedMDRef, expectedMDUsage)
314	}
315
316	// Then, using the current MD head, start at the root of the FS
317	// and recursively walk the directory tree to find all the blocks
318	// that are currently accessible.
319	rootNode, _, _, err := ops.getRootNode(ctx)
320	if err != nil {
321		return err
322	}
323	rootPath := ops.nodeCache.PathFromNode(rootNode)
324	if g, e := rootPath.TailPointer(), currMD.data.Dir.BlockPointer; g != e {
325		return fmt.Errorf("Current MD root pointer %v doesn't match root "+
326			"node pointer %v", e, g)
327	}
328	actualLiveBlocks[rootPath.TailPointer()] = currMD.data.Dir.EncodedSize
329	if err := sc.findAllBlocksInPath(ctx, lState, ops, currMD.ReadOnly(),
330		rootPath, actualLiveBlocks); err != nil {
331		return err
332	}
333	sc.log.CDebugf(ctx, "Folder %v has %d actual live blocks",
334		tlfID, len(actualLiveBlocks))
335
336	// Compare the two and see if there are any differences. Don't use
337	// reflect.DeepEqual so we can print out exactly what's wrong.
338	var extraBlocks []data.BlockPointer
339	actualSize := uint64(0)
340	actualMDSize := uint64(0)
341	for ptr, size := range actualLiveBlocks {
342		if ptr.GetBlockType() == keybase1.BlockType_MD {
343			actualMDSize += uint64(size)
344		} else {
345			actualSize += uint64(size)
346		}
347		if !expectedLiveBlocks[ptr] {
348			extraBlocks = append(extraBlocks, ptr)
349		}
350	}
351	if len(extraBlocks) != 0 {
352		sc.log.CWarningf(ctx, "%v: Extra live blocks found: %v",
353			tlfID, extraBlocks)
354		return fmt.Errorf("Folder %v has inconsistent state", tlfID)
355	}
356	var missingBlocks []data.BlockPointer
357	for ptr := range expectedLiveBlocks {
358		if _, ok := actualLiveBlocks[ptr]; !ok {
359			missingBlocks = append(missingBlocks, ptr)
360		}
361	}
362	if len(missingBlocks) != 0 {
363		sc.log.CWarningf(ctx, "%v: Expected live blocks not found: %v",
364			tlfID, missingBlocks)
365		return fmt.Errorf("Folder %v has inconsistent state", tlfID)
366	}
367
368	if actualSize != expectedRef {
369		return fmt.Errorf("Actual size %d doesn't match expected size %d",
370			actualSize, expectedRef)
371	}
372	if actualMDSize != expectedMDRef {
373		return fmt.Errorf("Actual MD size %d doesn't match expected MD size %d",
374			actualMDSize, expectedMDRef)
375	}
376
377	// Check that the set of referenced blocks matches exactly what
378	// the block server knows about.
379	bserverLocal, ok := sc.config.BlockServer().(blockServerLocal)
380	if !ok {
381		if jbs, jok := sc.config.BlockServer().(journalBlockServer); jok {
382			bserverLocal, ok = jbs.BlockServer.(blockServerLocal)
383			if !ok {
384				sc.log.CDebugf(ctx, "Bad block server: %T", jbs.BlockServer)
385			}
386		}
387	}
388	if !ok {
389		return errors.New("StateChecker only works against BlockServerLocal")
390	}
391	bserverKnownBlocks, err := bserverLocal.getAllRefsForTest(ctx, tlfID)
392	if err != nil {
393		return err
394	}
395
396	blockRefsByID := make(map[kbfsblock.ID]blockRefMap)
397	for ptr := range expectedLiveBlocks {
398		if _, ok := blockRefsByID[ptr.ID]; !ok {
399			blockRefsByID[ptr.ID] = make(blockRefMap)
400		}
401		err := blockRefsByID[ptr.ID].put(ptr.Context, liveBlockRef, "")
402		if err != nil {
403			return err
404		}
405	}
406	for ptr := range archivedBlocks {
407		if _, ok := blockRefsByID[ptr.ID]; !ok {
408			blockRefsByID[ptr.ID] = make(blockRefMap)
409		}
410		err := blockRefsByID[ptr.ID].put(ptr.Context, archivedBlockRef, "")
411		if err != nil {
412			return err
413		}
414	}
415
416	if g, e := bserverKnownBlocks, blockRefsByID; !reflect.DeepEqual(g, e) {
417		for id, eRefs := range e {
418			if gRefs := g[id]; !reflect.DeepEqual(gRefs, eRefs) {
419				sc.log.CDebugf(ctx, "Refs for ID %v don't match.  "+
420					"Got %v, expected %v", id, gRefs, eRefs)
421			}
422		}
423		for id, gRefs := range g {
424			if _, ok := e[id]; !ok {
425				sc.log.CDebugf(ctx, "Did not find matching expected "+
426					"ID for found block %v (with refs %v)", id, gRefs)
427			}
428		}
429
430		return fmt.Errorf("Folder %v has inconsistent state", tlfID)
431	}
432
433	// TODO: Check the archived and deleted blocks as well.
434	return nil
435}
436