Files
privatae-bastion/client-go/harness_tar_unix_test.go
T
Privatae 8822d5df85
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m51s
ci / fuzz (push) Successful in 2m55s
bastion 0.55.3
2026-10-25 23:16:45 +00:00

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)
}
}