Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
349 lines
11 KiB
Go
349 lines
11 KiB
Go
//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")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|