Skip to content

Commit c7335ae

Browse files
authored
Don't crash reading the config of a subrepo with no remote tree (#3577)
Building a Rust plugin against a repo with ~100 crate subrepos, we hit an intermittent segfault under remote execution whenever Please read a subrepo's config: ``` panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x18] please/src/remote/fs.New({...}, 0x0, {...}) fs.go:67 please/src/remote.(*Client).SubrepoFS(...) remote.go:330 please/src/core.(*Subrepo).FS.func1() subrepo.go:58 please/src/core.readSubrepoConfig(...) subrepo.go:119 ``` Note the literal `0x0` argument. `SubrepoFS` looks the subrepo's output tree up by label and passes whatever it finds straight to `remotefs.New`: ```go tree := c.subrepoTrees[target.Label] return remotefs.New(c.remoteFSClient, tree, root) ``` `subrepoTrees` is only ever populated in one place (`utils.go`, when a target's outputs are set from a *remote* build), so a subrepo that was built locally, served from the cache, or not built yet has no entry, and `tree` is nil. `New` then does `append(tree.Children, tree.Root)` and dies. `readSubrepoConfig` calls `Subrepo.FS()` unconditionally — it has to, in order to look for `.plzconfig` — so any repo whose subrepo targets can come from cache rather than a fresh remote build can hit this. That matches what we saw: it was sensitive to cache state and flipped between otherwise identical runs. This makes a nil tree (or one with no root) produce an empty filesystem, so the config lookup reports "does not exist" and parsing continues — which is already the handling for a subrepo that has no `.plzconfig`. The added test panics at the same `addr=0x18` without the change and passes with it. `plz test //src/remote/...` is green.
1 parent 9ee7615 commit c7335ae

2 files changed

Lines changed: 32 additions & 0 deletions

File tree

src/remote/fs/fs.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,16 @@ func (fs *CASFileSystem) Stat(name string) (iofs.FileInfo, error) {
6363
}
6464

6565
// New creates a new filesystem on top of the given proto, using client to download files from the CAS on demand.
66+
// A nil tree (or one with no root) yields an empty filesystem: callers such as SubrepoFS look trees up by label and
67+
// find nothing for a subrepo that wasn't built remotely during this invocation, and an empty filesystem reports that
68+
// as "does not exist" rather than crashing.
6669
func New(c Client, tree *pb.Tree, workingDir string) *CASFileSystem {
70+
if tree == nil {
71+
tree = &pb.Tree{}
72+
}
73+
if tree.Root == nil {
74+
tree.Root = &pb.Directory{}
75+
}
6776
directories := make(map[digest.Digest]*pb.Directory, len(tree.Children))
6877
for _, child := range append(tree.Children, tree.Root) {
6978
dg, err := digest.NewFromMessage(child)

src/remote/fs/fs_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,26 @@ func TestStat(t *testing.T) {
298298
})
299299
}
300300
}
301+
302+
func TestNilTree(t *testing.T) {
303+
// SubrepoFS looks trees up by label and gets nothing for a subrepo that
304+
// wasn't built remotely in this invocation (a cache hit, say). Reading
305+
// that subrepo's config then reached here with a nil tree and panicked.
306+
fc := &fakeClient{}
307+
for _, tc := range []struct {
308+
name string
309+
tree *pb.Tree
310+
}{
311+
{name: "nil tree", tree: nil},
312+
{name: "nil root", tree: &pb.Tree{}},
313+
} {
314+
t.Run(tc.name, func(t *testing.T) {
315+
fs := New(fc, tc.tree, ".")
316+
require.NotNil(t, fs)
317+
_, err := fs.Open(".plzconfig")
318+
assert.True(t, os.IsNotExist(err), "expected not-exist, got %v", err)
319+
_, err = iofs.Stat(fs, "anything")
320+
assert.True(t, os.IsNotExist(err), "expected not-exist, got %v", err)
321+
})
322+
}
323+
}

0 commit comments

Comments
 (0)