Skip to content

Commit 0c34cc9

Browse files
committed
Clear invalid JobScheduler data on boot
For unknown reasons, there have been two instances in the wild where Android, at some point, reassigned Custota's app UID. Perhaps something caused Custota's APK to disappear temporarily for one boot? When this happens, the JobScheduler XML file for the old UID gets left behind. While the old job does not show up in `dumpsys jobscheduler`, it still continues to run. This can cause crashes when UpdaterJob is launched with invalid old parameters. This commit works around the problem by deleting JobScheduler XML files with a matching package name, but invalid UID, during boot. Fixes: #116 Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
1 parent 8e46e68 commit 0c34cc9

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

app/module/post-fs-data.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ find /data/system/package_cache -name "${app_id}-*" -exec ls -ldZ {} \+
5555

5656
run_cli_apk com.chiller3.custota.standalone.ClearPackageManagerCachesKt
5757

58+
# On some devices, the UID for the app seems to get reassigned. If this happens,
59+
# nothing will clear out the old JobScheduler job and it still runs, despite it
60+
# not showing up in `dumpsys jobscheduler`. This can cause obscure crashes if
61+
# the old job launches UpdaterJob with unexpected parameters. Work around this
62+
# by forcibly deleting jobs with a matching package name, but invalid UID.
63+
64+
header Clear bad JobScheduler data
65+
66+
run_cli_apk com.chiller3.custota.standalone.ClearBadJobSchedulerDataKt
67+
5868
# Bind mount the appropriate CA stores so that update_engine will use the
5969
# regular system CA store.
6070

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Andrew Gunnerson
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
* Based on BCR code.
5+
*/
6+
7+
@file:Suppress("SameParameterValue")
8+
9+
package com.chiller3.custota.standalone
10+
11+
import android.annotation.SuppressLint
12+
import android.util.Log
13+
import android.util.Xml
14+
import com.chiller3.custota.BuildConfig
15+
import org.xmlpull.v1.XmlPullParser
16+
import java.io.InputStream
17+
import java.lang.invoke.MethodHandles
18+
import java.nio.file.Path
19+
import java.nio.file.Paths
20+
import kotlin.io.path.ExperimentalPathApi
21+
import kotlin.io.path.deleteIfExists
22+
import kotlin.io.path.inputStream
23+
import kotlin.io.path.isRegularFile
24+
import kotlin.io.path.walk
25+
import kotlin.system.exitProcess
26+
27+
private val TAG = MethodHandles.lookup().lookupClass().simpleName
28+
29+
private val PACKAGES_FILE = Paths.get("/data/system/packages.xml")
30+
private val JOB_SCHEDULER_DIR = Paths.get("/data/system/job")
31+
32+
private var dryRun = false
33+
34+
private fun delete(path: Path) {
35+
if (dryRun) {
36+
Log.i(TAG, "Would have deleted: $path")
37+
} else {
38+
Log.i(TAG, "Deleting: $path")
39+
path.deleteIfExists()
40+
}
41+
}
42+
43+
@SuppressLint("BlockedPrivateApi")
44+
private fun resolvePullParser(stream: InputStream): XmlPullParser {
45+
val method = Xml::class.java.getDeclaredMethod("resolvePullParser", InputStream::class.java)
46+
return method.invoke(null, stream) as XmlPullParser
47+
}
48+
49+
private fun parseJobPackage(parser: XmlPullParser): Pair<String, Int>? {
50+
val tags = mutableListOf<String>()
51+
52+
while (true) {
53+
val token = parser.nextToken()
54+
55+
when (token) {
56+
XmlPullParser.START_TAG -> {
57+
tags.add(parser.name)
58+
59+
if (tags.size == 2 && tags[0] == "job-info" && tags[1] == "job") {
60+
val name = (0 until parser.attributeCount)
61+
.find { parser.getAttributeName(it) == "package" }
62+
?.let { parser.getAttributeValue(it) }
63+
?: throw IllegalStateException("<job> has no 'package' attribute")
64+
val userId = (0 until parser.attributeCount)
65+
.find { parser.getAttributeName(it) == "uid" }
66+
?.let { parser.getAttributeValue(it) }
67+
?: throw IllegalStateException("<job> has no 'uid' attribute")
68+
69+
return name to userId.toInt()
70+
}
71+
}
72+
XmlPullParser.END_TAG -> {
73+
if (tags.removeLastOrNull() == null) {
74+
throw IllegalStateException("Tag stack is empty")
75+
}
76+
}
77+
XmlPullParser.END_DOCUMENT -> break
78+
}
79+
}
80+
81+
return null
82+
}
83+
84+
@OptIn(ExperimentalPathApi::class)
85+
private fun clearBadJobSchedulerData(packageName: String, uid: Int?): Boolean {
86+
var ret = true
87+
88+
for (path in JOB_SCHEDULER_DIR.walk()) {
89+
if (!path.isRegularFile()) {
90+
continue
91+
}
92+
93+
val (jobPackageName, jobUid) = try {
94+
path.inputStream().use { parseJobPackage(resolvePullParser(it)) } ?: continue
95+
} catch (e: Exception) {
96+
Log.w(TAG, "Failed to parse $path", e)
97+
ret = false
98+
continue
99+
}
100+
101+
try {
102+
if (jobPackageName == packageName && jobUid != uid) {
103+
delete(path)
104+
}
105+
} catch (e: Exception) {
106+
Log.w(TAG, "Failed to delete $path", e)
107+
ret = false
108+
}
109+
}
110+
111+
return ret
112+
}
113+
114+
private fun parsePackageUid(parser: XmlPullParser, packageName: String): Int? {
115+
val tags = mutableListOf<String>()
116+
117+
while (true) {
118+
val token = parser.nextToken()
119+
120+
when (token) {
121+
XmlPullParser.START_TAG -> {
122+
tags.add(parser.name)
123+
124+
if (tags.size == 2 && tags[0] == "packages" && tags[1] == "package") {
125+
val name = (0 until parser.attributeCount)
126+
.find { parser.getAttributeName(it) == "name" }
127+
?.let { parser.getAttributeValue(it) }
128+
?: throw IllegalStateException("<package> has no 'name' attribute")
129+
if (name == packageName) {
130+
val userId = (0 until parser.attributeCount)
131+
.find { parser.getAttributeName(it) == "userId" }
132+
?.let { parser.getAttributeValue(it) }
133+
?: throw IllegalStateException("<package> has no 'userId' attribute")
134+
135+
return userId.toInt()
136+
}
137+
}
138+
}
139+
XmlPullParser.END_TAG -> {
140+
if (tags.removeLastOrNull() == null) {
141+
throw IllegalStateException("Tag stack is empty")
142+
}
143+
}
144+
XmlPullParser.END_DOCUMENT -> break
145+
}
146+
}
147+
148+
return null
149+
}
150+
151+
private fun getPackageUid(packageName: String): Int? =
152+
PACKAGES_FILE.inputStream().use { input ->
153+
parsePackageUid(resolvePullParser(input), packageName)
154+
}
155+
156+
private fun mainInternal() {
157+
val expectedUid = getPackageUid(BuildConfig.APPLICATION_ID)
158+
Log.i(TAG, "Expected UID: $expectedUid")
159+
160+
clearBadJobSchedulerData(BuildConfig.APPLICATION_ID, expectedUid)
161+
}
162+
163+
fun main(args: Array<String>) {
164+
if ("--dry-run" in args) {
165+
dryRun = true
166+
}
167+
168+
try {
169+
mainInternal()
170+
} catch (e: Exception) {
171+
Log.e(TAG, "Failed to clear bad JobScheduler data", e)
172+
exitProcess(1)
173+
}
174+
}

0 commit comments

Comments
 (0)