| index b7986dd7d..e0c6a387f 100644 |
| --- a/internal/sim/hostlife/protocol_test.go |
| +++ b/internal/sim/hostlife/protocol_test.go |
| @@ -2015,15 +2015,30 @@ func (w *world) probeAccepted(b *box, c *claim) { |
| // parkStep advances one park of `prefix` in b's own bucket into dest. It |
| // returns true when the location is verified empty after the quiet time. |
| // keep, when set, names keys the park must leave alone. |
| -func (w *world) parkStep(b *box, prefix, dest string, quiet int, keep func(string) bool) bool { |
| - return w.parkDepth(b, prefix, dest, quiet, keep, 0) |
| +func (w *world) parkStep(b *box, prefix, dest string, quiet int, scope *parkScope, keep func(string) bool) bool { |
| + return w.parkDepth(b, prefix, dest, quiet, keep, scope, 0) |
| +} |
| + |
| +// member is INV-23's second half (claim rev 6.24 §2; R16-2, R17-1): every |
| +// reference a scoped park archives or deletes, in a manifest it builds |
| +// and in a persisted manifest it replays, must be within the scope. It |
| +// is checked for every reference, whatever rules are on: the listing's |
| +// confinement (rule scope-membership) is the implementation, this is the |
| +// oracle. An out-of-scope reference is a violation and is not acted on. |
| +func (w *world) member(scope *parkScope, id, what string, r ref) bool { |
| + if scope == nil || inScope(r.Key, scope.Location) { |
| + return true |
| + } |
| + w.violate("INV-23: chore %s (scope %s, incarnation %d) would %s %s@%d, outside its scope", id, scope.Location, scope.Inc, what, r.Key, r.Ver) |
| + return false |
| } |
| |
| -func (w *world) parkDepth(b *box, prefix, dest string, quiet int, keep func(string) bool, depth int) bool { |
| +func (w *world) parkDepth(b *box, prefix, dest string, quiet int, keep func(string) bool, scope *parkScope, depth int) bool { |
| if depth > 8 { |
| return false // a park that cannot finish: reported as non-convergence |
| } |
| bk := w.buckets[b.Region] |
| + id := dest |
| skip := func(k string) bool { |
| return (strings.HasSuffix(k, "lock.json") && on("park-skip-lock")) || (keep != nil && keep(k)) |
| } |
| @@ -2055,9 +2070,13 @@ func (w *world) parkDepth(b *box, prefix, dest string, quiet int, keep func(stri |
| if !on("park-all-versions") && !current { |
| continue |
| } |
| - dels = append(dels, ref{Region: b.Region, Key: k, Ver: v.ID}) |
| + r := ref{Region: b.Region, Key: k, Ver: v.ID} |
| + if !w.member(scope, id, "delete", r) { |
| + continue // out of scope: never deleted, never archived |
| + } |
| + dels = append(dels, r) |
| if !v.Marker { |
| - archive = append(archive, ref{Region: b.Region, Key: k, Ver: v.ID}) |
| + archive = append(archive, r) |
| } |
| } |
| } |
| @@ -2079,10 +2098,16 @@ func (w *world) parkDepth(b *box, prefix, dest string, quiet int, keep func(stri |
| w.issue(b.ID, &request{kind: rPut, region: b.Region, key: manKey, b: &blob{Kind: "manifest", Refs: archive, Dels: dels}}) |
| return false |
| } |
| - // replay the manifest's deletion set, by version id, in one batch per visit |
| + // replay the manifest's deletion set, by version id, in one batch per |
| + // visit; a persisted manifest's references are checked against the |
| + // scope as the fresh set was (R17-1: a manifest naming another |
| + // incarnation's keys under a valid scope is refused, not replayed) |
| issued := false |
| + for _, r := range man.B.Refs { |
| + w.member(scope, id, "archive", r) |
| + } |
| for _, r := range man.B.Dels { |
| - if bk.byID(r.Key, r.Ver) == nil { |
| + if !w.member(scope, id, "delete", r) || bk.byID(r.Key, r.Ver) == nil { |
| continue |
| } |
| if on("park-by-version") { |
| @@ -2106,7 +2131,7 @@ func (w *world) parkDepth(b *box, prefix, dest string, quiet int, keep func(stri |
| } |
| for _, v := range bk.objs[k] { |
| if !v.Marker { |
| - return w.parkDepth(b, prefix, dest+"s/", quiet, keep, depth+1) |
| + return w.parkDepth(b, prefix, dest+"s/", quiet, keep, scope, depth+1) |
| } |
| } |
| } |
| @@ -2202,12 +2227,6 @@ func (w *world) doObligation(b *box, c *claim, o obligation) { |
| if o.Scope != nil && o.Kind != "park-local" { |
| loc := o.Scope.Location |
| member = func(k string) bool { return inScope(k, loc) } |
| - for _, k := range w.buckets[b.Region].keys(o.Prefix) { |
| - if !member(k) && hasNonMarker(w.buckets[b.Region].objs[k]) && (o.Kind != "park-sweep" || w.sweepable(w.claim, b.Region, k)) && !on("scope-membership") { |
| - w.violate("INV-23: chore %s (%s, scope %s, incarnation %d) would archive %s, outside its scope", o.ID, o.Kind, loc, o.Scope.Inc, k) |
| - break |
| - } |
| - } |
| } |
| if o.Kind == "park-replica" || o.Kind == "park-superseded" || o.Kind == "park-staging" { |
| // the owner first proves the region's box runs nothing for the |
| @@ -2308,7 +2327,7 @@ func (w *world) doObligation(b *box, c *claim, o obligation) { |
| inner := keep |
| keep = func(k string) bool { return genOf(o.Prefix, k) != retire || inner != nil && inner(k) } |
| } |
| - if !w.parkStep(b, o.Prefix, o.Dest(), quiet, keep) { |
| + if !w.parkStep(b, o.Prefix, o.Dest(), quiet, o.Scope, keep) { |
| return |
| } |
| } |
| @@ -2399,6 +2418,25 @@ func (w *world) scoped(o obligation, evidenceAt, inc int) obligation { |
| // scopeLocation is the scope grammar's form of a park prefix. |
| func scopeLocation(prefix string) string { return strings.TrimSuffix(prefix, "/") } |
| |
| +// opScope is the scope an operation's own park runs under (a landing's |
| +// install park of the target region's earlier stay, a dirty return's and |
| +// a push's park of what their install supersedes): holder.location, the |
| +// holder's incarnation, the operation's own evidence time (R16-1, R17-1). |
| +func (w *world) opScope(c *claim, prefix string) *parkScope { |
| + inc := 0 |
| + if c.Holder != nil { |
| + inc = c.Holder.Inc |
| + } |
| + return &parkScope{Location: scopeLocation(prefix), Inc: inc, EvidenceAt: w.now, RequestLimit: requestLimitH} |
| +} |
| + |
| +// confine is the keep function that confines a park's listing to its |
| +// scope: a legacy instances/<host> scope excludes child incarnations. |
| +func confine(prefix string) func(string) bool { |
| + loc := scopeLocation(prefix) |
| + return func(k string) bool { return !inScope(k, loc) } |
| +} |
| + |
| // scopeOf is the scope location a key belongs to (claim rev 6.24 §2): a |
| // staging key's is moves/<host> (or its own lane, moves/<host>/<op id>, |
| // which inScope tests by prefix); an instance key's is its incarnation |