Refuse a build output directory that is a link out of the module (issue 1589)

This commit is contained in:
2026-10-21 09:46:47 +00:00
parent 800d6624d5
commit d7e6074d97
3 changed files with 73 additions and 0 deletions
@@ -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
}
@@ -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
@@ -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)
}
}