| 1 | //go:build linux |
| 2 | |
| 3 | package privatefile |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "errors" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "sync" |
| 11 | "sync/atomic" |
| 12 | "testing" |
| 13 | |
| 14 | "golang.org/x/sys/unix" |
| 15 | ) |
| 16 | |
| 17 | func privateDir(t *testing.T) string { |
| 18 | t.Helper() |
| 19 | d := t.TempDir() |
| 20 | // The test runner may put TMPDIR behind a symlink. Positive fixtures |
| 21 | // use its real directory; symlink-refusal cases add their own links. |
| 22 | d, err := filepath.EvalSymlinks(d) |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | if e := os.Chmod(d, 0700); e != nil { |
| 27 | t.Fatal(e) |
| 28 | } |
| 29 | return d |
| 30 | } |
| 31 | func TestExactAndExclusive(t *testing.T) { |
| 32 | p := filepath.Join(privateDir(t), "result") |
| 33 | b := bytes.Repeat([]byte{0, 255, 10, 32}, 16384) |
| 34 | if e := CreateExclusiveDurable(p, b, 65536); e != nil { |
| 35 | t.Fatal(e) |
| 36 | } |
| 37 | got, e := Read(p, 65536) |
| 38 | if e != nil || !bytes.Equal(got, b) { |
| 39 | t.Fatal("exact read failed", e) |
| 40 | } |
| 41 | if CreateExclusiveDurable(p, []byte("replacement"), 65536) != ErrUnavailable { |
| 42 | t.Fatal("overwrote") |
| 43 | } |
| 44 | if _, e = Read(p, 65535); e != ErrUnavailable { |
| 45 | t.Fatal("accepted oversize") |
| 46 | } |
| 47 | if CreateExclusiveDurable(p+"big", append(b, 0), 65536) != ErrUnavailable { |
| 48 | t.Fatal("accepted oversize create") |
| 49 | } |
| 50 | } |
| 51 | func TestUnsafeFiles(t *testing.T) { |
| 52 | for _, kind := range []string{"mode", "symlink", "ancestor", "hardlink", "fifo", "directory", "relative", "missing", "parent"} { |
| 53 | t.Run(kind, func(t *testing.T) { |
| 54 | d := privateDir(t) |
| 55 | p := filepath.Join(d, "input") |
| 56 | if e := os.WriteFile(p, []byte("private"), 0600); e != nil { |
| 57 | t.Fatal(e) |
| 58 | } |
| 59 | switch kind { |
| 60 | case "mode": |
| 61 | os.Chmod(p, 0640) |
| 62 | case "symlink": |
| 63 | q := p + "link" |
| 64 | os.Symlink(p, q) |
| 65 | p = q |
| 66 | case "ancestor": |
| 67 | q := d + "link" |
| 68 | os.Symlink(d, q) |
| 69 | t.Cleanup(func() { os.Remove(q) }) |
| 70 | p = filepath.Join(q, "input") |
| 71 | case "hardlink": |
| 72 | os.Link(p, p+"link") |
| 73 | case "fifo": |
| 74 | os.Remove(p) |
| 75 | if e := unix.Mkfifo(p, 0600); e != nil { |
| 76 | t.Fatal(e) |
| 77 | } |
| 78 | case "directory": |
| 79 | p = d |
| 80 | case "relative": |
| 81 | p = "relative" |
| 82 | case "missing": |
| 83 | p += "missing" |
| 84 | case "parent": |
| 85 | os.Chmod(d, 0755) |
| 86 | if CreateExclusiveDurable(p+"new", nil, 10) != ErrUnavailable { |
| 87 | t.Fatal("public parent") |
| 88 | } |
| 89 | return |
| 90 | } |
| 91 | if _, e := Read(p, 100); e != ErrUnavailable { |
| 92 | t.Fatal("unsafe read accepted", e) |
| 93 | } |
| 94 | if kind == "missing" { |
| 95 | return |
| 96 | } |
| 97 | if e := CreateExclusiveDurable(p, nil, 100); e != ErrUnavailable { |
| 98 | t.Fatal("unsafe create accepted", e) |
| 99 | } |
| 100 | }) |
| 101 | } |
| 102 | } |
| 103 | func TestConcurrentCreate(t *testing.T) { |
| 104 | p := filepath.Join(privateDir(t), "race") |
| 105 | var wg sync.WaitGroup |
| 106 | var successes atomic.Int32 |
| 107 | for i := 0; i < 32; i++ { |
| 108 | wg.Add(1) |
| 109 | go func() { |
| 110 | defer wg.Done() |
| 111 | if CreateExclusiveDurable(p, []byte("exact"), 100) == nil { |
| 112 | successes.Add(1) |
| 113 | } |
| 114 | }() |
| 115 | } |
| 116 | wg.Wait() |
| 117 | if successes.Load() != 1 { |
| 118 | t.Fatal(successes.Load()) |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | type failingFile struct { |
| 123 | *os.File |
| 124 | stage string |
| 125 | } |
| 126 | |
| 127 | func (f failingFile) Write(b []byte) (int, error) { |
| 128 | if f.stage == "write" { |
| 129 | n, _ := f.File.Write(b[:1]) |
| 130 | return n, errors.New("private diagnostic") |
| 131 | } |
| 132 | return f.File.Write(b) |
| 133 | } |
| 134 | func (f failingFile) Sync() error { |
| 135 | if f.stage == "sync" { |
| 136 | return errors.New("private diagnostic") |
| 137 | } |
| 138 | return f.File.Sync() |
| 139 | } |
| 140 | func (f failingFile) Close() error { |
| 141 | e := f.File.Close() |
| 142 | if f.stage == "close" { |
| 143 | return errors.New("private diagnostic") |
| 144 | } |
| 145 | return e |
| 146 | } |
| 147 | |
| 148 | type failingParent struct{} |
| 149 | |
| 150 | func (failingParent) Sync() error { return errors.New("private diagnostic") } |
| 151 | func TestFailurePreservesFileWithoutCompletionClaim(t *testing.T) { |
| 152 | for _, stage := range []string{"write", "sync", "close", "parent"} { |
| 153 | t.Run(stage, func(t *testing.T) { |
| 154 | d := privateDir(t) |
| 155 | p := filepath.Join(d, "interrupted") |
| 156 | f, e := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) |
| 157 | if e != nil { |
| 158 | t.Fatal(e) |
| 159 | } |
| 160 | parent, e := os.Open(d) |
| 161 | if e != nil { |
| 162 | t.Fatal(e) |
| 163 | } |
| 164 | defer parent.Close() |
| 165 | var barrier interface{ Sync() error } = parent |
| 166 | if stage == "parent" { |
| 167 | barrier = failingParent{} |
| 168 | } |
| 169 | if e = finish(failingFile{f, stage}, barrier, []byte("private")); e != ErrUnavailable { |
| 170 | t.Fatal(e) |
| 171 | } |
| 172 | if _, e = os.Stat(p); e != nil { |
| 173 | t.Fatal("failed file removed") |
| 174 | } |
| 175 | if CreateExclusiveDurable(p, []byte("replacement"), 100) != ErrUnavailable { |
| 176 | t.Fatal("failed file overwritten") |
| 177 | } |
| 178 | // A read can succeed even after failed sync; it never proves durability. |
| 179 | if _, e = Read(p, 100); e != nil { |
| 180 | t.Fatal(e) |
| 181 | } |
| 182 | }) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | func TestCreateThroughSymlinkAndBounds(t *testing.T) { |
| 187 | d := privateDir(t) |
| 188 | alias := d + "-alias" |
| 189 | if e := os.Symlink(d, alias); e != nil { |
| 190 | t.Fatal(e) |
| 191 | } |
| 192 | t.Cleanup(func() { os.Remove(alias) }) |
| 193 | if CreateExclusiveDurable(filepath.Join(alias, "new"), []byte("secret"), 10) != ErrUnavailable { |
| 194 | t.Fatal("ancestor symlink followed") |
| 195 | } |
| 196 | if _, e := os.Stat(filepath.Join(d, "new")); !os.IsNotExist(e) { |
| 197 | t.Fatal("created through alias") |
| 198 | } |
| 199 | for _, max := range []int{0, -1} { |
| 200 | if CreateExclusiveDurable(filepath.Join(d, "new"), nil, max) != ErrUnavailable { |
| 201 | t.Fatal("invalid bound") |
| 202 | } |
| 203 | if _, e := Read(filepath.Join(d, "new"), max); e != ErrUnavailable { |
| 204 | t.Fatal("invalid read bound") |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | |