fix: move image hardlink handling upstream during image indexing (#5196)

* chore(deps): pin stereoscope to the hardlink-inode-adoption branch

temporary pin to anchore/stereoscope#670 so the hardlink changes can be
exercised end to end. needs re-pinning to a release tag before merge.

pulls transitive bumps along with it: docker/cli, docker/go-connections and
gabriel-vasile/mimetype.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix: drop the image resolver hardlink workaround

stereoscope now adopts a hardlink's target at index time, so a hardlinked name
arrives already described as a regular file with the target's size, mime type
and content. the resolver-side fix from #5029 sat on top of that doing the same
job a second time, gated on a file type that no longer shows up, so both
`resolveHardLinkTarget` implementations and the extra tree walk they cost on
every `FilesByPath` are gone.

`file.NewVirtualLocationFromImage` stays as-is. it is exported and syft is v1,
so it keeps working for anyone using it, it just has no callers in syft now.

this picks up `FilesByMIMEType`, which #5029 explicitly could not fix, so
hardlinked names now reach mime-driven catalogers. on images built around
multi-call binaries (busybox and friends) that is a lot more file and executable
entries than before; distro and toolchain images move by about one entry.

one shape regresses: a hardlink stereoscope could not adopt (a link name that is
absent, empty, names a directory, names another un-adopted link, or points into
a lower layer) keeps `TypeHardLink`, and syft goes back to collapsing it onto
its target's path. no mainstream builder emits the cross-layer case.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump stereoscope to main

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-08-20 20:17:41 +00:00
committed by GitHub
parent c4597a08a2
commit 39c9c690df
7 changed files with 38 additions and 128 deletions
@@ -642,6 +642,24 @@ func TestImageResolvers_Hardlinks(t *testing.T) {
}
})
t.Run("FilesByMIMEType surfaces every hardlink name", func(t *testing.T) {
// this is the surface #5029 could not fix and stereoscope's index-time adoption does: MIME-driven
// catalogers see each name. it also pins that the names are separately addressable (distinct
// references), since this resolver dedups on reference and would return one entry if they shared it.
locs, err := resolver.FilesByMIMEType("text/plain")
require.NoError(t, err)
byPath := map[string]file.Location{}
for _, loc := range locs {
byPath[loc.RealPath] = loc
}
for _, path := range []string{"/file.txt", "/hardlink-a", "/hardlink-b"} {
loc, ok := byPath[path]
require.Truef(t, ok, "expected MIME search to surface %s (got %v)", path, byPath)
assertHardlink(t, resolver, path, loc)
}
})
t.Run("AllLocations includes every hardlink path as a regular file exactly once", func(t *testing.T) {
counts := map[string]int{}
locsByPath := map[string]file.Location{}