//go:build linux package emission import ( "bufio" "bytes" "context" "errors" "fmt" "github.com/netkingdom/access-engine/pkg/api" "io" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "time" ) func boundedFixture() api.DecisionEnvelope { return api.DecisionEnvelope{ID: "decision:bounded", Effect: api.DecisionEffectDeny, Resource: api.ResourceRef{ID: "synthetic", Type: "tenant", System: "tenant-engine"}} } func fixedOutbox(t *testing.T, dir string, max int64) *Outbox { t.Helper() o, err := OpenBounded(dir, "flex-auth.test", max) if err != nil { t.Fatal(err) } o.SetClock(func() time.Time { return time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC) }) return o } func TestOutboxLockChild(t *testing.T) { dir := os.Getenv("ACCESS_OUTBOX_LOCK_CHILD") if dir == "" { return } o, err := Open(dir, "flex-auth.test") if err != nil { fmt.Println("REFUSED") os.Exit(23) } fmt.Println("READY") var one [1]byte os.Stdin.Read(one[:]) o.Close() os.Exit(0) } func startLockChild(t *testing.T, dir string) (*exec.Cmd, io.WriteCloser, *bufio.Reader) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) child := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestOutboxLockChild$") child.Env = []string{"ACCESS_OUTBOX_LOCK_CHILD=" + dir} input, err := child.StdinPipe() if err != nil { t.Fatal(err) } output, err := child.StdoutPipe() if err != nil { t.Fatal(err) } if err := child.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { child.Process.Kill() }) return child, input, bufio.NewReader(output) } func TestActualProcessWriterExclusionAndCrashRelease(t *testing.T) { dir := t.TempDir() holder, input, output := startLockChild(t, dir) ready, err := output.ReadString('\n') if err != nil || ready != "READY\n" { t.Fatal("holder failed") } before, _ := os.ReadFile(filepath.Join(dir, outboxFile)) if _, err := Open(dir, "flex-auth.test"); err == nil { t.Fatal("second process writer opened") } after, _ := os.ReadFile(filepath.Join(dir, outboxFile)) if !bytes.Equal(before, after) { t.Fatal("contender mutated evidence") } other := fixedOutbox(t, t.TempDir(), MaxOutboxBytes) other.Close() contender, _, result := startLockChild(t, dir) text, _ := result.ReadString('\n') if contender.Wait() == nil || text != "REFUSED\n" { t.Fatal("OS contender did not refuse") } holder.Process.Kill() holder.Wait() input.Close() reopened := fixedOutbox(t, dir, MaxOutboxBytes) if err := reopened.Append(boundedFixture()); err != nil { t.Fatal(err) } reopened.Close() clean, cleanInput, cleanOutput := startLockChild(t, dir) if line, _ := cleanOutput.ReadString('\n'); line != "READY\n" { t.Fatal("close did not release lock") } cleanInput.Close() if clean.Wait() != nil { t.Fatal("clean child close failed") } final := fixedOutbox(t, dir, MaxOutboxBytes) defer final.Close() if final.Status().Committed[Class(api.DecisionEffectDeny)] != 1 { t.Fatal("crash/close recovery changed committed prefix") } if _, err := os.Stat(filepath.Join(dir, "writer.lock")); err != nil { t.Fatal("lock file was unlinked") } } func TestExactCapacityBatchEntryAndRestartBoundaries(t *testing.T) { probe := fixedOutbox(t, t.TempDir(), MaxOutboxBytes) if err := probe.Append(boundedFixture()); err != nil { t.Fatal(err) } size := probe.Status().CommittedBytes probe.Close() dir := t.TempDir() o := fixedOutbox(t, dir, size) if err := o.Append(boundedFixture()); err != nil { t.Fatal(err) } before, _ := os.ReadFile(o.path) if err := o.Append(boundedFixture()); !errors.Is(err, ErrCapacity) { t.Fatal("over-cap append accepted") } after, _ := os.ReadFile(o.path) if !bytes.Equal(before, after) || o.seq != 1 || o.cursor.Seq != 0 { t.Fatal("capacity refusal changed prefix/sequence/cursor") } status := o.Status() if status.RemainingBytes != 0 || !status.CapacityExhausted || status.MaxBytes != size { t.Fatal("capacity status inaccurate") } o.Close() o = fixedOutbox(t, dir, size) if o.Status().CommittedBytes != size || o.seq != 1 { t.Fatal("full outbox restart lost evidence") } o.Close() for _, max := range []int64{size - 1, size*2 - 1} { batch := fixedOutbox(t, t.TempDir(), max) count := 1 if max > size { count = 2 } items := make([]api.DecisionEnvelope, count) for i := range items { items[i] = boundedFixture() } if err := batch.AppendBatch(items); !errors.Is(err, ErrCapacity) { t.Fatal("over-cap batch accepted") } info, _ := os.Stat(batch.path) if info.Size() != 0 || batch.seq != 0 { t.Fatal("batch partially written") } batch.Close() } huge := fixedOutbox(t, t.TempDir(), MaxOutboxBytes) defer huge.Close() entry := boundedFixture() entry.Diagnostics = map[string]any{"payload": strings.Repeat("x", MaxEntryBytes)} if !errors.Is(huge.Append(entry), ErrCapacity) { t.Fatal("oversized entry accepted") } info, _ := os.Stat(huge.path) if info.Size() != 0 { t.Fatal("oversized entry written") } batchMemory := fixedOutbox(t, t.TempDir(), MaxOutboxBytes) defer batchMemory.Close() medium := boundedFixture() medium.Diagnostics = map[string]any{"payload": strings.Repeat("m", MaxBatchBytes/2)} if !errors.Is(batchMemory.AppendBatch([]api.DecisionEnvelope{medium, medium}), ErrCapacity) { t.Fatal("aggregate memory bound exceeded") } if info, _ := os.Stat(batchMemory.path); info.Size() != 0 { t.Fatal("oversized batch partially written") } for _, limit := range []int64{0, -1, MaxOutboxBytes + 1} { if _, err := OpenBounded(t.TempDir(), "flex-auth.test", limit); err == nil { t.Fatal("invalid configured limit") } } } func TestBoundedStartupPreservesOversizedCommittedEvidence(t *testing.T) { for _, sample := range []struct { name string raw []byte max int64 }{{"file", bytes.Repeat([]byte("x"), 1025), 1024}, {"line", append(bytes.Repeat([]byte("x"), MaxEntryBytes+1), '\n'), MaxOutboxBytes}, {"oversized-torn", bytes.Repeat([]byte("x"), MaxEntryBytes+1), MaxOutboxBytes}, {"terminated-corrupt", []byte("{bad}\n"), MaxOutboxBytes}} { t.Run(sample.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, outboxFile) os.WriteFile(path, sample.raw, 0600) if _, err := OpenBounded(dir, "flex-auth.test", sample.max); err == nil { t.Fatal("corrupt/oversized evidence accepted") } after, _ := os.ReadFile(path) if !bytes.Equal(after, sample.raw) { t.Fatal("committed evidence discarded") } }) } dir := t.TempDir() cursor := bytes.Repeat([]byte(" "), 4097) os.WriteFile(filepath.Join(dir, "drain.json"), cursor, 0600) if _, err := Open(dir, "flex-auth.test"); err == nil { t.Fatal("unbounded cursor accepted") } after, _ := os.ReadFile(filepath.Join(dir, "drain.json")) if !bytes.Equal(after, cursor) { t.Fatal("invalid cursor changed") } } func TestActualDiskFullAndFsyncFailureFenceUnconfirmedRollback(t *testing.T) { for _, device := range []string{"/dev/full", "/dev/null"} { t.Run(device, func(t *testing.T) { o := fixedOutbox(t, t.TempDir(), MaxOutboxBytes) defer o.Close() if err := o.Append(boundedFixture()); err != nil { t.Fatal(err) } before, _ := os.ReadFile(o.path) original := o.file fault, err := os.OpenFile(device, os.O_WRONLY, 0) if err != nil { t.Fatal(err) } o.file = fault failure := o.Append(boundedFixture()) if failure == nil { t.Fatal("device write/sync failure accepted") } if device == "/dev/full" && !errors.Is(failure, syscall.ENOSPC) { t.Fatal("did not exercise actual ENOSPC") } if device == "/dev/null" && !strings.Contains(failure.Error(), "sync outbox") { t.Fatal("did not exercise actual fsync failure") } if !o.writeFault || !o.Status().WriterFenced { t.Fatal("unconfirmed rollback did not fence writer") } fault.Close() o.file = original if err := o.Append(boundedFixture()); err == nil { t.Fatal("fenced writer resumed") } after, _ := os.ReadFile(o.path) if !bytes.Equal(before, after) || o.seq != 1 { t.Fatal("disk failure changed committed prefix") } }) } } func TestWriterLockRefusesUnsafeFilesAndStartupFailureReleases(t *testing.T) { for _, kind := range []string{"symlink", "world-readable", "hardlink"} { t.Run(kind, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "writer.lock") switch kind { case "symlink": target := filepath.Join(t.TempDir(), "target") os.WriteFile(target, []byte("private unchanged"), 0600) os.Symlink(target, path) case "world-readable": os.WriteFile(path, nil, 0644) case "hardlink": target := filepath.Join(t.TempDir(), "target") os.WriteFile(target, nil, 0600) os.Link(target, path) } if _, err := Open(dir, "flex-auth.test"); err == nil { t.Fatal("unsafe lock accepted") } }) } dir := t.TempDir() os.WriteFile(filepath.Join(dir, outboxFile), []byte("bad\n"), 0600) if _, err := Open(dir, "flex-auth.test"); err == nil { t.Fatal("corrupt state accepted") } // The failed Open has no live writer and cannot strand an OS lock. Manual // repair here is a disposable fixture action, never an automatic recovery. os.WriteFile(filepath.Join(dir, outboxFile), nil, 0600) o := fixedOutbox(t, dir, MaxOutboxBytes) o.Close() } func TestPVCFSGroupPermissionWideningRetainsWriterExclusion(t *testing.T) { dir := t.TempDir() o := fixedOutbox(t, dir, MaxOutboxBytes) o.Close() // Kubernetes writable-volume fsGroup reconciliation may OR group0660 on // remount. This nonsecret lock must still work for the same admitted group. if err := os.Chmod(filepath.Join(dir, "writer.lock"), 0660); err != nil { t.Fatal(err) } o = fixedOutbox(t, dir, MaxOutboxBytes) defer o.Close() if _, err := Open(dir, "flex-auth.test"); err == nil { t.Fatal("fsGroup-widened file lost exclusive lock") } } func TestJournalAndCursorRefuseAliasesAndFIFOs(t *testing.T) { for _, name := range []string{outboxFile, "drain.json"} { for _, kind := range []string{"symlink", "hardlink", "fifo"} { t.Run(name+"/"+kind, func(t *testing.T) { dir := t.TempDir() target := filepath.Join(t.TempDir(), "evidence") original := []byte("retained evidence\n") if err := os.WriteFile(target, original, 0600); err != nil { t.Fatal(err) } path := filepath.Join(dir, name) var err error switch kind { case "symlink": err = os.Symlink(target, path) case "hardlink": err = os.Link(target, path) case "fifo": err = syscall.Mkfifo(path, 0600) } if err != nil { t.Fatal(err) } if o, err := Open(dir, "flex-auth.test"); err == nil { o.Close() t.Fatal("unsafe evidence path accepted") } after, err := os.ReadFile(target) if err != nil || !bytes.Equal(original, after) { t.Fatal("refusal changed aliased evidence") } }) } } }