CVE-2024-0052 AI-generated analysis
platform/packages/modules/HealthFitness (Health Connect service), change logs / ExerciseSessionRecord route storage
Makes the Health Connect change-log read path apply the same exercise-route access rules as the normal read path. ExerciseSessionRecord routes returned through getChangeLogs are now limited to the caller's own sessions unless the caller holds READ_EXERCISE_ROUTE.
Root cause
HealthConnectServiceImpl.getChangeLogs read the upserted records through ReadTransactionRequest(Map<Integer, List<UUID>>, long startDateAccess). That constructor called RecordHelper.getReadTableRequest(uuids, startDateAccess), which called ExerciseSessionRecordHelper.getExtraDataReadRequests(uuids, startDateAccess). The override only checked isExerciseRouteFeatureEnabled() and built the route read request with a WHERE clause on UUID and start time. It never received the caller package name or the READ_EXERCISE_ROUTE grant state, so it did not restrict routes to sessions owned by the caller. The readRecords path (getExtraDataReadRequests(request, packageName, startDateAccess, extraPermsState)) already enforced self-read when READ_EXERCISE_ROUTE was not granted. As a result, change-log responses could include exercise routes written by other apps.
How the fix works
- service/java/com/android/server/healthconnect/HealthConnectServiceImpl.java:getChangeLogs: gets callerPackageName (Objects.requireNonNull(attributionSource.getPackageName())) and computes recordTypeToInsertedUuids first. It then calls mDataPermissionEnforcer.collectExtraReadPermissionToStateMapping(recordTypeToInsertedUuids.keySet(), attributionSource) to get the extra read permission grant state, and passes callerPackageName and that map into the new ReadTransactionRequest constructor.
- service/java/com/android/server/healthconnect/permission/DataPermissionEnforcer.java:collectExtraReadPermissionToStateMapping: the parameter changes from a single int recordTypeId to Set<Integer> recordTypeIds. The method collects the distinct extra read permissions of all involved record helpers and maps each one to isPermissionGranted(). The readRecords call site in HealthConnectServiceImpl is adapted to pass Set.of(request.getRecordType()).
- service/java/com/android/server/healthconnect/storage/request/ReadTransactionRequest.java: the UUID-map constructor now takes (String packageName, Map<Integer, List<UUID>> recordTypeToUuids, long startDateAccess, Map<String, Boolean> extraPermsState) and forwards packageName and extraPermsState to RecordHelper.getReadTableRequest.
- service/java/com/android/server/healthconnect/storage/datatypehelpers/RecordHelper.java: getReadTableRequest(packageName, uuids, startDateAccess, extraPermsState) and getExtraDataReadRequests(packageName, uuids, startDateAccess, extraPermsState) both gain the packageName and extraPermsState parameters. The base getExtraDataReadRequests still returns an empty list.
- service/java/com/android/server/healthconnect/storage/datatypehelpers/ExerciseSessionRecordHelper.java: adds getExerciseRouteReadAccessType(packageName, extraPermsState). It returns ROUTE_READ_ACCESS_TYPE_NONE when the feature is disabled, or when READ_EXERCISE_ROUTE is not granted and the package has no app-info id. It returns ROUTE_READ_ACCESS_TYPE_ALL when READ_EXERCISE_ROUTE is granted (looked up with getOrDefault(..., false)), and ROUTE_READ_ACCESS_TYPE_OWN otherwise. The change-log override getExtraDataReadRequests(packageName, uuids, ...) returns no route request for NONE. For OWN it adds addWhereInLongsClause(APP_INFO_ID_COLUMN_NAME, List.of(appId)), so only routes of sessions written by the caller are read. The readRecords override is refactored to use the same helper, with enforceSelfRead = (access type == OWN).
- service/java/com/android/server/healthconnect/storage/datatypehelpers/AppInfoHelper.java:getAppInfoId: returns DEFAULT_LONG for a null packageName, so a null package resolves to no route access.
- framework/java/android/health/connect/datatypes/ExerciseSessionRecord.java:toRecordInternal: now calls recordInternal.setHasRoute(hasRoute()), so the has_route flag is carried into the internal record whether or not the route object is present.
- Tests: ExerciseRouteAccessTest adds CTS cases for readRecords, using both filters and ids: own route, another app's route without and with READ_EXERCISE_ROUTE, and mixed sessions. For getChangeLogs it adds cases for the caller's own route and for another app's route with READ_EXERCISE_ROUTE. TestUtils adds READ_EXERCISE_ROUTE_PERMISSION and truncates the start time to millis. The RecentAccessViewModelTest and HealthConnectUiTestHelper changes are unrelated test adjustments bundled in the cherry-pick.
Completeness
- Covers the change-log read path for ExerciseSessionRecord routes, which previously had no ownership or READ_EXERCISE_ROUTE check. The readRecords path already enforced these rules and is only refactored to share getExerciseRouteReadAccessType.
- For the OWN case the route restriction is applied in SQL (APP_INFO_ID_COLUMN_NAME IN (callerAppId)). Other apps' sessions still appear in the change log, but without route data. The readRecords CTS tests assert that hasRoute stays true with a null route for other apps' sessions. No change-log test asserts this for the no-permission case.
- In the material, only ExerciseSessionRecordHelper overrides getExtraDataReadRequests. Other record types inherit the empty default, so no other extra-data path is changed.
- The READ_EXERCISE_ROUTE lookup now uses getOrDefault(READ_EXERCISE_ROUTE, false), so a missing key counts as not granted. Before the fix, the readRecords override unboxed extraPermsState.get() directly.
- When isExerciseRouteFeatureEnabled() is false, neither path returns a route (unchanged behavior).
Behavior changes
- getChangeLogs responses no longer include ExerciseRoute data for sessions written by other apps unless the caller holds android.permission.health.READ_EXERCISE_ROUTE. The caller's own routes are still returned.
- getChangeLogs now calls Objects.requireNonNull on the AttributionSource package name before scheduling work.
- DataPermissionEnforcer.collectExtraReadPermissionToStateMapping takes a Set<Integer> of record types and can evaluate permissions for several record types in one call.
- AppInfoHelper.getAppInfoId(null) returns DEFAULT_LONG.
- ExerciseSessionRecord.toRecordInternal propagates hasRoute() into the internal record.
Per major
- Android 14: Same fix: commit 178f4824574f in android-security-14.0.0_r6, a cherry-pick of 69c320af3eff (Change-Id I5ab3835a2ff451ef4ed18863858bb86af6a455c8). At android-security-14.0.0_r29 the patched region is fully present (8/8 lines).
- Android 15: Not affected per the bulletin. At android-security-15.0.0_r17, 6/8 lines of the patched region are present. The material does not show the differences.
- Android 16: Not affected per the bulletin. At android-security-16.0.0_r8, 6/8 lines of the patched region are present. The material does not show the differences.
- Android 17: Not affected per the bulletin. At android-security-17.0.0_r1, 6/8 lines of the patched region are present. The material does not show the differences.
Unknowns
- The material does not show whether isExerciseRouteFeatureEnabled() is enabled by default on affected builds.
- The material does not show the code in the Android 15/16/17 branches (6/8 patched lines present) or which two lines differ.
- The material does not say whether the ExerciseSessionRecord.toRecordInternal setHasRoute change is needed for the security behavior or only for test correctness.
- No CTS test in the material checks change-log behavior for another app's route when the caller lacks READ_EXERCISE_ROUTE.