CVE-2026-45529 AI-generated analysis
platform/packages/providers/TelephonyProvider, MmsSmsProvider (content://mms-sms)
MmsSmsProvider.query() now restricts the simple-conversations, conversation recipients/subject, search and searchSuggest URI paths to messages whose sub_id belongs to the calling user's subscriptions, returning an empty cursor when the user has no associated subscriptions.
Root cause
In src/com/android/providers/telephony/MmsSmsProvider.java, query() already computes a per-user subscription selection string (selectionBySubIds, derived from SubscriptionManager.getSubscriptionInfoListAssociatedWithUser per the test mock), but five URI cases ignored it: URI_CONVERSATIONS with simple=true (getSimpleConversations), URI_CONVERSATIONS_RECIPIENTS and URI_CONVERSATIONS_SUBJECT (getConversationById), URI_SEARCH_SUGGEST (raw SEARCH_QUERY on the words table) and URI_SEARCH (getTextSearchQuery over sms/pdu joined with words). These paths therefore returned conversation, recipient, subject and full-text search data for messages belonging to subscriptions not associated with the calling user, and returned data even when the user had no associated subscriptions at all.
How the fix works
- MmsSmsProvider.java:query(), URI_CONVERSATIONS simple branch: if selectionBySubIds == null, log and return emptyCursor; otherwise selection = DatabaseUtils.concatenateWhere(selection, selectionBySubIds) before calling getSimpleConversations().
- MmsSmsProvider.java:query(), URI_CONVERSATIONS_RECIPIENTS and URI_CONVERSATIONS_SUBJECT: same null check returning emptyCursor, then concatenateWhere(selection, selectionBySubIds) before getConversationById().
- MmsSmsProvider.java:SEARCH_QUERY constant: the trailing " ORDER BY snippet LIMIT 50;" is removed so a clause can be appended. In URI_SEARCH_SUGGEST: null check returning emptyCursor, then the raw query becomes SEARCH_QUERY + " AND " + selectionBySubIds.replace("'", "") + " ORDER BY snippet LIMIT 50;" passed to db.rawQuery with SEARCH_STRING.
- MmsSmsProvider.java:getTextSearchQuery(): new third parameter selectionBySubIds. When non-empty, an smsSubIdClause (" AND " + smsTable + "." + selectionBySubIds with quotes stripped) is inserted into the sms sub-select and an mmsSubIdClause (same with pduTable prefix) into the mms/part sub-select, before the words.table_to_use condition.
- MmsSmsProvider.java:query(), URI_SEARCH: null check returning emptyCursor (placed after the existing validation that throws for unsupported queries), then getTextSearchQuery(smsTable, pduTable, selectionBySubIds) is used for db.rawQuery.
- tests/.../MmsSmsProviderTest.java: adds testQuery_subIdEmpty_returnsEmptyCursor (mocks getSubscriptionInfoListAssociatedWithUser to return an empty list and asserts count 0 for the five URIs) and testQuery_subIdValid_doesNotReturnEmptyCursor (asserts a non-null cursor with the default sub mocks).
Completeness
- Covers exactly five URI cases: URI_CONVERSATIONS (simple=true only), URI_CONVERSATIONS_RECIPIENTS, URI_CONVERSATIONS_SUBJECT, URI_SEARCH_SUGGEST, URI_SEARCH. The non-simple else branch of URI_CONVERSATIONS and all other URI cases are not touched by this diff; the material does not show whether those already consumed selectionBySubIds, so no gap can be asserted from the code.
- The raw-SQL paths (URI_SEARCH_SUGGEST, getTextSearchQuery) strip single quotes from selectionBySubIds before embedding it, while the concatenateWhere paths pass it unchanged; the filter relies entirely on the correctness of the pre-existing selectionBySubIds string.
- The null-check branches are enforced regardless of caller; no exemption for privileged or system callers is visible in the diff.
- Test coverage checks only the empty-subscription short-circuit; the positive test does not verify that cross-subscription rows are actually excluded.
Behavior changes
- For the five patched URI paths, a caller whose user has no associated subscriptions now receives an empty (non-null) cursor instead of data, with a Log.d message.
- Results of those paths are additionally constrained by the sub_id clause, so messages/conversations tied to subscriptions not associated with the calling user no longer appear.
- SEARCH_QUERY no longer carries ORDER BY/LIMIT itself; the suggest query is now assembled at runtime (same final ordering and LIMIT 50).
- Private getTextSearchQuery signature gains a selectionBySubIds parameter (no external API impact).
Per major
- Android 14: same fix: commit 475853ddf6b3 in android-security-14.0.0_r29, 47-line provider change plus tests, same change content as 15/16 (different blob hashes and line offsets).
- Android 15: same fix: commit 4fab244beaa3 in android-security-15.0.0_r17; same change content as 14, and byte-identical diff to 16 (same pre/post blob hashes dd9f07cf..0fa97e1f).
- Android 16: same fix: commit 190c0a3b8e44 in android-security-16.0.0_r8, byte-identical diff to 15.
- Android 17: adapted fix: commit 1fab99e951d5 in android-security-17.0.0_r1; getTextSearchQuery already takes canReadRestrictedMessages and otpFilter, so selectionBySubIds is added as a fifth parameter and the sub_id clauses are appended after the read-restriction (and, for sms, OTP) clauses; the URI_SEARCH_SUGGEST check is placed after the existing otpFilter early-return; new tests carry @DisableFlags(Flags.FLAG_SECURE_ACCESS_TO_RESTRICTED_RCS_MESSAGES).
Unknowns
- How selectionBySubIds is built (exact SQL form, column name, quoting) and whether it is null only when the subscription list is empty; the diff only shows its use.
- Whether the words table and the tables queried by getConversationById/getSimpleConversations carry the column referenced by selectionBySubIds; the material shows no schema.
- Which other URI cases in query() already applied selectionBySubIds before this change.
- The specific reporter scenario and the user/subscription configuration used in the manual test (b/510423582).