Skip to content

add support for delta updates and switch from gzip to brotli; verify app icon hashes before decoding them with Glide - #542

Open
muhomorr wants to merge 14 commits into
GrapheneOS:mainfrom
muhomorr:main_26-10-21
Open

muhomorr wants to merge 14 commits into
GrapheneOS:mainfrom
muhomorr:main_26-10-21

Conversation

@muhomorr

@muhomorr muhomorr commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Comment on lines 470 to +480
makeTemporaryFileDescriptor().use { uncompressedFd ->
GZIPInputStream(FileInputStream(compressedFd), DEFAULT_BUFFER_SIZE).use { inputStream ->
DigestOutputStream(FileOutputStream(uncompressedFd.v), sha256).use { outputStream ->
val bytesCopied = inputStream.copyTo2(outputStream, job)
if (bytesCopied != apk.size) {
throw GeneralSecurityException("size mismatch for file ${apk.name}")
if (deltaBaseFd != null) {
try {
val res = inaPatch(deltaBaseFd, compressedFd, uncompressedFd.v)
if (res != apk.size) {
throw GeneralSecurityException("patched size mismatch for ${apk.name}: expected ${apk.size}, got $res")
}
lseekToStart(uncompressedFd.v)
if (!checkSha256(uncompressedFd.v, apk.size, sha256, apk.sha256)) {
throw GeneralSecurityException("sha256 mismatch for patched ${apk.name}")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous gzip-based code used inputStream.copyTo2(outputStream, job) where copyTo2 checked for coroutine cancellation between chunks. However, the new calls such as Ina.unbrotli() appear to lack cancellation signal support

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Adding cancellation support would substantially complicate the implemenation. In practice, decompression takes few hundred milliseconds. I think that the complexity increase is not worth it in this case.

osPackageInfo = pkgManager.getPackageInfoOrNull(pkgName)
}
}
}

@inthewaves inthewaves Oct 21, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setter for PackageState.osPackageInfo now calls onDeltaStatusChanged. In updateRepo at this osPackageInfo = pkgManager.getPackageInfoOrNull(pkgName), the setter is invoked which calls onDeltaStatusChanged. As this is done for every (non shared library) package, this leads to repo.packages.size main thread handlers being posted to dispatch onPackageStateChanged

UpdatesScreen is one listener that implements onPackageStateChanged:

override fun onPackageStateChanged(state: PackageState) {
    super.onPackageStateChanged(state)
    val status = state.status()
    if (status != PackageState.Status.OUT_OF_DATE && status != PackageState.Status.INSTALLING) {
        updateList()
    } else {
        updateAllViewsExceptList()
    }
}

updateList can be called here which would scan and call listAdapter.updateList on N packages, where N is the number of packages that are up-to-date, not installed, or disabled. This potentially leads to O(N^2) work from an updateRepo call

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rearranged onDeltaStatusChanged() calls to avoid this issue in the new revision.

Comment on lines +76 to +80
fun cancelPendingAction(notificationId: Int) {
checkMainThread()
pendingActions.removeAll { it.notificationId == notificationId }
Notifications.cancel(notificationId)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that addPendingAction waits 300 ms via mainHandler.postDelayed(300L) before inserting an action into pendingActions and showing its notification, so it's possible to race against that

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added handling for this in the new revision.

Comment on lines +491 to +493
} else {
val res = Ina.unbrotli(compressedFd, uncompressedFd.v)
if (res != apk.size) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it also make sense to cover this with inaPatcherSemaphore?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory usage for brotli decompression is low.

Comment on lines 380 to 399
inputStream.copyTo2(size, outputStream, job, path)
}}
}
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the server has a signature that passes the download’s length check but are subsequently rejected by the OS? It seems those bad signatures could be continued to be cached

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but what would be the point in re-downloading the signatures?

@muhomorr muhomorr Oct 21, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though APKs and deltas are re-downloaded on SHA-256 check failure, so having similar behavior for signatures would make sense in case of on-server or on-device data corruption.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added SHA-256 checks for APK signatures in the new revision. Signatures are re-downloaded after SHA-256 mismatch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants