mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
fix(sandbox): do not synthesize a .git inside an ancestor repository
A missing .git meant "this directory may become a repository", so the Windows plan materialized .git/config and .git/hooks to get the deny ACE in place before git first ran. That is right for a standalone directory. It is wrong when the workspace is a subdirectory of an existing repository: the created directory is a control directory competing with the ancestor's for git's discovery walk, synthesized inside a repository Zero does not own. The carveouts are now skipped when an ancestor carries git metadata, which is git's own discovery rule. That is the same argument the linked-worktree branch already makes: the metadata governing this workspace lives outside the write root, the sandboxed principal has no inherited access to it, so it needs no carveout here. The non-materialized deny-delete on <root>/.git is emitted separately and still guards the name if a repository is ever created. Ancestor detection accepts .git of either shape, since a linked worktree or submodule ancestor carries it as a pointer file and still owns the directory. Reported by jatmn. Worth recording that the stated consequence did not reproduce here: a .git holding only config and hooks is not a valid repository, so this git version walks past it and status, log and rev-parse still resolve to the ancestor. The fix stands on the narrower ground that creating repository metadata Zero does not own is wrong regardless, and anything later adding a HEAD there would break discovery for real.
This commit is contained in:
1 parent
7920d3be3d
commit
26958ac6dd
2 files changed
+159
No files matched your search
@@ -0,0 +1,118 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ZERO MUST NOT SYNTHESIZE A CONTROL DIRECTORY IN SOMEBODY ELSE'S REPOSITORY.
|
||||
//
|
||||
// A missing .git used to mean "this directory may become a repository", so the
|
||||
// Windows plan materialized .git/config and .git/hooks to get the deny ACE in
|
||||
// place before git first ran. That is right for a standalone directory. It is
|
||||
// wrong when the workspace is a subdirectory of an existing repository: the
|
||||
// created .git competes with the ancestor's for git's discovery walk, inside a
|
||||
// repository Zero does not own.
|
||||
//
|
||||
// The metadata for such a workspace belongs to the ancestor, sits outside this
|
||||
// write root, and the sandboxed principal has no inherited access to it, which
|
||||
// is the same reason the linked-worktree branch denies only the pointer file.
|
||||
func TestCarveoutsAreNotSynthesizedInsideAnAncestorRepository(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(parent, ".git"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
workspace := filepath.Join(parent, "sub", "project")
|
||||
if err := os.MkdirAll(workspace, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// SETUP: the workspace itself has no .git, or this is a different case.
|
||||
if _, err := os.Lstat(filepath.Join(workspace, ".git")); err == nil {
|
||||
t.Fatal("SETUP INVALID: the workspace already carries .git")
|
||||
}
|
||||
|
||||
specs := gitMetadataWriteCarveoutSpecs(workspace)
|
||||
for _, spec := range specs {
|
||||
if spec.Path == filepath.Join(workspace, ".git", "config") || spec.Path == filepath.Join(workspace, ".git", "hooks") {
|
||||
t.Fatalf("a nested workspace asks to materialize %s, which creates a control directory competing with the ancestor repository at %s", spec.Path, parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And a standalone directory keeps the materialized carveouts, or the guard
|
||||
// above would be satisfied by returning nothing everywhere and the protection
|
||||
// would be gone for the case it was written for.
|
||||
func TestCarveoutsStillCoverAStandaloneDirectory(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
if gitMetadataGovernedByAncestor(workspace) {
|
||||
t.Skip("this temp directory sits inside a repository, so it is not the standalone case")
|
||||
}
|
||||
|
||||
specs := gitMetadataWriteCarveoutSpecs(workspace)
|
||||
wantConfig := filepath.Join(workspace, ".git", "config")
|
||||
wantHooks := filepath.Join(workspace, ".git", "hooks")
|
||||
var sawConfig, sawHooks bool
|
||||
for _, spec := range specs {
|
||||
switch spec.Path {
|
||||
case wantConfig:
|
||||
sawConfig = true
|
||||
if !spec.IsFile {
|
||||
t.Errorf("%s is planned as a directory; creating .git/config as a directory makes git init fail", spec.Path)
|
||||
}
|
||||
case wantHooks:
|
||||
sawHooks = true
|
||||
}
|
||||
}
|
||||
if !sawConfig || !sawHooks {
|
||||
t.Fatalf("a standalone directory lost its carveouts: config=%t hooks=%t specs=%+v", sawConfig, sawHooks, specs)
|
||||
}
|
||||
}
|
||||
|
||||
// A linked worktree is unchanged: the pointer file is denied and nothing beneath
|
||||
// it is named, which is what keeps elevated setup from descending through a
|
||||
// regular file.
|
||||
func TestCarveoutsStillDenyTheLinkedWorktreePointer(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
pointer := filepath.Join(workspace, ".git")
|
||||
if err := os.WriteFile(pointer, []byte("gitdir: ../real/.git/worktrees/w\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
specs := gitMetadataWriteCarveoutSpecs(workspace)
|
||||
if len(specs) != 1 || specs[0].Path != pointer || !specs[0].IsFile {
|
||||
t.Fatalf("linked worktree carveouts = %+v, want exactly the file-shaped pointer %s", specs, pointer)
|
||||
}
|
||||
}
|
||||
|
||||
// The ancestor test itself has to distinguish shapes, because a linked worktree
|
||||
// or submodule ancestor carries .git as a FILE and still owns this directory.
|
||||
func TestAncestorDetectionAcceptsBothGitShapes(t *testing.T) {
|
||||
for _, shape := range []struct {
|
||||
name string
|
||||
make func(t *testing.T, parent string)
|
||||
}{
|
||||
{name: "directory", make: func(t *testing.T, parent string) {
|
||||
if err := os.MkdirAll(filepath.Join(parent, ".git"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
{name: "pointer file", make: func(t *testing.T, parent string) {
|
||||
if err := os.WriteFile(filepath.Join(parent, ".git"), []byte("gitdir: elsewhere\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
} {
|
||||
t.Run(shape.name, func(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
shape.make(t, parent)
|
||||
workspace := filepath.Join(parent, "nested", "deep")
|
||||
if err := os.MkdirAll(workspace, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !gitMetadataGovernedByAncestor(workspace) {
|
||||
t.Fatalf("an ancestor carrying .git as a %s was not recognised as governing %s", shape.name, workspace)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -157,12 +157,53 @@ func gitMetadataWriteCarveoutSpecs(root string) []gitMetadataCarveout {
|
||||
if info, err := os.Lstat(gitPath); err == nil && !info.IsDir() {
|
||||
return []gitMetadataCarveout{{Path: gitPath, IsFile: true}}
|
||||
}
|
||||
// A MISSING .git IS NOT THE SAME AS "THIS WILL BECOME A REPOSITORY".
|
||||
//
|
||||
// The directory-shaped carveouts below are materialized by the Windows plan so
|
||||
// the deny ACE is in place before git first runs. That is right for a
|
||||
// standalone directory somebody may later git init. It is wrong when this
|
||||
// workspace already sits inside a repository: creating .git/config and
|
||||
// .git/hooks synthesizes a control directory that competes with the ancestor's
|
||||
// for git's discovery walk, in a repository Zero does not own.
|
||||
//
|
||||
// The same argument the linked-worktree branch above makes applies here. This
|
||||
// workspace's git metadata belongs to the ancestor, it lives outside this write
|
||||
// root, and the sandboxed principal has no inherited access to it, so it needs
|
||||
// no carveout here. The non-materialized deny-delete on <root>/.git is emitted
|
||||
// separately and still guards the name if a repository is ever created here.
|
||||
if gitMetadataGovernedByAncestor(root) {
|
||||
return nil
|
||||
}
|
||||
return []gitMetadataCarveout{
|
||||
{Path: filepath.Join(root, ".git", "hooks")},
|
||||
{Path: filepath.Join(root, ".git", "config"), IsFile: true},
|
||||
}
|
||||
}
|
||||
|
||||
// gitMetadataGovernedByAncestor reports whether an ancestor of root carries git
|
||||
// metadata, which is git's own discovery rule: the nearest ancestor with a .git
|
||||
// entry owns this directory.
|
||||
//
|
||||
// Lstat rather than a resolved walk, because a .git of either shape answers the
|
||||
// question: a directory is an ordinary repository root and a file is a linked
|
||||
// worktree or submodule pointer. Both mean the metadata is not ours to create.
|
||||
func gitMetadataGovernedByAncestor(root string) bool {
|
||||
current := filepath.Clean(strings.TrimSpace(root))
|
||||
if current == "" || current == "." {
|
||||
return false
|
||||
}
|
||||
for {
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return false
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(parent, ".git")); err == nil {
|
||||
return true
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
// gitMetadataCarveoutSuffixBase is a sentinel root used only to recover the
|
||||
// trailing segments of the carveout specs. It is never touched on disk.
|
||||
const gitMetadataCarveoutSuffixBase = string(filepath.Separator) + "zero-carveout-base"
|
||||
|
||||
Reference in new issue
Block a user