fix(auth): harden runtime projection recovery
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
@@ -85,9 +84,7 @@ func TestInspectRejectsMissingBlockedMalformedAndTamperedCurrent(t *testing.T) {
|
||||
if _, err := Inspect(spec); !errors.Is(err, ErrIntegrity) {
|
||||
t.Fatalf("malformed error = %v", err)
|
||||
}
|
||||
if err := os.Remove(filepath.Join(spec.RuntimeRoot, "CURRENT")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
spec = testSpec(t)
|
||||
commitSnapshot(t, spec, testSnapshot(t, "tampered"))
|
||||
if err := os.WriteFile(filepath.Join(spec.RuntimeRoot, "CURRENT"), []byte(`{"version":1,"state":"ready"}`), runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -220,12 +217,161 @@ func TestInspectRejectsUnexpectedGenerationEntry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectRejectsReadyNamespaceOutsideCurrentHistory(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
mutate func(t *testing.T, spec Spec, status Status)
|
||||
}{
|
||||
{"extra generation", func(t *testing.T, spec Spec, _ Status) {
|
||||
name := strings.Repeat("e", 64)
|
||||
if err := os.Mkdir(filepath.Join(spec.RuntimeRoot, "generations", name), runtimeDirectoryMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
{"stage", func(t *testing.T, spec Spec, _ Status) {
|
||||
name := ".stage-" + strings.Repeat("a", 32) + "-" + strings.Repeat("e", 64)
|
||||
if err := os.Mkdir(filepath.Join(spec.RuntimeRoot, "generations", name), runtimeDirectoryMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
{"corrupt predecessor", func(t *testing.T, spec Spec, status Status) {
|
||||
if len(status.Selector.PreviousGenerations) != 1 {
|
||||
t.Fatalf("history = %#v", status.Selector.PreviousGenerations)
|
||||
}
|
||||
path := filepath.Join(spec.RuntimeRoot, "generations", status.Selector.PreviousGenerations[0], "auth.yaml")
|
||||
if err := os.WriteFile(path, []byte("tampered"), runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
{"missing predecessor", func(t *testing.T, spec Spec, status Status) {
|
||||
if len(status.Selector.PreviousGenerations) != 1 {
|
||||
t.Fatalf("history = %#v", status.Selector.PreviousGenerations)
|
||||
}
|
||||
if err := os.RemoveAll(filepath.Join(spec.RuntimeRoot, "generations", status.Selector.PreviousGenerations[0])); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
spec := testSpec(t)
|
||||
commitSnapshot(t, spec, testSnapshot(t, "prior-"+tc.name))
|
||||
status := commitSnapshot(t, spec, testSnapshot(t, "current-"+tc.name))
|
||||
tc.mutate(t, spec, status)
|
||||
if _, err := Inspect(spec); !errors.Is(err, ErrIntegrity) {
|
||||
t.Fatalf("Inspect error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeginRecoversStrictCurrentTemporaryBesideReadySelector(t *testing.T) {
|
||||
spec := testSpec(t)
|
||||
prior := testSnapshot(t, "ready-temporary")
|
||||
commitSnapshot(t, spec, prior)
|
||||
temporaryID := strings.Repeat("a", 32)
|
||||
temporary, err := encodeSelector(Selector{Version: SchemaVersion, State: "blocked", Transaction: temporaryID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(spec.RuntimeRoot, ".current-"+temporaryID+".tmp")
|
||||
if err := os.WriteFile(path, temporary, runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transaction, err := Begin(spec, &prior, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer transaction.Close()
|
||||
if transaction.prior == nil || transaction.prior.Snapshot.Generation != prior.Generation {
|
||||
t.Fatalf("prior = %#v", transaction.prior)
|
||||
}
|
||||
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("ready temporary remains: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeginRejectsMalformedCurrentWithoutCreatingGenerations(t *testing.T) {
|
||||
spec := testSpec(t)
|
||||
if err := os.WriteFile(filepath.Join(spec.RuntimeRoot, "CURRENT"), []byte("malformed"), runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Begin(spec, nil, false); !errors.Is(err, ErrIntegrity) {
|
||||
t.Fatalf("Begin error = %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(filepath.Join(spec.RuntimeRoot, "generations")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("Begin mutated malformed root: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitRefusesCorruptTargetUnlessCurrentIsMatchingBlockedTransaction(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
current func(t *testing.T, root string, transaction string, snapshot Snapshot)
|
||||
}{
|
||||
{"ready", func(t *testing.T, root, _ string, snapshot Snapshot) {
|
||||
data, err := encodeSelector(Selector{Version: SchemaVersion, State: "ready", Transaction: strings.Repeat("b", 32), Generation: snapshot.Generation})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "CURRENT"), data, runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
{"malformed", func(t *testing.T, root, _ string, _ Snapshot) {
|
||||
if err := os.WriteFile(filepath.Join(root, "CURRENT"), []byte("malformed"), runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
{"other blocked transaction", func(t *testing.T, root, transaction string, _ Snapshot) {
|
||||
other := strings.Repeat("b", 32)
|
||||
if other == transaction {
|
||||
other = strings.Repeat("c", 32)
|
||||
}
|
||||
data, err := encodeSelector(Selector{Version: SchemaVersion, State: "blocked", Transaction: other})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "CURRENT"), data, runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
spec := testSpec(t)
|
||||
snapshot := testSnapshot(t, "corrupt-"+tc.name)
|
||||
transaction, err := Begin(spec, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer transaction.Close()
|
||||
target := filepath.Join(spec.RuntimeRoot, "generations", snapshot.Generation)
|
||||
if err := os.Mkdir(target, runtimeDirectoryMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"auth.yaml", "users.yaml", "manifest.json"} {
|
||||
if err := os.WriteFile(filepath.Join(target, name), []byte("corrupt"), runtimeFileMode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
tc.current(t, spec.RuntimeRoot, transaction.transactionID, snapshot)
|
||||
if _, err := transaction.Commit(snapshot); !errors.Is(err, ErrIntegrity) {
|
||||
t.Fatalf("Commit error = %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(target); err != nil {
|
||||
t.Fatalf("corrupt target was removed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitLeavesBlockedAfterInjectedStageWriteFsyncRenameAndVerifyFailure(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
hooks testHooks
|
||||
}{
|
||||
{"stage", testHooks{beforeStageRename: func() error { return errors.New("sentinel") }}},
|
||||
{"stage write", testHooks{beforeStageWrite: func() error { return errors.New("sentinel") }}},
|
||||
{"stage fsync", testHooks{beforeStageFsync: func() error { return errors.New("sentinel") }}},
|
||||
{"stage rename", testHooks{beforeStageRename: func() error { return errors.New("sentinel") }}},
|
||||
{"current", testHooks{beforeCurrentRename: func() error { return errors.New("sentinel") }}},
|
||||
{"verify", testHooks{beforeFinalVerify: func() error { return errors.New("sentinel") }}},
|
||||
} {
|
||||
@@ -474,21 +620,30 @@ func TestConcurrentBeginSerializesAcrossCurrentRenameWithoutCreatingLockEntry(t
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
started := make(chan struct{})
|
||||
checked := make(chan error, 1)
|
||||
restore := setTestHooksForTest(testHooks{beforeFlock: func(fd int) {
|
||||
if err := unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB); !errors.Is(err, unix.EWOULDBLOCK) {
|
||||
checked <- fmt.Errorf("nonblocking flock error = %v", err)
|
||||
return
|
||||
}
|
||||
checked <- nil
|
||||
}})
|
||||
defer restore()
|
||||
finished := make(chan error, 1)
|
||||
go func() {
|
||||
close(started)
|
||||
second, err := Begin(spec, nil, false)
|
||||
if err == nil {
|
||||
err = second.Close()
|
||||
}
|
||||
finished <- err
|
||||
}()
|
||||
<-started
|
||||
if err := <-checked; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-finished:
|
||||
t.Fatalf("second Begin did not block: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
default:
|
||||
}
|
||||
if err := first.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user