CVE-2024-43093 AI-generated analysis
platform/frameworks/base, packages/ExternalStorageProvider (ExternalStorageProvider DocumentsProvider)
ExternalStorageProvider replaces the regex string match used to hide Android/data, Android/obb and Android/sandbox (and their subtrees) with a filesystem identity check (Files.isSameFile). The check walks the resolved file path and its parents against the restricted directories that exist under the document's root, and fails closed on any error.
Root cause
In ExternalStorageProvider.java, access to the Android/data, Android/obb and Android/sandbox subtrees was restricted by matching the string returned by getPathFromDocId(documentId) against PATTERN_RESTRICTED_ANDROID_SUBTREES ("^Android/(?:data|obb|sandbox)(?:/.+)?", CASE_INSENSITIVE). This checks only how the path is spelled, not which directory it resolves to on disk. The commit message says the fix is "Replacing path's pattern match check with file equality check". From that change one can infer (the material does not show it) that path strings which do not match the regex literally but resolve into a restricted tree were not treated as restricted. The exact bypassing input is not shown in the material.
How the fix works
- ExternalStorageProvider.java: removes PATTERN_RESTRICTED_ANDROID_SUBTREES and the CASE_INSENSITIVE/Pattern imports. Adds the constants PRIMARY_EMULATED_STORAGE_PATH = "/storage/emulated/" and STORAGE_PATH = "/storage/", plus imports for java.nio.file.Files/Paths, Arrays and Collectors.
- ExternalStorageProvider.java, restricted-subtree check method (name not visible in the hunk; it previously returned PATTERN_RESTRICTED_ANDROID_SUBTREES.matcher(path).matches()): inside a try block it now resolves RootInfo root = getRootFromDocId(documentId) and canonicalPath = getPathFromDocId(documentId), then returns isRestrictedPath(root.rootId, canonicalPath). Any Exception makes the method return true (restricted). The existing early 'return false' above the hunk is kept.
- ExternalStorageProvider.java:isRestrictedPath(String rootId, String canonicalPath) (new): returns true if rootId or canonicalPath is null. Builds rootPath as "/storage/emulated/" + UserHandle.myUserId() when rootId equalsIgnoreCase ROOT_ID_PRIMARY_EMULATED, otherwise as "/storage/" + rootId. Builds the paths rootPath/Android/data, rootPath/Android/obb and rootPath/Android/sandbox, and keeps only those for which Files.exists is true.
- isRestrictedPath: starting from Paths.get(rootPath, canonicalPath), it loops while the path is non-null. Each step tests Files.isSameFile(restrictedPath, filePathToCheck) against every existing restricted directory, then moves to getParent(). A match sets isRestricted = true (logged at Log.v) and ends the walk. Any Exception during the walk is logged at Log.w and sets isRestricted = true, so the check fails closed.
Completeness
- The check now compares the filesystem identity of the resolved document path and every ancestor against the three restricted directories. This covers the directories themselves and all nested content, for both the primary emulated root and other roots mapped to /storage/<rootId>.
- Only restricted directories that currently exist on disk are compared (Files::exists filter). If none of the three exist for the computed rootPath, validRestrictedPathsToCheck is empty, the loop never calls isSameFile, and the method returns false (not restricted).
- The non-primary root mapping assumes the volume is mounted at "/storage/" + rootId. The material does not show how rootId is derived for other volumes.
- The restriction is computed for UserHandle.myUserId(); no cross-user path handling is added.
- The walk is not bounded by rootPath: getParent() continues up to "/".
- Only this one check method is changed. getPathFromDocId, getRootFromDocId and the 'root of storage or blocked file from tree' check that follows the hunk are not modified by this commit.
Behavior changes
- The restriction decision now performs filesystem I/O (Files.exists, Files.isSameFile) on each call instead of a pure string match.
- Fail-closed: any exception while resolving the root or path, or during the identity walk, is now caught and the document is treated as restricted.
- Case-insensitive textual matching is replaced by filesystem identity.
- New verbose log on restriction and warning log on errors.
Per major
- Android 14: same fix: platform/frameworks/base 67d6e0832201 in android-security-14.0.0_r14 (cherry-pick of 02c5b768..., Change-Id I8879900e...); at android-security-14.0.0_r29, 7/8 lines of the patched region are present.
- Android 15: same fix: platform/frameworks/base 7f83c671626f in android-security-15.0.0_r2, identical diff (same blob hashes 3409c29d3c2c..defbc1142adb); at android-security-15.0.0_r17, 7/8 lines of the patched region are present.
- Android 16: not affected (not listed by the bulletin); at android-security-16.0.0_r8, 7/8 lines of the patched region are present.
- Android 17: not affected (not listed by the bulletin); at android-security-17.0.0_r1, 7/8 lines of the patched region are present.
Unknowns
- The name of the modified check method and the condition of its early 'return false' are not shown in the diff hunk.
- The concrete path form that bypassed the regex is not shown.
- How rootId is derived for non-primary volumes, and so whether "/storage/" + rootId is always the mount point.
- Which inputs cause Files.isSameFile to throw (and so be mapped to restricted) in this context.
- Whether getPathFromDocId could throw in the pre-fix code.
- Which of the 8 patched-region lines differs in the later security tags (7/8 present) and whether that difference is cosmetic.
- The commits used for Android 12, 12L and 13 are not included in the material.