1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.os;
18 
19 import static android.system.OsConstants.F_SETFD;
20 import static android.system.OsConstants.O_CLOEXEC;
21 import static android.system.OsConstants.POLLIN;
22 
23 import static com.android.internal.os.ZygoteConnectionConstants.CONNECTION_TIMEOUT_MILLIS;
24 import static com.android.internal.os.ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS;
25 
26 import android.compat.annotation.UnsupportedAppUsage;
27 import android.content.pm.ApplicationInfo;
28 import android.net.Credentials;
29 import android.net.LocalSocket;
30 import android.os.Parcel;
31 import android.os.Process;
32 import android.os.Trace;
33 import android.system.ErrnoException;
34 import android.system.Os;
35 import android.system.StructPollfd;
36 import android.util.Log;
37 
38 import dalvik.system.VMRuntime;
39 
40 import libcore.io.IoUtils;
41 
42 import java.io.BufferedReader;
43 import java.io.ByteArrayInputStream;
44 import java.io.DataInputStream;
45 import java.io.DataOutputStream;
46 import java.io.FileDescriptor;
47 import java.io.IOException;
48 import java.io.InputStreamReader;
49 import java.nio.charset.StandardCharsets;
50 import java.util.Base64;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * A connection that can make spawn requests.
55  */
56 class ZygoteConnection {
57     private static final String TAG = "Zygote";
58 
59     /**
60      * The command socket.
61      *
62      * mSocket is retained in the child process in "peer wait" mode, so
63      * that it closes when the child process terminates. In other cases,
64      * it is closed in the peer.
65      */
66     @UnsupportedAppUsage
67     private final LocalSocket mSocket;
68     @UnsupportedAppUsage
69     private final DataOutputStream mSocketOutStream;
70     private final BufferedReader mSocketReader;
71     @UnsupportedAppUsage
72     private final Credentials peer;
73     private final String abiList;
74     private boolean isEof;
75 
76     /**
77      * Constructs instance from connected socket.
78      *
79      * @param socket non-null; connected socket
80      * @param abiList non-null; a list of ABIs this zygote supports.
81      * @throws IOException If obtaining the peer credentials fails
82      */
ZygoteConnection(LocalSocket socket, String abiList)83     ZygoteConnection(LocalSocket socket, String abiList) throws IOException {
84         mSocket = socket;
85         this.abiList = abiList;
86 
87         mSocketOutStream = new DataOutputStream(socket.getOutputStream());
88         mSocketReader =
89                 new BufferedReader(
90                         new InputStreamReader(socket.getInputStream()), Zygote.SOCKET_BUFFER_SIZE);
91 
92         mSocket.setSoTimeout(CONNECTION_TIMEOUT_MILLIS);
93 
94         try {
95             peer = mSocket.getPeerCredentials();
96         } catch (IOException ex) {
97             Log.e(TAG, "Cannot read peer credentials", ex);
98             throw ex;
99         }
100 
101         isEof = false;
102     }
103 
104     /**
105      * Returns the file descriptor of the associated socket.
106      *
107      * @return null-ok; file descriptor
108      */
getFileDescriptor()109     FileDescriptor getFileDescriptor() {
110         return mSocket.getFileDescriptor();
111     }
112 
113     /**
114      * Reads one start command from the command socket. If successful, a child is forked and a
115      * {@code Runnable} that calls the childs main method (or equivalent) is returned in the child
116      * process. {@code null} is always returned in the parent process (the zygote).
117      *
118      * If the client closes the socket, an {@code EOF} condition is set, which callers can test
119      * for by calling {@code ZygoteConnection.isClosedByPeer}.
120      */
processOneCommand(ZygoteServer zygoteServer)121     Runnable processOneCommand(ZygoteServer zygoteServer) {
122         String[] args;
123 
124         try {
125             args = Zygote.readArgumentList(mSocketReader);
126         } catch (IOException ex) {
127             throw new IllegalStateException("IOException on command socket", ex);
128         }
129 
130         // readArgumentList returns null only when it has reached EOF with no available
131         // data to read. This will only happen when the remote socket has disconnected.
132         if (args == null) {
133             isEof = true;
134             return null;
135         }
136 
137         int pid;
138         FileDescriptor childPipeFd = null;
139         FileDescriptor serverPipeFd = null;
140 
141         ZygoteArguments parsedArgs = new ZygoteArguments(args);
142 
143         if (parsedArgs.mBootCompleted) {
144             handleBootCompleted();
145             return null;
146         }
147 
148         if (parsedArgs.mAbiListQuery) {
149             handleAbiListQuery();
150             return null;
151         }
152 
153         if (parsedArgs.mPidQuery) {
154             handlePidQuery();
155             return null;
156         }
157 
158         if (parsedArgs.mUsapPoolStatusSpecified) {
159             return handleUsapPoolStatusChange(zygoteServer, parsedArgs.mUsapPoolEnabled);
160         }
161 
162         if (parsedArgs.mPreloadDefault) {
163             handlePreload();
164             return null;
165         }
166 
167         if (parsedArgs.mPreloadPackage != null) {
168             handlePreloadPackage(parsedArgs.mPreloadPackage, parsedArgs.mPreloadPackageLibs,
169                     parsedArgs.mPreloadPackageLibFileName, parsedArgs.mPreloadPackageCacheKey);
170             return null;
171         }
172 
173         if (canPreloadApp() && parsedArgs.mPreloadApp != null) {
174             byte[] rawParcelData = Base64.getDecoder().decode(parsedArgs.mPreloadApp);
175             Parcel appInfoParcel = Parcel.obtain();
176             appInfoParcel.unmarshall(rawParcelData, 0, rawParcelData.length);
177             appInfoParcel.setDataPosition(0);
178             ApplicationInfo appInfo = ApplicationInfo.CREATOR.createFromParcel(appInfoParcel);
179             appInfoParcel.recycle();
180             if (appInfo != null) {
181                 handlePreloadApp(appInfo);
182             } else {
183                 throw new IllegalArgumentException("Failed to deserialize --preload-app");
184             }
185             return null;
186         }
187 
188         if (parsedArgs.mApiBlacklistExemptions != null) {
189             return handleApiBlacklistExemptions(zygoteServer, parsedArgs.mApiBlacklistExemptions);
190         }
191 
192         if (parsedArgs.mHiddenApiAccessLogSampleRate != -1
193                 || parsedArgs.mHiddenApiAccessStatslogSampleRate != -1) {
194             return handleHiddenApiAccessLogSampleRate(zygoteServer,
195                     parsedArgs.mHiddenApiAccessLogSampleRate,
196                     parsedArgs.mHiddenApiAccessStatslogSampleRate);
197         }
198 
199         if (parsedArgs.mPermittedCapabilities != 0 || parsedArgs.mEffectiveCapabilities != 0) {
200             throw new ZygoteSecurityException("Client may not specify capabilities: "
201                     + "permitted=0x" + Long.toHexString(parsedArgs.mPermittedCapabilities)
202                     + ", effective=0x" + Long.toHexString(parsedArgs.mEffectiveCapabilities));
203         }
204 
205         Zygote.applyUidSecurityPolicy(parsedArgs, peer);
206         Zygote.applyInvokeWithSecurityPolicy(parsedArgs, peer);
207 
208         Zygote.applyDebuggerSystemProperty(parsedArgs);
209         Zygote.applyInvokeWithSystemProperty(parsedArgs);
210 
211         int[][] rlimits = null;
212 
213         if (parsedArgs.mRLimits != null) {
214             rlimits = parsedArgs.mRLimits.toArray(Zygote.INT_ARRAY_2D);
215         }
216 
217         int[] fdsToIgnore = null;
218 
219         if (parsedArgs.mInvokeWith != null) {
220             try {
221                 FileDescriptor[] pipeFds = Os.pipe2(O_CLOEXEC);
222                 childPipeFd = pipeFds[1];
223                 serverPipeFd = pipeFds[0];
224                 Os.fcntlInt(childPipeFd, F_SETFD, 0);
225                 fdsToIgnore = new int[]{childPipeFd.getInt$(), serverPipeFd.getInt$()};
226             } catch (ErrnoException errnoEx) {
227                 throw new IllegalStateException("Unable to set up pipe for invoke-with", errnoEx);
228             }
229         }
230 
231         /*
232          * In order to avoid leaking descriptors to the Zygote child,
233          * the native code must close the two Zygote socket descriptors
234          * in the child process before it switches from Zygote-root to
235          * the UID and privileges of the application being launched.
236          *
237          * In order to avoid "bad file descriptor" errors when the
238          * two LocalSocket objects are closed, the Posix file
239          * descriptors are released via a dup2() call which closes
240          * the socket and substitutes an open descriptor to /dev/null.
241          */
242 
243         int [] fdsToClose = { -1, -1 };
244 
245         FileDescriptor fd = mSocket.getFileDescriptor();
246 
247         if (fd != null) {
248             fdsToClose[0] = fd.getInt$();
249         }
250 
251         fd = zygoteServer.getZygoteSocketFileDescriptor();
252 
253         if (fd != null) {
254             fdsToClose[1] = fd.getInt$();
255         }
256 
257         pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid, parsedArgs.mGids,
258                 parsedArgs.mRuntimeFlags, rlimits, parsedArgs.mMountExternal, parsedArgs.mSeInfo,
259                 parsedArgs.mNiceName, fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
260                 parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, parsedArgs.mIsTopApp,
261                 parsedArgs.mPkgDataInfoList, parsedArgs.mWhitelistedDataInfoList,
262                 parsedArgs.mBindMountAppDataDirs, parsedArgs.mBindMountAppStorageDirs);
263 
264         try {
265             if (pid == 0) {
266                 // in child
267                 zygoteServer.setForkChild();
268 
269                 zygoteServer.closeServerSocket();
270                 IoUtils.closeQuietly(serverPipeFd);
271                 serverPipeFd = null;
272 
273                 return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote);
274             } else {
275                 // In the parent. A pid < 0 indicates a failure and will be handled in
276                 // handleParentProc.
277                 IoUtils.closeQuietly(childPipeFd);
278                 childPipeFd = null;
279                 handleParentProc(pid, serverPipeFd);
280                 return null;
281             }
282         } finally {
283             IoUtils.closeQuietly(childPipeFd);
284             IoUtils.closeQuietly(serverPipeFd);
285         }
286     }
287 
handleAbiListQuery()288     private void handleAbiListQuery() {
289         try {
290             final byte[] abiListBytes = abiList.getBytes(StandardCharsets.US_ASCII);
291             mSocketOutStream.writeInt(abiListBytes.length);
292             mSocketOutStream.write(abiListBytes);
293         } catch (IOException ioe) {
294             throw new IllegalStateException("Error writing to command socket", ioe);
295         }
296     }
297 
handlePidQuery()298     private void handlePidQuery() {
299         try {
300             String pidString = String.valueOf(Process.myPid());
301             final byte[] pidStringBytes = pidString.getBytes(StandardCharsets.US_ASCII);
302             mSocketOutStream.writeInt(pidStringBytes.length);
303             mSocketOutStream.write(pidStringBytes);
304         } catch (IOException ioe) {
305             throw new IllegalStateException("Error writing to command socket", ioe);
306         }
307     }
308 
handleBootCompleted()309     private void handleBootCompleted() {
310         try {
311             mSocketOutStream.writeInt(0);
312         } catch (IOException ioe) {
313             throw new IllegalStateException("Error writing to command socket", ioe);
314         }
315 
316         VMRuntime.bootCompleted();
317     }
318 
319     /**
320      * Preloads resources if the zygote is in lazily preload mode. Writes the result of the
321      * preload operation; {@code 0} when a preload was initiated due to this request and {@code 1}
322      * if no preload was initiated. The latter implies that the zygote is not configured to load
323      * resources lazy or that the zygote has already handled a previous request to handlePreload.
324      */
handlePreload()325     private void handlePreload() {
326         try {
327             if (isPreloadComplete()) {
328                 mSocketOutStream.writeInt(1);
329             } else {
330                 preload();
331                 mSocketOutStream.writeInt(0);
332             }
333         } catch (IOException ioe) {
334             throw new IllegalStateException("Error writing to command socket", ioe);
335         }
336     }
337 
stateChangeWithUsapPoolReset(ZygoteServer zygoteServer, Runnable stateChangeCode)338     private Runnable stateChangeWithUsapPoolReset(ZygoteServer zygoteServer,
339             Runnable stateChangeCode) {
340         try {
341             if (zygoteServer.isUsapPoolEnabled()) {
342                 Log.i(TAG, "Emptying USAP Pool due to state change.");
343                 Zygote.emptyUsapPool();
344             }
345 
346             stateChangeCode.run();
347 
348             if (zygoteServer.isUsapPoolEnabled()) {
349                 Runnable fpResult =
350                         zygoteServer.fillUsapPool(
351                                 new int[]{mSocket.getFileDescriptor().getInt$()}, false);
352 
353                 if (fpResult != null) {
354                     zygoteServer.setForkChild();
355                     return fpResult;
356                 } else {
357                     Log.i(TAG, "Finished refilling USAP Pool after state change.");
358                 }
359             }
360 
361             mSocketOutStream.writeInt(0);
362 
363             return null;
364         } catch (IOException ioe) {
365             throw new IllegalStateException("Error writing to command socket", ioe);
366         }
367     }
368 
369     /**
370      * Makes the necessary changes to implement a new API blacklist exemption policy, and then
371      * responds to the system server, letting it know that the task has been completed.
372      *
373      * This necessitates a change to the internal state of the Zygote.  As such, if the USAP
374      * pool is enabled all existing USAPs have an incorrect API blacklist exemption list.  To
375      * properly handle this request the pool must be emptied and refilled.  This process can return
376      * a Runnable object that must be returned to ZygoteServer.runSelectLoop to be invoked.
377      *
378      * @param zygoteServer  The server object that received the request
379      * @param exemptions  The new exemption list.
380      * @return A Runnable object representing a new app in any USAPs spawned from here; the
381      *         zygote process will always receive a null value from this function.
382      */
handleApiBlacklistExemptions(ZygoteServer zygoteServer, String[] exemptions)383     private Runnable handleApiBlacklistExemptions(ZygoteServer zygoteServer, String[] exemptions) {
384         return stateChangeWithUsapPoolReset(zygoteServer,
385                 () -> ZygoteInit.setApiBlacklistExemptions(exemptions));
386     }
387 
handleUsapPoolStatusChange(ZygoteServer zygoteServer, boolean newStatus)388     private Runnable handleUsapPoolStatusChange(ZygoteServer zygoteServer, boolean newStatus) {
389         try {
390             Runnable fpResult = zygoteServer.setUsapPoolStatus(newStatus, mSocket);
391 
392             if (fpResult == null) {
393                 mSocketOutStream.writeInt(0);
394             } else {
395                 zygoteServer.setForkChild();
396             }
397 
398             return fpResult;
399         } catch (IOException ioe) {
400             throw new IllegalStateException("Error writing to command socket", ioe);
401         }
402     }
403 
404     /**
405      * Changes the API access log sample rate for the Zygote and processes spawned from it.
406      *
407      * This necessitates a change to the internal state of the Zygote.  As such, if the USAP
408      * pool is enabled all existing USAPs have an incorrect API access log sample rate.  To
409      * properly handle this request the pool must be emptied and refilled.  This process can return
410      * a Runnable object that must be returned to ZygoteServer.runSelectLoop to be invoked.
411      *
412      * @param zygoteServer  The server object that received the request
413      * @param samplingRate  The new sample rate for regular logging
414      * @param statsdSamplingRate  The new sample rate for statslog logging
415      * @return A Runnable object representing a new app in any blastulas spawned from here; the
416      *         zygote process will always receive a null value from this function.
417      */
handleHiddenApiAccessLogSampleRate(ZygoteServer zygoteServer, int samplingRate, int statsdSamplingRate)418     private Runnable handleHiddenApiAccessLogSampleRate(ZygoteServer zygoteServer,
419             int samplingRate, int statsdSamplingRate) {
420         return stateChangeWithUsapPoolReset(zygoteServer, () -> {
421             int maxSamplingRate = Math.max(samplingRate, statsdSamplingRate);
422             ZygoteInit.setHiddenApiAccessLogSampleRate(maxSamplingRate);
423             StatsdHiddenApiUsageLogger.setHiddenApiAccessLogSampleRates(
424                     samplingRate, statsdSamplingRate);
425             ZygoteInit.setHiddenApiUsageLogger(StatsdHiddenApiUsageLogger.getInstance());
426         });
427     }
428 
preload()429     protected void preload() {
430         ZygoteInit.lazyPreload();
431     }
432 
isPreloadComplete()433     protected boolean isPreloadComplete() {
434         return ZygoteInit.isPreloadComplete();
435     }
436 
getSocketOutputStream()437     protected DataOutputStream getSocketOutputStream() {
438         return mSocketOutStream;
439     }
440 
handlePreloadPackage(String packagePath, String libsPath, String libFileName, String cacheKey)441     protected void handlePreloadPackage(String packagePath, String libsPath, String libFileName,
442             String cacheKey) {
443         throw new RuntimeException("Zygote does not support package preloading");
444     }
445 
canPreloadApp()446     protected boolean canPreloadApp() {
447         return false;
448     }
449 
handlePreloadApp(ApplicationInfo aInfo)450     protected void handlePreloadApp(ApplicationInfo aInfo) {
451         throw new RuntimeException("Zygote does not support app preloading");
452     }
453 
454     /**
455      * Closes socket associated with this connection.
456      */
457     @UnsupportedAppUsage
closeSocket()458     void closeSocket() {
459         try {
460             mSocket.close();
461         } catch (IOException ex) {
462             Log.e(TAG, "Exception while closing command "
463                     + "socket in parent", ex);
464         }
465     }
466 
isClosedByPeer()467     boolean isClosedByPeer() {
468         return isEof;
469     }
470 
471     /**
472      * Handles post-fork setup of child proc, closing sockets as appropriate,
473      * reopen stdio as appropriate, and ultimately throwing MethodAndArgsCaller
474      * if successful or returning if failed.
475      *
476      * @param parsedArgs non-null; zygote args
477      * @param pipeFd null-ok; pipe for communication back to Zygote.
478      * @param isZygote whether this new child process is itself a new Zygote.
479      */
handleChildProc(ZygoteArguments parsedArgs, FileDescriptor pipeFd, boolean isZygote)480     private Runnable handleChildProc(ZygoteArguments parsedArgs,
481             FileDescriptor pipeFd, boolean isZygote) {
482         /*
483          * By the time we get here, the native code has closed the two actual Zygote
484          * socket connections, and substituted /dev/null in their place.  The LocalSocket
485          * objects still need to be closed properly.
486          */
487 
488         closeSocket();
489 
490         Zygote.setAppProcessName(parsedArgs, TAG);
491 
492         // End of the postFork event.
493         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
494         if (parsedArgs.mInvokeWith != null) {
495             WrapperInit.execApplication(parsedArgs.mInvokeWith,
496                     parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
497                     VMRuntime.getCurrentInstructionSet(),
498                     pipeFd, parsedArgs.mRemainingArgs);
499 
500             // Should not get here.
501             throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
502         } else {
503             if (!isZygote) {
504                 return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
505                         parsedArgs.mDisabledCompatChanges,
506                         parsedArgs.mRemainingArgs, null /* classLoader */);
507             } else {
508                 return ZygoteInit.childZygoteInit(parsedArgs.mTargetSdkVersion,
509                         parsedArgs.mRemainingArgs, null /* classLoader */);
510             }
511         }
512     }
513 
514     /**
515      * Handles post-fork cleanup of parent proc
516      *
517      * @param pid != 0; pid of child if &gt; 0 or indication of failed fork
518      * if &lt; 0;
519      * @param pipeFd null-ok; pipe for communication with child.
520      */
handleParentProc(int pid, FileDescriptor pipeFd)521     private void handleParentProc(int pid, FileDescriptor pipeFd) {
522         if (pid > 0) {
523             setChildPgid(pid);
524         }
525 
526         boolean usingWrapper = false;
527         if (pipeFd != null && pid > 0) {
528             int innerPid = -1;
529             try {
530                 // Do a busy loop here. We can't guarantee that a failure (and thus an exception
531                 // bail) happens in a timely manner.
532                 final int BYTES_REQUIRED = 4;  // Bytes in an int.
533 
534                 StructPollfd[] fds = new StructPollfd[] {
535                         new StructPollfd()
536                 };
537 
538                 byte[] data = new byte[BYTES_REQUIRED];
539 
540                 int remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS;
541                 int dataIndex = 0;
542                 long startTime = System.nanoTime();
543 
544                 while (dataIndex < data.length && remainingSleepTime > 0) {
545                     fds[0].fd = pipeFd;
546                     fds[0].events = (short) POLLIN;
547                     fds[0].revents = 0;
548                     fds[0].userData = null;
549 
550                     int res = android.system.Os.poll(fds, remainingSleepTime);
551                     long endTime = System.nanoTime();
552                     int elapsedTimeMs =
553                             (int) TimeUnit.MILLISECONDS.convert(
554                                     endTime - startTime,
555                                     TimeUnit.NANOSECONDS);
556                     remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS - elapsedTimeMs;
557 
558                     if (res > 0) {
559                         if ((fds[0].revents & POLLIN) != 0) {
560                             // Only read one byte, so as not to block.
561                             int readBytes = android.system.Os.read(pipeFd, data, dataIndex, 1);
562                             if (readBytes < 0) {
563                                 throw new RuntimeException("Some error");
564                             }
565                             dataIndex += readBytes;
566                         } else {
567                             // Error case. revents should contain one of the error bits.
568                             break;
569                         }
570                     } else if (res == 0) {
571                         Log.w(TAG, "Timed out waiting for child.");
572                     }
573                 }
574 
575                 if (dataIndex == data.length) {
576                     DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
577                     innerPid = is.readInt();
578                 }
579 
580                 if (innerPid == -1) {
581                     Log.w(TAG, "Error reading pid from wrapped process, child may have died");
582                 }
583             } catch (Exception ex) {
584                 Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex);
585             }
586 
587             // Ensure that the pid reported by the wrapped process is either the
588             // child process that we forked, or a descendant of it.
589             if (innerPid > 0) {
590                 int parentPid = innerPid;
591                 while (parentPid > 0 && parentPid != pid) {
592                     parentPid = Process.getParentPid(parentPid);
593                 }
594                 if (parentPid > 0) {
595                     Log.i(TAG, "Wrapped process has pid " + innerPid);
596                     pid = innerPid;
597                     usingWrapper = true;
598                 } else {
599                     Log.w(TAG, "Wrapped process reported a pid that is not a child of "
600                             + "the process that we forked: childPid=" + pid
601                             + " innerPid=" + innerPid);
602                 }
603             }
604         }
605 
606         try {
607             mSocketOutStream.writeInt(pid);
608             mSocketOutStream.writeBoolean(usingWrapper);
609         } catch (IOException ex) {
610             throw new IllegalStateException("Error writing to command socket", ex);
611         }
612     }
613 
setChildPgid(int pid)614     private void setChildPgid(int pid) {
615         // Try to move the new child into the peer's process group.
616         try {
617             Os.setpgid(pid, Os.getpgid(peer.getPid()));
618         } catch (ErrnoException ex) {
619             // This exception is expected in the case where
620             // the peer is not in our session
621             // TODO get rid of this log message in the case where
622             // getsid(0) != getsid(peer.getPid())
623             Log.i(TAG, "Zygote: setpgid failed. This is "
624                 + "normal if peer is not in our session");
625         }
626     }
627 }
628