Skip to content

Reproducible Builds are broken #13565

Open
@obfusk

Description

@obfusk

Bug description

Building an APK using the provided instructions and docker container results in APKs with differences in classes*.dex (and baseline.profm) compared to the official APKs published on Google Play (play flavour) and updates.signal.org (website flavour).

NB: I have found zero evidence of any kind of compromise. Some differences are yet unexplained but everything I found seems to be benign. I am disappointed that Reproducible Builds have been broken for months but I have zero reason to doubt Signal's security in any way.

Steps to reproduce

Actual result:

APKs with differences in classes*.dex (and baseline.profm).

(Also baseline.prof but that's a result of it containing a checksum for the differing .dex files.)

Expected result:

Identical APKs (except for the APK/JAR signature and possibly the baseline.profm).

Details

classes*.dex: non-deterministic order in Project.languageList() in app/build.gradle.kts

Introduced by ac5d0bf on Dec 4, 2023 and apparently unnoticed since.

The old app/build.gradle code used .sort():

def autoResConfig() {
    def files = []
    allStringsResourceFiles { f ->
        files.add(f.parentFile.name)
    }
    ['en'] + files.collect { f -> f =~ /^values-([a-z]{2,3}(-r[A-Z]{2})?)$/ }
            .findAll { matcher -> matcher.find() }
            .collect { matcher -> matcher.group(1) }
            .sort()
}

The new app/build.gradle.kts code is missing .sorted(), resulting in a non-deterministic order as "The order of the files in a FileTree is not stable, even on a single computer.".

fun Project.languageList(): List<String> {
  return fileTree("src/main/res") { include("**/strings.xml") }
    .map { stringFile -> stringFile.parentFile.name }
    .map { valuesFolderName -> valuesFolderName.replace("values-", "") }
    .filter { valuesFolderName -> valuesFolderName != "values" }
    .map { languageCode -> languageCode.replace("-r", "_") }
    .distinct() + "en"
}

Should be e.g.:

fun Project.languageList(): List<String> {
  return fileTree("src/main/res") { include("**/strings.xml") }
    .map { stringFile -> stringFile.parentFile.name }
    .map { valuesFolderName -> valuesFolderName.replace("values-", "") }
    .filter { valuesFolderName -> valuesFolderName != "values" }
    .map { languageCode -> languageCode.replace("-r", "_") }
    .distinct().sorted() + "en"
}

Relevant part of dexdump diff:

 | org.thoughtcrime.securesms.BuildConfig.<clinit>:()V
-|: const-string/jumbo v0, "uk"
-|: const-string/jumbo v1, "mk"
-|: const-string v2, "ar"
-|: const-string v3, "ko"
-|: const-string v4, "da"
-|: const-string v5, "bn"
-|: const-string/jumbo v6, "nb"
-|: const-string/jumbo v7, "lt"
-|: const-string v8, "bs"
-|: const-string v9, "gu"
-|: const-string/jumbo v10, "yue"
-|: const-string/jumbo v11, "tr"
-|: const-string/jumbo v12, "te"
-|: const-string v13, "fi"
-|: const-string/jumbo v14, "ro"
-|: const-string v15, "hi"
-|: const-string v16, "ja"
-|: const-string v17, "bg"
-|: const-string v18, "kn"
-|: const-string v19, "km"
-|: const-string v20, "af"
-|: const-string v21, "et"
-|: const-string v22, "in"
-|: const-string/jumbo v23, "sq"
-|: const-string/jumbo v24, "zh_HK"
-|: const-string v25, "fr"
-|: const-string v26, "kk"
-|: const-string/jumbo v27, "ms"
-|: const-string v28, "cs"
-|: const-string v29, "ca"
-|: const-string v30, "hu"
-|: const-string/jumbo v31, "ml"
-|: const-string v32, "el"
-|: const-string/jumbo v33, "ta"
-|: const-string/jumbo v34, "pa"
-|: const-string/jumbo v35, "th"
-|: const-string v36, "az"
-|: const-string/jumbo v37, "ug"
-|: const-string/jumbo v38, "nl"
-|: const-string/jumbo v39, "sw"
-|: const-string v40, "it"
-|: const-string v41, "de"
-|: const-string/jumbo v42, "vi"
+|: const-string v0, "es"
+|: const-string v1, "gu"
+|: const-string v2, "bn"
+|: const-string/jumbo v3, "sv"
+|: const-string v4, "ca"
+|: const-string/jumbo v5, "pt_BR"
+|: const-string/jumbo v6, "pt"
+|: const-string v7, "bs"
+|: const-string/jumbo v8, "zh_HK"
+|: const-string v9, "fa"
+|: const-string v10, "da"
+|: const-string/jumbo v11, "ml"
+|: const-string/jumbo v12, "sq"
+|: const-string/jumbo v13, "tr"
+|: const-string v14, "ja"
+|: const-string/jumbo v15, "zh_CN"
+|: const-string v16, "az"
+|: const-string/jumbo v17, "nl"
+|: const-string v18, "el"
+|: const-string v19, "et"
+|: const-string/jumbo v20, "pa"
+|: const-string/jumbo v21, "sk"
+|: const-string v22, "ar"
+|: const-string/jumbo v23, "sr"
+|: const-string v24, "kk"
+|: const-string/jumbo v25, "my"
+|: const-string/jumbo v26, "nb"
+|: const-string/jumbo v27, "vi"
+|: const-string/jumbo v28, "pl"
+|: const-string/jumbo v29, "ro"
+|: const-string/jumbo v30, "lt"
+|: const-string/jumbo v31, "ru"
+|: const-string v32, "ky"
+|: const-string/jumbo v33, "uk"
+|: const-string v34, "it"
+|: const-string v35, "ko"
+|: const-string v36, "cs"
+|: const-string v37, "fr"
+|: const-string/jumbo v38, "mk"
+|: const-string/jumbo v39, "ug"
+|: const-string/jumbo v40, "lv"
+|: const-string v41, "iw"
+|: const-string v42, "hi"
 |: const-string v43, "ga"
-|: const-string/jumbo v44, "zh_CN"
+|: const-string v44, "fi"
 |: const-string v45, "hr"
-|: const-string v46, "gl"
-|: const-string/jumbo v47, "tl"
-|: const-string/jumbo v48, "lv"
-|: const-string/jumbo v49, "my"
-|: const-string/jumbo v50, "ru"
-|: const-string v51, "ky"
-|: const-string v52, "es"
-|: const-string/jumbo v53, "sk"
-|: const-string/jumbo v54, "sv"
-|: const-string v55, "fa"
-|: const-string/jumbo v56, "sr"
-|: const-string v57, "ka"
-|: const-string v58, "eu"
-|: const-string/jumbo v59, "sl"
-|: const-string/jumbo v60, "ur"
-|: const-string/jumbo v61, "pl"
-|: const-string/jumbo v62, "pt"
-|: const-string/jumbo v63, "mr"
-|: const-string v64, "iw"
-|: const-string/jumbo v65, "zh_TW"
-|: const-string/jumbo v66, "pt_BR"
+|: const-string v46, "ka"
+|: const-string/jumbo v47, "yue"
+|: const-string v48, "hu"
+|: const-string v49, "af"
+|: const-string/jumbo v50, "th"
+|: const-string v51, "km"
+|: const-string/jumbo v52, "tl"
+|: const-string/jumbo v53, "zh_TW"
+|: const-string v54, "de"
+|: const-string v55, "eu"
+|: const-string/jumbo v56, "sl"
+|: const-string v57, "kn"
+|: const-string v58, "bg"
+|: const-string/jumbo v59, "ms"
+|: const-string/jumbo v60, "ta"
+|: const-string/jumbo v61, "te"
+|: const-string/jumbo v62, "mr"
+|: const-string v63, "gl"
+|: const-string/jumbo v64, "sw"
+|: const-string/jumbo v65, "ur"
+|: const-string v66, "in"
 |: const-string v67, "en"
 |: filled-new-array/range {v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63, v64, v65, v66, v67}, [Ljava/lang/String;

classes*.dex: unexplained differences

These differences occur in several classes*.dex files; they are identical for both play and website v7.6.2 ("variant 2") but different for play v7.7.1 ("variant 1").

Variant 1 (play v7.7.1):

   VISIBILITY_SYSTEM Ldalvik/annotation/MemberClasses; value={ Lorg/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragmentDirections$ActionRestoreFromBacakupFragmentToMoreOptions; }
@@ -570943,6 +570943,23 @@
         0x0000 - 0x0004 reg=0 this Lorg/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragmentDirections; 
 
     #1              : (in Lorg/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragmentDirections;)
+      name          : 'actionRestartToWelcomeFragment'
+      type          : '()Landroidx/navigation/NavDirections;'
+      access        : 0x0009 (PUBLIC STATIC)
+      code          -
+      registers     : 1
+      ins           : 0
+      outs          : 0
+      insns size    : 5 16-bit code units
+| org.thoughtcrime.securesms.backup.v2.ui.restore.RestoreFromBackupFragmentDirections.actionRestartToWelcomeFragment:()Landroidx/navigation/NavDirections;
+|: invoke-static {}, Lorg/thoughtcrime/securesms/SignupDirections;.actionRestartToWelcomeFragment:()Landroidx/navigation/NavDirections;
+|: move-result-object v0
+|: return-object v0
+      catches       : (none)
+      positions     : 
+      locals        : 
+
+    #2              : (in Lorg/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragmentDirections;)
       name          : 'actionRestoreFromBacakupFragmentToMoreOptions'
       type          : '(Lorg/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsMode;)Lorg/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragmentDirections$ActionRestoreFromBacakupFragmentToMoreOptions;'
       access        : 0x0009 (PUBLIC STATIC)

Variant 2 (play and website v7.6.2):

     #1              : (in Lorg/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragmentDirections;)
-      name          : 'actionRestartToWelcomeFragment'
+      name          : 'actionRestartToWelcomeV2Fragment'
       type          : '()Landroidx/navigation/NavDirections;'
       access        : 0x0009 (PUBLIC STATIC)
       code          -
@@ -569000,8 +569000,8 @@
       ins           : 0
       outs          : 0
       insns size    : 5 16-bit code units
-| org.thoughtcrime.securesms.backup.v2.ui.restore.RestoreFromBackupFragmentDirections.actionRestartToWelcomeFragment:()Landroidx/navigation/NavDirections;
-|: invoke-static {}, Lorg/thoughtcrime/securesms/SignupDirections;.actionRestartToWelcomeFragment:()Landroidx/navigation/NavDirections;
+| org.thoughtcrime.securesms.backup.v2.ui.restore.RestoreFromBackupFragmentDirections.actionRestartToWelcomeV2Fragment:()Landroidx/navigation/NavDirections;
+|: invoke-static {}, Lorg/thoughtcrime/securesms/SignupV2Directions;.actionRestartToWelcomeV2Fragment:()Landroidx/navigation/NavDirections;
 |: move-result-object v0
 |: return-object v0
       catches       : (none)

baseline.profm

The assets/dexopt/baseline.profm file is generated non-deterministically; both a workaround and a proper fix have been available for over a year, but instead of using either apkdiff.py simply skips this file.

Upstream bug (fixed in AGP 8.1.0-alpha03):

Analysis:

Workaround:

Workaround used by Threema (until no longer needed because AGP was updated to a version with the fix):

Workaround used by Tor Browser:

apkdiff.py

Even without the reproducibility issues outlined above, use of apkdiff.py instead of producing a bit-by-bit identical APK file does not meet the definition of "Reproducible Build" as used by reproducible-builds.org.

See http://reproducible-builds.org/reports/2022-12/#android-news.

Screenshots

n/a

Device info

n/a

Link to debug log

n/a

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions