| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | "syscall" |
| 11 | "testing" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | func TestApplyInputRefusesHardLinksAndSymlinkedRoot(t *testing.T) { |
| 16 | for _, kind := range []string{"request-hardlink", "artifact-hardlink", "root-symlink"} { |
| 17 | t.Run(kind, func(t *testing.T) { |
| 18 | f := newApplyFixture(t, "stable", "v1") |
| 19 | f.promote("v2", []byte("#!/bin/sh\necho carlos v2\n"), false) |
| 20 | p, err := f.a.verifyPointer(context.Background()) |
| 21 | if err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | req, err := f.a.readRequest() |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | switch kind { |
| 29 | case "request-hardlink": |
| 30 | err = os.Link(systemRequestPath(f.a.dataDir), filepath.Join(f.a.dataDir, "request-link")) |
| 31 | case "artifact-hardlink": |
| 32 | err = os.Link(systemCachePath(f.a.dataDir, "v2"), filepath.Join(f.a.dataDir, "artifact-link")) |
| 33 | case "root-symlink": |
| 34 | alias := filepath.Join(t.TempDir(), "data") |
| 35 | err = os.Symlink(f.a.dataDir, alias) |
| 36 | f.a.dataDir = alias |
| 37 | } |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | if kind != "artifact-hardlink" { |
| 42 | if _, err := f.a.readRequest(); err == nil { |
| 43 | t.Fatal("unsafe request reached the parser") |
| 44 | } |
| 45 | } |
| 46 | if kind != "request-hardlink" { |
| 47 | if _, err := f.a.stageCandidate(context.Background(), p, req); err == nil { |
| 48 | t.Fatal("unsafe artifact reached staging") |
| 49 | } |
| 50 | } |
| 51 | }) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | // The sandboxed agent owns /data. A FIFO planted where root expects a |
| 56 | // regular file would park root inside open(2) — while it holds the apply |
| 57 | // lock, wedging every future apply on the box. Both reads under /data must |
| 58 | // come back, refusing, in no time at all. |
| 59 | func TestApplyNeverBlocksOnAPlantedFIFO(t *testing.T) { |
| 60 | within := func(t *testing.T, what string, fn func() error) { |
| 61 | t.Helper() |
| 62 | done := make(chan error, 1) |
| 63 | go func() { done <- fn() }() |
| 64 | select { |
| 65 | case err := <-done: |
| 66 | if err == nil { |
| 67 | t.Fatalf("%s: a FIFO was accepted", what) |
| 68 | } |
| 69 | if !strings.Contains(err.Error(), "regular file") { |
| 70 | t.Fatalf("%s: err = %v, want the not-a-regular-file refusal", what, err) |
| 71 | } |
| 72 | case <-time.After(2 * time.Second): |
| 73 | t.Fatalf("%s: parked on a FIFO — root would hold the apply lock forever", what) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // The request. |
| 78 | f := newApplyFixture(t, "stable", "v1") |
| 79 | os.MkdirAll(systemDataDir(f.a.dataDir), 0o755) |
| 80 | if err := syscall.Mkfifo(systemRequestPath(f.a.dataDir), 0o644); err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | within(t, "readRequest", func() error { _, err := f.a.readRequest(); return err }) |
| 84 | |
| 85 | // The artifact cache. |
| 86 | f2 := newApplyFixture(t, "stable", "v1") |
| 87 | f2.promote("v2", []byte("#!/bin/sh\necho carlos v2\n"), false) |
| 88 | os.Remove(systemCachePath(f2.a.dataDir, "v2")) |
| 89 | if err := syscall.Mkfifo(systemCachePath(f2.a.dataDir, "v2"), 0o644); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | p, err := f2.a.verifyPointer(context.Background()) |
| 93 | if err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | req, err := f2.a.readRequest() |
| 97 | if err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | within(t, "stageCandidate", func() error { |
| 101 | _, err := f2.a.stageCandidate(context.Background(), p, req) |
| 102 | return err |
| 103 | }) |
| 104 | } |
| 105 | |