Refuse a bundle whose output directory leads out of the module through a link, so a repository cannot make the build write into or pack files it does not hold (issue 1609)
mesh/merge-gate head 0caa8c6844d9 merged into trunk 0205b8f7080a — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 0caa8c6844d9 merged into trunk 0205b8f7080a — pass: its merge-check.sh passed
mesh/delivery delivered

This commit is contained in:
2026-10-21 09:46:00 +00:00
parent fcca6ba9e0
commit 0caa8c6844
2 changed files with 84 additions and 0 deletions
+45
View File
@@ -102,3 +102,48 @@ func TestAModuleDirectoryThatIsALinkOutOfTheRepositoryIsRefused(t *testing.T) {
t.Errorf("an absent module directory was refused here: %v", err)
}
}
// A repository that commits `.mesh-build` as a link must not make a bundle's build write into, or pack,
// a directory outside the module (issue 1609): the compile, the bundling and the pack each refuse it.
func TestABundleWhoseOutputDirectoryIsALinkOutOfTheModuleIsRefused(t *testing.T) {
outside := t.TempDir()
if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("s"), 0o644); err != nil {
t.Fatal(err)
}
tree := t.TempDir()
if err := os.Symlink(outside, filepath.Join(tree, ".mesh-build")); err != nil {
t.Fatal(err)
}
chain, err := ToolchainFor("go")
if err != nil {
t.Fatal(err)
}
a := catalogue.Artifact{Name: "b", Kind: catalogue.ArtifactBundle, Language: "go", From: "cmd/b"}
ran := false
run := func(context.Context, string, string, ...string) (string, error) { ran = true; return "", nil }
realOutside, _ := filepath.EvalSymlinks(outside)
realTree, _ := filepath.EvalSymlinks(tree)
check := func(what string, err error) {
t.Helper()
if err == nil {
t.Fatalf("%s went through a link out of the module", what)
}
for _, want := range []string{realOutside, realTree} {
if !strings.Contains(err.Error(), want) {
t.Errorf("%s: the refusal %q does not name %q", what, err, want)
}
}
}
_, err = compile(context.Background(), run, tree, chain, "base", a)
check("compile", err)
_, err = bundled(context.Background(), run, tree, chain, "base", a, nil)
check("bundled", err)
check("the pack", outsideTheModule(tree, filepath.Join(tree, Out(a.Name))))
if ran {
t.Error("a command ran for a refused bundle")
}
if entries, _ := os.ReadDir(outside); len(entries) != 1 {
t.Errorf("a refused bundle wrote into the directory outside: %d entries", len(entries))
}
}
+39
View File
@@ -1295,6 +1295,9 @@ func one(ctx context.Context, run Runner, publish Publisher,
}
}
say("bundle", "compiled, packing")
if err := outsideTheModule(tree, compiled); err != nil {
return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err)
}
body, err := pack(compiled)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err)
@@ -1384,6 +1387,36 @@ func archiveRoot(tree string, a catalogue.Artifact) (string, error) {
return root, nil
}
// outsideTheModule refuses a path that, once every link in it is followed, is not inside the module's
// directory, naming where it really is and where the module is. For a path not there yet (an output
// directory about to be made) the deepest part that exists is the one followed. A repository commits
// its own links, so `.mesh-build` as a link would otherwise have a bundle's build write into, and pack,
// any directory the build account can read (issue 1609).
func outsideTheModule(tree, path string) error {
realTree, err := filepath.EvalSymlinks(tree)
if err != nil {
return err
}
real, rest := path, ""
for {
resolved, err := filepath.EvalSymlinks(real)
if err == nil {
real = filepath.Join(resolved, rest)
break
}
if !errors.Is(err, os.ErrNotExist) || filepath.Dir(real) == real {
return err
}
rest = filepath.Join(filepath.Base(real), rest)
real = filepath.Dir(real)
}
if rel, err := filepath.Rel(realTree, real); err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("%s leads out of the module through a link: it resolves to %s, and the module is %s",
path, real, realTree)
}
return nil
}
// pack tars and gzips a directory.
//
// **Deterministically**: entries sorted, and no timestamps, uid, gid or original names carried
@@ -1687,6 +1720,9 @@ func compile(ctx context.Context, run Runner, tree string, chain Toolchain,
// a package in a third (ADR 0040). Compiling them all into one place would have them overwrite
// each other and then be packed together, so each bundle compiles and packs alone.
out := Out(a.Name)
if err := outsideTheModule(tree, filepath.Join(tree, out)); err != nil {
return "", err
}
// **The output directory exists before the compiler is told about it.** `tsc --outDir` makes
// one; `go build -o` writes a file into a directory and does not create it, failing with a
@@ -2013,6 +2049,9 @@ func bundled(ctx context.Context, run Runner, tree string, chain Toolchain, base
a catalogue.Artifact, launchers map[string]string) (string, error) {
const within = "/app/modules/module"
out, final := Out(a.Name), Out(a.Name)+bundledSuffix
if err := outsideTheModule(tree, filepath.Join(tree, final)); err != nil {
return "", err
}
if err := os.RemoveAll(filepath.Join(tree, final)); err != nil {
return "", err
}