{"record":{"id":"a18e1958ee153d12","repo":"larksuite/cli","slug":"s-q-has-multiple-hard-links-so-writing-it-would","errorCode":null,"errorMessage":"%s %q has multiple hard links, so writing it would also rewrite the other names (hint: remove the file first, or write to a new name)","messagePattern":"(.+?) %q has multiple hard links, so writing it would also rewrite the other names \\(hint: remove the file first, or write to a new name\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/vfs/localfileio/path.go","lineNumber":37,"sourceCode":"// SafeOutputPath validates a download/export target path for --output flags.\nfunc SafeOutputPath(path string) (string, error) {\n\treturn safePath(path, \"--output\")\n}\n\n// rejectMultiplyLinkedTarget refuses to approve writing over an existing file\n// that carries more than one name. A hard link has no target to resolve, so\n// name-based containment cannot see that the same inode is also reachable from\n// outside the allowlist; a caller that truncates the approved path in place\n// would rewrite that outside file's contents. Writers that commit through a\n// temp file and rename are immune, but the check belongs here so it also covers\n// the ones that write directly.\n// A target that cannot be inspected, or that is not a regular file, carries no\n// link count to judge: the write layer reports the real failure with proper\n// typing, and a directory legitimately holds several names.\nfunc rejectMultiplyLinkedTarget(flagName, raw, resolved string) error {\n\tinfo, err := vfs.Lstat(resolved)\n\tif err == nil && info.Mode().IsRegular() && hasExtraHardLinks(resolved, info) {\n\t\treturn fmt.Errorf(\"%s %q has multiple hard links, so writing it would also rewrite the other names \"+\n\t\t\t\"(hint: remove the file first, or write to a new name)\", flagName, raw)\n\t}\n\treturn nil\n}\n\n// SafeInputPath validates an upload/read source path for --file flags.\n// The baseline invariant asserted by drive sync, upload flags, and the CI\n// quality gates is \"reject everything outside the built-in allowlist\"\n// (cwd, /tmp, ~/files) — see safePath. Out-of-tree content still reaches\n// flags via stdin (\"-\").\nfunc SafeInputPath(path string) (string, error) {\n\treturn safePath(path, \"--file\")\n}\n\n// LocalInputPath validates an input path in the process local filesystem\n// namespace. It intentionally does not impose allowlist containment or\n// canonicalize the returned path: absolute paths, parent-relative paths, and\n// symlink traversal retain their normal OS semantics (the grandfathered","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/vfs/localfileio/path.go#L19-L55","documentation":"This library refuses to approve writing to an existing regular file that has more than one hard link (st_nlink > 1). Because name-based allowlist containment cannot see that the same inode is also reachable under another name outside the allowed roots, truncating the approved path in place would silently rewrite the other file's contents. rejectMultiplyLinkedTarget fails closed instead, only for --output flags.","triggerScenarios":"Calling SafeOutputPath (safePath with flagName \"--output\") on a path whose Lstat shows a regular file with an extra hard link; the check runs for each interpretation of the path after deny/allow/relative checks pass.","commonSituations":"The output target is a hard-linked backup/snapshot (rsnapshot, Time Machine-like trees), a git object store blob, a deduplicating backup tool's store, or a temp-file pattern where an editor hard-linked the working copy. Also common in containers/CI where layers share inodes.","solutions":["Remove the existing file first (rm) so the writer creates a fresh inode, then run the command again","Write to a new, unlinked file name instead of overwriting the linked path","If you own the writer, commit through a temp file and rename() over the target instead of truncating in place","Check where the other link lives with `ls -li` / `find -samefile` and delete the unwanted name"],"exampleFix":"// before: overwriting a hard-linked backup file\nlark drive download --output /backups/daily/report.docx\n// error: --output ... has multiple hard links\n// after\nrm /backups/daily/report.docx\nlark drive download --output /backups/daily/report.docx","handlingStrategy":"validation","validationCode":"func isMultiplyLinked(p string) bool {\n    info, err := os.Lstat(p)\n    return err == nil && info.Mode().IsRegular() && hasHardLinks(p, info)\n}\n// skip the --output target if isMultiplyLinked(outPath)","typeGuard":"if info, err := os.Lstat(target); err != nil || !info.Mode().IsRegular() || sys, ok := info.Sys().(*syscall.Stat_t); !ok || sys.Nlink <= 1 {\n    // safe to write\n}","tryCatchPattern":"if err := writeFile(outPath, data); err != nil {\n    var linkErr interface{ Error() string }\n    if strings.Contains(err.Error(), \"multiple hard links\") {\n        os.Remove(outPath)\n        err = writeFile(outPath, data) // recreate fresh inode\n    }\n    return err\n}","preventionTips":["Never hard-link files you later overwrite in place","Commit writes via temp file + rename instead of truncating targets","Check `find -samefile` / link counts before reusing an existing path as output"],"tags":["filesystem","hardlink","path-validation","security"],"backgroundTag":"multiply-linked-output-target","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}