Summary
A name that is once filed in the shared allow_catchall bucket stays there for the lifetime of the process. allow_catchall is an allowlist, and allowlist is absolute in decide(), so that name can no longer be blocked by any rule from any source — including after the rule that caused it is withdrawn upstream.
Mechanism
A rule in the wrong kind of source is copied into a shared catchall bucket rather than dropped:
rpz-passthru. in a deny- or doubtlist source → pd.Lists["allowlist"]["allow_catchall"] (sources.go:533, sources.go:543)
- a blocking action in an allowlist source →
pd.Lists["doubtlist"]["doubt_catchall"] (sources.go:519)
Both buckets are created empty at startup (sources.go:133, sources.go:144), are keyed by name only, and are shared by every source. Nothing records which source contributed a name, and nothing ever deletes from them.
decide() stage 1 returns pd.Policy.AllowlistAction for any name with an allowlist hit, before the denylist is consulted (policy.go). An allow_catchall entry therefore outranks every later denylist rule for that name.
Reproduction
Observed 2026-08-24 against a denylist RPZ upstream:
- Publish
X CNAME rpz-passthru. in the denylist upstream. pop logs the misplaced rule and files X in allow_catchall. X is not in the served zone.
- Change the rule upstream to
X CNAME rpz-drop. (or .).
X is still absent from the served zone. It stays absent for every later rule for X, from any source.
Control: a name that was never published as passthru, carrying the same rpz-drop. rule, is served normally (as the configured policy.denylist.action).
The buckets are in-memory only, so a restart clears the state — which also means the served zone depends on the order rules arrived in, not only on the current inputs.
Impact
One rpz-passthru. rule from any upstream permanently exempts that name from filtering, and withdrawing the rule does not restore it. An upstream feed can disable filtering for a name in a way the operator cannot undo without restarting pop.
Fix direction
The catchall buckets need per-source provenance, so a per-source diff can be applied to them the way it is now applied to each source's own Names map. The #197 fix stages each transfer and applies the resulting delta, but it cannot cover these buckets: with no record of which source contributed an entry, there is nothing to diff a given source's transfer against.
Related: #175, #197.
Summary
A name that is once filed in the shared
allow_catchallbucket stays there for the lifetime of the process.allow_catchallis an allowlist, and allowlist is absolute indecide(), so that name can no longer be blocked by any rule from any source — including after the rule that caused it is withdrawn upstream.Mechanism
A rule in the wrong kind of source is copied into a shared catchall bucket rather than dropped:
rpz-passthru.in a deny- or doubtlist source →pd.Lists["allowlist"]["allow_catchall"](sources.go:533,sources.go:543)pd.Lists["doubtlist"]["doubt_catchall"](sources.go:519)Both buckets are created empty at startup (
sources.go:133,sources.go:144), are keyed by name only, and are shared by every source. Nothing records which source contributed a name, and nothing ever deletes from them.decide()stage 1 returnspd.Policy.AllowlistActionfor any name with an allowlist hit, before the denylist is consulted (policy.go). Anallow_catchallentry therefore outranks every later denylist rule for that name.Reproduction
Observed 2026-08-24 against a denylist RPZ upstream:
X CNAME rpz-passthru.in the denylist upstream. pop logs the misplaced rule and filesXinallow_catchall.Xis not in the served zone.X CNAME rpz-drop.(or.).Xis still absent from the served zone. It stays absent for every later rule forX, from any source.Control: a name that was never published as passthru, carrying the same
rpz-drop.rule, is served normally (as the configuredpolicy.denylist.action).The buckets are in-memory only, so a restart clears the state — which also means the served zone depends on the order rules arrived in, not only on the current inputs.
Impact
One
rpz-passthru.rule from any upstream permanently exempts that name from filtering, and withdrawing the rule does not restore it. An upstream feed can disable filtering for a name in a way the operator cannot undo without restarting pop.Fix direction
The catchall buckets need per-source provenance, so a per-source diff can be applied to them the way it is now applied to each source's own
Namesmap. The#197fix stages each transfer and applies the resulting delta, but it cannot cover these buckets: with no record of which source contributed an entry, there is nothing to diff a given source's transfer against.Related: #175, #197.