| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostbox" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 14 | "golang.org/x/sys/unix" |
| 15 | ) |
| 16 | |
| 17 | func TestPlan14GenerationPayloadPhysicalIdentity(t *testing.T) { |
| 18 | for _, attack := range []string{"none", "directory", "missing", "replaced", "symlink", "parent-symlink", "root-symlink", "wrong-device", "wrong-inode", "wrong-type", "intended", "intended-missing", "escape", "unknown-root", "changed-during-kernel"} { |
| 19 | t.Run(attack, func(t *testing.T) { |
| 20 | f, disk, _ := generationLaunchFixture(t) |
| 21 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | r := readGenerationLaunch(t, disk) |
| 25 | views, err := f.root.Guard.Root.GenerationInstallations(t.Context()) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | launch, err := f.journal.Read(t.Context(), f.target) |
| 30 | if err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | dir := t.TempDir() |
| 34 | t.Setenv("CARLOS_DATA_DIR", dir) |
| 35 | parent := filepath.Join(dir, "state") |
| 36 | if err := os.Mkdir(parent, 0700); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | path := filepath.Join(parent, "retained") |
| 40 | if attack == "directory" { |
| 41 | err = os.Mkdir(path, 0700) |
| 42 | } else { |
| 43 | err = os.WriteFile(path, []byte("retained payload"), 0600) |
| 44 | } |
| 45 | if err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | var st unix.Stat_t |
| 49 | if err := unix.Lstat(path, &st); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | entry := hostbox.StopInventoryEntry{Identity: inventoryIdentityFixture(t, dir, "state/retained"), Kind: "path", ArtifactID: "retained", ManagedRoot: "data", RelativePath: "state/retained", Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & unix.S_IFMT} |
| 53 | switch attack { |
| 54 | case "missing", "replaced", "symlink": |
| 55 | if err := os.Rename(path, path+".old"); err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | if attack == "replaced" { |
| 59 | err = os.WriteFile(path, []byte("replacement"), 0600) |
| 60 | } else if attack == "symlink" { |
| 61 | err = os.Symlink("retained.old", path) |
| 62 | } |
| 63 | case "parent-symlink": |
| 64 | if err := os.Rename(parent, parent+".old"); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | err = os.Symlink("state.old", parent) |
| 68 | case "root-symlink": |
| 69 | link := dir + ".link" |
| 70 | err = os.Symlink(dir, link) |
| 71 | t.Cleanup(func() { os.Remove(link) }) |
| 72 | t.Setenv("CARLOS_DATA_DIR", link) |
| 73 | case "wrong-device": |
| 74 | entry.Device++ |
| 75 | case "wrong-inode": |
| 76 | entry.Inode++ |
| 77 | case "wrong-type": |
| 78 | entry.Type = unix.S_IFDIR |
| 79 | case "intended", "intended-missing": |
| 80 | entry.Materialization = "intended" |
| 81 | if attack == "intended-missing" { |
| 82 | entry.Device, entry.Inode, entry.Type = 0, 0, 0 |
| 83 | err = os.Remove(path) |
| 84 | } |
| 85 | case "escape": |
| 86 | entry.RelativePath = "../" + filepath.Base(dir) + "/state/retained" |
| 87 | case "unknown-root": |
| 88 | entry.ManagedRoot = "other" |
| 89 | } |
| 90 | if err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Installations: views, Launches: []hostbox.LaunchInventory{launch}, Payloads: generationPayloadFixture(views)} |
| 94 | snapshot.Payloads[0].Inventory.Entries = append(snapshot.Payloads[0].Inventory.Entries, entry) |
| 95 | v := generationReconciler{read: func(context.Context) (generationInventory, error) { return snapshot, nil }, kernel: func(context.Context) (string, string, error) { |
| 96 | if attack == "changed-during-kernel" { |
| 97 | if err := os.Rename(path, path+".old"); err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | } |
| 101 | return "boot", r.Binding.Digest, nil |
| 102 | }} |
| 103 | _, err = v.reconcile(t.Context(), []release.GenerationReservation{r}) |
| 104 | if (err == nil) != (attack == "none" || attack == "directory") { |
| 105 | t.Fatalf("physical payload %s: %v", attack, err) |
| 106 | } |
| 107 | }) |
| 108 | } |
| 109 | } |
| 110 | |