Skip to content

Commit efb43de

Browse files
committed
[artifact] only inspect artifact contents
Artifact inspection would inspect the alloc directory for symlinks pointing outside of the alloc directory. This can be problematic for drivers like the raw exec driver which provide the root system and will likely include symlinks. Inspecting only the artifact after being fetched can be difficult if the artifact is fetched to a pre-existing destination as it would require discerning between new and old paths. Instead, if an artifact is to be inspected, it is fetched to a temporary location. The isolated artifact is then inspected before being moved to its final destination.
1 parent 3a20db3 commit efb43de

File tree

2 files changed

+300
-23
lines changed

2 files changed

+300
-23
lines changed

client/allocrunner/taskrunner/getter/sandbox_test.go

Lines changed: 206 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package getter
55

66
import (
7+
"archive/tar"
78
"fmt"
89
"net/http"
910
"net/http/cgi"
@@ -16,12 +17,15 @@ import (
1617
"time"
1718

1819
"github.com/hashicorp/nomad/client/config"
20+
"github.com/hashicorp/nomad/client/interfaces"
1921
"github.com/hashicorp/nomad/client/testutil"
2022
"github.com/hashicorp/nomad/helper/testlog"
2123
"github.com/hashicorp/nomad/nomad/structs"
2224
"github.com/shoenig/test/must"
2325
)
2426

27+
const testFileContent = "test-file-content"
28+
2529
func artifactConfig(timeout time.Duration) *config.ArtifactConfig {
2630
return &config.ArtifactConfig{
2731
HTTPReadTimeout: timeout,
@@ -119,13 +123,20 @@ func TestSandbox_Get_inspection(t *testing.T) {
119123
testutil.RequireRoot(t)
120124
logger := testlog.HCLogger(t)
121125

122-
// Create a temporary directory directly so the repos
123-
// don't end up being found improperly
124-
tdir, err := os.MkdirTemp("", "nomad-test")
125-
must.NoError(t, err, must.Sprint("failed to create top level local repo directory"))
126+
sandboxSetup := func() (string, *Sandbox, interfaces.EnvReplacer) {
127+
testutil.RequireRoot(t)
128+
logger := testlog.HCLogger(t)
129+
ac := artifactConfig(10 * time.Second)
130+
sbox := New(ac, logger)
131+
_, taskDir := SetupDir(t)
132+
env := noopTaskEnv(taskDir)
133+
sbox.ac.DisableFilesystemIsolation = true
134+
135+
return taskDir, sbox, env
136+
}
126137

127138
t.Run("symlink escaped sandbox", func(t *testing.T) {
128-
dir, err := os.MkdirTemp(tdir, "fake-repo")
139+
dir, err := os.MkdirTemp(t.TempDir(), "fake-repo")
129140
must.NoError(t, err, must.Sprint("failed to create local repo directory"))
130141
must.NoError(t, os.Symlink("/", filepath.Join(dir, "bad-file")), must.Sprint("could not create symlink in local repo"))
131142
srv := makeAndServeGitRepo(t, dir)
@@ -162,7 +173,7 @@ func TestSandbox_Get_inspection(t *testing.T) {
162173
})
163174

164175
t.Run("symlink within sandbox", func(t *testing.T) {
165-
dir, err := os.MkdirTemp(tdir, "fake-repo")
176+
dir, err := os.MkdirTemp(t.TempDir(), "fake-repo")
166177
must.NoError(t, err, must.Sprint("failed to create local repo"))
167178
// create a file to link to
168179
f, err := os.Create(filepath.Join(dir, "test-file"))
@@ -193,6 +204,195 @@ func TestSandbox_Get_inspection(t *testing.T) {
193204
err = sbox.Get(env, artifact, "nobody")
194205
must.NoError(t, err)
195206
})
207+
208+
t.Run("ignores existing symlinks", func(t *testing.T) {
209+
taskDir, sbox, env := sandboxSetup()
210+
src, _ := servTestFile(t, "test-file")
211+
must.NoError(t, os.Symlink("/", filepath.Join(taskDir, "bad-file")))
212+
213+
artifact := &structs.TaskArtifact{
214+
GetterSource: src,
215+
RelativeDest: "local/downloads",
216+
}
217+
218+
err := sbox.Get(env, artifact, "nobody")
219+
must.NoError(t, err)
220+
221+
_, err = os.Stat(filepath.Join(taskDir, "local", "downloads", "test-file"))
222+
must.NoError(t, err)
223+
})
224+
225+
t.Run("properly chowns destination", func(t *testing.T) {
226+
taskDir, sbox, env := sandboxSetup()
227+
src, _ := servTestFile(t, "test-file")
228+
229+
artifact := &structs.TaskArtifact{
230+
GetterSource: src,
231+
RelativeDest: "local/downloads",
232+
Chown: true,
233+
}
234+
235+
err := sbox.Get(env, artifact, "nobody")
236+
must.NoError(t, err)
237+
238+
info, err := os.Stat(filepath.Join(taskDir, "local", "downloads"))
239+
must.NoError(t, err)
240+
241+
uid := info.Sys().(*syscall.Stat_t).Uid
242+
must.Eq(t, 65534, uid) // nobody's conventional uid
243+
244+
info, err = os.Stat(filepath.Join(taskDir, "local", "downloads", "test-file"))
245+
must.NoError(t, err)
246+
247+
uid = info.Sys().(*syscall.Stat_t).Uid
248+
must.Eq(t, 65534, uid) // nobody's conventional uid
249+
})
250+
251+
t.Run("when destination file exists", func(t *testing.T) {
252+
taskDir, sbox, env := sandboxSetup()
253+
src, _ := servTestFile(t, "test-file")
254+
255+
testFile := filepath.Join(taskDir, "local", "downloads", "test-file")
256+
must.NoError(t, os.MkdirAll(filepath.Dir(testFile), 0755))
257+
f, err := os.OpenFile(testFile, os.O_CREATE, 0644)
258+
must.NoError(t, err)
259+
f.Write([]byte("testing"))
260+
f.Close()
261+
originalInfo, err := os.Stat(testFile)
262+
must.NoError(t, err)
263+
264+
artifact := &structs.TaskArtifact{
265+
GetterSource: src,
266+
RelativeDest: "local/downloads",
267+
Chown: true,
268+
}
269+
270+
err = sbox.Get(env, artifact, "nobody")
271+
must.NoError(t, err)
272+
273+
newInfo, err := os.Stat(testFile)
274+
must.NoError(t, err)
275+
276+
must.False(t, os.SameFile(originalInfo, newInfo))
277+
})
278+
279+
t.Run("when destination directory exists", func(t *testing.T) {
280+
taskDir, sbox, env := sandboxSetup()
281+
src, _ := servTestFile(t, "test-file")
282+
283+
testFile := filepath.Join(taskDir, "local", "downloads", "testfile.txt")
284+
must.NoError(t, os.MkdirAll(filepath.Dir(testFile), 0755))
285+
f, err := os.OpenFile(testFile, os.O_CREATE, 0644)
286+
must.NoError(t, err)
287+
f.Write([]byte("testing"))
288+
f.Close()
289+
290+
artifact := &structs.TaskArtifact{
291+
GetterSource: src,
292+
RelativeDest: "local/downloads",
293+
Chown: true,
294+
}
295+
296+
err = sbox.Get(env, artifact, "nobody")
297+
must.NoError(t, err)
298+
299+
// check that new file exists
300+
_, err = os.Stat(filepath.Join(taskDir, "local", "downloads", "test-file"))
301+
must.NoError(t, err)
302+
303+
// check that existing file still exists
304+
_, err = os.Stat(testFile)
305+
must.NoError(t, err)
306+
})
307+
308+
t.Run("when unpacking file to an existing directory", func(t *testing.T) {
309+
taskDir, sbox, env := sandboxSetup()
310+
311+
tarFiles := []string{
312+
"test.file",
313+
"nested/test.file",
314+
"other/test.file",
315+
}
316+
src, _ := servTarFile(t, tarFiles...)
317+
318+
testFile := filepath.Join(taskDir, "local", "downloads", "other", "testfile.txt")
319+
must.NoError(t, os.MkdirAll(filepath.Dir(testFile), 0755))
320+
f, err := os.Create(testFile)
321+
must.NoError(t, err)
322+
f.Write([]byte("testing"))
323+
f.Close()
324+
325+
artifact := &structs.TaskArtifact{
326+
GetterSource: src,
327+
RelativeDest: "local/downloads",
328+
Chown: true,
329+
}
330+
331+
err = sbox.Get(env, artifact, "nobody")
332+
must.NoError(t, err)
333+
334+
// check that all unpacked files exist
335+
for _, tarFile := range tarFiles {
336+
_, err := os.Stat(filepath.Join(taskDir, "local", "downloads", tarFile))
337+
must.NoError(t, err)
338+
}
339+
340+
// check existing file remains
341+
_, err = os.Stat(testFile)
342+
must.NoError(t, err)
343+
})
344+
}
345+
346+
func servTestFile(t *testing.T, filename string) (string, *httptest.Server) {
347+
t.Helper()
348+
349+
dir, err := os.MkdirTemp(t.TempDir(), "file")
350+
must.NoError(t, err)
351+
f, err := os.Create(filepath.Join(dir, filename))
352+
must.NoError(t, err)
353+
defer f.Close()
354+
f.Write([]byte(testFileContent))
355+
356+
s := servDir(t, dir)
357+
return fmt.Sprintf("%s/%s", s.URL, filename), s
358+
}
359+
360+
func servTarFile(t *testing.T, paths ...string) (string, *httptest.Server) {
361+
t.Helper()
362+
363+
dir, err := os.MkdirTemp(t.TempDir(), "tar")
364+
f, err := os.Create(filepath.Join(dir, "test-compressed.tar"))
365+
must.NoError(t, err)
366+
defer f.Close()
367+
368+
w := tar.NewWriter(f)
369+
defer w.Close()
370+
for _, path := range paths {
371+
err := w.WriteHeader(&tar.Header{
372+
Name: path,
373+
Mode: 0644,
374+
Size: int64(len(testFileContent)),
375+
})
376+
must.NoError(t, err)
377+
bytes, err := w.Write([]byte(testFileContent))
378+
must.NoError(t, err)
379+
must.Eq(t, len(testFileContent), bytes)
380+
}
381+
382+
s := servDir(t, dir)
383+
return fmt.Sprintf("%s/test-compressed.tar", s.URL), s
384+
}
385+
386+
func servDir(t *testing.T, dir string) *httptest.Server {
387+
t.Helper()
388+
389+
fs := os.DirFS(dir)
390+
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
391+
http.ServeFileFS(w, r, fs, r.URL.Path)
392+
}))
393+
t.Cleanup(s.Close)
394+
395+
return s
196396
}
197397

198398
func makeAndServeGitRepo(t *testing.T, repoPath string) *httptest.Server {

0 commit comments

Comments
 (0)