Repository navigation
fix(eql): schema-qualify operators in comparison wrapper bodies - #1551
Conversation
The eql_v3 comparison wrappers (eql_v3.lt, eq, ...) are unpinned LANGUAGE sql functions so they can inline into functional-index scans, which means PostgreSQL parses their bodies under the caller's search_path. The _ord_ore wrappers compare eql_v3_internal.ore_block_256, a composite type whose comparison operators are created in public. With public off the path (a SECURITY DEFINER helper with SET search_path = '', or a hardened role path) the bare < in the body resolved to pg_catalog's built-in record comparison instead, ordering the ciphertext bytes with no error. The generator now names every wrapper operator with its schema, taken from a new Term::operator_schema (public for ORE, pg_catalog for the HMAC, bloom and OPE terms), and the bloom guard qualifies cardinality too. Inlining and index engagement are unchanged: the inlined expression carries the same operator either way. The four hand-written json_entry ordering wrappers get the same treatment. Tests: a codegen test checks every generated wrapper body across the catalog uses no bare operator and names the right schema per term, and a new SQLx suite runs every ORE-carrying domain's wrappers and OPERATOR(public.<op>) forms under search_path = '' against the ORE comparator, on pairs where it disagrees with byte order (red on the previous installer, green now). Refs: CIP-5153 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jmn7p5byuYMWqoxDUAZQE
🦋 Changeset detectedLatest commit: 63da8b3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This comment has been minimized.
This comment has been minimized.
test:public_identifiers forbids private issue identifiers in tracked files; describe the behaviour directly instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jmn7p5byuYMWqoxDUAZQE
cipherstash-agent
left a comment
There was a problem hiding this comment.
Review attestation
Reviewed 63da8b3 · posted from Claude Code · claude-sonnet-5-5 · effort medium
Sources: code-review (medium) · claude-sonnet-5-5
Skills: code-review, review-cross-check, review-post
No findings.
Scope read: eql-codegen (context.rs, generate.rs, wrapper template), eql-domains/src/term.rs, json/operators.sql, the new SQLx suite, changeset and skill edit. The regenerated v3/scalars/* SQL was spot-checked via the generator tests, not line by line. No existing review threads; the one failed Blacksmith check (private-identifier scan) was on an earlier run and ci-required later passed.
Generated by Claude Code
Requested by CipherStash · project thread
Summary
Fixes CIP-5153. The
_ord_orecomparison functions (eql_v3.lt,lte,gt,gte,eq,neq) gave silently wrong answers whenpublicwas not on the caller'ssearch_path.The wrappers are unpinned
LANGUAGE sqlso they inline into functional-index scans, which means PostgreSQL parses their bodies under the caller'ssearch_path. The ORE wrappers compareeql_v3_internal.ore_block_256, a composite type whose operators are created inpublic. Withoutpublicon the path (aSECURITY DEFINERhelper withSET search_path = '', as Supabase recommends, or a hardened role path), the bare<resolved to pg_catalog's built-in record comparison, ordering the ciphertext bytes with no error. Reproduced on PostgreSQL 16: a malformed pair raises "Ciphertexts are different lengths" on the default path and returnstundersearch_path = ''.Changes
eql-domains: newTerm::operator_schema()(publicfor ORE,pg_catalogfor HMAC, bloom and OPE) andTerm::term_for_operator();extractor_for_operatornow delegates to it.eql-codegen:wrapper_entrytakes the servingTermand emitsOPERATOR(<schema>.<op>); the bloom empty-needle guard qualifiescardinalityand its operators too. All generated wrapper SQL undersrc/v3/scalars/is regenerated (body lines only).src/v3/json/operators.sql: the four hand-written json_entry ordering wrappers nameOPERATOR(pg_catalog.<op>).skills/stash-postgres: a new trap entry on querying encrypted columns whenpublicis off thesearch_path(a barecol < $1still falls back to native jsonb there; use the function form orOPERATOR(public.<)).@cipherstash/eqlpatch,stashpatch (skill).Verification
cargo test -p eql-domains -p eql-codegen: green apart from two tests that fail when run as root (generate_type_failure_does_not_delete_before_writing,write_failure_preserves_existing_target_and_leaves_no_temp); they fail the same way onmainin this environment.every_wrapper_body_schema_qualifies_its_operatorschecks every generated wrapper body across the catalog: no bare operator token, and the right schema per term (literal, so a wrongoperator_schemamapping fails it; confirmed by flipping ORE topg_catalog).v3_search_path_independence_tests: every ORE-carrying domain's wrappers, query-twin overloads andOPERATOR(public.<op>)forms undersearch_path = '', checked againstcompare_ore_block_256_termson pairs where ORE order and byte order disagree; plus aSET search_path = ''SECURITY DEFINERhelper on a malformed pair. Red on the published 3.0.6 installer, green on this branch (local PostgreSQL 16).lint_tests,v3_public_surface_tests,v3_operator_equivalents_tests,v3_jsonb_operator_surface_tests,v3_ore_fallback_tests,build_validation_tests,payload_schema_tests. Suites needing ZeroKMS-generated fixtures were not run locally (no credentials); CI runs them.EXPLAINonc < queryand oneql_v3.lt(c, query)undersearch_path = ''both use theeql_v3.ord_term_ore(c)btree index.cargo fmt --check,cargo clippyclean.Related
generate.rsand the generatedquery_*SQL, so whichever lands second needs a rebase and acargo run -p eql-codegenregenerate.Review notes
col < qwithpublicoff the path still resolves to native jsonb. That is the cross-family fall-through tracked in CIP-5155, out of scope here; the changeset and skill say so.packages/eql/packages/eql/sql,crates/eql-bindings/sql) are left alone; the release version hook rebuilds them when this changeset bumps EQL.Generated by Claude Code
Summary by CodeRabbit
publicis absent from PostgreSQL’ssearch_path, preventing incorrect results from unintended built-in comparisons.publicin the search path.publicin the search path. EQL must be reinstalled or upgraded to use the fix.