wagoodman/dive · error

unable to read symlink %q: %s

Error message

unable to read symlink %q: %s

What it means

Raised as a panic by NewFileInfoFromPath (dive/filetree/file_info.go:58) while hashing a directory tree. When a walked entry is a symlink (info.Mode()&os.ModeSymlink != 0), dive calls os.Readlink(realPath); if the OS refuses, the library panics with 'unable to read symlink %q: %s'. There is no error return path - the whole process unwinds.

Source

Thrown at dive/filetree/file_info.go:58

		Gid:      header.Gid,
		IsDir:    header.FileInfo().IsDir(),
	}
}

func NewFileInfo(realPath, path string, info os.FileInfo) FileInfo {
	var err error

	// todo: don't use tar types here, create our own...
	var fileType byte
	var linkName string
	var size int64

	if info.Mode()&os.ModeSymlink != 0 {
		fileType = tar.TypeSymlink

		linkName, err = os.Readlink(realPath)
		if err != nil {
			panic(fmt.Errorf("unable to read symlink %q: %s", realPath, err))
		}
	} else if info.IsDir() {
		fileType = tar.TypeDir
	} else {
		fileType = tar.TypeReg

		size = info.Size()
	}

	var hash uint64
	if fileType != tar.TypeDir {
		file, err := os.Open(realPath)
		if err != nil {
			panic(fmt.Errorf("unable to open file %q: %s", realPath, err))
		}
		defer file.Close()
		hash = getHashFromReader(file)
	}

View on GitHub (pinned to d6c691947f)

Solutions

  1. Re-run the analysis on a quiescent directory: stop the build/watcher that mutates symlinks while the walk runs
  2. Verify you can read the offending link yourself: readlink <path> using the same user dive runs as; fix ownership or run with adequate privileges
  3. Exclude the problematic subtree (symlink farms like /proc or vendored caches) from the scan
  4. As a library consumer, pre-filter symlinks before delegating to NewFileInfoFromPath (see validationCode)

Example fix

// before: passing every walked entry straight in
info, _ := d.Info()
fi := filetree.NewFileInfoFromPath(path, path, info) // panics on unreadable symlink

// after: probe the link first and skip entries you cannot resolve
info, _ := d.Info()
if info.Mode()&os.ModeSymlink != 0 {
    if _, err := os.Readlink(path); err != nil {
        log.Printf("skipping unreadable symlink %s: %v", path, err)
        return nil // skip, don't crash
    }
}
fi := filetree.NewFileInfoFromPath(path, path, info)
Defensive patterns

Strategy: validation

Validate before calling

// before calling NewFileInfoFromPath on a walked entry:
func readableSymlink(realPath string, info os.FileInfo) bool {
    if info.Mode()&os.ModeSymlink == 0 {
        return true
    }
    _, err := os.Readlink(realPath)
    return err == nil
}

if !readableSymlink(realPath, info) {
    return nil // skip entry, avoid the panic
}
fi := filetree.NewFileInfoFromPath(path, realPath, info)

Try / catch

// Go has no catch; contain the panic at the walk boundary if you must keep going:
func safeFileInfo(path, real string, info os.FileInfo) (fi filetree.FileInfo, ok bool) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("dive panicked on %s: %v", real, r)
            ok = false
        }
    }()
    return filetree.NewFileInfoFromPath(path, real, info), true
}

Prevention

When it happens

Trigger: Calling NewFileInfoFromPath on a filesystem walk where a symlink target disappears between lstat and readlink (TOCTOU), a symlink the effective user cannot read, or platform quirks such as Windows junctions/symlink reparse points that os.Readlink cannot parse.

Common situations: Running dive's directory-analysis on a build workspace that is being modified concurrently (a build deleting symlinks mid-walk), scanning root-owned or container-created symlinks as a non-root user, or scanning trees containing special filesystem links (/proc, node_modules on Windows mounts).

Related errors


AI-assisted analysis of wagoodman/dive@d6c691947f (2026-08-15). Data as JSON: /api/errors/d0d9f3b8b3f90c91. Report an issue: GitHub.