diff --git a/cmds/app_process/app_main.cpp b/cmds/app_process/app_main.cpp index b3c0b45048ad3..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() @@ -337,12 +335,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/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index a10c553707d8d..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) { @@ -7855,6 +7856,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 +8052,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..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; @@ -23,7 +21,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 +30,9 @@ static Bundle onBind(Context appContext, ActivityThread.AppBindData appBindData) } called = true; - AppGlobals.setInitialPackageId(appContext.getApplicationInfo().ext().getPackageId()); + 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/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index 21abe5f9398d0..79c05d16711b3 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 = { @@ -64,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 {} 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/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(); } 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; 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; 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/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; diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index f7eb7bdebf021..84a015ba87233 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,13 +213,12 @@ 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 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/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..7cc448f642be2 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -33,6 +33,8 @@ 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 com.android.internal.os.ZygoteType; import java.io.BufferedWriter; import java.io.DataInputStream; @@ -71,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; @@ -86,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, - LocalSocketAddress.Namespace.RESERVED); - mZygoteSecondarySocketAddress = - new LocalSocketAddress(Zygote.SECONDARY_SOCKET_NAME, - LocalSocketAddress.Namespace.RESERVED); - - mUsapPoolSocketAddress = - new LocalSocketAddress(Zygote.USAP_POOL_PRIMARY_SOCKET_NAME, + for (var type : ZygoteType.values()) { + int idx = type.ordinal(); + mZygoteSocketAddresses[idx] = new LocalSocketAddress(type.getSocketName(), 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()]; } /** @@ -265,14 +246,9 @@ boolean isClosed() { private int mHiddenApiAccessStatslogSampleRate; /** - * The state of the connection to the primary zygote. - */ - private ZygoteState primaryZygoteState; - - /** - * The state of the connection to the secondary zygote. + * Zygote connection states. */ - private ZygoteState secondaryZygoteState; + private ZygoteState[] mZygoteStates = new ZygoteState[NUM_ZYGOTE_TYPES]; /** * If this Zygote supports the creation and maintenance of a USAP pool. @@ -339,7 +315,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 +338,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"); @@ -557,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", @@ -629,7 +607,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 +632,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 +649,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 +782,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) { @@ -816,7 +791,7 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC 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); } @@ -861,11 +836,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(); + } } } @@ -874,10 +848,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); @@ -887,10 +861,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"); @@ -919,17 +893,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(); @@ -951,14 +925,31 @@ 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()) { + 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; + } } return ok; } } + private static boolean isLazilyStarted(ZygoteType type) { + switch (type) { + case ZygoteType.Compat: + return true; + default: + return false; + } + } + /** * Set the precentage of detected hidden API accesses that are logged to the event log. * @@ -969,8 +960,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()]); + } } } @@ -984,8 +976,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()]); + } } } @@ -1071,33 +1064,82 @@ 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()) { + 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(); + 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; + } + + @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()) { + // 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 % 50) == 0) { + Log.d(LOG_TAG, "waiting for compat zygote to start"); + } + SystemClock.sleep(RETRY_DELAY_MS); + } + } + } + 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. */ @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); } /** @@ -1107,17 +1149,23 @@ 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)) { + 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; } - 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; @@ -1145,11 +1193,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 { @@ -1188,7 +1236,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(); @@ -1202,11 +1252,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); } @@ -1224,7 +1274,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 { @@ -1243,48 +1293,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); - } } } @@ -1310,7 +1347,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 +1356,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 +1370,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 +1378,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/os/ZygoteSelectionMode.java b/core/java/android/os/ZygoteSelectionMode.java new file mode 100644 index 0000000000000..614d731c3bbdb --- /dev/null +++ b/core/java/android/os/ZygoteSelectionMode.java @@ -0,0 +1,9 @@ +package android.os; + +/** @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/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..603e52fa8658b 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -22,11 +22,13 @@ import android.os.Process; import android.os.UserHandle; import android.os.ZygoteProcess; +import android.os.ZygoteSelectionMode; import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; +import com.android.internal.os.ZygoteExtraArgs; /** @hide */ public class WebViewZygote { @@ -108,7 +110,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,10 +122,9 @@ 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); + 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/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java deleted file mode 100644 index 8c6afee60ad62..0000000000000 --- a/core/java/com/android/internal/os/ExecInit.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.android.internal.os; - -import android.os.Process; -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; - -/** - * Startup class for the process. - * @hide - */ -public class ExecInit { - /** - * Class not instantiable. - */ - 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. - * - * 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); - - 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; - } - - /** - * 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 { - if ((runtimeFlags & Zygote.DISABLE_HARDENED_MALLOC) != 0) { - // checked by bionic during early init - 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; - - 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); - } - } 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(); - if (Process.isIsolated()) { - System.gc(); - } - return RuntimeInit.applicationInit(targetSdkVersion, /*disabledCompatChanges*/ null, argv, classLoader); - } -} 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..26434bc695fb7 --- /dev/null +++ b/core/java/com/android/internal/os/ExecSpawning.java @@ -0,0 +1,164 @@ +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; +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"); + } + + 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); + } + } + } +} 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 c7565e793532e..a760ef7bf0c38 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 { @@ -209,15 +211,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. */ @@ -308,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) { @@ -379,21 +352,31 @@ 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( + 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, 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"); @@ -411,12 +394,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 @@ -449,17 +434,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"); @@ -480,12 +464,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. @@ -593,14 +577,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. @@ -735,7 +717,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. @@ -893,14 +875,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 @@ -1188,7 +1169,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); @@ -1384,6 +1364,7 @@ private static int getRequestedMemtagLevel( } private static int decideTaggingLevel( + @NonNull AtomicBoolean shouldForciblyEnableTagging, @NonNull ApplicationInfo info, @Nullable ProcessInfo processInfo, @Nullable IPlatformCompat platformCompat) { @@ -1401,8 +1382,8 @@ 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 + 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). // @@ -1501,6 +1482,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, @@ -1517,7 +1499,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; @@ -1534,13 +1516,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 @@ -1555,15 +1536,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); - - public static native int execveatWrapper(int dirFd, String filename, String[] argv, int flags); } 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..dd8adca9bc952 100644 --- a/core/java/com/android/internal/os/ZygoteExtraArgs.java +++ b/core/java/com/android/internal/os/ZygoteExtraArgs.java @@ -1,60 +1,171 @@ 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.ZygoteSelectionMode; -// 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 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); + public ZygoteSelectionMode getZygoteSelectionMode() { + return hasFlag(Flag.PREFER_COMPAT_ZYGOTE) ? + ZygoteSelectionMode.PreferCompatZygote : + ZygoteSelectionMode.Regular; } - 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 hasFlag(@Flag.Enum int flag) { + return (this.flags & flag) == flag; } - static ZygoteExtraArgs parse(String flat) { - String[] arr = flat.split(SEPARATOR); - long selinuxFlags = Long.parseLong(arr[IDX_SELINUX_FLAGS], 16); - return new ZygoteExtraArgs(selinuxFlags); + void setFlag(int flag, boolean value) { + if (value) { + this.flags |= flag; + } else { + this.flags &= ~flag; + } + } + + public boolean shouldUseExecSpawning() { + return !hasFlag(Flag.USE_ZYGOTE_SPAWNING); + } + + 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 2629255d4e48d..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,34 +829,36 @@ 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; + 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"; @@ -877,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); @@ -905,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 @@ -918,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); @@ -935,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) { @@ -967,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..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(); @@ -392,6 +381,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 +511,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/java/com/android/internal/os/ZygoteType.java b/core/java/com/android/internal/os/ZygoteType.java new file mode 100644 index 0000000000000..823c151411936 --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteType.java @@ -0,0 +1,35 @@ +package com.android.internal.os; + +/** + * @hide + */ +public enum ZygoteType { + Primary("zygote", "usap_pool_primary"), + Compat("zygote_compat", "usap_pool_compat"), + 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); + } +} 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..d66e76e5643e5 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 @@ -44,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +65,7 @@ #include #include #include +#include #include #include #include @@ -87,8 +90,6 @@ #include "nativebridge/native_bridge.h" -#include "ExecStrings.h" - #if defined(__BIONIC__) #include extern "C" void android_reset_stack_guards(); @@ -170,6 +171,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; @@ -356,16 +359,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; } }; @@ -910,6 +923,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))); @@ -1817,99 +1833,8 @@ 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, +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, @@ -1917,8 +1842,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); @@ -2081,9 +2005,94 @@ 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"); + } + } + + 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); + + 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; + + // 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; - HandleRuntimeFlags(env, runtime_flags, process_name, nice_name_ptr); + 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); @@ -2444,7 +2453,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 @@ -2455,7 +2464,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) { @@ -2479,7 +2488,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. @@ -2523,6 +2532,8 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_DEFAULT); } + gIsExecSpawning = false; + return pid; } @@ -2532,12 +2543,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); @@ -2578,12 +2589,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; } @@ -2617,11 +2628,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); @@ -2764,22 +2775,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); } /** @@ -2791,28 +2802,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 @@ -3025,6 +3046,170 @@ 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, + jboolean disable_hardened_malloc, + jboolean enable_compat_va_39_bit) { + 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) { + ALOGE("lseek(cmd_fd) failed: %s", strerror(errno)); + close(cmd_fd); + return -1; + } + } + + 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, + }; + + bool is_environment_cloned = false; + char** environment; + if (disable_hardened_malloc) { + // 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 + // 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(); + + if (pid != 0) { + // parent process + 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); + } + return pid; + } else { + // 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); + } + 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: %#m", cmd_fd + 1); + _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); + 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 execve() anyway + execve(argv[0], (char **) argv, environment); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execve failed: %#m"); + } +#else + execve(argv[0], (char **) argv, environment); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execve failed: %#m"); +#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) { @@ -3054,26 +3239,12 @@ 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 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[] = { + {"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}, @@ -3088,10 +3259,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}, @@ -3122,8 +3293,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}, - {"execveatWrapper", "(ILjava/lang/String;[Ljava/lang/String;I)I", (void*)execveatWrapper}, }; 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..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"; @@ -177,9 +179,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; @@ -190,22 +189,16 @@ 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; 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); diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index dbf40b69351a2..30beed10bd218 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", diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 989a0ce31a20a..abdd276a3c384 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,25 @@ 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); + // 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, userId, definingAppInfo, GosPackageState.get(definingPkgName, userId))) { + // app zygotes are pointless when exec spawning is used + hostingRecord = HostingRecord.byAppZygote_(r.instanceName, definingPkgName, + r.definingUid, r.serviceInfo.processName); + } } } } 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/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/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; } 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() 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; } 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..bd27fed10fdef --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnActivity.kt @@ -0,0 +1,666 @@ +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 +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() }, + onOpenAppInfo = { openAppInfo() }, + ) + } + } + } + } + + private fun openAppInfo() { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null)) + startActivity(intent) + } + + 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, + onOpenAppInfo: () -> 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 setting, then run the check again.", + style = MaterialTheme.typography.bodyMedium, + ) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + onClick = onOpenAppInfo, + modifier = Modifier.testTag(SecureSpawnUiTags.OPEN_SECURITY_SETTINGS_BUTTON), + ) { + Text("Open app settings") + } + } + + 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(), + ) + } + } + } + } +} + +@Composable +private fun ProcessStateCard(processState: SecureSpawnCheck.ProcessState) { + 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()) + 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 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..f545bd8390546 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/GosCompatSecureSpawnApp/src/app/grapheneos/goscompat/securespawn/SecureSpawnCheck.java @@ -0,0 +1,88 @@ +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 { + 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(System.getenv("IS_EXEC_SPAWNED_APP_PROCESS") != null); + } + + 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(boolean enabled) {} + + 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..9e9efb636ff5b --- /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 notExecSpawnedCompatZygote() { + SecureSpawnCheck.ProcessState result = SecureSpawnCheck.processState(); + assertProcessState(result); + assertWithMessage(failureMessage("expected execSpawned == false", result)) + .that(result.execSpawned()).isFalse(); + 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..40909b3bdcf61 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/README.md @@ -0,0 +1,16 @@ +# 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 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: + +```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..ec6b95a73ecc3 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnDisabledHostTest.java @@ -0,0 +1,55 @@ +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 { + + @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 + public void secureAppSpawningDisabledUsesZygoteInit() throws Exception { + resetPackageState(); + runDeviceTest("notExecSpawned"); + } + + @Test + public void exploitCompatibilityModeUsesCompatZygoteInit() throws Exception { + resetPackageState(); + editPackageState( + new int[] { GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE }, + new int[0]); + runDeviceTest("notExecSpawnedCompatZygote"); + } + + @Test + public void exploitCompatibilityModePassesMemoryAccountingCheck() throws Exception { + resetPackageState(); + editPackageState( + new int[] { GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE }, + new int[0]); + 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 new file mode 100644 index 0000000000000..850e417bd3ea3 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnEnabledHostTest.java @@ -0,0 +1,37 @@ +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 SecureSpawnEnabledHostTest extends SecureSpawnHostTestBase { + + @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 + 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..e572283ba9192 --- /dev/null +++ b/tests/GosCompatTests/GosCompatSecureSpawnTests/src/app/grapheneos/goscompat/securespawn/tests/SecureSpawnHostTestBase.java @@ -0,0 +1,327 @@ +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 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; + protected 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 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 abstract void resetPackageState() throws Exception; + + // 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()); + } + + 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 { + 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 = this instanceof SecureSpawnEnabledHostTest ? "enabled" : "disabled"; + String logName = resultLogName(methodName); + String report = "package=" + 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 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; + } +} 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..7bd39f8c4bc41 100644 --- a/tests/GosCompatTests/TEST_MAPPING +++ b/tests/GosCompatTests/TEST_MAPPING @@ -11,6 +11,9 @@ }, { "name": "GosCompatWifiScanTimeoutTests" + }, + { + "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() { } }