/* * Copyright (C) 2007 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. */ package com.android.internal.os; import static android.system.OsConstants.F_SETFD; import static android.system.OsConstants.O_CLOEXEC; import static android.system.OsConstants.POLLIN; import static com.android.internal.os.ZygoteConnectionConstants.CONNECTION_TIMEOUT_MILLIS; import static com.android.internal.os.ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; import android.net.Credentials; import android.net.LocalSocket; import android.os.Parcel; import android.os.Process; import android.os.Trace; import android.system.ErrnoException; import android.system.Os; import android.system.StructPollfd; import android.util.Log; import dalvik.system.VMRuntime; import dalvik.system.ZygoteHooks; import libcore.io.IoUtils; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileDescriptor; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.concurrent.TimeUnit; /** * A connection that can make spawn requests. */ class ZygoteConnection { private static final String TAG = "Zygote"; /** * The command socket. * * mSocket is retained in the child process in "peer wait" mode, so * that it closes when the child process terminates. In other cases, * it is closed in the peer. */ @UnsupportedAppUsage private final LocalSocket mSocket; @UnsupportedAppUsage private final DataOutputStream mSocketOutStream; @UnsupportedAppUsage private final Credentials peer; private final String abiList; private boolean isEof; /** * Constructs instance from connected socket. * * @param socket non-null; connected socket * @param abiList non-null; a list of ABIs this zygote supports. * @throws IOException If obtaining the peer credentials fails */ ZygoteConnection(LocalSocket socket, String abiList) throws IOException { mSocket = socket; this.abiList = abiList; mSocketOutStream = new DataOutputStream(socket.getOutputStream()); mSocket.setSoTimeout(CONNECTION_TIMEOUT_MILLIS); try { peer = mSocket.getPeerCredentials(); } catch (IOException ex) { Log.e(TAG, "Cannot read peer credentials", ex); throw ex; } if (peer.getUid() != Process.SYSTEM_UID) { throw new ZygoteSecurityException("Only system UID is allowed to connect to Zygote."); } isEof = false; } /** * Returns the file descriptor of the associated socket. * * @return null-ok; file descriptor */ FileDescriptor getFileDescriptor() { return mSocket.getFileDescriptor(); } /** * Reads a command from the command socket. If a child is successfully forked, a * {@code Runnable} that calls the childs main method (or equivalent) is returned in the child * process. {@code null} is always returned in the parent process (the zygote). * If multipleOK is set, we may keep processing additional fork commands before returning. * * 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) { ZygoteArguments parsedArgs; try (ZygoteCommandBuffer argBuffer = new ZygoteCommandBuffer(mSocket)) { while (true) { try { parsedArgs = 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) { isEof = true; return null; } int pid; FileDescriptor childPipeFd = null; FileDescriptor serverPipeFd = null; if (parsedArgs.mBootCompleted) { handleBootCompleted(); return null; } if (parsedArgs.mAbiListQuery) { handleAbiListQuery(); return null; } if (parsedArgs.mPidQuery) { handlePidQuery(); return null; } if (parsedArgs.mUsapPoolStatusSpecified || parsedArgs.mApiDenylistExemptions != null || parsedArgs.mHiddenApiAccessLogSampleRate != -1 || parsedArgs.mHiddenApiAccessStatslogSampleRate != -1) { // Handle these once we've released argBuffer, to avoid opening a second one. break; } if (parsedArgs.mPreloadDefault) { handlePreload(); return null; } if (parsedArgs.mPreloadPackage != null) { handlePreloadPackage(parsedArgs.mPreloadPackage, parsedArgs.mPreloadPackageLibs, parsedArgs.mPreloadPackageLibFileName, parsedArgs.mPreloadPackageCacheKey); return null; } if (canPreloadApp() && parsedArgs.mPreloadApp != null) { byte[] rawParcelData = Base64.getDecoder().decode(parsedArgs.mPreloadApp); Parcel appInfoParcel = Parcel.obtain(); appInfoParcel.unmarshall(rawParcelData, 0, rawParcelData.length); appInfoParcel.setDataPosition(0); ApplicationInfo appInfo = ApplicationInfo.CREATOR.createFromParcel(appInfoParcel); appInfoParcel.recycle(); if (appInfo != null) { handlePreloadApp(appInfo); } else { throw new IllegalArgumentException("Failed to deserialize --preload-app"); } return null; } if (parsedArgs.mPermittedCapabilities != 0 || parsedArgs.mEffectiveCapabilities != 0) { throw new ZygoteSecurityException("Client may not specify capabilities: " + "permitted=0x" + Long.toHexString(parsedArgs.mPermittedCapabilities) + ", effective=0x" + Long.toHexString(parsedArgs.mEffectiveCapabilities)); } Zygote.applyUidSecurityPolicy(parsedArgs, peer); Zygote.applyInvokeWithSecurityPolicy(parsedArgs, peer); Zygote.applyDebuggerSystemProperty(parsedArgs); Zygote.applyInvokeWithSystemProperty(parsedArgs); int[][] rlimits = null; if (parsedArgs.mRLimits != null) { rlimits = parsedArgs.mRLimits.toArray(Zygote.INT_ARRAY_2D); } int[] fdsToIgnore = null; if (parsedArgs.mInvokeWith != null) { try { FileDescriptor[] pipeFds = Os.pipe2(O_CLOEXEC); childPipeFd = pipeFds[1]; serverPipeFd = pipeFds[0]; Os.fcntlInt(childPipeFd, F_SETFD, 0); fdsToIgnore = new int[]{childPipeFd.getInt$(), serverPipeFd.getInt$()}; } catch (ErrnoException errnoEx) { throw new IllegalStateException("Unable to set up pipe for invoke-with", errnoEx); } } /* * In order to avoid leaking descriptors to the Zygote child, * the native code must close the two Zygote socket descriptors * in the child process before it switches from Zygote-root to * the UID and privileges of the application being launched. * * In order to avoid "bad file descriptor" errors when the * two LocalSocket objects are closed, the Posix file * descriptors are released via a dup2() call which closes * the socket and substitutes an open descriptor to /dev/null. */ int [] fdsToClose = { -1, -1 }; FileDescriptor fd = mSocket.getFileDescriptor(); if (fd != null) { fdsToClose[0] = fd.getInt$(); } FileDescriptor zygoteFd = zygoteServer.getZygoteSocketFileDescriptor(); if (zygoteFd != null) { fdsToClose[1] = zygoteFd.getInt$(); } if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote || !multipleOK || peer.getUid() != Process.SYSTEM_UID) { // 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); try { if (pid == 0) { // in child zygoteServer.setForkChild(); zygoteServer.closeServerSocket(); IoUtils.closeQuietly(serverPipeFd); serverPipeFd = null; return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote); } else { // In the parent. A pid < 0 indicates a failure and will be handled in // handleParentProc. IoUtils.closeQuietly(childPipeFd); childPipeFd = null; handleParentProc(pid, serverPipeFd); return null; } } finally { IoUtils.closeQuietly(childPipeFd); IoUtils.closeQuietly(serverPipeFd); } } else { ZygoteHooks.preFork(); Runnable result = Zygote.forkSimpleApps(argBuffer, zygoteServer.getZygoteSocketFileDescriptor(), peer.getUid(), Zygote.minChildUid(peer), parsedArgs.mNiceName); if (result == null) { // parent; we finished some number of forks. Result is Boolean. // We already did the equivalent of handleParentProc(). ZygoteHooks.postForkCommon(); // argBuffer contains a command not understood by forksimpleApps. continue; } else { // child; result is a Runnable. zygoteServer.setForkChild(); return result; } } } } // Handle anything that may need a ZygoteCommandBuffer after we've released ours. if (parsedArgs.mUsapPoolStatusSpecified) { return handleUsapPoolStatusChange(zygoteServer, parsedArgs.mUsapPoolEnabled); } if (parsedArgs.mApiDenylistExemptions != null) { return handleApiDenylistExemptions(zygoteServer, parsedArgs.mApiDenylistExemptions); } if (parsedArgs.mHiddenApiAccessLogSampleRate != -1 || parsedArgs.mHiddenApiAccessStatslogSampleRate != -1) { return handleHiddenApiAccessLogSampleRate(zygoteServer, parsedArgs.mHiddenApiAccessLogSampleRate, parsedArgs.mHiddenApiAccessStatslogSampleRate); } throw new AssertionError("Shouldn't get here"); } private void handleAbiListQuery() { try { final byte[] abiListBytes = abiList.getBytes(StandardCharsets.US_ASCII); mSocketOutStream.writeInt(abiListBytes.length); mSocketOutStream.write(abiListBytes); } catch (IOException ioe) { throw new IllegalStateException("Error writing to command socket", ioe); } } private void handlePidQuery() { try { String pidString = String.valueOf(Process.myPid()); final byte[] pidStringBytes = pidString.getBytes(StandardCharsets.US_ASCII); mSocketOutStream.writeInt(pidStringBytes.length); mSocketOutStream.write(pidStringBytes); } catch (IOException ioe) { throw new IllegalStateException("Error writing to command socket", ioe); } } private void handleBootCompleted() { try { mSocketOutStream.writeInt(0); } catch (IOException ioe) { throw new IllegalStateException("Error writing to command socket", ioe); } VMRuntime.bootCompleted(); } /** * Preloads resources if the zygote is in lazily preload mode. Writes the result of the * preload operation; {@code 0} when a preload was initiated due to this request and {@code 1} * if no preload was initiated. The latter implies that the zygote is not configured to load * resources lazy or that the zygote has already handled a previous request to handlePreload. */ private void handlePreload() { try { if (isPreloadComplete()) { mSocketOutStream.writeInt(1); } else { preload(); mSocketOutStream.writeInt(0); } } catch (IOException ioe) { throw new IllegalStateException("Error writing to command socket", ioe); } } private Runnable stateChangeWithUsapPoolReset(ZygoteServer zygoteServer, Runnable stateChangeCode) { try { if (zygoteServer.isUsapPoolEnabled()) { Log.i(TAG, "Emptying USAP Pool due to state change."); Zygote.emptyUsapPool(); } stateChangeCode.run(); if (zygoteServer.isUsapPoolEnabled()) { Runnable fpResult = zygoteServer.fillUsapPool( new int[]{mSocket.getFileDescriptor().getInt$()}, false); if (fpResult != null) { zygoteServer.setForkChild(); return fpResult; } else { Log.i(TAG, "Finished refilling USAP Pool after state change."); } } mSocketOutStream.writeInt(0); return null; } catch (IOException ioe) { throw new IllegalStateException("Error writing to command socket", ioe); } } /** * Makes the necessary changes to implement a new API deny list exemption policy, and then * responds to the system server, letting it know that the task has been completed. * * This necessitates a change to the internal state of the Zygote. As such, if the USAP * pool is enabled all existing USAPs have an incorrect API deny list exemption list. To * properly handle this request the pool must be emptied and refilled. This process can return * a Runnable object that must be returned to ZygoteServer.runSelectLoop to be invoked. * * @param zygoteServer The server object that received the request * @param exemptions The new exemption list. * @return A Runnable object representing a new app in any USAPs spawned from here; the * zygote process will always receive a null value from this function. */ private Runnable handleApiDenylistExemptions(ZygoteServer zygoteServer, String[] exemptions) { return stateChangeWithUsapPoolReset(zygoteServer, () -> ZygoteInit.setApiDenylistExemptions(exemptions)); } private Runnable handleUsapPoolStatusChange(ZygoteServer zygoteServer, boolean newStatus) { try { Runnable fpResult = zygoteServer.setUsapPoolStatus(newStatus, mSocket); if (fpResult == null) { mSocketOutStream.writeInt(0); } else { zygoteServer.setForkChild(); } return fpResult; } catch (IOException ioe) { throw new IllegalStateException("Error writing to command socket", ioe); } } /** * Changes the API access log sample rate for the Zygote and processes spawned from it. * * This necessitates a change to the internal state of the Zygote. As such, if the USAP * pool is enabled all existing USAPs have an incorrect API access log sample rate. To * properly handle this request the pool must be emptied and refilled. This process can return * a Runnable object that must be returned to ZygoteServer.runSelectLoop to be invoked. * * @param zygoteServer The server object that received the request * @param samplingRate The new sample rate for regular logging * @param statsdSamplingRate The new sample rate for statslog logging * @return A Runnable object representing a new app in any blastulas spawned from here; the * zygote process will always receive a null value from this function. */ private Runnable handleHiddenApiAccessLogSampleRate(ZygoteServer zygoteServer, int samplingRate, int statsdSamplingRate) { return stateChangeWithUsapPoolReset(zygoteServer, () -> { int maxSamplingRate = Math.max(samplingRate, statsdSamplingRate); ZygoteInit.setHiddenApiAccessLogSampleRate(maxSamplingRate); StatsdHiddenApiUsageLogger.setHiddenApiAccessLogSampleRates( samplingRate, statsdSamplingRate); ZygoteInit.setHiddenApiUsageLogger(StatsdHiddenApiUsageLogger.getInstance()); }); } protected void preload() { ZygoteInit.lazyPreload(); } protected boolean isPreloadComplete() { return ZygoteInit.isPreloadComplete(); } protected DataOutputStream getSocketOutputStream() { return mSocketOutStream; } protected void handlePreloadPackage(String packagePath, String libsPath, String libFileName, String cacheKey) { throw new RuntimeException("Zygote does not support package preloading"); } protected boolean canPreloadApp() { return false; } protected void handlePreloadApp(ApplicationInfo aInfo) { throw new RuntimeException("Zygote does not support app preloading"); } /** * Closes socket associated with this connection. */ @UnsupportedAppUsage void closeSocket() { try { mSocket.close(); } catch (IOException ex) { Log.e(TAG, "Exception while closing command " + "socket in parent", ex); } } boolean isClosedByPeer() { return isEof; } /** * Handles post-fork setup of child proc, closing sockets as appropriate, * reopen stdio as appropriate, and ultimately throwing MethodAndArgsCaller * if successful or returning if failed. * * @param parsedArgs non-null; zygote args * @param pipeFd null-ok; pipe for communication back to Zygote. * @param isZygote whether this new child process is itself a new Zygote. */ private Runnable handleChildProc(ZygoteArguments parsedArgs, FileDescriptor pipeFd, boolean isZygote) { /* * By the time we get here, the native code has closed the two actual Zygote * socket connections, and substituted /dev/null in their place. The LocalSocket * objects still need to be closed properly. */ closeSocket(); Zygote.setAppProcessName(parsedArgs, TAG); // End of the postFork event. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); if (parsedArgs.mInvokeWith != null) { WrapperInit.execApplication(parsedArgs.mInvokeWith, parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion, VMRuntime.getCurrentInstructionSet(), pipeFd, parsedArgs.mRemainingArgs); // Should not get here. throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned"); } else { if (!isZygote) { return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion, parsedArgs.mDisabledCompatChanges, parsedArgs.mRemainingArgs, null /* classLoader */); } else { return ZygoteInit.childZygoteInit( parsedArgs.mRemainingArgs /* classLoader */); } } } /** * Handles post-fork cleanup of parent proc * * @param pid != 0; pid of child if > 0 or indication of failed fork * if < 0; * @param pipeFd null-ok; pipe for communication with child. */ private void handleParentProc(int pid, FileDescriptor pipeFd) { if (pid > 0) { setChildPgid(pid); } boolean usingWrapper = false; if (pipeFd != null && pid > 0) { int innerPid = -1; try { // Do a busy loop here. We can't guarantee that a failure (and thus an exception // bail) happens in a timely manner. final int BYTES_REQUIRED = 4; // Bytes in an int. StructPollfd[] fds = new StructPollfd[] { new StructPollfd() }; byte[] data = new byte[BYTES_REQUIRED]; int remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS; int dataIndex = 0; long startTime = System.nanoTime(); while (dataIndex < data.length && remainingSleepTime > 0) { fds[0].fd = pipeFd; fds[0].events = (short) POLLIN; fds[0].revents = 0; fds[0].userData = null; int res = android.system.Os.poll(fds, remainingSleepTime); long endTime = System.nanoTime(); int elapsedTimeMs = (int) TimeUnit.MILLISECONDS.convert( endTime - startTime, TimeUnit.NANOSECONDS); remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS - elapsedTimeMs; if (res > 0) { if ((fds[0].revents & POLLIN) != 0) { // Only read one byte, so as not to block. Really needed? int readBytes = android.system.Os.read(pipeFd, data, dataIndex, 1); if (readBytes < 0) { throw new RuntimeException("Some error"); } dataIndex += readBytes; } else { // Error case. revents should contain one of the error bits. break; } } else if (res == 0) { Log.w(TAG, "Timed out waiting for child."); } } if (dataIndex == data.length) { DataInputStream is = new DataInputStream(new ByteArrayInputStream(data)); innerPid = is.readInt(); } if (innerPid == -1) { Log.w(TAG, "Error reading pid from wrapped process, child may have died"); } } catch (Exception ex) { Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex); } // Ensure that the pid reported by the wrapped process is either the // child process that we forked, or a descendant of it. if (innerPid > 0) { int parentPid = innerPid; while (parentPid > 0 && parentPid != pid) { parentPid = Process.getParentPid(parentPid); } if (parentPid > 0) { Log.i(TAG, "Wrapped process has pid " + innerPid); pid = innerPid; usingWrapper = true; } else { Log.w(TAG, "Wrapped process reported a pid that is not a child of " + "the process that we forked: childPid=" + pid + " innerPid=" + innerPid); } } } try { mSocketOutStream.writeInt(pid); mSocketOutStream.writeBoolean(usingWrapper); } catch (IOException ex) { throw new IllegalStateException("Error writing to command socket", ex); } } private void setChildPgid(int pid) { // Try to move the new child into the peer's process group. try { Os.setpgid(pid, Os.getpgid(peer.getPid())); } catch (ErrnoException ex) { // This exception is expected in the case where // the peer is not in our session // TODO get rid of this log message in the case where // getsid(0) != getsid(peer.getPid()) Log.i(TAG, "Zygote: setpgid failed. This is " + "normal if peer is not in our session"); } } }