Android’s Install Flow: A treasure map for exploit crafters
In my previous post, Samsung’s ISVP: Betraying the Trust of Security Researchers, I shared my experience uncovering a vulnerability in Samsung mobile devices that enabled silent app installation.
This flaw allowed non-privileged apps to bypass the android.permission.INSTALL_PACKAGES
signature permission, permitting silent application installations, bypassing the user confirmation dialog normally enforced by PackageInstallerActivity
Despite Samsung’s ISVP policy specifying a $50,000 reward for such vulnerabilities, I received only $500, and my follow-up inquiries were ignored. While some recommended publicly disclosing the vulnerability details, doing so would breach the program’s ToS.
Instead, this post examines the general Android app installation process, drawing on publicly available code from AOSP android-15.0.0_r1. The discussion is not specific to Samsung devices or the vulnerability I reported, focusing solely on open-source mechanisms.
Key Components
This section outlines the primary components involved in Android’s application installation process. It focuses on system services, privileged apps, and client interactions, providing a general overview independent of any vendor-specific implementations.
PackageInstallerService
The PackageInstallerService
is a core system service that manages application installation sessions. Hosted in PackageInstallerService.java, it provides methods to create, manage, and finalize installation sessions. Key methods include:
-
createSession(sessionParam, installerPackageName, callingAttributionTag, userId): Int
Create a new session using the given parameters, returning a unique ID that represents the session. Once created, the session can be opened multiple times across multiple device boots. The system may automatically destroy sessions that have not been finalized (either committed or abandoned) within a reasonable period of time, typically on the order of a day.
-
openSession(sessionId): IPackageInstallerSession
Open an existing session to actively perform work. To succeed, the caller must be the owner of the install session.
-
getSessionInfo(sessionId): PackageInstaller.SessionInfo
Return details for a specific session. Callers need to either declare
element with the specific package name in the app's manifest, have the `android.permission.QUERY_ALL_PACKAGES`, or be the session owner to retrieve these details. -
setPermissionsResult(sessionId, accepted)
A hidden API for privileged callers to notify
PackageInstallerService
that the user has approved an installation request, allowing the installation to proceed.
PackageInstallerSession
The PackageInstallerSession
class handles the active phase of an installation session, enabling data writing and session finalization. Key methods include:
-
openWrite(name, offset, length): ParcelFileDescriptor
Open a stream to write an APK file into the session. The returned stream will start writing data at the requested offset in the underlying file, which can be used to resume a partially written file. If a valid file length is specified, the system will preallocate the underlying disk space to optimize placement on disk. It’s strongly recommended to provide a valid file length when known. You can write data into the returned stream, optionally call fsync(OutputStream) as needed to ensure bytes have been persisted to disk, and then close when finished. All streams must be closed before calling commit(IntentSender).
-
commit(statusReceiver, forTransferred)
Commit the session when all constraints are satisfied. This is a convenient method to combine waitForInstallConstraints(List, PackageInstaller. InstallConstraints, IntentSender, long) and PackageInstaller. Session. commit(IntentSender). Once this method is called, the session is sealed and no additional mutations may be performed on the session. In the case of timeout, you may commit the session again using this method or PackageInstaller. Session. commit(IntentSender) for retries.
PackageInstaller
The PackageInstaller
system app (com.android.packageinstaller
) facilitates sideloading of applications. It interacts with PackageInstallerService
& PackageInstallerSession
to provide a user interface for installation prompts and manage the installation process.
OEM Privileged App Stores
Although not the focus of this post, OEM app stores (such as Google Play, Samsung Galaxy Store, Huawei AppGallery, and Oppo/Vivo/Xiaomi stores) typically hold the INSTALL_PACKAGES
permission. These stores are common entry points for installing third-party applications, often referred to as app store installations, in contrast to sideloading like PIA
or Session Install
.
Client Applications
Client apps are non-privileged applications that initiate app installations. They interact with PackageInstallerService
and (or) PackageInstaller
through APIs or ACTIONs to request installation sessions
Sideloading
Android supports two methods for requesting application installation: PIA
and Session Install
. These are informal terms used within the AOSP codebase. In this post, PIA
refers to the traditional app installation approach utilize the android.intent.action.INSTALL_PACKAGE
action, while Session Install
denotes a newer API set introduced in Android L. PIA
delegates most tasks to the PackageInstaller
app, whereas Session Install
offers apps finer control over the installation process. Additionally, only Session Install
supports multi-package installations, sometimes called splits
.
Since PackageInstaller
internally uses Session Install
to interact with PackageInstallerSession
for installations triggered by PIA
, I’ll discuss Session Install
first.
Session Install
The core APIs for Session Install
were covered in the previous chapter. In essence, the process can be summarized as follows:
1
2
3
4
5
6
7
8
9
10
with(packageManager.packageInstaller) {
val session = openSession(createSession(PackageInstaller.SessionParams(MODE_FULL_INSTALL)))
session.openWrite("foobar.apk", 0, -1).use(assets.open("magisk.apk")::copyTo)
session.commit(IntentSender(object : IIntentSender.Stub() { // ⬅️ 1
override fun send(code: Int, intent: Intent, resolvedType: String?, whitelistToken: IBinder?, finishedReceiver: IIntentReceiver?, requiredPermission: String?, options: Bundle?) {
// ⬇️ 2
intent.extras?.getParcelable(Intent.EXTRA_INTENT, Intent::class.java)?.let(::startActivity)
}
}))
}
This code initiates a session, writes the apk content, and commits the session. Once commit
is called, the PackageInstallerSession
workflow starts.
-
PackageInstallerSession.commit
PackageInstallerSession
dispatchesMSG_ON_SESSION_SEALED
,MSG_STREAM_VALIDATE_AND_COMMIT
, andMSG_INSTALL
sequentially, each following the completion of the previous step.MSG_INSTALL
When
MSG_INSTALL
is sent, thehandleInstall
method is invoked. This method callssendPendingUserActionIntentIfNeeded
to determine if user approval is required.sendPendingUserActionIntentIfNeeded
Invokes
checkUserActionRequirement
to evaluate the need for user action.checkUserActionRequirement
Uses
computeUserActionRequirement
to calculate the user action requirement. For non-privileged apps, the result is typicallyUSER_ACTION_REQUIRED
, triggering theIntentSender
to be sent with additional extras, including:Intent.EXTRA_INTENT
: The intent the client app must invoke to start the user action process, always pointing toPackageInstaller
with the actionACTION_CONFIRM_INSTALL
.EXTRA_SESSION_ID
: The current session ID.EXTRA_STATUS
: Indicates that the installation is suspended, awaiting user action.
At this point, the installation process is paused (state recorded internally and return), and the
IntentSender
provided by the client app is triggered. The client app is expected to handle the filled intent and start theIntent.EXTRA_INTENT
intent -
PackageInstaller
WorkflowThe
PackageInstaller
system app (com.android.packageinstaller
) manages the user-facing installation process in Android. When a client app try to resume the installation by starting theEXTRA_INTENT
in its intent, the system resolves it to theInstallStart
activity withinPackageInstaller
, based on its defined intent filter.1 2 3 4 5 6
<activity android:name=".InstallStart" android:exported="true" android:excludeFromRecents="true"> <intent-filter android:priority="1"> <action android:name="android.content.pm.action.CONFIRM_INSTALL" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
InstallStart
activity performs initial validation checks and delegates the remaining process toPackageInstallerActivity
.-
PackageInstallerActivity
The
PackageInstallerActivity
retrieves session details committed by the client app usingPackageInstaller.getSessionInfo(sessionId)
. It accesses the apk file saved byPackageInstallerService
, typically located at/data/app/vmdl${random-digital}.tmp/base.apk
. The activity parses the apk to extract metadata, such as the application’s icon and name, and displays a dialog prompting the user to confirm or cancel the installation.,
1 2 3 4
* Hist #1: ActivityRecord{1a9977 u0 com.android.packageinstaller/.PackageInstallerActivity t191} * Hist #0: ActivityRecord{59b2c54 u0 play.ground/.IndexActivity t191} * Hist #0: ActivityRecord{5bbee2e u0 com.android.launcher3/.uioverrides.QuickstepLauncher t178} * Hist #0: ActivityRecord{197f44f u0 com.android.dialer/.main.impl.MainActivity t187}
When the user clicks “Install” in the dialog,
PackageInstallerActivity
records aRESULT_OK
state and call the overriddenfinish
. OnRESULT_OK
,finish
callsPackageInstaller.setPermissionsResult(mSessionId, true)
via AIDL, promptingPackageInstallerService
to locate the session bysessionId
, set itsmPermissionsManuallyAccepted
flag totrue
, and resume the suspended installation process, completing the installation smoothly.The
setPermissionsResult
method, restricted to apps with theINSTALL_PACKAGES
permission, ensures only privileged components likePackageInstaller
can approve user-authorized installations, maintaining security by preventing unauthorized apps from manipulating the process.
-
PIA
While google is refactoring the process and UI, guarded by the pia_v2 flag, but this is not yet enabled. This discussion focuses on the traditional installation process.
Internally, PIA
functions similarly to a session-based installation but offers developers a simpler and more convenient API. PackageInstaller
handles most tasks for the client app, including creating the session, copying the apk stream to the session, and committing the session.
The installation process involves the following steps:
-
Initiating Installation:
- The client triggers an installation by sending an intent with the action
android.intent.action.INSTALL_PACKAGE
, specifying a content URI as the data source.
- The client triggers an installation by sending an intent with the action
-
Handling the Intent:
- The
PackageInstaller
receives the intent and processes it through theInstallStart
activity, configured as follows:
1 2 3 4 5 6 7 8 9 10 11
<activity android:name=".InstallStart" android:exported="true" android:excludeFromRecents="true"> <intent-filter android:priority="1"> <action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.INSTALL_PACKAGE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="content" /> <data android:mimeType="application/vnd.android.package-archive" /> </intent-filter> </activity>
- The
-
Staging the apk
- After validating and extracting necessary parameters, the
InstallStart
activity launches theInstallStaging
activity to prepare the apk for installation. This includes:- Creating session parameters for the installation.
- Displaying a progress dialog for staging.
- Copying the apk payload to a
PackageInstallSession
. - Retrieving the resolved base apk file path (generated by
PackageInstallSession
) and passing it to thePackageInstallerActivity
.
- After validating and extracting necessary parameters, the
-
User Confirmation:
The
PackageInstallerActivity
presents a confirmation dialog to the user. Once the user confirms the installation, control is passed toInstallInstalling
, which handles the final phase of committing the session and completing the installation. -
Completing the Installation: The
InstallInstalling
activity invokesPackageInstallationSession.commit(IntentSender, forTransfer)
to finalize the installation. Unlike a standard session-based installation, this session is created and commited byPackageInstaller
, which holds theINSTALL_PACKAGES
permission. As a result, thecomputeUserActionRequirement
method returnsUSER_ACTION_NOT_NEEDED
, allowing the installation to proceed without suspension.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
@UserActionRequirement private int computeUserActionRequirement() { ... // For the below cases, force user action prompt // 1. installFlags includes INSTALL_FORCE_PERMISSION_PROMPT // 2. params.requireUserAction is USER_ACTION_REQUIRED final boolean forceUserActionPrompt = (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0 || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED; final int userActionNotTypicallyNeededResponse = forceUserActionPrompt ? USER_ACTION_REQUIRED : USER_ACTION_NOT_NEEDED; ... final boolean isInstallPermissionGranted = (snapshot.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGES, mInstallerUid) == PackageManager.PERMISSION_GRANTED); // ⬅️ ... final boolean isPermissionGranted = isInstallPermissionGranted || (isUpdatePermissionGranted && isUpdate) || (isSelfUpdatePermissionGranted && isSelfUpdate) || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver); ... if (isPermissionGranted) { return userActionNotTypicallyNeededResponse; // ⬅️ } ... }
Conclusion
Android’s sideloading process, whether Session Install
or PIA
, hinges on strong user consent in PackageInstaller
, gated by the INSTALL_PACKAGES
privilege permission, orchestrated by system services like PackageInstallerService
, form a critical security barrier
My experience with Samsung’s ISVP revealed a silent installation vulnerability that bypassed user interaction. Such exploitation patterns have significant real-world implications if adopted by threat actors. When OEMs modify AOSP code to serve commercial interests without sufficient security testing, the consequences can be severe. In these cases, collaboration with security researchers isn’t optional, it’s essential.
Engage sincerely with white-hat researchers and honor your commitments, repeated disappointment and broken promises will ultimately erode your reputation.