Skip to content

sonic: enforce per-key ownership of ACL tables#2370

Open
ideaship wants to merge 1 commit into
mainfrom
sonic-acl-multi-owner-guard
Open

sonic: enforce per-key ownership of ACL tables#2370
ideaship wants to merge 1 commit into
mainfrom
sonic-acl-multi-owner-guard

Conversation

@ideaship

Copy link
Copy Markdown
Contributor

Problem

ACL_TABLE and ACL_RULE are owned by more than one control-plane section helper (SSH, SNMP, gNMI). The ownership model settled in the generate_sonic_config docstring covers whole-table ownership but not how several helpers share one owned table. The open ACL PRs each rebind the whole table — config["ACL_TABLE"] = {...} — so whichever helper runs second drops the first's entries, leaving SSH or SNMP/gNMI unrestricted. Each PR passes its own tests because neither calls the other helper, so the collision is invisible when the PRs are reviewed in isolation.

Change

Co-owned tables must be merged per named entry, never rebound. The rule is locked in code rather than left to review, so a divergent PR fails CI before it can merge:

  • MULTI_OWNER_OWNED_TABLE_KEYS lists ACL_TABLE/ACL_RULE.
  • A static guard (TestMultiOwnerTableGuard) parses the generator source and fails on every form that rebinds a listed table wholesale on config: config["X"] = <expr>, config.update({"X": ...}), config |= {"X": ...}, and config = {**config, "X": ...}. These mirror the wholesale-write forms the existing mutation backstop recognizes, so no analyzable write can rebind a listed table without being caught.
  • In-place forms that mutate the table dict are left alone: config["X"]["k"] = ..., config["X"].update(...), config["X"] |= ..., and config.setdefault("X", {}). Synthetic-source detector tests prove the guard catches each rebind form and passes the in-place forms, so it cannot pass vacuously.

The tables are listed ahead of the helpers that introduce them, so the first ACL helper to land is forced onto the per-key pattern rather than establishing the unsafe one. Two invariants tie the list to the rest of the model: a referenced multi-owner table must also be in OWNED_TABLE_KEYS (so the central up-front drop clears it and no stale entry survives the merge), and multi-owner tables must not be inherited or image-consumed (those regimes are not dropped). Both hold now and after the ACL helpers add ACL_TABLE/ACL_RULE to ON_DEMAND_OWNED_TABLE_KEYS.

Note for the ACL PRs

This guard is dormant until a helper references the tables. When the control-plane ACL work lands, each helper must merge its own keys and the PR must add ACL_TABLE/ACL_RULE to ON_DEMAND_OWNED_TABLE_KEYS in the same change (the test_referenced_multi_owner_tables_are_owned invariant enforces this).

Related

ACL_TABLE and ACL_RULE are owned by more than one control-plane
section helper (SSH, SNMP, gNMI). The decided ownership model
(generate_sonic_config docstring, locked by #2297 and the static
classification guard from #2339) settles whole-table ownership but not
how several helpers share one owned table. The open ACL PRs each rebind
the whole table -- config["ACL_TABLE"] = {...} -- so whichever helper
runs second drops the first's entries, leaving SSH or SNMP/gNMI
unrestricted. Each PR passes its own tests because neither calls the
other helper, so the collision is invisible when the PRs are reviewed
in isolation.

The fix is a rule that co-owned tables must be merged per named entry,
never rebound. Following the approach of the #2339 classification
guard, the rule is locked in code rather than left to review, so a
divergent PR fails CI before it can merge.

Add MULTI_OWNER_OWNED_TABLE_KEYS listing ACL_TABLE/ACL_RULE, and a
static guard (TestMultiOwnerTableGuard) that parses the generator
source and fails on every form that rebinds a listed table wholesale on
config: config["X"] = <expr>, config.update({"X": ...}),
config |= {"X": ...}, and config = {**config, "X": ...}. These mirror
the wholesale-write forms the #2339 mutation backstop recognizes, so no
analyzable write can rebind a listed table without being caught. Forms
that mutate the table dict in place are left alone: a nested-subscript
write config["X"]["k"] = ..., config["X"].update(...),
config["X"] |= ..., and config.setdefault("X", {}). Synthetic-source
detector tests prove the guard catches each rebind form and passes the
in-place forms, so it cannot pass vacuously.

The tables are listed ahead of the ACL helpers that introduce them, so
the first such helper to land is forced onto the per-key pattern rather
than establishing the unsafe one. Two invariants tie the list to the
rest of the model: a referenced multi-owner table must also be in
OWNED_TABLE_KEYS (so the central up-front drop clears it and no stale
entry survives the merge), and multi-owner tables must not be inherited
or image-consumed (those regimes are not dropped). Both hold now and
after the ACL helpers add ACL_TABLE/ACL_RULE to ON_DEMAND_OWNED.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Roger Luethi <luethi@osism.tech>
@ideaship ideaship requested a review from berendt June 13, 2026 11:02
@ideaship ideaship marked this pull request as ready for review June 13, 2026 11:02

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

2 participants