//go:build linux && rootrunner package host import ( "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) func TestPlan14GenerationPrivateJournalRootRunner(t *testing.T) { dir := generationRootRunnerDir(t) private := filepath.Join(dir, "private") if err := os.Mkdir(private, 0700); err != nil { t.Fatal(err) } path := filepath.Join(private, "journals.db") journal, err := hostbox.OpenLaunchJournal(path) if err != nil { t.Fatal(err) } t.Cleanup(func() { journal.Close() }) if _, err := hostbox.NewJournalService(journal); err != nil { t.Fatal(err) } check := func(t *testing.T, refuse bool) { t.Helper() reader, err := openGenerationLaunchInventory(path) if reader != nil { defer reader.Close() } if (err != nil) != refuse { t.Fatalf("private journal reader: refuse=%v error=%v", refuse, err) } if err == nil { if _, err := reader.GenerationPayloads(t.Context()); err != nil { t.Fatal(err) } if _, err := reader.GenerationLaunches(t.Context()); err != nil { t.Fatal(err) } } } if f, err := rootstate.OpenTrustedFile(path); err == nil { f.Close() t.Fatal("public-file opener unexpectedly accepted private ancestry") } check(t, false) for _, suffix := range []string{"", "-wal", "-shm"} { for _, attack := range []string{"writable", "service-owned", "symlink", "hardlink", "fifo", "directory"} { t.Run("journal"+suffix+"/"+attack, func(t *testing.T) { leaf := path + suffix info, err := os.Stat(leaf) if err != nil { t.Fatal(err) } must := func(err error) { t.Helper() if err != nil { t.Fatal(err) } } switch attack { case "writable": must(os.Chmod(leaf, 0666)) defer func() { must(os.Chmod(leaf, info.Mode().Perm())) }() case "service-owned": must(os.Chown(leaf, 65534, 65534)) defer func() { must(os.Chown(leaf, 0, 0)) }() case "hardlink": must(os.Link(leaf, leaf+".alias")) defer func() { must(os.Remove(leaf + ".alias")) }() default: must(os.Rename(leaf, leaf+".saved")) defer func() { must(os.Remove(leaf)) must(os.Rename(leaf+".saved", leaf)) }() switch attack { case "symlink": must(os.Symlink(filepath.Base(leaf)+".saved", leaf)) case "fifo": must(unix.Mkfifo(leaf, 0600)) case "directory": must(os.Mkdir(leaf, 0700)) } } check(t, true) }) check(t, false) } } for _, attack := range []string{"writable", "service-owned", "symlink"} { t.Run("ancestor/"+attack, func(t *testing.T) { must := func(err error) { t.Helper() if err != nil { t.Fatal(err) } } switch attack { case "writable": must(os.Chmod(private, 0777)) defer func() { must(os.Chmod(private, 0700)) }() case "service-owned": must(os.Chown(private, 65534, 65534)) defer func() { must(os.Chown(private, 0, 0)) }() case "symlink": must(os.Rename(private, private+".saved")) must(os.Symlink(private+".saved", private)) defer func() { must(os.Remove(private)) must(os.Rename(private+".saved", private)) }() } check(t, true) }) check(t, false) } if err := journal.Close(); err != nil { t.Fatal(err) } check(t, false) }