Track active symlink expansions precisely

This commit is contained in:
2026-10-20 00:26:30 +00:00
parent 75da54150f
commit 7f8ad81065
4 changed files with 114 additions and 14 deletions

View File

@@ -109,13 +109,26 @@ func (a *filesystemAccess) importEntryTarget(name string, entry fs.DirEntry, fol
return importEntryTarget{symlink: symlink, dir: info.IsDir(), identity: name}, nil
}
// A link stays active until its target has resolved, not until the entire
// original pathname has resolved. Remaining segments may reuse a finished link.
type importLinkFrame struct {
pending []string
link string
}
func resolveHostImportPath(access *filesystemAccess, name string) (string, error) {
pending := strings.Split(filepath.ToSlash(name), "/")
frames := []importLinkFrame{{pending: strings.Split(filepath.ToSlash(name), "/")}}
resolved := ""
activeLinks := make(map[string]struct{})
for len(pending) > 0 {
segment := pending[0]
pending = pending[1:]
for len(frames) > 0 {
frame := &frames[len(frames)-1]
if len(frame.pending) == 0 {
delete(activeLinks, frame.link)
frames = frames[:len(frames)-1]
continue
}
segment := frame.pending[0]
frame.pending = frame.pending[1:]
candidate := path.Join(resolved, segment)
if candidate == "" {
candidate = "."
@@ -149,8 +162,7 @@ func resolveHostImportPath(access *filesystemAccess, name string) (string, error
if !withinPortableRoot(targetPath) {
return "", errImportPathOutsideRoot
}
remaining := append([]string(nil), pending...)
pending = append(strings.Split(targetPath, "/"), remaining...)
frames = append(frames, importLinkFrame{pending: strings.Split(targetPath, "/"), link: candidate})
resolved = ""
}
if resolved == "" {
@@ -167,12 +179,18 @@ func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS,
if !fs.ValidPath(name) || !access.contains(name) {
return "", fs.ErrInvalid
}
pending := strings.Split(name, "/")
frames := []importLinkFrame{{pending: strings.Split(name, "/")}}
resolved := ""
activeLinks := make(map[string]struct{})
for len(pending) > 0 {
segment := pending[0]
pending = pending[1:]
for len(frames) > 0 {
frame := &frames[len(frames)-1]
if len(frame.pending) == 0 {
delete(activeLinks, frame.link)
frames = frames[:len(frames)-1]
continue
}
segment := frame.pending[0]
frame.pending = frame.pending[1:]
candidate := path.Join(resolved, segment)
if candidate == "" {
candidate = "."
@@ -205,8 +223,7 @@ func resolveNativeImportPath(access *filesystemAccess, filesystem fs.ReadLinkFS,
if !fs.ValidPath(targetPath) || !access.contains(targetPath) {
return "", errImportPathOutsideRoot
}
remaining := append([]string(nil), pending...)
pending = append(strings.Split(targetPath, "/"), remaining...)
frames = append(frames, importLinkFrame{pending: strings.Split(targetPath, "/"), link: candidate})
resolved = ""
}
if resolved == "" {

View File

@@ -0,0 +1,47 @@
package engine
import (
"errors"
"io/fs"
"os"
"path/filepath"
"testing"
)
func TestImportLinkResolutionTracksActiveExpansions(t *testing.T) {
root := t.TempDir()
if err := os.Mkdir(filepath.Join(root, "parts"), 0o700); err != nil {
t.Fatal(err)
}
for name, target := range map[string]string{"parts/up": "..", "self": "self", "first": "second", "second": "first"} {
if err := os.Symlink(target, filepath.Join(root, filepath.FromSlash(name))); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
}
host, err := openHostAccess(root)
if err != nil {
t.Fatal(err)
}
defer host.close()
filesystem := os.DirFS(root)
links, ok := filesystem.(fs.ReadLinkFS)
if !ok {
t.Fatal("native directory filesystem does not expose link inspection")
}
native := newNativeAccess(filesystem, ".", &namespaceToken{})
for name, resolve := range map[string]func(string) (string, error){
"host": func(name string) (string, error) { return resolveHostImportPath(host, name) },
"native": func(name string) (string, error) { return resolveNativeImportPath(native, links, name) },
} {
t.Run(name, func(t *testing.T) {
if got, err := resolve("parts/up/parts/up/parts"); err != nil || got != "parts" {
t.Errorf("finite link reuse = %q, %v", got, err)
}
for _, cycle := range []string{"self", "first"} {
if _, err := resolve(cycle); !errors.Is(err, errImportDirectoryLinkCycle) {
t.Errorf("%s cycle = %v", cycle, err)
}
}
})
}
}