//go:build linux package host import ( "errors" "os" "path/filepath" "strconv" "testing" "github.com/carlosframework/platform/internal/stopevidence" ) func TestRev4WritersPrefixCoverage(t *testing.T) { for _, shape := range []string{"deleted-wal", "nested", "deleted-nested", "empty-directory"} { t.Run(shape, func(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) // No journal identity exists for a file first opened after launch. fx.journal.Files = nil target := fx.db + "-wal" if shape == "nested" || shape == "deleted-nested" { target = filepath.Join(filepath.Dir(fx.db), "sqlite", "later.db") } if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(target, nil, 0600); err != nil { t.Fatal(err) } held, err := os.Open(target) if err != nil { t.Fatal(err) } defer held.Close() if shape != "nested" { if err := os.Remove(target); err != nil { t.Fatal(err) } target += " (deleted)" } if shape == "empty-directory" { if err := os.Remove(fx.db); err != nil { t.Fatal(err) } } // A real descriptor retains its inode after unlink, unlike a dangling fixture symlink. pid := strconv.Itoa(os.Getpid()) if err := os.Symlink(filepath.Join("/proc", pid), filepath.Join(fx.proc, pid)); err != nil { t.Fatal(err) } if _, err := fx.verify(t); !errors.Is(err, stopevidence.ErrForeignHolder) { t.Fatalf("foreign %s holder passed writers-stopped: %v", shape, err) } }) } } func TestRev4WritersPrefixRetainsDescriptorIdentity(t *testing.T) { fx := newWritersKernelFixture(t) target := filepath.Join(filepath.Dir(fx.db), "sqlite", "later.db") if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(target, nil, 0600); err != nil { t.Fatal(err) } rev3HoldTarget(t, fx, 20, 8, target) dev, ino := fileID(t, target) fx.hints = append(fx.hints, stopevidence.DescriptorHint{FD: 8, Device: dev, Inode: ino}) got, err := fx.verify(t) if err != nil { t.Fatal(err) } want := stopevidence.Descriptor{PID: 20, TID: 20, FD: 8, Device: dev, Inode: ino} if len(got.Retained) != 2 || got.Retained[1] != want { t.Fatalf("lost prefix descriptor identity: %+v", got.Retained) } fx.hints = fx.hints[:1] if _, err := fx.verify(t); !errors.Is(err, stopevidence.ErrForeignHolder) { t.Fatalf("unhinted prefix: %v", err) } } func TestRev4WritersPrefixBoundaryAndScanErrors(t *testing.T) { t.Run("sibling", func(t *testing.T) { fx := newWritersKernelFixture(t) target := filepath.Join(filepath.Dir(fx.db)+"-old", "other.db") if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(target, nil, 0600); err != nil { t.Fatal(err) } rev3HoldTarget(t, fx, 31, 8, target) if _, err := fx.verify(t); err != nil { t.Fatalf("sibling matched: %v", err) } }) t.Run("empty-unreadable", func(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) for _, p := range []string{fx.db, fx.db + "-wal"} { if err := os.Remove(p); err != nil { t.Fatal(err) } } dir := filepath.Join(fx.proc, "31", "task", "31", "fd") if err := os.MkdirAll(dir, 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "8"), nil, 0600); err != nil { t.Fatal(err) } if _, err := fx.verify(t); err == nil { t.Fatal("unreadable table yielded writers-stopped") } }) } func TestRev4WritersDeletedReplicatorIdentity(t *testing.T) { fx := newWritersKernelFixture(t) fx.dropAgentHolds(t) path := fx.db + "-wal" held, err := os.Open(path) if err != nil { t.Fatal(err) } defer held.Close() dev, ino := fileID(t, path) if err := os.Remove(path); err != nil { t.Fatal(err) } // Keep one real kernel descriptor table under the authenticated fixture // process. Readlink reports the deleted name; Stat still reaches the inode. fdDir := filepath.Join(fx.proc, "20", "task", "20", "fd") if err := os.Remove(fdDir); err != nil { t.Fatal(err) } if err := os.Symlink("/proc/self/fd", fdDir); err != nil { t.Fatal(err) } hint := stopevidence.DescriptorHint{FD: int(held.Fd()), Device: dev, Inode: ino} fx.hints = []stopevidence.DescriptorHint{hint} got, err := fx.verify(t) if err != nil { t.Fatal(err) } want := stopevidence.Descriptor{PID: 20, TID: 20, FD: hint.FD, Device: dev, Inode: ino} if len(got.Retained) != 1 || got.Retained[0] != want { t.Fatalf("deleted replicator identity: %+v", got.Retained) } fx.hints[0].Inode++ if _, err := fx.verify(t); !errors.Is(err, stopevidence.ErrForeignHolder) { t.Fatalf("substituted hint exempted deleted file: %v", err) } }