bastion 0.55.3
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user