carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package instancefs
4
5import (
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12
13 "golang.org/x/sys/unix"
14)
15
16func TestOpenBeneathReadsAPlainFile(t *testing.T) {
17 dir := t.TempDir()
18 if err := os.WriteFile(filepath.Join(dir, "h.env"), []byte("A=1\n"), 0o600); err != nil {
19 t.Fatal(err)
20 }
21 got, err := ReadBeneath(dir, "h.env")
22 if err != nil {
23 t.Fatalf("ReadBeneath: %v", err)
24 }
25 if string(got) != "A=1\n" {
26 t.Errorf("got %q", got)
27 }
28}
29
30func TestOpenBeneathRefusesASymlinkAtTheLeaf(t *testing.T) {
31 dir := t.TempDir()
32 secret := filepath.Join(t.TempDir(), "secret")
33 if err := os.WriteFile(secret, []byte("token"), 0o600); err != nil {
34 t.Fatal(err)
35 }
36 if err := os.Symlink(secret, filepath.Join(dir, "h.env")); err != nil {
37 t.Fatal(err)
38 }
39 _, err := ReadBeneath(dir, "h.env")
40 if !errors.Is(err, ErrSymlink) {
41 t.Fatalf("ReadBeneath through a symlink: err = %v, want ErrSymlink", err)
42 }
43}
44
45func TestOpenBeneathRefusesASymlinkedDirectory(t *testing.T) {
46 dir := t.TempDir()
47 other := t.TempDir()
48 if err := os.WriteFile(filepath.Join(other, "x"), []byte("x"), 0o600); err != nil {
49 t.Fatal(err)
50 }
51 if err := os.Symlink(other, filepath.Join(dir, "sub")); err != nil {
52 t.Fatal(err)
53 }
54 _, err := ReadBeneath(dir, "sub/x")
55 if !errors.Is(err, ErrSymlink) {
56 t.Fatalf("err = %v, want ErrSymlink", err)
57 }
58}
59
60// TestOpenBeneathRefusesASymlinkedDir pins the review fix: openDir must
61// refuse a symlink at dir itself, not only at an intermediate component
62// under it. A same-uid process that swaps the instance directory for a
63// symlink before the mount option lands must be caught the same way a
64// symlink deeper in the path already is.
65func TestOpenBeneathRefusesASymlinkedDir(t *testing.T) {
66 real := t.TempDir()
67 if err := os.WriteFile(filepath.Join(real, "x"), []byte("x"), 0o600); err != nil {
68 t.Fatal(err)
69 }
70 link := filepath.Join(t.TempDir(), "linked-dir")
71 if err := os.Symlink(real, link); err != nil {
72 t.Fatal(err)
73 }
74 _, err := ReadBeneath(link, "x")
75 if !errors.Is(err, ErrSymlink) {
76 t.Fatalf("ReadBeneath(%q, \"x\"): err = %v, want ErrSymlink", link, err)
77 }
78}
79
80func TestOpenBeneathRefusesDotDotAndAbsolute(t *testing.T) {
81 dir := t.TempDir()
82 for _, name := range []string{"../x", "a/../../x", "/etc/passwd"} {
83 _, err := ReadBeneath(dir, name)
84 if !errors.Is(err, ErrEscapes) {
85 t.Errorf("ReadBeneath(%q): err = %v, want ErrEscapes", name, err)
86 }
87 }
88}
89
90func TestWriteAtomicReplacesAndNeverFollows(t *testing.T) {
91 dir := t.TempDir()
92 if err := WriteAtomic(dir, "h.config.env", []byte("one\n"), 0o600); err != nil {
93 t.Fatalf("WriteAtomic: %v", err)
94 }
95 if err := WriteAtomic(dir, "h.config.env", []byte("two\n"), 0o600); err != nil {
96 t.Fatalf("WriteAtomic (replace): %v", err)
97 }
98 got, err := os.ReadFile(filepath.Join(dir, "h.config.env"))
99 if err != nil || string(got) != "two\n" {
100 t.Fatalf("got %q, %v", got, err)
101 }
102 fi, err := os.Stat(filepath.Join(dir, "h.config.env"))
103 if err != nil || fi.Mode().Perm() != 0o600 {
104 t.Errorf("mode = %v, %v", fi.Mode(), err)
105 }
106 // A symlink planted at the destination is REPLACED as a directory
107 // entry; its target is untouched.
108 victim := filepath.Join(t.TempDir(), "victim")
109 if err := os.WriteFile(victim, []byte("keep"), 0o600); err != nil {
110 t.Fatal(err)
111 }
112 if err := os.Remove(filepath.Join(dir, "h.config.env")); err != nil {
113 t.Fatal(err)
114 }
115 if err := os.Symlink(victim, filepath.Join(dir, "h.config.env")); err != nil {
116 t.Fatal(err)
117 }
118 if err := WriteAtomic(dir, "h.config.env", []byte("three\n"), 0o600); err != nil {
119 t.Fatalf("WriteAtomic over a symlink: %v", err)
120 }
121 if b, _ := os.ReadFile(victim); string(b) != "keep" {
122 t.Errorf("symlink target was written through: %q", b)
123 }
124 if fi, err := os.Lstat(filepath.Join(dir, "h.config.env")); err != nil || fi.Mode()&os.ModeSymlink != 0 {
125 t.Errorf("destination is still a symlink: %v %v", fi, err)
126 }
127 // No temp file is left behind.
128 entries, _ := os.ReadDir(dir)
129 for _, e := range entries {
130 if strings.Contains(e.Name(), ".tmp-") {
131 t.Errorf("temp file left behind: %s", e.Name())
132 }
133 }
134}
135
136func TestMountNoSymfollowReadsMountinfo(t *testing.T) {
137 // / is a mount and does not carry nosymfollow on any ordinary box; a
138 // path that is not a mountpoint is reported as not a nosymfollow mount
139 // with a nil error (the caller's message names the path).
140 got, err := MountNoSymfollow("/")
141 if err != nil {
142 t.Fatalf("MountNoSymfollow(/): %v", err)
143 }
144 if got {
145 t.Error("/ reported as nosymfollow")
146 }
147 got, err = MountNoSymfollow(t.TempDir())
148 if err != nil || got {
149 t.Errorf("temp dir: %v %v", got, err)
150 }
151 // The parser, against a fixture line, since a real nosymfollow mount
152 // needs root to create.
153 line := "36 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,nosymfollow,relatime - ext4 /dev/root rw"
154 if !mountinfoLineHas(line, "/data/instances", "nosymfollow") {
155 t.Error("parser missed nosymfollow on a fixture line")
156 }
157 if mountinfoLineHas(line, "/data", "nosymfollow") {
158 t.Error("parser matched the wrong mountpoint")
159 }
160
161 // Stacked mounts at the same mountpoint list oldest first; the LAST
162 // line is the one actually in effect, so the verdict must follow it
163 // even when an earlier line at the same mountpoint disagrees.
164 first := "36 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,relatime - ext4 /dev/root rw"
165 last := "37 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,nosymfollow,relatime - ext4 /dev/root rw"
166 stacked := strings.NewReader(first + "\n" + last + "\n")
167 got, err = mountOptionOf(stacked, "/data/instances", "nosymfollow")
168 if err != nil || !got {
169 t.Errorf("stacked mounts, last with nosymfollow: got %v, %v, want true, nil", got, err)
170 }
171
172 stackedReversed := strings.NewReader(last + "\n" + first + "\n")
173 got, err = mountOptionOf(stackedReversed, "/data/instances", "nosymfollow")
174 if err != nil || got {
175 t.Errorf("stacked mounts, last WITHOUT nosymfollow: got %v, %v, want false, nil (the earlier nosymfollow line must not outvote the effective one)", got, err)
176 }
177}
178
179// TestMountOptionOfIsTheGeneralForm pins that MountNoSymfollow is now a
180// thin wrapper over MountOptionOf, not a separate implementation: asking
181// for an option other than nosymfollow (here "ro", the option the
182// cgroup-ro-mount probe check asks about) reads the same field.
183func TestMountOptionOfIsTheGeneralForm(t *testing.T) {
184 line := "36 35 98:0 / /sys/fs/cgroup ro,nosuid,nodev,noexec - cgroup2 cgroup2 rw"
185 got, err := mountOptionOf(strings.NewReader(line+"\n"), "/sys/fs/cgroup", "ro")
186 if err != nil || !got {
187 t.Errorf("MountOptionOf(/sys/fs/cgroup, ro): got %v, %v, want true, nil", got, err)
188 }
189 got, err = mountOptionOf(strings.NewReader(line+"\n"), "/sys/fs/cgroup", "nosymfollow")
190 if err != nil || got {
191 t.Errorf("MountOptionOf(/sys/fs/cgroup, nosymfollow) on a line without it: got %v, %v, want false, nil", got, err)
192 }
193 got, err = MountOptionOf("/", "ro")
194 if err != nil {
195 t.Fatalf("MountOptionOf(/, ro) against the live mountinfo: %v", err)
196 }
197 _ = got // / is not reliably ro or rw across test hosts; only the absence of an error matters here.
198}
199
200// TestOpenBeneathRefusesAFIFOInsteadOfBlocking pins the review fix: a FIFO
201// with no writer blocks open(2) indefinitely, and a tenant can plant one at
202// any name the agent reads by contract. The read must fail fast with
203// ErrNotRegular rather than hang the caller, so this runs it in a goroutine
204// with its own timeout: a regression here must fail the test, not wedge it.
205func TestOpenBeneathRefusesAFIFOInsteadOfBlocking(t *testing.T) {
206 dir := t.TempDir()
207 if err := unix.Mkfifo(filepath.Join(dir, "h.env"), 0o600); err != nil {
208 t.Fatalf("Mkfifo: %v", err)
209 }
210 done := make(chan error, 1)
211 go func() {
212 _, err := ReadBeneath(dir, "h.env")
213 done <- err
214 }()
215 select {
216 case err := <-done:
217 if !errors.Is(err, ErrNotRegular) {
218 t.Fatalf("ReadBeneath a FIFO: err = %v, want ErrNotRegular", err)
219 }
220 case <-time.After(2 * time.Second):
221 t.Fatal("ReadBeneath blocked on a FIFO with no writer instead of refusing it")
222 }
223}
224
225// TestOpenBeneathRefusesAWriteOpenOfAFIFO pins the write-side half of the
226// same fix: a write-open of a FIFO with no reader on the other end blocks
227// open(2) indefinitely too, and the admin token file this backs an append
228// onto is on the same tenant-writable ground as anything OpenBeneath reads.
229// O_NONBLOCK turns that into an immediate ENXIO instead.
230func TestOpenBeneathRefusesAWriteOpenOfAFIFO(t *testing.T) {
231 dir := t.TempDir()
232 if err := unix.Mkfifo(filepath.Join(dir, "token"), 0o600); err != nil {
233 t.Fatalf("Mkfifo: %v", err)
234 }
235 done := make(chan error, 1)
236 go func() {
237 _, err := OpenBeneath(dir, "token", os.O_WRONLY|os.O_APPEND, 0)
238 done <- err
239 }()
240 select {
241 case err := <-done:
242 if err == nil {
243 t.Fatal("write-open of a FIFO with no reader succeeded, want an error")
244 }
245 case <-time.After(2 * time.Second):
246 t.Fatal("write-open blocked on a FIFO with no reader instead of refusing it")
247 }
248}
249
250func TestReadBeneathLimitRefusesALargeFile(t *testing.T) {
251 dir := t.TempDir()
252 if err := os.WriteFile(filepath.Join(dir, "big"), make([]byte, 4096), 0o600); err != nil {
253 t.Fatal(err)
254 }
255 if _, err := ReadBeneathLimit(dir, "big", 1024); !errors.Is(err, ErrTooLarge) {
256 t.Fatalf("err = %v, want ErrTooLarge", err)
257 }
258 got, err := ReadBeneathLimit(dir, "big", 4096)
259 if err != nil || len(got) != 4096 {
260 t.Fatalf("exact limit: %d bytes, %v", len(got), err)
261 }
262}
263
264// TestSelfCheckPassesWhereOpenat2Works pins the shape of the agent's own
265// openat2 check: a plain directory passes, a symlinked one is refused as a
266// symlink (the same rule as every other open here), and a missing one
267// reports its own error rather than ErrOpenat2Unavailable, which is kept
268// for the one answer that means the call itself was blocked.
269func TestSelfCheckPassesWhereOpenat2Works(t *testing.T) {
270 dir := t.TempDir()
271 if err := SelfCheck(dir); err != nil {
272 t.Fatalf("SelfCheck(%s) = %v, want nil", dir, err)
273 }
274 link := filepath.Join(dir, "link")
275 if err := os.Symlink(dir, link); err != nil {
276 t.Fatal(err)
277 }
278 if err := SelfCheck(link); !errors.Is(err, ErrSymlink) {
279 t.Errorf("SelfCheck(symlink) = %v, want ErrSymlink", err)
280 }
281 err := SelfCheck(filepath.Join(dir, "missing"))
282 if err == nil || errors.Is(err, ErrOpenat2Unavailable) {
283 t.Errorf("SelfCheck(missing) = %v, want ENOENT and not ErrOpenat2Unavailable", err)
284 }
285}
286