// Copyright (C) 2022-2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later // Package btrfscheck implements userspace utilities for checking // btrfs filesystems. package btrfscheck import ( "context" "fmt" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsitem" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsprim" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfstree" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol" ) type GraphCallbacks interface { FSErr(ctx context.Context, e error) Want(ctx context.Context, reason string, treeID btrfsprim.ObjID, objID btrfsprim.ObjID, typ btrfsprim.ItemType) WantOff(ctx context.Context, reason string, treeID btrfsprim.ObjID, objID btrfsprim.ObjID, typ btrfsprim.ItemType, off uint64) WantDirIndex(ctx context.Context, reason string, treeID btrfsprim.ObjID, objID btrfsprim.ObjID, name []byte) WantCSum(ctx context.Context, reason string, inodeTree, inodeItem btrfsprim.ObjID, beg, end btrfsvol.LogicalAddr) // interval is [beg, end) WantFileExt(ctx context.Context, reason string, treeID btrfsprim.ObjID, ino btrfsprim.ObjID, size int64) } // HandleItemWouldBeNoOp returns whether or not a call to HandleItem // for a given item type would be a no-op. func HandleItemWouldBeNoOp(typ btrfsprim.ItemType) bool { switch typ { case // btrfsitem.Dev btrfsprim.DEV_ITEM_KEY, // btrfsitem.DevStats btrfsprim.PERSISTENT_ITEM_KEY, // btrfsitem.Empty btrfsprim.ORPHAN_ITEM_KEY, btrfsprim.TREE_BLOCK_REF_KEY, btrfsprim.SHARED_BLOCK_REF_KEY, btrfsprim.FREE_SPACE_EXTENT_KEY, btrfsprim.QGROUP_RELATION_KEY, // btrfsite.ExtentCSum btrfsprim.EXTENT_CSUM_KEY: return true default: return false } } func HandleItem(ctx context.Context, o GraphCallbacks, treeID btrfsprim.ObjID, item btrfstree.Item) { // Notionally, just express the relationships shown in // https://btrfs.wiki.kernel.org/index.php/File:References.png (from the page // https://btrfs.wiki.kernel.org/index.php/Data_Structures ) switch body := item.Body.(type) { case *btrfsitem.BlockGroup: o.Want(ctx, "Chunk", btrfsprim.CHUNK_TREE_OBJECTID, body.ChunkObjectID, btrfsitem.CHUNK_ITEM_KEY) o.WantOff(ctx, "FreeSpaceInfo", btrfsprim.FREE_SPACE_TREE_OBJECTID, item.Key.ObjectID, btrfsitem.FREE_SPACE_INFO_KEY, item.Key.Offset) case *btrfsitem.Chunk: o.Want(ctx, "owning Root", btrfsprim.ROOT_TREE_OBJECTID, body.Head.Owner, btrfsitem.ROOT_ITEM_KEY) case *btrfsitem.Dev: // nothing case *btrfsitem.DevExtent: o.WantOff(ctx, "Chunk", body.ChunkTree, body.ChunkObjectID, btrfsitem.CHUNK_ITEM_KEY, uint64(body.ChunkOffset)) case *btrfsitem.DevStats: // nothing case *btrfsitem.DirEntry: // containing-directory o.WantOff(ctx, "containing dir inode", treeID, item.Key.ObjectID, btrfsitem.INODE_ITEM_KEY, 0) // siblings switch item.Key.ItemType { case btrfsitem.DIR_ITEM_KEY: o.WantDirIndex(ctx, "corresponding DIR_INDEX", treeID, item.Key.ObjectID, body.Name) case btrfsitem.DIR_INDEX_KEY: o.WantOff(ctx, "corresponding DIR_ITEM", treeID, item.Key.ObjectID, btrfsitem.DIR_ITEM_KEY, btrfsitem.NameHash(body.Name)) case btrfsitem.XATTR_ITEM_KEY: // nothing default: // This is a panic because the item decoder should not emit a // btrfsitem.DirEntry for other item types without this code also being // updated. panic(fmt.Errorf("should not happen: DirEntry: unexpected ItemType=%v", item.Key.ItemType)) } // item-within-directory if body.Location != (btrfsprim.Key{}) { switch body.Location.ItemType { case btrfsitem.INODE_ITEM_KEY: o.WantOff(ctx, "item being pointed to", treeID, body.Location.ObjectID, body.Location.ItemType, body.Location.Offset) o.WantOff(ctx, "backref from item being pointed to", treeID, body.Location.ObjectID, btrfsitem.INODE_REF_KEY, uint64(item.Key.ObjectID)) case btrfsitem.ROOT_ITEM_KEY: o.Want(ctx, "Root of subvolume being pointed to", btrfsprim.ROOT_TREE_OBJECTID, body.Location.ObjectID, body.Location.ItemType) default: o.FSErr(ctx, fmt.Errorf("DirEntry: unexpected .Location.ItemType=%v", body.Location.ItemType)) } } case *btrfsitem.Empty: // nothing case *btrfsitem.Extent: // if body.Head.Flags.Has(btrfsitem.EXTENT_FLAG_TREE_BLOCK) { // // Supposedly this flag indicates that // // body.Info.Key identifies a node by the // // first key in the node. But nothing in the // // kernel ever reads this, so who knows if it // // always gets updated correctly? // } for i, ref := range body.Refs { switch refBody := ref.Body.(type) { case nil: // nothing case *btrfsitem.ExtentDataRef: o.WantOff(ctx, "referencing Inode", refBody.Root, refBody.ObjectID, btrfsitem.INODE_ITEM_KEY, 0) o.WantOff(ctx, "referencing FileExtent", refBody.Root, refBody.ObjectID, btrfsitem.EXTENT_DATA_KEY, uint64(refBody.Offset)) case *btrfsitem.SharedDataRef: // nothing default: // This is a panic because the item decoder should not emit a new // type to ref.Body without this code also being updated. panic(fmt.Errorf("should not happen: Extent: unexpected .Refs[%d].Body type %T", i, refBody)) } } case *btrfsitem.ExtentCSum: // nothing case *btrfsitem.ExtentDataRef: o.Want(ctx, "Extent being referenced", btrfsprim.EXTENT_TREE_OBJECTID, item.Key.ObjectID, btrfsitem.EXTENT_ITEM_KEY) o.WantOff(ctx, "referencing Inode", body.Root, body.ObjectID, btrfsitem.INODE_ITEM_KEY, 0) o.WantOff(ctx, "referencing FileExtent", body.Root, body.ObjectID, btrfsitem.EXTENT_DATA_KEY, uint64(body.Offset)) case *btrfsitem.FileExtent: o.WantOff(ctx, "containing Inode", treeID, item.Key.ObjectID, btrfsitem.INODE_ITEM_KEY, 0) switch body.Type { case btrfsitem.FILE_EXTENT_INLINE: // nothing case btrfsitem.FILE_EXTENT_REG, btrfsitem.FILE_EXTENT_PREALLOC: // NB: o.WantCSum checks inodeBody.Flags.Has(btrfsitem.INODE_NODATASUM) for us. o.WantCSum(ctx, "data sum", treeID, item.Key.ObjectID, body.BodyExtent.DiskByteNr, body.BodyExtent.DiskByteNr.Add(body.BodyExtent.DiskNumBytes)) default: o.FSErr(ctx, fmt.Errorf("FileExtent: unexpected body.Type=%v", body.Type)) } case *btrfsitem.FreeSpaceBitmap: o.WantOff(ctx, "FreeSpaceInfo", treeID, item.Key.ObjectID, btrfsitem.FREE_SPACE_INFO_KEY, item.Key.Offset) case *btrfsitem.FreeSpaceHeader: o.WantOff(ctx, ".Location", treeID, body.Location.ObjectID, body.Location.ItemType, body.Location.Offset) case *btrfsitem.FreeSpaceInfo: if body.Flags.Has(btrfsitem.FREE_SPACE_USING_BITMAPS) { o.WantOff(ctx, "FreeSpaceBitmap", treeID, item.Key.ObjectID, btrfsitem.FREE_SPACE_BITMAP_KEY, item.Key.Offset) } case *btrfsitem.Inode: o.Want(ctx, "backrefs", treeID, // TODO: validate the number of these against body.NLink item.Key.ObjectID, btrfsitem.INODE_REF_KEY) o.WantFileExt(ctx, "FileExtents", treeID, item.Key.ObjectID, body.Size) if body.BlockGroup != 0 { o.Want(ctx, "BlockGroup", btrfsprim.EXTENT_TREE_OBJECTID, body.BlockGroup, btrfsitem.BLOCK_GROUP_ITEM_KEY) } case *btrfsitem.InodeRefs: o.WantOff(ctx, "child Inode", treeID, item.Key.ObjectID, btrfsitem.INODE_ITEM_KEY, 0) o.WantOff(ctx, "parent Inode", treeID, btrfsprim.ObjID(item.Key.Offset), btrfsitem.INODE_ITEM_KEY, 0) for _, ref := range body.Refs { o.WantOff(ctx, "DIR_ITEM", treeID, btrfsprim.ObjID(item.Key.Offset), btrfsitem.DIR_ITEM_KEY, btrfsitem.NameHash(ref.Name)) o.WantOff(ctx, "DIR_INDEX", treeID, btrfsprim.ObjID(item.Key.Offset), btrfsitem.DIR_INDEX_KEY, uint64(ref.Index)) } case *btrfsitem.Metadata: for i, ref := range body.Refs { switch refBody := ref.Body.(type) { case nil: // nothing case *btrfsitem.ExtentDataRef: o.WantOff(ctx, "referencing INode", refBody.Root, refBody.ObjectID, btrfsitem.INODE_ITEM_KEY, 0) o.WantOff(ctx, "referencing FileExtent", refBody.Root, refBody.ObjectID, btrfsitem.EXTENT_DATA_KEY, uint64(refBody.Offset)) case *btrfsitem.SharedDataRef: // nothing default: // This is a panic because the item decoder should not emit a new // type to ref.Body without this code also being updated. panic(fmt.Errorf("should not happen: Metadata: unexpected .Refs[%d].Body type %T", i, refBody)) } } case *btrfsitem.Root: if body.RootDirID != 0 { o.WantOff(ctx, "root directory", item.Key.ObjectID, body.RootDirID, btrfsitem.INODE_ITEM_KEY, 0) } if body.UUID != (btrfsprim.UUID{}) { key := btrfsitem.UUIDToKey(body.UUID) o.WantOff(ctx, "uuid", btrfsprim.UUID_TREE_OBJECTID, key.ObjectID, key.ItemType, key.Offset) } if body.ParentUUID != (btrfsprim.UUID{}) { key := btrfsitem.UUIDToKey(body.ParentUUID) o.WantOff(ctx, "parent uuid", btrfsprim.UUID_TREE_OBJECTID, key.ObjectID, key.ItemType, key.Offset) } case *btrfsitem.RootRef: var otherType btrfsprim.ItemType var parent, child btrfsprim.ObjID switch item.Key.ItemType { case btrfsitem.ROOT_REF_KEY: otherType = btrfsitem.ROOT_BACKREF_KEY parent = item.Key.ObjectID child = btrfsprim.ObjID(item.Key.Offset) case btrfsitem.ROOT_BACKREF_KEY: otherType = btrfsitem.ROOT_REF_KEY parent = btrfsprim.ObjID(item.Key.Offset) child = item.Key.ObjectID default: // This is a panic because the item decoder should not emit a // btrfsitem.RootRef for other item types without this code also being // updated. panic(fmt.Errorf("should not happen: RootRef: unexpected ItemType=%v", item.Key.ItemType)) } // sibling o.WantOff(ctx, fmt.Sprintf("corresponding %v", otherType), treeID, btrfsprim.ObjID(item.Key.Offset), otherType, uint64(item.Key.ObjectID)) // parent o.Want(ctx, "parent subvolume: Root", treeID, parent, btrfsitem.ROOT_ITEM_KEY) o.WantOff(ctx, "parent subvolume: Inode of parent dir", parent, body.DirID, btrfsitem.INODE_ITEM_KEY, 0) o.WantOff(ctx, "parent subvolume: DIR_ITEM in parent dir", parent, body.DirID, btrfsitem.DIR_ITEM_KEY, btrfsitem.NameHash(body.Name)) o.WantOff(ctx, "parent subvolume: DIR_INDEX in parent dir", parent, body.DirID, btrfsitem.DIR_INDEX_KEY, uint64(body.Sequence)) // child o.Want(ctx, "child subvolume: Root", treeID, child, btrfsitem.ROOT_ITEM_KEY) case *btrfsitem.SharedDataRef: o.Want(ctx, "Extent", btrfsprim.EXTENT_TREE_OBJECTID, item.Key.ObjectID, btrfsitem.EXTENT_ITEM_KEY) case *btrfsitem.UUIDMap: o.Want(ctx, "subvolume Root", btrfsprim.ROOT_TREE_OBJECTID, body.ObjID, btrfsitem.ROOT_ITEM_KEY) case *btrfsitem.Error: o.FSErr(ctx, fmt.Errorf("error decoding item: %w", body.Err)) default: // This is a panic because the item decoder should not emit new types without this // code also being updated. panic(fmt.Errorf("should not happen: unexpected item type: %T", body)) } }