Pier Angelo Vendrame pushed to branch tor-browser-152.0a1-16.0-2 at The Tor Project / Applications / Tor Browser Commits: 5c903fac by Pier Angelo Vendrame at 2026-07-01T08:31:43+02:00 BB 45086: Replace omni.ja URI parsing in GetApkURI with a direct JNI call - - - - - 63c810a9 by Beatriz Rizental at 2026-07-01T08:31:43+02:00 TB 45086: [android] Repack omni.ja file with LZMA - - - - - 16 changed files: - mobile/android/fenix/app/build.gradle - mobile/android/geckoview/build.gradle - + mobile/android/geckoview/omnijar_repack.sh - mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java - mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java - + mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XzExtractor.java - + mobile/android/geckoview/src/test/assets/empty-hash.sha256 - + mobile/android/geckoview/src/test/assets/empty-hash.xz - + mobile/android/geckoview/src/test/assets/no-hash.xz - + mobile/android/geckoview/src/test/assets/test.sha256 - + mobile/android/geckoview/src/test/assets/test.xz - + mobile/android/geckoview/src/test/java/org/mozilla/gecko/util/XzExtractorTest.java - mobile/android/gradle/with_gecko_binaries.gradle - netwerk/protocol/res/nsResProtocolHandler.cpp - python/mozbuild/mozbuild/action/fat_aar.py - python/mozbuild/mozpack/packager/unpack.py Changes: ===================================== mobile/android/fenix/app/build.gradle ===================================== @@ -197,7 +197,7 @@ android { // manifest.template.json is converted to manifest.json at build time. // No need to package the template in the APK. - ignoreAssetsPattern = "manifest.template.json" + ignoreAssetsPattern = "manifest.template.json:omni.ja" } testOptions { ===================================== mobile/android/geckoview/build.gradle ===================================== @@ -82,10 +82,20 @@ android { buildConfigField 'boolean', 'MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS', mozconfig.substs.MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS ? 'true' : 'false'; } + aaptOptions { + noCompress 'xz' + } + lintOptions { abortOnError = false } + testOptions { + unitTests { + includeAndroidResources = true + } + } + sourceSets { main { java { @@ -194,6 +204,7 @@ dependencies { implementation libs.androidx.lifecycle.common implementation libs.androidx.lifecycle.process implementation libs.play.services.fido + implementation "org.tukaani:xz:1.12" implementation "org.yaml:snakeyaml:2.2" if (mozconfig.substs.MOZ_ANDROID_HLS_SUPPORT) { ===================================== mobile/android/geckoview/omnijar_repack.sh ===================================== @@ -0,0 +1,12 @@ +#!/bin/sh +# Repack a .ja (ZIP) stored uncompressed, compress with XZ, write SHA-256 sidecar. +# Usage: omnijar_repack.sh <src> <dst-file> <dst-tag> +set -e +src="$1" dst_file="$2" dst_tag="$3" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +cp "$src" "$tmp/omni.ja" +# Repack all entries as ZIP_STORED (strip per-entry deflate) +( cd "$tmp" && unzip -q omni.ja -d unpacked && cd unpacked && zip -q -0 -r ../repacked.ja . ) +xz -9e -k -c "$tmp/repacked.ja" > "$dst_file" +sha256sum -b "$tmp/repacked.ja" | awk '{print $1}' > "$dst_tag" ===================================== mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java ===================================== @@ -1761,6 +1761,11 @@ public class GeckoAppShell { return id == 0 ? info.nonLocalizedLabel.toString() : context.getString(id); } + @WrapForJNI + public static String getApkPath() { + return getApplicationContext().getPackageResourcePath(); + } + @WrapForJNI(calledFrom = "gecko") private static int getMemoryUsage(final String stateName) { final Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); ===================================== mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java ===================================== @@ -19,6 +19,8 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; @@ -31,6 +33,7 @@ import org.mozilla.gecko.annotation.WrapForJNI; import org.mozilla.gecko.mozglue.GeckoLoader; import org.mozilla.gecko.process.MemoryController; import org.mozilla.gecko.util.GeckoBundle; +import org.mozilla.gecko.util.XzExtractor; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.geckoview.BuildConfig; import org.mozilla.geckoview.GeckoResult; @@ -329,6 +332,15 @@ public class GeckoThread extends Thread { loadGeckoLibs(context); } + private static File extractOmnijar(final Context context) throws IOException { + return new XzExtractor( + context, + "omni.ja", + context.getNoBackupFilesDir(), + "omni.ja") + .extract(); + } + private String[] getMainProcessArgs() { final Context context = GeckoAppShell.getApplicationContext(); final ArrayList<String> args = new ArrayList<>(); @@ -337,8 +349,13 @@ public class GeckoThread extends Thread { args.add(context.getPackageName()); if (!mInitInfo.xpcshell) { - args.add("-greomni"); - args.add(context.getPackageResourcePath()); + try { + args.add("-greomni"); + String greomni = extractOmnijar(context).getAbsolutePath(); + args.add(greomni); + } catch (final IOException e) { + throw new RuntimeException("Failed to extract omnijar", e); + } } if (mInitInfo.args != null) { ===================================== mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XzExtractor.java ===================================== @@ -0,0 +1,75 @@ +package org.mozilla.gecko.util; + +import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import org.tukaani.xz.XZInputStream; + +public class XzExtractor { + private static final String TAG = "XzExtractor"; + private final Context mContext; + private final String mSource; + private String mSourceHash; + private final File mTarget; + private final File mTargetHash; + + public XzExtractor(Context context, String source, File targetDir, String destination) { + mContext = context; + mSource = source; + mTarget = new File(targetDir, destination); + mTargetHash = new File(targetDir, destination + ".sha256"); + } + + public File extract() throws IOException { + Log.d(TAG, "Extracting " + mSource + " -> " + mTarget.getAbsolutePath()); + readSourceTag(); + if (checkExisting()) { + Log.d(TAG, mSource + ": up to date, skipping extraction"); + } else { + Log.d(TAG, mSource + ": extracting from assets"); + extractFromAssets(); + Log.d(TAG, mSource + ": extraction complete"); + } + return mTarget; + } + + private void readSourceTag() throws IOException { + // We use sha256 because we need a unique deterministic string + // to verify if current resources are up to date or need to be + // re-extracted. THIS IS NOT AN INTEGRITY CHECK. + final String asset = mSource + ".sha256"; + final Context context = mContext; + try (InputStream in = context.getAssets().open(asset, AssetManager.ACCESS_BUFFER); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + mSourceHash = reader.readLine(); + } + if (mSourceHash == null || mSourceHash.isEmpty()) { + throw new IOException(asset + " is empty"); + } + } + + private boolean checkExisting() throws IOException { + if (!mTarget.exists() || !mTargetHash.exists()) { + return false; + } + String destHash = new String(Files.readAllBytes(mTargetHash.toPath()), StandardCharsets.UTF_8).trim(); + return destHash.equals(mSourceHash); + } + + private void extractFromAssets() throws IOException { + final Context context = mContext; + try (InputStream assetIn = context.getAssets().open(mSource + ".xz"); + XZInputStream xzIn = new XZInputStream(assetIn)) { + Files.copy(xzIn, mTarget.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + Files.write(mTargetHash.toPath(), mSourceHash.getBytes(StandardCharsets.UTF_8)); + } +} ===================================== mobile/android/geckoview/src/test/assets/empty-hash.sha256 ===================================== ===================================== mobile/android/geckoview/src/test/assets/empty-hash.xz ===================================== Binary files /dev/null and b/mobile/android/geckoview/src/test/assets/empty-hash.xz differ ===================================== mobile/android/geckoview/src/test/assets/no-hash.xz ===================================== Binary files /dev/null and b/mobile/android/geckoview/src/test/assets/no-hash.xz differ ===================================== mobile/android/geckoview/src/test/assets/test.sha256 ===================================== @@ -0,0 +1 @@ +d05557c2592a0b2f4d5553ff10a810168755dd98adfabae60fc055273819fd06 ===================================== mobile/android/geckoview/src/test/assets/test.xz ===================================== Binary files /dev/null and b/mobile/android/geckoview/src/test/assets/test.xz differ ===================================== mobile/android/geckoview/src/test/java/org/mozilla/gecko/util/XzExtractorTest.java ===================================== @@ -0,0 +1,112 @@ +package org.mozilla.gecko.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.test.suitebuilder.annotation.SmallTest; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@SmallTest +public class XzExtractorTest { + private Context mContext; + private File mTargetDir; + + private String mTestFileContents = "#!/bin/sh\necho hello world"; + + @Before + public void setUp() throws IOException { + mContext = RuntimeEnvironment.getApplication(); + mTargetDir = Files.createTempDirectory("xz-extractor-test").toFile(); + } + + @After + public void tearDown() { + deleteRecursively(mTargetDir); + } + + @Test + public void freshExtraction() throws IOException, InterruptedException { + XzExtractor extractor = new XzExtractor(mContext, "test", mTargetDir, "test"); + File extracted = extractor.extract(); + + assertTrue(extracted.exists()); + assertTrue(new File(mTargetDir, "test.sha256").exists()); + } + + @Test + public void upToDateSkipsExtraction() throws IOException { + XzExtractor extractor = new XzExtractor(mContext, "test", mTargetDir, "test"); + File extracted = extractor.extract(); + assertEquals( + mTestFileContents, + new String(Files.readAllBytes(extracted.toPath()), StandardCharsets.UTF_8).trim()); + + Files.write(extracted.toPath(), "tampered".getBytes(StandardCharsets.UTF_8)); + extractor.extract(); + + assertEquals( + "tampered", + new String(Files.readAllBytes(extracted.toPath()), StandardCharsets.UTF_8)); + } + + @Test + public void staleHashTriggersReExtraction() throws IOException, InterruptedException { + XzExtractor extractor = new XzExtractor(mContext, "test", mTargetDir, "test"); + File extracted = extractor.extract(); + File hashFile = new File(mTargetDir, "test.sha256"); + + assertEquals( + mTestFileContents, + new String(Files.readAllBytes(extracted.toPath()), StandardCharsets.UTF_8).trim()); + + Files.write(hashFile.toPath(), "new-hash".getBytes(StandardCharsets.UTF_8)); + Files.write(extracted.toPath(), "tampered".getBytes(StandardCharsets.UTF_8)); + + extractor.extract(); + assertEquals( + mTestFileContents, + new String(Files.readAllBytes(extracted.toPath()), StandardCharsets.UTF_8).trim()); + } + + @Test + public void missingAssetThrows() { + XzExtractor extractor = new XzExtractor(mContext, "nonexistent", mTargetDir, "test"); + assertThrows(IOException.class, extractor::extract); + } + + @Test + public void missingHashFileThrows() { + XzExtractor extractor = new XzExtractor(mContext, "no-hash", mTargetDir, "test"); + assertThrows(IOException.class, extractor::extract); + } + + @Test + public void emptyHashFileThrows() { + XzExtractor extractor = new XzExtractor(mContext, "empty-hash", mTargetDir, "test"); + assertThrows(IOException.class, extractor::extract); + } + + private void deleteRecursively(final File file) { + if (file.isDirectory()) { + final File[] children = file.listFiles(); + if (children != null) { + for (final File child : children) { + deleteRecursively(child); + } + } + } + file.delete(); + } +} ===================================== mobile/android/gradle/with_gecko_binaries.gradle ===================================== @@ -23,10 +23,27 @@ ext.configureVariantWithGeckoBinaries = { variant -> // the moz.build system and should never be re-entrant in this way. def assetGenTask = tasks.findByName("generate${variant.name.capitalize()}Assets") def jniLibFoldersTask = tasks.findByName("merge${variant.name.capitalize()}JniLibFolders") + + def omnijarName = "omni.ja" + def assetsDir = "${topobjdir}/dist/geckoview/assets" + def compressOmnijarTask = tasks.register("compressOmnijar${variant.name.capitalize()}", Exec) { + inputs.file("${assetsDir}/${omnijarName}") + outputs.files( + "${assetsDir}/${omnijarName}.xz", + "${assetsDir}/${omnijarName}.sha256" + ) + commandLine "sh", + "${topsrcdir}/mobile/android/geckoview/omnijar_repack.sh", + "${assetsDir}/${omnijarName}", + "${assetsDir}/${omnijarName}.xz", + "${assetsDir}/${omnijarName}.sha256" + } + if (!mozconfig.substs.MOZILLA_OFFICIAL && !mozconfig.substs.ENABLE_MOZSEARCH_PLUGIN) { - assetGenTask.dependsOn rootProject.machStagePackage + compressOmnijarTask.configure { dependsOn rootProject.machStagePackage } jniLibFoldersTask.dependsOn rootProject.machStagePackage } + assetGenTask.dependsOn compressOmnijarTask } } ===================================== netwerk/protocol/res/nsResProtocolHandler.cpp ===================================== @@ -14,6 +14,10 @@ #include "mozilla/Omnijar.h" +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/java/GeckoAppShellWrappers.h" +#endif + using mozilla::LogLevel; using mozilla::dom::ContentParent; @@ -68,29 +72,12 @@ nsresult nsResProtocolHandler::Init() { #ifdef ANDROID nsresult nsResProtocolHandler::GetApkURI(nsACString& aResult) { - nsCString::const_iterator start, iter; - mGREURI.BeginReading(start); - mGREURI.EndReading(iter); - nsCString::const_iterator start_iter = start; - - // This is like jar:jar:file://path/to/apk/base.apk!/path/to/omni.ja!/ - bool found = FindInReadable("!/"_ns, start_iter, iter); - NS_ENSURE_TRUE(found, NS_ERROR_UNEXPECTED); - - // like jar:jar:file://path/to/apk/base.apk!/ - const nsDependentCSubstring& withoutPath = Substring(start, iter); - NS_ENSURE_TRUE(withoutPath.Length() >= 4, NS_ERROR_UNEXPECTED); - - // Let's make sure we're removing what we expect to remove - NS_ENSURE_TRUE(Substring(withoutPath, 0, 4).EqualsLiteral("jar:"), - NS_ERROR_UNEXPECTED); - - // like jar:file://path/to/apk/base.apk!/ - aResult = ToNewCString(Substring(withoutPath, 4)); - - // Remove the trailing / - NS_ENSURE_TRUE(aResult.Length() >= 1, NS_ERROR_UNEXPECTED); - aResult.Truncate(aResult.Length() - 1); + mozilla::jni::String::LocalRef path = + mozilla::java::GeckoAppShell::GetApkPath(); + if (!path) { + return NS_ERROR_UNEXPECTED; + } + aResult = "jar:file://"_ns + path->ToCString() + "!"_ns; return NS_OK; } #endif ===================================== python/mozbuild/mozbuild/action/fat_aar.py ===================================== @@ -90,7 +90,7 @@ def fat_aar(distdir, zip_paths, no_process=False, no_compatibility_check=False): jar_finder = JarFinder( aar_file.file.filename, JarReader(fileobj=aar_file.open()) ) - for path, fileobj in UnpackFinder(jar_finder): + for path, fileobj in UnpackFinder(jar_finder, omnijar_name="assets/omni.ja.xz"): # Native libraries go straight through. if mozpath.match(path, "jni/**"): copier.add(path, fileobj) @@ -130,6 +130,7 @@ def fat_aar(distdir, zip_paths, no_process=False, no_compatibility_check=False): "**/*.ftl", "**/*.dtd", "**/*.properties", + "assets/omni.ja.sha256", } not_allowed = OrderedDict() ===================================== python/mozbuild/mozpack/packager/unpack.py ===================================== @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import codecs +import lzma from urllib.parse import urlparse import mozpack.path as mozpath @@ -143,7 +144,11 @@ class UnpackFinder(BaseFinder): Return a JarReader for the given BaseFile instance, keeping a log of the preloaded entries it has. """ - jar = JarReader(fileobj=file.open()) + if path.endswith(".xz"): + f = lzma.open(file.open()) + else: + f = file.open() + jar = JarReader(fileobj=f) self.compressed = max(self.compressed, jar.compression) if jar.last_preloaded: jarlog = list(jar.entries.keys()) @@ -158,8 +163,17 @@ class UnpackFinder(BaseFinder): """ Return whether the given BaseFile looks like a ZIP/Jar. """ + + def check_header(header): + return len(header) == 8 and (header[0:2] == b"PK" or header[4:6] == b"PK") + header = file.open().read(8) - return len(header) == 8 and (header[0:2] == b"PK" or header[4:6] == b"PK") + if check_header(header): + return True + if header[0:6] == b"\xfd7zXZ\x00": + with lzma.open(file.open()) as f: + return check_header(f.read(8)) + return False def _unjarize(self, entry, relpath): """ View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/5b410a2... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/5b410a2... You're receiving this email because of your account on gitlab.torproject.org. Manage all notifications: https://gitlab.torproject.org/-/profile/notifications | Help: https://gitlab.torproject.org/help
participants (1)
-
Pier Angelo Vendrame (@pierov)