package storage import ( "archive/tar" "errors" "fmt" "io" "os" "path" "path/filepath" "sort" "strings" "sync" "time" drivers "github.com/containers/storage/drivers" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/types" "github.com/sirupsen/logrus" ) var ( // ErrLayerUnaccounted describes a layer that is present in the lower-level storage driver, // but which is not known to or managed by the higher-level driver-agnostic logic. ErrLayerUnaccounted = types.ErrLayerUnaccounted // ErrLayerUnreferenced describes a layer which is not used by any image or container. ErrLayerUnreferenced = types.ErrLayerUnreferenced // ErrLayerIncorrectContentDigest describes a layer for which the contents of one or more // files which were added in the layer appear to have changed. It may instead look like an // unnamed "file integrity checksum failed" error. ErrLayerIncorrectContentDigest = types.ErrLayerIncorrectContentDigest // ErrLayerIncorrectContentSize describes a layer for which regenerating the diff that was // used to populate the layer produced a diff of a different size. We check the digest // first, so it's highly unlikely you'll ever see this error. ErrLayerIncorrectContentSize = types.ErrLayerIncorrectContentSize // ErrLayerContentModified describes a layer which contains contents which should not be // there, or for which ownership/permissions/dates have been changed. ErrLayerContentModified = types.ErrLayerContentModified // ErrLayerDataMissing describes a layer which is missing a big data item. ErrLayerDataMissing = types.ErrLayerDataMissing // ErrLayerMissing describes a layer which is the missing parent of a layer. ErrLayerMissing = types.ErrLayerMissing // ErrImageLayerMissing describes an image which claims to have a layer that we don't know // about. ErrImageLayerMissing = types.ErrImageLayerMissing // ErrImageDataMissing describes an image which is missing a big data item. ErrImageDataMissing = types.ErrImageDataMissing // ErrImageDataIncorrectSize describes an image which has a big data item which looks like // its size has changed, likely because it's been modified somehow. ErrImageDataIncorrectSize = types.ErrImageDataIncorrectSize // ErrContainerImageMissing describes a container which claims to be based on an image that // we don't know about. ErrContainerImageMissing = types.ErrContainerImageMissing // ErrContainerDataMissing describes a container which is missing a big data item. ErrContainerDataMissing = types.ErrContainerDataMissing // ErrContainerDataIncorrectSize describes a container which has a big data item which looks // like its size has changed, likely because it's been modified somehow. ErrContainerDataIncorrectSize = types.ErrContainerDataIncorrectSize ) const ( defaultMaximumUnreferencedLayerAge = 24 * time.Hour ) // CheckOptions is the set of options for Check(), specifying which tests to perform. type CheckOptions struct { LayerUnreferencedMaximumAge *time.Duration // maximum allowed age of unreferenced layers LayerDigests bool // check that contents of image layer diffs can still be reconstructed LayerMountable bool // check that layers are mountable LayerContents bool // check that contents of image layers match their diffs, with no unexpected changes, requires LayerMountable LayerData bool // check that associated "big" data items are present and can be read ImageData bool // check that associated "big" data items are present, can be read, and match the recorded size ContainerData bool // check that associated "big" data items are present and can be read } // checkIgnore is used to tell functions that compare the contents of a mounted // layer to the contents that we'd expect it to have to ignore certain // discrepancies type checkIgnore struct { ownership, timestamps, permissions bool } // CheckMost returns a CheckOptions with mostly just "quick" checks enabled. func CheckMost() *CheckOptions { return &CheckOptions{ LayerDigests: true, LayerMountable: true, LayerContents: false, LayerData: true, ImageData: true, ContainerData: true, } } // CheckEverything returns a CheckOptions with every check enabled. func CheckEverything() *CheckOptions { return &CheckOptions{ LayerDigests: true, LayerMountable: true, LayerContents: true, LayerData: true, ImageData: true, ContainerData: true, } } // CheckReport is a list of detected problems. type CheckReport struct { Layers map[string][]error // damaged read-write layers ROLayers map[string][]error // damaged read-only layers layerParentsByLayerID map[string]string layerOrder map[string]int Images map[string][]error // damaged read-write images (including those with damaged layers) ROImages map[string][]error // damaged read-only images (including those with damaged layers) Containers map[string][]error // damaged containers (including those based on damaged images) } // RepairOptions is the set of options for Repair(). type RepairOptions struct { RemoveContainers bool // Remove damaged containers } // RepairEverything returns a RepairOptions with every optional remediation // enabled. func RepairEverything() *RepairOptions { return &RepairOptions{ RemoveContainers: true, } } // Check returns a list of problems with what's in the store, as a whole. It can be very expensive // to call. func (s *store) Check(options *CheckOptions) (CheckReport, error) { var ignore checkIgnore for _, o := range s.graphOptions { if strings.Contains(o, "ignore_chown_errors=true") { ignore.ownership = true } if strings.HasPrefix(o, "force_mask=") { ignore.permissions = true } } for o := range s.pullOptions { if strings.Contains(o, "use_hard_links") { if s.pullOptions[o] == "true" { ignore.timestamps = true } } } if options == nil { options = CheckMost() } report := CheckReport{ Layers: make(map[string][]error), ROLayers: make(map[string][]error), layerParentsByLayerID: make(map[string]string), // layers ID -> their parent's ID, if there is one layerOrder: make(map[string]int), // layers ID -> order for removal, if we needed to remove them all Images: make(map[string][]error), ROImages: make(map[string][]error), Containers: make(map[string][]error), } // This map will track known layer IDs. If we have multiple stores, read-only ones can // contain copies of layers that are in the read-write store, but we'll only ever be // mounting or extracting contents from the read-write versions, since we always search it // first. The boolean will track if the layer is referenced by at least one image or // container. referencedLayers := make(map[string]bool) referencedROLayers := make(map[string]bool) // This map caches the headers for items included in layer diffs. diffHeadersByLayer := make(map[string][]*tar.Header) var diffHeadersByLayerMutex sync.Mutex // Walk the list of layer stores, looking at each layer that we didn't see in a // previously-visited store. if _, _, err := readOrWriteAllLayerStores(s, func(store roLayerStore) (struct{}, bool, error) { layers, err := store.Layers() if err != nil { return struct{}{}, true, err } isReadWrite := roLayerStoreIsReallyReadWrite(store) readWriteDesc := "" if !isReadWrite { readWriteDesc = "read-only " } // Examine each layer in turn. for i := range layers { layer := layers[i] id := layer.ID // If we've already seen a layer with this ID, no need to process it again. if _, checked := referencedLayers[id]; checked { continue } if _, checked := referencedROLayers[id]; checked { continue } // Note the parent of this layer, and add it to the map of known layers so // that we know that we've visited it, but we haven't confirmed that it's // used by anything. report.layerParentsByLayerID[id] = layer.Parent if isReadWrite { referencedLayers[id] = false } else { referencedROLayers[id] = false } logrus.Debugf("checking %slayer %s", readWriteDesc, id) // Check that all of the big data items are present and can be read. We // have no digest or size information to compare the contents to (grumble), // so we can't verify that the contents haven't been changed since they // were stored. if options.LayerData { for _, name := range layer.BigDataNames { func() { rc, err := store.BigData(id, name) if err != nil { if errors.Is(err, os.ErrNotExist) { err := fmt.Errorf("%slayer %s: data item %q: %w", readWriteDesc, id, name, ErrLayerDataMissing) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } err = fmt.Errorf("%slayer %s: data item %q: %w", readWriteDesc, id, name, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } defer rc.Close() if _, err = io.Copy(io.Discard, rc); err != nil { err = fmt.Errorf("%slayer %s: data item %q: %w", readWriteDesc, id, name, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } }() } } // Check that the content we get back when extracting the layer's contents // match the recorded digest and size. A layer for which they're not given // isn't a part of an image, and is likely the read-write layer for a // container, and we can't vouch for the integrity of its contents. // For each layer with known contents, record the headers for the layer's // diff, which we can use to reconstruct the expected contents for the tree // we see when the layer is mounted. if options.LayerDigests && layer.UncompressedDigest != "" { func() { expectedDigest := layer.UncompressedDigest // Double-check that the digest isn't invalid somehow. if err := layer.UncompressedDigest.Validate(); err != nil { err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } // Extract the diff. uncompressed := archive.Uncompressed diffOptions := DiffOptions{ Compression: &uncompressed, } diff, err := store.Diff("", id, &diffOptions) if err != nil { err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } // Digest and count the length of the diff. digester := expectedDigest.Algorithm().Digester() counter := ioutils.NewWriteCounter(digester.Hash()) reader := io.TeeReader(diff, counter) var wg sync.WaitGroup var archiveErr error wg.Add(1) go func(layerID string, diffReader io.Reader) { // Read the diff, one item at a time. tr := tar.NewReader(diffReader) hdr, err := tr.Next() for err == nil { diffHeadersByLayerMutex.Lock() diffHeadersByLayer[layerID] = append(diffHeadersByLayer[layerID], hdr) diffHeadersByLayerMutex.Unlock() hdr, err = tr.Next() } if !errors.Is(err, io.EOF) { archiveErr = err } // consume any trailer after the EOF marker io.Copy(io.Discard, diffReader) wg.Done() }(id, reader) wg.Wait() diff.Close() if archiveErr != nil { // Reading the diff didn't end as expected diffHeadersByLayerMutex.Lock() delete(diffHeadersByLayer, id) diffHeadersByLayerMutex.Unlock() archiveErr = fmt.Errorf("%slayer %s: %w", readWriteDesc, id, archiveErr) if isReadWrite { report.Layers[id] = append(report.Layers[id], archiveErr) } else { report.ROLayers[id] = append(report.ROLayers[id], archiveErr) } return } if digester.Digest() != layer.UncompressedDigest { // The diff digest didn't match. diffHeadersByLayerMutex.Lock() delete(diffHeadersByLayer, id) diffHeadersByLayerMutex.Unlock() err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, ErrLayerIncorrectContentDigest) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } } if layer.UncompressedSize != -1 && counter.Count != layer.UncompressedSize { // We expected the diff to have a specific size, and // it didn't match. diffHeadersByLayerMutex.Lock() delete(diffHeadersByLayer, id) diffHeadersByLayerMutex.Unlock() err := fmt.Errorf("%slayer %s: read %d bytes instead of %d bytes: %w", readWriteDesc, id, counter.Count, layer.UncompressedSize, ErrLayerIncorrectContentSize) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } } }() } } // At this point we're out of things that we can be sure will work in read-only // stores, so skip the rest for any stores that aren't also read-write stores. if !isReadWrite { return struct{}{}, false, nil } // Content and mount checks are also things that we can only be sure will work in // read-write stores. for i := range layers { layer := layers[i] id := layer.ID // Compare to what we see when we mount the layer and walk the tree, and // flag cases where content is in the layer that shouldn't be there. The // tar-split implementation of Diff() won't catch this problem by itself. if options.LayerMountable { func() { // Mount the layer. mountPoint, err := s.graphDriver.Get(id, drivers.MountOpts{MountLabel: layer.MountLabel}) if err != nil { err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } // Unmount the layer when we're done in here. defer func() { if err := s.graphDriver.Put(id); err != nil { err := fmt.Errorf("%slayer %s: %w", readWriteDesc, id, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } }() // If we're not looking at layer contents, or we didn't // look at the diff for this layer, we're done here. if !options.LayerDigests || layer.UncompressedDigest == "" || !options.LayerContents { return } // Build a list of all of the changes in all of the layers // that make up the tree we're looking at. diffHeaderSet := [][]*tar.Header{} // If we don't know _all_ of the changes that produced this // layer, it's not part of an image, so we're done here. for layerID := id; layerID != ""; layerID = report.layerParentsByLayerID[layerID] { diffHeadersByLayerMutex.Lock() layerChanges, haveChanges := diffHeadersByLayer[layerID] diffHeadersByLayerMutex.Unlock() if !haveChanges { return } // The diff headers for this layer go _before_ those of // layers that inherited some of its contents. diffHeaderSet = append([][]*tar.Header{layerChanges}, diffHeaderSet...) } expectedCheckDirectory := newCheckDirectoryDefaults() for _, diffHeaders := range diffHeaderSet { expectedCheckDirectory.headers(diffHeaders) } // Scan the directory tree under the mount point. var idmap *idtools.IDMappings if !s.canUseShifting(layer.UIDMap, layer.GIDMap) { // we would have had to chown() layer contents to match ID maps idmap = idtools.NewIDMappingsFromMaps(layer.UIDMap, layer.GIDMap) } actualCheckDirectory, err := newCheckDirectoryFromDirectory(mountPoint) if err != nil { err := fmt.Errorf("scanning contents of %slayer %s: %w", readWriteDesc, id, err) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } return } // Every departure from our expectations is an error. diffs := compareCheckDirectory(expectedCheckDirectory, actualCheckDirectory, idmap, ignore) for _, diff := range diffs { err := fmt.Errorf("%slayer %s: %s, %w", readWriteDesc, id, diff, ErrLayerContentModified) if isReadWrite { report.Layers[id] = append(report.Layers[id], err) } else { report.ROLayers[id] = append(report.ROLayers[id], err) } } }() } } // Check that we don't have any dangling parent layer references. for id, parent := range report.layerParentsByLayerID { // If this layer doesn't have a parent, no problem. if parent == "" { continue } // If we've already seen a layer with this parent ID, skip it. if _, checked := referencedLayers[parent]; checked { continue } if _, checked := referencedROLayers[parent]; checked { continue } // We haven't seen a layer with the ID that this layer's record // says is its parent's ID. err := fmt.Errorf("%slayer %s: %w", readWriteDesc, parent, ErrLayerMissing) report.Layers[id] = append(report.Layers[id], err) } return struct{}{}, false, nil }); err != nil { return CheckReport{}, err } // This map will track examined images. If we have multiple stores, read-only ones can // contain copies of images that are also in the read-write store, or the read-write store // may contain a duplicate entry that refers to layers in the read-only stores, but when // trying to export them, we only look at the first copy of the image. examinedImages := make(map[string]struct{}) // Walk the list of image stores, looking at each image that we didn't see in a // previously-visited store. if _, _, err := readAllImageStores(s, func(store roImageStore) (struct{}, bool, error) { images, err := store.Images() if err != nil { return struct{}{}, true, err } isReadWrite := roImageStoreIsReallyReadWrite(store) readWriteDesc := "" if !isReadWrite { readWriteDesc = "read-only " } // Examine each image in turn. for i := range images { image := images[i] id := image.ID // If we've already seen an image with this ID, skip it. if _, checked := examinedImages[id]; checked { continue } examinedImages[id] = struct{}{} logrus.Debugf("checking %simage %s", readWriteDesc, id) if options.ImageData { // Check that all of the big data items are present and reading them // back gives us the right amount of data. Even though we record // digests that can be used to look them up, we don't know how they // were calculated (they're only used as lookup keys), so do not try // to check them. for _, key := range image.BigDataNames { func() { data, err := store.BigData(id, key) if err != nil { if errors.Is(err, os.ErrNotExist) { err = fmt.Errorf("%simage %s: data item %q: %w", readWriteDesc, id, key, ErrImageDataMissing) if isReadWrite { report.Images[id] = append(report.Images[id], err) } else { report.ROImages[id] = append(report.ROImages[id], err) } return } err = fmt.Errorf("%simage %s: data item %q: %w", readWriteDesc, id, key, err) if isReadWrite { report.Images[id] = append(report.Images[id], err) } else { report.ROImages[id] = append(report.ROImages[id], err) } return } if int64(len(data)) != image.BigDataSizes[key] { err = fmt.Errorf("%simage %s: data item %q: %w", readWriteDesc, id, key, ErrImageDataIncorrectSize) if isReadWrite { report.Images[id] = append(report.Images[id], err) } else { report.ROImages[id] = append(report.ROImages[id], err) } return } }() } } // Walk the layers list for the image. For every layer that the image uses // that has errors, the layer's errors are also the image's errors. examinedImageLayers := make(map[string]struct{}) for _, topLayer := range append([]string{image.TopLayer}, image.MappedTopLayers...) { if topLayer == "" { continue } if _, checked := examinedImageLayers[topLayer]; checked { continue } examinedImageLayers[topLayer] = struct{}{} for layer := topLayer; layer != ""; layer = report.layerParentsByLayerID[layer] { // The referenced layer should have a corresponding entry in // one map or the other. _, checked := referencedLayers[layer] _, checkedRO := referencedROLayers[layer] if !checked && !checkedRO { err := fmt.Errorf("layer %s: %w", layer, ErrImageLayerMissing) err = fmt.Errorf("%simage %s: %w", readWriteDesc, id, err) if isReadWrite { report.Images[id] = append(report.Images[id], err) } else { report.ROImages[id] = append(report.ROImages[id], err) } } else { // Count this layer as referenced. Whether by the // image or one of its child layers doesn't matter // at this point. if _, ok := referencedLayers[layer]; ok { referencedLayers[layer] = true } if _, ok := referencedROLayers[layer]; ok { referencedROLayers[layer] = true } } if isReadWrite { if len(report.Layers[layer]) > 0 { report.Images[id] = append(report.Images[id], report.Layers[layer]...) } if len(report.ROLayers[layer]) > 0 { report.Images[id] = append(report.Images[id], report.ROLayers[layer]...) } } else { if len(report.Layers[layer]) > 0 { report.ROImages[id] = append(report.ROImages[id], report.Layers[layer]...) } if len(report.ROLayers[layer]) > 0 { report.ROImages[id] = append(report.ROImages[id], report.ROLayers[layer]...) } } } } } return struct{}{}, false, nil }); err != nil { return CheckReport{}, err } // Iterate over each container in turn. if _, _, err := readContainerStore(s, func() (struct{}, bool, error) { containers, err := s.containerStore.Containers() if err != nil { return struct{}{}, true, err } for i := range containers { container := containers[i] id := container.ID logrus.Debugf("checking container %s", id) if options.ContainerData { // Check that all of the big data items are present and reading them // back gives us the right amount of data. for _, key := range container.BigDataNames { func() { data, err := s.containerStore.BigData(id, key) if err != nil { if errors.Is(err, os.ErrNotExist) { err = fmt.Errorf("container %s: data item %q: %w", id, key, ErrContainerDataMissing) report.Containers[id] = append(report.Containers[id], err) return } err = fmt.Errorf("container %s: data item %q: %w", id, key, err) report.Containers[id] = append(report.Containers[id], err) return } if int64(len(data)) != container.BigDataSizes[key] { err = fmt.Errorf("container %s: data item %q: %w", id, key, ErrContainerDataIncorrectSize) report.Containers[id] = append(report.Containers[id], err) return } }() } } // Look at the container's base image. If the image has errors, the image's errors // are the container's errors. if container.ImageID != "" { if _, checked := examinedImages[container.ImageID]; !checked { err := fmt.Errorf("image %s: %w", container.ImageID, ErrContainerImageMissing) report.Containers[id] = append(report.Containers[id], err) } if len(report.Images[container.ImageID]) > 0 { report.Containers[id] = append(report.Containers[id], report.Images[container.ImageID]...) } if len(report.ROImages[container.ImageID]) > 0 { report.Containers[id] = append(report.Containers[id], report.ROImages[container.ImageID]...) } } // Count the container's layer as referenced. if container.LayerID != "" { referencedLayers[container.LayerID] = true } } return struct{}{}, false, nil }); err != nil { return CheckReport{}, err } // Now go back through all of the layer stores, and flag any layers which don't belong // to an image or a container, and has been around longer than we can reasonably expect // such a layer to be present before a corresponding image record is added. if _, _, err := readAllLayerStores(s, func(store roLayerStore) (struct{}, bool, error) { if isReadWrite := roLayerStoreIsReallyReadWrite(store); !isReadWrite { return struct{}{}, false, nil } layers, err := store.Layers() if err != nil { return struct{}{}, true, err } for _, layer := range layers { maximumAge := defaultMaximumUnreferencedLayerAge if options.LayerUnreferencedMaximumAge != nil { maximumAge = *options.LayerUnreferencedMaximumAge } if referenced := referencedLayers[layer.ID]; !referenced { if layer.Created.IsZero() || layer.Created.Add(maximumAge).Before(time.Now()) { // Either we don't (and never will) know when this layer was // created, or it was created far enough in the past that we're // reasonably sure it's not part of an image that's being written // right now. err := fmt.Errorf("layer %s: %w", layer.ID, ErrLayerUnreferenced) report.Layers[layer.ID] = append(report.Layers[layer.ID], err) } } } return struct{}{}, false, nil }); err != nil { return CheckReport{}, err } // If the driver can tell us about which layers it knows about, we should have previously // examined all of them. Any that we didn't are probably just wasted space. // Note: if the driver doesn't support enumerating layers, it returns ErrNotSupported. if err := s.startUsingGraphDriver(); err != nil { return CheckReport{}, err } defer s.stopUsingGraphDriver() layerList, err := s.graphDriver.ListLayers() if err != nil && !errors.Is(err, drivers.ErrNotSupported) { return CheckReport{}, err } if !errors.Is(err, drivers.ErrNotSupported) { for i, id := range layerList { if _, known := referencedLayers[id]; !known { err := fmt.Errorf("layer %s: %w", id, ErrLayerUnaccounted) report.Layers[id] = append(report.Layers[id], err) } report.layerOrder[id] = i + 1 } } return report, nil } func roLayerStoreIsReallyReadWrite(store roLayerStore) bool { return store.(*layerStore).lockfile.IsReadWrite() } func roImageStoreIsReallyReadWrite(store roImageStore) bool { return store.(*imageStore).lockfile.IsReadWrite() } // Repair removes items which are themselves damaged, or which depend on items which are damaged. // Errors are returned if an attempt to delete an item fails. func (s *store) Repair(report CheckReport, options *RepairOptions) []error { if options == nil { options = RepairEverything() } var errs []error // Just delete damaged containers. if options.RemoveContainers { for id := range report.Containers { err := s.DeleteContainer(id) if err != nil && !errors.Is(err, ErrContainerUnknown) { err := fmt.Errorf("deleting container %s: %w", id, err) errs = append(errs, err) } } } // Now delete damaged images. Note which layers were removed as part of removing those images. deletedLayers := make(map[string]struct{}) for id := range report.Images { layers, err := s.DeleteImage(id, true) if err != nil { if !errors.Is(err, ErrImageUnknown) && !errors.Is(err, ErrLayerUnknown) { err := fmt.Errorf("deleting image %s: %w", id, err) errs = append(errs, err) } } else { for _, layer := range layers { logrus.Debugf("deleted layer %s", layer) deletedLayers[layer] = struct{}{} } logrus.Debugf("deleted image %s", id) } } // Build a list of the layers that we need to remove, sorted with parents of layers before // layers that they are parents of. layersToDelete := make([]string, 0, len(report.Layers)) for id := range report.Layers { layersToDelete = append(layersToDelete, id) } depth := func(id string) int { d := 0 parent := report.layerParentsByLayerID[id] for parent != "" { d++ parent = report.layerParentsByLayerID[parent] } return d } isUnaccounted := func(errs []error) bool { for _, err := range errs { if errors.Is(err, ErrLayerUnaccounted) { return true } } return false } sort.Slice(layersToDelete, func(i, j int) bool { // we've not heard of either of them, so remove them in the order the driver suggested if isUnaccounted(report.Layers[layersToDelete[i]]) && isUnaccounted(report.Layers[layersToDelete[j]]) && report.layerOrder[layersToDelete[i]] != 0 && report.layerOrder[layersToDelete[j]] != 0 { return report.layerOrder[layersToDelete[i]] < report.layerOrder[layersToDelete[j]] } // always delete the one we've heard of first if isUnaccounted(report.Layers[layersToDelete[i]]) && !isUnaccounted(report.Layers[layersToDelete[j]]) { return false } // always delete the one we've heard of first if !isUnaccounted(report.Layers[layersToDelete[i]]) && isUnaccounted(report.Layers[layersToDelete[j]]) { return true } // we've heard of both of them; the one that's on the end of a longer chain goes first return depth(layersToDelete[i]) > depth(layersToDelete[j]) // closer-to-a-notional-base layers get removed later }) // Now delete the layers that haven't been removed along with images. for _, id := range layersToDelete { if _, ok := deletedLayers[id]; ok { continue } for _, reportedErr := range report.Layers[id] { var err error // If a layer was unaccounted for, remove it at the storage driver level. // Otherwise, remove it at the higher level and let the higher level // logic worry about telling the storage driver to delete the layer. if errors.Is(reportedErr, ErrLayerUnaccounted) { if err = s.graphDriver.Remove(id); err != nil { err = fmt.Errorf("deleting storage layer %s: %v", id, err) } else { logrus.Debugf("deleted storage layer %s", id) } } else { var stillMounted bool if stillMounted, err = s.Unmount(id, true); err == nil && !stillMounted { logrus.Debugf("unmounted layer %s", id) } else if err != nil { logrus.Debugf("unmounting layer %s: %v", id, err) } else { logrus.Debugf("layer %s still mounted", id) } if err = s.DeleteLayer(id); err != nil { err = fmt.Errorf("deleting layer %s: %w", id, err) logrus.Debugf("deleted layer %s", id) } } if err != nil && !errors.Is(err, ErrLayerUnknown) && !errors.Is(err, ErrNotALayer) && !errors.Is(err, os.ErrNotExist) { errs = append(errs, err) } } } return errs } // compareFileInfo returns a string summarizing what's different between the two checkFileInfos func compareFileInfo(a, b checkFileInfo, idmap *idtools.IDMappings, ignore checkIgnore) string { var comparison []string if a.typeflag != b.typeflag { comparison = append(comparison, fmt.Sprintf("filetype:%v→%v", a.typeflag, b.typeflag)) } if idmap != nil && !idmap.Empty() { mappedUID, mappedGID, err := idmap.ToContainer(idtools.IDPair{UID: b.uid, GID: b.gid}) if err != nil { return err.Error() } b.uid, b.gid = mappedUID, mappedGID } if a.uid != b.uid && !ignore.ownership { comparison = append(comparison, fmt.Sprintf("uid:%d→%d", a.uid, b.uid)) } if a.gid != b.gid && !ignore.ownership { comparison = append(comparison, fmt.Sprintf("gid:%d→%d", a.gid, b.gid)) } if a.size != b.size { comparison = append(comparison, fmt.Sprintf("size:%d→%d", a.size, b.size)) } if (os.ModeType|os.ModePerm)&a.mode != (os.ModeType|os.ModePerm)&b.mode && !ignore.permissions { comparison = append(comparison, fmt.Sprintf("mode:%04o→%04o", a.mode, b.mode)) } if a.mtime != b.mtime && !ignore.timestamps { comparison = append(comparison, fmt.Sprintf("mtime:0x%x→0x%x", a.mtime, b.mtime)) } return strings.Join(comparison, ",") } // checkFileInfo is what we care about for files type checkFileInfo struct { typeflag byte uid, gid int size int64 mode os.FileMode mtime int64 // unix-style whole seconds } // checkDirectory is a node in a filesystem record, possibly the top type checkDirectory struct { directory map[string]*checkDirectory // subdirectories file map[string]checkFileInfo // non-directories checkFileInfo } // newCheckDirectory creates an empty checkDirectory func newCheckDirectory(uid, gid int, size int64, mode os.FileMode, mtime int64) *checkDirectory { return &checkDirectory{ directory: make(map[string]*checkDirectory), file: make(map[string]checkFileInfo), checkFileInfo: checkFileInfo{ typeflag: tar.TypeDir, uid: uid, gid: gid, size: size, mode: mode, mtime: mtime, }, } } // newCheckDirectoryDefaults creates an empty checkDirectory with hardwired defaults for the UID // (0), GID (0), size (0) and permissions (0o555) func newCheckDirectoryDefaults() *checkDirectory { return newCheckDirectory(0, 0, 0, 0o555, time.Now().Unix()) } // newCheckDirectoryFromDirectory creates a checkDirectory for an on-disk directory tree func newCheckDirectoryFromDirectory(dir string) (*checkDirectory, error) { cd := newCheckDirectoryDefaults() err := filepath.Walk(dir, func(walkpath string, info os.FileInfo, err error) error { if err != nil && !errors.Is(err, os.ErrNotExist) { return err } rel, err := filepath.Rel(dir, walkpath) if err != nil { return err } hdr, err := tar.FileInfoHeader(info, "") // we don't record link targets, so don't bother looking it up if err != nil { return err } hdr.Name = filepath.ToSlash(rel) cd.header(hdr) return nil }) if err != nil { return nil, err } return cd, nil } // add adds an item to a checkDirectory func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int64, mode os.FileMode, mtime int64) { components := strings.Split(path, "/") if components[len(components)-1] == "" { components = components[:len(components)-1] } if components[0] == "." { components = components[1:] } if typeflag != tar.TypeReg { size = 0 } switch len(components) { case 0: c.uid = uid c.gid = gid c.mode = mode c.mtime = mtime return case 1: switch typeflag { case tar.TypeDir: delete(c.file, components[0]) // directory entries are mergers, not replacements if _, present := c.directory[components[0]]; !present { c.directory[components[0]] = newCheckDirectory(uid, gid, size, mode, mtime) } else { c.directory[components[0]].checkFileInfo = checkFileInfo{ typeflag: tar.TypeDir, uid: uid, gid: gid, size: size, mode: mode, mtime: mtime, } } default: // treat these as TypeReg items delete(c.directory, components[0]) c.file[components[0]] = checkFileInfo{ typeflag: typeflag, uid: uid, gid: gid, size: size, mode: mode, mtime: mtime, } case tar.TypeXGlobalHeader: // ignore, since even though it looks like a valid pathname, it doesn't end // up on the filesystem } return } subdirectory := c.directory[components[0]] if subdirectory == nil { subdirectory = newCheckDirectory(uid, gid, size, mode, mtime) c.directory[components[0]] = subdirectory } subdirectory.add(strings.Join(components[1:], "/"), typeflag, uid, gid, size, mode, mtime) } // remove removes an item from a checkDirectory func (c *checkDirectory) remove(path string) { components := strings.Split(path, "/") if len(components) == 1 { delete(c.directory, components[0]) delete(c.file, components[0]) return } subdirectory := c.directory[components[0]] if subdirectory != nil { subdirectory.remove(strings.Join(components[1:], "/")) } } // header updates a checkDirectory using information from the passed-in header func (c *checkDirectory) header(hdr *tar.Header) { name := path.Clean(hdr.Name) dir, base := path.Split(name) if strings.HasPrefix(base, archive.WhiteoutPrefix) { if base == archive.WhiteoutOpaqueDir { c.remove(path.Clean(dir)) c.add(path.Clean(dir), tar.TypeDir, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode), hdr.ModTime.Unix()) } else { c.remove(path.Join(dir, base[len(archive.WhiteoutPrefix):])) } } else { if hdr.Typeflag == tar.TypeLink { // look up the attributes of the target of the hard link // n.b. by convention, Linkname is always relative to the // root directory of the archive, which is not always the // same as being relative to hdr.Name directory := c for _, component := range strings.Split(path.Clean(hdr.Linkname), "/") { if component == "." || component == ".." { continue } if subdir, ok := directory.directory[component]; ok { directory = subdir continue } if file, ok := directory.file[component]; ok { hdr.Typeflag = file.typeflag hdr.Uid = file.uid hdr.Gid = file.gid hdr.Size = file.size hdr.Mode = int64(file.mode) hdr.ModTime = time.Unix(file.mtime, 0) } break } } c.add(name, hdr.Typeflag, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode), hdr.ModTime.Unix()) } } // headers updates a checkDirectory using information from the passed-in header slice func (c *checkDirectory) headers(hdrs []*tar.Header) { hdrs = append([]*tar.Header{}, hdrs...) // sort the headers from the diff to ensure that whiteouts appear // before content when they both appear in the same directory, per // https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts // and that hard links appear after other types of entries sort.SliceStable(hdrs, func(i, j int) bool { if hdrs[i].Typeflag != tar.TypeLink && hdrs[j].Typeflag == tar.TypeLink { return true } if hdrs[i].Typeflag == tar.TypeLink && hdrs[j].Typeflag != tar.TypeLink { return false } idir, ifile := path.Split(hdrs[i].Name) jdir, jfile := path.Split(hdrs[j].Name) if idir != jdir { return hdrs[i].Name < hdrs[j].Name } if ifile == archive.WhiteoutOpaqueDir { return true } if strings.HasPrefix(ifile, archive.WhiteoutPrefix) && !strings.HasPrefix(jfile, archive.WhiteoutPrefix) { return true } return false }) for _, hdr := range hdrs { c.header(hdr) } } // names provides a sorted list of the path names in the directory tree func (c *checkDirectory) names() []string { names := make([]string, 0, len(c.file)+len(c.directory)) for name := range c.file { names = append(names, name) } for name, subdirectory := range c.directory { names = append(names, name+"/") for _, subname := range subdirectory.names() { names = append(names, name+"/"+subname) } } return names } // compareCheckSubdirectory walks two subdirectory trees and returns a list of differences func compareCheckSubdirectory(path string, a, b *checkDirectory, idmap *idtools.IDMappings, ignore checkIgnore) []string { var diff []string if a == nil { a = newCheckDirectoryDefaults() } if b == nil { b = newCheckDirectoryDefaults() } for aname, adir := range a.directory { if bdir, present := b.directory[aname]; !present { // directory was removed diff = append(diff, "-"+path+"/"+aname+"/") diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, nil, idmap, ignore)...) } else { // directory is in both trees; descend if attributes := compareFileInfo(adir.checkFileInfo, bdir.checkFileInfo, idmap, ignore); attributes != "" { diff = append(diff, path+"/"+aname+"("+attributes+")") } diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, bdir, idmap, ignore)...) } } for bname, bdir := range b.directory { if _, present := a.directory[bname]; !present { // directory added diff = append(diff, "+"+path+"/"+bname+"/") diff = append(diff, compareCheckSubdirectory(path+"/"+bname, nil, bdir, idmap, ignore)...) } } for aname, afile := range a.file { if bfile, present := b.file[aname]; !present { // non-directory removed or replaced diff = append(diff, "-"+path+"/"+aname) } else { // item is in both trees; compare if attributes := compareFileInfo(afile, bfile, idmap, ignore); attributes != "" { diff = append(diff, path+"/"+aname+"("+attributes+")") } } } for bname := range b.file { filetype, present := a.file[bname] if !present { // non-directory added or replaced with something else diff = append(diff, "+"+path+"/"+bname) continue } if attributes := compareFileInfo(filetype, b.file[bname], idmap, ignore); attributes != "" { // non-directory replaced with non-directory diff = append(diff, "+"+path+"/"+bname+"("+attributes+")") } } return diff } // compareCheckDirectory walks two directory trees and returns a sorted list of differences func compareCheckDirectory(a, b *checkDirectory, idmap *idtools.IDMappings, ignore checkIgnore) []string { diff := compareCheckSubdirectory("", a, b, idmap, ignore) sort.Slice(diff, func(i, j int) bool { if strings.Compare(diff[i][1:], diff[j][1:]) < 0 { return true } if diff[i][0] == '-' { return true } return false }) return diff }