From 188eb8fd06481577cffbf19bfecec6990d7b94fc Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 01/35] fixup! support for per-app dynamic code loading restrictions --- .../ext/settings/app/AswRestrictMemoryDynCodeLoading.java | 2 +- .../ext/settings/app/AswRestrictStorageDynCodeLoading.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java b/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java index 2dd6e8da635ee..9ee310df68397 100644 --- a/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java +++ b/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java @@ -19,7 +19,7 @@ private AswRestrictMemoryDynCodeLoading() { gosPsFlagNonDefault = GosPackageStateFlag.RESTRICT_MEMORY_DYN_CODE_LOADING_NON_DEFAULT; gosPsFlag = GosPackageStateFlag.RESTRICT_MEMORY_DYN_CODE_LOADING; gosPsFlagSuppressNotif = GosPackageStateFlag.RESTRICT_MEMORY_DYN_CODE_LOADING_SUPPRESS_NOTIF; - compatChangeToDisableHardening = AppCompatProtos.ALLOW_MEMORY_DYN_CODE_EXEC; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_MEMORY_DYN_CODE_LOADING; } private static volatile ArraySet allowedSystemPkgs; diff --git a/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java b/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java index 31417bf422be9..656c07ecb937e 100644 --- a/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java +++ b/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java @@ -22,7 +22,7 @@ private AswRestrictStorageDynCodeLoading() { gosPsFlagNonDefault = GosPackageStateFlag.RESTRICT_STORAGE_DYN_CODE_LOADING_NON_DEFAULT; gosPsFlag = GosPackageStateFlag.RESTRICT_STORAGE_DYN_CODE_LOADING; gosPsFlagSuppressNotif = GosPackageStateFlag.RESTRICT_STORAGE_DYN_CODE_LOADING_SUPPRESS_NOTIF; - compatChangeToDisableHardening = AppCompatProtos.ALLOW_STORAGE_DYN_CODE_EXEC; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_STORAGE_DYN_CODE_LOADING; } private static volatile ArraySet allowedSystemPkgs; From af53bfd3ede883c6c81479c3ccff28c42c8460e5 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 02/35] Revert "match the initial call stack between exec and zygote spawning" This reverts commit 97f2e33da5a50752cde3390c8b3777d0697493ec. --- cmds/app_process/app_main.cpp | 7 +------ core/java/com/android/internal/os/ExecInit.java | 17 +---------------- .../com/android/internal/os/ZygoteInit.java | 10 ---------- 3 files changed, 2 insertions(+), 32 deletions(-) diff --git a/cmds/app_process/app_main.cpp b/cmds/app_process/app_main.cpp index b3c0b45048ad3..9ce4bef5bb6c1 100644 --- a/cmds/app_process/app_main.cpp +++ b/cmds/app_process/app_main.cpp @@ -337,12 +337,7 @@ int main(int argc, char* const argv[]) if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (!className.empty()) { - const char* isExecSpawning = getenv("IS_EXEC_SPAWNED_APP_PROCESS"); - if (isExecSpawning != nullptr && strcmp(isExecSpawning, "1") == 0) { - runtime.start("com.android.internal.os.ZygoteInit", args, false); - } else { - runtime.start("com.android.internal.os.RuntimeInit", args, zygote); - } + runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } else { fprintf(stderr, "Error: no class name or --zygote supplied.\n"); app_usage(); diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java index 8c6afee60ad62..5873a011ffa39 100644 --- a/core/java/com/android/internal/os/ExecInit.java +++ b/core/java/com/android/internal/os/ExecInit.java @@ -20,8 +20,6 @@ public class ExecInit { private ExecInit() { } - public static final String IS_EXEC_SPAWNED_APP_PROCESS = "IS_EXEC_SPAWNED_APP_PROCESS"; - /** * The main function called when starting a runtime application. * @@ -49,18 +47,7 @@ public static void main(String[] args) { Zygote.nativeHandleRuntimeFlags(runtimeFlags); - pendingExecInit = r; - } - - private static Runnable pendingExecInit; - - static Runnable getPendingExecInit() { - Runnable res = pendingExecInit; - if (res == null) { - throw new IllegalStateException("pendingExecInit is null"); - } - pendingExecInit = null; - return res; + r.run(); } /** @@ -98,8 +85,6 @@ public static void execApplication(String niceName, int targetSdkVersion, Os.setenv("DISABLE_HARDENED_MALLOC", "1", true); } - Os.setenv(IS_EXEC_SPAWNED_APP_PROCESS, "1", true); - if ((runtimeFlags & Zygote.ENABLE_COMPAT_VA_39_BIT) != 0) { final int FLAG_COMPAT_VA_39_BIT = 1 << 30; diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 2629255d4e48d..6ae653d4d601a 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -828,16 +828,6 @@ private static Runnable forkSystemServer(String abiList, String socketName, */ @UnsupportedAppUsage public static void main(String[] argv) { - if ("1".equals(Os.getenv(ExecInit.IS_EXEC_SPAWNED_APP_PROCESS))) { - Log.d(TAG, "IS_EXEC_SPAWNED_APP_PROCESS is 1"); - RuntimeInit.main(argv); - // Some apps perform a weak security check by looking at the main thread stack trace. - // Executing the ExecInit.execInit() runnable from here makes the exec spawning call - // stack match the zygote spawning call stack - ExecInit.getPendingExecInit().run(); - return; - } - ZygoteServer zygoteServer = null; // Mark zygote start. This ensures that thread creation will throw From 6211b1204ce63cb1f3626168973f50e8609e086c Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 03/35] Revert "exec spawning: add workaround for late init of ART userfaultfd GC" This reverts commit 4a2b6c9fc498fd80fb2441bd52edd1fef4f5f517. --- core/java/com/android/internal/os/ExecInit.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java index 5873a011ffa39..a24bbf94f698f 100644 --- a/core/java/com/android/internal/os/ExecInit.java +++ b/core/java/com/android/internal/os/ExecInit.java @@ -1,6 +1,5 @@ package com.android.internal.os; -import android.os.Process; import android.os.Trace; import android.system.ErrnoException; import android.system.Os; @@ -141,9 +140,6 @@ private static Runnable execInit(int targetSdkVersion, String[] argv) { // Perform the same initialization that would happen after the Zygote forks. Zygote.nativePreApplicationInit(); - if (Process.isIsolated()) { - System.gc(); - } return RuntimeInit.applicationInit(targetSdkVersion, /*disabledCompatChanges*/ null, argv, classLoader); } } From 8ac7055364f0b5c010ba64f3df2f353ea96200ee Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 04/35] Revert "exec spawning: support disabling hardened_malloc and extended VA space" This reverts commit 3e721e6db1f3e980dffc8aeec93474af06e574e4. --- .../com/android/internal/os/ExecInit.java | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java index a24bbf94f698f..39f08b6a0f15b 100644 --- a/core/java/com/android/internal/os/ExecInit.java +++ b/core/java/com/android/internal/os/ExecInit.java @@ -3,7 +3,6 @@ import android.os.Trace; import android.system.ErrnoException; import android.system.Os; -import android.system.OsConstants; import android.util.Slog; import android.util.TimingsTraceLog; import dalvik.system.VMRuntime; @@ -79,26 +78,7 @@ public static void execApplication(String niceName, int targetSdkVersion, WrapperInit.preserveCapabilities(); try { - if ((runtimeFlags & Zygote.DISABLE_HARDENED_MALLOC) != 0) { - // checked by bionic during early init - Os.setenv("DISABLE_HARDENED_MALLOC", "1", true); - } - - if ((runtimeFlags & Zygote.ENABLE_COMPAT_VA_39_BIT) != 0) { - final int FLAG_COMPAT_VA_39_BIT = 1 << 30; - - int errno = Zygote.execveatWrapper(-1, argv[0], argv, FLAG_COMPAT_VA_39_BIT); - - if (errno == OsConstants.EINVAL) { - // kernel doesn't support FLAG_COMPAT_VA_39_BIT, or a different error that will - // be thrown by execv() anyway - Os.execv(argv[0], argv); - } else { - throw new ErrnoException("execveat", errno); - } - } else { - Os.execv(argv[0], argv); - } + Os.execv(argv[0], argv); } catch (ErrnoException e) { throw new RuntimeException(e); } From 78965f15409be45744e01145653f154fd4598817 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 05/35] Revert "exec spawning: don't close the binder connection when the app crashes" This reverts commit 4d61cd0fb1ed5d732618affcf1cc6487e3452464. --- cmds/app_process/app_main.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmds/app_process/app_main.cpp b/cmds/app_process/app_main.cpp index 9ce4bef5bb6c1..4e41f2c1ac35a 100644 --- a/cmds/app_process/app_main.cpp +++ b/cmds/app_process/app_main.cpp @@ -85,10 +85,8 @@ class AppRuntime : public AndroidRuntime AndroidRuntime* ar = AndroidRuntime::getRuntime(); ar->callMain(mClassName, mClass, mArgs); - if (mClassName != "com.android.internal.os.ExecInit") { - IPCThreadState::self()->stopProcess(); - hardware::IPCThreadState::self()->stopProcess(); - } + IPCThreadState::self()->stopProcess(); + hardware::IPCThreadState::self()->stopProcess(); } virtual void onZygoteInit() From 480df60a51bc5bba1497f6fd260b48d005f88496 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 06/35] Revert "add a wrapper for execveat(2)" This reverts commit 6716125f --- core/java/com/android/internal/os/Zygote.java | 2 - core/jni/Android.bp | 1 - core/jni/ExecStrings.cpp | 74 ------------------- core/jni/ExecStrings.h | 37 ---------- core/jni/com_android_internal_os_Zygote.cpp | 15 ---- 5 files changed, 129 deletions(-) delete mode 100644 core/jni/ExecStrings.cpp delete mode 100644 core/jni/ExecStrings.h diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index c7565e793532e..2aad41dc104e0 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -1564,6 +1564,4 @@ && isCompatChangeEnabled( * @hide */ public static native void nativeHandleRuntimeFlags(int runtimeFlags); - - public static native int execveatWrapper(int dirFd, String filename, String[] argv, int flags); } diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 33226ccee2aa2..25d62ece00d8d 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -282,7 +282,6 @@ cc_defaults_for_libandroid_runtime { "android_tracing_PerfettoDataSource.cpp", "android_tracing_PerfettoDataSourceInstance.cpp", "android_tracing_PerfettoProducer.cpp", - "ExecStrings.cpp", ] + select(release_flag("RELEASE_NATIVE_FRAMEWORK_PROTOTYPE"), { true: [ "android_os_ZygoteProcess.cpp", diff --git a/core/jni/ExecStrings.cpp b/core/jni/ExecStrings.cpp deleted file mode 100644 index 6fdca3a39c63c..0000000000000 --- a/core/jni/ExecStrings.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied from libcore/luni/src/main/native/ExecStrings.cpp, commit cab01ac294bb8ded259851673baa4c6ca226f828 - -#define LOG_TAG "ExecStrings" - -#include "ExecStrings.h" - -#include - -#include - -#include - -ExecStrings::ExecStrings(JNIEnv* env, jobjectArray java_string_array) - : env_(env), java_array_(java_string_array), array_(NULL) { - if (java_array_ == NULL) { - return; - } - - jsize length = env_->GetArrayLength(java_array_); - array_ = new char*[length + 1]; - array_[length] = NULL; - for (jsize i = 0; i < length; ++i) { - ScopedLocalRef java_string(env_, reinterpret_cast(env_->GetObjectArrayElement(java_array_, i))); - // We need to pass these strings to const-unfriendly code. - char* string = const_cast(env_->GetStringUTFChars(java_string.get(), NULL)); - array_[i] = string; - } -} - -ExecStrings::~ExecStrings() { - if (array_ == NULL) { - return; - } - - // Temporarily clear any pending exception so we can clean up. - jthrowable pending_exception = env_->ExceptionOccurred(); - if (pending_exception != NULL) { - env_->ExceptionClear(); - } - - jsize length = env_->GetArrayLength(java_array_); - for (jsize i = 0; i < length; ++i) { - ScopedLocalRef java_string(env_, reinterpret_cast(env_->GetObjectArrayElement(java_array_, i))); - env_->ReleaseStringUTFChars(java_string.get(), array_[i]); - } - delete[] array_; - - // Re-throw any pending exception. - if (pending_exception != NULL) { - if (env_->Throw(pending_exception) < 0) { - ALOGE("Error rethrowing exception!"); - } - } -} - -char** ExecStrings::get() { - return array_; -} diff --git a/core/jni/ExecStrings.h b/core/jni/ExecStrings.h deleted file mode 100644 index 7a161b589741d..0000000000000 --- a/core/jni/ExecStrings.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied from libcore/luni/src/main/native/ExecStrings.h, commit cab01ac294bb8ded259851673baa4c6ca226f828 - -#include "jni.h" - -class ExecStrings { - public: - ExecStrings(JNIEnv* env, jobjectArray java_string_array); - - ~ExecStrings(); - - char** get(); - - private: - JNIEnv* env_; - jobjectArray java_array_; - char** array_; - - // Disallow copy and assignment. - ExecStrings(const ExecStrings&); - void operator=(const ExecStrings&); -}; diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index ac0ca27c881d6..8873236b21dd4 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -87,8 +87,6 @@ #include "nativebridge/native_bridge.h" -#include "ExecStrings.h" - #if defined(__BIONIC__) #include extern "C" void android_reset_stack_guards(); @@ -3058,18 +3056,6 @@ static void nativeHandleRuntimeFlagsWrapper(JNIEnv* env, jclass, jint runtime_fl HandleRuntimeFlags(env, runtime_flags, nullptr, nullptr); } -static jint execveatWrapper(JNIEnv* env, jclass, jint dirFd, jstring javaFilename, jobjectArray javaArgv, jint flags) { - ScopedUtfChars path(env, javaFilename); - if (path.c_str() == NULL) { - return EINVAL; - } - - ExecStrings argv(env, javaArgv); - TEMP_FAILURE_RETRY(execveat(dirFd, path.c_str(), argv.get(), environ, flags)); - // execveat never returns on success - return errno; -} - static const JNINativeMethod gMethods[] = { {"nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" @@ -3123,7 +3109,6 @@ static const JNINativeMethod gMethods[] = { {"nativeAllowFilesOpenedByPreload", "()V", (void*)com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload}, {"nativeHandleRuntimeFlags", "(I)V", (void*)nativeHandleRuntimeFlagsWrapper}, - {"execveatWrapper", "(ILjava/lang/String;[Ljava/lang/String;I)I", (void*)execveatWrapper}, }; int register_com_android_internal_os_Zygote(JNIEnv* env) { From 0c93020acab4087e47aed263a7395b6535a1897a Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 07/35] remove the previous exec spawning implementation --- .../com/android/internal/os/ExecInit.java | 125 ------------- .../com/android/internal/os/WrapperInit.java | 2 +- core/java/com/android/internal/os/Zygote.java | 21 --- core/jni/com_android_internal_os_Zygote.cpp | 175 ++++++++---------- ...ndroid_internal_os_ZygoteCommandBuffer.cpp | 14 -- 5 files changed, 78 insertions(+), 259 deletions(-) delete mode 100644 core/java/com/android/internal/os/ExecInit.java diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java deleted file mode 100644 index 39f08b6a0f15b..0000000000000 --- a/core/java/com/android/internal/os/ExecInit.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.android.internal.os; - -import android.os.Trace; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Slog; -import android.util.TimingsTraceLog; -import dalvik.system.VMRuntime; - -/** - * Startup class for the process. - * @hide - */ -public class ExecInit { - /** - * Class not instantiable. - */ - private ExecInit() { - } - - /** - * The main function called when starting a runtime application. - * - * The first argument is the target SDK version for the app. - * - * The remaining arguments are passed to the runtime. - * - * @param args The command-line arguments. - */ - public static void main(String[] args) { - // Parse our mandatory argument. - int targetSdkVersion = Integer.parseInt(args[0], 10); - - // Parse the runtime_flags. - int runtimeFlags = Integer.parseInt(args[1], 10); - - // Mimic system Zygote preloading. - ZygoteInit.preload(new TimingsTraceLog("ExecInitTiming", - Trace.TRACE_TAG_DALVIK), false); - - // Launch the application. - String[] runtimeArgs = new String[args.length - 2]; - System.arraycopy(args, 2, runtimeArgs, 0, runtimeArgs.length); - Runnable r = execInit(targetSdkVersion, runtimeArgs); - - Zygote.nativeHandleRuntimeFlags(runtimeFlags); - - r.run(); - } - - /** - * Executes a runtime application with exec-based spawning. - * This method never returns. - * - * @param niceName The nice name for the application, or null if none. - * @param targetSdkVersion The target SDK version for the app. - * @param args Arguments for {@link RuntimeInit#main}. - */ - public static void execApplication(String niceName, int targetSdkVersion, - String instructionSet, int runtimeFlags, String[] args) { - int niceArgs = niceName == null ? 0 : 1; - int baseArgs = 6 + niceArgs; - String[] argv = new String[baseArgs + args.length]; - if (VMRuntime.is64BitInstructionSet(instructionSet)) { - argv[0] = "/system/bin/app_process64"; - } else { - argv[0] = "/system/bin/app_process32"; - } - argv[1] = "/system/bin"; - argv[2] = "--application"; - if (niceName != null) { - argv[3] = "--nice-name=" + niceName; - } - argv[3 + niceArgs] = "com.android.internal.os.ExecInit"; - argv[4 + niceArgs] = Integer.toString(targetSdkVersion); - argv[5 + niceArgs] = Integer.toString(runtimeFlags); - System.arraycopy(args, 0, argv, baseArgs, args.length); - - WrapperInit.preserveCapabilities(); - try { - Os.execv(argv[0], argv); - } catch (ErrnoException e) { - throw new RuntimeException(e); - } - } - - public static boolean isExecSpawned; - - /** - * The main function called when an application is started with exec-based spawning. - * - * When the app starts, the runtime starts {@link RuntimeInit#main} - * which calls {@link main} which then calls this method. - * So we don't need to call commonInit() here. - * - * @param targetSdkVersion target SDK version - * @param argv arg strings - */ - private static Runnable execInit(int targetSdkVersion, String[] argv) { - if (RuntimeInit.DEBUG) { - Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from exec"); - } - - isExecSpawned = true; - - // Check whether the first argument is a "-cp" in argv, and assume the next argument is the - // classpath. If found, create a PathClassLoader and use it for applicationInit. - ClassLoader classLoader = null; - if (argv != null && argv.length > 2 && argv[0].equals("-cp")) { - classLoader = ZygoteInit.createPathClassLoader(argv[1], targetSdkVersion); - - // Install this classloader as the context classloader, too. - Thread.currentThread().setContextClassLoader(classLoader); - - // Remove the classpath from the arguments. - String removedArgs[] = new String[argv.length - 2]; - System.arraycopy(argv, 2, removedArgs, 0, argv.length - 2); - argv = removedArgs; - } - - // Perform the same initialization that would happen after the Zygote forks. - Zygote.nativePreApplicationInit(); - return RuntimeInit.applicationInit(targetSdkVersion, /*disabledCompatChanges*/ null, argv, classLoader); - } -} diff --git a/core/java/com/android/internal/os/WrapperInit.java b/core/java/com/android/internal/os/WrapperInit.java index a2eef62f80be2..6860759eea8ae 100644 --- a/core/java/com/android/internal/os/WrapperInit.java +++ b/core/java/com/android/internal/os/WrapperInit.java @@ -186,7 +186,7 @@ private static Runnable wrapperInit(int targetSdkVersion, String[] argv) { * This is acceptable here as failure will leave the wrapped app with strictly less * capabilities, which may make it crash, but not exceed its allowances. */ - public static void preserveCapabilities() { + private static void preserveCapabilities() { StructCapUserHeader header = new StructCapUserHeader( OsConstants._LINUX_CAPABILITY_VERSION_3, 0); StructCapUserData[] data; diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 2aad41dc104e0..3d6f8f7930b21 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -209,15 +209,6 @@ public final class Zygote { /** Load 4KB ELF files on 16KB device using appcompat mode */ public static final int ENABLE_PAGE_SIZE_APP_COMPAT = 1 << 26; - public static final int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 28; - public static final int DISABLE_HARDENED_MALLOC = 1 << 29; - public static final int ENABLE_COMPAT_VA_39_BIT = 1 << 30; - - // make sure to update isSimpleForkCommand() in core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp - // when adding new flags that depend on exec spawning - public static final int RUNTIME_FLAGS_DEPENDENT_ON_EXEC_SPAWNING = DISABLE_HARDENED_MALLOC | ENABLE_COMPAT_VA_39_BIT; - public static final int CUSTOM_RUNTIME_FLAGS = DISABLE_HARDENED_MALLOC | ENABLE_COMPAT_VA_39_BIT | FORCIBLY_ENABLE_MEMORY_TAGGING; - /** No external storage should be mounted. */ public static final int MOUNT_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE; /** Default external storage should be mounted. */ @@ -1188,7 +1179,6 @@ private static void callPostForkSystemServerHooks(int runtimeFlags) { @SuppressWarnings("unused") private static void callPostForkChildHooks(int runtimeFlags, boolean isSystemServer, boolean isZygote, String instructionSet) { - runtimeFlags &= ~CUSTOM_RUNTIME_FLAGS; // a warning is printed when an unknown flag is passed android.os.Binder.onZygotePostForkChild(); android.os.BinderProxy.onZygotePostForkChild(); ZygoteHooks.postForkChild(runtimeFlags, isSystemServer, isZygote, instructionSet); @@ -1401,8 +1391,6 @@ private static int decideTaggingLevel( level = MEMORY_TAG_LEVEL_ASYNC; if (!si.isImmutable()) { - level |= FORCIBLY_ENABLE_MEMORY_TAGGING; - // This flag prevents the app from downgrading the heap memory tagging level and // from intercepting MTE SIGSEGV signal (it's used for crashing the process // after tag check failure). // @@ -1555,13 +1543,4 @@ && isCompatChangeEnabled( } return runtimeFlags; } - - /** - * Used on GrapheneOS to set up runtime flags - * - * @param runtimeFlags flags to be passed to the native method - * - * @hide - */ - public static native void nativeHandleRuntimeFlags(int runtimeFlags); } diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 8873236b21dd4..1e6a32ad42f8b 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -1815,97 +1815,6 @@ static void BindMountStorageDirs(JNIEnv* env, jobjectArray pkg_data_info_list, } } -static void HandleRuntimeFlags(JNIEnv* env, jint& runtime_flags, const char* process_name, const char* nice_name_ptr) { - // Set process properties to enable debugging if required. - if ((runtime_flags & RuntimeFlags::DEBUG_ENABLE_PTRACE) != 0) { - EnableDebugger(); - // Don't pass unknown flag to the ART runtime. - runtime_flags &= ~RuntimeFlags::DEBUG_ENABLE_PTRACE; - } - if ((runtime_flags & RuntimeFlags::PROFILE_FROM_SHELL) != 0) { - // simpleperf needs the process to be dumpable to profile it. - if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) { - ALOGE("prctl(PR_SET_DUMPABLE) failed: %s", strerror(errno)); - RuntimeAbort(env, __LINE__, "prctl(PR_SET_DUMPABLE, 1) failed"); - } - } - - HeapTaggingLevel heap_tagging_level; - switch (runtime_flags & RuntimeFlags::MEMORY_TAG_LEVEL_MASK) { - case RuntimeFlags::MEMORY_TAG_LEVEL_TBI: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_TBI; - break; - case RuntimeFlags::MEMORY_TAG_LEVEL_ASYNC: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_ASYNC; - break; - case RuntimeFlags::MEMORY_TAG_LEVEL_SYNC: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_SYNC; - break; - default: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_NONE; - break; - } - mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, heap_tagging_level); - - const int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 28; - if (runtime_flags & FORCIBLY_ENABLE_MEMORY_TAGGING) { - if (mallopt(M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE, 0) != (int) true) { - RuntimeAbort(env, __LINE__, "mallopt(M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE) failed"); - } - if (mallopt(M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION, 0) != (int) true) { - RuntimeAbort(env, __LINE__, "mallopt(M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION) failed"); - } - runtime_flags &= ~FORCIBLY_ENABLE_MEMORY_TAGGING; - } - - // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART - // runtime. - runtime_flags &= ~RuntimeFlags::MEMORY_TAG_LEVEL_MASK; - - // Avoid heap zero initialization for applications without MTE. Zero init may - // cause app compat problems, use more memory, or reduce performance. While it - // would be nice to have them for apps, we will have to wait until they are - // proven out, have more efficient hardware, and/or apply them only to new - // applications. - if (!(runtime_flags & RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED)) { - mallopt(M_BIONIC_ZERO_INIT, 0); - } - - // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART - // runtime. - runtime_flags &= ~RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED; - - android_mallopt_gwp_asan_options_t gwp_asan_options; - const char* kGwpAsanAppRecoverableSysprop = - "persist.device_config.memory_safety_native.gwp_asan_recoverable_apps"; - // The system server doesn't have its nice name set by the time SpecializeCommon is called. - gwp_asan_options.program_name = nice_name_ptr ?: process_name; - switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { - default: - case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT: - gwp_asan_options.mode = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true) - ? Mode::APP_MANIFEST_DEFAULT - : Mode::APP_MANIFEST_NEVER; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - case RuntimeFlags::GWP_ASAN_LEVEL_NEVER: - gwp_asan_options.mode = Mode::APP_MANIFEST_NEVER; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS: - gwp_asan_options.mode = Mode::APP_MANIFEST_ALWAYS; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY: - gwp_asan_options.mode = Mode::APP_MANIFEST_DEFAULT; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - } - // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART - // runtime. - runtime_flags &= ~RuntimeFlags::GWP_ASAN_LEVEL_MASK; -} - // Utility routine to specialize a zygote child process. static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities, @@ -2079,9 +1988,84 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, } } - const char* nice_name_ptr = nice_name.has_value() ? nice_name.value().c_str() : nullptr; + // Set process properties to enable debugging if required. + if ((runtime_flags & RuntimeFlags::DEBUG_ENABLE_PTRACE) != 0) { + EnableDebugger(); + // Don't pass unknown flag to the ART runtime. + runtime_flags &= ~RuntimeFlags::DEBUG_ENABLE_PTRACE; + } + if ((runtime_flags & RuntimeFlags::PROFILE_FROM_SHELL) != 0) { + // simpleperf needs the process to be dumpable to profile it. + if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) { + ALOGE("prctl(PR_SET_DUMPABLE) failed: %s", strerror(errno)); + RuntimeAbort(env, __LINE__, "prctl(PR_SET_DUMPABLE, 1) failed"); + } + } - HandleRuntimeFlags(env, runtime_flags, process_name, nice_name_ptr); + HeapTaggingLevel heap_tagging_level; + switch (runtime_flags & RuntimeFlags::MEMORY_TAG_LEVEL_MASK) { + case RuntimeFlags::MEMORY_TAG_LEVEL_TBI: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_TBI; + break; + case RuntimeFlags::MEMORY_TAG_LEVEL_ASYNC: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_ASYNC; + break; + case RuntimeFlags::MEMORY_TAG_LEVEL_SYNC: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_SYNC; + break; + default: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_NONE; + break; + } + mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, heap_tagging_level); + + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART + // runtime. + runtime_flags &= ~RuntimeFlags::MEMORY_TAG_LEVEL_MASK; + + // Avoid heap zero initialization for applications without MTE. Zero init may + // cause app compat problems, use more memory, or reduce performance. While it + // would be nice to have them for apps, we will have to wait until they are + // proven out, have more efficient hardware, and/or apply them only to new + // applications. + if (!(runtime_flags & RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED)) { + mallopt(M_BIONIC_ZERO_INIT, 0); + } + + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART + // runtime. + runtime_flags &= ~RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED; + + const char* nice_name_ptr = nice_name.has_value() ? nice_name.value().c_str() : nullptr; + android_mallopt_gwp_asan_options_t gwp_asan_options; + const char* kGwpAsanAppRecoverableSysprop = + "persist.device_config.memory_safety_native.gwp_asan_recoverable_apps"; + // The system server doesn't have its nice name set by the time SpecializeCommon is called. + gwp_asan_options.program_name = nice_name_ptr ?: process_name; + switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { + default: + case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT: + gwp_asan_options.mode = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true) + ? Mode::APP_MANIFEST_DEFAULT + : Mode::APP_MANIFEST_NEVER; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + case RuntimeFlags::GWP_ASAN_LEVEL_NEVER: + gwp_asan_options.mode = Mode::APP_MANIFEST_NEVER; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS: + gwp_asan_options.mode = Mode::APP_MANIFEST_ALWAYS; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY: + gwp_asan_options.mode = Mode::APP_MANIFEST_DEFAULT; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + } + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART + // runtime. + runtime_flags &= ~RuntimeFlags::GWP_ASAN_LEVEL_MASK; SetCapabilities(permitted_capabilities, effective_capabilities, permitted_capabilities, fail_fn); @@ -3052,10 +3036,6 @@ static void com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload(JNIEn gPreloadFdsExtracted = true; } -static void nativeHandleRuntimeFlagsWrapper(JNIEnv* env, jclass, jint runtime_flags) { - HandleRuntimeFlags(env, runtime_flags, nullptr, nullptr); -} - static const JNINativeMethod gMethods[] = { {"nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" @@ -3108,7 +3088,6 @@ static const JNINativeMethod gMethods[] = { (void*)com_android_internal_os_Zygote_nativeMarkOpenedFilesBeforePreload}, {"nativeAllowFilesOpenedByPreload", "()V", (void*)com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload}, - {"nativeHandleRuntimeFlags", "(I)V", (void*)nativeHandleRuntimeFlagsWrapper}, }; int register_com_android_internal_os_Zygote(JNIEnv* env) { diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index ac187b53201cd..2fbe4e8a6b878 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -177,9 +177,6 @@ class NativeCommandBuffer { static const size_t NN_LENGTH = strlen(NICE_NAME); static const size_t ITA_LENGTH = strlen(IS_TOP_APP); - static const char* RUNTIME_FLAGS = "--runtime-flags="; - static const size_t RF_LENGTH = strlen(RUNTIME_FLAGS); - bool saw_setuid = false, saw_setgid = false; bool saw_runtime_args = false; bool is_top_app = false; @@ -195,17 +192,6 @@ class NativeCommandBuffer { saw_runtime_args = true; continue; } - if (static_cast(arg_end - arg_start) >= RF_LENGTH - && strncmp(arg_start, RUNTIME_FLAGS, RF_LENGTH) == 0) { - int flags = digitsVal(arg_start + RF_LENGTH, arg_end); - const int DISABLE_HARDENED_MALLOC = 1 << 29; - const int ENABLE_COMPAT_VA_39_BIT = 1 << 30; - if (flags & (DISABLE_HARDENED_MALLOC | ENABLE_COMPAT_VA_39_BIT)) { - // fallback to the slow path that calls ExecInit - return std::make_pair(false, false); - } - continue; - } if (static_cast(arg_end - arg_start) >= NN_LENGTH && strncmp(arg_start, NICE_NAME, NN_LENGTH) == 0) { size_t name_len = arg_end - (arg_start + NN_LENGTH); From cf94099d16481711762ff5cb6ab668d30d2d1aec Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 08/35] fixup! AppBindArgs: infrastructure for passing extra args to app process init Run the onBind() hook at an earlier point by removing its dependency on the app context object. This is required by the next commit. --- core/java/android/app/ActivityThread.java | 3 ++- core/java/android/app/ActivityThreadHooks.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index a10c553707d8d..73be579af6951 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -7855,6 +7855,8 @@ private String getInstrumentationLibrary(ApplicationInfo appInfo, Instrumentatio @UnsupportedAppUsage @RavenwoodThrow(comment = "See ActivityThread_ravenwood for initialization on Ravenwood") private void handleBindApplication(AppBindData data) { + final Bundle extraAppBindArgs = ActivityThreadHooks.onBind(data); + mDdmSyncStageUpdater.next(Stage.Bind); // Register the UI Thread as a sensitive thread to the runtime. @@ -8049,7 +8051,6 @@ private void handleBindApplication(AppBindData data) { final IActivityManager mgr = ActivityManager.getService(); final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); mConfigurationController.updateLocaleListFromAppContext(appContext); - final Bundle extraAppBindArgs = ActivityThreadHooks.onBind(appContext, data); // Initialize the default http proxy in this process. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Setup proxies"); diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java index dfd34d0a9dd70..11358c3a85f31 100644 --- a/core/java/android/app/ActivityThreadHooks.java +++ b/core/java/android/app/ActivityThreadHooks.java @@ -23,7 +23,7 @@ class ActivityThreadHooks { // called after the initial app context is constructed // ActivityThread.handleBindApplication - static Bundle onBind(Context appContext, ActivityThread.AppBindData appBindData) { + static Bundle onBind(ActivityThread.AppBindData appBindData) { Bundle args = appBindData.extraArgs; Objects.requireNonNull(args, "args bundle is null"); @@ -32,7 +32,7 @@ static Bundle onBind(Context appContext, ActivityThread.AppBindData appBindData) } called = true; - AppGlobals.setInitialPackageId(appContext.getApplicationInfo().ext().getPackageId()); + AppGlobals.setInitialPackageId(appBindData.appInfo.ext().getPackageId()); int[] flags = Objects.requireNonNull(args.getIntArray(AppBindArgs.KEY_FLAGS_ARRAY)); From 9b6c191beff398a73e762c0800dc0d21ce1de683 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 09/35] fixup! exec spawning: support runtime resource overlays Use AppBindArgs to pass systemIdmapPaths to the app instead of performing an extra Binder IPC call to ActivityManagerService during app process launch. --- core/java/android/app/ActivityThreadHooks.java | 6 +++--- core/java/android/app/AppBindArgs.java | 1 + core/java/android/app/IActivityManager.aidl | 2 -- core/java/android/content/res/AssetManager.java | 17 +++++------------ .../server/am/ActivityManagerService.java | 6 ------ .../android/server/ext/PackageManagerHooks.java | 1 + 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java index 11358c3a85f31..6542e25e887de 100644 --- a/core/java/android/app/ActivityThreadHooks.java +++ b/core/java/android/app/ActivityThreadHooks.java @@ -1,15 +1,13 @@ package android.app; -import android.annotation.Nullable; import android.content.Context; import android.content.pm.GosPackageState; import android.content.pm.SrtPermissions; +import android.content.res.AssetManager; import android.ext.dcl.DynCodeLoading; import android.location.HookedLocationManager; import android.os.Bundle; import android.os.Process; -import android.os.RemoteException; -import android.util.Log; import com.android.internal.app.ContactScopes; import com.android.internal.app.StorageScopesAppHooks; @@ -32,6 +30,8 @@ static Bundle onBind(ActivityThread.AppBindData appBindData) { } called = true; + AssetManager.systemIdmapPaths_ = args.getStringArray(AppBindArgs.KEY_SYSTEM_IDMAP_PATHS); + AppGlobals.setInitialPackageId(appBindData.appInfo.ext().getPackageId()); int[] flags = Objects.requireNonNull(args.getIntArray(AppBindArgs.KEY_FLAGS_ARRAY)); diff --git a/core/java/android/app/AppBindArgs.java b/core/java/android/app/AppBindArgs.java index 1609a74998b8c..207e6946f5acf 100644 --- a/core/java/android/app/AppBindArgs.java +++ b/core/java/android/app/AppBindArgs.java @@ -3,6 +3,7 @@ /** @hide */ public interface AppBindArgs { String KEY_GOS_PACKAGE_STATE = "gosPs"; + String KEY_SYSTEM_IDMAP_PATHS = "idmapPaths"; String KEY_FLAGS_ARRAY = "flagsArr"; int FLAGS_IDX_SPECIAL_RUNTIME_PERMISSIONS = 0; diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index ed0422424d88d..e3b49e5f41925 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1051,8 +1051,6 @@ interface IActivityManager { @EnforcePermission("INTERACT_ACROSS_USERS_FULL") IBinder refreshIntentCreatorToken(in Intent intent); - String[] getSystemIdmapPaths(); - oneway void showDynCodeLoadingNotification(int type, String pkgName, @nullable String path, in List reportBody, String denialType); diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index bbb10309970e0..56f999c14ba9c 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -49,7 +49,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.om.OverlayConfig; -import com.android.internal.os.ExecInit; import com.android.internal.ravenwood.RavenwoodHelperBridge; import java.io.FileDescriptor; @@ -302,17 +301,11 @@ public static void createSystemAssetsInZygoteLocked(boolean reinitialize, // When exec-based spawning in used, in-memory cache of assets is lost, and the spawned // process is unable to recreate it, since it's not allowed to create idmaps. // - // As a workaround, ask the ActivityManager to return paths of cached idmaps and use - // them directly. ActivityManager runs in system_server, which always uses zygote-based - // spawning. - if (ExecInit.isExecSpawned) { - try { - systemIdmapPaths = ActivityManager.getService().getSystemIdmapPaths(); - Objects.requireNonNull(systemIdmapPaths); - } catch (Throwable t) { - Log.e(TAG, "unable to retrieve systemIdmapPaths", t); - systemIdmapPaths = new String[0]; - } + // To resolve this issue, idmap paths are passed from system_server via AppBindArgs. + // system_server always uses zygote-based spawning. + systemIdmapPaths = systemIdmapPaths_; + if (systemIdmapPaths != null) { + Log.d(TAG, "reusing systemIdmapPaths"); } else { systemIdmapPaths = OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote(); systemIdmapPaths_ = systemIdmapPaths; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index e7d5fb29c348f..5538b0e52ccd4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -20046,12 +20046,6 @@ public void sendProfilingTrigger(int uid, @NonNull String packageName, int trigg }); } - @Override - public String[] getSystemIdmapPaths() { - // see comment in AssetManager#createSystemAssetsInZygoteLocked() - return android.content.res.AssetManager.systemIdmapPaths_; - } - @Override public void showDynCodeLoadingNotification(int type, String pkgName, @Nullable String path, List reportBody, String denialType) { diff --git a/services/core/java/com/android/server/ext/PackageManagerHooks.java b/services/core/java/com/android/server/ext/PackageManagerHooks.java index 832b0818c38fc..4b8642dd19eb6 100644 --- a/services/core/java/com/android/server/ext/PackageManagerHooks.java +++ b/services/core/java/com/android/server/ext/PackageManagerHooks.java @@ -123,6 +123,7 @@ public static Bundle getExtraAppBindArgs(Context context, PackageManagerInternal var b = new Bundle(); b.putParcelable(AppBindArgs.KEY_GOS_PACKAGE_STATE, gosPs); b.putIntArray(AppBindArgs.KEY_FLAGS_ARRAY, flagsArr); + b.putStringArray(AppBindArgs.KEY_SYSTEM_IDMAP_PATHS, android.content.res.AssetManager.systemIdmapPaths_); return b; } From c8d4d09cf3e96b086dfa36e37539712ea6d56a67 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 10/35] gosps: add USE_EXEC_SPAWNING flags --- core/java/android/content/pm/GosPackageStateFlag.java | 2 ++ .../com/android/server/pm/GosPackageStatePermissions.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/core/java/android/content/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index 21abe5f9398d0..3939ec82061e3 100644 --- a/core/java/android/content/pm/GosPackageStateFlag.java +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -36,6 +36,8 @@ public interface GosPackageStateFlag { /** @hide */ int PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE = 26; /** @hide */ int SUPPRESS_PLAY_INTEGRITY_API_NOTIF = 27; /** @hide */ int BLOCK_PLAY_INTEGRITY_API = 28; + /** @hide */ int USE_EXEC_SPAWNING_NON_DEFAULT = 29; + /** @hide */ int USE_EXEC_SPAWNING = 30; /** @hide */ @IntDef(value = { diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java index 5c1348efce7f5..8dc9b77d2f85d 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java @@ -42,6 +42,8 @@ import static android.content.pm.GosPackageStateFlag.RESTRICT_WEBVIEW_DYN_CODE_LOADING_NON_DEFAULT; import static android.content.pm.GosPackageStateFlag.STORAGE_SCOPES_ENABLED; import static android.content.pm.GosPackageStateFlag.SUPPRESS_PLAY_INTEGRITY_API_NOTIF; +import static android.content.pm.GosPackageStateFlag.USE_EXEC_SPAWNING; +import static android.content.pm.GosPackageStateFlag.USE_EXEC_SPAWNING_NON_DEFAULT; import static android.content.pm.GosPackageStateFlag.USE_EXTENDED_VA_SPACE; import static android.content.pm.GosPackageStateFlag.USE_EXTENDED_VA_SPACE_NON_DEFAULT; import static android.content.pm.GosPackageStateFlag.USE_HARDENED_MALLOC; @@ -140,6 +142,8 @@ static void init(PackageManagerService pm) { FORCE_MEMTAG_NON_DEFAULT, FORCE_MEMTAG, FORCE_MEMTAG_SUPPRESS_NOTIF, + USE_EXEC_SPAWNING_NON_DEFAULT, + USE_EXEC_SPAWNING, ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE, }; builder() From 5e5cda3a4c60714af9b6c760ef2d880865bff800 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 11/35] add exec spawning implementation For background, see https://grapheneos.org/usage#exec-spawning New app processes are created by forking zygote, then immediately exec()-ing a new zygote and replaying commands that were sent to the original zygote in the new zygote. Commands are passed to the new zygote through a memfd. Exec spawning can be disabled on a per-app basis. App processes that are spawned through secondary zygotes (WebView processes and service processes which use an app zygote) are instead spawned through the primary zygote. Boot-time WebView zygote startup is disabled to reduce memory usage. The WebView zygote is started on-demand to support apps that are opted out of exec spawning. Test: atest GosCompatSecureSpawnTests --- .../ext/settings/app/AswUseExecSpawning.java | 37 +++ core/java/android/os/AppZygote.java | 28 +-- core/java/android/os/ChildZygoteProcess.java | 2 +- core/java/android/os/Process.java | 23 +- core/java/android/os/ZygoteProcess.java | 30 ++- core/java/android/webkit/WebViewFactory.java | 5 + core/java/android/webkit/WebViewZygote.java | 6 +- .../com/android/internal/os/ExecSpawning.java | 104 +++++++++ core/java/com/android/internal/os/Zygote.java | 75 +++--- .../android/internal/os/ZygoteArguments.java | 194 +++++++++++++++- .../internal/os/ZygoteCommandRecorder.java | 94 ++++++++ .../android/internal/os/ZygoteConnection.java | 139 +++++++---- .../android/internal/os/ZygoteExtraArgs.java | 175 +++++++++++--- .../com/android/internal/os/ZygoteInit.java | 55 +++-- .../com/android/internal/os/ZygoteServer.java | 6 +- core/jni/com_android_internal_os_Zygote.cpp | 216 +++++++++++++++--- ...ndroid_internal_os_ZygoteCommandBuffer.cpp | 7 + core/jni/fd_utils.cpp | 4 +- core/jni/fd_utils.h | 3 + .../com/android/server/am/ActiveServices.java | 13 +- .../com/android/server/am/HostingRecord.java | 10 +- .../com/android/server/am/ProcessList.java | 75 +++--- .../com/android/server/am/ProcessRecord.java | 2 +- .../webkit/WebViewUpdateServiceImpl2.java | 12 +- .../server/am/ApplicationExitInfoTest.java | 2 +- .../server/am/ApplicationStartInfoTest.java | 2 +- 26 files changed, 1039 insertions(+), 280 deletions(-) create mode 100644 core/java/android/ext/settings/app/AswUseExecSpawning.java create mode 100644 core/java/com/android/internal/os/ExecSpawning.java create mode 100644 core/java/com/android/internal/os/ZygoteCommandRecorder.java diff --git a/core/java/android/ext/settings/app/AswUseExecSpawning.java b/core/java/android/ext/settings/app/AswUseExecSpawning.java new file mode 100644 index 0000000000000..7935fd2536952 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseExecSpawning.java @@ -0,0 +1,37 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.settings.ExtSettings; + +import com.android.server.os.nano.AppCompatProtos; + +/** @hide */ +public class AswUseExecSpawning extends AppSwitch { + public static final AswUseExecSpawning I = new AswUseExecSpawning(); + + private AswUseExecSpawning() { + gosPsFlag = GosPackageStateFlag.USE_EXEC_SPAWNING; + gosPsFlagNonDefault = GosPackageStateFlag.USE_EXEC_SPAWNING_NON_DEFAULT; + compatChangeToDisableHardening = AppCompatProtos.USE_ZYGOTE_SPAWNING; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return Boolean.FALSE; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return ExtSettings.EXEC_SPAWNING.get(); + } +} diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index f7eb7bdebf021..f1abc792e85f4 100644 --- a/core/java/android/os/AppZygote.java +++ b/core/java/android/os/AppZygote.java @@ -27,6 +27,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; +import com.android.internal.os.ZygoteExtraArgs; import dalvik.system.VMRuntime; @@ -79,11 +80,11 @@ public AppZygote(ApplicationInfo appInfo, ProcessInfo processInfo, int zygoteUid * Returns the zygote process associated with this app zygote. * Creates the process if it's not already running. */ - public ChildZygoteProcess getProcess(@Nullable String flatExtraArgs) { + public ChildZygoteProcess getProcess(ZygoteExtraArgs zygoteExtArgs) { synchronized (mLock) { if (mZygote != null) return mZygote; - connectToZygoteIfNeededLocked(flatExtraArgs); + connectToZygoteIfNeededLocked(zygoteExtArgs); return mZygote; } } @@ -129,7 +130,9 @@ public ApplicationInfo getAppInfo() { * @return An object that describes the result of the attempt to start the process. * @throws RuntimeException on fatal start failure */ - public final Process.ProcessStartResult startProcess(@NonNull final String processClass, + public final Process.ProcessStartResult startProcess( + @NonNull ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, final String niceName, int uid, @Nullable int[] gids, int runtimeFlags, int mountExternal, @@ -146,19 +149,18 @@ public final Process.ProcessStartResult startProcess(@NonNull final String proce @Nullable Map> allowlistedDataInfoList, long startSeq, - @Nullable String[] zygoteArgs, - @Nullable String flatExtraArgs) { + @Nullable String[] zygoteArgs) { try { - return getProcess(flatExtraArgs).start(processClass, + return getProcess(zygoteExtArgs).start(zygoteExtArgs, processClass, niceName, uid, uid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, null, packageName, /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp, disabledCompatChanges, pkgDataInfoMap, allowlistedDataInfoList, false, false, false, startSeq, - zygoteArgs, null); + zygoteArgs); } catch (RuntimeException e) { - final boolean zygote_dead = getProcess(flatExtraArgs).isDead(); + final boolean zygote_dead = getProcess(zygoteExtArgs).isDead(); if (!zygote_dead) { throw e; // Zygote process is alive. Do nothing. } @@ -166,14 +168,14 @@ public final Process.ProcessStartResult startProcess(@NonNull final String proce // Retry here if the previous start fails. Log.w(LOG_TAG, "retry starting process " + niceName); stopZygote(); - return getProcess(flatExtraArgs).start(processClass, + return getProcess(zygoteExtArgs).start(zygoteExtArgs, processClass, niceName, uid, uid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, null, packageName, /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp, disabledCompatChanges, pkgDataInfoMap, allowlistedDataInfoList, false, false, false, startSeq, - zygoteArgs, null); + zygoteArgs); } @GuardedBy("mLock") @@ -189,7 +191,7 @@ private void stopZygoteLocked() { } @GuardedBy("mLock") - private void connectToZygoteIfNeededLocked(@Nullable String flatExtraArgs) { + private void connectToZygoteIfNeededLocked(ZygoteExtraArgs zygoteExtArgs) { String abi = mAppInfo.primaryCpuAbi != null ? mAppInfo.primaryCpuAbi : Build.SUPPORTED_ABIS[0]; try { @@ -199,6 +201,7 @@ private void connectToZygoteIfNeededLocked(@Nullable String flatExtraArgs) { final int[] sharedAppGid = { UserHandle.getSharedAppGid(UserHandle.getAppId(mAppInfo.uid)) }; mZygote = Process.ZYGOTE_PROCESS.startChildZygote( + zygoteExtArgs, "com.android.internal.os.AppZygoteInit", mAppInfo.processName + "_zygote", mZygoteUid, @@ -210,8 +213,7 @@ private void connectToZygoteIfNeededLocked(@Nullable String flatExtraArgs) { abi, // acceptedAbiList VMRuntime.getInstructionSet(abi), // instructionSet mZygoteUidGidMin, - mZygoteUidGidMax, - flatExtraArgs); + mZygoteUidGidMax); ZygoteProcess.waitForConnectionToZygote(mZygote.getPrimarySocketAddress()); // preload application code in the zygote diff --git a/core/java/android/os/ChildZygoteProcess.java b/core/java/android/os/ChildZygoteProcess.java index 2724324816152..6377d15d5b3b9 100644 --- a/core/java/android/os/ChildZygoteProcess.java +++ b/core/java/android/os/ChildZygoteProcess.java @@ -47,7 +47,7 @@ public class ChildZygoteProcess extends ZygoteProcess { ChildZygoteProcess(LocalSocketAddress socketAddress, int pid, int uid) { - super(socketAddress, null); + super(socketAddress); mPid = pid; mUid = uid; mDead = new AtomicBoolean(false); diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 71981540da106..542d7432325ad 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -42,6 +42,7 @@ import android.util.Pair; import android.webkit.WebViewZygote; +import com.android.internal.os.ZygoteExtraArgs; import com.android.sdksandbox.flags.Flags; import dalvik.annotation.optimization.CriticalNative; @@ -738,7 +739,8 @@ public class Process { * * @hide */ - public static ProcessStartResult start(@NonNull final String processClass, + public static ProcessStartResult start(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, @Nullable final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, @@ -761,18 +763,19 @@ public static ProcessStartResult start(@NonNull final String processClass, boolean bindMountAppStorageDirs, boolean bindMountSystemOverrides, long startSeq, - @Nullable String[] zygoteArgs, - @Nullable String flatExtraArgs) { - return ZYGOTE_PROCESS.start(processClass, niceName, uid, gid, gids, + @Nullable String[] zygoteArgs) { + return ZYGOTE_PROCESS.start(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges, pkgDataInfoMap, whitelistedDataInfoMap, bindMountAppsData, - bindMountAppStorageDirs, bindMountSystemOverrides, startSeq, zygoteArgs, flatExtraArgs); + bindMountAppStorageDirs, bindMountSystemOverrides, startSeq, zygoteArgs); } /** @hide */ - public static ProcessStartResult startWebView(@NonNull final String processClass, + public static ProcessStartResult startWebView(ZygoteProcess zp, + @NonNull ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, @Nullable final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, @@ -786,19 +789,17 @@ public static ProcessStartResult startWebView(@NonNull final String processClass @Nullable String packageName, @Nullable long[] disabledCompatChanges, long startSeq, - @Nullable String[] zygoteArgs, - @Nullable String flatExtraArgs) { + @Nullable String[] zygoteArgs) { // Webview zygote can't access app private data files, so doesn't need to know its data // info. - return WebViewZygote.getProcess().start(processClass, niceName, uid, gid, gids, + return zp.start(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, packageName, /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, /*isTopApp=*/ false, disabledCompatChanges, /* pkgDataInfoMap */ null, /* whitelistedDataInfoMap */ null, /* bindMountAppsData */ false, /* bindMountAppStorageDirs */ false, /* bindMountSyspropOverrides */ false, - startSeq, zygoteArgs, - flatExtraArgs); + startSeq, zygoteArgs); } /** diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index f95a5d745d1bf..65ff5f6c46bf6 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -33,6 +33,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; import com.android.internal.os.ZygoteConfig; +import com.android.internal.os.ZygoteExtraArgs; import java.io.BufferedWriter; import java.io.DataInputStream; @@ -339,7 +340,8 @@ boolean isClosed() { * @return An object that describes the result of the attempt to start the process. * @throws RuntimeException on fatal start failure */ - public final Process.ProcessStartResult start(@NonNull final String processClass, + public final Process.ProcessStartResult start(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, int mountExternal, @@ -361,19 +363,19 @@ public final Process.ProcessStartResult start(@NonNull final String processClass boolean bindMountAppStorageDirs, boolean bindOverrideSysprops, long startSeq, - @Nullable String[] zygoteArgs, @Nullable String flatExtraArgs) { + @Nullable String[] zygoteArgs) { // TODO (chriswailes): Is there a better place to check this value? if (fetchUsapPoolEnabledPropWithMinInterval()) { informZygotesOfUsapPoolStatus(); } try { - return startViaZygote(processClass, niceName, uid, gid, gids, + return startViaZygote(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, /*startChildZygote=*/ false, packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges, pkgDataInfoMap, allowlistedDataInfoList, bindMountAppsData, - bindMountAppStorageDirs, bindOverrideSysprops, startSeq, zygoteArgs, flatExtraArgs); + bindMountAppStorageDirs, bindOverrideSysprops, startSeq, zygoteArgs); } catch (ZygoteStartFailedEx ex) { Log.e(LOG_TAG, "Starting VM process through Zygote failed"); @@ -629,7 +631,8 @@ private static native int nativeStartNativeProcess( * @return An object that describes the result of the attempt to start the process. * @throws ZygoteStartFailedEx if process start failed for any reason */ - private Process.ProcessStartResult startViaZygote(@NonNull final String processClass, + private Process.ProcessStartResult startViaZygote(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, @Nullable final String niceName, final int uid, final int gid, @Nullable final int[] gids, @@ -653,8 +656,7 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC boolean bindMountAppStorageDirs, boolean bindMountOverrideSysprops, long startSeq, - @Nullable String[] extraArgs, - @Nullable String flatExtraArgs) + @Nullable String[] extraArgs) throws ZygoteStartFailedEx { if (Flags.nativeFrameworkPrototype() && (zygotePolicyFlags & ZYGOTE_POLICY_FLAG_NATIVE_PROCESS) != 0) { @@ -671,6 +673,7 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC } ArrayList argsForZygote = new ArrayList<>(); + zygoteExtArgs.toZygoteArgList(argsForZygote); // --runtime-args, --setuid=, --setgid=, // and --setgroups= must go first @@ -803,10 +806,6 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC argsForZygote.add(sb.toString()); } - if (flatExtraArgs != null) { - argsForZygote.add(flatExtraArgs); - } - argsForZygote.add(processClass); if (extraArgs != null) { @@ -1310,7 +1309,7 @@ private void informZygotesOfUsapPoolStatus() { * @param uidRangeStart The first UID in the range the child zygote may setuid()/setgid() to * @param uidRangeEnd The last UID in the range the child zygote may setuid()/setgid() to */ - public ChildZygoteProcess startChildZygote(final String processClass, + public ChildZygoteProcess startChildZygote(final ZygoteExtraArgs zygoteExtArgs, final String processClass, final String niceName, int uid, int gid, int[] gids, int runtimeFlags, @@ -1319,8 +1318,7 @@ public ChildZygoteProcess startChildZygote(final String processClass, String acceptedAbiList, String instructionSet, int uidRangeStart, - int uidRangeEnd, - @Nullable String flatExtraArgs) { + int uidRangeEnd) { // Create an unguessable address in the global abstract namespace. final LocalSocketAddress serverAddress = new LocalSocketAddress( processClass + "/" + UUID.randomUUID().toString()); @@ -1334,7 +1332,7 @@ public ChildZygoteProcess startChildZygote(final String processClass, try { // We will bind mount app data dirs so app zygote can't access /data/data, while // we don't need to bind mount storage dirs as /storage won't be mounted. - result = startViaZygote(processClass, niceName, uid, gid, + result = startViaZygote(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, 0 /* mountExternal */, 0 /* targetSdkVersion */, seInfo, abi, instructionSet, null /* appDataDir */, null /* invokeWith */, true /* startChildZygote */, null /* packageName */, @@ -1342,7 +1340,7 @@ public ChildZygoteProcess startChildZygote(final String processClass, null /* disabledCompatChanges */, null /* pkgDataInfoMap */, null /* allowlistedDataInfoList */, true /* bindMountAppsData*/, /* bindMountAppStorageDirs */ false, /*bindMountOverrideSysprops */ false, - /* startSeq */ 0, extraArgs, flatExtraArgs); + /* startSeq */ 0, extraArgs); } catch (ZygoteStartFailedEx ex) { throw new RuntimeException("Starting child-zygote through Zygote failed", ex); diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 2f740ba6c3141..2b743c1c35665 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -289,6 +289,11 @@ public static int loadWebViewNativeLibraryFromPackage(String packageName, return LIBLOAD_WRONG_PACKAGE_NAME; } + if (com.android.internal.os.ExecSpawning.isExecSpawnedProcess()) { + Log.d(LOGTAG, "loadWebViewNativeLibraryFromPackage called in exec spawned process, returning LIBLOAD_ADDRESS_SPACE_NOT_RESERVED early"); + return WebViewFactory.LIBLOAD_ADDRESS_SPACE_NOT_RESERVED; + } + Application initialApplication = AppGlobals.getInitialApplication(); WebViewProviderResponse response = null; try { diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index 6b19d21328a89..940cadb8e682c 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -27,6 +27,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; +import com.android.internal.os.ZygoteExtraArgs; /** @hide */ public class WebViewZygote { @@ -108,7 +109,7 @@ private static void connectToZygoteIfNeededLocked() { sPackage.applicationInfo, null); final int[] sharedAppGid = { UserHandle.getSharedAppGid(UserHandle.getAppId(sPackage.applicationInfo.uid)) }; - sZygote = Process.ZYGOTE_PROCESS.startChildZygote( + sZygote = Process.ZYGOTE_PROCESS.startChildZygote(ZygoteExtraArgs.createForWebviewZygote(), "com.android.internal.os.WebViewZygoteInit", "webview_zygote", Process.WEBVIEW_ZYGOTE_UID, @@ -120,8 +121,7 @@ private static void connectToZygoteIfNeededLocked() { TextUtils.join(",", Build.SUPPORTED_ABIS), null, // instructionSet Process.FIRST_ISOLATED_UID, - Integer.MAX_VALUE, // TODO(b/123615476) deal with user-id ranges properly - null /* flatExtraArgs */); + Integer.MAX_VALUE); // TODO(b/123615476) deal with user-id ranges properly ZygoteProcess.waitForConnectionToZygote(sZygote.getPrimarySocketAddress()); sZygote.preloadApp(sPackage.applicationInfo, abi); } catch (Exception e) { diff --git a/core/java/com/android/internal/os/ExecSpawning.java b/core/java/com/android/internal/os/ExecSpawning.java new file mode 100644 index 0000000000000..89073adda5cab --- /dev/null +++ b/core/java/com/android/internal/os/ExecSpawning.java @@ -0,0 +1,104 @@ +package com.android.internal.os; + +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import java.io.IOException; +import java.util.Arrays; + +import static com.android.internal.util.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class ExecSpawning { + static final String TAG = "ExecSpawning"; + + // Zygote argument for passing commands from zygote to exec-spawned app process + static final String COMMAND_FD_ARG = "--command-fd="; + + private static boolean isExecSpawnedProcess; + private static ZygoteArguments[] commandsToReplay; + + public static boolean isExecSpawnedProcess() { + return isExecSpawnedProcess; + } + + public static boolean isReplayingZygoteCommands() { + return commandsToReplay != null; + } + + static void init(String[] argv) { + boolean logv = Log.isLoggable(TAG, Log.VERBOSE); + if (logv) Log.v(TAG, "argv: " + Arrays.toString(argv)); + + String arg = argv[argv.length - 1]; + if (!arg.startsWith(COMMAND_FD_ARG)) { + if (arg.equals("--socket-name=zygote")) { + // we're in the primary 64-bit zygote + ZygoteCommandRecorder.enable(true); + } else if (arg.equals("--enable-lazy-preload") && "--socket-name=zygote_secondary".equals(argv[argv.length - 2])) { + // we're in the 32-bit zygote + ZygoteCommandRecorder.enable(false); + } + return; + } + + if (logv) Log.v(TAG, "init"); + + try { + Os.setenv("IS_EXEC_SPAWNED_APP_PROCESS", "1", true); + } catch (ErrnoException e) { + throw new IllegalStateException(e); + } + + isExecSpawnedProcess = true; + + byte[] serializedCmds; + { + int separator = arg.indexOf('_', COMMAND_FD_ARG.length()); + int fd = Integer.parseInt(arg, COMMAND_FD_ARG.length(), separator, 10); + int fdSize = Integer.parseInt(arg, separator + 1, arg.length(), 10); + try (var stream = new ParcelFileDescriptor.AutoCloseInputStream(ParcelFileDescriptor.adoptFd(fd))) { + serializedCmds = stream.readNBytes(fdSize); + } catch (IOException e) { + Log.e(TAG, "", e); + // fd is a memfd, using it should never fail + throw new IllegalStateException(e); + } + checkState(serializedCmds.length == fdSize); + } + checkState(commandsToReplay == null); + commandsToReplay = ZygoteCommandRecorder.deserializeCommands(serializedCmds);; + } + + static Runnable replayCommands(ZygoteServer zygoteServer) { + ZygoteArguments[] commandsToReplay = ExecSpawning.commandsToReplay; + requireNonNull(commandsToReplay); + + ZygoteConnection pseudoConnection; + try { + pseudoConnection = new ZygoteConnection(null, null); + } catch (IOException e) { + // pseudo ZygoteConnection doesn't perform any IO + throw new IllegalStateException(e); + } + + boolean logv = Log.isLoggable(TAG, Log.VERBOSE); + + for (int i = 0; i < commandsToReplay.length; ++i) { + ZygoteArguments cmd = commandsToReplay[i]; + if (logv) Log.v(TAG, "replaying cmd " + (i + 1) + " / " + commandsToReplay.length + ": " + cmd); + Runnable r = pseudoConnection.processCommand(zygoteServer, false, cmd); + if (i == commandsToReplay.length - 1) { + checkState(r != null); + ExecSpawning.commandsToReplay = null; + return r; + } + if (r != null) { // some commands run in-place, without a Runnable + r.run(); + } + } + throw new IllegalStateException("unreachable"); + } +} diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 3d6f8f7930b21..c148718388711 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -45,6 +45,7 @@ import android.provider.DeviceConfig; import android.system.ErrnoException; import android.system.Os; +import android.util.DisplayMetrics; import android.util.Log; import com.android.internal.compat.IPlatformCompat; @@ -60,6 +61,7 @@ import java.io.DataOutputStream; import java.io.FileDescriptor; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; /** @hide */ public final class Zygote { @@ -370,21 +372,33 @@ private static boolean containsInetGid(int[] gids) { * @return 0 if this is the child, pid of the child * if this is the parent, or -1 on error. */ - static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags, + static int forkAndSpecialize(ZygoteExtraArgs extraArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs, - boolean bindMountSyspropOverrides, ZygoteExtraArgs extraArgs) { + boolean bindMountSyspropOverrides) { + boolean isExecSpawning = ExecSpawning.isExecSpawnedProcess(); + if (isExecSpawning) { + // needed to run DisplayMetrics class initializer while the process is running in the + // zygote SELinux context + // noinspection unused + int val = DisplayMetrics.DENSITY_DEVICE_STABLE; + } + + // preFork is needed in exec spawned processes too since it switches the process into + // single-threaded mode which is required for changing the SELinux context ZygoteHooks.preFork(); - boolean useFifoUi = SystemProperties.getInt("sys.use_fifo_ui", 0) == 1; - int pid = nativeForkAndSpecialize( + boolean useFifoUi = isExecSpawning || + (com.android.internal.os.Flags.zygoteEarlyFifoBoost() ? + SystemProperties.getInt("sys.use_fifo_ui", 0) == 1 : false); + int pid = nativeForkAndSpecialize(extraArgs.makeJniLongArray(), uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp, - com.android.internal.os.Flags.zygoteEarlyFifoBoost() ? useFifoUi : false, + useFifoUi, pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs, - bindMountAppStorageDirs, bindMountSyspropOverrides, extraArgs.makeJniLongArray()); + bindMountAppStorageDirs, bindMountSyspropOverrides); if (pid == 0) { // Note that this event ends at the end of handleChildProc, Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork"); @@ -402,12 +416,14 @@ static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags, return pid; } - private static native int nativeForkAndSpecialize(int uid, int gid, int[] gids, + static native int nativeForkExec(boolean is64Bit, byte[] commandBuf, boolean disableHardenedMalloc, boolean enableCompatVa39Bit); + + private static native int nativeForkAndSpecialize(long[] extraLongArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, boolean useFifoUi, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, - boolean bindMountAppStorageDirs, boolean bindMountSyspropOverrides, long[] extraLongArgs); + boolean bindMountAppStorageDirs, boolean bindMountSyspropOverrides); /** * Specialize an unspecialized app process. The current VM must have been started @@ -440,17 +456,16 @@ private static native int nativeForkAndSpecialize(int uid, int gid, int[] gids, * @param bindMountSyspropOverrides True if the zygote needs to mount the override system * properties */ - private static void specializeAppProcess(int uid, int gid, int[] gids, int runtimeFlags, + private static void specializeAppProcess(ZygoteExtraArgs extraArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs, - boolean bindMountSyspropOverrides, ZygoteExtraArgs extraArgs) { - nativeSpecializeAppProcess(uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, + boolean bindMountSyspropOverrides) { + nativeSpecializeAppProcess(extraArgs.makeJniLongArray(), uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, startChildZygote, instructionSet, appDataDir, isTopApp, pkgDataInfoList, allowlistedDataInfoList, - bindMountAppDataDirs, bindMountAppStorageDirs, bindMountSyspropOverrides, - extraArgs.makeJniLongArray()); + bindMountAppDataDirs, bindMountAppStorageDirs, bindMountSyspropOverrides); // Note that this event ends at the end of handleChildProc. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork"); @@ -471,12 +486,12 @@ private static void specializeAppProcess(int uid, int gid, int[] gids, int runti ZygoteHooks.postForkCommon(); } - private static native void nativeSpecializeAppProcess(int uid, int gid, int[] gids, + private static native void nativeSpecializeAppProcess(long[] extraLongArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs, - boolean bindMountSyspropOverrides, long[] extraLongArgs); + boolean bindMountSyspropOverrides); /** * Called to do any initialization before starting an application. @@ -584,14 +599,12 @@ static void allowFilesOpenedByPreload() { * - Initializing security properties * - Unmounting storage as appropriate * - Loading necessary performance profile information - * - * @param isPrimary True if this is the zygote process, false if it is zygote_secondary */ - static void initNativeState(boolean isPrimary) { - nativeInitNativeState(isPrimary); + static void initNativeState(ZygoteType type) { + nativeInitNativeState(ExecSpawning.isExecSpawnedProcess(), type.getSocketName(), type.getUsapPoolSocketName()); } - protected static native void nativeInitNativeState(boolean isPrimary); + protected static native void nativeInitNativeState(boolean isExecSpawning, String socketName, String usapPoolSocketName); /** * Returns the raw string value of a system property. @@ -726,7 +739,7 @@ private static native int nativeForkApp(int readPipeFD, * read more * @param zygoteSocket socket from which to obtain new connections when current argBuffer * one is disconnected - * @param expectedUId Uid of peer for initial requests. Subsequent requests from a different + * @param expectedUid Uid of peer for initial requests. Subsequent requests from a different * peer will cause us to return rather than perform the requested fork. * @param minUid Minimum Uid enforced for all but first fork request. The caller checks * the Uid policy for the initial request. @@ -884,14 +897,13 @@ private static Runnable childMain(@Nullable ZygoteCommandBuffer argBuffer, } } - specializeAppProcess(args.mUid, args.mGid, args.mGids, + specializeAppProcess(args.mExtraArgs, args.mUid, args.mGid, args.mGids, args.mRuntimeFlags, rlimits, args.mMountExternal, args.mSeInfo, args.mNiceName, args.mStartChildZygote, args.mInstructionSet, args.mAppDataDir, args.mIsTopApp, args.mPkgDataInfoList, args.mAllowlistedDataInfoList, args.mBindMountAppDataDirs, args.mBindMountAppStorageDirs, - args.mBindMountSyspropOverrides, - args.mExtraArgs); + args.mBindMountSyspropOverrides); // While `specializeAppProcess` sets the thread name on the process's main thread, this // is distinct from the app process name which appears in stack traces, as the latter is @@ -1374,6 +1386,7 @@ private static int getRequestedMemtagLevel( } private static int decideTaggingLevel( + @NonNull AtomicBoolean shouldForciblyEnableTagging, @NonNull ApplicationInfo info, @Nullable ProcessInfo processInfo, @Nullable IPlatformCompat platformCompat) { @@ -1391,6 +1404,8 @@ private static int decideTaggingLevel( level = MEMORY_TAG_LEVEL_ASYNC; if (!si.isImmutable()) { + shouldForciblyEnableTagging.set(true); + // This option prevents the app from downgrading the heap memory tagging level and // from intercepting MTE SIGSEGV signal (it's used for crashing the process // after tag check failure). // @@ -1489,6 +1504,7 @@ private static boolean enableNativeHeapZeroInit( * for a given app. */ public static int getMemorySafetyRuntimeFlags( + @NonNull AtomicBoolean shouldForciblyEnableTagging, @NonNull ApplicationInfo info, @Nullable ProcessInfo processInfo, @Nullable String instructionSet, @@ -1505,7 +1521,7 @@ public static int getMemorySafetyRuntimeFlags( // fine as we haven't seen this configuration in practice, and we can reasonable assume // that if tagging is desired, the system server will be 64-bit. if (instructionSet == null || instructionSet.equals("arm64")) { - runtimeFlags |= decideTaggingLevel(info, processInfo, platformCompat); + runtimeFlags |= decideTaggingLevel(shouldForciblyEnableTagging, info, processInfo, platformCompat); } if (enableNativeHeapZeroInit(info, processInfo, platformCompat)) { runtimeFlags |= NATIVE_HEAP_ZERO_INIT_ENABLED; @@ -1522,13 +1538,12 @@ public static int getMemorySafetyRuntimeFlagsForSecondaryZygote( final IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + var shouldForciblyEnableTagging = new AtomicBoolean(); int runtimeFlags = - getMemorySafetyRuntimeFlags( + getMemorySafetyRuntimeFlags(shouldForciblyEnableTagging, info, processInfo, null /*instructionSet*/, platformCompat); - - // Memory tagging can be forcibly enabled only in immediate children of the primary zygote - // (which includes secondary zygotes) - runtimeFlags &= ~FORCIBLY_ENABLE_MEMORY_TAGGING; + // the value of shouldForciblyEnableTagging is intentionally ignored since it's determined + // already at an earlier point in ProcessList.startLocked() // TBI ("fake" pointer tagging) in AppZygote is controlled by a separate compat feature. if ((runtimeFlags & MEMORY_TAG_LEVEL_MASK) == MEMORY_TAG_LEVEL_TBI diff --git a/core/java/com/android/internal/os/ZygoteArguments.java b/core/java/com/android/internal/os/ZygoteArguments.java index 74fb1f60d0f4f..c97647aa4ab68 100644 --- a/core/java/com/android/internal/os/ZygoteArguments.java +++ b/core/java/com/android/internal/os/ZygoteArguments.java @@ -16,8 +16,12 @@ package com.android.internal.os; +import android.os.Parcel; +import android.os.Parcelable; + import java.io.EOFException; import java.util.ArrayList; +import java.util.Arrays; /** * Handles argument parsing for args related to the zygote spawner. @@ -47,7 +51,132 @@ *
  • [--] <args for RuntimeInit > * */ -class ZygoteArguments { +class ZygoteArguments implements Parcelable { + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel p, int flags) { + if (android.os.Flags.isDevBuild()) { + int numFields = ZygoteArguments.class.getDeclaredFields().length; + int expectedFields = 41; + if (numFields != expectedFields) { + throw new IllegalStateException("expected " + expectedFields + " fields, got " + numFields); + } + } + p.writeInt(mUid); + p.writeBoolean(mUidSpecified); + p.writeInt(mGid); + p.writeBoolean(mGidSpecified); + p.writeIntArray(mGids); + p.writeInt(mRuntimeFlags); + p.writeParcelable(mExtraArgs, 0); + p.writeInt(mMountExternal); + p.writeBoolean(mTargetSdkVersionSpecified); + p.writeInt(mTargetSdkVersion); + p.writeString8(mNiceName); + p.writeBoolean(mCapabilitiesSpecified); + p.writeLong(mPermittedCapabilities); + p.writeLong(mEffectiveCapabilities); + p.writeBoolean(mSeInfoSpecified); + p.writeString8(mSeInfo); + p.writeBoolean(mUsapPoolEnabled); + p.writeBoolean(mUsapPoolStatusSpecified); + int rlimitsListSize = mRLimits != null ? mRLimits.size() : -1; + p.writeInt(rlimitsListSize); + if (rlimitsListSize > 0) { + for (int i = 0; i < rlimitsListSize; ++i) { + p.writeIntArray(mRLimits.get(i)); + } + } + p.writeString8(mInvokeWith); + p.writeString8(mPackageName); + p.writeString8Array(mRemainingArgs); + p.writeBoolean(mAbiListQuery); + p.writeString8(mInstructionSet); + p.writeString8(mAppDataDir); + p.writeString8(mPreloadApp); + p.writeBoolean(mPreloadDefault); + p.writeBoolean(mStartChildZygote); + p.writeBoolean(mPidQuery); + p.writeBoolean(mBootCompleted); + p.writeString8Array(mApiDenylistExemptions); + p.writeInt(mHiddenApiAccessLogSampleRate); + p.writeInt(mHiddenApiAccessStatslogSampleRate); + p.writeBoolean(mIsTopApp); + p.writeLongArray(mDisabledCompatChanges); + p.writeString8Array(mPkgDataInfoList); + p.writeString8Array(mAllowlistedDataInfoList); + p.writeBoolean(mBindMountAppStorageDirs); + p.writeBoolean(mBindMountAppDataDirs); + p.writeBoolean(mBindMountSyspropOverrides); + } + + private ZygoteArguments(Parcel p) { + mUid = p.readInt(); + mUidSpecified = p.readBoolean(); + mGid = p.readInt(); + mGidSpecified = p.readBoolean(); + mGids = p.createIntArray(); + mRuntimeFlags = p.readInt(); + mExtraArgs = p.readParcelable(ZygoteExtraArgs.class.getClassLoader(), ZygoteExtraArgs.class); + mMountExternal = p.readInt(); + mTargetSdkVersionSpecified = p.readBoolean(); + mTargetSdkVersion = p.readInt(); + mNiceName = p.readString8(); + mCapabilitiesSpecified = p.readBoolean(); + mPermittedCapabilities = p.readLong(); + mEffectiveCapabilities = p.readLong(); + mSeInfoSpecified = p.readBoolean(); + mSeInfo = p.readString8(); + mUsapPoolEnabled = p.readBoolean(); + mUsapPoolStatusSpecified = p.readBoolean(); + int rlimitsListSize = p.readInt(); + if (rlimitsListSize == -1) { + mRLimits = null; + } else { + mRLimits = new ArrayList<>(rlimitsListSize); + for (int i = 0; i < rlimitsListSize; ++i) { + mRLimits.add(p.createIntArray()); + } + } + mInvokeWith = p.readString8(); + mPackageName = p.readString8(); + mRemainingArgs = p.createString8Array(); + mAbiListQuery = p.readBoolean(); + mInstructionSet = p.readString8(); + mAppDataDir = p.readString8(); + mPreloadApp = p.readString8(); + mPreloadDefault = p.readBoolean(); + mStartChildZygote = p.readBoolean(); + mPidQuery = p.readBoolean(); + mBootCompleted = p.readBoolean(); + mApiDenylistExemptions = p.createString8Array(); + mHiddenApiAccessLogSampleRate = p.readInt(); + mHiddenApiAccessStatslogSampleRate = p.readInt(); + mIsTopApp = p.readBoolean(); + mDisabledCompatChanges = p.createLongArray(); + mPkgDataInfoList = p.createString8Array(); + mAllowlistedDataInfoList = p.createString8Array(); + mBindMountAppStorageDirs = p.readBoolean(); + mBindMountAppDataDirs = p.readBoolean(); + mBindMountSyspropOverrides = p.readBoolean(); + } + + public static final Parcelable.Creator CREATOR = new Creator<>() { + @Override + public ZygoteArguments createFromParcel(Parcel source) { + return new ZygoteArguments(source); + } + + @Override + public ZygoteArguments[] newArray(int size) { + return new ZygoteArguments[size]; + } + }; /** * from --setuid @@ -72,7 +201,7 @@ class ZygoteArguments { int mRuntimeFlags; /** - * From --flat-extra-args + * From --gos-extra-args */ ZygoteExtraArgs mExtraArgs = ZygoteExtraArgs.DEFAULT; @@ -308,7 +437,10 @@ private void parseArgs(ZygoteCommandBuffer args, int argCount) seenRuntimeArgs = true; } else if (arg.startsWith("--runtime-flags=")) { mRuntimeFlags = Integer.parseInt(getAssignmentValue(arg)); - } else if (arg.startsWith(ZygoteExtraArgs.PREFIX)) { + } else if (arg.equals(ZygoteExtraArgs.ARG_COMPLEX_COMMAND_MARKER)) { + // ignored here, this arg is needed only for isSimpleForkCommand() in + // core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp + } else if (arg.startsWith(ZygoteExtraArgs.ARG_PREFIX)) { mExtraArgs = ZygoteExtraArgs.parse(getAssignmentValue(arg)); } else if (arg.startsWith("--seinfo=")) { if (mSeInfoSpecified) { @@ -528,4 +660,60 @@ private static String getAssignmentValue(String arg) { private static String[] getAssignmentList(String arg) { return getAssignmentValue(arg).split(","); } + + @Override + public String toString() { + var b = new StringBuilder("ZygoteArguments{"); + b.append("mUid=").append(mUid); + b.append(", mUidSpecified=").append(mUidSpecified); + b.append(", mGid=").append(mGid); + b.append(", mGidSpecified=").append(mGidSpecified); + b.append(", mGids=").append(Arrays.toString(mGids)); + b.append(", mRuntimeFlags=").append(Integer.toHexString(mRuntimeFlags)); + b.append(", mExtraArgs=").append(mExtraArgs); + b.append(", mMountExternal=").append(mMountExternal); + b.append(", mTargetSdkVersionSpecified=").append(mTargetSdkVersionSpecified); + b.append(", mTargetSdkVersion=").append(mTargetSdkVersion); + b.append(", mNiceName=").append(mNiceName); + b.append(", mCapabilitiesSpecified=").append(mCapabilitiesSpecified); + b.append(", mPermittedCapabilities=").append(mPermittedCapabilities); + b.append(", mEffectiveCapabilities=").append(mEffectiveCapabilities); + b.append(", mSeInfoSpecified=").append(mSeInfoSpecified); + b.append(", mSeInfo=").append(mSeInfo); + b.append(", mUsapPoolEnabled=").append(mUsapPoolEnabled); + b.append(", mUsapPoolStatusSpecified=").append(mUsapPoolStatusSpecified); + if (mRLimits == null) { + b.append(", mRLimits=null"); + } else { + b.append(", mRLimits=["); + for (int i = 0; i < mRLimits.size(); i++) { + b.append(Arrays.toString(mRLimits.get(i))); + if (i < mRLimits.size() - 1) b.append(", "); + } + b.append("]"); + } + b.append(", mInvokeWith=").append(mInvokeWith); + b.append(", mPackageName=").append(mPackageName); + b.append(", mRemainingArgs=").append(Arrays.toString(mRemainingArgs)); + b.append(", mAbiListQuery=").append(mAbiListQuery); + b.append(", mInstructionSet=").append(mInstructionSet); + b.append(", mAppDataDir=").append(mAppDataDir); + b.append(", mPreloadApp=").append(mPreloadApp); + b.append(", mPreloadDefault=").append(mPreloadDefault); + b.append(", mStartChildZygote=").append(mStartChildZygote); + b.append(", mPidQuery=").append(mPidQuery); + b.append(", mBootCompleted=").append(mBootCompleted); + b.append(", mApiDenylistExemptions=").append(Arrays.toString(mApiDenylistExemptions)); + b.append(", mHiddenApiAccessLogSampleRate=").append(mHiddenApiAccessLogSampleRate); + b.append(", mHiddenApiAccessStatslogSampleRate=").append(mHiddenApiAccessStatslogSampleRate); + b.append(", mIsTopApp=").append(mIsTopApp); + b.append(", mDisabledCompatChanges=").append(Arrays.toString(mDisabledCompatChanges)); + b.append(", mPkgDataInfoList=").append(Arrays.toString(mPkgDataInfoList)); + b.append(", mAllowlistedDataInfoList=").append(Arrays.toString(mAllowlistedDataInfoList)); + b.append(", mBindMountAppStorageDirs=").append(mBindMountAppStorageDirs); + b.append(", mBindMountAppDataDirs=").append(mBindMountAppDataDirs); + b.append(", mBindMountSyspropOverrides=").append(mBindMountSyspropOverrides); + b.append('}'); + return b.toString(); + } } diff --git a/core/java/com/android/internal/os/ZygoteCommandRecorder.java b/core/java/com/android/internal/os/ZygoteCommandRecorder.java new file mode 100644 index 0000000000000..a94e4a0ba45f1 --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteCommandRecorder.java @@ -0,0 +1,94 @@ +package com.android.internal.os; + +import android.os.Parcel; +import android.util.Log; + +import java.util.LinkedHashMap; +import java.util.Objects; + +import static com.android.internal.util.Preconditions.checkState; + +// Records zygote commands that have to be replayed in exec-spawned zygote as part of exec spawning +class ZygoteCommandRecorder { + private static final String TAG = "ZygoteCommandRecorder"; + private static boolean isEnabled; + private static boolean is64bit; + + enum CommandType { + BootCompleted, + ApiDenylistExemptions, + HiddenApiAccessLogSampleRate, + } + + // intentionally using an insertion-ordered map + private static LinkedHashMap capturedCommands; + + static void enable(boolean is64bit) { + Log.d(TAG, "enabled, is64bit: " + is64bit); + checkState(!isEnabled); + isEnabled = true; + ZygoteCommandRecorder.is64bit = is64bit; + capturedCommands = new LinkedHashMap<>(); + } + + static boolean is64bit() { + return is64bit; + } + + static void maybeAddReplayCommand(CommandType type, ZygoteArguments cmd) { + if (!isEnabled) { + return; + } + Objects.requireNonNull(cmd); + + if (ExecSpawning.isExecSpawnedProcess()) { + return; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "maybeAddReplayCommand: added " + type + ": " + cmd, new Throwable()); + } + + capturedCommands.put(type, cmd); + } + + static byte[] addFinalCommandAndSerialize(ZygoteArguments finalCmd) { + checkState(isEnabled); + int numReplayCmds = capturedCommands.size(); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "addFinalCommandAndSerialize: " + finalCmd); + } + + var p = Parcel.obtain(); + byte[] serializedCommands; + try { + p.writeInt(numReplayCmds); + for (ZygoteArguments cmd : capturedCommands.values()) { + cmd.writeToParcel(p, 0); + } + finalCmd.writeToParcel(p, 0); + serializedCommands = p.marshall(); + } finally { + p.recycle(); + } + return serializedCommands; + } + + static ZygoteArguments[] deserializeCommands(byte[] data) { + Parcel p = Parcel.obtain(); + try { + p.unmarshall(data, 0, data.length); + p.setDataPosition(0); + int numReplayCmds = p.readInt(); + ZygoteArguments[] commands = new ZygoteArguments[numReplayCmds + 1]; + for (int i = 0; i < numReplayCmds; ++i) { + commands[i] = ZygoteArguments.CREATOR.createFromParcel(p); + } + commands[numReplayCmds] = ZygoteArguments.CREATOR.createFromParcel(p); + return commands; + } finally { + p.recycle(); + } + } +} diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 5d737de48c2c5..b7ca1b4b80d06 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -22,10 +22,11 @@ import static com.android.internal.os.ZygoteConnectionConstants.CONNECTION_TIMEOUT_MILLIS; import static com.android.internal.os.ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS; +import static com.android.internal.util.Preconditions.checkState; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; -import android.ext.settings.ExtSettings; import android.net.Credentials; import android.net.LocalSocket; import android.os.Parcel; @@ -36,6 +37,8 @@ import android.system.StructPollfd; import android.util.Log; +import com.android.internal.os.ZygoteCommandRecorder.CommandType; + import dalvik.system.VMRuntime; import dalvik.system.ZygoteHooks; @@ -46,8 +49,10 @@ import java.io.DataOutputStream; import java.io.FileDescriptor; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -83,6 +88,12 @@ class ZygoteConnection { mSocket = socket; this.abiList = abiList; + if (ExecSpawning.isReplayingZygoteCommands()) { + mSocketOutStream = new DataOutputStream(OutputStream.nullOutputStream()); + peer = null; + return; + } + mSocketOutStream = new DataOutputStream(socket.getOutputStream()); mSocket.setSoTimeout(CONNECTION_TIMEOUT_MILLIS); @@ -118,18 +129,27 @@ FileDescriptor getFileDescriptor() { * If the client closes the socket, an {@code EOF} condition is set, which callers can test * for by calling {@code ZygoteConnection.isClosedByPeer}. */ - Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { + Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK, @Nullable ZygoteArguments command) { ZygoteArguments parsedArgs; - try (ZygoteCommandBuffer argBuffer = new ZygoteCommandBuffer(mSocket)) { + boolean isReplayingZygoteCommands = ExecSpawning.isReplayingZygoteCommands(); + + if (!isReplayingZygoteCommands) { + checkState(command == null); + } + + try (ZygoteCommandBuffer argBuffer = + command != null ? null : + new ZygoteCommandBuffer(mSocket)) { while (true) { try { - parsedArgs = ZygoteArguments.getInstance(argBuffer); + parsedArgs = command != null ? command : ZygoteArguments.getInstance(argBuffer); // Keep argBuffer around, since we need it to fork. } catch (IOException ex) { throw new IllegalStateException("IOException on command socket", ex); } if (parsedArgs == null) { + checkState(!isReplayingZygoteCommands); isEof = true; return null; } @@ -139,16 +159,19 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { FileDescriptor serverPipeFd = null; if (parsedArgs.mBootCompleted) { + ZygoteCommandRecorder.maybeAddReplayCommand(CommandType.BootCompleted, parsedArgs); handleBootCompleted(); return null; } if (parsedArgs.mAbiListQuery) { + checkState(!isReplayingZygoteCommands); handleAbiListQuery(); return null; } if (parsedArgs.mPidQuery) { + checkState(!isReplayingZygoteCommands); handlePidQuery(); return null; } @@ -162,11 +185,13 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { } if (parsedArgs.mPreloadDefault) { + checkState(!isReplayingZygoteCommands); handlePreload(); return null; } if (canPreloadApp() && parsedArgs.mPreloadApp != null) { + checkState(!isReplayingZygoteCommands); byte[] rawParcelData = Base64.getDecoder().decode(parsedArgs.mPreloadApp); Parcel appInfoParcel = Parcel.obtain(); appInfoParcel.unmarshall(rawParcelData, 0, rawParcelData.length); @@ -190,11 +215,13 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { + Long.toHexString(parsedArgs.mEffectiveCapabilities)); } - Zygote.applyUidSecurityPolicy(parsedArgs, peer); - Zygote.applyInvokeWithSecurityPolicy(parsedArgs, peer); + if (!isReplayingZygoteCommands) { + Zygote.applyUidSecurityPolicy(parsedArgs, peer); + Zygote.applyInvokeWithSecurityPolicy(parsedArgs, peer); - Zygote.applyDebuggerSystemProperty(parsedArgs); - Zygote.applyInvokeWithSystemProperty(parsedArgs); + Zygote.applyDebuggerSystemProperty(parsedArgs); + Zygote.applyInvokeWithSystemProperty(parsedArgs); + } int[][] rlimits = null; @@ -205,6 +232,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { int[] fdsToIgnore = null; if (parsedArgs.mInvokeWith != null) { + checkState(!isReplayingZygoteCommands); try { FileDescriptor[] pipeFds = Os.pipe2(O_CLOEXEC); childPipeFd = pipeFds[1]; @@ -231,37 +259,53 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { int [] fdsToClose = { -1, -1 }; - FileDescriptor fd = mSocket.getFileDescriptor(); + if (!isReplayingZygoteCommands) { // ZygoteServer sockets are null in exec spawned zygote + FileDescriptor fd = mSocket.getFileDescriptor(); - if (fd != null) { - fdsToClose[0] = fd.getInt$(); - } + if (fd != null) { + fdsToClose[0] = fd.getInt$(); + } - FileDescriptor zygoteFd = zygoteServer.getZygoteSocketFileDescriptor(); + FileDescriptor zygoteFd = zygoteServer.getZygoteSocketFileDescriptor(); - if (zygoteFd != null) { - fdsToClose[1] = zygoteFd.getInt$(); + if (zygoteFd != null) { + fdsToClose[1] = zygoteFd.getInt$(); + } } - if (parsedArgs.mInvokeWith != null || ExtSettings.EXEC_SPAWNING.get() || parsedArgs.mStartChildZygote - || !multipleOK || peer.getUid() != Process.SYSTEM_UID - || (parsedArgs.mRuntimeFlags & Zygote.RUNTIME_FLAGS_DEPENDENT_ON_EXEC_SPAWNING) != 0) { - Log.w(TAG, "Resorting to Java fork code; multipleOK = " + multipleOK - + (parsedArgs.mInvokeWith != null ? "; invokeWith used" : "")); - // Continue using old code for now. TODO: Handle these cases in the other path. - pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid, - parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits, - parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName, - fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote, - parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, - parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList, - parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs, - parsedArgs.mBindMountAppStorageDirs, - parsedArgs.mBindMountSyspropOverrides, parsedArgs.mExtraArgs); + boolean shouldUseExecSpawning = !isReplayingZygoteCommands + && parsedArgs.mExtraArgs.shouldUseExecSpawning() + && parsedArgs.mInvokeWith == null + && !parsedArgs.mStartChildZygote; + if (shouldUseExecSpawning || isReplayingZygoteCommands || parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote + || !multipleOK || peer.getUid() != Process.SYSTEM_UID) { + + if (shouldUseExecSpawning) { + byte[] commands = ZygoteCommandRecorder.addFinalCommandAndSerialize(parsedArgs); + pid = Zygote.nativeForkExec( + ZygoteCommandRecorder.is64bit(), + commands, + parsedArgs.mExtraArgs.hasFlag(ZygoteExtraArgs.Flag.DISABLE_HARDENED_MALLOC), + parsedArgs.mExtraArgs.hasFlag(ZygoteExtraArgs.Flag.ENABLE_COMPAT_VA_39_BIT)); + } else { + Log.d(TAG, "Resorting to Java fork code; isReplayingZygoteCommands: " + isReplayingZygoteCommands + " multipleOK = " + multipleOK + + (parsedArgs.mInvokeWith != null ? "; invokeWith used" : "")); + // Continue using old code for now. TODO: Handle these cases in the other path. + pid = Zygote.forkAndSpecialize(parsedArgs.mExtraArgs, parsedArgs.mUid, parsedArgs.mGid, + parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits, + parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName, + fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote, + parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, + parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList, + parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs, + parsedArgs.mBindMountAppStorageDirs, + parsedArgs.mBindMountSyspropOverrides); + } try { if (pid == 0) { // in child + checkState(!shouldUseExecSpawning); // should never be reached zygoteServer.setForkChild(); zygoteServer.closeServerSocket(); @@ -271,6 +315,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote); } else { + checkState(!isReplayingZygoteCommands); // In the parent. A pid < 0 indicates a failure and will be handled in // handleParentProc. IoUtils.closeQuietly(childPipeFd); @@ -283,6 +328,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { IoUtils.closeQuietly(serverPipeFd); } } else { + Objects.requireNonNull(argBuffer); ZygoteHooks.preFork(); Runnable result = Zygote.forkSimpleApps(argBuffer, zygoteServer.getZygoteSocketFileDescriptor(), @@ -304,14 +350,17 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { } // Handle anything that may need a ZygoteCommandBuffer after we've released ours. if (parsedArgs.mUsapPoolStatusSpecified) { + checkState(!isReplayingZygoteCommands); return handleUsapPoolStatusChange(zygoteServer, parsedArgs.mUsapPoolEnabled); } if (parsedArgs.mApiDenylistExemptions != null) { + ZygoteCommandRecorder.maybeAddReplayCommand(CommandType.ApiDenylistExemptions, parsedArgs); return handleApiDenylistExemptions(zygoteServer, parsedArgs.mApiDenylistExemptions); } if (parsedArgs.mHiddenApiAccessLogSampleRate != -1 || parsedArgs.mHiddenApiAccessStatslogSampleRate != -1) { + ZygoteCommandRecorder.maybeAddReplayCommand(CommandType.HiddenApiAccessLogSampleRate, parsedArgs); return handleHiddenApiAccessLogSampleRate(zygoteServer, parsedArgs.mHiddenApiAccessLogSampleRate, parsedArgs.mHiddenApiAccessStatslogSampleRate); @@ -321,6 +370,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { private void handleAbiListQuery() { try { + Log.d(TAG, "handleAbiListQuery"); final byte[] abiListBytes = abiList.getBytes(StandardCharsets.US_ASCII); mSocketOutStream.writeInt(abiListBytes.length); mSocketOutStream.write(abiListBytes); @@ -341,6 +391,7 @@ private void handlePidQuery() { } private void handleBootCompleted() { + Log.d(TAG, "handleBootCompleted"); try { mSocketOutStream.writeInt(0); } catch (IOException ioe) { @@ -485,11 +536,15 @@ protected void handlePreloadApp(ApplicationInfo aInfo) { */ @UnsupportedAppUsage void closeSocket() { - try { - mSocket.close(); - } catch (IOException ex) { - Log.e(TAG, "Exception while closing command " - + "socket in parent", ex); + if (ExecSpawning.isReplayingZygoteCommands()) { + checkState(mSocket == null); + } else { + try { + mSocket.close(); + } catch (IOException ex) { + Log.e(TAG, "Exception while closing command " + + "socket in parent", ex); + } } } @@ -530,20 +585,6 @@ private Runnable handleChildProc(ZygoteArguments parsedArgs, throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned"); } else { if (!isZygote) { - final int runtimeFlags = parsedArgs.mRuntimeFlags; - boolean useExecInit = - ((runtimeFlags & Zygote.RUNTIME_FLAGS_DEPENDENT_ON_EXEC_SPAWNING) != 0 - || ExtSettings.EXEC_SPAWNING.get()) - && - (runtimeFlags & ApplicationInfo.FLAG_DEBUGGABLE) == 0; - - if (useExecInit) { - ExecInit.execApplication(parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion, - VMRuntime.getCurrentInstructionSet(), runtimeFlags, parsedArgs.mRemainingArgs); - - // Should not get here. - throw new IllegalStateException("ExecInit.execApplication unexpectedly returned"); - } return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion, parsedArgs.mDisabledCompatChanges, parsedArgs.mRemainingArgs, null /* classLoader */); diff --git a/core/java/com/android/internal/os/ZygoteExtraArgs.java b/core/java/com/android/internal/os/ZygoteExtraArgs.java index 755c44841115c..ee51bbe104a16 100644 --- a/core/java/com/android/internal/os/ZygoteExtraArgs.java +++ b/core/java/com/android/internal/os/ZygoteExtraArgs.java @@ -1,60 +1,165 @@ package com.android.internal.os; -import android.annotation.Nullable; +import android.annotation.IntDef; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.GosPackageState; +import android.ext.settings.app.AswUseExecSpawning; +import android.ext.settings.app.AswUseExtendedVaSpace; +import android.ext.settings.app.AswUseHardenedMalloc; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ZygoteProcess; -// Extra args for: -// - children of main zygote{,64}, including AppZygotes, but excluding WebViewZygote -// - children of WebViewZygote -// -// AppZygote is treated differently from WebViewZygote because the former runs untrusted app code -// (see android.app.ZygotePreload). -public class ZygoteExtraArgs { - public final long selinuxFlags; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.HexFormat; - public static final String PREFIX = "--flat-extra-args="; +public class ZygoteExtraArgs implements Parcelable { + private long selinuxFlags; + private int flags; - public static final ZygoteExtraArgs DEFAULT = new ZygoteExtraArgs(0L); + public static final String ARG_PREFIX = "--gos-extra-args="; + // checked by isSimpleForkCommand() in core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp + public static final String ARG_COMPLEX_COMMAND_MARKER = "--is-complex-zygote-command"; - public ZygoteExtraArgs(long selinuxFlags) { - this.selinuxFlags = selinuxFlags; + public static final ZygoteExtraArgs DEFAULT = new ZygoteExtraArgs(); + + private ZygoteExtraArgs() {} + + // keep in sync with ExtraArgsFlag in core/jni/com_android_internal_os_Zygote.cpp + public interface Flag { + // hardened_malloc is always disabled when PREFER_COMPAT_ZYGOTE is set + int DISABLE_HARDENED_MALLOC = 1; + // 39-bit is available only on arm64 and is always enabled when PREFER_COMPAT_ZYGOTE is set + int ENABLE_COMPAT_VA_39_BIT = 1 << 1; + int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 2; + int USE_ZYGOTE_SPAWNING = 1 << 3; + int PREFER_COMPAT_ZYGOTE = 1 << 4; + + @IntDef(flag = true, value = { + DISABLE_HARDENED_MALLOC, + ENABLE_COMPAT_VA_39_BIT, + FORCIBLY_ENABLE_MEMORY_TAGGING, + PREFER_COMPAT_ZYGOTE, + }) + @Retention(RetentionPolicy.SOURCE) + @interface Enum {} } - private static final int IDX_SELINUX_FLAGS = 0; - private static final int ARR_LEN = 1; - private static final String SEPARATOR = "\t"; + public static ZygoteExtraArgs create(Context ctx, int userId, ApplicationInfo appInfo, + boolean shouldForciblyEnableMemoryTagging, + GosPackageState ps, + boolean isIsolatedProcess) { + var res = new ZygoteExtraArgs(); + res.selinuxFlags = SELinuxFlags.get(ctx, userId, appInfo, ps, isIsolatedProcess); + boolean useZygoteSpawning = !AswUseExecSpawning.I.get(ctx, userId, appInfo, ps); + if (useZygoteSpawning) { + res.setFlag(Flag.USE_ZYGOTE_SPAWNING, true); + if (!AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)) { + res.setFlag(Flag.PREFER_COMPAT_ZYGOTE, true); + } + } else { + res.setFlag(Flag.DISABLE_HARDENED_MALLOC, !AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)); + res.setFlag(Flag.ENABLE_COMPAT_VA_39_BIT, !AswUseExtendedVaSpace.I.get(ctx, userId, appInfo, ps)); + } + res.setFlag(Flag.FORCIBLY_ENABLE_MEMORY_TAGGING, shouldForciblyEnableMemoryTagging); + return res; + } + + public static ZygoteExtraArgs createForWebviewZygote() { + return DEFAULT; + } + + public static ZygoteExtraArgs createForWebviewProcess(Context ctx, int userId, + ApplicationInfo callerAppInfo, GosPackageState callerPs) { + var res = new ZygoteExtraArgs(); + res.selinuxFlags = SELinuxFlags.getForWebViewProcess(ctx, userId, callerAppInfo, callerPs); + res.setFlag(Flag.USE_ZYGOTE_SPAWNING, !AswUseExecSpawning.I.get(ctx, userId, callerAppInfo, callerPs)); + return res; + } + + public boolean hasFlag(@Flag.Enum int flag) { + return (this.flags & flag) == flag; + } - public static String createFlat(Context ctx, int userId, ApplicationInfo appInfo, - GosPackageState ps, - boolean isIsolatedProcess) { - String[] arr = new String[ARR_LEN]; - arr[IDX_SELINUX_FLAGS] = Long.toHexString( - SELinuxFlags.get(ctx, userId, appInfo, ps, isIsolatedProcess) - ); - return PREFIX + String.join(SEPARATOR, arr); + void setFlag(int flag, boolean value) { + if (value) { + this.flags |= flag; + } else { + this.flags &= ~flag; + } } - public static String createFlatForWebviewProcess(Context ctx, int userId, - ApplicationInfo callerAppInfo, GosPackageState callerPs) { - String[] arr = new String[ARR_LEN]; - arr[IDX_SELINUX_FLAGS] = Long.toHexString( - SELinuxFlags.getForWebViewProcess(ctx, userId, callerAppInfo, callerPs) - ); - return PREFIX + String.join(SEPARATOR, arr); + public boolean shouldUseExecSpawning() { + return !hasFlag(Flag.USE_ZYGOTE_SPAWNING); } - static ZygoteExtraArgs parse(String flat) { - String[] arr = flat.split(SEPARATOR); - long selinuxFlags = Long.parseLong(arr[IDX_SELINUX_FLAGS], 16); - return new ZygoteExtraArgs(selinuxFlags); + static ZygoteExtraArgs parse(String argValue) { + byte[] serialized = HexFormat.of().parseHex(argValue); + Parcel p = Parcel.obtain(); + try { + p.unmarshall(serialized, 0, serialized.length); + p.setDataPosition(0); + return new ZygoteExtraArgs(p); + } finally { + p.recycle(); + } + } + + public void toZygoteArgList(ArrayList argsForZygote) { + if (shouldUseExecSpawning()) { + argsForZygote.add(ARG_COMPLEX_COMMAND_MARKER); + } + byte[] serialized; + Parcel p = Parcel.obtain(); + try { + writeToParcel(p, 0); + serialized = p.marshall(); + } finally { + p.recycle(); + } + argsForZygote.add(ARG_PREFIX + HexFormat.of().formatHex(serialized)); } // keep in sync with ExtraArgs struct in core/jni/com_android_internal_os_Zygote.cpp + private static final int IDX_SELINUX_FLAGS = 0; + private static final int IDX_FLAGS = 1; + private static final int ARR_LEN = 2; + public long[] makeJniLongArray() { long[] res = new long[ARR_LEN]; res[IDX_SELINUX_FLAGS] = selinuxFlags; + res[IDX_FLAGS] = flags; return res; } + + @Override + public void writeToParcel(Parcel p, int parcelFlags) { + p.writeLong(this.selinuxFlags); + p.writeInt(this.flags); + } + + ZygoteExtraArgs(Parcel p) { + selinuxFlags = p.readLong(); + flags = p.readInt(); + } + + public static final Parcelable.Creator CREATOR = new Creator<>() { + @Override + public ZygoteExtraArgs createFromParcel(Parcel p) { + return new ZygoteExtraArgs(p); + } + + @Override + public ZygoteExtraArgs[] newArray(int size) { + return new ZygoteExtraArgs[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 6ae653d4d601a..f282b26a1d587 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -20,6 +20,7 @@ import static android.system.OsConstants.S_IRWXG; import static android.system.OsConstants.S_IRWXO; +import static com.android.internal.os.ExecSpawning.COMMAND_FD_ARG; import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START; import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START; @@ -828,24 +829,36 @@ private static Runnable forkSystemServer(String abiList, String socketName, */ @UnsupportedAppUsage public static void main(String[] argv) { + try { + ExecSpawning.init(argv); + } catch (Throwable e) { + Log.e(TAG, "ExecSpawning init failed", e); + throw e; + } + ZygoteServer zygoteServer = null; + + boolean isExecSpawning = ExecSpawning.isExecSpawnedProcess(); + // Mark zygote start. This ensures that thread creation will throw // an error. ZygoteHooks.startZygoteNoThreadCreation(); - // Zygote goes into its own process group. - try { - Os.setpgid(0, 0); - } catch (ErrnoException ex) { - throw new RuntimeException("Failed to setpgid(0,0)", ex); + if (!isExecSpawning) { + // Zygote goes into its own process group. + try { + Os.setpgid(0, 0); + } catch (ErrnoException ex) { + throw new RuntimeException("Failed to setpgid(0,0)", ex); + } } Runnable caller; try { // Store now for StatsLogging later. final long startTime = SystemClock.elapsedRealtime(); - final boolean isRuntimeRestarted = "1".equals( + final boolean isRuntimeRestarted = !isExecSpawning && "1".equals( SystemProperties.get("sys.boot_completed")); String bootTimeTag = Process.is64Bit() ? "Zygote64Timing" : "Zygote32Timing"; @@ -867,18 +880,22 @@ public static void main(String[] argv) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length()); + } else if (argv[i].startsWith(COMMAND_FD_ARG)) { + Preconditions.checkState(isExecSpawning); + // handled by ExecSpawning.init() + continue; } else { throw new RuntimeException("Unknown command line argument: " + argv[i]); } } - final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME); - if (!isRuntimeRestarted) { - if (isPrimaryZygote) { + if (!isExecSpawning && !isRuntimeRestarted) { + ZygoteType zygoteType = ZygoteType.fromSocketName(zygoteSocketName); + if (zygoteType == ZygoteType.Primary) { FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED, BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START, startTime); - } else if (zygoteSocketName.equals(Zygote.SECONDARY_SOCKET_NAME)) { + } else if (zygoteType == ZygoteType.Secondary) { FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED, BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START, startTime); @@ -895,7 +912,7 @@ public static void main(String[] argv) { bootTimingsTraceLog.traceBegin("ZygotePreload"); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); - preload(bootTimingsTraceLog); + preload(bootTimingsTraceLog, !isExecSpawning); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis()); bootTimingsTraceLog.traceEnd(); // ZygotePreload @@ -908,11 +925,13 @@ public static void main(String[] argv) { bootTimingsTraceLog.traceEnd(); // ZygoteInit - Zygote.initNativeState(isPrimaryZygote); + final ZygoteType zygoteType = ZygoteType.fromSocketName(zygoteSocketName); + + Zygote.initNativeState(zygoteType); ZygoteHooks.stopZygoteNoThreadCreation(); - zygoteServer = new ZygoteServer(isPrimaryZygote); + zygoteServer = isExecSpawning ? new ZygoteServer() : new ZygoteServer(zygoteType); if (startSystemServer) { Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer); @@ -925,13 +944,13 @@ public static void main(String[] argv) { } } - Log.i(TAG, "Accepting command socket connections"); + if (!isExecSpawning) Log.i(TAG, "Accepting command socket connections"); // The select loop returns early in the child process after a fork and // loops forever in the zygote. caller = zygoteServer.runSelectLoop(abiList); } catch (Throwable ex) { - Log.e(TAG, "System zygote died with fatal exception", ex); + Log.e(TAG, (isExecSpawning ? "Exec spawned process" : "System zygote died") + " with fatal exception", ex); throw ex; } finally { if (zygoteServer != null) { @@ -957,9 +976,9 @@ private static boolean hasSecondZygote(String abiList) { } private static void waitForSecondaryZygote(String socketName) { - String otherZygoteName = Zygote.PRIMARY_SOCKET_NAME.equals(socketName) - ? Zygote.SECONDARY_SOCKET_NAME : Zygote.PRIMARY_SOCKET_NAME; - ZygoteProcess.waitForConnectionToZygote(otherZygoteName); + ZygoteType otherZygoteType = ZygoteType.Primary.getSocketName().equals(socketName) + ? ZygoteType.Secondary : ZygoteType.Primary; + ZygoteProcess.waitForConnectionToZygote(otherZygoteType); } static boolean isPreloadComplete() { diff --git a/core/java/com/android/internal/os/ZygoteServer.java b/core/java/com/android/internal/os/ZygoteServer.java index f8598f2f471a6..3e111e6af180d 100644 --- a/core/java/com/android/internal/os/ZygoteServer.java +++ b/core/java/com/android/internal/os/ZygoteServer.java @@ -392,6 +392,10 @@ private void resetUsapRefillState() { * @param abiList list of ABIs supported by this zygote. */ Runnable runSelectLoop(String abiList) { + if (ExecSpawning.isReplayingZygoteCommands()) { + return ExecSpawning.replayCommands(this); + } + ArrayList socketFDs = new ArrayList<>(); ArrayList peers = new ArrayList<>(); @@ -518,7 +522,7 @@ Runnable runSelectLoop(String abiList) { boolean multipleForksOK = !isUsapPoolEnabled() && ZygoteHooks.isIndefiniteThreadSuspensionSafe(); final Runnable command = - connection.processCommand(this, multipleForksOK); + connection.processCommand(this, multipleForksOK, null); // TODO (chriswailes): Is this extra check necessary? if (mIsForkChild) { diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 1e6a32ad42f8b..63fca40e8184e 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,7 @@ #include #include #include +#include #include #include #include @@ -168,6 +170,8 @@ static int gUsapPoolEventFD = -1; */ static int gSystemServerSocketFd = -1; +static bool gIsExecSpawning = false; + static constexpr int DEFAULT_DATA_DIR_PERMISSION = 0751; static constexpr const uint64_t UPPER_HALF_WORD_MASK = 0xFFFF'FFFF'0000'0000; @@ -354,16 +358,26 @@ enum RuntimeFlags : uint32_t { ENABLE_PAGE_SIZE_APP_COMPAT = 1 << 26, }; +namespace ExtraArgsFlag { + static const int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 2; +} + struct ExtraArgs { uint64_t selinux_flags = 0; + int flags = 0; ExtraArgs() {} ExtraArgs(JNIEnv* env, jlongArray jlongArgs) { - const size_t num_jlong_args = 1; + const size_t num_jlong_args = 2; jlong jlong_arr[num_jlong_args]; env->GetLongArrayRegion(jlongArgs, 0, num_jlong_args, (jlong *) &jlong_arr); selinux_flags = (uint64_t) jlong_arr[0]; + flags = (int) jlong_arr[1]; + } + + bool hasFlag(int flag) { + return flags & flag; } }; @@ -908,6 +922,9 @@ static void DetachDescriptors(JNIEnv* env, } for (int fd : fds_to_close) { + if (fd == -1 && gIsExecSpawning) { + continue; + } ALOGV("Switching descriptor %d to /dev/null", fd); if (TEMP_FAILURE_RETRY(dup3(devnull_fd, fd, O_CLOEXEC)) == -1) { fail_fn(StringPrintf("Failed dup3() on descriptor %d: %s", fd, strerror(errno))); @@ -1816,7 +1833,7 @@ static void BindMountStorageDirs(JNIEnv* env, jobjectArray pkg_data_info_list, } // Utility routine to specialize a zygote child process. -static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags, +static void SpecializeCommon(JNIEnv* env, ExtraArgs& extra_args, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities, jlong effective_capabilities, jlong bounding_capabilities, jint mount_external, jstring managed_se_info, @@ -1824,8 +1841,7 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jstring managed_instruction_set, jstring managed_app_data_dir, bool is_top_app, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, bool mount_data_dirs, - bool mount_storage_dirs, bool mount_sysprop_overrides, - ExtraArgs& extra_args) { + bool mount_storage_dirs, bool mount_sysprop_overrides) { const char* process_name = is_system_server ? "system_server" : "zygote"; auto fail_fn = std::bind(ZygoteFailure, env, process_name, managed_nice_name, _1); auto extract_fn = std::bind(ExtractJString, env, process_name, managed_nice_name, _1); @@ -2019,6 +2035,16 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, } mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, heap_tagging_level); + if (extra_args.hasFlag(ExtraArgsFlag::FORCIBLY_ENABLE_MEMORY_TAGGING)) { + ALOGD("FORCIBLY_ENABLE_MEMORY_TAGGING is set, enabling M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE and M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION"); + if (mallopt(M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE, 0) != (int) true) { + RuntimeAbort(env, __LINE__, "mallopt(M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE) failed"); + } + if (mallopt(M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION, 0) != (int) true) { + RuntimeAbort(env, __LINE__, "mallopt(M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION) failed"); + } + } + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART // runtime. runtime_flags &= ~RuntimeFlags::MEMORY_TAG_LEVEL_MASK; @@ -2426,7 +2452,7 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, android_fdsan_error_level fdsan_error_level = android_fdsan_get_error_level(); - if (purge) { + if (!gIsExecSpawning && purge) { // Purge unused native memory in an attempt to reduce the amount of false // sharing with the child process. By reducing the size of the libc_malloc // region shared with the child process we reduce the number of pages that @@ -2437,7 +2463,7 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, } } - pid_t pid = fork(); + pid_t pid = gIsExecSpawning ? 0 : fork(); if (pid == 0) { if (is_top_app && use_fifo_ui) { @@ -2461,7 +2487,7 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, #if defined(__BIONIC__) && !defined(NO_RESET_STACK_PROTECTOR) // Reset the stack guard for the new process. - android_reset_stack_guards(); + if (!gIsExecSpawning) android_reset_stack_guards(); #endif // The child process. @@ -2505,6 +2531,8 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_DEFAULT); } + gIsExecSpawning = false; + return pid; } @@ -2514,12 +2542,12 @@ static void com_android_internal_os_Zygote_nativePreApplicationInit(JNIEnv*, jcl NO_STACK_PROTECTOR static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( - JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags, + JNIEnv* env, jclass, jlongArray extra_jlong_args, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jintArray managed_fds_to_close, jintArray managed_fds_to_ignore, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jboolean use_fifo_ui, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, - jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides, jlongArray extra_jlong_args) { + jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) { ExtraArgs extra_args(env, extra_jlong_args); jlong capabilities = zygote::CalculateCapabilities(env, uid, gid, gids, is_child_zygote); jlong bounding_capabilities = zygote::CalculateBoundingCapabilities(env, uid, gid, gids); @@ -2560,12 +2588,12 @@ static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( true, is_top_app == JNI_TRUE, use_fifo_ui == JNI_TRUE); if (pid == 0) { - SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, + SpecializeCommon(env, extra_args, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, bounding_capabilities, mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE, instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list, mount_data_dirs == JNI_TRUE, mount_storage_dirs == JNI_TRUE, - mount_sysprop_overrides == JNI_TRUE, extra_args); + mount_sysprop_overrides == JNI_TRUE); } return pid; } @@ -2599,11 +2627,11 @@ static jint com_android_internal_os_Zygote_nativeForkSystemServer( // System server prcoess does not need data isolation so no need to // know pkg_data_info_list. ExtraArgs extra_args; - SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities, + SpecializeCommon(env, extra_args, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities, effective_capabilities, 0, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true, false, nullptr, nullptr, /* is_top_app= */ false, /* pkg_data_info_list */ nullptr, - /* allowlisted_data_info_list */ nullptr, false, false, false, extra_args); + /* allowlisted_data_info_list */ nullptr, false, false, false); } else if (pid > 0) { // The zygote process checks whether the child process has died or not. ALOGI("System server process %d has been created", pid); @@ -2746,22 +2774,22 @@ static void com_android_internal_os_Zygote_nativeInstallSeccompUidGidFilter( * @param is_top_app If the process is for top (high priority) application */ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess( - JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags, + JNIEnv* env, jclass, jlongArray extra_jlong_args, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, jboolean mount_data_dirs, - jboolean mount_storage_dirs, jboolean mount_sysprop_overrides, jlongArray extra_jlong_args) { - ExtraArgs extra_args(env, extra_jlong_args); + jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) { jlong capabilities = zygote::CalculateCapabilities(env, uid, gid, gids, is_child_zygote); jlong bounding_capabilities = zygote::CalculateBoundingCapabilities(env, uid, gid, gids); + ExtraArgs extra_args(env, extra_jlong_args); - SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, + SpecializeCommon(env, extra_args, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, bounding_capabilities, mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE, instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list, mount_data_dirs == JNI_TRUE, mount_storage_dirs == JNI_TRUE, - mount_sysprop_overrides == JNI_TRUE, extra_args); + mount_sysprop_overrides == JNI_TRUE); } /** @@ -2773,28 +2801,38 @@ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess( * of the environment variable storing the file descriptors. */ static void com_android_internal_os_Zygote_nativeInitNativeState(JNIEnv* env, jclass, - jboolean is_primary) { + jboolean is_exec_spawning, + jstring j_socket_name, + jstring j_usap_pool_socket_name) { + gIsExecSpawning = is_exec_spawning; /* * Obtain file descriptors created by init from the environment. */ - gZygoteSocketFD = - android_get_control_socket(is_primary ? "zygote" : "zygote_secondary"); - if (gZygoteSocketFD >= 0) { - ALOGV("Zygote:zygoteSocketFD = %d", gZygoteSocketFD); - } else { - ALOGE("Unable to fetch Zygote socket file descriptor"); - } + if (!gIsExecSpawning) { + ScopedUtfChars socket_name(env, j_socket_name); + ScopedUtfChars usap_pool_socket_name(env, j_usap_pool_socket_name); - gUsapPoolSocketFD = - android_get_control_socket(is_primary ? "usap_pool_primary" : "usap_pool_secondary"); - if (gUsapPoolSocketFD >= 0) { - ALOGV("Zygote:usapPoolSocketFD = %d", gUsapPoolSocketFD); - } else { - ALOGE("Unable to fetch USAP pool socket file descriptor"); + gZygoteSocketFD = + android_get_control_socket(socket_name.c_str()); + if (gZygoteSocketFD >= 0) { + ALOGV("Zygote:zygoteSocketFD = %d", gZygoteSocketFD); + } else { + ALOGE("Unable to fetch Zygote socket file descriptor"); + } + + gUsapPoolSocketFD = + android_get_control_socket(usap_pool_socket_name.c_str()); + if (gUsapPoolSocketFD >= 0) { + ALOGV("Zygote:usapPoolSocketFD = %d", gUsapPoolSocketFD); + } else { + ALOGE("Unable to fetch USAP pool socket file descriptor"); + } } - initUnsolSocketToSystemServer(); + if (!gIsExecSpawning) { + initUnsolSocketToSystemServer(); + } /* * Security Initialization @@ -3007,6 +3045,106 @@ static jint com_android_internal_os_Zygote_nativeCurrentTaggingLevel(JNIEnv* env #endif // defined(__aarch64__) } +static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, + jboolean is_64_bit, + jbyteArray command_buf, + jboolean disable_hardened_malloc, + jboolean enable_compat_va_39_bit) { + auto fail_fn = std::bind(zygote::ZygoteFailure, env, + "exec_spawning_zygote", + nullptr, _1); + + int cmd_fd = memfd_create("zygote_fork_exec_cmds", 0); + if (cmd_fd < 0) { + ALOGE("memfd_create failed: %s", strerror(errno)); + return -1; + } + size_t cmd_buf_size; + { + ScopedByteArrayRO cmd(env, command_buf); + cmd_buf_size = cmd.size(); + if (!android::base::WriteFully(cmd_fd, cmd.get(), cmd_buf_size)) { + ALOGE("WriteFully failed: %s", strerror(errno)); + close(cmd_fd); + return -1; + } + + if (lseek(cmd_fd, 0, SEEK_SET) != 0) { + env->FatalError(CREATE_ERROR("unable to lseek cmd_fd: %s", strerror(errno)).c_str()); + } + } + + char cmd_fd_arg[50]; + snprintf(cmd_fd_arg, sizeof(cmd_fd_arg), "--command-fd=%d_%zu", cmd_fd, cmd_buf_size); + + const char *const argv[] = { + is_64_bit ? "/system/bin/app_process64" : "/system/bin/app_process32", + "-Xzygote", + "/system/bin", + "--zygote", + cmd_fd_arg, + nullptr, + }; + + if (disable_hardened_malloc) { + if (setenv("DISABLE_HARDENED_MALLOC", "1", 1) != 0) { + ALOGE("setenv failed: %s", strerror(errno)); + close(cmd_fd); + return -1; + } + } + + // see zygote::ForkCommon for the reasoning behind this sequence + // (SetSignalHandlers -> block SIGCHLD -> get open fds) + SetSignalHandlers(); + BlockSignal(SIGCHLD, fail_fn); + + std::vector fds_to_ignore; + fds_to_ignore.push_back(cmd_fd); + std::unique_ptr> open_fds = GetOpenFdsIgnoring(fds_to_ignore, fail_fn); + + pid_t pid = fork(); + UnblockSignal(SIGCHLD, fail_fn); + + if (pid != 0) { + // parent process + if (pid == -1) { + ALOGE("fork failed: %s", strerror(errno)); + } + close(cmd_fd); + if (disable_hardened_malloc) { + if (unsetenv("DISABLE_HARDENED_MALLOC") != 0) { + env->FatalError(CREATE_ERROR("unsetenv(DISABLE_HARDENED_MALLOC) failed: %s", strerror(errno)).c_str()); + } + } + return pid; + } else { + // close all file descriptors except for the command file descriptor + for (int fd : *open_fds) { + if (close(fd) != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close(%d) failed: %s", fd, strerror(errno)); + } + } +#if defined(__aarch64__) + const int FLAG_COMPAT_VA_39_BIT = 1 << 30; + execveat(-1, argv[0], (char **) argv, environ, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execveat failed: %s", strerror(errno)); + if (errno == EINVAL) { + // kernel doesn't support FLAG_COMPAT_VA_39_BIT, or a different error that will + // be returned by execv() anyway + execv(argv[0], (char **) argv); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %s", strerror(errno)); + } +#else + execv(argv[0], (char **) argv); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %s", strerror(errno)); +#endif // defined(__aarch64__) + + // exec failed + _exit(1); + } +} + static void com_android_internal_os_Zygote_nativeMarkOpenedFilesBeforePreload(JNIEnv* env, jclass) { // Ignore invocations when too early or too late. if (gPreloadFds) { @@ -3037,9 +3175,11 @@ static void com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload(JNIEn } static const JNINativeMethod gMethods[] = { + {"nativeForkExec", "(Z[BZZ)I", + (void*)com_android_internal_os_Zygote_nativeForkExec}, {"nativeForkAndSpecialize", - "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" - "String;ZZ[Ljava/lang/String;[Ljava/lang/String;ZZZ[J)I", + "([JII[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" + "String;ZZ[Ljava/lang/String;[Ljava/lang/String;ZZZ)I", (void*)com_android_internal_os_Zygote_nativeForkAndSpecialize}, {"nativeForkSystemServer", "(II[II[[IJJ)I", (void*)com_android_internal_os_Zygote_nativeForkSystemServer}, @@ -3054,10 +3194,10 @@ static const JNINativeMethod gMethods[] = { {"nativeAddUsapTableEntry", "(II)V", (void*)com_android_internal_os_Zygote_nativeAddUsapTableEntry}, {"nativeSpecializeAppProcess", - "(II[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/" - "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZZ[J)V", + "([JII[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/" + "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZZ)V", (void*)com_android_internal_os_Zygote_nativeSpecializeAppProcess}, - {"nativeInitNativeState", "(Z)V", + {"nativeInitNativeState", "(ZLjava/lang/String;Ljava/lang/String;)V", (void*)com_android_internal_os_Zygote_nativeInitNativeState}, {"nativeGetUsapPipeFDs", "()[I", (void*)com_android_internal_os_Zygote_nativeGetUsapPipeFDs}, diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index 2fbe4e8a6b878..5c886ba920215 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -160,6 +160,8 @@ class NativeCommandBuffer { if (mLinesLeft <= 0 || mLinesLeft >= static_cast(MAX_COMMAND_BYTES / 2)) { return std::make_pair(false, false); } + static const char* IS_COMPLEX_CMD = "--is-complex-zygote-command"; + static const size_t ICC_LENGTH = strlen(IS_COMPLEX_CMD); static const char* RUNTIME_ARGS = "--runtime-args"; static const char* INVOKE_WITH = "--invoke-with"; static const char* CHILD_ZYGOTE = "--start-child-zygote"; @@ -187,6 +189,11 @@ class NativeCommandBuffer { return std::make_pair(false, false); } const auto [arg_start, arg_end] = read_result.value(); + if (static_cast(arg_end - arg_start) == ICC_LENGTH && + strncmp(arg_start, IS_COMPLEX_CMD, ICC_LENGTH) == 0) { + // return to the Java code to handle exec spawning + return std::make_pair(false, false); + } if (static_cast(arg_end - arg_start) == RA_LENGTH && strncmp(arg_start, RUNTIME_ARGS, RA_LENGTH) == 0) { saw_runtime_args = true; diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index dbf40b69351a2..dbd19ece16490 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -483,8 +483,6 @@ void FileDescriptorInfo::DetachSocket(fail_fn_t fail_fn) const { // TODO: Move the definitions here and eliminate the forward declarations. They // temporarily help making code reviews easier. static int ParseFd(dirent* dir_entry, int dir_fd); -static std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, - fail_fn_t fail_fn); FileDescriptorTable* FileDescriptorTable::Create(const std::vector& fds_to_ignore, fail_fn_t fail_fn) { @@ -496,7 +494,7 @@ FileDescriptorTable* FileDescriptorTable::Create(const std::vector& fds_to_ return new FileDescriptorTable(std::move(open_fd_map)); } -static std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, +std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, fail_fn_t fail_fn) { DIR* proc_fd_dir = opendir(kFdPath); if (proc_fd_dir == nullptr) { diff --git a/core/jni/fd_utils.h b/core/jni/fd_utils.h index 6a7b2806ebfae..19cdbfe3a43cd 100644 --- a/core/jni/fd_utils.h +++ b/core/jni/fd_utils.h @@ -110,4 +110,7 @@ class FileDescriptorTable { DISALLOW_COPY_AND_ASSIGN(FileDescriptorTable); }; +std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, + fail_fn_t fail_fn); + #endif // FRAMEWORKS_BASE_CORE_JNI_FD_UTILS_H_ diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 989a0ce31a20a..5b29c7b101125 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -183,12 +183,14 @@ import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo.ForegroundServiceType; +import android.ext.settings.app.AswUseExecSpawning; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -248,6 +250,7 @@ import com.android.server.am.LowMemDetector.MemFactor; import com.android.server.am.ServiceRecord.ShortFgsInfo; import com.android.server.am.ServiceRecord.TimeLimitedFgsInfo; +import com.android.server.ext.SystemErrorNotification; import com.android.server.pm.KnownPackages; import com.android.server.uri.NeededUriGrants; import com.android.server.utils.AnrTimer; @@ -6087,8 +6090,14 @@ private String bringUpServiceInnerLocked(ServiceRecord r, int intentFlags, boole r.definingUid, r.serviceInfo.processName); } if ((r.serviceInfo.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) { - hostingRecord = HostingRecord.byAppZygote(r.instanceName, r.definingPackageName, - r.definingUid, r.serviceInfo.processName); + if (!r.definingPackageName.equals(r.appInfo.packageName)) { + new SystemErrorNotification("exec spawning error", "mismatch between definingPackageName (" + r.definingPackageName + ") and appInfo package name (" + r.appInfo.packageName + ") for service " + r.name).show(mAm.mContext); + } + if (!AswUseExecSpawning.I.get(mAm.mContext, r.userId, r.appInfo, GosPackageState.get(r.definingPackageName, r.userId))) { + // app zygotes are pointless when exec spawning is used + hostingRecord = HostingRecord.byAppZygote_(r.instanceName, r.definingPackageName, + r.definingUid, r.serviceInfo.processName); + } } } } diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java index 1a78a13cbf8cb..eff44824af494 100644 --- a/services/core/java/com/android/server/am/HostingRecord.java +++ b/services/core/java/com/android/server/am/HostingRecord.java @@ -184,7 +184,7 @@ public boolean isTopApp() { /** * Returns the UID of the package defining the component we want to start. Only valid - * when {@link #usesAppZygote()} returns true. + * when {@link #usesAppZygote_()} returns true. * * @return the UID of the hosting application */ @@ -194,7 +194,7 @@ public int getDefiningUid() { /** * Returns the packageName of the package defining the component we want to start. Only valid - * when {@link #usesAppZygote()} returns true. + * when {@link #usesAppZygote_()} returns true. * * @return the packageName of the hosting application */ @@ -245,7 +245,7 @@ public static HostingRecord byWebviewZygote(ComponentName hostingName, * @param definingUid uid of the package defining the service * @return The constructed HostingRecord */ - public static HostingRecord byAppZygote(ComponentName hostingName, String definingPackageName, + public static HostingRecord byAppZygote_/* underscore was added to keep track of callers */(ComponentName hostingName, String definingPackageName, int definingUid, String definingProcessName) { return new HostingRecord(HostingRecord.HOSTING_TYPE_EMPTY, hostingName.toShortString(), APP_ZYGOTE, definingPackageName, definingUid, false /* isTopApp */, @@ -255,14 +255,14 @@ public static HostingRecord byAppZygote(ComponentName hostingName, String defini /** * @return whether the process should spawn from the application zygote */ - public boolean usesAppZygote() { + public boolean usesAppZygote_() { // underscore was added to keep track of callers return mHostingZygote == APP_ZYGOTE; } /** * @return whether the process should spawn from the webview zygote */ - public boolean usesWebviewZygote() { + public boolean usesWebviewZygote_() { // underscore was added to keep track of callers return mHostingZygote == WEBVIEW_ZYGOTE; } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 1a12925d4c0a9..76d53eb99d610 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -96,8 +96,6 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Resources; -import android.ext.settings.app.AswUseExtendedVaSpace; -import android.ext.settings.app.AswUseHardenedMalloc; import android.graphics.Point; import android.net.LocalSocket; import android.net.LocalSocketAddress; @@ -120,6 +118,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.os.ZygoteProcess; import android.provider.DeviceConfig; import android.system.Os; import android.system.OsConstants; @@ -137,6 +136,7 @@ import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.view.Display; +import android.webkit.WebViewZygote; import com.android.internal.annotations.CompositeRWLock; import com.android.internal.annotations.GuardedBy; @@ -181,6 +181,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; @@ -2108,13 +2109,14 @@ boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord, definingAppInfo = app.info; } - runtimeFlags |= Zygote.getMemorySafetyRuntimeFlags( + var shouldForciblyEnableMemoryTagging = new AtomicBoolean(false); + runtimeFlags |= Zygote.getMemorySafetyRuntimeFlags(shouldForciblyEnableMemoryTagging, definingAppInfo, app.processInfo, instructionSet, mPlatformCompat); final Context ctx = mService.mContext; final PackageManagerInternal pmi = mService.getPackageManagerInternal(); - final String flatExtraArgs; - if (hostingRecord.usesWebviewZygote()) { + final ZygoteExtraArgs zygoteExtArgs; + if (hostingRecord.usesWebviewZygote_()) { // See commits fe85ed2ef53a7b1442a0e87f47e23e407e3e2b8c and b9a8666eb5504f022343fef9087135b7d937ddf8 String callerPkgName = app.info.packageName; @@ -2134,19 +2136,12 @@ boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord, GosPackageState callerPs = pmi.getGosPackageState(callerPkgName, userId); - flatExtraArgs = ZygoteExtraArgs.createFlatForWebviewProcess(ctx, userId, callerAppInfo, callerPs); + zygoteExtArgs = ZygoteExtraArgs.createForWebviewProcess(ctx, userId, callerAppInfo, callerPs); } else { GosPackageState ps = pmi.getGosPackageState(definingAppInfo.packageName, userId); - flatExtraArgs = ZygoteExtraArgs.createFlat(ctx, userId, definingAppInfo, ps, app.isolated); - - if (!AswUseHardenedMalloc.I.get(ctx, userId, definingAppInfo, ps)) { - runtimeFlags |= Zygote.DISABLE_HARDENED_MALLOC; - } - - if (!AswUseExtendedVaSpace.I.get(ctx, userId, definingAppInfo, ps)) { - runtimeFlags |= Zygote.ENABLE_COMPAT_VA_39_BIT; - } + zygoteExtArgs = ZygoteExtraArgs.create(ctx, userId, definingAppInfo, + shouldForciblyEnableMemoryTagging.get(), ps, app.isolated); } // the per-user SELinux context must be set @@ -2162,9 +2157,9 @@ boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord, // the PID of the new process, or else throw a RuntimeException. final String entryPoint = "android.app.ActivityThread"; - return startProcessLocked(hostingRecord, entryPoint, app, uid, gids, + return startProcessLocked(zygoteExtArgs, hostingRecord, entryPoint, app, uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi, - instructionSet, invokeWith, startUptime, startElapsedTime, flatExtraArgs); + instructionSet, invokeWith, startUptime, startElapsedTime); } catch (RuntimeException e) { Slog.e(ActivityManagerService.TAG, "Failure starting process " + app.processName, e); @@ -2206,10 +2201,10 @@ && getProcessListSettingsListener().applySdkSandboxRestrictionsAudit()) { } @GuardedBy("mService") - boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app, + boolean startProcessLocked(ZygoteExtraArgs zygoteExtArgs, HostingRecord hostingRecord, String entryPoint, ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags, int mountExternal, String seInfo, String requiredAbi, String instructionSet, String invokeWith, - long startUptime, long startElapsedTime, final String flatExtraArgs) { + long startUptime, long startElapsedTime) { app.setPendingStart(true); app.setRemoved(false); synchronized (mProcLock) { @@ -2241,16 +2236,16 @@ boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, Proce if (mService.mConstants.FLAG_PROCESS_START_ASYNC) { if (DEBUG_PROCESSES) Slog.i(TAG_PROCESSES, "Posting procStart msg for " + app.toShortString()); - mService.mProcStartHandler.post(() -> handleProcessStart( + mService.mProcStartHandler.post(() -> handleProcessStart(zygoteExtArgs, app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal, - requiredAbi, instructionSet, invokeWith, startSeq, flatExtraArgs)); + requiredAbi, instructionSet, invokeWith, startSeq)); return true; } else { try { - final Process.ProcessStartResult startResult = startProcess(hostingRecord, + final Process.ProcessStartResult startResult = startProcess(zygoteExtArgs, hostingRecord, entryPoint, app, uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, - requiredAbi, instructionSet, invokeWith, startUptime, flatExtraArgs); + requiredAbi, instructionSet, invokeWith, startUptime); handleProcessStartedLocked(app, startResult.pid, startResult.usingWrapper, startSeq, false); } catch (RuntimeException e) { @@ -2269,16 +2264,16 @@ boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, Proce * *

    Note: this function doesn't hold the global AM lock intentionally.

    */ - private void handleProcessStart(final ProcessRecord app, final String entryPoint, + private void handleProcessStart(final ZygoteExtraArgs zygoteExtArgs, final ProcessRecord app, final String entryPoint, final int[] gids, final int runtimeFlags, int zygotePolicyFlags, final int mountExternal, final String requiredAbi, final String instructionSet, - final String invokeWith, final long startSeq, final String flatExtraArgs) { + final String invokeWith, final long startSeq) { final Runnable startRunnable = () -> { try { - final Process.ProcessStartResult startResult = startProcess(app.getHostingRecord(), + final Process.ProcessStartResult startResult = startProcess(zygoteExtArgs, app.getHostingRecord(), entryPoint, app, app.getStartUid(), gids, runtimeFlags, zygotePolicyFlags, mountExternal, app.getSeInfo(), requiredAbi, instructionSet, invokeWith, - app.getStartTime(), flatExtraArgs); + app.getStartTime()); synchronized (mService) { handleProcessStartedLocked(app, startResult, startSeq); @@ -2494,10 +2489,10 @@ private boolean needsStorageDataIsolation(StorageManagerInternal storageManagerI && mountMode != Zygote.MOUNT_EXTERNAL_NONE; } - private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint, + private Process.ProcessStartResult startProcess(ZygoteExtraArgs zygoteExtArgs, HostingRecord hostingRecord, String entryPoint, ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags, int mountExternal, String seInfo, String requiredAbi, String instructionSet, - String invokeWith, long startTime, final String flatExtraArgs) { + String invokeWith, long startTime) { try { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " + app.processName); @@ -2616,31 +2611,33 @@ private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, Str } final Process.ProcessStartResult startResult; - boolean regularZygote = false; + boolean regularZygote = zygoteExtArgs.shouldUseExecSpawning(); app.mProcessGroupCreated = false; app.mSkipProcessGroupCreation = false; long forkTimeNs = SystemClock.uptimeNanos(); - if (hostingRecord.usesWebviewZygote()) { - startResult = startWebView(entryPoint, + if (hostingRecord.usesWebviewZygote_()) { + ZygoteProcess zygoteProcess = zygoteExtArgs.shouldUseExecSpawning() ? + Process.ZYGOTE_PROCESS : WebViewZygote.getProcess(); + startResult = startWebView(zygoteProcess, zygoteExtArgs, entryPoint, app.processName, uid, uid, gids, runtimeFlags, mountExternal, app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet, app.info.dataDir, null, app.info.packageName, app.getDisabledCompatChanges(), app.getStartSeq(), - new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()}, flatExtraArgs); - } else if (hostingRecord.usesAppZygote()) { + new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()}); + } else if (hostingRecord.usesAppZygote_()) { final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app); // We can't isolate app data and storage data as parent zygote already did that. - startResult = appZygote.startProcess(entryPoint, + startResult = appZygote.startProcess(zygoteExtArgs, entryPoint, app.processName, uid, gids, runtimeFlags, mountExternal, app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet, app.info.dataDir, app.info.packageName, isTopApp, app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap, app.getStartSeq(), - new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()}, flatExtraArgs); + new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()}); } else { regularZygote = true; - startResult = Process.start(entryPoint, + startResult = Process.start(zygoteExtArgs, entryPoint, app.processName, uid, uid, gids, runtimeFlags, mountExternal, app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet, app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags, @@ -2648,7 +2645,7 @@ private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, Str allowlistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs, bindOverrideSysprops, app.getStartSeq(), - new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()}, flatExtraArgs); + new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()}); // By now the process group should have been created by zygote. app.mProcessGroupCreated = true; } @@ -3427,7 +3424,7 @@ void addProcessNameLocked(ProcessRecord proc) { @GuardedBy("mService") private IsolatedUidRange getOrCreateIsolatedUidRangeLocked(ApplicationInfo info, HostingRecord hostingRecord) { - if (hostingRecord == null || !hostingRecord.usesAppZygote()) { + if (hostingRecord == null || !hostingRecord.usesAppZygote_()) { // Allocate an isolated UID from the global range return mGlobalIsolatedUids; } else { diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 8c6137a92dd00..e6a206fd97da6 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -1309,7 +1309,7 @@ public void killLocked(String reason, String description, @Reason int reasonCode void killProcessGroupIfNecessaryLocked(boolean async, String reason) { final boolean killProcessGroup; if (mHostingRecord != null - && (mHostingRecord.usesWebviewZygote() || mHostingRecord.usesAppZygote())) { + && (mHostingRecord.usesWebviewZygote_() || mHostingRecord.usesAppZygote_())) { synchronized (ProcessRecord.this) { killProcessGroup = mProcessGroupCreated; if (!killProcessGroup) { diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index a5a02cdedf97d..51d9fafdc10d7 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -247,13 +247,6 @@ public void prepareWebViewInSystemServer() { } } - private void startZygoteWhenReady() { - // Wait on a background thread for RELRO creation to be done. We ignore the return value - // because even if RELRO creation failed we still want to start the zygote. - waitForAndGetProvider(); - mSystemInterface.ensureZygoteStarted(); - } - public void handleNewUser(int userId) { // The system user is always started at boot, and by that point we have already run one // round of the package-changing logic (through prepareWebViewInSystemServer()), so early @@ -417,9 +410,8 @@ private void onWebViewProviderChanged(PackageInfo newPackage) { } } - // Once we've notified the system that the provider has changed and started RELRO creation, - // try to restart the zygote so that it will be ready when apps use it. - AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady); + // Don't start the WebView zygote eagerly since it's not used by exec-spawned WebView + // processes, which are enabled by default. } /** Fetch only the currently valid WebView packages. */ diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java index 59f7cb35f986a..59b0c72ddce1b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java @@ -1240,7 +1240,7 @@ static ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer if (definingUid != null) { final String dummyPackageName = "com.android.test"; final String dummyClassName = ".Foo"; - app.setHostingRecord(HostingRecord.byAppZygote(new ComponentName( + app.setHostingRecord(HostingRecord.byAppZygote_(new ComponentName( dummyPackageName, dummyClassName), "", definingUid, "")); } app.mServices.setConnectionGroup(connectionGroup); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java index 08974db0c2f3b..8b342358d0db2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java @@ -774,7 +774,7 @@ static ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer app.setPid(pid); app.info.uid = packageUid; if (definingUid != null) { - app.setHostingRecord(HostingRecord.byAppZygote(COMPONENT, "", definingUid, "")); + app.setHostingRecord(HostingRecord.byAppZygote_(COMPONENT, "", definingUid, "")); } return app; } From 6c6f222a5a165666cd2a8a08f83fd890d9422cb7 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 12/35] refactor zygote infrastructure to support arbitrary number of zygotes --- core/java/android/os/AppZygote.java | 2 +- core/java/android/os/ZygoteProcess.java | 221 ++++++++---------- core/java/android/os/ZygoteSelectionMode.java | 6 + core/java/android/webkit/WebViewZygote.java | 3 +- core/java/com/android/internal/os/Zygote.java | 20 -- .../android/internal/os/ZygoteExtraArgs.java | 6 +- .../com/android/internal/os/ZygoteServer.java | 17 +- .../com/android/internal/os/ZygoteType.java | 34 +++ 8 files changed, 146 insertions(+), 163 deletions(-) create mode 100644 core/java/android/os/ZygoteSelectionMode.java create mode 100644 core/java/com/android/internal/os/ZygoteType.java diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index f1abc792e85f4..84a015ba87233 100644 --- a/core/java/android/os/AppZygote.java +++ b/core/java/android/os/AppZygote.java @@ -218,7 +218,7 @@ private void connectToZygoteIfNeededLocked(ZygoteExtraArgs zygoteExtArgs) { ZygoteProcess.waitForConnectionToZygote(mZygote.getPrimarySocketAddress()); // preload application code in the zygote Log.i(LOG_TAG, "Starting application preload."); - mZygote.preloadApp(mAppInfo, abi); + mZygote.preloadApp(mAppInfo, abi, zygoteExtArgs.getZygoteSelectionMode()); Log.i(LOG_TAG, "Application preload done."); } catch (Exception e) { Log.e(LOG_TAG, "Error connecting to app zygote", e); diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 65ff5f6c46bf6..7acfd1e06a5db 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -34,6 +34,7 @@ import com.android.internal.os.Zygote; import com.android.internal.os.ZygoteConfig; import com.android.internal.os.ZygoteExtraArgs; +import com.android.internal.os.ZygoteType; import java.io.BufferedWriter; import java.io.DataInputStream; @@ -72,6 +73,7 @@ * @hide */ public class ZygoteProcess { + private static final int NUM_ZYGOTE_TYPES = ZygoteType.values().length; private static final int ZYGOTE_CONNECT_TIMEOUT_MS = 60000; @@ -87,60 +89,38 @@ public class ZygoteProcess { private static final String LOG_TAG = "ZygoteProcess"; /** - * The name of the socket used to communicate with the primary zygote. + * The name of the sockets used to communicate with the zygotes. */ - private final LocalSocketAddress mZygoteSocketAddress; + private final LocalSocketAddress[] mZygoteSocketAddresses = new LocalSocketAddress[NUM_ZYGOTE_TYPES]; /** - * The name of the secondary (alternate ABI) zygote socket. + * The names of the sockets used to communicate with the zygote's USAP pool. */ - private final LocalSocketAddress mZygoteSecondarySocketAddress; - - /** - * The name of the socket used to communicate with the primary USAP pool. - */ - private final LocalSocketAddress mUsapPoolSocketAddress; - - /** - * The name of the socket used to communicate with the secondary (alternate ABI) USAP pool. - */ - private final LocalSocketAddress mUsapPoolSecondarySocketAddress; + private final LocalSocketAddress[] mUsapPoolSocketAddresses = new LocalSocketAddress[NUM_ZYGOTE_TYPES]; public ZygoteProcess() { - mZygoteSocketAddress = - new LocalSocketAddress(Zygote.PRIMARY_SOCKET_NAME, + for (var type : ZygoteType.values()) { + int idx = type.ordinal(); + mZygoteSocketAddresses[idx] = new LocalSocketAddress(type.getSocketName(), LocalSocketAddress.Namespace.RESERVED); - mZygoteSecondarySocketAddress = - new LocalSocketAddress(Zygote.SECONDARY_SOCKET_NAME, - LocalSocketAddress.Namespace.RESERVED); - - mUsapPoolSocketAddress = - new LocalSocketAddress(Zygote.USAP_POOL_PRIMARY_SOCKET_NAME, - LocalSocketAddress.Namespace.RESERVED); - mUsapPoolSecondarySocketAddress = - new LocalSocketAddress(Zygote.USAP_POOL_SECONDARY_SOCKET_NAME, + mUsapPoolSocketAddresses[idx] = new LocalSocketAddress(type.getUsapPoolSocketName(), LocalSocketAddress.Namespace.RESERVED); + } // This constructor is used to create the primary and secondary Zygotes, which can support // Unspecialized App Process Pools. mUsapPoolSupported = true; } - public ZygoteProcess(LocalSocketAddress primarySocketAddress, - LocalSocketAddress secondarySocketAddress) { - mZygoteSocketAddress = primarySocketAddress; - mZygoteSecondarySocketAddress = secondarySocketAddress; - - mUsapPoolSocketAddress = null; - mUsapPoolSecondarySocketAddress = null; - - // This constructor is used to create the primary and secondary Zygotes, which CAN NOT + public ZygoteProcess(LocalSocketAddress primarySocketAddress) { + mZygoteSocketAddresses[ZygoteType.Primary.ordinal()] = primarySocketAddress; + // This constructor is used to create the primary Zygotes which CAN NOT // support Unspecialized App Process Pools. mUsapPoolSupported = false; } public LocalSocketAddress getPrimarySocketAddress() { - return mZygoteSocketAddress; + return mZygoteSocketAddresses[ZygoteType.Primary.ordinal()]; } /** @@ -266,14 +246,9 @@ boolean isClosed() { private int mHiddenApiAccessStatslogSampleRate; /** - * The state of the connection to the primary zygote. + * Zygote connection states. */ - private ZygoteState primaryZygoteState; - - /** - * The state of the connection to the secondary zygote. - */ - private ZygoteState secondaryZygoteState; + private ZygoteState[] mZygoteStates = new ZygoteState[NUM_ZYGOTE_TYPES]; /** * If this Zygote supports the creation and maintenance of a USAP pool. @@ -815,7 +790,7 @@ private Process.ProcessStartResult startViaZygote(@NonNull final ZygoteExtraArgs synchronized(mLock) { // The USAP pool can not be used if the application will not use the systems graphics // driver. If that driver is requested use the Zygote application start path. - return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), + return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi, zygoteExtArgs.getZygoteSelectionMode()), zygotePolicyFlags, argsForZygote); } @@ -860,11 +835,10 @@ private boolean fetchUsapPoolEnabledPropWithMinInterval() { * Closes the connections to the zygote, if they exist. */ public void close() { - if (primaryZygoteState != null) { - primaryZygoteState.close(); - } - if (secondaryZygoteState != null) { - secondaryZygoteState.close(); + for (ZygoteState state : mZygoteStates) { + if (state != null) { + state.close(); + } } } @@ -873,10 +847,10 @@ public void close() { * and retry if the zygote is unresponsive. This method is a no-op if a connection is * already open. */ - public void establishZygoteConnectionForAbi(String abi) { + public void establishZygoteConnectionForAbi(String abi, ZygoteSelectionMode zsm) { try { synchronized(mLock) { - openZygoteSocketIfNeeded(abi); + openZygoteSocketIfNeeded(abi, zsm); } } catch (ZygoteStartFailedEx ex) { throw new RuntimeException("Unable to connect to zygote for abi: " + abi, ex); @@ -886,10 +860,10 @@ public void establishZygoteConnectionForAbi(String abi) { /** * Attempt to retrieve the PID of the zygote serving the given abi. */ - public int getZygotePid(String abi) { + public int getZygotePid(String abi, ZygoteSelectionMode zsm) { try { synchronized (mLock) { - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, zsm); // Each query starts with the argument count (1 in this case) state.mZygoteOutputWriter.write("1"); @@ -918,17 +892,17 @@ public int getZygotePid(String abi) { public void bootCompleted() { // Notify both the 32-bit and 64-bit zygote. if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { - bootCompleted(Build.SUPPORTED_32_BIT_ABIS[0]); + bootCompleted(Build.SUPPORTED_32_BIT_ABIS[0], ZygoteSelectionMode.Regular); } if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { - bootCompleted(Build.SUPPORTED_64_BIT_ABIS[0]); + bootCompleted(Build.SUPPORTED_64_BIT_ABIS[0], ZygoteSelectionMode.Regular); } } - private void bootCompleted(String abi) { + private void bootCompleted(String abi, ZygoteSelectionMode zsm) { try { synchronized (mLock) { - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, zsm); state.mZygoteOutputWriter.write("1\n--boot-completed\n"); state.mZygoteOutputWriter.flush(); state.mZygoteInputStream.readInt(); @@ -950,9 +924,11 @@ private void bootCompleted(String abi) { public boolean setApiDenylistExemptions(List exemptions) { synchronized (mLock) { mApiDenylistExemptions = exemptions; - boolean ok = maybeSetApiDenylistExemptions(primaryZygoteState, true); - if (ok) { - ok = maybeSetApiDenylistExemptions(secondaryZygoteState, true); + boolean ok = true; + for (var type : ZygoteType.values()) { + if (!maybeSetApiDenylistExemptions(mZygoteStates[type.ordinal()], true)) { + ok = false; + } } return ok; } @@ -968,8 +944,9 @@ public boolean setApiDenylistExemptions(List exemptions) { public void setHiddenApiAccessLogSampleRate(int rate) { synchronized (mLock) { mHiddenApiAccessLogSampleRate = rate; - maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); - maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); + for (var type : ZygoteType.values()) { + maybeSetHiddenApiAccessLogSampleRate(mZygoteStates[type.ordinal()]); + } } } @@ -983,8 +960,9 @@ public void setHiddenApiAccessLogSampleRate(int rate) { public void setHiddenApiAccessStatslogSampleRate(int rate) { synchronized (mLock) { mHiddenApiAccessStatslogSampleRate = rate; - maybeSetHiddenApiAccessStatslogSampleRate(primaryZygoteState); - maybeSetHiddenApiAccessStatslogSampleRate(secondaryZygoteState); + for (var type : ZygoteType.values()) { + maybeSetHiddenApiAccessStatslogSampleRate(mZygoteStates[type.ordinal()]); + } } } @@ -1070,33 +1048,36 @@ private void maybeSetHiddenApiAccessStatslogSampleRate(ZygoteState state) { } } + @GuardedBy("mLock") + private ZygoteState attemptConnectionToZygote(ZygoteType type) throws IOException { + int typeIdx = type.ordinal(); + ZygoteState zygoteState = mZygoteStates[typeIdx]; + if (zygoteState == null || zygoteState.isClosed()) { + Log.d(LOG_TAG, "attemptConnectionToZygote " + type, new Throwable()); + zygoteState = + ZygoteState.connect(mZygoteSocketAddresses[typeIdx], mUsapPoolSocketAddresses[typeIdx]); + mZygoteStates[typeIdx] = zygoteState; + + maybeSetApiDenylistExemptions(zygoteState, false); + maybeSetHiddenApiAccessLogSampleRate(zygoteState); + } + return zygoteState; + } + /** * Creates a ZygoteState for the primary zygote if it doesn't exist or has been disconnected. */ @GuardedBy("mLock") - private void attemptConnectionToPrimaryZygote() throws IOException { - if (primaryZygoteState == null || primaryZygoteState.isClosed()) { - primaryZygoteState = - ZygoteState.connect(mZygoteSocketAddress, mUsapPoolSocketAddress); - - maybeSetApiDenylistExemptions(primaryZygoteState, false); - maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); - } + private ZygoteState attemptConnectionToPrimaryZygote() throws IOException { + return attemptConnectionToZygote(ZygoteType.Primary); } /** * Creates a ZygoteState for the secondary zygote if it doesn't exist or has been disconnected. */ @GuardedBy("mLock") - private void attemptConnectionToSecondaryZygote() throws IOException { - if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) { - secondaryZygoteState = - ZygoteState.connect(mZygoteSecondarySocketAddress, - mUsapPoolSecondarySocketAddress); - - maybeSetApiDenylistExemptions(secondaryZygoteState, false); - maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); - } + private ZygoteState attemptConnectionToSecondaryZygote() throws IOException { + return attemptConnectionToZygote(ZygoteType.Secondary); } /** @@ -1106,17 +1087,16 @@ private void attemptConnectionToSecondaryZygote() throws IOException { * appropriate one. Requires that mLock be held. */ @GuardedBy("mLock") - private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx { + private ZygoteState openZygoteSocketIfNeeded(String abi, ZygoteSelectionMode zsm) throws ZygoteStartFailedEx { try { - attemptConnectionToPrimaryZygote(); - + ZygoteState primaryZygoteState = attemptConnectionToPrimaryZygote(); if (primaryZygoteState.matches(abi)) { return primaryZygoteState; } - if (mZygoteSecondarySocketAddress != null) { + if (mZygoteSocketAddresses[ZygoteType.Secondary.ordinal()] != null) { // The primary zygote didn't match. Try the secondary. - attemptConnectionToSecondaryZygote(); + ZygoteState secondaryZygoteState = attemptConnectionToSecondaryZygote(); if (secondaryZygoteState.matches(abi)) { return secondaryZygoteState; @@ -1144,11 +1124,11 @@ public static void setAppZygotePreloadTimeout(int timeoutMs) { * Instructs the zygote to pre-load the application code for the given Application. * Only the app zygote supports this function. */ - public boolean preloadApp(ApplicationInfo appInfo, String abi) + public boolean preloadApp(ApplicationInfo appInfo, String abi, ZygoteSelectionMode zsm) throws ZygoteStartFailedEx, IOException { synchronized (mLock) { int ret; - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, zsm); int previousSocketTimeout = state.mZygoteSessionSocket.getSoTimeout(); try { @@ -1187,7 +1167,9 @@ public boolean preloadApp(ApplicationInfo appInfo, String abi) */ public boolean preloadDefault(String abi) throws ZygoteStartFailedEx, IOException { synchronized (mLock) { - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, + // preloadDefault() is called only for 32-bit zygote + ZygoteSelectionMode.Regular); // Each query starts with the argument count (1 in this case) state.mZygoteOutputWriter.write("1"); state.mZygoteOutputWriter.newLine(); @@ -1201,11 +1183,11 @@ public boolean preloadDefault(String abi) throws ZygoteStartFailedEx, IOExceptio /** * Try connecting to the Zygote over and over again until we hit a time-out. - * @param zygoteSocketName The name of the socket to connect to. + * @param zygoteType The type of the zygote to connect to. */ - public static void waitForConnectionToZygote(String zygoteSocketName) { + public static void waitForConnectionToZygote(ZygoteType zygoteType) { final LocalSocketAddress zygoteSocketAddress = - new LocalSocketAddress(zygoteSocketName, LocalSocketAddress.Namespace.RESERVED); + new LocalSocketAddress(zygoteType.getSocketName(), LocalSocketAddress.Namespace.RESERVED); waitForConnectionToZygote(zygoteSocketAddress); } @@ -1223,7 +1205,7 @@ public static void waitForConnectionToZygote(LocalSocketAddress zygoteSocketAddr return; } catch (IOException ioe) { Log.w(LOG_TAG, - "Got error connecting to zygote, retrying. msg= " + ioe.getMessage()); + "Got error connecting to zygote, retrying. msg= " + ioe.getMessage(), ioe); } try { @@ -1242,48 +1224,35 @@ private void informZygotesOfUsapPoolStatus() { final String command = "1\n--usap-pool-enabled=" + mUsapPoolEnabled + "\n"; synchronized (mLock) { - try { - attemptConnectionToPrimaryZygote(); - - primaryZygoteState.mZygoteOutputWriter.write(command); - primaryZygoteState.mZygoteOutputWriter.flush(); - } catch (IOException ioe) { - mUsapPoolEnabled = !mUsapPoolEnabled; - Log.w(LOG_TAG, "Failed to inform zygotes of USAP pool status: " - + ioe.getMessage()); - return; + for (var type : ZygoteType.values()) { + if (mZygoteSocketAddresses[type.ordinal()] != null) { + try { + ZygoteState zygoteState = attemptConnectionToZygote(type); + zygoteState.mZygoteOutputWriter.write(command); + zygoteState.mZygoteOutputWriter.flush(); + } catch (IOException e) { + mUsapPoolEnabled = !mUsapPoolEnabled; + Log.w(LOG_TAG, "Failed to inform zygote " + type + " of USAP pool status", e); + if (type == ZygoteType.Primary) { + return; + } + } + } } - if (mZygoteSecondarySocketAddress != null) { - try { - attemptConnectionToSecondaryZygote(); - + for (var type : ZygoteType.values()) { + var state = mZygoteStates[type.ordinal()]; + if (state != null) { try { - secondaryZygoteState.mZygoteOutputWriter.write(command); - secondaryZygoteState.mZygoteOutputWriter.flush(); - - // Wait for the secondary Zygote to finish its work. - secondaryZygoteState.mZygoteInputStream.readInt(); - } catch (IOException ioe) { + // Wait for zygote to finish its work. + state.mZygoteInputStream.readInt(); + } catch (IOException e) { throw new IllegalStateException( "USAP pool state change cause an irrecoverable error", - ioe); + e); } - } catch (IOException ioe) { - // No secondary zygote present. This is expected on some devices. } } - - // Wait for the response from the primary zygote here so the primary/secondary zygotes - // can work concurrently. - try { - // Wait for the primary zygote to finish its work. - primaryZygoteState.mZygoteInputStream.readInt(); - } catch (IOException ioe) { - throw new IllegalStateException( - "USAP pool state change cause an irrecoverable error", - ioe); - } } } diff --git a/core/java/android/os/ZygoteSelectionMode.java b/core/java/android/os/ZygoteSelectionMode.java new file mode 100644 index 0000000000000..bf3c5302d2dd4 --- /dev/null +++ b/core/java/android/os/ZygoteSelectionMode.java @@ -0,0 +1,6 @@ +package android.os; + +/** @hide */ +public enum ZygoteSelectionMode { + Regular, +} diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index 940cadb8e682c..603e52fa8658b 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -22,6 +22,7 @@ import android.os.Process; import android.os.UserHandle; import android.os.ZygoteProcess; +import android.os.ZygoteSelectionMode; import android.text.TextUtils; import android.util.Log; @@ -123,7 +124,7 @@ private static void connectToZygoteIfNeededLocked() { Process.FIRST_ISOLATED_UID, Integer.MAX_VALUE); // TODO(b/123615476) deal with user-id ranges properly ZygoteProcess.waitForConnectionToZygote(sZygote.getPrimarySocketAddress()); - sZygote.preloadApp(sPackage.applicationInfo, abi); + sZygote.preloadApp(sPackage.applicationInfo, abi, ZygoteSelectionMode.Regular); } catch (Exception e) { Log.e(LOGTAG, "Error connecting to webview zygote", e); stopZygoteLocked(); diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index c148718388711..ff5b68f6c7bb6 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -301,26 +301,6 @@ public final class Zygote { /** a prototype instance for a future List.toArray() */ static final int[][] INT_ARRAY_2D = new int[0][0]; - /** - * @hide for internal use only. - */ - public static final String PRIMARY_SOCKET_NAME = "zygote"; - - /** - * @hide for internal use only. - */ - public static final String SECONDARY_SOCKET_NAME = "zygote_secondary"; - - /** - * @hide for internal use only - */ - public static final String USAP_POOL_PRIMARY_SOCKET_NAME = "usap_pool_primary"; - - /** - * @hide for internal use only - */ - public static final String USAP_POOL_SECONDARY_SOCKET_NAME = "usap_pool_secondary"; - private Zygote() {} private static boolean containsInetGid(int[] gids) { diff --git a/core/java/com/android/internal/os/ZygoteExtraArgs.java b/core/java/com/android/internal/os/ZygoteExtraArgs.java index ee51bbe104a16..cd3409212acac 100644 --- a/core/java/com/android/internal/os/ZygoteExtraArgs.java +++ b/core/java/com/android/internal/os/ZygoteExtraArgs.java @@ -9,7 +9,7 @@ import android.ext.settings.app.AswUseHardenedMalloc; import android.os.Parcel; import android.os.Parcelable; -import android.os.ZygoteProcess; +import android.os.ZygoteSelectionMode; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -80,6 +80,10 @@ public static ZygoteExtraArgs createForWebviewProcess(Context ctx, int userId, return res; } + public ZygoteSelectionMode getZygoteSelectionMode() { + return ZygoteSelectionMode.Regular; + } + public boolean hasFlag(@Flag.Enum int flag) { return (this.flags & flag) == flag; } diff --git a/core/java/com/android/internal/os/ZygoteServer.java b/core/java/com/android/internal/os/ZygoteServer.java index 3e111e6af180d..646228615095c 100644 --- a/core/java/com/android/internal/os/ZygoteServer.java +++ b/core/java/com/android/internal/os/ZygoteServer.java @@ -146,23 +146,12 @@ private enum UsapPoolRefillAction { /** * Initialize the Zygote server with the Zygote server socket, USAP pool server socket, and USAP * pool event FD. - * - * @param isPrimaryZygote If this is the primary Zygote or not. */ - ZygoteServer(boolean isPrimaryZygote) { + ZygoteServer(ZygoteType type) { mUsapPoolEventFD = Zygote.getUsapPoolEventFD(); - if (isPrimaryZygote) { - mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.PRIMARY_SOCKET_NAME); - mUsapPoolSocket = - Zygote.createManagedSocketFromInitSocket( - Zygote.USAP_POOL_PRIMARY_SOCKET_NAME); - } else { - mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.SECONDARY_SOCKET_NAME); - mUsapPoolSocket = - Zygote.createManagedSocketFromInitSocket( - Zygote.USAP_POOL_SECONDARY_SOCKET_NAME); - } + mZygoteSocket = Zygote.createManagedSocketFromInitSocket(type.getSocketName()); + mUsapPoolSocket = Zygote.createManagedSocketFromInitSocket(type.getUsapPoolSocketName()); mUsapPoolSupported = true; fetchUsapPoolPolicyProps(); diff --git a/core/java/com/android/internal/os/ZygoteType.java b/core/java/com/android/internal/os/ZygoteType.java new file mode 100644 index 0000000000000..d885c50764abf --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteType.java @@ -0,0 +1,34 @@ +package com.android.internal.os; + +/** + * @hide + */ +public enum ZygoteType { + Primary("zygote", "usap_pool_primary"), + Secondary("zygote_secondary", "usap_pool_secondary"); + + private final String socketName; + private final String usapPoolSocketName; + + ZygoteType(String socketName, String usapPoolSocketName) { + this.socketName = socketName; + this.usapPoolSocketName = usapPoolSocketName; + } + + public String getSocketName() { + return socketName; + } + + public String getUsapPoolSocketName() { + return usapPoolSocketName; + } + + public static ZygoteType fromSocketName(String socketName) { + for (var t : ZygoteType.values()) { + if (socketName.equals(t.socketName)) { + return t; + } + } + throw new IllegalArgumentException(socketName); + } +} From 2d1e1b69797c61d2fb312a80ecdb3e2fdfce2451 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 13/35] add compat zygote instance which uses scudo instead of hardened_malloc This zygote instance is used when both exec spawning and hardened_malloc are disabled. --- core/java/android/os/ZygoteProcess.java | 43 +++++++++++++++++++ core/java/android/os/ZygoteSelectionMode.java | 3 ++ .../android/internal/os/ZygoteExtraArgs.java | 4 +- .../com/android/internal/os/ZygoteType.java | 1 + core/jni/fd_utils.cpp | 2 + 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 7acfd1e06a5db..56edab0233058 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -1054,16 +1054,52 @@ private ZygoteState attemptConnectionToZygote(ZygoteType type) throws IOExceptio ZygoteState zygoteState = mZygoteStates[typeIdx]; if (zygoteState == null || zygoteState.isClosed()) { Log.d(LOG_TAG, "attemptConnectionToZygote " + type, new Throwable()); + if (type == ZygoteType.Compat) { + if (!"running".equals(SystemProperties.get("init.svc.zygote_compat", null))) { + long start = SystemClock.elapsedRealtime(); + startCompatZygote(); + Log.d(LOG_TAG, "waited " + (SystemClock.elapsedRealtime() - start) + " ms for compat zygote"); + } + } zygoteState = ZygoteState.connect(mZygoteSocketAddresses[typeIdx], mUsapPoolSocketAddresses[typeIdx]); mZygoteStates[typeIdx] = zygoteState; maybeSetApiDenylistExemptions(zygoteState, false); maybeSetHiddenApiAccessLogSampleRate(zygoteState); + if (type == ZygoteType.Compat) { + // compat zygote is started on-demand, it might not be running when bootCompleted() + // is dispatched to other zygotes + bootCompleted(Build.SUPPORTED_64_BIT_ABIS[0], ZygoteSelectionMode.PreferCompatZygote); + } } return zygoteState; } + private void startCompatZygote() throws IOException { + SystemProperties.set("sys.start_compat_zygote", "1"); + boolean started = false; + LocalSocketAddress zygoteSocketAddress = mZygoteSocketAddresses[ZygoteType.Compat.ordinal()]; + + try (var zygoteSocket = new LocalSocket()) { + for (int i = 0; i < 2000; ++i) { + try { + zygoteSocket.connect(zygoteSocketAddress); + started = true; + break; + } catch (IOException e) { + if ((i % 20) == 0) { + Log.d(LOG_TAG, "waiting for compat zygote to start"); + } + SystemClock.sleep(10); + } + } + } + if (!started) { + throw new RuntimeException("timed out while waiting for compat zygote"); + } + } + /** * Creates a ZygoteState for the primary zygote if it doesn't exist or has been disconnected. */ @@ -1091,6 +1127,13 @@ private ZygoteState openZygoteSocketIfNeeded(String abi, ZygoteSelectionMode zsm try { ZygoteState primaryZygoteState = attemptConnectionToPrimaryZygote(); if (primaryZygoteState.matches(abi)) { + if (zsm == ZygoteSelectionMode.PreferCompatZygote) { + ZygoteState compatZygoteState = attemptConnectionToZygote(ZygoteType.Compat); + if (!compatZygoteState.matches(abi)) { + throw new IllegalStateException("primary and compat zygotes must match same ABIs"); + } + return compatZygoteState; + } return primaryZygoteState; } diff --git a/core/java/android/os/ZygoteSelectionMode.java b/core/java/android/os/ZygoteSelectionMode.java index bf3c5302d2dd4..614d731c3bbdb 100644 --- a/core/java/android/os/ZygoteSelectionMode.java +++ b/core/java/android/os/ZygoteSelectionMode.java @@ -3,4 +3,7 @@ /** @hide */ public enum ZygoteSelectionMode { Regular, + // Use the compat zygote if the command is addressed to 64-bit zygote. Compat zygote uses + // scudo instead of hardened_malloc + PreferCompatZygote, } diff --git a/core/java/com/android/internal/os/ZygoteExtraArgs.java b/core/java/com/android/internal/os/ZygoteExtraArgs.java index cd3409212acac..dd8adca9bc952 100644 --- a/core/java/com/android/internal/os/ZygoteExtraArgs.java +++ b/core/java/com/android/internal/os/ZygoteExtraArgs.java @@ -81,7 +81,9 @@ public static ZygoteExtraArgs createForWebviewProcess(Context ctx, int userId, } public ZygoteSelectionMode getZygoteSelectionMode() { - return ZygoteSelectionMode.Regular; + return hasFlag(Flag.PREFER_COMPAT_ZYGOTE) ? + ZygoteSelectionMode.PreferCompatZygote : + ZygoteSelectionMode.Regular; } public boolean hasFlag(@Flag.Enum int flag) { diff --git a/core/java/com/android/internal/os/ZygoteType.java b/core/java/com/android/internal/os/ZygoteType.java index d885c50764abf..823c151411936 100644 --- a/core/java/com/android/internal/os/ZygoteType.java +++ b/core/java/com/android/internal/os/ZygoteType.java @@ -5,6 +5,7 @@ */ public enum ZygoteType { Primary("zygote", "usap_pool_primary"), + Compat("zygote_compat", "usap_pool_compat"), Secondary("zygote_secondary", "usap_pool_secondary"); private final String socketName; diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index dbd19ece16490..6ae0e319828f1 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -36,8 +36,10 @@ static const char* kPathAllowlist[] = { "/dev/null", "/dev/socket/zygote", + "/dev/socket/zygote_compat", "/dev/socket/zygote_secondary", "/dev/socket/usap_pool_primary", + "/dev/socket/usap_pool_compat", "/dev/socket/usap_pool_secondary", "/dev/socket/webview_zygote", "/dev/socket/heapprofd", From 14d1513e156ceb7dd5df00cfefceee07b08b77f5 Mon Sep 17 00:00:00 2001 From: inthewaves Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 14/35] add GosCompatSecureSpawnTests --- .../GosCompatCheckApp/AndroidManifest.xml | 4 + .../checks/GosCompatCheckActivity.kt | 39 + .../GosCompatSecureSpawnTests/Android.bp | 22 + .../GosCompatSecureSpawnTests/AndroidTest.xml | 16 + .../GosCompatSecureSpawnApp/Android.bp | 76 ++ .../AndroidManifest.profileable.xml | 16 + .../AndroidManifest.xml | 24 + .../jni/secure_spawn_jni.c | 28 + .../securespawn/SecureSpawnActivity.kt | 677 ++++++++++++++++++ .../securespawn/SecureSpawnCheck.java | 109 +++ .../securespawn/SecureSpawnDeviceTest.java | 161 +++++ .../shared/SecureSpawnDumpableCheck.java | 25 + .../shared/SecureSpawnHiddenApiCheck.java | 67 ++ .../SecureSpawnReflectiveDumpCheck.java | 483 +++++++++++++ .../shared/SecureSpawnSmapsCheck.java | 211 ++++++ .../shared/SecureSpawnTestApiCompatCheck.java | 238 ++++++ .../GosCompatSecureSpawnTests/README.md | 24 + .../tests/SecureSpawnDisabledHostTest.java | 51 ++ .../tests/SecureSpawnEnabledHostTest.java | 31 + .../tests/SecureSpawnHostTestBase.java | 427 +++++++++++ tests/GosCompatTests/README.md | 7 +- tests/GosCompatTests/TEST_MAPPING | 4 + .../goscompat/checks/GosCompatContract.java | 8 + 23 files changed, 2745 insertions(+), 3 deletions(-) create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/Android.bp create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/AndroidTest.xml create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/Android.bp create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.profileable.xml create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.xml create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/jni/secure_spawn_jni.c create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnDumpableCheck.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnHiddenApiCheck.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnReflectiveDumpCheck.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnSmapsCheck.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnTestApiCompatCheck.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/README.md create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java create mode 100644 tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java diff --git a/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml b/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml index 2e6e818ccc050..b9ae39049e4ca 100644 --- a/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml +++ b/tests/GosCompatTests/GosCompatCheckApp/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + , @@ -152,6 +169,7 @@ private fun GosCompatCheckScreen( reflectiveMapsScanRunning: Boolean, onRunDirectMapsScan: () -> Unit, onRunReflectiveMapsScan: () -> Unit, + onRunSecureSpawn: () -> Unit, dmaBufReleaseContent: @Composable () -> Unit, ) { Column( @@ -168,6 +186,27 @@ private fun GosCompatCheckScreen( fontWeight = FontWeight.SemiBold, ) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text( + text = "Secure app spawning", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Button(onClick = onRunSecureSpawn) { + Text("Open check") + } + } + } + Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/Android.bp b/tests/GosCompatTests/GosCompatSecureSpawnTests/Android.bp new file mode 100644 index 0000000000000..21dea19e82f8e --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/Android.bp @@ -0,0 +1,22 @@ +java_test_host { + name: "GosCompatSecureSpawnTests", + srcs: [ + "src/**/*.java", + ":GosPackageStateFlags", + ], + libs: [ + "tradefed", + "compatibility-tradefed", + "compatibility-host-util", + ], + static_libs: [ + "framework-annotations-lib", + ], + test_suites: [ + "general-tests", + ], + device_common_data: [ + ":GosCompatSecureSpawnApp", + ":GosCompatSecureSpawnProfileableApp", + ], +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/AndroidTest.xml b/tests/GosCompatTests/GosCompatSecureSpawnTests/AndroidTest.xml new file mode 100644 index 0000000000000..4b8dbbbae2a1b --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/AndroidTest.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/Android.bp b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/Android.bp new file mode 100644 index 0000000000000..3013769a48ab8 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/Android.bp @@ -0,0 +1,76 @@ +cc_library_shared { + name: "libgoscompat_secure_spawn_jni", + srcs: ["jni/secure_spawn_jni.c"], + sdk_version: "current", + header_libs: ["jni_headers"], + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + ], + stl: "none", +} + +goscompat_secure_spawn_java_srcs = [ + "src/**/*.java", +] + +goscompat_secure_spawn_app_srcs = goscompat_secure_spawn_java_srcs + [ + "src/**/*.kt", +] + +goscompat_secure_spawn_test_static_libs = [ + "androidx.test.ext.junit", + "androidx.test.runner", + "truth", +] + +goscompat_secure_spawn_app_static_libs = goscompat_secure_spawn_test_static_libs + [ + "androidx.activity_activity", + "androidx.activity_activity-compose", + "androidx.core_core", + "androidx.compose.foundation_foundation", + "androidx.compose.foundation_foundation-layout", + "androidx.compose.material3_material3", + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui", + "androidx.compose.ui_ui-graphics", + "androidx.compose.ui_ui-text", + "androidx.compose.ui_ui-unit", +] + +android_test_helper_app { + name: "GosCompatSecureSpawnApp", + srcs: goscompat_secure_spawn_app_srcs, + manifest: "AndroidManifest.xml", + jni_libs: [ + "libgoscompat_secure_spawn_jni", + ], + use_embedded_native_libs: true, + compile_multilib: "both", + libs: [ + "android.test.runner.stubs", + ], + static_libs: goscompat_secure_spawn_app_static_libs, + kotlincflags: [ + "-Xjvm-default=all", + "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true", + ], + sdk_version: "current", +} + +android_test_helper_app { + name: "GosCompatSecureSpawnProfileableApp", + srcs: goscompat_secure_spawn_java_srcs, + manifest: "AndroidManifest.profileable.xml", + jni_libs: [ + "libgoscompat_secure_spawn_jni", + ], + use_embedded_native_libs: true, + compile_multilib: "both", + libs: [ + "android.test.runner.stubs", + ], + static_libs: goscompat_secure_spawn_test_static_libs, + sdk_version: "current", +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.profileable.xml b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.profileable.xml new file mode 100644 index 0000000000000..cabfb3a7d16be --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.profileable.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.xml b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.xml new file mode 100644 index 0000000000000..d9e2fa360011d --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/jni/secure_spawn_jni.c b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/jni/secure_spawn_jni.c new file mode 100644 index 0000000000000..6c353123d5eb6 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/jni/secure_spawn_jni.c @@ -0,0 +1,28 @@ +#include +#include +#include + +JNIEXPORT jstring JNICALL +Java_app_grapheneos_goscompat_securespawn_SecureSpawnCheck_nativeSystemProperty( + JNIEnv* env, jclass clazz, jstring key) { + (void) clazz; + + const char* key_chars = (*env)->GetStringUTFChars(env, key, NULL); + if (key_chars == NULL) { + return NULL; + } + + char value[PROP_VALUE_MAX] = ""; + __system_property_get(key_chars, value); + (*env)->ReleaseStringUTFChars(env, key, key_chars); + return (*env)->NewStringUTF(env, value); +} + +JNIEXPORT jint JNICALL +Java_app_grapheneos_goscompat_securespawn_SecureSpawnCheck_nativeDumpable( + JNIEnv* env, jclass clazz) { + (void) env; + (void) clazz; + + return prctl(PR_GET_DUMPABLE); +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt new file mode 100644 index 0000000000000..390732d4cc955 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt @@ -0,0 +1,677 @@ +package app.grapheneos.goscompat.securespawn + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.graphics.Color as AndroidColor +import android.os.Bundle +import android.provider.Settings +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnHiddenApiCheck +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnReflectiveDumpCheck +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnSmapsCheck +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnTestApiCompatCheck +import kotlin.concurrent.thread + +object SecureSpawnUiTags { + const val RUN_CHECK_BUTTON = "secure_spawn_run_check_button" + const val OPEN_SECURITY_SETTINGS_BUTTON = "secure_spawn_open_security_settings_button" + const val SECURE_APP_SPAWNING_STATE = "secure_spawn_setting_state" + const val CHECK_RESULT = "secure_spawn_check_result" + const val HIDDEN_API_REFLECTION_RESULT = "secure_spawn_hidden_api_reflection_result" + const val TEST_API_COMPAT_DEFAULT_RESULT = "secure_spawn_test_api_compat_default_result" + const val ACYCLIC_REFLECTIVE_DUMP_RESULT = "secure_spawn_acyclic_reflective_dump_result" +} + +class SecureSpawnActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + AndroidColor.TRANSPARENT, + AndroidColor.TRANSPARENT, + ), + navigationBarStyle = SystemBarStyle.auto( + AndroidColor.TRANSPARENT, + AndroidColor.TRANSPARENT, + ), + ) + + setContent { + val colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + var resultState by remember { + mutableStateOf(CheckResultState.NotRun) + } + var settingState by remember { + mutableStateOf(SettingResultState.Loading) + } + var running by remember { mutableStateOf(false) } + + fun refreshSecureAppSpawningState() { + settingState = SettingResultState.Loading + readSecureAppSpawningSetting { result -> + settingState = result + } + } + + fun runCheckFromUi() { + if (running) { + return + } + running = true + runCheck { result -> + if (result is CheckResultState.Success) { + settingState = SettingResultState.Success( + result.result.secureAppSpawningSetting(), + ) + } + resultState = result + running = false + } + } + + LaunchedEffect(Unit) { + refreshSecureAppSpawningState() + } + + MaterialTheme(colorScheme = colorScheme) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + SecureSpawnScreen( + settingState = settingState, + resultState = resultState, + running = running, + onRunCheck = { runCheckFromUi() }, + onOpenSecuritySettings = { openSecuritySettings() }, + ) + } + } + } + } + + private fun openSecuritySettings() { + try { + startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) + } catch (e: ActivityNotFoundException) { + Toast.makeText(this, "Security settings unavailable", Toast.LENGTH_SHORT).show() + } + } + + private fun runCheck(onResult: (CheckResultState) -> Unit) { + thread(name = "secure-spawn-check") { + val result = try { + CheckResultState.Success(SecureSpawnCheck.run()) + } catch (t: RuntimeException) { + CheckResultState.Error("${t.javaClass.simpleName}: ${t.message}") + } catch (t: UnsatisfiedLinkError) { + CheckResultState.Error("${t.javaClass.simpleName}: ${t.message}") + } + runOnUiThread { onResult(result) } + } + } + + private fun readSecureAppSpawningSetting(onResult: (SettingResultState) -> Unit) { + thread(name = "secure-spawn-setting") { + val result = try { + SettingResultState.Success(SecureSpawnCheck.secureAppSpawningSetting()) + } catch (t: RuntimeException) { + SettingResultState.Error("${t.javaClass.simpleName}: ${t.message}") + } catch (t: UnsatisfiedLinkError) { + SettingResultState.Error("${t.javaClass.simpleName}: ${t.message}") + } + runOnUiThread { onResult(result) } + } + } +} + +private sealed class SettingResultState { + object Loading : SettingResultState() + data class Success(val setting: SecureSpawnCheck.SecureAppSpawningSetting) : + SettingResultState() + data class Error(val message: String) : SettingResultState() +} + +private sealed class CheckResultState { + object NotRun : CheckResultState() + data class Success(val result: SecureSpawnCheck.Result) : CheckResultState() + data class Error(val message: String) : CheckResultState() +} + +@Composable +private fun SecureSpawnScreen( + settingState: SettingResultState, + resultState: CheckResultState, + running: Boolean, + onRunCheck: () -> Unit, + onOpenSecuritySettings: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(18.dp), + ) { + Text( + text = "GOS Secure Spawn Checks", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = "For manual comparison, change secure app spawning, reboot, then run the " + + "check again.", + style = MaterialTheme.typography.bodyMedium, + ) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + onClick = onOpenSecuritySettings, + modifier = Modifier.testTag(SecureSpawnUiTags.OPEN_SECURITY_SETTINGS_BUTTON), + ) { + Text("Open security settings") + } + Text( + text = "Security & privacy > Exploit protection > Secure app spawning", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + SecureAppSpawningCard(settingState) + + CheckRunnerCard(running, onRunCheck) + CheckResultContent( + resultState = resultState, + running = running, + modifier = Modifier + .fillMaxWidth() + .testTag(SecureSpawnUiTags.CHECK_RESULT), + ) + } +} + +@Composable +private fun CheckRunnerCard(running: Boolean, onRunCheck: () -> Unit) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + ) { + Text( + text = "Secure spawn checks", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + Button( + onClick = onRunCheck, + enabled = !running, + modifier = Modifier.testTag(SecureSpawnUiTags.RUN_CHECK_BUTTON), + ) { + Text(if (running) "Running" else "Run checks") + } + } + } +} + +@Composable +private fun CheckResultContent( + resultState: CheckResultState, + running: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + when { + running -> ResultCard { + StatusHeader(label = "Status", status = "RUNNING", passed = null) + } + resultState is CheckResultState.NotRun -> + ResultCard { + StatusHeader(label = "Status", status = "NOT RUN", passed = null) + } + resultState is CheckResultState.Error -> ErrorSection(resultState.message) + resultState is CheckResultState.Success -> StructuredResult(resultState.result) + } + } +} + +@Composable +private fun StructuredResult(result: SecureSpawnCheck.Result) { + ProcessStateCard(result.processState()) + RuntimeMemoryAccountingCard(result.androidRuntimeSmaps()) + HiddenApiReflectionCard(result.hiddenApiEnforcement()) + TestApiCompatDefaultCard(result.testApiCompatDefault()) + AcyclicReflectiveDumpCard(result.acyclicReflectiveDump()) +} + +@Composable +private fun SecureAppSpawningCard(settingState: SettingResultState) { + Card( + modifier = Modifier + .fillMaxWidth() + .testTag(SecureSpawnUiTags.SECURE_APP_SPAWNING_STATE), + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "Secure app spawning", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + when (settingState) { + is SettingResultState.Loading -> + StatusHeader(label = "State", status = "READING", passed = null) + is SettingResultState.Error -> + Text( + text = settingState.message, + style = MaterialTheme.typography.bodyMedium, + ) + is SettingResultState.Success -> { + SettingStateRow( + label = "Current state", + status = if (settingState.setting.enabled()) "ENABLED" else "DISABLED", + enabled = settingState.setting.enabled(), + ) + DetailRow("Property", settingState.setting.rawValue().ifEmpty { "" }) + } + } + } + } +} + +@Composable +private fun ProcessStateCard(processState: SecureSpawnCheck.ProcessState) { + val passed = processState.pid() > 0 && processState.tid() > 0 && + (!processState.hardenedMallocDisabled() || processState.execSpawned()) + ResultCard { + Section(label = "Process state", status = if (passed) "PASS" else "FAIL", passed = passed) { + DetailRow("Exec spawned", processState.execSpawned().toString()) + DetailRow("hardened_malloc disabled", processState.hardenedMallocDisabled().toString()) + DetailRow("PID", processState.pid().toString()) + DetailRow("TID", processState.tid().toString()) + if (!passed) { + Text( + text = "The process state check failed because hardened_malloc was reported " + + "disabled without the process being exec spawned, or because PID/TID " + + "could not be read.", + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } +} + +@Composable +private fun RuntimeMemoryAccountingCard(smaps: SecureSpawnSmapsCheck.AndroidRuntimeSmaps) { + val passed = smaps.sections() > 0 && + smaps.androidRuntimeSections() > 0 && + smaps.isWithinMemoryBounds() + ResultCard { + Section( + label = "Runtime memory accounting", + status = if (passed) "PASS" else "FAIL", + passed = passed, + ) { + Text( + text = runtimeMemoryAccountingSummary(smaps), + style = MaterialTheme.typography.bodyMedium, + ) + DetailRow("Total smaps sections", smaps.sections().toString()) + DetailRow("libandroid_runtime.so sections", smaps.androidRuntimeSections().toString()) + DetailRow("Measured sections", smaps.measuredSections().toString()) + DetailRow("Out-of-bounds sections", smaps.outOfBoundsSections().toString()) + DetailRow("Max libandroid_runtime.so Shared_Clean", + formatBytes(smaps.maxAndroidRuntimeSharedClean())) + DetailRow("Max measured Shared_Clean", formatBytes(smaps.maxMeasuredSharedClean())) + DetailRow("Max out-of-bounds Shared_Clean", + formatBytes(smaps.maxOutOfBoundsSharedClean())) + DetailRow("Shared_Clean limit", formatBytes(smaps.sharedCleanLimit())) + if (smaps.measuredDetails().isNotEmpty()) { + MonospaceBlock("Measured mappings", smaps.measuredDetails()) + } + if (smaps.firstOutOfBoundsHeader().isNotEmpty()) { + DetailRow("First out-of-bounds Shared_Clean", + formatBytes(smaps.firstOutOfBoundsSharedClean())) + DetailRow("First out-of-bounds Shared_Dirty", + formatBytes(smaps.firstOutOfBoundsSharedDirty())) + MonospaceBlock("First out-of-bounds maps line", smaps.firstOutOfBoundsHeader()) + } + } + } +} + +@Composable +private fun HiddenApiReflectionCard( + hiddenApi: SecureSpawnHiddenApiCheck.HiddenApiEnforcement, +) { + val passed = hiddenApi.objectShadowFieldsHidden() + ResultCard( + modifier = Modifier.testTag(SecureSpawnUiTags.HIDDEN_API_REFLECTION_RESULT), + ) { + Section( + label = "Hidden API reflection", + status = if (passed) "PASS" else "FAIL", + passed = passed, + ) { + Text( + text = if (passed) { + "Passed because Object shadow fields were hidden from reflection." + } else { + "Failed because Object shadow fields were visible to reflection." + }, + style = MaterialTheme.typography.bodyMedium, + ) + DetailRow("Exec spawned", hiddenApi.execSpawned().toString()) + DetailRow("Object declared field count", + hiddenApi.objectDeclaredFieldCount().toString()) + DetailRow("shadow\$_klass_ visible", + hiddenApi.objectShadowKlassVisible().toString()) + DetailRow("shadow\$_monitor_ visible", + hiddenApi.objectShadowMonitorVisible().toString()) + MonospaceBlock("Object declared field names", hiddenApi.objectDeclaredFieldNames()) + } + } +} + +@Composable +private fun TestApiCompatDefaultCard( + testApi: SecureSpawnTestApiCompatCheck.TestApiCompat, +) { + val passed = testApi.accessResult().outcome() == + SecureSpawnTestApiCompatCheck.AccessOutcome.ACCESS_DENIED + ResultCard( + modifier = Modifier.testTag(SecureSpawnUiTags.TEST_API_COMPAT_DEFAULT_RESULT), + ) { + Section( + label = "Test API compat default", + status = if (passed) "PASS" else "FAIL", + passed = passed, + ) { + Text( + text = if (passed) { + "Passed because default test API access was denied." + } else { + "Failed because default test API access was not denied." + }, + style = MaterialTheme.typography.bodyMedium, + ) + DetailRow("Exec spawned", testApi.execSpawned().toString()) + DetailRow("ALLOW_TEST_API_ACCESS change ID", + testApi.allowTestApiAccessChangeId().toString()) + DetailRow("Framework compat enabled", testApi.frameworkCompatEnabled()) + DetailRow("Candidate", testApi.accessResult().candidate()) + DetailRow("Outcome", testApi.accessResult().outcome().toString()) + DetailRow("Detail", testApi.accessResult().detail()) + DetailRow("Access allowed", testApi.accessAllowed().toString()) + } + } +} + +@Composable +private fun AcyclicReflectiveDumpCard( + dump: SecureSpawnReflectiveDumpCheck.AcyclicReflectiveDump, +) { + val passed = dump.threadTid() > 0 && dump.fixtureDepth() > 1 && dump.completed() + ResultCard( + modifier = Modifier.testTag(SecureSpawnUiTags.ACYCLIC_REFLECTIVE_DUMP_RESULT), + ) { + Section( + label = "Acyclic reflective dump", + status = if (passed) "PASS" else "FAIL", + passed = passed, + ) { + Text( + text = acyclicReflectiveDumpSummary(dump), + style = MaterialTheme.typography.bodyMedium, + ) + DetailRow("Exec spawned", dump.execSpawned().toString()) + DetailRow("Fixture depth", dump.fixtureDepth().toString()) + DetailRow("Completed", dump.completed().toString()) + DetailRow("Result length", dump.resultLength().toString()) + DetailRow("Thread", dump.threadName().ifEmpty { "" }) + DetailRow("Thread TID", dump.threadTid().toString()) + if (dump.failureClass().isNotEmpty()) { + DetailRow("Failure class", dump.failureClass()) + } + if (dump.failureMessage().isNotEmpty()) { + DetailRow("Failure message", dump.failureMessage()) + } + DetailRow("dumpObject frames", dump.dumpObjectFrames().toString()) + DetailRow("getAllFields frames", dump.getAllFieldsFrames().toString()) + DetailRow("Arrays.toArray frames", dump.arraysToArrayFrames().toString()) + DetailRow("ArrayList.addAll frames", dump.arrayListAddAllFrames().toString()) + DetailRow("Compatibility frames", dump.compatibilityFrames().toString()) + if (dump.stackTraceSample().isNotEmpty()) { + MonospaceBlock("Stack sample", dump.stackTraceSample()) + } + } + } +} + +@Composable +private fun MonospaceBlock(label: String, value: String) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + ) { + Text( + text = value, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + ), + softWrap = false, + ) + } +} + +private fun acyclicReflectiveDumpSummary( + dump: SecureSpawnReflectiveDumpCheck.AcyclicReflectiveDump, +): String { + if (dump.threadTid() <= 0) { + return "Failed because the reflective dump worker thread did not report a TID." + } + if (dump.fixtureDepth() <= 1) { + return "Failed because the acyclic fixture was not deep enough." + } + if (!dump.completed()) { + return "Failed because the bounded acyclic reflective dump did not complete." + } + return "Passed because the bounded acyclic reflective dump completed depth " + + "${dump.fixtureDepth()} without walking hidden runtime internals." +} + +@Composable +private fun ResultCard( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + content() + } + } +} + +private fun runtimeMemoryAccountingSummary( + smaps: SecureSpawnSmapsCheck.AndroidRuntimeSmaps, +): String { + if (smaps.sections() <= 0) { + return "Failed because /proc/self/smaps did not contain parseable sections." + } + if (smaps.androidRuntimeSections() <= 0) { + return "Failed because no libandroid_runtime.so section was found in /proc/self/smaps." + } + if (!smaps.isWithinMemoryBounds()) { + return "Failed because ${smaps.outOfBoundsSections()} libandroid_runtime.so section(s) " + + "exceeded the memory accounting bound." + } + return "Passed because libandroid_runtime.so memory accounting stayed within bounds." +} + +@Composable +private fun ErrorSection(message: String) { + ResultCard { + Section(label = "Status", status = "ERROR", passed = false) { + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} + +@Composable +private fun Section( + label: String, + status: String? = null, + passed: Boolean? = null, + content: @Composable () -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + if (status == null) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + } else { + StatusHeader(label = label, status = status, passed = passed) + } + content() + } +} + +@Composable +private fun StatusHeader(label: String, status: String, passed: Boolean?) { + val statusColor = when (passed) { + true -> MaterialTheme.colorScheme.primary + false -> MaterialTheme.colorScheme.error + null -> MaterialTheme.colorScheme.tertiary + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = status, + modifier = Modifier + .background(statusColor.copy(alpha = 0.12f), RoundedCornerShape(6.dp)) + .padding(horizontal = 10.dp, vertical = 4.dp), + color = statusColor, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + } +} + +@Composable +private fun DetailRow(label: String, value: String) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +@Composable +private fun SettingStateRow(label: String, status: String, enabled: Boolean) { + val statusColor = if (enabled) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = status, + modifier = Modifier + .background(statusColor.copy(alpha = 0.12f), RoundedCornerShape(6.dp)) + .padding(horizontal = 10.dp, vertical = 4.dp), + color = statusColor, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +private fun formatBytes(bytes: Long): String { + return "$bytes bytes (${bytes / 1024L} KiB)" +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java new file mode 100644 index 0000000000000..ada2d2078cb5e --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java @@ -0,0 +1,109 @@ +package app.grapheneos.goscompat.securespawn; + +import android.os.Process; + +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnHiddenApiCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnReflectiveDumpCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnSmapsCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnTestApiCompatCheck; + +public final class SecureSpawnCheck { + private static final String EXEC_SPAWN_PROPERTY = "persist.security.exec_spawn"; + + static { + System.loadLibrary("goscompat_secure_spawn_jni"); + } + + private SecureSpawnCheck() { + } + + public static Result run() { + ProcessState processState = processState(); + return new Result( + secureAppSpawningSetting(), + processState, + androidRuntimeSmaps(), + SecureSpawnHiddenApiCheck.run(processState.execSpawned()), + SecureSpawnTestApiCompatCheck.run(processState.execSpawned()), + SecureSpawnReflectiveDumpCheck.run(processState.execSpawned())); + } + + public static SecureAppSpawningSetting secureAppSpawningSetting() { + return new SecureAppSpawningSetting(nativeSystemProperty(EXEC_SPAWN_PROPERTY)); + } + + public static ProcessState processState() { + return new ProcessState( + System.getenv("IS_EXEC_SPAWNED_APP_PROCESS") != null, + System.getenv("DISABLE_HARDENED_MALLOC") != null, + Process.myPid(), + Process.myTid()); + } + + public static SecureSpawnSmapsCheck.AndroidRuntimeSmaps androidRuntimeSmaps() { + return SecureSpawnSmapsCheck.run(); + } + + public static SecureSpawnHiddenApiCheck.HiddenApiEnforcement hiddenApiEnforcement() { + return SecureSpawnHiddenApiCheck.run(processState().execSpawned()); + } + + public static SecureSpawnTestApiCompatCheck.TestApiCompat testApiCompatDefault() { + return SecureSpawnTestApiCompatCheck.run(processState().execSpawned()); + } + + public static SecureSpawnReflectiveDumpCheck.AcyclicReflectiveDump + acyclicReflectiveDump() { + return SecureSpawnReflectiveDumpCheck.run(processState().execSpawned()); + } + + public static int dumpable() { + return nativeDumpable(); + } + + private static native String nativeSystemProperty(String key); + private static native int nativeDumpable(); + + public record Result( + SecureAppSpawningSetting secureAppSpawningSetting, + ProcessState processState, + SecureSpawnSmapsCheck.AndroidRuntimeSmaps androidRuntimeSmaps, + SecureSpawnHiddenApiCheck.HiddenApiEnforcement hiddenApiEnforcement, + SecureSpawnTestApiCompatCheck.TestApiCompat testApiCompatDefault, + SecureSpawnReflectiveDumpCheck.AcyclicReflectiveDump acyclicReflectiveDump) {} + + public record SecureAppSpawningSetting(String rawValue) { + public boolean enabled() { + if (rawValue().isEmpty()) { + return true; + } + if (rawValue().equals("true") || rawValue().equals("1")) { + return true; + } + if (rawValue().equals("false") || rawValue().equals("0")) { + return false; + } + return true; + } + + @Override + public String toString() { + return "enabled=" + enabled() + + "\nproperty=" + (rawValue().isEmpty() ? "" : rawValue()); + } + } + + public record ProcessState( + boolean execSpawned, + boolean hardenedMallocDisabled, + int pid, + int tid) { + @Override + public String toString() { + return "execSpawned=" + execSpawned() + + "\nhardenedMallocDisabled=" + hardenedMallocDisabled() + + "\npid=" + pid() + + "\ntid=" + tid(); + } + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java new file mode 100644 index 0000000000000..a3a263576a9a8 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java @@ -0,0 +1,161 @@ +package app.grapheneos.goscompat.securespawn; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.os.Process; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnDumpableCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnHiddenApiCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnReflectiveDumpCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnSmapsCheck; +import app.grapheneos.goscompat.securespawn.shared.SecureSpawnTestApiCompatCheck; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public final class SecureSpawnDeviceTest { + private static final String TAG = "GosCompatSecureSpawn"; + + @Test + public void execSpawned() { + SecureSpawnCheck.ProcessState result = SecureSpawnCheck.processState(); + assertProcessState(result); + assertWithMessage(failureMessage("expected execSpawned == true", result)) + .that(result.execSpawned()).isTrue(); + assertWithMessage(failureMessage("expected hardenedMallocDisabled == false", result)) + .that(result.hardenedMallocDisabled()).isFalse(); + } + + @Test + public void notExecSpawned() { + SecureSpawnCheck.ProcessState result = SecureSpawnCheck.processState(); + assertProcessState(result); + assertWithMessage(failureMessage("expected execSpawned == false", result)) + .that(result.execSpawned()).isFalse(); + assertWithMessage(failureMessage("expected hardenedMallocDisabled == false", result)) + .that(result.hardenedMallocDisabled()).isFalse(); + } + + @Test + public void hardenedMallocDisabled() { + SecureSpawnCheck.ProcessState result = SecureSpawnCheck.processState(); + assertProcessState(result); + assertWithMessage(failureMessage("expected execSpawned == true", result)) + .that(result.execSpawned()).isTrue(); + assertWithMessage(failureMessage("expected hardenedMallocDisabled == true", result)) + .that(result.hardenedMallocDisabled()).isTrue(); + } + + @Test + public void runtimeMemoryAccountingCheck() { + SecureSpawnSmapsCheck.AndroidRuntimeSmaps result = SecureSpawnSmapsCheck.run(); + Log.i(TAG, "runtimeMemoryAccountingCheck\n" + result); + assertWithMessage(failureMessage("expected sections > 0", result)) + .that(result.sections()).isGreaterThan(0); + assertWithMessage(failureMessage("expected androidRuntimeSections > 0", result)) + .that(result.androidRuntimeSections()).isGreaterThan(0); + assertWithMessage(failureMessage("expected isWithinMemoryBounds == true", result)) + .that(result.isWithinMemoryBounds()).isTrue(); + } + + @Test + public void hiddenApiEnforcementCheck() { + SecureSpawnCheck.ProcessState processState = SecureSpawnCheck.processState(); + SecureSpawnHiddenApiCheck.HiddenApiEnforcement result = + SecureSpawnHiddenApiCheck.run(processState.execSpawned()); + Log.i(TAG, "hiddenApiEnforcementCheck\n" + result); + assertProcessState(processState); + assertWithMessage(failureMessage( + "expected hidden API execSpawned to match process execSpawned", result)) + .that(result.execSpawned()) + .isEqualTo(processState.execSpawned()); + assertWithMessage(failureMessage("expected objectShadowFieldsHidden == true", result)) + .that(result.objectShadowFieldsHidden()).isTrue(); + } + + @Test + public void testApiCompatDefaultCheck() { + testApiCompatCheck("testApiCompatDefaultCheck", false); + } + + @Test + public void testApiCompatDisabledCheck() { + testApiCompatCheck("testApiCompatDisabledCheck", false); + } + + @Test + public void testApiCompatEnabledCheck() { + testApiCompatCheck("testApiCompatEnabledCheck", true); + } + + @Test + public void profileableFromShellDumpableCheck() { + SecureSpawnCheck.ProcessState processState = SecureSpawnCheck.processState(); + SecureSpawnDumpableCheck.DumpableState result = + SecureSpawnDumpableCheck.run(processState.execSpawned()); + Log.i(TAG, "profileableFromShellDumpableCheck\n" + result); + assertProcessState(processState); + assertWithMessage(failureMessage( + "expected dumpable check execSpawned to match process execSpawned", result)) + .that(result.execSpawned()) + .isEqualTo(processState.execSpawned()); + assertWithMessage(failureMessage("expected isDumpable == true", result)) + .that(result.isDumpable()).isTrue(); + } + + @Test + public void acyclicReflectiveDumpCheck() { + SecureSpawnCheck.ProcessState processState = SecureSpawnCheck.processState(); + SecureSpawnReflectiveDumpCheck.AcyclicReflectiveDump result = + SecureSpawnReflectiveDumpCheck.run(processState.execSpawned()); + Log.i(TAG, "acyclicReflectiveDumpCheck\n" + result); + assertProcessState(processState); + assertWithMessage(failureMessage( + "expected reflective dump execSpawned to match process execSpawned", result)) + .that(result.execSpawned()) + .isEqualTo(processState.execSpawned()); + assertWithMessage(failureMessage("expected threadTid > 0", result)) + .that(result.threadTid()).isGreaterThan(0); + assertWithMessage(failureMessage("expected threadTid != Process.myTid()", result)) + .that(result.threadTid()).isNotEqualTo(Process.myTid()); + assertWithMessage(failureMessage("expected fixtureDepth > 1", result)) + .that(result.fixtureDepth()).isGreaterThan(1); + assertWithMessage(failureMessage("expected completed == true", result)) + .that(result.completed()).isTrue(); + assertWithMessage(failureMessage("expected resultLength > 0", result)) + .that(result.resultLength()).isGreaterThan(0); + } + + private static void testApiCompatCheck(String methodName, boolean expectedAccessAllowed) { + SecureSpawnCheck.ProcessState processState = SecureSpawnCheck.processState(); + SecureSpawnTestApiCompatCheck.TestApiCompat result = + SecureSpawnTestApiCompatCheck.run(processState.execSpawned()); + Log.i(TAG, methodName + "\n" + result); + assertProcessState(processState); + assertWithMessage(failureMessage( + "expected test API compat execSpawned to match process execSpawned", result)) + .that(result.execSpawned()) + .isEqualTo(processState.execSpawned()); + SecureSpawnTestApiCompatCheck.AccessOutcome expectedOutcome = expectedAccessAllowed + ? SecureSpawnTestApiCompatCheck.AccessOutcome.ACCESS_ALLOWED + : SecureSpawnTestApiCompatCheck.AccessOutcome.ACCESS_DENIED; + assertWithMessage(failureMessage("expected test API access outcome == " + + expectedOutcome, result)).that(result.accessResult().outcome()) + .isEqualTo(expectedOutcome); + } + + private static void assertProcessState(SecureSpawnCheck.ProcessState result) { + assertWithMessage(failureMessage("expected pid > 0", result)) + .that(result.pid()).isGreaterThan(0); + assertWithMessage(failureMessage("expected tid > 0", result)) + .that(result.tid()).isGreaterThan(0); + } + + private static String failureMessage(String expectation, Object result) { + return expectation + "\n" + result; + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnDumpableCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnDumpableCheck.java new file mode 100644 index 0000000000000..bb9e4ff915c30 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnDumpableCheck.java @@ -0,0 +1,25 @@ +package app.grapheneos.goscompat.securespawn.shared; + +import app.grapheneos.goscompat.securespawn.SecureSpawnCheck; + +public final class SecureSpawnDumpableCheck { + private SecureSpawnDumpableCheck() { + } + + public static DumpableState run(boolean execSpawned) { + return new DumpableState(execSpawned, SecureSpawnCheck.dumpable()); + } + + public record DumpableState(boolean execSpawned, int dumpable) { + public boolean isDumpable() { + return dumpable() == 1; + } + + @Override + public String toString() { + return "execSpawned=" + execSpawned() + + "\ndumpable=" + dumpable() + + "\nisDumpable=" + isDumpable(); + } + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnHiddenApiCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnHiddenApiCheck.java new file mode 100644 index 0000000000000..c153e8053c476 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnHiddenApiCheck.java @@ -0,0 +1,67 @@ +package app.grapheneos.goscompat.securespawn.shared; + +import java.lang.reflect.Field; + +public final class SecureSpawnHiddenApiCheck { + private static final String OBJECT_SHADOW_KLASS_FIELD = "shadow$_klass_"; + private static final String OBJECT_SHADOW_MONITOR_FIELD = "shadow$_monitor_"; + + private SecureSpawnHiddenApiCheck() { + } + + public static HiddenApiEnforcement run(boolean execSpawned) { + Field[] objectFields = Object.class.getDeclaredFields(); + return new HiddenApiEnforcement( + execSpawned, + objectFields.length, + fieldNames(objectFields), + hasField(objectFields, OBJECT_SHADOW_KLASS_FIELD), + hasField(objectFields, OBJECT_SHADOW_MONITOR_FIELD)); + } + + private static boolean hasField(Field[] fields, String name) { + for (Field field : fields) { + if (field.getName().equals(name)) { + return true; + } + } + return false; + } + + private static String fieldNames(Field[] fields) { + if (fields.length == 0) { + return "[]"; + } + + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < fields.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(fields[i].getName()); + } + return sb.append(']').toString(); + } + + public record HiddenApiEnforcement( + boolean execSpawned, + int objectDeclaredFieldCount, + String objectDeclaredFieldNames, + boolean objectShadowKlassVisible, + boolean objectShadowMonitorVisible) { + public boolean objectShadowFieldsHidden() { + return !objectShadowKlassVisible() + && !objectShadowMonitorVisible(); + } + + @Override + public String toString() { + return "execSpawned=" + execSpawned() + + "\nobjectDeclaredFieldCount=" + objectDeclaredFieldCount() + + "\nobjectDeclaredFieldNames=" + objectDeclaredFieldNames() + + "\nobjectShadowKlassVisible=" + objectShadowKlassVisible() + + "\nobjectShadowMonitorVisible=" + objectShadowMonitorVisible() + + "\nobjectShadowFieldsHidden=" + objectShadowFieldsHidden(); + } + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnReflectiveDumpCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnReflectiveDumpCheck.java new file mode 100644 index 0000000000000..44a773fa5b742 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnReflectiveDumpCheck.java @@ -0,0 +1,483 @@ +package app.grapheneos.goscompat.securespawn.shared; + +import android.os.Process; +import android.util.Log; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +public final class SecureSpawnReflectiveDumpCheck { + private static final String ROOT_CAUSE_LOG_TAG = "GosCompatSecureSpawnRc"; + // Deep enough to produce meaningful recursive traversal symptoms without intentionally + // exhausting the Java thread stack on a correct hidden API path. + private static final int ACYCLIC_FIXTURE_DEPTH = 16; + private static final int STACK_TRACE_SAMPLE_FRAMES = 24; + private static final int ROOT_CAUSE_LOG_LINE_LIMIT = 200; + private static final int MAX_REPORTED_PATH_LENGTH = 240; + private static final long ARRAYS_AS_LIST_TO_ARRAY_CHANGE_ID = 202956589L; + private static final long OVERRIDDEN_THREAD_START_CHANGE_ID = 418924588L; + private static final String ARRAYS_ARRAY_LIST_CLASS = "java.util.Arrays$ArrayList"; + private static final String ARRAY_LIST_CLASS = "java.util.ArrayList"; + private static final String COMPATIBILITY_CLASS = "android.compat.Compatibility"; + private static final AtomicInteger sNextAttemptId = new AtomicInteger(); + + private SecureSpawnReflectiveDumpCheck() { + } + + public static AcyclicReflectiveDump run(boolean execSpawned) { + ReflectiveDumpWorker worker = new ReflectiveDumpWorker( + sNextAttemptId.incrementAndGet(), execSpawned); + Thread thread = new Thread(worker); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted waiting for reflective dump probe", e); + } + return worker.result(); + } + + private static ReflectiveDumpResult runAcyclicDump(DumpTrace trace) { + try { + String result = dumpObject(new RecursiveRequest(buildNodeChain(ACYCLIC_FIXTURE_DEPTH)), + trace, 0, "request"); + trace.log("completed resultLength=" + result.length()); + return ReflectiveDumpResult.completed(result.length()); + } catch (StackOverflowError e) { + trace.log("stackOverflow message=" + safeString(e.getMessage())); + return ReflectiveDumpResult.failed(e); + } catch (ReflectiveOperationException | RuntimeException e) { + trace.log("failure throwable=" + e.getClass().getName() + + " message=" + safeString(e.getMessage())); + return ReflectiveDumpResult.failed(e); + } + } + + private static Node buildNodeChain(int depth) { + Node node = null; + for (int i = 0; i < depth; i++) { + node = new Node("node", node); + } + return node; + } + + private static List getAllFields(Class cls, DumpTrace trace, int recursionDepth) { + ArrayList fields = new ArrayList<>(); + while (cls != null) { + Field[] declaredFields = cls.getDeclaredFields(); + trace.log("getAllFields recursionDepth=" + recursionDepth + + " class=" + cls.getName() + + " declaredFieldCount=" + declaredFields.length + + " fields=" + describeFields(declaredFields)); + fields.addAll(Arrays.asList(declaredFields)); + cls = cls.getSuperclass(); + } + return fields; + } + + private static String dumpObject(Object obj, DumpTrace trace, int recursionDepth, String path) + throws ReflectiveOperationException { + if (obj == null) { + trace.log("dumpObject recursionDepth=" + recursionDepth + + " path=" + path + + " object=null"); + return ""; + } + trace.log("dumpObject recursionDepth=" + recursionDepth + + " path=" + path + + " class=" + obj.getClass().getName() + + " identity=" + identityString(obj) + + " " + trace.seenInfo(obj, path)); + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Number || obj instanceof Boolean) { + return String.valueOf(obj); + } + if (obj instanceof Map) { + Map map = (Map) obj; + StringBuilder sb = new StringBuilder(); + for (Object key : map.keySet()) { + String keyPath = trace.childPath(path, ""); + sb.append(dumpObject(key, trace, recursionDepth + 1, keyPath)); + sb.append(dumpObject(map.get(key), trace, recursionDepth + 1, + trace.childPath(path, ""))); + } + return sb.toString(); + } + + StringBuilder sb = new StringBuilder(); + for (Field field : getAllFields(obj.getClass(), trace, recursionDepth)) { + String fieldPath = trace.childPath(path, field.getName()); + try { + field.setAccessible(true); + trace.log("field recursionDepth=" + recursionDepth + + " path=" + fieldPath + + " owner=" + field.getDeclaringClass().getName() + + " name=" + field.getName() + + " modifiers=" + modifierString(field.getModifiers()) + + " synthetic=" + field.isSynthetic() + + " type=" + field.getType().getName() + + " setAccessible=ok"); + } catch (RuntimeException e) { + trace.log("field recursionDepth=" + recursionDepth + + " path=" + fieldPath + + " owner=" + field.getDeclaringClass().getName() + + " name=" + field.getName() + + " setAccessible=failed:" + + e.getClass().getSimpleName()); + throw e; + } + Object value = field.get(obj); + trace.log("fieldValue recursionDepth=" + recursionDepth + + " path=" + fieldPath + + " valueClass=" + className(value) + + " valueIdentity=" + identityString(value) + + " " + trace.peekSeenInfo(value)); + sb.append(field.getName()); + sb.append('{'); + sb.append(dumpObject(value, trace, recursionDepth + 1, fieldPath)); + sb.append('}'); + } + return sb.toString(); + } + + private static boolean isRootCauseLoggingEnabled() { + return Log.isLoggable(ROOT_CAUSE_LOG_TAG, Log.DEBUG); + } + + private static String describeFields(Field[] fields) { + if (fields.length == 0) { + return "[]"; + } + + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < fields.length; i++) { + if (i > 0) { + sb.append(", "); + } + Field field = fields[i]; + sb.append(field.getName()) + .append(':') + .append(field.getType().getName()) + .append(":modifiers=") + .append(modifierString(field.getModifiers())) + .append(":synthetic=") + .append(field.isSynthetic()); + } + return sb.append(']').toString(); + } + + private static String modifierString(int modifiers) { + String value = Modifier.toString(modifiers); + return value.isEmpty() ? "" : value; + } + + private static String className(Object obj) { + return obj == null ? "null" : obj.getClass().getName(); + } + + private static String identityString(Object obj) { + if (obj == null) { + return "null"; + } + return "0x" + Integer.toHexString(System.identityHashCode(obj)); + } + + private static String currentTargetSdkVersion() { + try { + Class vmRuntime = Class.forName("dalvik.system.VMRuntime"); + Object runtime = vmRuntime.getMethod("getRuntime").invoke(null); + Object targetSdkVersion = + vmRuntime.getMethod("getTargetSdkVersion").invoke(runtime); + return String.valueOf(targetSdkVersion); + } catch (ReflectiveOperationException | RuntimeException e) { + return "unavailable:" + e.getClass().getSimpleName(); + } + } + + private static String compatChangeEnabled(long changeId) { + try { + Class compatibility = Class.forName("android.compat.Compatibility"); + Object enabled = compatibility.getMethod("isChangeEnabled", long.class) + .invoke(null, changeId); + return String.valueOf(enabled); + } catch (ReflectiveOperationException | RuntimeException e) { + return "unavailable:" + e.getClass().getSimpleName(); + } + } + + private static String safeString(String value) { + return value == null ? "" : value; + } + + private static final class ReflectiveDumpWorker implements Runnable { + private final int mAttemptId; + private final boolean mExecSpawned; + private AcyclicReflectiveDump mResult; + private Throwable mFailure; + + ReflectiveDumpWorker(int attemptId, boolean execSpawned) { + mAttemptId = attemptId; + mExecSpawned = execSpawned; + } + + @Override + public void run() { + DumpTrace trace = DumpTrace.create(mAttemptId); + trace.logWorkerStart(mExecSpawned); + try { + ReflectiveDumpResult dumpResult = runAcyclicDump(trace); + mResult = new AcyclicReflectiveDump( + mExecSpawned, + Thread.currentThread().getName(), + Process.myTid(), + ACYCLIC_FIXTURE_DEPTH, + dumpResult.completed(), + dumpResult.resultLength(), + dumpResult.failureClass(), + dumpResult.failureMessage(), + dumpResult.dumpObjectFrames(), + dumpResult.getAllFieldsFrames(), + dumpResult.arraysToArrayFrames(), + dumpResult.arrayListAddAllFrames(), + dumpResult.compatibilityFrames(), + dumpResult.stackTraceSample()); + } catch (Throwable e) { + trace.log("workerFailure throwable=" + e.getClass().getName() + + " message=" + safeString(e.getMessage())); + mFailure = e; + } + } + + AcyclicReflectiveDump result() { + if (mFailure instanceof RuntimeException) { + throw (RuntimeException) mFailure; + } + if (mFailure instanceof Error) { + throw (Error) mFailure; + } + if (mFailure != null) { + throw new IllegalStateException("Reflective dump probe failed", mFailure); + } + if (mResult == null) { + throw new IllegalStateException("Reflective dump probe did not report a result"); + } + return mResult; + } + } + + private static final class DumpTrace { + private final int mAttemptId; + private final boolean mEnabled; + private final IdentityHashMap mSeen; + private int mLines; + private boolean mTruncated; + + private DumpTrace(int attemptId, boolean enabled) { + mAttemptId = attemptId; + mEnabled = enabled; + mSeen = enabled ? new IdentityHashMap<>() : null; + } + + static DumpTrace create(int attemptId) { + return new DumpTrace(attemptId, isRootCauseLoggingEnabled()); + } + + void logWorkerStart(boolean execSpawned) { + log("workerStart" + + " execSpawnedArg=" + execSpawned + + " envExecSpawned=" + + safeString(System.getenv("IS_EXEC_SPAWNED_APP_PROCESS")) + + " envDisableHardenedMalloc=" + + safeString(System.getenv("DISABLE_HARDENED_MALLOC")) + + " threadName=" + Thread.currentThread().getName() + + " threadTid=" + Process.myTid() + + " pid=" + Process.myPid() + + " targetSdk=" + currentTargetSdkVersion() + + " arraysAsListToArrayChange=" + + compatChangeEnabled(ARRAYS_AS_LIST_TO_ARRAY_CHANGE_ID) + + " overriddenThreadStartChange=" + + compatChangeEnabled(OVERRIDDEN_THREAD_START_CHANGE_ID)); + } + + void log(String message) { + if (!mEnabled) { + return; + } + if (mLines >= ROOT_CAUSE_LOG_LINE_LIMIT) { + if (!mTruncated) { + Log.d(ROOT_CAUSE_LOG_TAG, prefix() + + "logLineLimitReached limit=" + ROOT_CAUSE_LOG_LINE_LIMIT); + mTruncated = true; + } + return; + } + Log.d(ROOT_CAUSE_LOG_TAG, prefix() + message); + mLines++; + } + + String seenInfo(Object obj, String path) { + if (!mEnabled || obj == null) { + return "seenBefore=false"; + } + String firstPath = mSeen.get(obj); + if (firstPath != null) { + return "seenBefore=true firstPath=" + firstPath; + } + mSeen.put(obj, path); + return "seenBefore=false"; + } + + String peekSeenInfo(Object obj) { + if (!mEnabled || obj == null) { + return "seenBefore=false"; + } + String firstPath = mSeen.get(obj); + if (firstPath == null) { + return "seenBefore=false"; + } + return "seenBefore=true firstPath=" + firstPath; + } + + String childPath(String path, String child) { + if (!mEnabled) { + return ""; + } + String next = path.isEmpty() ? child : path + "." + child; + if (next.length() <= MAX_REPORTED_PATH_LENGTH) { + return next; + } + return next.substring(0, MAX_REPORTED_PATH_LENGTH) + "..."; + } + + private String prefix() { + return "attempt=" + mAttemptId + " "; + } + } + + private record ReflectiveDumpResult( + boolean completed, + int resultLength, + String failureClass, + String failureMessage, + int dumpObjectFrames, + int getAllFieldsFrames, + int arraysToArrayFrames, + int arrayListAddAllFrames, + int compatibilityFrames, + String stackTraceSample) { + static ReflectiveDumpResult completed(int resultLength) { + return new ReflectiveDumpResult( + true, resultLength, "", "", 0, 0, 0, 0, 0, ""); + } + + static ReflectiveDumpResult failed(Throwable t) { + StackTraceElement[] stack = t.getStackTrace(); + String checkClass = SecureSpawnReflectiveDumpCheck.class.getName(); + return new ReflectiveDumpResult( + false, + 0, + t.getClass().getName(), + safeString(t.getMessage()), + countFrames(stack, checkClass, "dumpObject"), + countFrames(stack, checkClass, "getAllFields"), + countFrames(stack, ARRAYS_ARRAY_LIST_CLASS, "toArray"), + countFrames(stack, ARRAY_LIST_CLASS, "addAll"), + countFrames(stack, COMPATIBILITY_CLASS, "isChangeEnabled"), + sampleFrames(stack)); + } + + private static int countFrames( + StackTraceElement[] stack, String className, String methodName) { + int count = 0; + for (StackTraceElement frame : stack) { + if (className.equals(frame.getClassName()) + && methodName.equals(frame.getMethodName())) { + count++; + } + } + return count; + } + + private static String sampleFrames(StackTraceElement[] stack) { + StringBuilder sample = new StringBuilder(); + int frameCount = Math.min(stack.length, STACK_TRACE_SAMPLE_FRAMES); + for (int i = 0; i < frameCount; i++) { + if (i > 0) { + sample.append('\n'); + } + sample.append(i).append(' ').append(stack[i]); + } + if (stack.length > frameCount) { + if (sample.length() > 0) { + sample.append('\n'); + } + sample.append("... totalFrames=").append(stack.length); + } + return sample.toString(); + } + } + + private static final class RecursiveRequest { + private final String clientId = "client"; + private final String username = "user"; + private final String password = "secret"; + private final Node root; + + RecursiveRequest(Node root) { + this.root = root; + } + } + + private static final class Node { + private final String name; + private final Node child; + + Node(String name, Node child) { + this.name = name; + this.child = child; + } + } + + public record AcyclicReflectiveDump( + boolean execSpawned, + String threadName, + int threadTid, + int fixtureDepth, + boolean completed, + int resultLength, + String failureClass, + String failureMessage, + int dumpObjectFrames, + int getAllFieldsFrames, + int arraysToArrayFrames, + int arrayListAddAllFrames, + int compatibilityFrames, + String stackTraceSample) { + @Override + public String toString() { + return "execSpawned=" + execSpawned() + + "\nthreadName=" + threadName() + + "\nthreadTid=" + threadTid() + + "\nfixtureDepth=" + fixtureDepth() + + "\ncompleted=" + completed() + + "\nresultLength=" + resultLength() + + "\nfailureClass=" + failureClass() + + "\nfailureMessage=" + failureMessage() + + "\ndumpObjectFrames=" + dumpObjectFrames() + + "\ngetAllFieldsFrames=" + getAllFieldsFrames() + + "\narraysToArrayFrames=" + arraysToArrayFrames() + + "\narrayListAddAllFrames=" + arrayListAddAllFrames() + + "\ncompatibilityFrames=" + compatibilityFrames() + + "\nstackTraceSample=" + stackTraceSample(); + } + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnSmapsCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnSmapsCheck.java new file mode 100644 index 0000000000000..ca08ac69c6461 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnSmapsCheck.java @@ -0,0 +1,211 @@ +package app.grapheneos.goscompat.securespawn.shared; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +public final class SecureSpawnSmapsCheck { + private static final String ANDROID_RUNTIME_LIBRARY = "libandroid_runtime.so"; + private static final String SHARED_CLEAN_LABEL = "Shared_Clean:"; + private static final String SHARED_DIRTY_LABEL = "Shared_Dirty:"; + private static final long BYTES_PER_KIB = 1024L; + private static final long ANDROID_RUNTIME_SHARED_CLEAN_LIMIT = 480L * BYTES_PER_KIB; + + private SecureSpawnSmapsCheck() { + } + + public static AndroidRuntimeSmaps run() { + int sections = 0; + int androidRuntimeSections = 0; + int measuredSections = 0; + int outOfBoundsSections = 0; + long maxAndroidRuntimeSharedClean = 0; + long maxMeasuredSharedClean = 0; + long maxOutOfBoundsSharedClean = 0; + StringBuilder measuredDetails = new StringBuilder(); + String firstOutOfBoundsHeader = ""; + long firstOutOfBoundsSharedClean = 0; + long firstOutOfBoundsSharedDirty = 0; + String header = null; + long sharedClean = 0; + long sharedDirty = 0; + + try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/smaps"))) { + while (true) { + String line = reader.readLine(); + if (line == null || isSmapsHeader(line)) { + if (header != null) { + sections++; + boolean androidRuntime = header.contains(ANDROID_RUNTIME_LIBRARY); + boolean measured = androidRuntime && isMeasuredAndroidRuntimeHeader(header); + boolean outOfBounds = measured + && sharedDirty <= 0 + && sharedClean >= ANDROID_RUNTIME_SHARED_CLEAN_LIMIT; + + if (androidRuntime) { + androidRuntimeSections++; + maxAndroidRuntimeSharedClean = Math.max( + maxAndroidRuntimeSharedClean, sharedClean); + } + + if (measured) { + measuredSections++; + maxMeasuredSharedClean = Math.max( + maxMeasuredSharedClean, sharedClean); + appendMeasuredDetail(measuredDetails, header, sharedClean, + sharedDirty, outOfBounds); + } + + if (outOfBounds) { + outOfBoundsSections++; + maxOutOfBoundsSharedClean = Math.max( + maxOutOfBoundsSharedClean, sharedClean); + if (firstOutOfBoundsHeader.isEmpty()) { + firstOutOfBoundsHeader = header; + firstOutOfBoundsSharedClean = sharedClean; + firstOutOfBoundsSharedDirty = sharedDirty; + } + } + } + + if (line == null) { + break; + } + + header = line; + sharedClean = 0; + sharedDirty = 0; + continue; + } + + if (header == null) { + continue; + } + + long nextSharedClean = parseSmapsValue(line, SHARED_CLEAN_LABEL); + if (nextSharedClean >= 0) { + sharedClean = nextSharedClean; + continue; + } + + long nextSharedDirty = parseSmapsValue(line, SHARED_DIRTY_LABEL); + if (nextSharedDirty >= 0) { + sharedDirty = nextSharedDirty; + } + } + } catch (IOException e) { + throw new IllegalStateException("Unable to read smaps", e); + } + + return new AndroidRuntimeSmaps( + sections, + androidRuntimeSections, + measuredSections, + outOfBoundsSections, + maxAndroidRuntimeSharedClean, + maxMeasuredSharedClean, + maxOutOfBoundsSharedClean, + ANDROID_RUNTIME_SHARED_CLEAN_LIMIT, + measuredDetails.toString(), + firstOutOfBoundsHeader, + firstOutOfBoundsSharedClean, + firstOutOfBoundsSharedDirty); + } + + private static boolean isSmapsHeader(String line) { + int dash = line.indexOf('-'); + int space = line.indexOf(' '); + return dash > 0 && space > dash + 1 + && isHex(line, 0, dash) + && isHex(line, dash + 1, space); + } + + private static boolean isHex(String value, int start, int end) { + for (int i = start; i < end; i++) { + char c = value.charAt(i); + if ((c < '0' || c > '9') + && (c < 'a' || c > 'f') + && (c < 'A' || c > 'F')) { + return false; + } + } + return start < end; + } + + private static long parseSmapsValue(String line, String label) { + if (!line.startsWith(label)) { + return -1; + } + + String value = line.substring(label.length()).trim(); + int separator = value.indexOf(' '); + if (separator >= 0) { + value = value.substring(0, separator); + } + + try { + return Long.parseLong(value) * BYTES_PER_KIB; + } catch (NumberFormatException e) { + return -1; + } + } + + private static boolean isMeasuredAndroidRuntimeHeader(String header) { + String[] fields = header.split("\\s+"); + if (fields.length <= 1) { + return false; + } + + String permissions = fields[1]; + return permissions.length() > 3 + && permissions.charAt(1) != 'w' + && permissions.charAt(2) != 'x' + && permissions.charAt(3) == 'p'; + } + + private static void appendMeasuredDetail( + StringBuilder details, String header, long sharedClean, long sharedDirty, + boolean outOfBounds) { + if (details.length() > 0) { + details.append('\n'); + } + details.append("outOfBounds=").append(outOfBounds) + .append(" Shared_Clean=").append(sharedClean) + .append(" Shared_Dirty=").append(sharedDirty) + .append(" header=").append(header); + } + + public record AndroidRuntimeSmaps( + int sections, + int androidRuntimeSections, + int measuredSections, + int outOfBoundsSections, + long maxAndroidRuntimeSharedClean, + long maxMeasuredSharedClean, + long maxOutOfBoundsSharedClean, + long sharedCleanLimit, + String measuredDetails, + String firstOutOfBoundsHeader, + long firstOutOfBoundsSharedClean, + long firstOutOfBoundsSharedDirty) { + public boolean isWithinMemoryBounds() { + return outOfBoundsSections() == 0; + } + + @Override + public String toString() { + return "sections=" + sections() + + "\nandroidRuntimeSections=" + androidRuntimeSections() + + "\nmeasuredSections=" + measuredSections() + + "\noutOfBoundsSections=" + outOfBoundsSections() + + "\nmaxAndroidRuntimeSharedClean=" + maxAndroidRuntimeSharedClean() + + "\nmaxMeasuredSharedClean=" + maxMeasuredSharedClean() + + "\nmaxOutOfBoundsSharedClean=" + maxOutOfBoundsSharedClean() + + "\nsharedCleanLimit=" + sharedCleanLimit() + + "\nmeasuredDetails=" + measuredDetails() + + "\nfirstOutOfBoundsHeader=" + firstOutOfBoundsHeader() + + "\nfirstOutOfBoundsSharedClean=" + firstOutOfBoundsSharedClean() + + "\nfirstOutOfBoundsSharedDirty=" + firstOutOfBoundsSharedDirty(); + } + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnTestApiCompatCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnTestApiCompatCheck.java new file mode 100644 index 0000000000000..3493a3d9ea863 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/shared/SecureSpawnTestApiCompatCheck.java @@ -0,0 +1,238 @@ +package app.grapheneos.goscompat.securespawn.shared; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public final class SecureSpawnTestApiCompatCheck { + // From libcore/libart/src/main/java/dalvik/system/VMRuntime.java: + // private static final long ALLOW_TEST_API_ACCESS = 166236554; + public static final long ALLOW_TEST_API_ACCESS_CHANGE_ID = 166236554L; + + // These members are blocked,test-api in out/soong/hiddenapi/hiddenapi-flags.csv. + private static final Candidate[] CANDIDATES = { + /* + * From frameworks/base/core/java/android/app/Activity.java: + * + * @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + * @TestApi + * public final boolean addDumpable(@NonNull Dumpable dumpable) + */ + Candidate.method("android.app.Activity", "addDumpable", "android.util.Dumpable"), + Candidate.field("android.app.Activity", "DUMP_ARG_DUMP_DUMPABLE", "--dump-dumpable"), + Candidate.field("android.app.Activity", "DUMP_ARG_LIST_DUMPABLES", "--list-dumpables"), + Candidate.field("android.Manifest$permission", "ACCESSIBILITY_MOTION_EVENT_OBSERVING", + "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING"), + }; + + private SecureSpawnTestApiCompatCheck() { + } + + public static TestApiCompat run(boolean execSpawned) { + return new TestApiCompat( + execSpawned, + ALLOW_TEST_API_ACCESS_CHANGE_ID, + compatChangeEnabled(ALLOW_TEST_API_ACCESS_CHANGE_ID), + probeCandidates()); + } + + private static AccessResult probeCandidates() { + AccessResult lastMissing = null; + for (Candidate candidate : CANDIDATES) { + AccessResult result = probeCandidate(candidate); + if (result.outcome() != AccessOutcome.MEMBER_NOT_PRESENT) { + return result; + } + lastMissing = result; + } + return lastMissing == null + ? AccessResult.failed("", "no candidates configured") + : lastMissing; + } + + private static AccessResult probeCandidate(Candidate candidate) { + Class clazz; + try { + clazz = Class.forName(candidate.className()); + } catch (ClassNotFoundException e) { + return AccessResult.missing(candidate, e); + } catch (LinkageError e) { + return AccessResult.failed(candidate, e); + } + + if (candidate.isMethod()) { + return probeMethodCandidate(clazz, candidate); + } else { + return probeFieldCandidate(clazz, candidate); + } + } + + private static AccessResult probeFieldCandidate(Class clazz, Candidate candidate) { + try { + /* + * Hidden/test API enforcement happens in this lookup. Class.getDeclaredField() is + * native on Android and reaches ART's Class_getDeclaredField(), which throws + * NoSuchFieldException when ShouldDenyAccessToMember(result->GetArtField(), ...) + * rejects the blocked,test-api member. ACCESS_DENIED should exit through the catch + * below before the probe can read the public static field. + */ + Field field; + field = clazz.getDeclaredField(candidate.memberName()); + Object value = field.get(null); + String actualValue = String.valueOf(value); + if (!candidate.expectedValue().equals(actualValue)) { + return AccessResult.failed(candidate, + "expected value " + candidate.expectedValue() + + " but was " + actualValue); + } + return AccessResult.allowed(candidate, "value=" + actualValue); + } catch (NoSuchFieldException e) { + return AccessResult.denied(candidate, e); + } catch (SecurityException e) { + return AccessResult.denied(candidate, e); + } catch (IllegalAccessException | RuntimeException e) { + return AccessResult.denied(candidate, e); + } + } + + private static AccessResult probeMethodCandidate(Class clazz, Candidate candidate) { + Class[] parameterTypes = new Class[candidate.parameterClassNames().length]; + for (int i = 0; i < parameterTypes.length; ++i) { + try { + parameterTypes[i] = Class.forName(candidate.parameterClassNames()[i]); + } catch (ClassNotFoundException e) { + return AccessResult.missing(candidate, e); + } catch (LinkageError e) { + return AccessResult.failed(candidate, e); + } + } + + try { + /* + * Hidden/test API enforcement happens in this lookup. On Android, + * Class.getDeclaredMethod() reaches ART's Class_getDeclaredMethodInternal(), which + * returns null when ShouldDenyAccessToMember(result->GetArtMethod(), ...) rejects the + * blocked,test-api member. Class.getMethod() then translates the null result into + * NoSuchMethodException, so ACCESS_DENIED should exit through the catch below before + * the probe can inspect the method. + */ + Method method; + method = clazz.getDeclaredMethod(candidate.memberName(), parameterTypes); + return AccessResult.allowed(candidate, + "returnType=" + method.getReturnType().getName()); + } catch (NoSuchMethodException e) { + return AccessResult.denied(candidate, e); + } catch (SecurityException e) { + return AccessResult.denied(candidate, e); + } catch (RuntimeException e) { + return AccessResult.denied(candidate, e); + } + } + + private static String compatChangeEnabled(long changeId) { + try { + Class compatibility = Class.forName("android.compat.Compatibility"); + Object enabled = compatibility.getMethod("isChangeEnabled", long.class) + .invoke(null, changeId); + return String.valueOf(enabled); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException + | InvocationTargetException | RuntimeException e) { + return "unavailable:" + e.getClass().getSimpleName(); + } + } + + private static String exceptionDetail(Throwable t) { + String message = t.getMessage(); + return t.getClass().getSimpleName() + (message == null ? "" : ":" + message); + } + + private record Candidate( + String className, + String memberName, + String expectedValue, + String[] parameterClassNames) { + static Candidate field(String className, String fieldName, String expectedValue) { + return new Candidate(className, fieldName, expectedValue, null); + } + + static Candidate method(String className, String methodName, + String... parameterClassNames) { + return new Candidate(className, methodName, null, parameterClassNames); + } + + boolean isMethod() { + return parameterClassNames() != null; + } + + @Override + public String toString() { + if (!isMethod()) { + return className() + "#" + memberName(); + } + + return className() + "#" + memberName() + "(" + + String.join(",", parameterClassNames()) + ")"; + } + } + + public enum AccessOutcome { + ACCESS_ALLOWED, + ACCESS_DENIED, + MEMBER_NOT_PRESENT, + ACCESS_FAILED, + } + + public record AccessResult( + String candidate, + AccessOutcome outcome, + String detail) { + static AccessResult allowed(Candidate candidate, String detail) { + return new AccessResult(candidate.toString(), AccessOutcome.ACCESS_ALLOWED, + detail); + } + + static AccessResult denied(Candidate candidate, Throwable t) { + return new AccessResult(candidate.toString(), AccessOutcome.ACCESS_DENIED, + exceptionDetail(t)); + } + + static AccessResult missing(Candidate candidate, Throwable t) { + return new AccessResult(candidate.toString(), AccessOutcome.MEMBER_NOT_PRESENT, + exceptionDetail(t)); + } + + static AccessResult failed(Candidate candidate, Throwable t) { + return new AccessResult(candidate.toString(), AccessOutcome.ACCESS_FAILED, + exceptionDetail(t)); + } + + static AccessResult failed(Candidate candidate, String detail) { + return new AccessResult(candidate.toString(), AccessOutcome.ACCESS_FAILED, detail); + } + + static AccessResult failed(String candidate, String detail) { + return new AccessResult(candidate, AccessOutcome.ACCESS_FAILED, detail); + } + } + + public record TestApiCompat( + boolean execSpawned, + long allowTestApiAccessChangeId, + String frameworkCompatEnabled, + AccessResult accessResult) { + public boolean accessAllowed() { + return accessResult().outcome() == AccessOutcome.ACCESS_ALLOWED; + } + + @Override + public String toString() { + return "execSpawned=" + execSpawned() + + "\nallowTestApiAccessChangeId=" + allowTestApiAccessChangeId() + + "\nframeworkCompatEnabled=" + frameworkCompatEnabled() + + "\ncandidate=" + accessResult().candidate() + + "\noutcome=" + accessResult().outcome() + + "\ndetail=" + accessResult().detail() + + "\naccessAllowed=" + accessAllowed(); + } + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md b/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md new file mode 100644 index 0000000000000..61191a7b42b68 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md @@ -0,0 +1,24 @@ +# GosCompatSecureSpawnTests + +This module verifies the app process startup path selected by secure app spawning and by app +compatibility flags that require exec spawning. + +The package under test is `GosCompatSecureSpawnApp`, not `GosCompatCheckApp`. Exec spawning is +disabled for packages with `ApplicationInfo.FLAG_DEBUGGABLE`, and `GosCompatCheckApp` is intentionally +debuggable because the existing GosCompat modules use `run-as app.grapheneos.goscompat.checks` to read +result files from its app data directory. Making that shared helper non-debuggable would require +reworking those result collection paths or splitting the existing modules first, which is more churn +than this regression test needs. + +When `adb root` is available, then tests will set the secure app spawning to the needed state for +those tests and reboot. After those tests finish, the test will reset to the original secure app +spawning state and do another reboot. If `adb root` is not available, then the secure app spawning +prop cannot be set by shell, so the tests will fail the assumption. If secure app spawning state is +already set to the state needed for the test, no prop changes + reboot is needed and tests will just +run. + +Run the module directly with: + +```sh +atest GosCompatSecureSpawnTests +``` diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java new file mode 100644 index 0000000000000..7fe58100f49e2 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java @@ -0,0 +1,51 @@ +package app.grapheneos.goscompat.securespawn.tests; + +import android.content.pm.GosPackageStateFlag; + +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.AfterClassWithInfo; +import com.android.tradefed.testtype.junit4.BeforeClassWithInfo; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DeviceJUnit4ClassRunner.class) +public final class SecureSpawnDisabledHostTest extends SecureSpawnHostTestBase { + private static ExecSpawningClassState sExecSpawningState; + + @BeforeClassWithInfo + public static void beforeClass(TestInformation testInfo) throws Exception { + sExecSpawningState = captureExecSpawningState(testInfo, false); + sExecSpawningState = enterExecSpawningMode(testInfo, sExecSpawningState, false); + } + + @AfterClassWithInfo + public static void afterClass(TestInformation testInfo) throws Exception { + restoreExecSpawningMode(testInfo, sExecSpawningState); + } + + @Test + public void secureAppSpawningDisabledUsesZygoteInit() throws Exception { + resetPackageState(); + runDeviceTest("notExecSpawned"); + } + + @Test + public void disabledHardenedMallocUsesExecInit() throws Exception { + resetPackageState(); + editPackageState( + new int[] { GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT }, + new int[] { GosPackageStateFlag.USE_HARDENED_MALLOC }); + runDeviceTest("hardenedMallocDisabled"); + } + + @Test + public void exploitCompatibilityModeUsesExecInit() throws Exception { + resetPackageState(); + editPackageState( + new int[] { GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE }, + new int[0]); + runDeviceTest("hardenedMallocDisabled"); + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java new file mode 100644 index 0000000000000..62e4ac258f9dc --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java @@ -0,0 +1,31 @@ +package app.grapheneos.goscompat.securespawn.tests; + +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.AfterClassWithInfo; +import com.android.tradefed.testtype.junit4.BeforeClassWithInfo; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DeviceJUnit4ClassRunner.class) +public final class SecureSpawnEnabledHostTest extends SecureSpawnHostTestBase { + private static ExecSpawningClassState sExecSpawningState; + + @BeforeClassWithInfo + public static void beforeClass(TestInformation testInfo) throws Exception { + sExecSpawningState = captureExecSpawningState(testInfo, true); + sExecSpawningState = enterExecSpawningMode(testInfo, sExecSpawningState, true); + } + + @AfterClassWithInfo + public static void afterClass(TestInformation testInfo) throws Exception { + restoreExecSpawningMode(testInfo, sExecSpawningState); + } + + @Test + public void secureAppSpawningUsesExecInit() throws Exception { + resetPackageState(); + runDeviceTest("execSpawned"); + } +} diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java new file mode 100644 index 0000000000000..c4bbbeb6f6756 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java @@ -0,0 +1,427 @@ +package app.grapheneos.goscompat.securespawn.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import android.content.pm.GosPackageStateFlag; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.result.ByteArrayInputStreamSource; +import com.android.tradefed.result.LogDataType; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.testtype.junit4.DeviceTestRunOptions; +import com.android.tradefed.util.CommandResult; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +abstract class SecureSpawnHostTestBase extends BaseHostJUnit4Test { + private static final String PACKAGE_NAME = "app.grapheneos.goscompat.securespawn"; + private static final String PROFILEABLE_PACKAGE_NAME = + "app.grapheneos.goscompat.securespawn.profileable"; + private static final String TEST_CLASS = + "app.grapheneos.goscompat.securespawn.SecureSpawnDeviceTest"; + private static final String EXEC_SPAWN_PROPERTY = "persist.security.exec_spawn"; + private static final String LOG_TAG = "GosCompatSecureSpawn"; + private static final String ROOT_CAUSE_LOG_TAG = "GosCompatSecureSpawnRc"; + private static final String ROOT_CAUSE_LOG_TAG_PROPERTY = "log.tag." + ROOT_CAUSE_LOG_TAG; + private static final String MEMORY_ACCOUNTING_METHOD = + "runtimeMemoryAccountingCheck"; + private static final String HIDDEN_API_METHOD = + "hiddenApiEnforcementCheck"; + private static final String TEST_API_COMPAT_DEFAULT_METHOD = + "testApiCompatDefaultCheck"; + private static final String TEST_API_COMPAT_DISABLED_METHOD = + "testApiCompatDisabledCheck"; + private static final String TEST_API_COMPAT_ENABLED_METHOD = + "testApiCompatEnabledCheck"; + private static final String PROFILEABLE_DUMPABLE_METHOD = + "profileableFromShellDumpableCheck"; + private static final String ACYCLIC_REFLECTIVE_DUMP_METHOD = + "acyclicReflectiveDumpCheck"; + // From libcore/libart/src/main/java/dalvik/system/VMRuntime.java: + // private static final long ALLOW_TEST_API_ACCESS = 166236554; + private static final long ALLOW_TEST_API_ACCESS_CHANGE_ID = 166236554L; + + @Rule + public final TestLogData mLogs = new TestLogData(); + + @Before + public void setUp() throws Exception { + resetCompatState(); + resetPackageState(); + } + + @After + public void tearDown() throws Exception { + resetCompatState(); + resetPackageState(); + shell("am force-stop " + PACKAGE_NAME); + shell("am force-stop " + PROFILEABLE_PACKAGE_NAME); + } + + @Test + public void runtimeMemoryAccountingCheck() throws Exception { + runCheckCase(MEMORY_ACCOUNTING_METHOD); + } + + @Test + public void hiddenApiEnforcementCheck() throws Exception { + runCheckCase(HIDDEN_API_METHOD); + } + + @Test + public void testApiCompatDefaultCheck() throws Exception { + runTestApiCompatCheckCase(TEST_API_COMPAT_DEFAULT_METHOD, CompatOverride.DEFAULT); + } + + @Test + public void testApiCompatDisabledCheck() throws Exception { + runTestApiCompatCheckCase(TEST_API_COMPAT_DISABLED_METHOD, CompatOverride.DISABLED); + } + + @Test + public void testApiCompatEnabledCheck() throws Exception { + runTestApiCompatCheckCase(TEST_API_COMPAT_ENABLED_METHOD, CompatOverride.ENABLED); + } + + @Test + public void profileableFromShellDumpableCheck() throws Exception { + runProfileableCheckCase(PROFILEABLE_DUMPABLE_METHOD); + } + + @Test + public void acyclicReflectiveDumpCheck() throws Exception { + runCheckCase(ACYCLIC_REFLECTIVE_DUMP_METHOD); + } + + protected void runDeviceTest(String methodName) throws Exception { + runDeviceTest(PACKAGE_NAME, methodName); + } + + private void runDeviceTest(String packageName, String methodName) throws Exception { + boolean collectResultLog = resultLogName(methodName) != null; + boolean collectRootCauseLog = ACYCLIC_REFLECTIVE_DUMP_METHOD.equals(methodName); + String originalRootCauseLogTag = null; + try { + if (collectRootCauseLog) { + originalRootCauseLogTag = shell("getprop " + ROOT_CAUSE_LOG_TAG_PROPERTY).trim(); + setRootCauseLogTag("DEBUG"); + } + + shell("am force-stop " + packageName); + if (collectResultLog) { + shell("logcat -c"); + } + DeviceTestRunOptions options = new DeviceTestRunOptions(packageName); + options.setTestClassName(TEST_CLASS); + options.setTestMethodName(methodName); + if (isTestApiCompatMethod(methodName)) { + // Adds --no-test-api-access so ALLOW_TEST_API_ACCESS is the deciding signal. + options.setDisableTestApiCheck(false); + } + boolean passed = false; + AssertionError deviceAssertion = null; + try { + passed = runDeviceTests(options); + } catch (AssertionError e) { + deviceAssertion = e; + } catch (DeviceNotAvailableException e) { + throw new IllegalStateException(e); + } + if (collectResultLog) { + try { + logCheckResult(packageName, methodName); + } catch (AssertionError | Exception e) { + if (deviceAssertion == null) { + throw e; + } + deviceAssertion.addSuppressed(e); + } + } + if (deviceAssertion != null) { + throw deviceAssertion; + } + assertTrue(methodName, passed); + } finally { + if (collectRootCauseLog) { + shell("am force-stop " + packageName); + restoreRootCauseLogTag(originalRootCauseLogTag); + } + } + } + + protected void resetPackageState() throws Exception { + editPackageState( + new int[0], + new int[] { + GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE, + GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT, + GosPackageStateFlag.USE_HARDENED_MALLOC, + GosPackageStateFlag.USE_EXTENDED_VA_SPACE_NON_DEFAULT, + GosPackageStateFlag.USE_EXTENDED_VA_SPACE, + }); + } + + // TODO: Refactor this when HardeningTests get refactored + protected void editPackageState(int[] addFlags, int[] clearFlags) throws Exception { + StringBuilder command = new StringBuilder("pm edit-gos-package-state ") + .append(PACKAGE_NAME) + .append(' ') + .append(getDevice().getCurrentUser()); + for (int flag : addFlags) { + command.append(" add-flag ").append(flag); + } + for (int flag : clearFlags) { + command.append(" clear-flag ").append(flag); + } + command.append(" set-kill-uid-after-apply true"); + shell(command.toString()); + } + + protected static ExecSpawningClassState captureExecSpawningState( + TestInformation testInfo, boolean enabled) throws Exception { + ITestDevice device = testInfo.getDevice(); + ExecSpawningSetting original = readExecSpawning(device); + return new ExecSpawningClassState(original, original.enabled() != enabled, false); + } + + protected static ExecSpawningClassState enterExecSpawningMode( + TestInformation testInfo, ExecSpawningClassState state, boolean enabled) + throws Exception { + if (!state.needsChange()) { + return state; + } + + ITestDevice device = testInfo.getDevice(); + assumeTrue("adb root is needed to change " + EXEC_SPAWN_PROPERTY, + device.enableAdbRoot()); + assumeTrue(EXEC_SPAWN_PROPERTY + "=" + (enabled ? "1" : "0"), + setExecSpawning(device, enabled ? "1" : "0")); + state.markChanged(); + device.reboot(); + assertEquals(enabled, readExecSpawning(device).enabled()); + return state; + } + + protected static void restoreExecSpawningMode( + TestInformation testInfo, ExecSpawningClassState state) throws Exception { + if (state == null || !state.changed()) { + return; + } + + ITestDevice device = testInfo.getDevice(); + assertTrue("adb root", device.enableAdbRoot()); + assertTrue(EXEC_SPAWN_PROPERTY + "=" + state.original().rawValue(), + setExecSpawning(device, state.original().rawValue())); + device.reboot(); + assertEquals(state.original(), readExecSpawning(device)); + } + + protected static final class ExecSpawningClassState { + private final ExecSpawningSetting mOriginal; + private final boolean mNeedsChange; + private boolean mChanged; + + ExecSpawningClassState( + ExecSpawningSetting original, + boolean needsChange, + boolean changed) { + mOriginal = original; + mNeedsChange = needsChange; + mChanged = changed; + } + + ExecSpawningSetting original() { + return mOriginal; + } + + boolean needsChange() { + return mNeedsChange; + } + + boolean changed() { + return mChanged; + } + + void markChanged() { + mChanged = true; + } + } + + private void runCheckCase(String methodName) throws Exception { + try { + resetPackageState(); + runDeviceTest(methodName); + } catch (AssertionError | RuntimeException e) { + throw new AssertionError(methodName, e); + } + } + + private void runTestApiCompatCheckCase(String methodName, CompatOverride compatOverride) + throws Exception { + assumeCompatOverrideSupported(compatOverride); + try { + resetPackageState(); + setTestApiCompatOverride(compatOverride); + runDeviceTest(methodName); + } catch (AssertionError | RuntimeException e) { + throw new AssertionError(methodName, e); + } finally { + resetCompatState(); + } + } + + private void runProfileableCheckCase(String methodName) throws Exception { + try { + runDeviceTest(PROFILEABLE_PACKAGE_NAME, methodName); + } catch (AssertionError | RuntimeException e) { + throw new AssertionError(methodName, e); + } + } + + private String shell(String command) throws Exception { + return shell(getDevice(), command); + } + + private void logCheckResult(String packageName, String methodName) throws Exception { + ExecSpawningSetting setting = readExecSpawning(getDevice()); + String logcatTags = LOG_TAG + ":I"; + if (ACYCLIC_REFLECTIVE_DUMP_METHOD.equals(methodName)) { + logcatTags += " " + ROOT_CAUSE_LOG_TAG + ":D"; + } + String logcat = shell("logcat -d -v threadtime -s " + logcatTags + " '*:S'"); + String mode = setting.enabled() ? "enabled" : "disabled"; + String logName = resultLogName(methodName); + String report = "secureAppSpawning=" + setting.enabled() + + "\nproperty=" + (setting.rawValue().isEmpty() ? "" : setting.rawValue()) + + "\npackage=" + packageName + + "\nmethod=" + methodName + + "\n\n" + + logcat; + try (ByteArrayInputStreamSource source = + new ByteArrayInputStreamSource(report.getBytes(StandardCharsets.UTF_8))) { + mLogs.addTestLog("goscompat_secure_spawn_" + logName + "_" + mode, + LogDataType.TEXT, source); + } + } + + private static String resultLogName(String methodName) { + if (MEMORY_ACCOUNTING_METHOD.equals(methodName)) { + return "memory_accounting"; + } + if (HIDDEN_API_METHOD.equals(methodName)) { + return "hidden_api"; + } + if (TEST_API_COMPAT_DEFAULT_METHOD.equals(methodName)) { + return "test_api_compat_default"; + } + if (TEST_API_COMPAT_DISABLED_METHOD.equals(methodName)) { + return "test_api_compat_disabled"; + } + if (TEST_API_COMPAT_ENABLED_METHOD.equals(methodName)) { + return "test_api_compat_enabled"; + } + if (PROFILEABLE_DUMPABLE_METHOD.equals(methodName)) { + return "profileable_dumpable"; + } + if (ACYCLIC_REFLECTIVE_DUMP_METHOD.equals(methodName)) { + return "acyclic_reflective_dump"; + } + return null; + } + + private static boolean isTestApiCompatMethod(String methodName) { + return TEST_API_COMPAT_DEFAULT_METHOD.equals(methodName) + || TEST_API_COMPAT_DISABLED_METHOD.equals(methodName) + || TEST_API_COMPAT_ENABLED_METHOD.equals(methodName); + } + + private void setTestApiCompatOverride(CompatOverride compatOverride) throws Exception { + setCompatChange(compatOverride); + } + + private void assumeCompatOverrideSupported(CompatOverride compatOverride) throws Exception { + if (compatOverride == CompatOverride.DEFAULT) { + return; + } + + String buildType = shell("getprop ro.build.type").trim(); + assumeTrue("compat overrides for non-debuggable apps require a non-user build" + + " (ro.build.type=" + buildType + ")", + !"user".equals(buildType)); + } + + private void resetCompatState() throws Exception { + setCompatChange(CompatOverride.DEFAULT); + } + + private void setCompatChange(CompatOverride compatOverride) throws Exception { + shell("am compat " + compatOverride.command() + " " + + ALLOW_TEST_API_ACCESS_CHANGE_ID + " " + PACKAGE_NAME); + } + + private enum CompatOverride { + DEFAULT("reset"), + DISABLED("disable"), + ENABLED("enable"); + + private final String mCommand; + + CompatOverride(String command) { + mCommand = command; + } + + String command() { + return mCommand; + } + } + + private void restoreRootCauseLogTag(String value) throws Exception { + setRootCauseLogTag(value == null ? "" : value); + } + + private void setRootCauseLogTag(String value) throws Exception { + assertTrue(ROOT_CAUSE_LOG_TAG_PROPERTY + "=" + + (value.isEmpty() ? "" : value), + getDevice().setProperty(ROOT_CAUSE_LOG_TAG_PROPERTY, value)); + } + + private static String shell(ITestDevice device, String command) throws Exception { + CommandResult result = device.executeShellV2Command(command); + assertEquals(result.toString(), 0L, (long) result.getExitCode()); + return result.getStdout() == null ? "" : result.getStdout(); + } + + private static ExecSpawningSetting readExecSpawning(ITestDevice device) throws Exception { + String value = shell(device, "getprop " + EXEC_SPAWN_PROPERTY).trim(); + return new ExecSpawningSetting(value, parseExecSpawning(value)); + } + + private static boolean setExecSpawning(ITestDevice device, String value) throws Exception { + return device.setProperty(EXEC_SPAWN_PROPERTY, value); + } + + private static boolean parseExecSpawning(String value) { + if (value.isEmpty()) { + return true; + } + if (value.equals("true") || value.equals("1")) { + return true; + } + if (value.equals("false") || value.equals("0")) { + return false; + } + return true; + } + + protected record ExecSpawningSetting(String rawValue, boolean enabled) {} +} diff --git a/tests/GosCompatTests/README.md b/tests/GosCompatTests/README.md index 188dae45d722b..4edfe7b9fbed8 100644 --- a/tests/GosCompatTests/README.md +++ b/tests/GosCompatTests/README.md @@ -1,6 +1,6 @@ # GosCompatTests -These tests are generally shapped as non-privileged apps in order to regression test compatability +These tests are generally shapped as non-privileged apps in order to regression test compatability with patterns or code used in actual apps. `GosCompatCheckApp` is a standalone helper app with a manual UI. See subdirectories for descriptions @@ -12,8 +12,9 @@ You can run tests from the checkout root via this directory's `TEST_MAPPING`: atest --test-mapping frameworks/base/tests/GosCompatTests:gos_postsubmit ``` -Generally, the device should be unlocked and on user 0 while the tests are running. The tests can -be run on user builds both via `atest` and using the standalone UI in `GosCompatCheckApp`. +Generally, the device should be unlocked and on user 0 while the tests are running. Most tests can +be run on user builds both via `atest` and using the standalone UI in `GosCompatCheckApp`. Host +modules that mutate persistent device or package state can require adb root. Alternatively, view the `TEST_MAPPING` file or Android.bp files and run test modules directly with `atest`. diff --git a/tests/GosCompatTests/TEST_MAPPING b/tests/GosCompatTests/TEST_MAPPING index 2e625181ca3c4..3c521642e83a1 100644 --- a/tests/GosCompatTests/TEST_MAPPING +++ b/tests/GosCompatTests/TEST_MAPPING @@ -11,6 +11,10 @@ }, { "name": "GosCompatWifiScanTimeoutTests" + }, + // Needs adb root to set secure spawning prop, else ASSUMPTION_FAILED for tests with wrong state + { + "name": "GosCompatSecureSpawnTests" } ] } diff --git a/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java b/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java index 44931627f4b4c..526f9785c3848 100644 --- a/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java +++ b/tests/GosCompatTests/common/src/app/grapheneos/goscompat/checks/GosCompatContract.java @@ -181,6 +181,14 @@ private DmaBufRelease() { } } + public static final class SecureSpawn { + public static final String PACKAGE_NAME = "app.grapheneos.goscompat.securespawn"; + public static final String ACTIVITY_CLASS = PACKAGE_NAME + ".SecureSpawnActivity"; + + private SecureSpawn() { + } + } + private GosCompatContract() { } } From 26163a3754b826d63cca308daf0c89cdbb9939b6 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 15/35] adjust GosCompatSecureSpawnTests to exec spawning changes --- .../securespawn/SecureSpawnActivity.kt | 35 ++---- .../securespawn/SecureSpawnCheck.java | 25 +--- .../securespawn/SecureSpawnDeviceTest.java | 6 +- .../GosCompatSecureSpawnTests/README.md | 18 +-- .../tests/SecureSpawnDisabledHostTest.java | 36 +++--- .../tests/SecureSpawnEnabledHostTest.java | 26 +++-- .../tests/SecureSpawnHostTestBase.java | 108 +----------------- tests/GosCompatTests/TEST_MAPPING | 1 - 8 files changed, 62 insertions(+), 193 deletions(-) diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt index 390732d4cc955..bd27fed10fdef 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt @@ -3,6 +3,7 @@ package app.grapheneos.goscompat.securespawn import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.Color as AndroidColor +import android.net.Uri import android.os.Bundle import android.provider.Settings import android.widget.Toast @@ -121,19 +122,17 @@ class SecureSpawnActivity : ComponentActivity() { resultState = resultState, running = running, onRunCheck = { runCheckFromUi() }, - onOpenSecuritySettings = { openSecuritySettings() }, + onOpenAppInfo = { openAppInfo() }, ) } } } } - private fun openSecuritySettings() { - try { - startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) - } catch (e: ActivityNotFoundException) { - Toast.makeText(this, "Security settings unavailable", Toast.LENGTH_SHORT).show() - } + private fun openAppInfo() { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null)) + startActivity(intent) } private fun runCheck(onResult: (CheckResultState) -> Unit) { @@ -182,7 +181,7 @@ private fun SecureSpawnScreen( resultState: CheckResultState, running: Boolean, onRunCheck: () -> Unit, - onOpenSecuritySettings: () -> Unit, + onOpenAppInfo: () -> Unit, ) { Column( modifier = Modifier @@ -198,22 +197,16 @@ private fun SecureSpawnScreen( fontWeight = FontWeight.SemiBold, ) Text( - text = "For manual comparison, change secure app spawning, reboot, then run the " + - "check again.", + text = "For manual comparison, change secure app spawning setting, then run the check again.", style = MaterialTheme.typography.bodyMedium, ) Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Button( - onClick = onOpenSecuritySettings, + onClick = onOpenAppInfo, modifier = Modifier.testTag(SecureSpawnUiTags.OPEN_SECURITY_SETTINGS_BUTTON), ) { - Text("Open security settings") + Text("Open app settings") } - Text( - text = "Security & privacy > Exploit protection > Secure app spawning", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) } SecureAppSpawningCard(settingState) @@ -323,7 +316,6 @@ private fun SecureAppSpawningCard(settingState: SettingResultState) { status = if (settingState.setting.enabled()) "ENABLED" else "DISABLED", enabled = settingState.setting.enabled(), ) - DetailRow("Property", settingState.setting.rawValue().ifEmpty { "" }) } } } @@ -332,8 +324,7 @@ private fun SecureAppSpawningCard(settingState: SettingResultState) { @Composable private fun ProcessStateCard(processState: SecureSpawnCheck.ProcessState) { - val passed = processState.pid() > 0 && processState.tid() > 0 && - (!processState.hardenedMallocDisabled() || processState.execSpawned()) + val passed = processState.pid() > 0 && processState.tid() > 0 ResultCard { Section(label = "Process state", status = if (passed) "PASS" else "FAIL", passed = passed) { DetailRow("Exec spawned", processState.execSpawned().toString()) @@ -342,9 +333,7 @@ private fun ProcessStateCard(processState: SecureSpawnCheck.ProcessState) { DetailRow("TID", processState.tid().toString()) if (!passed) { Text( - text = "The process state check failed because hardened_malloc was reported " + - "disabled without the process being exec spawned, or because PID/TID " + - "could not be read.", + text = "The process state check failed because PID/TID could not be read.", style = MaterialTheme.typography.bodyMedium, ) } diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java index ada2d2078cb5e..f545bd8390546 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java @@ -8,8 +8,6 @@ import app.grapheneos.goscompat.securespawn.shared.SecureSpawnTestApiCompatCheck; public final class SecureSpawnCheck { - private static final String EXEC_SPAWN_PROPERTY = "persist.security.exec_spawn"; - static { System.loadLibrary("goscompat_secure_spawn_jni"); } @@ -29,7 +27,7 @@ public static Result run() { } public static SecureAppSpawningSetting secureAppSpawningSetting() { - return new SecureAppSpawningSetting(nativeSystemProperty(EXEC_SPAWN_PROPERTY)); + return new SecureAppSpawningSetting(System.getenv("IS_EXEC_SPAWNED_APP_PROCESS") != null); } public static ProcessState processState() { @@ -72,26 +70,7 @@ public record Result( SecureSpawnTestApiCompatCheck.TestApiCompat testApiCompatDefault, SecureSpawnReflectiveDumpCheck.AcyclicReflectiveDump acyclicReflectiveDump) {} - public record SecureAppSpawningSetting(String rawValue) { - public boolean enabled() { - if (rawValue().isEmpty()) { - return true; - } - if (rawValue().equals("true") || rawValue().equals("1")) { - return true; - } - if (rawValue().equals("false") || rawValue().equals("0")) { - return false; - } - return true; - } - - @Override - public String toString() { - return "enabled=" + enabled() - + "\nproperty=" + (rawValue().isEmpty() ? "" : rawValue()); - } - } + public record SecureAppSpawningSetting(boolean enabled) {} public record ProcessState( boolean execSpawned, diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java index a3a263576a9a8..9e9efb636ff5b 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnDeviceTest.java @@ -41,11 +41,11 @@ public void notExecSpawned() { } @Test - public void hardenedMallocDisabled() { + public void notExecSpawnedCompatZygote() { SecureSpawnCheck.ProcessState result = SecureSpawnCheck.processState(); assertProcessState(result); - assertWithMessage(failureMessage("expected execSpawned == true", result)) - .that(result.execSpawned()).isTrue(); + assertWithMessage(failureMessage("expected execSpawned == false", result)) + .that(result.execSpawned()).isFalse(); assertWithMessage(failureMessage("expected hardenedMallocDisabled == true", result)) .that(result.hardenedMallocDisabled()).isTrue(); } diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md b/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md index 61191a7b42b68..40909b3bdcf61 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md @@ -3,19 +3,11 @@ This module verifies the app process startup path selected by secure app spawning and by app compatibility flags that require exec spawning. -The package under test is `GosCompatSecureSpawnApp`, not `GosCompatCheckApp`. Exec spawning is -disabled for packages with `ApplicationInfo.FLAG_DEBUGGABLE`, and `GosCompatCheckApp` is intentionally -debuggable because the existing GosCompat modules use `run-as app.grapheneos.goscompat.checks` to read -result files from its app data directory. Making that shared helper non-debuggable would require -reworking those result collection paths or splitting the existing modules first, which is more churn -than this regression test needs. - -When `adb root` is available, then tests will set the secure app spawning to the needed state for -those tests and reboot. After those tests finish, the test will reset to the original secure app -spawning state and do another reboot. If `adb root` is not available, then the secure app spawning -prop cannot be set by shell, so the tests will fail the assumption. If secure app spawning state is -already set to the state needed for the test, no prop changes + reboot is needed and tests will just -run. +The package under test is the non-debuggable `GosCompatSecureSpawnApp`, not `GosCompatCheckApp`. +`GosCompatCheckApp` is intentionally debuggable because the existing GosCompat modules use +`run-as app.grapheneos.goscompat.checks` to read result files from its app data directory. Making +that shared helper non-debuggable would require reworking those result collection paths or splitting +the existing modules first, which is more churn than this regression test needs. Run the module directly with: diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java index 7fe58100f49e2..ec6b95a73ecc3 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java @@ -12,17 +12,21 @@ @RunWith(DeviceJUnit4ClassRunner.class) public final class SecureSpawnDisabledHostTest extends SecureSpawnHostTestBase { - private static ExecSpawningClassState sExecSpawningState; - @BeforeClassWithInfo - public static void beforeClass(TestInformation testInfo) throws Exception { - sExecSpawningState = captureExecSpawningState(testInfo, false); - sExecSpawningState = enterExecSpawningMode(testInfo, sExecSpawningState, false); - } - - @AfterClassWithInfo - public static void afterClass(TestInformation testInfo) throws Exception { - restoreExecSpawningMode(testInfo, sExecSpawningState); + @Override + protected void resetPackageState() throws Exception { + editPackageState( + new int[] { + GosPackageStateFlag.USE_EXEC_SPAWNING_NON_DEFAULT, + }, + new int[] { + GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE, + GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT, + GosPackageStateFlag.USE_HARDENED_MALLOC, + GosPackageStateFlag.USE_EXTENDED_VA_SPACE_NON_DEFAULT, + GosPackageStateFlag.USE_EXTENDED_VA_SPACE, + GosPackageStateFlag.USE_EXEC_SPAWNING, + }); } @Test @@ -32,20 +36,20 @@ public void secureAppSpawningDisabledUsesZygoteInit() throws Exception { } @Test - public void disabledHardenedMallocUsesExecInit() throws Exception { + public void exploitCompatibilityModeUsesCompatZygoteInit() throws Exception { resetPackageState(); editPackageState( - new int[] { GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT }, - new int[] { GosPackageStateFlag.USE_HARDENED_MALLOC }); - runDeviceTest("hardenedMallocDisabled"); + new int[] { GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE }, + new int[0]); + runDeviceTest("notExecSpawnedCompatZygote"); } @Test - public void exploitCompatibilityModeUsesExecInit() throws Exception { + public void exploitCompatibilityModePassesMemoryAccountingCheck() throws Exception { resetPackageState(); editPackageState( new int[] { GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE }, new int[0]); - runDeviceTest("hardenedMallocDisabled"); + runDeviceTest(MEMORY_ACCOUNTING_METHOD); } } diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java index 62e4ac258f9dc..850e417bd3ea3 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java @@ -1,5 +1,7 @@ package app.grapheneos.goscompat.securespawn.tests; +import android.content.pm.GosPackageStateFlag; + import com.android.tradefed.invoker.TestInformation; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.AfterClassWithInfo; @@ -10,17 +12,21 @@ @RunWith(DeviceJUnit4ClassRunner.class) public final class SecureSpawnEnabledHostTest extends SecureSpawnHostTestBase { - private static ExecSpawningClassState sExecSpawningState; - - @BeforeClassWithInfo - public static void beforeClass(TestInformation testInfo) throws Exception { - sExecSpawningState = captureExecSpawningState(testInfo, true); - sExecSpawningState = enterExecSpawningMode(testInfo, sExecSpawningState, true); - } - @AfterClassWithInfo - public static void afterClass(TestInformation testInfo) throws Exception { - restoreExecSpawningMode(testInfo, sExecSpawningState); + @Override + protected void resetPackageState() throws Exception { + editPackageState( + new int[] { + GosPackageStateFlag.USE_EXEC_SPAWNING_NON_DEFAULT, + GosPackageStateFlag.USE_EXEC_SPAWNING, + }, + new int[] { + GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE, + GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT, + GosPackageStateFlag.USE_HARDENED_MALLOC, + GosPackageStateFlag.USE_EXTENDED_VA_SPACE_NON_DEFAULT, + GosPackageStateFlag.USE_EXTENDED_VA_SPACE, + }); } @Test diff --git a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java index c4bbbeb6f6756..e572283ba9192 100644 --- a/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java @@ -29,11 +29,10 @@ abstract class SecureSpawnHostTestBase extends BaseHostJUnit4Test { "app.grapheneos.goscompat.securespawn.profileable"; private static final String TEST_CLASS = "app.grapheneos.goscompat.securespawn.SecureSpawnDeviceTest"; - private static final String EXEC_SPAWN_PROPERTY = "persist.security.exec_spawn"; private static final String LOG_TAG = "GosCompatSecureSpawn"; private static final String ROOT_CAUSE_LOG_TAG = "GosCompatSecureSpawnRc"; private static final String ROOT_CAUSE_LOG_TAG_PROPERTY = "log.tag." + ROOT_CAUSE_LOG_TAG; - private static final String MEMORY_ACCOUNTING_METHOD = + protected static final String MEMORY_ACCOUNTING_METHOD = "runtimeMemoryAccountingCheck"; private static final String HIDDEN_API_METHOD = "hiddenApiEnforcementCheck"; @@ -68,11 +67,6 @@ public void tearDown() throws Exception { shell("am force-stop " + PROFILEABLE_PACKAGE_NAME); } - @Test - public void runtimeMemoryAccountingCheck() throws Exception { - runCheckCase(MEMORY_ACCOUNTING_METHOD); - } - @Test public void hiddenApiEnforcementCheck() throws Exception { runCheckCase(HIDDEN_API_METHOD); @@ -159,17 +153,7 @@ private void runDeviceTest(String packageName, String methodName) throws Excepti } } - protected void resetPackageState() throws Exception { - editPackageState( - new int[0], - new int[] { - GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE, - GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT, - GosPackageStateFlag.USE_HARDENED_MALLOC, - GosPackageStateFlag.USE_EXTENDED_VA_SPACE_NON_DEFAULT, - GosPackageStateFlag.USE_EXTENDED_VA_SPACE, - }); - } + protected abstract void resetPackageState() throws Exception; // TODO: Refactor this when HardeningTests get refactored protected void editPackageState(int[] addFlags, int[] clearFlags) throws Exception { @@ -187,76 +171,6 @@ protected void editPackageState(int[] addFlags, int[] clearFlags) throws Excepti shell(command.toString()); } - protected static ExecSpawningClassState captureExecSpawningState( - TestInformation testInfo, boolean enabled) throws Exception { - ITestDevice device = testInfo.getDevice(); - ExecSpawningSetting original = readExecSpawning(device); - return new ExecSpawningClassState(original, original.enabled() != enabled, false); - } - - protected static ExecSpawningClassState enterExecSpawningMode( - TestInformation testInfo, ExecSpawningClassState state, boolean enabled) - throws Exception { - if (!state.needsChange()) { - return state; - } - - ITestDevice device = testInfo.getDevice(); - assumeTrue("adb root is needed to change " + EXEC_SPAWN_PROPERTY, - device.enableAdbRoot()); - assumeTrue(EXEC_SPAWN_PROPERTY + "=" + (enabled ? "1" : "0"), - setExecSpawning(device, enabled ? "1" : "0")); - state.markChanged(); - device.reboot(); - assertEquals(enabled, readExecSpawning(device).enabled()); - return state; - } - - protected static void restoreExecSpawningMode( - TestInformation testInfo, ExecSpawningClassState state) throws Exception { - if (state == null || !state.changed()) { - return; - } - - ITestDevice device = testInfo.getDevice(); - assertTrue("adb root", device.enableAdbRoot()); - assertTrue(EXEC_SPAWN_PROPERTY + "=" + state.original().rawValue(), - setExecSpawning(device, state.original().rawValue())); - device.reboot(); - assertEquals(state.original(), readExecSpawning(device)); - } - - protected static final class ExecSpawningClassState { - private final ExecSpawningSetting mOriginal; - private final boolean mNeedsChange; - private boolean mChanged; - - ExecSpawningClassState( - ExecSpawningSetting original, - boolean needsChange, - boolean changed) { - mOriginal = original; - mNeedsChange = needsChange; - mChanged = changed; - } - - ExecSpawningSetting original() { - return mOriginal; - } - - boolean needsChange() { - return mNeedsChange; - } - - boolean changed() { - return mChanged; - } - - void markChanged() { - mChanged = true; - } - } - private void runCheckCase(String methodName) throws Exception { try { resetPackageState(); @@ -293,17 +207,14 @@ private String shell(String command) throws Exception { } private void logCheckResult(String packageName, String methodName) throws Exception { - ExecSpawningSetting setting = readExecSpawning(getDevice()); String logcatTags = LOG_TAG + ":I"; if (ACYCLIC_REFLECTIVE_DUMP_METHOD.equals(methodName)) { logcatTags += " " + ROOT_CAUSE_LOG_TAG + ":D"; } String logcat = shell("logcat -d -v threadtime -s " + logcatTags + " '*:S'"); - String mode = setting.enabled() ? "enabled" : "disabled"; + String mode = this instanceof SecureSpawnEnabledHostTest ? "enabled" : "disabled"; String logName = resultLogName(methodName); - String report = "secureAppSpawning=" + setting.enabled() - + "\nproperty=" + (setting.rawValue().isEmpty() ? "" : setting.rawValue()) - + "\npackage=" + packageName + String report = "package=" + packageName + "\nmethod=" + methodName + "\n\n" + logcat; @@ -401,15 +312,6 @@ private static String shell(ITestDevice device, String command) throws Exception return result.getStdout() == null ? "" : result.getStdout(); } - private static ExecSpawningSetting readExecSpawning(ITestDevice device) throws Exception { - String value = shell(device, "getprop " + EXEC_SPAWN_PROPERTY).trim(); - return new ExecSpawningSetting(value, parseExecSpawning(value)); - } - - private static boolean setExecSpawning(ITestDevice device, String value) throws Exception { - return device.setProperty(EXEC_SPAWN_PROPERTY, value); - } - private static boolean parseExecSpawning(String value) { if (value.isEmpty()) { return true; @@ -422,6 +324,4 @@ private static boolean parseExecSpawning(String value) { } return true; } - - protected record ExecSpawningSetting(String rawValue, boolean enabled) {} } diff --git a/tests/GosCompatTests/TEST_MAPPING b/tests/GosCompatTests/TEST_MAPPING index 3c521642e83a1..7bd39f8c4bc41 100644 --- a/tests/GosCompatTests/TEST_MAPPING +++ b/tests/GosCompatTests/TEST_MAPPING @@ -12,7 +12,6 @@ { "name": "GosCompatWifiScanTimeoutTests" }, - // Needs adb root to set secure spawning prop, else ASSUMPTION_FAILED for tests with wrong state { "name": "GosCompatSecureSpawnTests" } From 346244e1007443eaca761af692c057c89fce0b12 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 16/35] fixup! add base class for complex per-app switches --- core/java/android/ext/settings/app/AppSwitch.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/android/ext/settings/app/AppSwitch.java b/core/java/android/ext/settings/app/AppSwitch.java index d4b172e7212e6..989c268bf4c43 100644 --- a/core/java/android/ext/settings/app/AppSwitch.java +++ b/core/java/android/ext/settings/app/AppSwitch.java @@ -31,6 +31,7 @@ public abstract class AppSwitch { public static final int IR_IS_DEBUGGABLE_APP = 5; public static final int IR_EXPLOIT_PROTECTION_COMPAT_MODE = 6; public static final int IR_REQUIRED_BY_HARDENED_MALLOC = 7; + public static final int IR_REQUIRED_BY_ZYGOTE_SPAWNING = 8; // default value reasons public static final int DVR_UNKNOWN = 0; From 82c56cea3169faad4cbb833f154e9b2338cc7827 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 17/35] fixup! add per-app setting for hardened_malloc and extended VA space --- .../settings/app/AswUseExtendedVaSpace.java | 41 ++++++++++++++++--- .../settings/app/AswUseHardenedMalloc.java | 7 ---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java b/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java index 3f20df325d4d9..3b1e07327a5e8 100644 --- a/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java +++ b/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java @@ -4,9 +4,12 @@ import android.content.pm.ApplicationInfo; import android.content.pm.GosPackageState; import android.content.pm.GosPackageStateFlag; +import android.os.Build; import com.android.server.os.nano.AppCompatProtos; +import java.util.Objects; + import dalvik.system.VMRuntime; /** @hide */ @@ -24,23 +27,51 @@ public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInf GosPackageState ps, StateInfo si) { if (AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)) { si.immutabilityReason = IR_REQUIRED_BY_HARDENED_MALLOC; - return true; + return Boolean.TRUE; } String primaryAbi = appInfo.primaryCpuAbi; - if (primaryAbi != null && !VMRuntime.is64BitAbi(primaryAbi)) { - si.immutabilityReason = IR_NON_64_BIT_NATIVE_CODE; - return false; + if (primaryAbi != null) { + String isa = Objects.requireNonNull(VMRuntime.getInstructionSet(primaryAbi)); + if (!VMRuntime.is64BitInstructionSet(isa)) { + si.immutabilityReason = IR_NON_64_BIT_NATIVE_CODE; + return Boolean.FALSE; + } + if (!isAvailable(isa)) { + return Boolean.TRUE; + } + } + + if (!isAvailable()) { + return Boolean.TRUE; + } + + if (!AswUseExecSpawning.I.get(ctx, userId, appInfo, ps)) { + // When zygote spawning is used, extended VA space can't be used without also using + // hardened_malloc. This is a consequence of having 2 zygotes: + // - primary zygote with hardened_malloc and extended VA space + // - compat zygote with scudo and, on arm64, 39-bit VA space + si.immutabilityReason = IR_REQUIRED_BY_ZYGOTE_SPAWNING; + return Boolean.FALSE; } if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; - return false; + return Boolean.FALSE; } return null; } + public static boolean isAvailable() { + return isAvailable(VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0])); + } + + public static boolean isAvailable(String isa) { + // disabling extended VA space is supported only on arm64 + return isa.equals("arm64"); + } + @Override protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, GosPackageState ps, StateInfo si) { diff --git a/core/java/android/ext/settings/app/AswUseHardenedMalloc.java b/core/java/android/ext/settings/app/AswUseHardenedMalloc.java index d02a8d2afaeae..f219cf31b5694 100644 --- a/core/java/android/ext/settings/app/AswUseHardenedMalloc.java +++ b/core/java/android/ext/settings/app/AswUseHardenedMalloc.java @@ -34,13 +34,6 @@ public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInf return false; } - if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { - // turning off hardened_malloc requires exec spawning, which is always disabled for - // debuggable apps - si.immutabilityReason = IR_IS_DEBUGGABLE_APP; - return true; - } - if (appInfo.isSystemApp()) { si.immutabilityReason = IR_IS_SYSTEM_APP; return true; From 632185991ff394685089582254ca07360051b434 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 17:13:02 +0000 Subject: [PATCH 18/35] fixup! add connectivity checks setting and its migration from Settings.Global ConnChecksSetting.get() is used by Vanadium. Accessing it worked without the UnsupportedAppUsage annotation due to broken hidden API access enforcement with the previous exec spawning implementation. --- core/java/android/ext/settings/ConnChecksSetting.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/java/android/ext/settings/ConnChecksSetting.java b/core/java/android/ext/settings/ConnChecksSetting.java index 0bcc7287a2298..b6247a0acb371 100644 --- a/core/java/android/ext/settings/ConnChecksSetting.java +++ b/core/java/android/ext/settings/ConnChecksSetting.java @@ -1,6 +1,7 @@ package android.ext.settings; import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; import android.os.SystemProperties; /** @hide */ @@ -23,6 +24,7 @@ public class ConnChecksSetting { ConnChecksSetting.VAL_GRAPHENEOS, ConnChecksSetting.VAL_STANDARD, ConnChecksSetting.VAL_DISABLED ); + @UnsupportedAppUsage public static int get() { return SYS_PROP.get(); } From e30221d92b2846ff2b4ff4d64e92209ceb185507 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Fri, 12 Jun 2026 18:52:58 +0000 Subject: [PATCH 19/35] fixup! add exec spawning implementation Remove leftover useFifoUi change. --- core/java/com/android/internal/os/Zygote.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index ff5b68f6c7bb6..a760ef7bf0c38 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -370,13 +370,11 @@ static int forkAndSpecialize(ZygoteExtraArgs extraArgs, int uid, int gid, int[] // single-threaded mode which is required for changing the SELinux context ZygoteHooks.preFork(); - boolean useFifoUi = isExecSpawning || - (com.android.internal.os.Flags.zygoteEarlyFifoBoost() ? - SystemProperties.getInt("sys.use_fifo_ui", 0) == 1 : false); + boolean useFifoUi = SystemProperties.getInt("sys.use_fifo_ui", 0) == 1; int pid = nativeForkAndSpecialize(extraArgs.makeJniLongArray(), uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp, - useFifoUi, + com.android.internal.os.Flags.zygoteEarlyFifoBoost() ? useFifoUi : false, pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs, bindMountAppStorageDirs, bindMountSyspropOverrides); if (pid == 0) { From 92b6c5abedd9507d2f8772da3378e66638039df7 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 05:11:03 +0000 Subject: [PATCH 20/35] fixup! gosps: add USE_EXEC_SPAWNING flags --- core/java/android/content/pm/GosPackageStateFlag.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/java/android/content/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index 3939ec82061e3..79c05d16711b3 100644 --- a/core/java/android/content/pm/GosPackageStateFlag.java +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -66,6 +66,8 @@ public interface GosPackageStateFlag { PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE, SUPPRESS_PLAY_INTEGRITY_API_NOTIF, BLOCK_PLAY_INTEGRITY_API, + USE_EXEC_SPAWNING_NON_DEFAULT, + USE_EXEC_SPAWNING, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} From 8a9dd1404ca4f779dd11d1262864694d0a68d41f Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 06:03:22 +0000 Subject: [PATCH 21/35] fixup! refactor zygote infrastructure to support arbitrary number of zygotes --- core/java/android/os/ZygoteProcess.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 56edab0233058..4846a1296787b 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -926,6 +926,12 @@ public boolean setApiDenylistExemptions(List exemptions) { mApiDenylistExemptions = exemptions; boolean ok = true; for (var type : ZygoteType.values()) { + ZygoteState zygoteState = mZygoteStates[type.ordinal()]; + if (zygoteState == null && isLazilyStarted(type)) { + // maybeSetApiDenylistExemptions() is called during initial attemptConnectionToZygote() + continue; + } + if (!maybeSetApiDenylistExemptions(mZygoteStates[type.ordinal()], true)) { ok = false; } @@ -934,6 +940,13 @@ public boolean setApiDenylistExemptions(List exemptions) { } } + private static boolean isLazilyStarted(ZygoteType type) { + switch (type) { + default: + return false; + } + } + /** * Set the precentage of detected hidden API accesses that are logged to the event log. * From 43dbbca872cc8befb7b39b28c7dc55627c35c258 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 06:03:28 +0000 Subject: [PATCH 22/35] fixup! add compat zygote instance which uses scudo instead of hardened_malloc --- core/java/android/os/ZygoteProcess.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 4846a1296787b..0e6fb3e58b4f3 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -942,6 +942,8 @@ public boolean setApiDenylistExemptions(List exemptions) { private static boolean isLazilyStarted(ZygoteType type) { switch (type) { + case ZygoteType.Compat: + return true; default: return false; } From 6c983a6f73cff1d5a97d3cb43927d912b9b0bf87 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 06:38:05 +0000 Subject: [PATCH 23/35] fixup! add exec spawning implementation --- core/java/android/os/ZygoteProcess.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 0e6fb3e58b4f3..adf05f8a0607e 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -534,6 +534,7 @@ private static boolean policySpecifiesUsapPoolLaunch(int zygotePolicyFlags) { * arguments. */ private static final String[] INVALID_USAP_FLAGS = { + ZygoteExtraArgs.ARG_COMPLEX_COMMAND_MARKER, // USAPs are pointless when exec spawning is used "--query-abi-list", "--get-pid", "--preload-default", From 8ce048e167cc1255e2eebcacfbb2855080725c6d Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 07:08:55 +0000 Subject: [PATCH 24/35] fixup! add exec spawning implementation Simplify and speed up handling of parent file descriptors in the child process by using the close_range(CLOSE_RANGE_CLOEXEC) + exec() pattern. This change also fixes a potential issue of a Java daemon thread opening a file descriptor between GetOpenFdsIgnoring() and fork() which would have caused file descriptor table check to fail in the child process. --- core/jni/com_android_internal_os_Zygote.cpp | 31 ++++++++------------- core/jni/fd_utils.cpp | 4 ++- core/jni/fd_utils.h | 3 -- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 63fca40e8184e..3ebdbffd0344a 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -3050,10 +3051,6 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, jbyteArray command_buf, jboolean disable_hardened_malloc, jboolean enable_compat_va_39_bit) { - auto fail_fn = std::bind(zygote::ZygoteFailure, env, - "exec_spawning_zygote", - nullptr, _1); - int cmd_fd = memfd_create("zygote_fork_exec_cmds", 0); if (cmd_fd < 0) { ALOGE("memfd_create failed: %s", strerror(errno)); @@ -3094,17 +3091,7 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, } } - // see zygote::ForkCommon for the reasoning behind this sequence - // (SetSignalHandlers -> block SIGCHLD -> get open fds) - SetSignalHandlers(); - BlockSignal(SIGCHLD, fail_fn); - - std::vector fds_to_ignore; - fds_to_ignore.push_back(cmd_fd); - std::unique_ptr> open_fds = GetOpenFdsIgnoring(fds_to_ignore, fail_fn); - pid_t pid = fork(); - UnblockSignal(SIGCHLD, fail_fn); if (pid != 0) { // parent process @@ -3119,12 +3106,18 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, } return pid; } else { - // close all file descriptors except for the command file descriptor - for (int fd : *open_fds) { - if (close(fd) != 0) { - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close(%d) failed: %s", fd, strerror(errno)); + // Close all file descriptors except for the command file descriptor. Note that the parent + // process is multithreaded at fork time since it has Java daemon threads in addition to the + // main thread. + if (close_range(0, cmd_fd - 1, CLOSE_RANGE_CLOEXEC) != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) up to %d failed: %s", cmd_fd - 1, strerror(errno)); + _exit(1); } - } + if (close_range(cmd_fd + 1, ~0U, CLOSE_RANGE_CLOEXEC) != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) from %d failed: %s", cmd_fd + 1, strerror(errno)); + _exit(1); + } + #if defined(__aarch64__) const int FLAG_COMPAT_VA_39_BIT = 1 << 30; execveat(-1, argv[0], (char **) argv, environ, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0); diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index 6ae0e319828f1..30beed10bd218 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -485,6 +485,8 @@ void FileDescriptorInfo::DetachSocket(fail_fn_t fail_fn) const { // TODO: Move the definitions here and eliminate the forward declarations. They // temporarily help making code reviews easier. static int ParseFd(dirent* dir_entry, int dir_fd); +static std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, + fail_fn_t fail_fn); FileDescriptorTable* FileDescriptorTable::Create(const std::vector& fds_to_ignore, fail_fn_t fail_fn) { @@ -496,7 +498,7 @@ FileDescriptorTable* FileDescriptorTable::Create(const std::vector& fds_to_ return new FileDescriptorTable(std::move(open_fd_map)); } -std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, +static std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, fail_fn_t fail_fn) { DIR* proc_fd_dir = opendir(kFdPath); if (proc_fd_dir == nullptr) { diff --git a/core/jni/fd_utils.h b/core/jni/fd_utils.h index 19cdbfe3a43cd..6a7b2806ebfae 100644 --- a/core/jni/fd_utils.h +++ b/core/jni/fd_utils.h @@ -110,7 +110,4 @@ class FileDescriptorTable { DISALLOW_COPY_AND_ASSIGN(FileDescriptorTable); }; -std::unique_ptr> GetOpenFdsIgnoring(const std::vector& fds_to_ignore, - fail_fn_t fail_fn); - #endif // FRAMEWORKS_BASE_CORE_JNI_FD_UTILS_H_ From 7e9106751494e1e864d87702befac1289f024eab Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 07:19:35 +0000 Subject: [PATCH 25/35] fixup! add compat zygote instance which uses scudo instead of hardened_malloc --- core/java/android/os/ZygoteProcess.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index adf05f8a0607e..9ea940572a492 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -1069,7 +1069,9 @@ private ZygoteState attemptConnectionToZygote(ZygoteType type) throws IOExceptio int typeIdx = type.ordinal(); ZygoteState zygoteState = mZygoteStates[typeIdx]; if (zygoteState == null || zygoteState.isClosed()) { - Log.d(LOG_TAG, "attemptConnectionToZygote " + type, new Throwable()); + if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + Log.v(LOG_TAG, "attemptConnectionToZygote " + type, new Throwable()); + } if (type == ZygoteType.Compat) { if (!"running".equals(SystemProperties.get("init.svc.zygote_compat", null))) { long start = SystemClock.elapsedRealtime(); From f5ac3d7db3f4a2855781d2d639c9d17a6e078acb Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 07:33:05 +0000 Subject: [PATCH 26/35] fixup! add compat zygote instance which uses scudo instead of hardened_malloc --- core/java/android/os/ZygoteProcess.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 9ea940572a492..9f956be3bfbbe 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -1094,22 +1094,30 @@ private ZygoteState attemptConnectionToZygote(ZygoteType type) throws IOExceptio return zygoteState; } + @GuardedBy("mLock") private void startCompatZygote() throws IOException { SystemProperties.set("sys.start_compat_zygote", "1"); boolean started = false; LocalSocketAddress zygoteSocketAddress = mZygoteSocketAddresses[ZygoteType.Compat.ordinal()]; try (var zygoteSocket = new LocalSocket()) { - for (int i = 0; i < 2000; ++i) { + // The following loop is a lighter-weight variant of waitForConnectionToZygote(). Note + // that both mLock and the global ActivityManagerService lock are held at this point. + // zygote_compat startup/ usually completes in under 2 seconds. Starting zygote_compat + // lazily saves ~200 MiB of RAM as of Android 16 QPR2 when zygote_compat isn't needed. + final int TIMEOUT_MS = 20_000; + final int RETRY_DELAY_MS = 10; + int numRetries = TIMEOUT_MS / RETRY_DELAY_MS; + for (int i = 0; i < numRetries; ++i) { try { zygoteSocket.connect(zygoteSocketAddress); started = true; break; } catch (IOException e) { - if ((i % 20) == 0) { + if ((i % 50) == 0) { Log.d(LOG_TAG, "waiting for compat zygote to start"); } - SystemClock.sleep(10); + SystemClock.sleep(RETRY_DELAY_MS); } } } From e0acbf06bf744ff75db1fb9f89b180a230ca8756 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 08:11:07 +0000 Subject: [PATCH 27/35] fixup! add exec spawning implementation strerror() is not guaranteed to be async-safe. Use "%#m" instead, which outputs the name (not description) of errno value, e.g. EPERM. --- core/jni/com_android_internal_os_Zygote.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 3ebdbffd0344a..1dc8476c4f4e9 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3110,27 +3110,27 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, // process is multithreaded at fork time since it has Java daemon threads in addition to the // main thread. if (close_range(0, cmd_fd - 1, CLOSE_RANGE_CLOEXEC) != 0) { - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) up to %d failed: %s", cmd_fd - 1, strerror(errno)); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) up to %d failed: %#m", cmd_fd - 1); _exit(1); } if (close_range(cmd_fd + 1, ~0U, CLOSE_RANGE_CLOEXEC) != 0) { - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) from %d failed: %s", cmd_fd + 1, strerror(errno)); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) from %d failed: %#m", cmd_fd + 1); _exit(1); } #if defined(__aarch64__) const int FLAG_COMPAT_VA_39_BIT = 1 << 30; execveat(-1, argv[0], (char **) argv, environ, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0); - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execveat failed: %s", strerror(errno)); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execveat failed: %#m"); if (errno == EINVAL) { // kernel doesn't support FLAG_COMPAT_VA_39_BIT, or a different error that will // be returned by execv() anyway execv(argv[0], (char **) argv); - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %s", strerror(errno)); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %#m"); } #else execv(argv[0], (char **) argv); - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %s", strerror(errno)); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %#m"); #endif // defined(__aarch64__) // exec failed From 837ec5724b6cfe2bd109f08669ca1ba32c68c592 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 09:07:00 +0000 Subject: [PATCH 28/35] fixup! add exec spawning implementation Add handling of app zygote preloading. --- core/java/android/app/ActivityThread.java | 1 + .../com/android/internal/os/ExecSpawning.java | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 73be579af6951..7fe280fa2b215 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -5545,6 +5545,7 @@ private void handleCreateService(CreateServiceData data) { cl = packageInfo.getClassLoader(); } { + com.android.internal.os.ExecSpawning.handleAppZygotePreload(data.info, packageInfo); String className = data.info.name; service = ActivityThreadHooks.instantiateService(className); if (service == null) { diff --git a/core/java/com/android/internal/os/ExecSpawning.java b/core/java/com/android/internal/os/ExecSpawning.java index 89073adda5cab..26434bc695fb7 100644 --- a/core/java/com/android/internal/os/ExecSpawning.java +++ b/core/java/com/android/internal/os/ExecSpawning.java @@ -1,5 +1,9 @@ package com.android.internal.os; +import android.app.LoadedApk; +import android.app.ZygotePreload; +import android.content.ComponentName; +import android.content.pm.ServiceInfo; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.Os; @@ -101,4 +105,60 @@ static Runnable replayCommands(ZygoteServer zygoteServer) { } throw new IllegalStateException("unreachable"); } + + private static boolean handledAppZygotePreload; + + // App zygote preloading is pointless when exec spawning is used but apps might depend on it + public static void handleAppZygotePreload(ServiceInfo serviceInfo, LoadedApk loadedApk) { + if (!isExecSpawnedProcess()) { + return; + } + if ((serviceInfo.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) == 0) { + return; + } + + String zygotePreloadName = serviceInfo.applicationInfo.zygotePreloadName; + if (zygotePreloadName == null) { + Log.e(TAG, "maybePerformAppZygotePreload: FLAG_USE_APP_ZYGOTE is set but zygotePreloadName is null"); + return; + } + + switch (zygotePreloadName) { + case "org.chromium.chrome.app.TrichromeZygotePreload": + case "org.chromium.content_public.app.ZygotePreload": + case "org.mozilla.gecko.process.ZygotePreload": + Log.i(TAG, "skipping ZygotePreload that is known to be optional: " + zygotePreloadName); + return; + } + + // Note that the process is in the isolated_app SELinux domain at this point. When zygote + // spawning is used, app zygote is running in the app_zygote SELinux domain when it performs + // preloading. This shouldn't cause issues in practice since app_zygote and isolated_app + // domains have similar level of isolation. + synchronized (ExecSpawning.class) { + if (handledAppZygotePreload) { + throw new IllegalStateException("app zygote preloading was already handled"); + } + handledAppZygotePreload = true; + + String className = ComponentName.createRelative( + serviceInfo.applicationInfo.packageName, zygotePreloadName).getClassName(); + + // copied from AppZygoteInit.handlePreloadApp() + try { + Class cls = Class.forName(className, true, loadedApk.getClassLoader()); + if (!ZygotePreload.class.isAssignableFrom(cls)) { + Log.e(TAG, className + " does not implement " + + ZygotePreload.class.getName()); + return; + } + var preloadObject = (ZygotePreload) cls.getConstructor().newInstance(); + Log.i(TAG, "handleAppZygotePreload: starting preload via " + zygotePreloadName); + preloadObject.doPreload(serviceInfo.applicationInfo); + Log.i(TAG, "handleAppZygotePreload: finished preload"); + } catch (ReflectiveOperationException e) { + Log.e(TAG, "preload failed for " + zygotePreloadName, e); + } + } + } } From aa8c224e4eb6ffd434ad86d2ad1166226e783a24 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 09:27:24 +0000 Subject: [PATCH 29/35] fixup! add exec spawning implementation Add missing handling for external services which are spawned from an app zygote. --- core/java/android/os/ZygoteProcess.java | 2 +- .../com/android/server/am/ActiveServices.java | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 9f956be3bfbbe..7cc448f642be2 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -1103,7 +1103,7 @@ private void startCompatZygote() throws IOException { try (var zygoteSocket = new LocalSocket()) { // The following loop is a lighter-weight variant of waitForConnectionToZygote(). Note // that both mLock and the global ActivityManagerService lock are held at this point. - // zygote_compat startup/ usually completes in under 2 seconds. Starting zygote_compat + // zygote_compat startup usually completes in under 2 seconds. Starting zygote_compat // lazily saves ~200 MiB of RAM as of Android 16 QPR2 when zygote_compat isn't needed. final int TIMEOUT_MS = 20_000; final int RETRY_DELAY_MS = 10; diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 5b29c7b101125..abdd276a3c384 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -6090,12 +6090,23 @@ private String bringUpServiceInnerLocked(ServiceRecord r, int intentFlags, boole r.definingUid, r.serviceInfo.processName); } if ((r.serviceInfo.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) { - if (!r.definingPackageName.equals(r.appInfo.packageName)) { - new SystemErrorNotification("exec spawning error", "mismatch between definingPackageName (" + r.definingPackageName + ") and appInfo package name (" + r.appInfo.packageName + ") for service " + r.name).show(mAm.mContext); + // definingPackageName is distinct from r.appInfo.packageName when the service + // is an external service, see ServiceInfo.FLAG_EXTERNAL_SERVICE + String definingPkgName = r.definingPackageName; + int userId = r.userId; + + var pmi = LocalServices.getService(PackageManagerInternal.class); + ApplicationInfo definingAppInfo = pmi.getApplicationInfo(definingPkgName, 0, + SYSTEM_UID, userId); + + if (definingAppInfo == null) { + Slog.e(TAG, "bringUpServiceInnerLocked: definingAppInfo is null"); + return null; } - if (!AswUseExecSpawning.I.get(mAm.mContext, r.userId, r.appInfo, GosPackageState.get(r.definingPackageName, r.userId))) { + + if (!AswUseExecSpawning.I.get(mAm.mContext, userId, definingAppInfo, GosPackageState.get(definingPkgName, userId))) { // app zygotes are pointless when exec spawning is used - hostingRecord = HostingRecord.byAppZygote_(r.instanceName, r.definingPackageName, + hostingRecord = HostingRecord.byAppZygote_(r.instanceName, definingPkgName, r.definingUid, r.serviceInfo.processName); } } From 2d9b0db21927022d147e52907350b636bf34cef0 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 11:33:06 +0000 Subject: [PATCH 30/35] fixup! add exec spawning implementation --- core/jni/com_android_internal_os_Zygote.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 1dc8476c4f4e9..ff7b625c767bc 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3106,9 +3106,9 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, } return pid; } else { - // Close all file descriptors except for the command file descriptor. Note that the parent - // process is multithreaded at fork time since it has Java daemon threads in addition to the - // main thread. + // Set CLOEXEC for all file descriptors except for the command file descriptor. Note that + // the parent process is multithreaded at fork time since it has Java daemon threads in + // addition to the main thread. if (close_range(0, cmd_fd - 1, CLOSE_RANGE_CLOEXEC) != 0) { async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) up to %d failed: %#m", cmd_fd - 1); _exit(1); From 22141b78b747255f334c4e27e8ff3303f267110d Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 12:58:53 +0000 Subject: [PATCH 31/35] fixup! add exec spawning implementation --- core/jni/com_android_internal_os_Zygote.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index ff7b625c767bc..f8b0a4cdf5a6c 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3091,7 +3091,8 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, } } - pid_t pid = fork(); + // fork() runs bionic fork hooks which are unnecessary for this use-case + pid_t pid = _Fork(); if (pid != 0) { // parent process From 3a5a33d42f7ef00eadd9f87b1a436e90ff5a97b7 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 13:01:00 +0000 Subject: [PATCH 32/35] fixup! add exec spawning implementation --- core/jni/com_android_internal_os_Zygote.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index f8b0a4cdf5a6c..bba4b55d789b4 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3091,6 +3091,10 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, } } + // Signal handlers are set at this point in 64-bit zygote since system_server is forked from it + // first, but 32-bit zygote won't have them set if this is the first app launch zygote command. + SetSignalHandlers(); + // fork() runs bionic fork hooks which are unnecessary for this use-case pid_t pid = _Fork(); From ea6744b3d0dd70b3c5d43dfb0e7e5fa7fea794e3 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 13:47:02 +0000 Subject: [PATCH 33/35] fixup! add exec spawning implementation Replace non-thread-safe usages of setenv() and unsetenv(). --- core/jni/com_android_internal_os_Zygote.cpp | 68 +++++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index bba4b55d789b4..b3c518c642d8a 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3046,6 +3046,45 @@ static jint com_android_internal_os_Zygote_nativeCurrentTaggingLevel(JNIEnv* env #endif // defined(__aarch64__) } +static void free_environ(char** env) { + for (size_t i = 0; env[i] != nullptr; ++i) { + free(env[i]); + } + free(env); +} + +static char** clone_environ(const char* extra_variable) { + size_t count = 0; + + while (environ[count] != nullptr) { + ++count; + } + + // 1 slot for NULL terminator and 1 slot for extra_variable + char** new_environ = (char**) calloc(count + 2, sizeof(char*)); + if (new_environ == nullptr) { + return nullptr; + } + + for (size_t i = 0; i < count; ++i) { + new_environ[i] = strdup(environ[i]); + if (new_environ[i] == nullptr) { + free_environ(new_environ); + return nullptr; + } + } + + if (extra_variable != nullptr) { + new_environ[count] = strdup(extra_variable); + if (new_environ[count] == nullptr) { + free_environ(new_environ); + return nullptr; + } + } + + return new_environ; +} + static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, jboolean is_64_bit, jbyteArray command_buf, @@ -3083,12 +3122,19 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, nullptr, }; + bool is_environment_cloned = false; + char** environment; if (disable_hardened_malloc) { - if (setenv("DISABLE_HARDENED_MALLOC", "1", 1) != 0) { - ALOGE("setenv failed: %s", strerror(errno)); + // setenv() can't be used since zygote is multi-threaded at this point + environment = clone_environ("DISABLE_HARDENED_MALLOC=1"); + if (environment == nullptr) { + ALOGE("clone_environ failed: %s", strerror(errno)); close(cmd_fd); return -1; } + is_environment_cloned = true; + } else { + environment = environ; } // Signal handlers are set at this point in 64-bit zygote since system_server is forked from it @@ -3104,10 +3150,8 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, ALOGE("fork failed: %s", strerror(errno)); } close(cmd_fd); - if (disable_hardened_malloc) { - if (unsetenv("DISABLE_HARDENED_MALLOC") != 0) { - env->FatalError(CREATE_ERROR("unsetenv(DISABLE_HARDENED_MALLOC) failed: %s", strerror(errno)).c_str()); - } + if (is_environment_cloned) { + free_environ(environment); } return pid; } else { @@ -3125,17 +3169,17 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, #if defined(__aarch64__) const int FLAG_COMPAT_VA_39_BIT = 1 << 30; - execveat(-1, argv[0], (char **) argv, environ, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0); + execveat(-1, argv[0], (char **) argv, environment, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0); async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execveat failed: %#m"); if (errno == EINVAL) { // kernel doesn't support FLAG_COMPAT_VA_39_BIT, or a different error that will - // be returned by execv() anyway - execv(argv[0], (char **) argv); - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %#m"); + // be returned by execve() anyway + execve(argv[0], (char **) argv, environment); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execve failed: %#m"); } #else - execv(argv[0], (char **) argv); - async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execv failed: %#m"); + execve(argv[0], (char **) argv, environment); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execve failed: %#m"); #endif // defined(__aarch64__) // exec failed From 486c24cdc6a8f5c5e4c28d1de07bf3bc4617b675 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 13:47:28 +0000 Subject: [PATCH 34/35] fixup! add exec spawning implementation --- core/jni/com_android_internal_os_Zygote.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index b3c518c642d8a..8d79189d8310e 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3106,7 +3106,9 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, } if (lseek(cmd_fd, 0, SEEK_SET) != 0) { - env->FatalError(CREATE_ERROR("unable to lseek cmd_fd: %s", strerror(errno)).c_str()); + ALOGE("lseek(cmd_fd) failed: %s", strerror(errno)); + close(cmd_fd); + return -1; } } From 3063d1a286584b99984b55d1f52900230c122138 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Sat, 13 Jun 2026 14:57:52 +0000 Subject: [PATCH 35/35] fixup! add exec spawning implementation Ensure that no new file descriptors are racily opened by signal handlers in the child process. --- core/jni/com_android_internal_os_Zygote.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 8d79189d8310e..d66e76e5643e5 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -3143,6 +3143,18 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, // first, but 32-bit zygote won't have them set if this is the first app launch zygote command. SetSignalHandlers(); + sigset64_t full_sig_set; + sigfillset64(&full_sig_set); + + sigset64_t prev_sig_set; + + // ensure that no new file descriptors are racily opened by signal handlers in the child process + if (int err = pthread_sigmask64(SIG_BLOCK, &full_sig_set, &prev_sig_set); err != 0) { + ALOGE("pthread_sigmask64 failed before fork: %s", strerror(err)); + close(cmd_fd); + return -1; + } + // fork() runs bionic fork hooks which are unnecessary for this use-case pid_t pid = _Fork(); @@ -3151,6 +3163,10 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, if (pid == -1) { ALOGE("fork failed: %s", strerror(errno)); } + if (int err = pthread_sigmask64(SIG_SETMASK, &prev_sig_set, nullptr); err != 0) { + ALOGE("pthread_sigmask64 failed in parent after fork: %s", strerror(err)); + _exit(1); + } close(cmd_fd); if (is_environment_cloned) { free_environ(environment); @@ -3169,6 +3185,11 @@ static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, _exit(1); } + if (int err = pthread_sigmask64(SIG_SETMASK, &prev_sig_set, nullptr); err != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "pthread_sigmask64 failed in child after fork: %s", strerrorname_np(err)); + _exit(1); + } + #if defined(__aarch64__) const int FLAG_COMPAT_VA_39_BIT = 1 << 30; execveat(-1, argv[0], (char **) argv, environment, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0);