From 541ec04220725945ab166053c89e52314bf0416e Mon Sep 17 00:00:00 2001 From: inthewaves Date: Mon, 1 Jun 2026 00:26:40 -0700 Subject: [PATCH 1/2] apply exec-spawn policy before app startup Exec-spawned app processes do not run the normal zygote post-fork hook that applies hidden API and test API policy from runtime flags. This change calls ZygoteHooks.postExecSpawn(int) from ExecInit after normalizing exec-spawned runtime mappings and before resolving the app entrypoint, so policy is applied before app code is loaded. Requires the corresponding changes to art and libcore Test: atest GosSecureSpawnTests:SecureSpawnEnabledHostTest#{acyclicReflectiveDumpCheck,hiddenApiEnforcementCheck} --- core/java/com/android/internal/os/ExecInit.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java index 8c6afee60ad62..f83746ffab7f1 100644 --- a/core/java/com/android/internal/os/ExecInit.java +++ b/core/java/com/android/internal/os/ExecInit.java @@ -8,6 +8,7 @@ import android.util.Slog; import android.util.TimingsTraceLog; import dalvik.system.VMRuntime; +import dalvik.system.ZygoteHooks; /** * Startup class for the process. @@ -41,6 +42,9 @@ public static void main(String[] args) { // Mimic system Zygote preloading. ZygoteInit.preload(new TimingsTraceLog("ExecInitTiming", Trace.TRACE_TAG_DALVIK), false); + // Match the hidden API policy setup normally performed by + // ZygoteHooks.postForkChild() before loading the app entrypoint. + ZygoteHooks.postExecSpawn(runtimeFlags & ~Zygote.CUSTOM_RUNTIME_FLAGS); // Launch the application. String[] runtimeArgs = new String[args.length - 2]; From df0d30e39819bff9a3eef9acf5cc616b5b2f87be Mon Sep 17 00:00:00 2001 From: inthewaves Date: Mon, 1 Jun 2026 00:49:04 -0700 Subject: [PATCH 2/2] pass disabled compat changes through exec spawning Dropping the disabled compat change list before RuntimeInit.applicationInit. made ART treat ALLOW_TEST_API_ACCESS as enabled even when framework compat had disabled it for the package. Carry parsed disabled compat changes through ExecInit so exec spawned apps get the same compat state as the zygote fork path. Test: atest GosCompatSecureSpawnTests:SecureSpawnEnabledHostTest#testApiCompatDefaultCheck Test: atest CtsHiddenApiBlocklistCurrentApiTestCases CtsHiddenApiBlocklistTestApiTestCases (both with exec spawning on, then re-running both tests after turning off exec spawning + reboot) --- .../com/android/internal/os/ExecInit.java | 51 ++++++++++++++++--- .../android/internal/os/ZygoteConnection.java | 3 +- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java index f83746ffab7f1..2078bfefc8a4e 100644 --- a/core/java/com/android/internal/os/ExecInit.java +++ b/core/java/com/android/internal/os/ExecInit.java @@ -28,17 +28,40 @@ private ExecInit() { * * The first argument is the target SDK version for the app. * + * The second argument is the runtime flags. + * + * The third argument is the number of disabled compat changes, followed by + * the disabled compat change IDs. + * * The remaining arguments are passed to the runtime. * * @param args The command-line arguments. */ public static void main(String[] args) { + if (args.length < 3) { + throw new IllegalArgumentException("Missing mandatory arguments"); + } + // Parse our mandatory argument. int targetSdkVersion = Integer.parseInt(args[0], 10); // Parse the runtime_flags. int runtimeFlags = Integer.parseInt(args[1], 10); + int disabledCompatChangesCount = Integer.parseInt(args[2], 10); + if (disabledCompatChangesCount < 0 || disabledCompatChangesCount > args.length - 3) { + throw new IllegalArgumentException("Invalid disabled compat changes count: " + + disabledCompatChangesCount); + } + + long[] disabledCompatChanges = null; + if (disabledCompatChangesCount != 0) { + disabledCompatChanges = new long[disabledCompatChangesCount]; + for (int i = 0; i < disabledCompatChangesCount; ++i) { + disabledCompatChanges[i] = Long.parseLong(args[3 + i], 10); + } + } + // Mimic system Zygote preloading. ZygoteInit.preload(new TimingsTraceLog("ExecInitTiming", Trace.TRACE_TAG_DALVIK), false); @@ -47,9 +70,10 @@ public static void main(String[] args) { ZygoteHooks.postExecSpawn(runtimeFlags & ~Zygote.CUSTOM_RUNTIME_FLAGS); // Launch the application. - String[] runtimeArgs = new String[args.length - 2]; - System.arraycopy(args, 2, runtimeArgs, 0, runtimeArgs.length); - Runnable r = execInit(targetSdkVersion, runtimeArgs); + int runtimeArgsStart = 3 + disabledCompatChangesCount; + String[] runtimeArgs = new String[args.length - runtimeArgsStart]; + System.arraycopy(args, runtimeArgsStart, runtimeArgs, 0, runtimeArgs.length); + Runnable r = execInit(targetSdkVersion, disabledCompatChanges, runtimeArgs); Zygote.nativeHandleRuntimeFlags(runtimeFlags); @@ -73,12 +97,18 @@ static Runnable getPendingExecInit() { * * @param niceName The nice name for the application, or null if none. * @param targetSdkVersion The target SDK version for the app. + * @param instructionSet The instruction set to use. + * @param runtimeFlags The runtime flags for the app. + * @param disabledCompatChanges nullable list of disabled compat changes for the process being + * started. * @param args Arguments for {@link RuntimeInit#main}. */ public static void execApplication(String niceName, int targetSdkVersion, - String instructionSet, int runtimeFlags, String[] args) { + String instructionSet, int runtimeFlags, long[] disabledCompatChanges, String[] args) { int niceArgs = niceName == null ? 0 : 1; - int baseArgs = 6 + niceArgs; + int disabledCompatChangesCount = + disabledCompatChanges != null ? disabledCompatChanges.length : 0; + int baseArgs = 7 + niceArgs + disabledCompatChangesCount; String[] argv = new String[baseArgs + args.length]; if (VMRuntime.is64BitInstructionSet(instructionSet)) { argv[0] = "/system/bin/app_process64"; @@ -93,6 +123,10 @@ public static void execApplication(String niceName, int targetSdkVersion, argv[3 + niceArgs] = "com.android.internal.os.ExecInit"; argv[4 + niceArgs] = Integer.toString(targetSdkVersion); argv[5 + niceArgs] = Integer.toString(runtimeFlags); + argv[6 + niceArgs] = Integer.toString(disabledCompatChangesCount); + for (int i = 0; i < disabledCompatChangesCount; ++i) { + argv[7 + niceArgs + i] = Long.toString(disabledCompatChanges[i]); + } System.arraycopy(args, 0, argv, baseArgs, args.length); WrapperInit.preserveCapabilities(); @@ -134,9 +168,11 @@ public static void execApplication(String niceName, int targetSdkVersion, * So we don't need to call commonInit() here. * * @param targetSdkVersion target SDK version + * @param disabledCompatChanges set of disabled compat changes for the process * @param argv arg strings */ - private static Runnable execInit(int targetSdkVersion, String[] argv) { + private static Runnable execInit(int targetSdkVersion, long[] disabledCompatChanges, + String[] argv) { if (RuntimeInit.DEBUG) { Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from exec"); } @@ -163,6 +199,7 @@ private static Runnable execInit(int targetSdkVersion, String[] argv) { if (Process.isIsolated()) { System.gc(); } - return RuntimeInit.applicationInit(targetSdkVersion, /*disabledCompatChanges*/ null, argv, classLoader); + return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv, + classLoader); } } diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 5d737de48c2c5..a3e77cceb998d 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -539,7 +539,8 @@ private Runnable handleChildProc(ZygoteArguments parsedArgs, if (useExecInit) { ExecInit.execApplication(parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion, - VMRuntime.getCurrentInstructionSet(), runtimeFlags, parsedArgs.mRemainingArgs); + VMRuntime.getCurrentInstructionSet(), runtimeFlags, + parsedArgs.mDisabledCompatChanges, parsedArgs.mRemainingArgs); // Should not get here. throw new IllegalStateException("ExecInit.execApplication unexpectedly returned");