Check native overlay link targets

This commit is contained in:
2026-11-09 21:16:30 +00:00
parent 9a08438769
commit 81c27bb9cd
3 changed files with 87 additions and 7 deletions

View File

@@ -1529,6 +1529,10 @@ base import graph is composed and its schema version is checked, but before defa
and typed decoding. Overlay bytes, nodes, and source records count toward the same
aggregate limits as base inputs. Its canonical path must remain within the configured
root and must not duplicate a file already present in the base graph.
For native filesystems that implement `fs.ReadLinkFS`, these checks resolve
link targets in the selected namespace. For other native filesystems, the
checks use namespace-relative paths and reads follow that filesystem's
ordinary behavior.
Collection destinations follow the
[collection operation and presence rules](#strict-document-composition) above.

View File

@@ -218,3 +218,43 @@ func TestLayeredOverlayNativeContext(t *testing.T) {
}
t.Fatalf("diagnostics = %#v", err.(*configkit.LoadError).Diagnostics())
}
func TestLayeredOverlayNativeLinks(t *testing.T) {
for _, tc := range []struct {
name, mainPath, target string
want configkit.Code
}{
{"outside root", "scope/main.yml", "../../outside.yml", configkit.CodeOverlayOutsideRoot},
{"overlay aliases main", "scope/main.yml", "../main.yml", configkit.CodeOverlayRepeated},
{"main aliases overlay target", "scope/alias.yml", "../target.yml", configkit.CodeOverlayRepeated},
{"contained overlay", "scope/main.yml", "../target.yml", ""},
} {
t.Run(tc.name, func(t *testing.T) {
native := configkit.NewFilesystem("bundle", fstest.MapFS{
"scope/main.yml": {Data: []byte("count: 1\n")},
"scope/alias.yml": {Data: []byte("target.yml"), Mode: os.ModeSymlink},
"scope/target.yml": {Data: []byte("count: 7\n")},
"scope/overlays/test.yml": {Data: []byte(tc.target), Mode: os.ModeSymlink},
"outside.yml": {Data: []byte("count: 9\n")},
})
main := configkit.Input{Source: native.File(tc.mainPath), Filesystem: &configkit.FilesystemContext{Root: "scope", FS: &native}}
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test"})
if tc.want != "" {
if err == nil {
t.Fatal("overlay link unexpectedly loaded")
}
diagnostics := err.(*configkit.LoadError).Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != tc.want {
t.Fatalf("diagnostics = %#v, want %v", diagnostics, tc.want)
}
return
}
if err != nil {
t.Fatal(err)
}
if result.Config.Count != 7 || result.Sources()[1].Path != "scope/target.yml" {
t.Fatalf("config = %#v, sources = %#v", result.Config, result.Sources())
}
})
}
}

View File

@@ -76,12 +76,21 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces
lookupDirectory = access.rootPath
}
overlayPath := access.join(lookupDirectory, "overlays/"+request.name+".yml")
canonical, err := access.canonical(overlayPath)
links, _ := access.native.(fs.ReadLinkFS)
var canonical string
var err error
if links != nil {
canonical, err = resolveNativeImportPath(access, links, overlayPath)
} else {
canonical, err = access.canonical(overlayPath)
}
if err != nil {
code := CodeOverlayInvalid
message := "selected overlay could not be resolved"
if errors.Is(err, fs.ErrNotExist) {
code, message = CodeOverlayMissing, "selected overlay does not exist"
} else if errors.Is(err, errImportPathOutsideRoot) {
code, message = CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root"
} else if errors.Is(err, fs.ErrPermission) {
code, message = CodeImportPermission, "selected overlay cannot be accessed"
}
@@ -90,12 +99,8 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces
if !access.contains(canonical) {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root", nil)}
}
for _, existing := range sources {
if existing.Path != "" && existing.Path == canonical &&
((access.native == nil && existing.Kind == SourceKindFile) ||
(access.native != nil && existing.Kind == SourceKindFilesystem && existing.namespace == access.token)) {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)}
}
if overlayRepeatsSource(access, links, canonical, sources) {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)}
}
if len(sources)+1 > limits.sources {
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the source-count limit", nil)}
@@ -174,6 +179,37 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces
return sources, nil
}
func overlayRepeatsSource(access *filesystemAccess, links fs.ReadLinkFS, canonical string, sources []SourceInfo) bool {
// Other inputs can have a wider root. Compare their identities in the
// shared native namespace while keeping overlay lookup rooted separately.
namespaceAccess := *access
namespaceAccess.rootPath = "."
for _, source := range sources {
if source.Path == "" {
continue
}
if access.native == nil {
if source.Kind == SourceKindFile && source.Path == canonical {
return true
}
continue
}
if source.Kind != SourceKindFilesystem || source.namespace != access.token {
continue
}
if source.Path == canonical {
return true
}
if links != nil {
target, err := resolveNativeImportPath(&namespaceAccess, links, source.Path)
if err == nil && target == canonical {
return true
}
}
}
return false
}
func readOverlayFile(access *filesystemAccess, name, sourceID string, remaining int64) ([]byte, *Diagnostic) {
limit := min(int64(maxSourceBytes), remaining)
if limit < 0 {