Skip to content

[bug] names filed in allow_catchall are never removed, permanently allowlisting them #199

Description

@johanix

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:

  1. 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.
  2. Change the rule upstream to X CNAME rpz-drop. (or .).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions