//go:build linux && rootrunner package host import ( "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) const generationDeviceRebootDir = "/var/lib/carlos-rootrunner-p3-devices" func TestRootRebootGenerationDevicesPhase1(t *testing.T) { if os.Geteuid() != 0 { t.Fatal("root required") } if err := os.Mkdir(generationDeviceRebootDir, 0755); err != nil { t.Fatal(err) } tree := filepath.Join(generationDeviceRebootDir, "tree") if err := os.Mkdir(tree, 0755); err != nil { t.Fatal(err) } path := filepath.Join(tree, "app") if err := os.WriteFile(path, []byte("retained executable"), 0700); err != nil { t.Fatal(err) } f, err := rootstate.OpenTrustedFile(path) if err != nil { t.Fatal(err) } defer f.Close() d, err := os.Open(tree) if err != nil { t.Fatal(err) } defer d.Close() id, err := rootstate.Identity(f) if err != nil { t.Fatal(err) } tid, err := rootstate.Identity(d) if err != nil { t.Fatal(err) } persistent, err := rootstate.ObjectIdentity(f) if err != nil { t.Fatal(err) } tp, err := rootstate.ObjectIdentity(d) if err != nil { t.Fatal(err) } b := release.GenerationBinding{Path: path, Tree: tree, Executable: id, TreeIdentity: tid, ExecutableProof: persistent, TreeProof: tp} s, err := rootstate.Open(filepath.Join(generationDeviceRebootDir, "authority"), true, false) if err != nil { t.Fatal(err) } defer s.Close() raw, _ := json.Marshal(b) if err = s.Write("binding.json", raw); err != nil { t.Fatal(err) } t.Logf("boot=%s device=%d mount=%d", persistent.Boot, id.Device, persistent.Mount) } func TestRootRebootGenerationDevicesPhase2(t *testing.T) { defer os.RemoveAll(generationDeviceRebootDir) s, err := rootstate.Open(filepath.Join(generationDeviceRebootDir, "authority"), false, false) if err != nil { t.Fatal(err) } defer s.Close() raw, err := s.Read("binding.json") if err != nil { t.Fatal(err) } var b release.GenerationBinding if err = json.Unmarshal(raw, &b); err != nil { t.Fatal(err) } boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil || strings.TrimSpace(string(boot)) == b.ExecutableProof.Boot { t.Fatal("actual reboot required", err) } current, err := generationCurrentFiles(b, rootstate.OpenTrustedFile) if err != nil { t.Fatal("retained identity across boot", err) } t.Logf("prior-device=%d current-device=%d", b.Executable.Device, current.Executable.Device) // The reboot need not renumber this particular instance. Exercise that // branch explicitly while retaining the actual previous boot's handles. changed := b changed.Executable.Device++ changed.TreeIdentity.Device++ changed.ExecutableProof.Device++ changed.ExecutableProof.Mount++ changed.TreeProof.Device++ changed.TreeProof.Mount++ if _, err = generationCurrentFiles(changed, rootstate.OpenTrustedFile); err != nil { t.Fatal("boot-local numbers persisted as authority", err) } foreign := b foreign.ExecutableProof.Filesystem = "different-filesystem" if _, err = generationCurrentFiles(foreign, rootstate.OpenTrustedFile); err == nil { t.Fatal("foreign filesystem accepted") } if err = unix.Mount(b.Tree, b.Tree, "", unix.MS_BIND, ""); err != nil { t.Fatal(err) } if _, err = generationCurrentFiles(b, rootstate.OpenTrustedFile); err == nil { t.Fatal("substituted mount accepted") } if err = unix.Unmount(b.Tree, 0); err != nil { t.Fatal(err) } if err = os.Rename(b.Path, b.Path+"-old"); err != nil { t.Fatal(err) } if err = os.WriteFile(b.Path, []byte("retained executable"), 0700); err != nil { t.Fatal(err) } if _, err = generationCurrentFiles(b, rootstate.OpenTrustedFile); err == nil { t.Fatal("substituted inode accepted") } }