Formal methods for SQL RBAC: design and kernel implementation - #38186
Draft
jasonhernandez wants to merge 4 commits into
Draft
Formal methods for SQL RBAC: design and kernel implementation#38186jasonhernandez wants to merge 4 commits into
jasonhernandez wants to merge 4 commits into
Conversation
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
marked this pull request as draft
August 13, 2026 06:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EXPERIMENTAL
Motivation
The RBAC implementation in
src/sql/src/rbac.rsis 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
RbacRequirementsDescription, a public owned type that describes what a plan requires, separate from the internalRbacRequirementsrepresentationdescribe_rbac_requirements()to dump the policy table as data, enabling review and assertionsrc/adapter/tests/rbac.rs) that renders requirements in human-readable formsrc/adapter/tests/testdata/rbacdemonstrating the policy for common statementsLayer 1: Verified kernel
src/sql/src/rbac/kernel.rswith integer-encoded implementations of role membership closuremembership_closure()as a fixpoint over machine words (bounded model checking friendly)encode_membership()to convert catalog role graphs to the integer encodingmembership_closure_reference()as a set-based reference implementationKey design decisions:
Verification
src/sql/src/rbac/kernel.rsverify closure correctness on arbitrary membership graphssrc/adapter/tests/rbac.rsrender the policy table for representative statementscargo test -p mz-sqlandcargo test -p mz-adapter --test rbac