Summary
On newer Android versions, updating a preinstalled (system) app requires the incoming APK to have fs-verity. The only way to satisfy that through PackageInstaller is to stage the APK's v4 signature (<name>.apk.idsig) as an additional file in the same session. Ackpine currently writes only the APK itself, so installs of this kind fail and there is no way to work around it through the public API.
I'd like Ackpine to support supplying a v4 signature alongside each APK. I'm happy to implement this and open a PR - filing first to agree on the API shape.
Motivation
We ship an Android-based OS with a number of our own apps preinstalled in /product/priv-app, and our in-house app store (which uses Ackpine) updates them. On our older devices this works fine. On our newest device (Android 17 / API 37) every update of a preinstalled app fails:
INSTALL_FAILED_INTERNAL_ERROR: fs-verity not set up for system package update
com.android.server.pm.PackageManagerException: APK doesn't have fs-verity: /data/app/vmdl…/base.apk
This affects any app that is part of a system image and later updated from a store - not just our setup.
Why the existing paths don't solve it
Incremental install is not an option. The platform explicitly refuses incremental installation for system apps. adb install (which does know about .idsig files) tries incremental first and is rejected, then silently falls back to a streamed install that drops the signature:
Performing Incremental Install
Failure [INSTALL_FAILED_SESSION_INVALID: Incremental installation of this package is not allowed.]
Performing Streamed Install
adb: failed to install …: Failure [INSTALL_FAILED_INTERNAL_ERROR: fs-verity not set up for
system package update … APK doesn't have fs-verity: /data/app/vmdl…/base.apk]
Confirmed on the device side:
PackageInstallerSession: Session […] was destroyed because of
[Incremental installation of this package is not allowed.]
Staging the .idsig in an ordinary session does work. The same APK installs successfully when the v4 signature is written into the session as a second file:
SID=$(adb shell pm install-create -r | sed -n 's/.*\[\([0-9]*\)\].*/\1/p')
adb shell pm install-write -S $APK_SIZE $SID base.apk /data/local/tmp/base.apk
adb shell pm install-write -S $IDSIG_SIZE $SID base.apk.idsig /data/local/tmp/base.apk.idsig
adb shell pm install-commit $SID
# Success
So the capability exists in PackageInstaller - Session.openWrite() accepts an arbitrary entry name, and the platform associates <name>.idsig with <name>. It just isn't reachable through Ackpine today.
Current behaviour in Ackpine
SessionBasedInstallSession writes only the APK, naming entries by index:
fun writeApk(afd: AssetFileDescriptor, index: Int) = afd.createInputStream().use { apkStream ->
…
val sessionStream = openWrite("$index.apk", 0, length)
…
}
There is no way for a caller to add a .idsig (or any non-APK file) to the session.
Proposed change
Allow an optional v4 signature per APK. Two shapes that would fit the existing API - happy to follow whichever you prefer:
// A: pair the signature with the APK at the point the APK is declared
InstallParameters.Builder(apks)
.addApk(apkUri, v4Signature = idsigUri)
.build()
// B: a separate mapping, leaving the existing apks list untouched
InstallParameters.Builder(apks)
.setV4Signatures(mapOf(apkUri to idsigUri))
.build()
Implementation is small and local to SessionBasedInstallSession.writeApk - after writing "$index.apk", write the signature to "$index.apk.idsig" when one was supplied:
v4Signature?.let { signatureAfd ->
signatureAfd.createInputStream().use { input ->
openWrite("$index.apk.idsig", 0, signatureAfd.declaredLength).use { output ->
input.copyTo(output)
fsync(output)
}
}
}
It only needs to apply to the session-based installer; the intent-based one can ignore it.
Important compatibility caveat
This must be opt-in / gated - it cannot be applied unconditionally. Older Android versions do not recognise .idsig session entries and try to parse them as APKs, which fails the whole commit. Verified on Android 14 (API 34) with the exact sequence above:
Success: streamed 95796 bytes # writing the idsig succeeds
commit: Failure [INSTALL_PARSE_FAILED_NOT_APK: Failed to parse …/base.apk.idsig:
Failed to load asset path …/base.apk.idsig]
Both base.apk.idsig and 0.apk.idsig naming failed identically there, so the rejection is about the extra entry, not the name. The same APK installs fine on API 34 when the signature is omitted.
Because of this, a caller supplying a v4 signature should get it staged only where the platform supports it, or the library should expose the behaviour explicitly so callers can decide.
Open questions
- Entry naming. I verified
base.apk + base.apk.idsig works on API 37. Ackpine names entries $index.apk, so the signature would be $index.apk.idsig - I have not yet confirmed that variant satisfies fs-verity on API 37, only that the naming is irrelevant to the API 34 rejection. Worth verifying before merging.
- Split APKs / APK sets. Each APK in a multi-APK install would need its own
.idsig, so the API should express the association per-APK rather than as a single signature for the session.
- Version gating. Is a runtime capability check preferable to an API-level check? The requirement appears somewhere between API 34 and 37; I have not narrowed it further.
Environment
- Fails: Android 17 (API 37), AOSP-based (GrapheneOS), app preinstalled in
/product/priv-app, flags SYSTEM | PRIVILEGED | PRODUCT
- Works without
.idsig: Android 14 (API 34), app preinstalled in /system/priv-app
- APK signed with v2 + v3 + v4 (
apksigner --v4-signing-enabled true), identical certificate to the preinstalled version, higher versionCode
- fs-verity is available on the failing device:
/data is f2fs with the verity feature and the kernel has CONFIG_FS_VERITY=y - the signature simply never reaches the platform
Summary
On newer Android versions, updating a preinstalled (system) app requires the incoming APK to have fs-verity. The only way to satisfy that through
PackageInstalleris to stage the APK's v4 signature (<name>.apk.idsig) as an additional file in the same session. Ackpine currently writes only the APK itself, so installs of this kind fail and there is no way to work around it through the public API.I'd like Ackpine to support supplying a v4 signature alongside each APK. I'm happy to implement this and open a PR - filing first to agree on the API shape.
Motivation
We ship an Android-based OS with a number of our own apps preinstalled in
/product/priv-app, and our in-house app store (which uses Ackpine) updates them. On our older devices this works fine. On our newest device (Android 17 / API 37) every update of a preinstalled app fails:This affects any app that is part of a system image and later updated from a store - not just our setup.
Why the existing paths don't solve it
Incremental install is not an option. The platform explicitly refuses incremental installation for system apps.
adb install(which does know about.idsigfiles) tries incremental first and is rejected, then silently falls back to a streamed install that drops the signature:Confirmed on the device side:
Staging the
.idsigin an ordinary session does work. The same APK installs successfully when the v4 signature is written into the session as a second file:So the capability exists in
PackageInstaller-Session.openWrite()accepts an arbitrary entry name, and the platform associates<name>.idsigwith<name>. It just isn't reachable through Ackpine today.Current behaviour in Ackpine
SessionBasedInstallSessionwrites only the APK, naming entries by index:There is no way for a caller to add a
.idsig(or any non-APK file) to the session.Proposed change
Allow an optional v4 signature per APK. Two shapes that would fit the existing API - happy to follow whichever you prefer:
Implementation is small and local to
SessionBasedInstallSession.writeApk- after writing"$index.apk", write the signature to"$index.apk.idsig"when one was supplied:It only needs to apply to the session-based installer; the intent-based one can ignore it.
Important compatibility caveat
This must be opt-in / gated - it cannot be applied unconditionally. Older Android versions do not recognise
.idsigsession entries and try to parse them as APKs, which fails the whole commit. Verified on Android 14 (API 34) with the exact sequence above:Both
base.apk.idsigand0.apk.idsignaming failed identically there, so the rejection is about the extra entry, not the name. The same APK installs fine on API 34 when the signature is omitted.Because of this, a caller supplying a v4 signature should get it staged only where the platform supports it, or the library should expose the behaviour explicitly so callers can decide.
Open questions
base.apk+base.apk.idsigworks on API 37. Ackpine names entries$index.apk, so the signature would be$index.apk.idsig- I have not yet confirmed that variant satisfies fs-verity on API 37, only that the naming is irrelevant to the API 34 rejection. Worth verifying before merging..idsig, so the API should express the association per-APK rather than as a single signature for the session.Environment
/product/priv-app, flagsSYSTEM | PRIVILEGED | PRODUCT.idsig: Android 14 (API 34), app preinstalled in/system/priv-appapksigner --v4-signing-enabled true), identical certificate to the preinstalled version, higherversionCode/datais f2fs with theverityfeature and the kernel hasCONFIG_FS_VERITY=y- the signature simply never reaches the platform