A dangling symlink makes fileset() fail and discard every other match
Terraform Version
Terraform v1.18.0-dev
on darwin_arm64
(Built from main at f8e7458f5. The code path is unchanged in released versions.)
Terraform Configuration Files
output "files" {
value = fileset(path.module, "*.txt")
}
with, in the same directory:
real.txt # an ordinary file
dangling.txt -> gone.txt # a symlink whose target does not exist
Debug Output
$ ls -la
lrwxr-xr-x 1 user staff ... dangling.txt -> /tmp/tffileset/gone.txt
-rw-r--r-- 1 user staff 3 real.txt
$ echo 'fileset(path.cwd, "*.txt")' | terraform console
╷
│ Error: Error in function call
│
│ on <console-input> line 1:
│ (source code not available)
│
│ Call to function "fileset" failed: failed to stat
│ "/tmp/tffileset/dangling.txt": stat /tmp/tffileset/dangling.txt: no such
│ file or directory.
╵
Expected Behavior
toset([
"real.txt",
])
A symlink whose target is missing is not a regular file, so it cannot be a member of the result either way. Every other path that matched the pattern should still be returned.
Actual Behavior
The call fails, and no file is returned — real.txt is lost along with the broken link. One unresolvable name in a matched directory takes the whole fileset result with it.
Steps to Reproduce
mkdir /tmp/tffileset && cd /tmp/tffileset
echo hi > real.txt
ln -s /tmp/tffileset/gone.txt dangling.txt
echo 'fileset(path.cwd, "*.txt")' | terraform console
Additional Context
internal/lang/funcs/filesystem.go, in MakeFileSetFunc:
for _, match := range matches {
fi, err := os.Stat(match)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat %s: %w", redactIfSensitive(match, marks...), err)
}
if !fi.Mode().IsRegular() {
continue
}
os.Stat follows symlinks, so a dangling link returns fs.ErrNotExist and the loop aborts the whole call. The very next statement would have skipped that entry anyway: a broken link is not a regular file.
Two ways to hit this, neither of them exotic:
- A dangling symlink in the tree. Vendored dependencies, build output, a checkout of a repository that carries links to files excluded by a sparse checkout, or a link into a path that only exists on another machine. Nothing needs to reference it; it only has to match the glob.
- A file removed between the glob and the stat.
doublestar.Globlists the names first and the loop stats them afterwards, so anything that disappears in that window produces the same error. That makes the failure intermittent and, in CI where a parallel step is writing to the same tree, hard to attribute.
In both cases the name does not resolve to anything, so it cannot be a regular file and cannot belong in the returned set. Skipping it produces exactly the set the configuration asked for. A stat that fails for another reason — a permission problem, say — is a different matter and should still surface, since the file may well exist and be readable by someone.
I have that change working with a test and will open a PR referencing this issue.
References
internal/lang/funcs/filesystem.go—MakeFileSetFunc
Transparency, per the AI Usage section of CONTRIBUTING.md: this report was prepared with AI assistance. The reproduction above was run against a binary built from main and the output is copied verbatim from that run.
Source: hashicorp/terraform