{"record":{"id":"d0d9f3b8b3f90c91","repo":"wagoodman/dive","slug":"unable-to-read-symlink-q-s","errorCode":null,"errorMessage":"unable to read symlink %q: %s","messagePattern":"unable to read symlink %q: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"dive/filetree/file_info.go","lineNumber":58,"sourceCode":"\t\tGid:      header.Gid,\n\t\tIsDir:    header.FileInfo().IsDir(),\n\t}\n}\n\nfunc NewFileInfo(realPath, path string, info os.FileInfo) FileInfo {\n\tvar err error\n\n\t// todo: don't use tar types here, create our own...\n\tvar fileType byte\n\tvar linkName string\n\tvar size int64\n\n\tif info.Mode()&os.ModeSymlink != 0 {\n\t\tfileType = tar.TypeSymlink\n\n\t\tlinkName, err = os.Readlink(realPath)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"unable to read symlink %q: %s\", realPath, err))\n\t\t}\n\t} else if info.IsDir() {\n\t\tfileType = tar.TypeDir\n\t} else {\n\t\tfileType = tar.TypeReg\n\n\t\tsize = info.Size()\n\t}\n\n\tvar hash uint64\n\tif fileType != tar.TypeDir {\n\t\tfile, err := os.Open(realPath)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"unable to open file %q: %s\", realPath, err))\n\t\t}\n\t\tdefer file.Close()\n\t\thash = getHashFromReader(file)\n\t}","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/wagoodman/dive/blob/d6c691947f8fda635c952a17ee3b7555379d58f0/dive/filetree/file_info.go#L40-L76","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Re-run the analysis on a quiescent directory: stop the build/watcher that mutates symlinks while the walk runs","Verify you can read the offending link yourself: readlink <path> using the same user dive runs as; fix ownership or run with adequate privileges","Exclude the problematic subtree (symlink farms like /proc or vendored caches) from the scan","As a library consumer, pre-filter symlinks before delegating to NewFileInfoFromPath (see validationCode)"],"exampleFix":"// before: passing every walked entry straight in\ninfo, _ := d.Info()\nfi := filetree.NewFileInfoFromPath(path, path, info) // panics on unreadable symlink\n\n// after: probe the link first and skip entries you cannot resolve\ninfo, _ := d.Info()\nif info.Mode()&os.ModeSymlink != 0 {\n    if _, err := os.Readlink(path); err != nil {\n        log.Printf(\"skipping unreadable symlink %s: %v\", path, err)\n        return nil // skip, don't crash\n    }\n}\nfi := filetree.NewFileInfoFromPath(path, path, info)","handlingStrategy":"validation","validationCode":"// before calling NewFileInfoFromPath on a walked entry:\nfunc readableSymlink(realPath string, info os.FileInfo) bool {\n    if info.Mode()&os.ModeSymlink == 0 {\n        return true\n    }\n    _, err := os.Readlink(realPath)\n    return err == nil\n}\n\nif !readableSymlink(realPath, info) {\n    return nil // skip entry, avoid the panic\n}\nfi := filetree.NewFileInfoFromPath(path, realPath, info)","typeGuard":null,"tryCatchPattern":"// Go has no catch; contain the panic at the walk boundary if you must keep going:\nfunc safeFileInfo(path, real string, info os.FileInfo) (fi filetree.FileInfo, ok bool) {\n    defer func() {\n        if r := recover(); r != nil {\n            log.Printf(\"dive panicked on %s: %v\", real, r)\n            ok = false\n        }\n    }()\n    return filetree.NewFileInfoFromPath(path, real, info), true\n}","preventionTips":["Scan static snapshots: copy (cp -a/rsync) volatile directories before analysis","Run dive as a user that can read every symlink target in the tree (or prune what it cannot)","Exclude symlink-heavy pseudo-filesystems and caches from the walk"],"tags":["filesystem","symlink","panic","permissions","toctou"],"backgroundTag":null,"analyzedSha":"d6c691947f8fda635c952a17ee3b7555379d58f0","analyzedAt":"2026-08-15T09:42:35.293Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}