CVE-2025-48548 AI-generated analysis
platform/frameworks/av (audiopolicy service, media/utils ServiceUtilities) and platform/frameworks/base (PermissionManagerService AppOps checks, SystemUI AppOpsControllerImpl)
Makes audioserver's silencing of a recording client match the AppOps op state. startRecording now returns the exact permission result (granted/soft/hard denied). startInput un-silences a client only when the op was granted. finishRecording is called only for clients that were not silenced. Sources that do not need the record op are checked against their own op instead of having their result ignored. Companion frameworks/base changes fix AppOps attribution-chain preflight and cleanup, and the SystemUI mic indicator suppression logic.
Root cause
In media/utils/ServiceUtilities.cpp, checkRecordingInternal reduced the PermissionChecker result to a bool that was true for anything other than PERMISSION_HARD_DENIED, so a soft-denied result looked like success. AudioPolicyService::startInput (AudioPolicyInterfaceImpl.cpp) then always called setAppState_l(client, APP_STATE_TOP), forcing the client un-silenced whatever the real op state was. stopInput and the startInput failure path always called finishRecording, even for silenced clients, so op start/finish counts did not match. startInput also ignored a failing startRecording result for AUDIO_SOURCE_FM_TUNER, REMOTE_SUBMIX and ECHO_REFERENCE through an OR condition. In setAppState_l, a failed startRecording only set a local silenced flag and the function carried on. In AudioRecordClient.cpp, OpRecordAudioMonitor also monitored OP_RECORD_AUDIO whenever its op was different, even for sources that do not need it. A failed target SDK lookup returned -1, which was treated as pre-U, so WATCH_FOREGROUND_CHANGES was not used. On the frameworks/base side, the attribution-chain loop in PermissionManagerService did not finish proxy ops it had already started when a later link failed during startDataDelivery. The preflight path in performOpTransaction checked the first chain entry (the datasource, e.g. audioserver), so preflight could fail where delivery would succeed. SystemUI AppOpsControllerImpl suppressed the microphone indicator if any recording for a UID was paused, rather than only when all of them were.
How the fix works
- av c962764b8234 (preparatory refactor): moves OpRecordAudioMonitor and AudioRecordClient out of AudioPolicyService.h/.cpp into new services/audiopolicy/service/AudioRecordClient.h/.cpp (namespace android::media::audiopolicy). It adds the new file to Android.bp and makes AudioCommandThread and AudioClient public so the new classes can reference them. The code is moved, not changed.
- av fa795ebf0e65, media/utils/ServiceUtilities.cpp: getOpForSource now returns OP_NONE for AUDIO_SOURCE_FM_TUNER. A new isRecordOpRequired(source) returns false for FM_TUNER, ECHO_REFERENCE, REMOTE_SUBMIX and VOICE_DOWNLINK. checkRecordingInternal now returns an int permission result. For record-op sources on start, it first calls checkPermissionForPreflightFromDatasource. Only if that returns GRANTED does it call checkPermissionForStartDataDeliveryFromDatasource; otherwise it calls checkPermissionForDataDeliveryFromDatasource and discards the result. For non-record-op sources it calls AppOpsManager::startOpNoThrow on the attributed op directly, and returns GRANTED if the op is OP_NONE. finishRecording mirrors this: finishDataDeliveryFromDatasource for record-op sources, AppOpsManager::finishOp for the others. startRecording now returns int; recordingAllowed keeps bool semantics (!= HARD_DENIED).
- av fa795ebf0e65, AudioPolicyInterfaceImpl.cpp AudioPolicyService::startInput: removes the FM_TUNER/REMOTE_SUBMIX/ECHO_REFERENCE OR condition and returns PERMISSION_DENIED only when permitted == PERMISSION_HARD_DENIED. It calls setAppState_l(client, APP_STATE_TOP) only on PERMISSION_GRANTED and APP_STATE_IDLE otherwise. In the error path it calls finishRecording only if !client->silenced. AudioPolicyService::stopInput: calls finishRecording only if !client->silenced.
- av fa795ebf0e65, AudioPolicyService.cpp AudioPolicyService::setAppState_l: when startRecording != PERMISSION_GRANTED, the function returns instead of setting silenced = true and continuing.
- av fa795ebf0e65, AudioRecordClient.cpp/.h: OpRecordAudioMonitor gains a const bool mShouldMonitorRecord, set from isRecordOpRequired and passed in by createIfNeeded. onFirstRef registers for OP_RECORD_AUDIO, and checkOp also requires OP_RECORD_AUDIO, only when mShouldMonitorRecord is set. When the package manager call fails, getTargetSdkForPackageName now returns __ANDROID_API_FUTURE__ instead of -1, so WATCH_FOREGROUND_CHANGES is used.
- base bbd8db54b3c9, PermissionManagerService.java attribution-chain loop: tracks prev. When startDataDelivery is set, opMode != MODE_ALLOWED and prev != null, it truncates the chain after prev.next and calls finishDataDelivery.
- base 2c18e5f08282, PermissionManagerService.performOpTransaction (preflight, !forDataDelivery): when skipProxyOperation && !singleReceiverFromDatasource, it uses MODE_ALLOWED for the first chain entry instead of unsafeCheckOpRawNoThrow. This matches the data-delivery path. Per the commit message, it avoids preflight failing where delivery would succeed, e.g. for audioserver.
- base 00344da68fce (Android 15 commit), packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java: isAnyRecordingPausedLocked is replaced by isAllRecordingPausedLocked. The mic indicator item is disabled only if mMicMuted is set or every AudioRecordingConfiguration for the UID reports isClientSilenced(). Tests are updated in AppOpsControllerTest.java.
Completeness
- Covers startInput and stopInput in AudioPolicyInterfaceImpl.cpp and the un-silence path in setAppState_l. finishRecording is gated on client->silenced in the startInput error path and in stopInput; setAppState_l returns early when startRecording is not GRANTED.
- All audio_source_t values go through getOpForSource/isRecordOpRequired, so the old OR condition in startInput is gone. FM_TUNER now maps to OP_NONE, so no op is checked for it.
- The code comment calls the preflight-then-start sequence in checkRecordingInternal 'technically racy' and a workaround for the AppOps paused-op behavior (TODO b/294609684). This material does not change the AppOps pause state itself.
- The frameworks/base chain cleanup runs only when prev != null.
- The SystemUI indicator change is shown only as an Android 15 commit. The material does not show whether an equivalent change was applied to Android 14.
Behavior changes
- startInput still returns PERMISSION_DENIED on hard denial. On a non-granted, non-hard-denied result, the input now starts silenced (APP_STATE_IDLE) instead of being forced un-silenced.
- REMOTE_SUBMIX and ECHO_REFERENCE recordings now go through AppOpsManager::startOpNoThrow/finishOp for OP_RECORD_AUDIO_OUTPUT. VOICE_DOWNLINK uses OP_RECORD_INCOMING_PHONE_AUDIO without also requiring OP_RECORD_AUDIO. FM_TUNER has no op (OP_NONE).
- When a start is restricted, a data-delivery (note) check is issued instead of a start, so op-rejected listeners still fire (per the code comment).
- finishRecording is skipped for silenced clients.
- If the target SDK lookup fails, the package is treated as targeting a future SDK and WATCH_FOREGROUND_CHANGES is applied.
- PermissionManagerService preflight no longer checks the op on the first chain entry when skipProxyOperation is set and the source is not singleReceiverFromDatasource. A failing later link during startDataDelivery now finishes the proxy op started for the previous link.
- SystemUI shows the microphone indicator unless all recordings for the UID are silenced or the mic is muted (Android 15 commit).
Per major
- Android 13: Listed as affected by the bulletin. The material contains no commit or code for Android 13.
- Android 14: Fixed at android-security-14.0.0_r22 by RESTRICT AUTOMERGE cherry-picks: av c962764b8234 (refactor), av fa795ebf0e65 (core fix), base 2c18e5f08282 (preflight skip) and base bbd8db54b3c9 (chain cleanup). At android-security-14.0.0_r29 the patched region is fully present (8/8 lines) and the code context matches the post-fix state.
- Android 15: Resolved at android-security-15.0.0_r10. The Android 15 commit shown is base 00344da68fce (SystemUI AppOpsControllerImpl isAllRecordingPausedLocked). At android-security-15.0.0_r17, 6/8 lines of the patched region are present. The material does not show whether the av or PermissionManagerService commits were applied to 15.
- Android 16: Not affected (not listed by the bulletin). At android-security-16.0.0_r8 the patched region is present (8/8 lines).
- Android 17: Not affected (not listed by the bulletin). At android-security-17.0.0_r1 the patched region is present (8/8 lines).
Unknowns
- Google's description of the exact trigger and impact is not in the material. The commit message only says the AppOps op state and audioserver's silenced state could diverge.
- An intermediate frameworks/av commit between c962764b8234 and fa795ebf0e65 (adding the reg()/check() lambdas in OpRecordAudioMonitor) is implied by the diff context but not provided.
- The material does not show whether the av and PermissionManagerService commits were applied to Android 15, or whether the SystemUI change was applied to Android 14.
- The material does not show whether a single-link failure in the chain can leave a started op behind.
- Why the bulletin lists '16-next' while the per-major facts mark Android 16 as not listed is not explained.