//go:build linux package privatefile import ( "bytes" "errors" "os" "path/filepath" "sync" "sync/atomic" "testing" "golang.org/x/sys/unix" ) func privateDir(t *testing.T) string { t.Helper() d := t.TempDir() // The test runner may put TMPDIR behind a symlink. Positive fixtures // use its real directory; symlink-refusal cases add their own links. d, err := filepath.EvalSymlinks(d) if err != nil { t.Fatal(err) } if e := os.Chmod(d, 0700); e != nil { t.Fatal(e) } return d } func TestExactAndExclusive(t *testing.T) { p := filepath.Join(privateDir(t), "result") b := bytes.Repeat([]byte{0, 255, 10, 32}, 16384) if e := CreateExclusiveDurable(p, b, 65536); e != nil { t.Fatal(e) } got, e := Read(p, 65536) if e != nil || !bytes.Equal(got, b) { t.Fatal("exact read failed", e) } if CreateExclusiveDurable(p, []byte("replacement"), 65536) != ErrUnavailable { t.Fatal("overwrote") } if _, e = Read(p, 65535); e != ErrUnavailable { t.Fatal("accepted oversize") } if CreateExclusiveDurable(p+"big", append(b, 0), 65536) != ErrUnavailable { t.Fatal("accepted oversize create") } } func TestUnsafeFiles(t *testing.T) { for _, kind := range []string{"mode", "symlink", "ancestor", "hardlink", "fifo", "directory", "relative", "missing", "parent"} { t.Run(kind, func(t *testing.T) { d := privateDir(t) p := filepath.Join(d, "input") if e := os.WriteFile(p, []byte("private"), 0600); e != nil { t.Fatal(e) } switch kind { case "mode": os.Chmod(p, 0640) case "symlink": q := p + "link" os.Symlink(p, q) p = q case "ancestor": q := d + "link" os.Symlink(d, q) t.Cleanup(func() { os.Remove(q) }) p = filepath.Join(q, "input") case "hardlink": os.Link(p, p+"link") case "fifo": os.Remove(p) if e := unix.Mkfifo(p, 0600); e != nil { t.Fatal(e) } case "directory": p = d case "relative": p = "relative" case "missing": p += "missing" case "parent": os.Chmod(d, 0755) if CreateExclusiveDurable(p+"new", nil, 10) != ErrUnavailable { t.Fatal("public parent") } return } if _, e := Read(p, 100); e != ErrUnavailable { t.Fatal("unsafe read accepted", e) } if kind == "missing" { return } if e := CreateExclusiveDurable(p, nil, 100); e != ErrUnavailable { t.Fatal("unsafe create accepted", e) } }) } } func TestConcurrentCreate(t *testing.T) { p := filepath.Join(privateDir(t), "race") var wg sync.WaitGroup var successes atomic.Int32 for i := 0; i < 32; i++ { wg.Add(1) go func() { defer wg.Done() if CreateExclusiveDurable(p, []byte("exact"), 100) == nil { successes.Add(1) } }() } wg.Wait() if successes.Load() != 1 { t.Fatal(successes.Load()) } } type failingFile struct { *os.File stage string } func (f failingFile) Write(b []byte) (int, error) { if f.stage == "write" { n, _ := f.File.Write(b[:1]) return n, errors.New("private diagnostic") } return f.File.Write(b) } func (f failingFile) Sync() error { if f.stage == "sync" { return errors.New("private diagnostic") } return f.File.Sync() } func (f failingFile) Close() error { e := f.File.Close() if f.stage == "close" { return errors.New("private diagnostic") } return e } type failingParent struct{} func (failingParent) Sync() error { return errors.New("private diagnostic") } func TestFailurePreservesFileWithoutCompletionClaim(t *testing.T) { for _, stage := range []string{"write", "sync", "close", "parent"} { t.Run(stage, func(t *testing.T) { d := privateDir(t) p := filepath.Join(d, "interrupted") f, e := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) if e != nil { t.Fatal(e) } parent, e := os.Open(d) if e != nil { t.Fatal(e) } defer parent.Close() var barrier interface{ Sync() error } = parent if stage == "parent" { barrier = failingParent{} } if e = finish(failingFile{f, stage}, barrier, []byte("private")); e != ErrUnavailable { t.Fatal(e) } if _, e = os.Stat(p); e != nil { t.Fatal("failed file removed") } if CreateExclusiveDurable(p, []byte("replacement"), 100) != ErrUnavailable { t.Fatal("failed file overwritten") } // A read can succeed even after failed sync; it never proves durability. if _, e = Read(p, 100); e != nil { t.Fatal(e) } }) } } func TestCreateThroughSymlinkAndBounds(t *testing.T) { d := privateDir(t) alias := d + "-alias" if e := os.Symlink(d, alias); e != nil { t.Fatal(e) } t.Cleanup(func() { os.Remove(alias) }) if CreateExclusiveDurable(filepath.Join(alias, "new"), []byte("secret"), 10) != ErrUnavailable { t.Fatal("ancestor symlink followed") } if _, e := os.Stat(filepath.Join(d, "new")); !os.IsNotExist(e) { t.Fatal("created through alias") } for _, max := range []int{0, -1} { if CreateExclusiveDurable(filepath.Join(d, "new"), nil, max) != ErrUnavailable { t.Fatal("invalid bound") } if _, e := Read(filepath.Join(d, "new"), max); e != ErrUnavailable { t.Fatal("invalid read bound") } } }