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 unix
2
3package archive
4
5import (
6 "errors"
7 "io"
8 "os"
9 "path/filepath"
10 "strings"
11 "syscall"
12 "testing"
13 "time"
14)
15
16// Between the walk and the open, a tenant can swap a listed file for a
17// FIFO (an open would block forever), another file, or a symlink out of
18// the files directory (the open would read a host file into the archive).
19// Each is refused.
20func TestOpenRefusesASwappedEntry(t *testing.T) {
21 dir := t.TempDir()
22 mustWrite(t, filepath.Join(dir, "a"), []byte("a"), 0o644)
23 mustWrite(t, filepath.Join(dir, "b"), []byte("b"), 0o644)
24 outside := filepath.Join(t.TempDir(), "host-secret")
25 mustWrite(t, outside, []byte("secret"), 0o600)
26 if err := os.Symlink(outside, filepath.Join(dir, "esc")); err != nil {
27 t.Fatal(err)
28 }
29 if err := syscall.Mkfifo(filepath.Join(dir, "fifo"), 0o644); err != nil {
30 t.Fatal(err)
31 }
32 root, err := os.OpenRoot(dir)
33 if err != nil {
34 t.Fatal(err)
35 }
36 defer root.Close()
37 listed, err := root.Lstat("a")
38 if err != nil {
39 t.Fatal(err)
40 }
41 for _, swapped := range []string{"fifo", "b", "esc"} {
42 done := make(chan error, 1)
43 go func() {
44 rc, err := openItemChecked(item{name: "files/" + swapped, rel: swapped, root: root, info: listed})
45 if rc != nil {
46 rc.Close()
47 }
48 done <- err
49 }()
50 select {
51 case err := <-done:
52 if err == nil {
53 t.Fatalf("%s opened as the listed file", swapped)
54 }
55 if swapped != "esc" && !errors.Is(err, ErrChanged) {
56 t.Fatalf("%s: %v", swapped, err)
57 }
58 case <-time.After(5 * time.Second):
59 t.Fatalf("%s: open blocked", swapped)
60 }
61 }
62 // The listed file itself opens.
63 rc, err := openItemChecked(item{name: "files/a", rel: "a", root: root, info: listed})
64 if err != nil {
65 t.Fatal(err)
66 }
67 rc.Close()
68}
69
70// Fix round 1, ruling 1: Phase 1 does not carry hard links. A hard link to
71// a file outside the instance directory is a regular file with the listed
72// identity, and os.Root constrains names, not inodes: without a link-count
73// check Build would read the outside inode. Two links inside the directory
74// are refused too.
75func TestBuildRefusesHardLinks(t *testing.T) {
76 dir, env := filesFixture(t)
77 outside := filepath.Join(t.TempDir(), "host-secret")
78 mustWrite(t, outside, []byte("secret"), 0o600)
79 if err := os.Link(outside, filepath.Join(dir, "hl")); err != nil {
80 t.Skipf("no hard links here: %v", err)
81 }
82 if _, _, _, err := Build(dir, env, io.Discard); !errors.Is(err, ErrSpecialFile) || !strings.Contains(err.Error(), "hl") || !strings.Contains(err.Error(), "hard link") {
83 t.Fatalf("outside hard link: %v", err)
84 }
85 if _, _, _, err := PlainDigest(dir, env); !errors.Is(err, ErrSpecialFile) {
86 t.Fatalf("PlainDigest of an outside hard link: %v", err)
87 }
88
89 dir2, env2 := filesFixture(t)
90 if err := os.Link(filepath.Join(dir2, "a.txt"), filepath.Join(dir2, "sub", "a-again")); err != nil {
91 t.Fatal(err)
92 }
93 if _, _, _, err := Build(dir2, env2, io.Discard); !errors.Is(err, ErrSpecialFile) || !strings.Contains(err.Error(), "hard link") {
94 t.Fatalf("two links inside: %v", err)
95 }
96}
97
98// A Build refusal that names a file carries the path as data
99// (*archive.PathError), so a caller can say which file without reading the
100// error's text; errors.Is and the text are unchanged.
101func TestBuildRefusalsCarryTheirPath(t *testing.T) {
102 for name, tc := range map[string]struct {
103 setup func(t *testing.T, files string)
104 sentinel error
105 path string
106 }{
107 "symlink": {func(t *testing.T, files string) {
108 if err := os.Symlink("/etc/passwd", filepath.Join(files, "link")); err != nil {
109 t.Fatal(err)
110 }
111 }, ErrSpecialFile, "link"},
112 "hard link": {func(t *testing.T, files string) {
113 if err := os.WriteFile(filepath.Join(files, "a"), []byte("x"), 0o644); err != nil {
114 t.Fatal(err)
115 }
116 if err := os.Link(filepath.Join(files, "a"), filepath.Join(files, "b")); err != nil {
117 t.Fatal(err)
118 }
119 }, ErrSpecialFile, "a"},
120 } {
121 t.Run(name, func(t *testing.T) {
122 files := filepath.Join(t.TempDir(), "files")
123 if err := os.MkdirAll(files, 0o755); err != nil {
124 t.Fatal(err)
125 }
126 tc.setup(t, files)
127 _, _, _, err := PlainDigest(files, "")
128 var pe *PathError
129 if !errors.Is(err, tc.sentinel) || !errors.As(err, &pe) || pe.Path != tc.path {
130 t.Fatalf("err %v, path error %+v", err, pe)
131 }
132 if !strings.Contains(err.Error(), tc.sentinel.Error()+": ") {
133 t.Fatalf("the text changed: %q", err)
134 }
135 })
136 }
137}
138
139// A FIFO (a platform socket or pipe in a nested instance directory) that
140// skip accepts is never opened or typed: BuildFiltered archives around it.
141// The same FIFO unskipped is refused, naming it.
142func TestBuildFilteredNeverTypesASkippedFIFO(t *testing.T) {
143 files := t.TempDir()
144 if err := os.WriteFile(filepath.Join(files, "a"), []byte("a"), 0o644); err != nil {
145 t.Fatal(err)
146 }
147 if err := syscall.Mkfifo(filepath.Join(files, "app.fifo"), 0o644); err != nil {
148 t.Fatal(err)
149 }
150 var pe *PathError
151 if _, _, _, err := BuildFiltered(files, "", nil, io.Discard); !errors.Is(err, ErrSpecialFile) || !errors.As(err, &pe) || pe.Path != "app.fifo" {
152 t.Fatalf("unskipped FIFO: %v", err)
153 }
154 _, entries, _, err := BuildFiltered(files, "", func(rel string) bool { return rel == "app.fifo" }, io.Discard)
155 if err != nil || len(entries) != 1 || entries[0].Path != "files/a" {
156 t.Fatalf("skipped FIFO: %v %v", entries, err)
157 }
158}
159