From d7e6074d97285a5f8457c01cfc298edac89034b1 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Wed, 21 Oct 2026 09:46:47 +0000 Subject: [PATCH] Refuse a build output directory that is a link out of the module (issue 1589) --- .../cmd/mesh-build-go/archive.go | 43 +++++++++++++++++++ .../mesh-build-go/cmd/mesh-build-go/build.go | 6 +++ .../cmd/mesh-build-go/build_test.go | 24 +++++++++++ 3 files changed, 73 insertions(+) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/archive.go b/modules/mesh-build-go/cmd/mesh-build-go/archive.go index 0cef5fde..f8e11b65 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/archive.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/archive.go @@ -300,3 +300,46 @@ func within(base, path string) bool { rel, err := filepath.Rel(base, path) return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) } + +// resolveDeepest follows the links of the deepest part of p that exists and appends the rest, so a path not yet made +// is judged by where it would be made. +func resolveDeepest(p string) (string, error) { + rest := "" + for cur := filepath.Clean(p); ; { + if real, err := filepath.EvalSymlinks(cur); err == nil { + return filepath.Join(real, rest), nil + } else if !os.IsNotExist(err) { + return "", err + } + parent := filepath.Dir(cur) + if parent == cur { + return "", fmt.Errorf("no part of %s exists", p) + } + rest = filepath.Join(filepath.Base(cur), rest) + cur = parent + } +} + +// insideModule refuses a path whose real place is not inside the module's directory, which is itself inside the +// workspace's tree: a link committed in the repository must not lead a build's writes or its pack out. +func (m *Module) insideModule(workspace, dir, path string) error { + realTree, err := filepath.EvalSymlinks(filepath.Join(m.Work, workspace, "tree")) + if err != nil { + return err + } + realDir, err := filepath.EvalSymlinks(dir) + if err != nil { + return err + } + if !within(realTree, realDir) { + return fmt.Errorf("the module directory resolves to %s, which lies outside the workspace's tree %s", realDir, realTree) + } + real, err := resolveDeepest(path) + if err != nil { + return err + } + if !within(realDir, real) { + return fmt.Errorf("%s resolves to %s, which lies outside the module %s", path, real, realDir) + } + return nil +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build.go b/modules/mesh-build-go/cmd/mesh-build-go/build.go index 746aa839..f6839629 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build.go @@ -117,6 +117,9 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { // Its own output directory, made empty: a build of the same tree twice is one build. out := filepath.Join(dir, outDir, art.Name) + if err := m.insideModule(workspace, dir, out); err != nil { + return nil, fmt.Errorf("the output directory of %s: %w", art.Name, err) + } if err := os.RemoveAll(out); err != nil { return nil, err } @@ -133,6 +136,9 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { if r := m.Run(ctx, m.buildEnv(dir), "go", cmd...); r.Status != 0 || r.Err != "" { return nil, fmt.Errorf("go build of %s: %s", art.Name, causeLine(r.Err+"\n"+r.Stderr+"\n"+r.Stdout)) } + if err := m.insideModule(workspace, dir, out); err != nil { + return nil, fmt.Errorf("the output directory of %s: %w", art.Name, err) + } res, err := m.packInto(workspace, module, art.Name, out) if err != nil { return nil, err diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build_test.go b/modules/mesh-build-go/cmd/mesh-build-go/build_test.go index fa369bb7..0d36081f 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build_test.go @@ -215,3 +215,27 @@ func TestAModuleDirectoryOutsideTheWorkspaceIsRefused(t *testing.T) { t.Errorf("the step ended %s after %d commands", st.State, len(*calls)) } } + +// Review of issue 1589: the output directory lies in the module; a committed `.mesh-build` that is a link out of it +// must not take the build's writes, removals or the pack outside. +func TestABuildOutputDirectoryLinkedOutsideTheModuleIsRefused(t *testing.T) { + m, states, calls := buildModule(t, manifestWith(goBundle), "the program") + outside := t.TempDir() + if err := os.MkdirAll(filepath.Join(outside, "tools"), 0o755); err != nil { + t.Fatal(err) + } + kept := filepath.Join(outside, "tools", "precious") + if err := os.WriteFile(kept, []byte("mine"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outside, filepath.Join(m.Work, "ws1", "tree", "modules", "demo", ".mesh-build")); err != nil { + t.Fatal(err) + } + _, st := runBuild(t, m, states, "tools") + if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "outside") || !strings.Contains(st.Reason, outside) || len(*calls) != 0 { + t.Errorf("the step ended %s: %q after %d commands", st.State, st.Reason, len(*calls)) + } + if body, err := os.ReadFile(kept); err != nil || string(body) != "mine" { + t.Errorf("a file outside the module was touched: %q, %v", body, err) + } +}