fix(auth): harden runtime projection recovery
This commit is contained in:
@@ -16,6 +16,9 @@ import (
|
||||
const maximumDirectoryEntries = 16
|
||||
|
||||
type testHooks struct {
|
||||
beforeFlock func(int)
|
||||
beforeStageWrite func() error
|
||||
beforeStageFsync func() error
|
||||
beforeStageRename func() error
|
||||
beforeCurrentRename func() error
|
||||
beforeFinalVerify func() error
|
||||
@@ -49,6 +52,15 @@ func hook(selectHook func(testHooks) func() error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func rendezvousBeforeFlock(root int) {
|
||||
hookState.Lock()
|
||||
callback := hookState.hooks.beforeFlock
|
||||
hookState.Unlock()
|
||||
if callback != nil {
|
||||
callback(root)
|
||||
}
|
||||
}
|
||||
|
||||
func Inspect(spec Spec) (Status, error) {
|
||||
root, err := openRoot(spec)
|
||||
if err != nil {
|
||||
@@ -63,16 +75,12 @@ func Begin(spec Spec, before *Snapshot, requireReadyMatch bool) (*Transaction, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rendezvousBeforeFlock(root)
|
||||
if unix.Flock(root, unix.LOCK_EX) != nil {
|
||||
unix.Close(root)
|
||||
return nil, ErrIntegrity
|
||||
}
|
||||
transaction := &Transaction{spec: spec, before: before, rootFD: root, lockFD: root}
|
||||
if ensureGenerations(root, spec) != nil {
|
||||
_ = transaction.Close()
|
||||
return nil, ErrIntegrity
|
||||
}
|
||||
|
||||
selector, selectorErr := readSelector(root, spec)
|
||||
switch {
|
||||
case selectorErr == nil && selector.State == "blocked":
|
||||
@@ -81,7 +89,7 @@ func Begin(spec Spec, before *Snapshot, requireReadyMatch bool) (*Transaction, e
|
||||
return nil, ErrIntegrity
|
||||
}
|
||||
case selectorErr == nil:
|
||||
if validateRootEntries(root, spec) != nil {
|
||||
if recoverCurrentTemporaries(root, spec) != nil || validateRootEntries(root, spec) != nil {
|
||||
_ = transaction.Close()
|
||||
return nil, ErrIntegrity
|
||||
}
|
||||
@@ -98,7 +106,10 @@ func Begin(spec Spec, before *Snapshot, requireReadyMatch bool) (*Transaction, e
|
||||
case currentExists(root):
|
||||
_ = transaction.Close()
|
||||
return nil, ErrIntegrity
|
||||
case requireReadyMatch || validateInitialRootEntries(root, spec) != nil:
|
||||
case requireReadyMatch || validateEmptyRootEntries(root, spec) != nil:
|
||||
_ = transaction.Close()
|
||||
return nil, ErrIntegrity
|
||||
case ensureGenerations(root, spec) != nil:
|
||||
_ = transaction.Close()
|
||||
return nil, ErrIntegrity
|
||||
}
|
||||
@@ -135,7 +146,7 @@ func (transaction *Transaction) Commit(after Snapshot) (Status, error) {
|
||||
if writeSelector(transaction.rootFD, transaction.spec, selector) != nil {
|
||||
return Status{}, ErrIntegrity
|
||||
}
|
||||
status, err := inspectFD(transaction.rootFD, transaction.spec)
|
||||
status, err := inspectSelectedGeneration(transaction.rootFD, transaction.spec, selector)
|
||||
if err != nil || status.Snapshot.Generation != after.Generation {
|
||||
return Status{}, ErrIntegrity
|
||||
}
|
||||
@@ -146,6 +157,10 @@ func (transaction *Transaction) Commit(after Snapshot) (Status, error) {
|
||||
if retain(transaction.rootFD, transaction.spec, selector) != nil {
|
||||
return Status{}, ErrIntegrity
|
||||
}
|
||||
status, err = inspectFD(transaction.rootFD, transaction.spec)
|
||||
if err != nil || status.Snapshot.Generation != after.Generation {
|
||||
return Status{}, ErrIntegrity
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
@@ -313,9 +328,9 @@ func validateRootEntries(root int, spec Spec) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func validateInitialRootEntries(root int, spec Spec) error {
|
||||
func validateEmptyRootEntries(root int, spec Spec) error {
|
||||
names, err := scanDirectoryNames(root, spec)
|
||||
if err != nil || len(names) != 1 || !names["generations"] {
|
||||
if err != nil || len(names) != 0 {
|
||||
return ErrIntegrity
|
||||
}
|
||||
return nil
|
||||
@@ -332,6 +347,20 @@ func inspectFD(root int, spec Spec) (Status, error) {
|
||||
return inspectCapturedSelector(root, spec, selector)
|
||||
}
|
||||
func inspectCapturedSelector(root int, spec Spec, selector Selector) (Status, error) {
|
||||
if selector.State == "blocked" {
|
||||
return Status{Selector: selector}, ErrBlocked
|
||||
}
|
||||
status, err := inspectSelectedGeneration(root, spec, selector)
|
||||
if err != nil {
|
||||
return Status{}, err
|
||||
}
|
||||
if validateReadyGenerationNamespace(root, spec, selector) != nil {
|
||||
return Status{}, ErrIntegrity
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func inspectSelectedGeneration(root int, spec Spec, selector Selector) (Status, error) {
|
||||
if selector.State == "blocked" {
|
||||
return Status{Selector: selector}, ErrBlocked
|
||||
}
|
||||
@@ -342,6 +371,34 @@ func inspectCapturedSelector(root int, spec Spec, selector Selector) (Status, er
|
||||
return Status{Selector: selector, Snapshot: snapshot}, nil
|
||||
}
|
||||
|
||||
func validateReadyGenerationNamespace(root int, spec Spec, selector Selector) error {
|
||||
generations, err := openDirectoryAt(root, "generations", spec)
|
||||
if err != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
defer unix.Close(generations)
|
||||
names, err := scanDirectoryNames(generations, spec)
|
||||
if err != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
keep := map[string]bool{selector.Generation: true}
|
||||
for _, generation := range selector.PreviousGenerations {
|
||||
keep[generation] = true
|
||||
}
|
||||
if len(names) != len(keep) {
|
||||
return ErrIntegrity
|
||||
}
|
||||
for name := range names {
|
||||
if !validHex(name, 64) || !keep[name] {
|
||||
return ErrIntegrity
|
||||
}
|
||||
if _, err := readGeneration(root, spec, name); err != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSelector(root int, spec Spec) (Selector, error) {
|
||||
data, err := readRegularAt(root, "CURRENT", spec, maximumSelectorBytes)
|
||||
if err != nil {
|
||||
@@ -516,7 +573,7 @@ func stageGeneration(root int, spec Spec, transactionID string, snapshot Snapsho
|
||||
if found, readErr := readGeneration(root, spec, snapshot.Generation); readErr == nil && found.Generation == snapshot.Generation {
|
||||
return nil
|
||||
}
|
||||
if removeConfinedDirectory(generations, snapshot.Generation, spec, false) != nil {
|
||||
if !currentMatchesBlockedTransaction(root, spec, transactionID) || removeConfinedDirectory(generations, snapshot.Generation, spec, false) != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
} else if !errors.Is(err, unix.ENOENT) {
|
||||
@@ -546,7 +603,16 @@ func stageGeneration(root int, spec Spec, transactionID string, snapshot Snapsho
|
||||
if err != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
if writeRegularAt(stage, "auth.yaml", spec, snapshot.Auth) != nil || (snapshot.Mode == "local" && writeRegularAt(stage, "users.yaml", spec, snapshot.Users) != nil) || writeRegularAt(stage, "manifest.json", spec, manifestData) != nil || unix.Fsync(stage) != nil {
|
||||
if err := hook(func(h testHooks) func() error { return h.beforeStageWrite }); err != nil {
|
||||
return err
|
||||
}
|
||||
if writeRegularAt(stage, "auth.yaml", spec, snapshot.Auth) != nil || (snapshot.Mode == "local" && writeRegularAt(stage, "users.yaml", spec, snapshot.Users) != nil) || writeRegularAt(stage, "manifest.json", spec, manifestData) != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
if err := hook(func(h testHooks) func() error { return h.beforeStageFsync }); err != nil {
|
||||
return err
|
||||
}
|
||||
if unix.Fsync(stage) != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
if err := hook(func(h testHooks) func() error { return h.beforeStageRename }); err != nil {
|
||||
@@ -622,6 +688,13 @@ func recoverTemporary(root int, spec Spec, blocked Selector) error {
|
||||
return ErrIntegrity
|
||||
}
|
||||
}
|
||||
if unix.Fsync(generations) != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
return recoverCurrentTemporaries(root, spec)
|
||||
}
|
||||
|
||||
func recoverCurrentTemporaries(root int, spec Spec) error {
|
||||
rootNames, err := scanDirectoryNames(root, spec)
|
||||
if err != nil || !rootNames["CURRENT"] || !rootNames["generations"] {
|
||||
return ErrIntegrity
|
||||
@@ -643,11 +716,16 @@ func recoverTemporary(root int, spec Spec, blocked Selector) error {
|
||||
return ErrIntegrity
|
||||
}
|
||||
}
|
||||
if unix.Fsync(generations) != nil || unix.Fsync(root) != nil {
|
||||
if unix.Fsync(root) != nil {
|
||||
return ErrIntegrity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentMatchesBlockedTransaction(root int, spec Spec, transactionID string) bool {
|
||||
selector, err := readSelector(root, spec)
|
||||
return err == nil && selector.State == "blocked" && selector.Transaction == transactionID
|
||||
}
|
||||
func stageGenerationName(name, transactionID string) (string, bool) {
|
||||
prefix := ".stage-" + transactionID + "-"
|
||||
if len(name) != len(prefix)+64 || name[:len(prefix)] != prefix {
|
||||
|
||||
Reference in New Issue
Block a user