CVE-2026-45518 AI-generated analysis
platform/frameworks/base (core/java/android/database/sqlite/SQLiteTokenizer) and platform/packages/providers/ContactsProvider (com.android.providers.contacts.util.SelectionBuilder)
Adds an OPTION_CHECK_BRACKETS mode to android.database.sqlite.SQLiteTokenizer that rejects SQL fragments with unbalanced parentheses, and makes ContactsProvider's SelectionBuilder.addClause() validate every clause with it so a caller-supplied selection cannot close the parentheses that SelectionBuilder wraps around each clause.
Root cause
SelectionBuilder concatenates clauses as "(clause1) AND (clause2) ..." (see SelectionBuilderTest expectations such as "(A) AND (B)"). The base selection passed to the constructor is, per its javadoc, "typically the user supplied selection arg". addClause() only checked TextUtils.isEmpty() and stored the clause verbatim, so a selection containing unbalanced parentheses (for example the test input "1=1)) OR ((1=1") could break out of the wrapping parentheses and change how it combines with the other clauses. SQLiteTokenizer had no facility to detect this, and it also contained a bug where `options &= OPTION_TOKEN_ONLY` (compound assignment) cleared all other option bits when that check was reached.
How the fix works
- 1. platform/frameworks/base 9183bfe6574b, core/java/android/database/sqlite/SQLiteTokenizer.java: adds `public static final int OPTION_CHECK_BRACKETS = 1 << 1`.
- 2. SQLiteTokenizer.tokenize(String, int, Consumer<String>): the `checker` parameter becomes @Nullable and the three `checker.accept(token)` call sites (the identifier path, the quote-handling path using tokenUnquoted, and the quoted-token path between quoteStart/quoteEnd) are guarded with `if (checker != null)`, so the method can be used purely as a validator.
- 3. SQLiteTokenizer.tokenize: both `if ((options &= OPTION_TOKEN_ONLY) != 0)` tests are corrected to `(options & OPTION_TOKEN_ONLY)`. Without this, reaching those checks would strip OPTION_CHECK_BRACKETS from `options` and the new check would not fire.
- 4. SQLiteTokenizer.tokenize: a new `int level` counter is maintained in the generic-character path (after the semicolon check; parentheses inside comments are not counted, as testCheckBracketsInComments verifies). '(' increments level; ')' decrements it and, if level drops below 0 and OPTION_CHECK_BRACKETS is set, throws genException("Unbalanced brackets", sql). After the loop, if level != 0 and the option is set, the same exception is thrown. This rejects both premature closes and unclosed opens.
- 5. platform/packages/providers/ContactsProvider 3f606a02d7e6, src/com/android/providers/contacts/util/SelectionBuilder.java: SelectionBuilder.addClause(String) now calls `SQLiteTokenizer.tokenize(clause, SQLiteTokenizer.OPTION_CHECK_BRACKETS, null)` before adding a non-empty clause to mWhereClauses. Because the constructor SelectionBuilder(String baseSelection) delegates to addClause(), the base selection is validated too. An IllegalArgumentException is thrown for unbalanced input.
- 6. Tests: SQLiteTokenizerTest.testCheckBrackets / testCheckBracketsInComments and SelectionBuilderTest.testInjection cover balanced input, missing close, extra close, the "1=1)) OR ((1=1" pattern, and parentheses inside comments.
Completeness
- Covers every clause that goes through SelectionBuilder.addClause(), including the constructor's base selection; the actual ContactsProvider call sites are not shown in the material.
- The tokenizer check is opt-in (OPTION_CHECK_BRACKETS); existing SQLiteTokenizer callers that do not pass the flag get no new validation. Only ContactsProvider's SelectionBuilder is changed to pass it in the material shown.
- The check only enforces parenthesis balance. Any balanced SQL fragment is still accepted; the fix relies on the existing wrapping parentheses plus balance to keep the caller's clause self-contained. It does not validate tokens or grammar (OPTION_TOKEN_ONLY is not combined with it).
- Parentheses inside comments are not counted (tested), avoiding false rejections.
- The `&=` to `&` correction also fixes a latent option-handling bug for any future multi-flag use of SQLiteTokenizer.
Behavior changes
- SelectionBuilder(String) and SelectionBuilder.addClause(String) now throw IllegalArgumentException ("Unbalanced brackets in '...'") for clauses with unmatched parentheses; callers passing such selections into SelectionBuilder will now get this exception.
- SQLiteTokenizer.tokenize(String, int, Consumer<String>) accepts a null checker.
- New public constant SQLiteTokenizer.OPTION_CHECK_BRACKETS; passing it causes IllegalArgumentException on unbalanced parentheses outside comments and quotes.
- Option bits are no longer destroyed mid-tokenization (the `&=` bug); no observable change for callers that only use OPTION_NONE or OPTION_TOKEN_ONLY.
Per major
- Android 14: same fix: platform/frameworks/base 9183bfe6574b (SQLiteTokenizer OPTION_CHECK_BRACKETS) and platform/packages/providers/ContactsProvider 3f606a02d7e6 (SelectionBuilder.addClause check) in android-security-14.0.0_r29, both MendIt cherry-picks; resolver reports 7/8 lines of the patched region present.
- Android 15: same fix: platform/packages/providers/ContactsProvider b8e00892122c in android-security-15.0.0_r17, identical SelectionBuilder diff (test file has different blob ids); 8/8 lines present. The matching frameworks/base SQLiteTokenizer commit is not shown in the material but is required for the ContactsProvider change to compile.
- Android 16: same fix: platform/packages/providers/ContactsProvider 3c11261b2a6b in android-security-16.0.0_r8, identical SelectionBuilder diff; 8/8 lines present. frameworks/base counterpart not shown in the material.
- Android 17: same fix: platform/frameworks/base 5a73367d5b3a in android-security-17.0.0_r1, identical SQLiteTokenizer diff (test file has different blob ids); 7/8 lines present. The ContactsProvider counterpart is not shown in the material.
Unknowns
- Which ContactsProvider query/update/delete paths pass caller-controlled selections into SelectionBuilder, and which appended clauses they protect, are not shown.
- The frameworks/base commits for Android 15 and 16 and the ContactsProvider commit for Android 17 are not included in the material, though the per-major facts mark all four majors as resolved.
- Why the resolver reports only 7/8 lines of the patched region on Android 14 and 17 is not shown.
- Whether any other SQLiteTokenizer users elsewhere in the platform were also switched to OPTION_CHECK_BRACKETS is not shown.