//go:build linux package host import ( "context" "os" "path/filepath" "strings" "syscall" "testing" "time" ) func TestApplyInputRefusesHardLinksAndSymlinkedRoot(t *testing.T) { for _, kind := range []string{"request-hardlink", "artifact-hardlink", "root-symlink"} { t.Run(kind, func(t *testing.T) { f := newApplyFixture(t, "stable", "v1") f.promote("v2", []byte("#!/bin/sh\necho carlos v2\n"), false) p, err := f.a.verifyPointer(context.Background()) if err != nil { t.Fatal(err) } req, err := f.a.readRequest() if err != nil { t.Fatal(err) } switch kind { case "request-hardlink": err = os.Link(systemRequestPath(f.a.dataDir), filepath.Join(f.a.dataDir, "request-link")) case "artifact-hardlink": err = os.Link(systemCachePath(f.a.dataDir, "v2"), filepath.Join(f.a.dataDir, "artifact-link")) case "root-symlink": alias := filepath.Join(t.TempDir(), "data") err = os.Symlink(f.a.dataDir, alias) f.a.dataDir = alias } if err != nil { t.Fatal(err) } if kind != "artifact-hardlink" { if _, err := f.a.readRequest(); err == nil { t.Fatal("unsafe request reached the parser") } } if kind != "request-hardlink" { if _, err := f.a.stageCandidate(context.Background(), p, req); err == nil { t.Fatal("unsafe artifact reached staging") } } }) } } // The sandboxed agent owns /data. A FIFO planted where root expects a // regular file would park root inside open(2) — while it holds the apply // lock, wedging every future apply on the box. Both reads under /data must // come back, refusing, in no time at all. func TestApplyNeverBlocksOnAPlantedFIFO(t *testing.T) { within := func(t *testing.T, what string, fn func() error) { t.Helper() done := make(chan error, 1) go func() { done <- fn() }() select { case err := <-done: if err == nil { t.Fatalf("%s: a FIFO was accepted", what) } if !strings.Contains(err.Error(), "regular file") { t.Fatalf("%s: err = %v, want the not-a-regular-file refusal", what, err) } case <-time.After(2 * time.Second): t.Fatalf("%s: parked on a FIFO — root would hold the apply lock forever", what) } } // The request. f := newApplyFixture(t, "stable", "v1") os.MkdirAll(systemDataDir(f.a.dataDir), 0o755) if err := syscall.Mkfifo(systemRequestPath(f.a.dataDir), 0o644); err != nil { t.Fatal(err) } within(t, "readRequest", func() error { _, err := f.a.readRequest(); return err }) // The artifact cache. f2 := newApplyFixture(t, "stable", "v1") f2.promote("v2", []byte("#!/bin/sh\necho carlos v2\n"), false) os.Remove(systemCachePath(f2.a.dataDir, "v2")) if err := syscall.Mkfifo(systemCachePath(f2.a.dataDir, "v2"), 0o644); err != nil { t.Fatal(err) } p, err := f2.a.verifyPointer(context.Background()) if err != nil { t.Fatal(err) } req, err := f2.a.readRequest() if err != nil { t.Fatal(err) } within(t, "stageCandidate", func() error { _, err := f2.a.stageCandidate(context.Background(), p, req) return err }) }