Skip to content

Formal methods for SQL RBAC: design and kernel implementation - #38186

Draft
jasonhernandez wants to merge 4 commits into
mainfrom
claude/sql-rbac-formal-methods-79oiky
Draft

Formal methods for SQL RBAC: design and kernel implementation#38186
jasonhernandez wants to merge 4 commits into
mainfrom
claude/sql-rbac-formal-methods-79oiky

Conversation

@jasonhernandez

@jasonhernandez jasonhernandez commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

EXPERIMENTAL

Motivation

The RBAC implementation in src/sql/src/rbac.rs is security-critical but verified almost entirely by example. The authorization policy is embedded in ~1,200 lines of pattern matching with no separate specification, making it difficult to review as a policy, impossible to tell omissions from deliberate decisions, and prone to divergence between multiple implementations of the same predicates.

This change introduces a formal methods framework to address these issues systematically, starting with the lowest-cost, highest-value steps: writing the policy down as reviewable data and building a verified kernel for the decision procedure.

Description

This PR implements the first two layers of a four-layer formal methods ladder for RBAC:

Layer 0a: Policy as data

  • Adds RbacRequirementsDescription, a public owned type that describes what a plan requires, separate from the internal RbacRequirements representation
  • Exposes describe_rbac_requirements() to dump the policy table as data, enabling review and assertion
  • Adds a datadriven test harness (src/adapter/tests/rbac.rs) that renders requirements in human-readable form
  • Includes initial test cases in src/adapter/tests/testdata/rbac demonstrating the policy for common statements

Layer 1: Verified kernel

  • Adds src/sql/src/rbac/kernel.rs with integer-encoded implementations of role membership closure
  • Implements membership_closure() as a fixpoint over machine words (bounded model checking friendly)
  • Provides encode_membership() to convert catalog role graphs to the integer encoding
  • Includes membership_closure_reference() as a set-based reference implementation
  • Adds property-based tests verifying the encoded kernel matches the reference implementation on arbitrary graphs, including cyclic ones (property P4)

Key design decisions:

  • The kernel uses single-word (u64) bitmasks to represent role sets, enabling bounded model checking with a practical bound (MAX_ROLES = 64). Graphs larger than this are reported as unanalyzable rather than truncated, preserving safety.
  • The policy description type is deliberately separate from the internal representation, so the interface is stable even as implementation details change.
  • The datadriven test format makes policy changes visible as diffs in the same PR that causes them, enabling review of the policy as a policy rather than as code.
  • Property-based testing of the kernel against the reference implementation catches divergences automatically.

Verification

  • New property-based tests in src/sql/src/rbac/kernel.rs verify closure correctness on arbitrary membership graphs
  • New datadriven tests in src/adapter/tests/rbac.rs render the policy table for representative statements
  • All tests pass with cargo test -p mz-sql and cargo test -p mz-adapter --test rbac
  • The kernel implementation is total on all inputs (including cyclic graphs) and terminates by construction

claude added 4 commits August 12, 2026 15:00
Records what "correctness" can mean for the RBAC layer, formalizes the
authorization judgment as currently implemented, and proposes a ladder of
verification techniques ordered by value per unit cost.

The judgment is stated precisely (membership closure, effective privilege,
and the four validation obligations) so that candidate properties can be
written down rather than argued informally. Fourteen properties are
enumerated and classified by the kind of tool that can discharge them:
properties of the decision procedure alone, agreement between the several
independent implementations of the privilege predicate, and properties that
no proposed technique reaches.

The proposal deliberately puts writing the specification down and making
the authorization chokepoint a compile-time property ahead of any model
checking, on the grounds that the failure modes we actually have (an
unconsidered plan arm, a call site that skips the check, two copies of a
predicate drifting apart) are invisible to a model disconnected from the
policy table. Residual unverified risk is stated explicitly, concentrated
in read-set soundness for resolved_ids and in policy intent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DFkbBGJizQwV8Y9n4iiVx
The shape of the code, not its logic, is what blocks every verification
technique the previous revision proposed. check_plan is generic over
`impl SessionCatalog` (68 methods, returning trait objects) and uses 18 of
those methods, eight of which serve only to format error messages. Records
a three-way split into gather, decide, and diagnose that leaves a
monomorphic data-only core, along with the fail-closed requirement that
keeps an incomplete fact gather from becoming a bypass rather than a
spurious denial. Adds three further refactors: policy arms built from
combinators so the uniformity properties hold by construction, a single
role membership implementation shared with the SQL-visible function, and
the Authorized<Plan> typestate.

Adds a tool selection section comparing proptest, Kani, Verus, Creusot, and
Aeneas with Lean across guarantee strength, annotation cost, and what each
demands of the code. Aeneas cannot extract generic functions with trait
bounds and needs concrete monomorphised types, so it does not apply to the
current code at all, which reframes the choice as the refactor first and
the tool second. Notes the one property where a Lean formalization buys
something the Rust-native tools cannot express, namely equivalence between
the Rust decision and the SQL privilege functions, and recommends the
cheaper differential test until that equivalence proves hard to maintain by
hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DFkbBGJizQwV8Y9n4iiVx
Reading Kani 0.67.0's own performance and bounded-arbitrary tests answers
the open question the previous revision left for a spike, and the answer
rules out the naive approach. Inserting one nondeterministic u32 into a
BTreeSet is itself a performance test upstream, at roughly 10 seconds and
255 MB, and the bounded-arbitrary harness for BTreeMap uses a bound of one
because larger bounds take too long. A nondeterministic role graph is a
nested map of sets, so flat sorted vectors are not a sufficient hedge
either.

Records the workable division instead: give Kani an integer-encoded view,
with the role graph as adjacency bitmasks so closure is a fixpoint over
integers and provable at 64 roles rather than 2, and check the encoder with
proptest. AclMode is already a bitflags integer, which makes the algebra
proofs the place to start.

Three further constraints, all cheap to satisfy when known in advance:
autoharness needs arguments implementing Arbitrary and so cannot apply
before the gather/decide/diagnose split, loop contracts do not support
`while let` loops and both worklist traversals we care about are written
that way, and contracts, loop contracts, quantifiers, and autoharness are
all experimental. Also notes that Kani rewrites debug_assert! into assert!,
which cuts against the profile-dependent behavior we rely on elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DFkbBGJizQwV8Y9n4iiVx
Starts the refactor the design doc argues for, in the order it argues for:
make the policy inspectable before changing it, and check the properties
that need no catalog before building anything that does.

Adds RbacRequirementsDescription and describe_rbac_requirements, an owned
view of what a statement requires. It is deliberately a separate type from
the internal RbacRequirements so the internal representation stays free to
change, and deliberately unfiltered, so it reports what the statement
requires rather than what a particular session would face.

Adds property tests for P3, relaxation soundness: dropping to mandatory
requirements must weaken and never strengthen. Every superuser session and
every RBAC-disabled session takes that path, so an inversion there would be
broadly exploitable and invisible on inspection. The AbstractState the
property evaluates against is the seed of the executable specification the
design doc calls for, and evaluates the membership, ownership, and
privilege obligations without a catalog.

Adds an integer-encoded membership closure kernel. Kani's own test suite
shows bounded model checking cannot reach a BTreeMap of BTreeSets at any
useful bound, so the kernel is a fixpoint over adjacency bitmasks, checked
against a set-based reference by proptest. The reference avoids `while let`
because Kani's loop contracts do not support it.

Adds a datadriven golden dump of the policy table over the debug catalog,
so the policy can be reviewed as a policy and a change to it shows up as a
diff in the pull request that causes it.

Validation so far: cargo check -p mz-sql --lib passes. The property tests
and the golden dump have not been run yet, and the golden file is still
empty pending a rewrite pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DFkbBGJizQwV8Y9n4iiVx
@jasonhernandez
jasonhernandez requested a review from a team as a code owner August 13, 2026 06:09
@jasonhernandez
jasonhernandez marked this pull request as draft August 13, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants