CVE-2025-22439 AI-generated analysis
platform/packages/apps/DocumentsUI, picker (Storage Access Framework) ActionHandler
DocumentsUI's file picker now runs the existing restricted-initial-path check (/Android/data, /Android/obb, /Android/sandbox) on the restored last-accessed DocumentStack. If the stack's top document is restricted, the picker falls back to the default location.
Root cause
In src/com/android/documentsui/picker/ActionHandler.java, shouldPreemptivelyRestrictRequestedInitialUri() treats a URI as restricted when its external-storage path matches PATTERN_RESTRICTED_INITIAL_PATH (/Android/data, /Android/obb, /Android/sandbox and their subdirectories, per the code comment). But onLastAccessedStackLoaded() did not run this check. It is reached from both initLoadLastAccessedStack() and loadLastAccessedStack() (the latter via LoadLastAccessedStackTask), and for any non-null stack from mLastAccessed it called mState.stack.reset(stack) and refreshed the UI. A last-accessed stack for the calling package that pointed into a restricted directory was therefore restored without the check. The commit message says this bypassed the scoped storage restriction placed on the initial URI.
How the fix works
- src/com/android/documentsui/picker/ActionHandler.java:onLastAccessedStackLoaded(): adds an `else if (shouldPreemptivelyRestrictRequestedInitialUri(stack.peek().getDocumentUri()))` branch between the null check and the restore branch. When the top document of the loaded stack is restricted, it calls loadDefaultLocation() instead of mState.stack.reset(stack) followed by mActivity.refreshCurrentRootAndDirectory().
- Reuses the existing restriction predicate. The path is resolved via FileUtils.getPathFromStorageDocId and matched against PATTERN_RESTRICTED_INITIAL_PATH, and an IOException while resolving the path also counts as restricted. The same policy used for the requested initial URI therefore now applies to the last-accessed stack.
- Single-commit fix that adds 3 lines. Both entry points (synchronous initLoadLastAccessedStack and asynchronous loadLastAccessedStack) go through the patched method.
Completeness
- Covers both the blocking (initLoadLastAccessedStack) and asynchronous (LoadLastAccessedStackTask) load paths, since both call onLastAccessedStackLoaded.
- Only the top entry of the stack (stack.peek()) is checked. Lower entries are not validated one by one.
- The check runs only when the stack is loaded. The diff does not touch how the last-accessed stack is stored, so a restricted stack can stay stored and is simply not restored.
- The new branch calls stack.peek().getDocumentUri() without a null check. The material does not show what happens with a non-null but empty DocumentStack.
Behavior changes
- When the calling package's last-accessed stack points at a restricted external-storage path (or its path cannot be resolved), the picker now opens the default location instead of restoring the previous directory.
- No new error, result code or user-visible message. The fallback is the same loadDefaultLocation() already used when no stack exists.
Per major
- Android 14: same fix: commit 1f283d5b87d0 in android-security-14.0.0_r19 (cherry-pick of an internal change, Change-Id I4de8bad7174273c9390da978e186ad6a85f27be5). All 5 lines of the patched region are present at android-security-14.0.0_r29.
- Android 15: same fix: commit a9fe0826e308 in android-security-15.0.0_r7, with the identical 3-line hunk at a slightly different offset (line 272 instead of 278). All 5 lines are present at android-security-15.0.0_r17.
- Android 16: not affected (not listed by the bulletin). The patched region is present at android-security-16.0.0_r8 (5/5 lines).
- Android 17: not affected (not listed by the bulletin). The patched region is present at android-security-17.0.0_r1 (5/5 lines).
Unknowns
- How a restricted-path stack comes to be stored for a calling package (the write path for the last-accessed stack is not in the material).
- Whether the Android 13 fix (the bulletin lists 13 as affected) is identical; no Android 13 commit is provided.
- How the new branch behaves for a non-null but empty DocumentStack (stack.peek() nullability not shown).
- Whether checking only the top entry is enough when lower entries are restricted; the full PATTERN_RESTRICTED_INITIAL_PATH regex is not shown.
- The full body of shouldPreemptivelyRestrictRequestedInitialUri (the lines before 248), for example how URIs not from ExternalStorageProvider are handled.
- How the last-accessed load path is chosen relative to a supplied initial URI.