205 lines
5.8 KiB
Go
205 lines
5.8 KiB
Go
//go:build !windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const tarOutsideSecret = "outside-the-tree-3f9a"
|
|
|
|
// tarInsideA is as long as the outside file: a writer who knows a file's size
|
|
// can make the one it swaps out the same size, so the tar's header fits.
|
|
const tarInsideA = "inside-the-tree-a-000"
|
|
|
|
// tarEntries reads back what makeTar produced: name -> content.
|
|
func tarEntries(t *testing.T, b []byte) map[string]string {
|
|
t.Helper()
|
|
out := map[string]string{}
|
|
tr := tar.NewReader(bytes.NewReader(b))
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
return out
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("read tar: %v", err)
|
|
}
|
|
data, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", hdr.Name, err)
|
|
}
|
|
out[hdr.Name] = string(data)
|
|
}
|
|
}
|
|
|
|
// outsideAndTree makes a file outside the tree that a run must never return,
|
|
// and a tree with one file and one folder.
|
|
func outsideAndTree(t *testing.T) (outsideDir, secret, tree string) {
|
|
t.Helper()
|
|
outsideDir = t.TempDir()
|
|
secret = filepath.Join(outsideDir, "secret")
|
|
if err := os.WriteFile(secret, []byte(tarOutsideSecret), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tree = t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(tree, "a.txt"), []byte(tarInsideA), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(tree, "sub"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Named as the outside file is: a writer who knows the names in a folder
|
|
// outside can fill a folder of the tree with the same names, then swap it.
|
|
if err := os.WriteFile(filepath.Join(tree, "sub", "secret"), []byte("inside b"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return outsideDir, secret, tree
|
|
}
|
|
|
|
func assertNoOutside(t *testing.T, ents map[string]string) {
|
|
t.Helper()
|
|
for name, data := range ents {
|
|
if strings.Contains(data, tarOutsideSecret) {
|
|
t.Fatalf("the tar holds a file from outside the tree, as %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The tree's writer swaps a file for a link, and a folder for a link, after
|
|
// the walk has seen each one as a plain file or folder and before it opens it.
|
|
// The hook stands exactly in that gap, so the swap lands there every time.
|
|
func TestMakeTarNeverReadsThroughALinkSwappedInBeforeTheOpen(t *testing.T) {
|
|
for _, swap := range []string{"a.txt", "sub"} {
|
|
t.Run(swap, func(t *testing.T) {
|
|
outsideDir, secret, tree := outsideAndTree(t)
|
|
packTreeBeforeOpen = func(rel string) {
|
|
if rel != swap {
|
|
return
|
|
}
|
|
p := filepath.Join(tree, swap)
|
|
_ = os.RemoveAll(p)
|
|
if swap == "sub" {
|
|
_ = os.Symlink(outsideDir, p)
|
|
} else {
|
|
_ = os.Symlink(secret, p)
|
|
}
|
|
}
|
|
defer func() { packTreeBeforeOpen = nil }()
|
|
b, err := makeTar(tree)
|
|
if err != nil {
|
|
t.Fatalf("makeTar: %v", err)
|
|
}
|
|
ents := tarEntries(t, b)
|
|
assertNoOutside(t, ents)
|
|
if swap == "a.txt" {
|
|
if _, ok := ents["a.txt"]; ok {
|
|
t.Fatalf("a.txt was a link when it was opened and must be left out, got it in the tar")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// No hook: a writer flips one name between a plain file and a link to a file
|
|
// outside the tree as fast as it can while the tree is packed again and again.
|
|
func TestMakeTarNeverReadsThroughALinkUnderAConcurrentWriter(t *testing.T) {
|
|
_, secret, tree := outsideAndTree(t)
|
|
name := filepath.Join(tree, "a.txt")
|
|
tmpLink, tmpFile := filepath.Join(tree, ".l"), filepath.Join(tree, ".f")
|
|
stop := make(chan struct{})
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
}
|
|
_ = os.WriteFile(tmpFile, []byte(tarInsideA), 0o644)
|
|
_ = os.Rename(tmpFile, name)
|
|
_ = os.Symlink(secret, tmpLink)
|
|
_ = os.Rename(tmpLink, name)
|
|
}
|
|
}()
|
|
defer func() { close(stop); <-done }()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
passes := 0
|
|
defer func() { t.Logf("%d passes", passes) }()
|
|
for i := 0; time.Now().Before(deadline); i++ {
|
|
passes = i + 1
|
|
b, err := makeTar(tree)
|
|
if err != nil {
|
|
continue // a name that vanished mid-walk; the next pass tries again
|
|
}
|
|
for n, data := range tarEntries(t, b) {
|
|
if strings.Contains(data, tarOutsideSecret) {
|
|
t.Fatalf("pass %d: the tar holds the file outside the tree, as %q", i, n)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A link, a named pipe and a socket in the tree are left out and named as
|
|
// left out; the pipe is never opened, so the walk does not wait on it.
|
|
func TestMakeTarLeavesOutAndNamesWhatIsNotAPlainFileOrFolder(t *testing.T) {
|
|
_, secret, tree := outsideAndTree(t)
|
|
if err := os.Symlink(secret, filepath.Join(tree, "link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := unix.Mkfifo(filepath.Join(tree, "sub", "pipe"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(tree, ".git"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tree, ".git", "config"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
type res struct {
|
|
b []byte
|
|
skipped []string
|
|
err error
|
|
}
|
|
ch := make(chan res, 1)
|
|
go func() { b, s, err := packWorktree(tree); ch <- res{b, s, err} }()
|
|
var r res
|
|
select {
|
|
case r = <-ch:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("packing the tree waited on the named pipe")
|
|
}
|
|
if r.err != nil {
|
|
t.Fatalf("packWorktree: %v", r.err)
|
|
}
|
|
ents := tarEntries(t, r.b)
|
|
assertNoOutside(t, ents)
|
|
for _, want := range []string{"a.txt", "sub", "sub/secret"} {
|
|
if _, ok := ents[want]; !ok {
|
|
t.Errorf("%s missing from the tar (got %v)", want, ents)
|
|
}
|
|
}
|
|
if ents["a.txt"] != tarInsideA || ents["sub/secret"] != "inside b" {
|
|
t.Errorf("file contents changed in the tar: %v", ents)
|
|
}
|
|
for _, gone := range []string{"link", "sub/pipe", ".git", ".git/config"} {
|
|
if _, ok := ents[gone]; ok {
|
|
t.Errorf("%s is in the tar and must not be", gone)
|
|
}
|
|
}
|
|
got := strings.Join(r.skipped, ",")
|
|
if got != "link,sub/pipe" {
|
|
t.Errorf("left out = %q, want link,sub/pipe", got)
|
|
}
|
|
}
|