diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt index 1a1b6930b..d61a95912 100644 --- a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -518,8 +518,12 @@ object FileSystem { fun getLogs(zipFd: ParcelFileDescriptor) { runCatching { ZipOutputStream(java.io.FileOutputStream(zipFd.fileDescriptor)).use { os -> + // The commit, not just the version code: the code is the commit count on master, so + // every branch build at the same depth wears the number of an official build it was + // never made from. Without it an attached archive cannot be tied to a binary. val comment = - "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} " + + "(${BuildConfig.VERSION_CODE}) ${BuildConfig.VERSION_HASH}" os.setComment(comment) os.setLevel(java.util.zip.Deflater.BEST_COMPRESSION) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df596a88a..59e4a1167 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,8 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } [libraries] +# Build-only: reads the optimised DEX in checkXResourcesIsolation. +smali-dexlib2 = { group = "com.android.tools.smali", name = "smali-dexlib2", version = "3.0.9" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.19.0" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } diff --git a/legacy/consumer-rules.pro b/legacy/consumer-rules.pro index 7da43889d..2698c9b56 100644 --- a/legacy/consumer-rules.pro +++ b/legacy/consumer-rules.pro @@ -1,5 +1,14 @@ +# Keeping a class also keeps the optimiser from merging it into another one, so the resource types +# below stay classes of their own. That says nothing about the classes the optimiser *invents*: a +# lambda written anywhere becomes a class, and classes of the same shape are merged afterwards, so a +# reference can end up in a class no one wrote and no rule here names. Types whose super class is +# generated at runtime must not travel that way, since they cannot be resolved until the device has +# built the super class and the runtime never retries a failed resolution. That invariant is checked +# against the optimised dex by checkXResourcesIsolationRelease, not from this file. -keep class android.** { *; } -keep class de.robv.android.xposed.** { *; } -# Workaround to bypass verification of in-memory built class xposed.dummy.XResourcesSuperClass +# The in-memory built class xposed.dummy.XResourcesSuperClass exists only on the device, so the +# class below is a deliberate split: it isolates the reference to XResources from its owner, which +# would otherwise be verified long before that super class exists. -keepclassmembers class org.matrix.vector.legacy.LegacyDelegateImpl$ResourceProxy { *; } diff --git a/legacy/src/main/java/android/content/res/XResources.java b/legacy/src/main/java/android/content/res/XResources.java index 833ea926e..d1e53636f 100644 --- a/legacy/src/main/java/android/content/res/XResources.java +++ b/legacy/src/main/java/android/content/res/XResources.java @@ -75,7 +75,8 @@ public class XResources extends XResourcesSuperClass { private static final WeakHashMap sXmlInstanceDetails = new WeakHashMap<>(); private static final String EXTRA_XML_INSTANCE_DETAILS = "xmlInstanceDetails"; - private static final ThreadLocal> sIncludedLayouts = ThreadLocal.withInitial(() -> new LinkedList<>()); + // No lambda, and no anonymous ThreadLocal either. See the note above [includedLayouts]. + private static final ThreadLocal> sIncludedLayouts = new ThreadLocal<>(); private static final HashMap sResDirLastModified = new HashMap<>(); private static final HashMap sResDirPackageNames = new HashMap<>(); @@ -92,11 +93,53 @@ public XResources(ClassLoader classLoader, String resDir) { if (resDir != null) { synchronized (sReplacementsCacheMap) { - mReplacementsCache = sReplacementsCacheMap.computeIfAbsent(resDir, k -> new byte[128]); + // Not computeIfAbsent: its mapping function would be a lambda, which this class may + // not create. See the note above [includedLayouts]. Under this lock the two spellings + // describe the same operation. + byte[] cache = sReplacementsCacheMap.get(resDir); + if (cache == null) { + cache = new byte[128]; + sReplacementsCacheMap.put(resDir, cache); + } + mReplacementsCache = cache; } } } + /** + * The thread's stack of `LayoutInflater.parseInclude` calls, created on first use. + * + * Written the long way on purpose. `ThreadLocal.withInitial(() -> ...)`, and an anonymous + * `ThreadLocal` overriding `initialValue`, would each add a class that names `XResources`, and + * this class may not be named by classes it does not control. + * + * The reason is its super class. {@link xposed.dummy.XResourcesSuperClass} is in no dex; it is + * generated at runtime, because it has to inherit from whichever `Resources` subclass the + * platform actually provides. A type whose super class does not yet exist cannot be resolved, + * and a resolution failure is remembered: the runtime marks the class erroneous and re-throws + * for every later attempt. So the fragility is not local. It travels along references — anything + * naming `XResources` is unusable in the same window, and stays unusable afterwards. + * + * A whole-program optimiser extends that reach in a way the source cannot show. Each lambda + * becomes a class of its own, and classes of the same shape are then merged, so two lambdas + * written in unrelated files can end up as one class. A lambda here is therefore not a private + * detail of this file: it is a reference to `XResources` placed inside a class that arbitrary + * code may instantiate, and every such instantiation inherits the window above. + * + * Keep rules cannot express this — they govern names and members, not which classes an optimiser + * invents. So the rule is stated here, next to what it protects, and checked where it is really + * decided: `checkXResourcesIsolationRelease` reads the optimised dex and fails the build if any + * class outside resource hooking has come to name one of these types. + */ + private static LinkedList includedLayouts() { + LinkedList layouts = sIncludedLayouts.get(); + if (layouts == null) { + layouts = new LinkedList<>(); + sIncludedLayouts.set(layouts); + } + return layouts; + } + /** Dummy, will never be called (objects are transferred to this class only). */ // private XResources() { // throw new UnsupportedOperationException(); @@ -220,12 +263,12 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { final XC_MethodHook parseIncludeHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - sIncludedLayouts.get().push(param); + includedLayouts().push(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { - sIncludedLayouts.get().pop(); + includedLayouts().pop(); if (param.hasThrowable()) return; @@ -962,7 +1005,7 @@ public XmlResourceParser getLayout(int id) throws NotFoundException { sXmlInstanceDetails.put(result, details); // if we were called inside LayoutInflater.parseInclude, store the details for it - MethodHookParam top = sIncludedLayouts.get().peek(); + MethodHookParam top = includedLayouts().peek(); if (top != null) top.setObjectExtra(EXTRA_XML_INSTANCE_DETAILS, details); } diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt new file mode 100644 index 000000000..b6895ed03 --- /dev/null +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/log/LogArchiveName.kt @@ -0,0 +1,42 @@ +package org.matrix.vector.manager.data.log + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import org.matrix.vector.manager.BuildConfig + +/** + * What a saved bug report is called, wherever it is saved from. + * + * Three places produce one — the log panel, the troubleshooting page, and the root export that + * tars the daemon's folder — and they used to name it three ways. The name is the first thing + * anyone attaching one to an issue sees, and it has to say which build it came from: a report from + * a debug build explains behaviour that a release build does not have, and asking after the fact + * is a round trip that the file name can save. + * + * Not a string resource. It was one, translated into nineteen locales, but a file name is not + * language — a report named in Persian and one named in German are the same file, and the + * translations only made it possible for them to disagree. + * + * [extension] rather than a fixed `zip`: the manager builds a zip through SAF, while the root + * export shells out to `tar`, which is what Android actually ships. Only the extension differs. + */ +fun logArchiveName(extension: String): String = + "Vector-logs-${BuildConfig.BUILD_TYPE}-${LocalDateTime.now().format(ARCHIVE_STAMP)}.$extension" + +/** + * Which build wrote an archive, for the archive itself to carry. + * + * The name says the build type and no more, because a name has to stay short enough to read. What + * identifies a *binary* is the commit: the version code is the commit count on master, so every + * branch build at the same depth wears the number of an official build it was never made from. + * + * Where this goes depends on what the format offers. A zip has a comment field and gets this + * verbatim; a backup is our own document and carries it as a field. `tar` has no such slot at all, + * so the root export can only say what its name says. + */ +fun archiveBuildStamp(): String = + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} " + + "(${BuildConfig.VERSION_CODE}) ${BuildConfig.VERSION_HASH}" + +/** Sortable, no separators a file manager or a shell would have to be told about. */ +private val ARCHIVE_STAMP: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt index a8c7a9fa2..091985e2b 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/data/repository/BackupRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.lsposed.lspd.models.Application +import org.matrix.vector.manager.data.log.archiveBuildStamp import org.matrix.vector.manager.ipc.DaemonClient import org.matrix.vector.manager.logE import org.matrix.vector.manager.logW @@ -28,6 +29,10 @@ class BackupRepository(private val context: Context, private val daemon: DaemonC @Serializable private data class BackupFile( val version: Int = FORMAT_VERSION, + // Which build wrote this. gzip's own comment field is not reachable through + // GZIPOutputStream, and this document is ours, so it says so itself -- `zcat file | head` + // answers "where did this come from" without a restore. + val build: String = archiveBuildStamp(), val createdAt: Long, val modules: List, ) diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt index 2763b3cc4..5d630b7fd 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/logs/LogsScreen.kt @@ -100,9 +100,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.LayoutDirection import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter import kotlinx.coroutines.launch +import org.matrix.vector.manager.data.log.logArchiveName import org.matrix.vector.manager.ui.components.VectorAlertDialog import org.matrix.vector.manager.ui.theme.LocalizedOverlay import org.matrix.vector.manager.R @@ -154,11 +153,8 @@ fun LogsScreen( ) { uri: Uri? -> if (uri != null) viewModel.saveTo(uri) } - val fileNameTemplate = stringResource(R.string.logs_save_name) fun launchSave() { - saveLauncher.launch( - String.format(fileNameTemplate, LocalDateTime.now().format(FILE_STAMP)) - ) + saveLauncher.launch(logArchiveName("zip")) } val savingLabel = stringResource(R.string.logs_saving) @@ -1012,8 +1008,6 @@ LocalizedOverlay { } } -private val FILE_STAMP: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") - private fun shareZip(context: Context, uri: Uri) { val intent = Intent(Intent.ACTION_SEND).apply { diff --git a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt index 4bf8fe0e4..346f00c5a 100644 --- a/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt +++ b/manager/src/main/kotlin/org/matrix/vector/manager/ui/screens/report/TroubleshootScreen.kt @@ -1,5 +1,4 @@ package org.matrix.vector.manager.ui.screens.report -import android.content.Context import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -43,14 +42,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.vector.manager.R import org.matrix.vector.manager.data.github.GitHubRepository +import org.matrix.vector.manager.data.log.logArchiveName import org.matrix.vector.manager.di.ServiceLocator import org.matrix.vector.manager.logE import org.matrix.vector.manager.ui.components.SnackbarTone @@ -184,17 +182,7 @@ fun TroubleshootScreen( title = stringResource(R.string.report_step_logs), body = stringResource(R.string.report_step_logs_body), ) { - Button( - onClick = { - saveLauncher.launch( - String.format( - stringResourceOf(context, R.string.logs_save_name), - LocalDateTime.now() - .format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")), - ) - ) - } - ) { + Button(onClick = { saveLauncher.launch(logArchiveName("zip")) }) { Icon( Icons.Rounded.Save, contentDescription = null, @@ -320,8 +308,6 @@ private fun Step( } } -private fun stringResourceOf(context: Context, id: Int): String = context.getString(id) - /** * Copies the daemon's log folder somewhere the user can attach it, using root. * @@ -338,8 +324,7 @@ private fun stringResourceOf(context: Context, id: Int): String = context.getStr * going to be attached to an issue. */ private fun exportWithRoot(): Result = runCatching { - val stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")) - val target = "/sdcard/Download/vector-logs-$stamp.tar.gz" + val target = "/sdcard/Download/${logArchiveName("tar.gz")}" val process = ProcessBuilder("su", "-c", "tar -czf $target -C /data/adb/lspd log && chmod 644 $target") .redirectErrorStream(true) diff --git a/manager/src/main/res/values-ar/strings_logs.xml b/manager/src/main/res/values-ar/strings_logs.xml index c1a555251..331be0324 100644 --- a/manager/src/main/res/values-ar/strings_logs.xml +++ b/manager/src/main/res/values-ar/strings_logs.xml @@ -69,7 +69,6 @@ رفضت الخدمة بدء سجل جديد إلغاء - vector-logs-%1$s.zip جارٍ جمع السجلات وملفات tombstone وdmesg… حُفظ البلاغ تعذّر حفظ البلاغ diff --git a/manager/src/main/res/values-de/strings_logs.xml b/manager/src/main/res/values-de/strings_logs.xml index eae016b67..867975c66 100644 --- a/manager/src/main/res/values-de/strings_logs.xml +++ b/manager/src/main/res/values-de/strings_logs.xml @@ -57,7 +57,6 @@ Der Daemon hat es abgelehnt, ein neues Protokoll zu beginnen Abbrechen - vector-logs-%1$s.zip Protokolle, Tombstones und dmesg werden gesammelt… Fehlerbericht gespeichert Der Fehlerbericht konnte nicht gespeichert werden diff --git a/manager/src/main/res/values-es/strings_logs.xml b/manager/src/main/res/values-es/strings_logs.xml index 896e2b9fd..949dc3114 100644 --- a/manager/src/main/res/values-es/strings_logs.xml +++ b/manager/src/main/res/values-es/strings_logs.xml @@ -57,7 +57,6 @@ El daemon se negó a empezar un registro nuevo Cancelar - vector-logs-%1$s.zip Recogiendo registros, tombstones y dmesg… Informe de error guardado No se pudo guardar el informe de error diff --git a/manager/src/main/res/values-fa/strings_logs.xml b/manager/src/main/res/values-fa/strings_logs.xml index 144f8aa4a..8fcd99b72 100644 --- a/manager/src/main/res/values-fa/strings_logs.xml +++ b/manager/src/main/res/values-fa/strings_logs.xml @@ -57,7 +57,6 @@ سرویس از آغاز گزارش تازه سر باز زد انصراف - vector-logs-%1$s.zip در حال گردآوری گزارش‌ها، tombstone و dmesg… گزارش اشکال ذخیره شد ذخیرهٔ گزارش اشکال ممکن نشد diff --git a/manager/src/main/res/values-fr/strings_logs.xml b/manager/src/main/res/values-fr/strings_logs.xml index 6b490b410..58fa34fd2 100644 --- a/manager/src/main/res/values-fr/strings_logs.xml +++ b/manager/src/main/res/values-fr/strings_logs.xml @@ -57,7 +57,6 @@ Le démon a refusé de commencer un nouveau journal Annuler - vector-logs-%1$s.zip Collecte des journaux, des tombstones et de dmesg… Rapport de bug enregistré Impossible d\'enregistrer le rapport de bug diff --git a/manager/src/main/res/values-in/strings_logs.xml b/manager/src/main/res/values-in/strings_logs.xml index 8973886d7..d42133eb0 100644 --- a/manager/src/main/res/values-in/strings_logs.xml +++ b/manager/src/main/res/values-in/strings_logs.xml @@ -54,7 +54,6 @@ Daemon menolak memulai log baru Batal - vector-logs-%1$s.zip Mengumpulkan log, tombstone, dan dmesg… Laporan bug tersimpan Laporan bug tidak bisa disimpan diff --git a/manager/src/main/res/values-it/strings_logs.xml b/manager/src/main/res/values-it/strings_logs.xml index c0b6bf1b7..3f6ff977c 100644 --- a/manager/src/main/res/values-it/strings_logs.xml +++ b/manager/src/main/res/values-it/strings_logs.xml @@ -57,7 +57,6 @@ Il daemon si è rifiutato di cominciare un nuovo log Annulla - vector-logs-%1$s.zip Raccolta di log, tombstone e dmesg… Segnalazione di bug salvata Impossibile salvare la segnalazione di bug diff --git a/manager/src/main/res/values-iw/strings_logs.xml b/manager/src/main/res/values-iw/strings_logs.xml index 408a846ab..54ef2ba26 100644 --- a/manager/src/main/res/values-iw/strings_logs.xml +++ b/manager/src/main/res/values-iw/strings_logs.xml @@ -63,7 +63,6 @@ השירות סירב להתחיל יומן חדש ביטול - vector-logs-%1$s.zip אוסף יומנים, קובצי tombstone ו-dmesg… דיווח התקלה נשמר לא ניתן היה לשמור את דיווח התקלה diff --git a/manager/src/main/res/values-ja/strings_logs.xml b/manager/src/main/res/values-ja/strings_logs.xml index 3b3264be4..b3bb02d4c 100644 --- a/manager/src/main/res/values-ja/strings_logs.xml +++ b/manager/src/main/res/values-ja/strings_logs.xml @@ -54,7 +54,6 @@ デーモンが新しいログの開始を拒否しました キャンセル - vector-logs-%1$s.zip ログ・tombstone・dmesg を集めています… 不具合報告を保存しました 不具合報告を保存できませんでした diff --git a/manager/src/main/res/values-ko/strings_logs.xml b/manager/src/main/res/values-ko/strings_logs.xml index 6503610b8..bc78f45be 100644 --- a/manager/src/main/res/values-ko/strings_logs.xml +++ b/manager/src/main/res/values-ko/strings_logs.xml @@ -54,7 +54,6 @@ 데몬이 새 로그 시작을 거부했습니다 취소 - vector-logs-%1$s.zip 로그, tombstone, dmesg를 모으는 중… 버그 신고를 저장했습니다 버그 신고를 저장하지 못했습니다 diff --git a/manager/src/main/res/values-pl/strings_logs.xml b/manager/src/main/res/values-pl/strings_logs.xml index 83ca60450..14defcb4e 100644 --- a/manager/src/main/res/values-pl/strings_logs.xml +++ b/manager/src/main/res/values-pl/strings_logs.xml @@ -63,7 +63,6 @@ Usługa odmówiła rozpoczęcia nowego dziennika Anuluj - vector-logs-%1$s.zip Zbieranie dzienników, tombstone\'ów i dmesg… Zapisano zgłoszenie błędu Nie udało się zapisać zgłoszenia błędu diff --git a/manager/src/main/res/values-pt-rBR/strings_logs.xml b/manager/src/main/res/values-pt-rBR/strings_logs.xml index d0b9814d6..14a6ebd8c 100644 --- a/manager/src/main/res/values-pt-rBR/strings_logs.xml +++ b/manager/src/main/res/values-pt-rBR/strings_logs.xml @@ -57,7 +57,6 @@ O daemon se recusou a começar um registro novo Cancelar - vector-logs-%1$s.zip Reunindo registros, tombstones e dmesg… Relatório de erro salvo Não foi possível salvar o relatório de erro diff --git a/manager/src/main/res/values-ru/strings_logs.xml b/manager/src/main/res/values-ru/strings_logs.xml index ef5f02770..029442ad8 100644 --- a/manager/src/main/res/values-ru/strings_logs.xml +++ b/manager/src/main/res/values-ru/strings_logs.xml @@ -53,7 +53,6 @@ Новый журнал начат Служба отказалась начать новый журнал Отмена - vector-logs-%1$s.zip Сбор журналов, tombstone и dmesg… Отчёт об ошибке сохранён Не удалось сохранить отчёт об ошибке diff --git a/manager/src/main/res/values-tr/strings_logs.xml b/manager/src/main/res/values-tr/strings_logs.xml index 793cfecf3..acbe87a15 100644 --- a/manager/src/main/res/values-tr/strings_logs.xml +++ b/manager/src/main/res/values-tr/strings_logs.xml @@ -57,7 +57,6 @@ Art alan süreci yeni günlük başlatmayı reddetti Vazgeç - vector-logs-%1$s.zip Günlükler, tombstone\'lar ve dmesg toplanıyor… Hata raporu kaydedildi Hata raporu kaydedilemedi diff --git a/manager/src/main/res/values-uk/strings_logs.xml b/manager/src/main/res/values-uk/strings_logs.xml index a02226db1..604d37cf6 100644 --- a/manager/src/main/res/values-uk/strings_logs.xml +++ b/manager/src/main/res/values-uk/strings_logs.xml @@ -63,7 +63,6 @@ Служба відмовилася починати новий журнал Скасувати - vector-logs-%1$s.zip Збирання журналів, tombstone і dmesg… Звіт про помилку збережено Не вдалося зберегти звіт про помилку diff --git a/manager/src/main/res/values-vi/strings_logs.xml b/manager/src/main/res/values-vi/strings_logs.xml index 1802e40e7..40f85239e 100644 --- a/manager/src/main/res/values-vi/strings_logs.xml +++ b/manager/src/main/res/values-vi/strings_logs.xml @@ -54,7 +54,6 @@ Tiến trình nền từ chối bắt đầu nhật ký mới Huỷ - vector-logs-%1$s.zip Đang thu thập nhật ký, tombstone và dmesg… Đã lưu báo cáo lỗi Không lưu được báo cáo lỗi diff --git a/manager/src/main/res/values-zh-rCN/strings_logs.xml b/manager/src/main/res/values-zh-rCN/strings_logs.xml index 37c02e6cd..5fb01539d 100644 --- a/manager/src/main/res/values-zh-rCN/strings_logs.xml +++ b/manager/src/main/res/values-zh-rCN/strings_logs.xml @@ -54,7 +54,6 @@ 守护进程拒绝开始新的日志 取消 - vector-logs-%1$s.zip 正在收集日志、tombstone 和 dmesg… 问题报告已保存 无法保存问题报告 diff --git a/manager/src/main/res/values-zh-rTW/strings_logs.xml b/manager/src/main/res/values-zh-rTW/strings_logs.xml index 52436f609..e24218441 100644 --- a/manager/src/main/res/values-zh-rTW/strings_logs.xml +++ b/manager/src/main/res/values-zh-rTW/strings_logs.xml @@ -54,7 +54,6 @@ 常駐程式拒絕開始新的日誌 取消 - vector-logs-%1$s.zip 正在收集日誌、tombstone 與 dmesg… 問題報告已儲存 無法儲存問題報告 diff --git a/manager/src/main/res/values/strings_logs.xml b/manager/src/main/res/values/strings_logs.xml index e7bc55865..451fe83ff 100644 --- a/manager/src/main/res/values/strings_logs.xml +++ b/manager/src/main/res/values/strings_logs.xml @@ -75,7 +75,6 @@ Cancel - vector-logs-%1$s.zip Collecting logs, tombstones and dmesg… Bug report saved Could not save the bug report diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts index 60726a312..dcfe1846c 100644 --- a/zygisk/build.gradle.kts +++ b/zygisk/build.gradle.kts @@ -1,7 +1,20 @@ +import com.android.tools.smali.dexlib2.DexFileFactory +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference import java.security.MessageDigest import org.apache.commons.codec.binary.Hex import org.apache.tools.ant.filters.ReplaceTokens +// Reading the optimised DEX needs a DEX reader; see checkXResourcesIsolation below. +buildscript { + repositories { mavenCentral() } + dependencies { classpath(libs.smali.dexlib2) } +} + plugins { alias(libs.plugins.agp.app) alias(libs.plugins.ktfmt) @@ -225,3 +238,167 @@ androidComponents { evaluationDependsOn(":manager") evaluationDependsOn(":daemon") + +/** + * Fails the build when a type whose super class is generated at runtime leaks into a class that + * does not deal with resources. + * + * Some framework types extend super classes that no DEX contains: they are built on the device, so + * that they can inherit from whichever platform classes it actually provides. A type whose super + * class does not yet exist cannot be resolved, and the runtime records the failure instead of + * retrying it, so the damage outlives the window that caused it. The fragility is therefore + * transitive — every class naming such a type acquires it, and is unusable in the same window and + * unusable afterwards. + * + * The reach is not visible in the source. A whole-program optimiser lowers each lambda to a class + * of its own and afterwards merges classes of the same shape, so a lambda written in a fragile + * class can come to share its class with lambdas from anywhere in the program; the reference then + * sits inside a class that arbitrary code instantiates. Keep rules do not help, because they govern + * the classes one writes, not the ones an optimiser invents. + * + * So the invariant is checked where it is actually decided — in the optimised DEX. Every class that + * mentions a guarded type must be one of the classes that legitimately implements it. Names are + * recovered through the mapping file, since by this point the interesting classes are called `k` + * and `n0`. Adding to [resourceOwners] is a deliberate act: it means a class has been given a + * reference that only resolves once the device has generated the super class. + */ +val guardedTypes = + listOf("Landroid/content/res/XResources;", "Landroid/content/res/XResources\$XTypedArray;") + +/** The classes allowed to name a guarded type, by their original (pre-optimiser) names. */ +val resourceOwners = + listOf( + "android.content.res.", + "de.robv.android.xposed.XposedInit", + "de.robv.android.xposed.callbacks.XC_InitPackageResources", + "de.robv.android.xposed.callbacks.XC_LayoutInflated", + "org.matrix.vector.legacy.LegacyDelegateImpl\$ResourceProxy", + ) + +/** Every type named anywhere in [cls] — its shape, its members and its instruction stream. */ +fun typesNamedBy(cls: ClassDef): Set { + val named = mutableSetOf() + cls.superclass?.let(named::add) + named += cls.interfaces + cls.fields.forEach { named += it.type } + cls.methods.forEach { method -> + named += method.returnType + method.parameterTypes.forEach { named += it.toString() } + method.implementation?.instructions?.forEach { instruction -> + val reference = + runCatching { (instruction as? ReferenceInstruction)?.reference }.getOrNull() + when (reference) { + is TypeReference -> named += reference.type + is FieldReference -> { + named += reference.definingClass + named += reference.type + } + is MethodReference -> { + named += reference.definingClass + named += reference.returnType + reference.parameterTypes.forEach { named += it.toString() } + } + else -> Unit + } + } + } + return named +} + +/** Residual class name -> original, read from the optimiser's mapping file. */ +fun originalNames(mapping: File): Map = + if (!mapping.exists()) emptyMap() + else + mapping + .readLines() + .filter { !it.startsWith(" ") && it.endsWith(":") && it.contains(" -> ") } + .associate { line -> + val (original, residual) = line.dropLast(1).split(" -> ", limit = 2) + residual to original + } + +androidComponents { + onVariants(selector().withBuildType("release")) { variant -> + val variantCapped = variant.name.replaceFirstChar { it.uppercase() } + val dexDir = + layout.buildDirectory.dir( + "intermediates/dex/${variant.name}/minify${variantCapped}WithR8" + ) + val mappingFile = layout.buildDirectory.file("outputs/mapping/${variant.name}/mapping.txt") + + val check = + tasks.register("checkXResourcesIsolation$variantCapped") { + group = "verification" + description = + "Rejects references to runtime-super-classed types from unrelated classes." + dependsOn("minify${variantCapped}WithR8") + inputs.dir(dexDir) + inputs.file(mappingFile).optional(true) + outputs.file( + layout.buildDirectory.file("reports/xresources-isolation-${variant.name}.txt") + ) + + doLast { + val names = originalNames(mappingFile.get().asFile) + val offenders = sortedMapOf>() + var scanned = 0 + + dexDir + .get() + .asFile + .walkTopDown() + .filter { it.extension == "dex" } + .forEach { dex -> + DexFileFactory.loadDexFile(dex, Opcodes.forApi(27)).classes.forEach { + cls -> + scanned++ + val named = typesNamedBy(cls) + val hits = guardedTypes.filter { it in named && it != cls.type } + if (hits.isEmpty()) return@forEach + val residual = + cls.type.removePrefix("L").removeSuffix(";").replace('/', '.') + val original = names[residual] ?: residual + if (resourceOwners.none { original.startsWith(it) }) { + offenders.getOrPut( + if (original == residual) original + else "$original ($residual)" + ) { + mutableSetOf() + } += hits + } + } + } + + if (offenders.isNotEmpty()) { + throw GradleException( + buildString { + appendLine( + "These classes name a type whose super class is generated at runtime:" + ) + offenders.forEach { (owner, types) -> + appendLine(" $owner -> ${types.joinToString()}") + } + appendLine() + append( + "Such a type cannot be resolved until the device has built its " + + "super class, and a failed resolution is permanent, so every " + + "class holding the reference is unusable for the life of the " + + "process. A lambda is the usual way one arrives here: it " + + "becomes a class, and classes of the same shape are merged. " + + "Either remove the reference, or add the class to " + + "resourceOwners in zygisk/build.gradle.kts if it genuinely " + + "implements resource hooking." + ) + } + ) + } + outputs.files.singleFile.apply { + parentFile.mkdirs() + writeText("scanned $scanned classes, no stray references\n") + } + } + } + + tasks.named("zip$variantCapped") { dependsOn(check) } + } +}