Severity: Medium · bug (correctness) · policy
GenerateRpzAxfr's denylist-emission loop emits every denylisted name directly, without consulting decide() / the allowlist:
for name := range pd.DenylistedNames {
data[name+pd.Rpz.ZoneName] = &tapir.RpzName{ ... Action: pd.Policy.DenylistAction }
}
But decide() enforces the documented invariant "allowlist is absolute" (a name on any allowlist is never in the output, winning over deny and doubt). The incremental path (GenerateRpzIxfr) routes names through decide() and so honours it; the full rebuild does not.
Consequence: if a name appears on both an allowlist and a denylist (feeds disagree), the served zone diverges depending on the last operation:
- after a full AXFR rebuild → the name IS in the zone (deny emitted directly), violating allowlist precedence;
- after an incremental update → the name is NOT in the zone (allowlist wins via
decide()).
So the served output is path-dependent and contradicts both decide() and the #156 design doc's invariant #1.
Origin: this is #156-lineage, not the #149 snapshot work — the old commented-out if pd.Allowlisted(k) filter at rpz.go:45 was never restored when the deny loop was rewritten. It is live on main (post-#156 merge).
Fix (separate PR off main):
- Filter the denylist-emission through the allowlist check (or run each deny name through
decide()), so allowlisted names are excluded from a full rebuild too.
- Add a table-driven regression test: a name on allowlist + denylist must NOT appear in the published snapshot's
Data after GenerateRpzAxfr (mirrors the existing TestDecide "allowlist beats deny and doubt" case, but at the rebuild level).
Found in the adversarial review of PR #174 (§2.3): docs/2026-06-03-pop-174-adversarial-review.md. Related: #156, #149/#174.
Severity: Medium · bug (correctness) · policy
GenerateRpzAxfr's denylist-emission loop emits every denylisted name directly, without consultingdecide()/ the allowlist:But
decide()enforces the documented invariant "allowlist is absolute" (a name on any allowlist is never in the output, winning over deny and doubt). The incremental path (GenerateRpzIxfr) routes names throughdecide()and so honours it; the full rebuild does not.Consequence: if a name appears on both an allowlist and a denylist (feeds disagree), the served zone diverges depending on the last operation:
decide()).So the served output is path-dependent and contradicts both
decide()and the #156 design doc's invariant #1.Origin: this is #156-lineage, not the #149 snapshot work — the old commented-out
if pd.Allowlisted(k)filter atrpz.go:45was never restored when the deny loop was rewritten. It is live onmain(post-#156 merge).Fix (separate PR off main):
decide()), so allowlisted names are excluded from a full rebuild too.DataafterGenerateRpzAxfr(mirrors the existingTestDecide"allowlist beats deny and doubt" case, but at the rebuild level).Found in the adversarial review of PR #174 (§2.3):
docs/2026-06-03-pop-174-adversarial-review.md. Related: #156, #149/#174.