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 
262         try {
263             if (pid == 0) {
264                 // in child
265                 zygoteServer.setForkChild();
266 
267                 zygoteServer.closeServerSocket();
268                 IoUtils.closeQuietly(serverPipeFd);
269                 serverPipeFd = null;
270 
271                 return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote);
272             } else {
273                 // In the parent. A pid < 0 indicates a failure and will be handled in
274                 // handleParentProc.
275                 IoUtils.closeQuietly(childPipeFd);
276                 childPipeFd = null;
277                 handleParentProc(pid, serverPipeFd);
278                 return null;
279             }
280         } finally {
281             IoUtils.closeQuietly(childPipeFd);
282             IoUtils.closeQuietly(serverPipeFd);
283         }
284     }
285 
handleAbiListQuery()286     private void handleAbiListQuery() {
287         try {
288             final byte[] abiListBytes = abiList.getBytes(StandardCharsets.US_ASCII);
289             mSocketOutStream.writeInt(abiListBytes.length);
290             mSocketOutStream.write(abiListBytes);
291         } catch (IOException ioe) {
292             throw new IllegalStateException("Error writing to command socket", ioe);
293         }
294     }
295 
handlePidQuery()296     private void handlePidQuery() {
297         try {
298             String pidString = String.valueOf(Process.myPid());
299             final byte[] pidStringBytes = pidString.getBytes(StandardCharsets.US_ASCII);
300             mSocketOutStream.writeInt(pidStringBytes.length);
301             mSocketOutStream.write(pidStringBytes);
302         } catch (IOException ioe) {
303             throw new IllegalStateException("Error writing to command socket", ioe);
304         }
305     }
306 
handleBootCompleted()307     private void handleBootCompleted() {
308         try {
309             mSocketOutStream.writeInt(0);
310         } catch (IOException ioe) {
311             throw new IllegalStateException("Error writing to command socket", ioe);
312         }
313 
314         VMRuntime.bootCompleted();
315     }
316 
317     /**
318      * Preloads resources if the zygote is in lazily preload mode. Writes the result of the
319      * preload operation; {@code 0} when a preload was initiated due to this request and {@code 1}
320      * if no preload was initiated. The latter implies that the zygote is not configured to load
321      * resources lazy or that the zygote has already handled a previous request to handlePreload.
322      */
handlePreload()323     private void handlePreload() {
324         try {
325             if (isPreloadComplete()) {
326                 mSocketOutStream.writeInt(1);
327             } else {
328                 preload();
329                 mSocketOutStream.writeInt(0);
330             }
331         } catch (IOException ioe) {
332             throw new IllegalStateException("Error writing to command socket", ioe);
333         }
334     }
335 
stateChangeWithUsapPoolReset(ZygoteServer zygoteServer, Runnable stateChangeCode)336     private Runnable stateChangeWithUsapPoolReset(ZygoteServer zygoteServer,
337             Runnable stateChangeCode) {
338         try {
339             if (zygoteServer.isUsapPoolEnabled()) {
340                 Log.i(TAG, "Emptying USAP Pool due to state change.");
341                 Zygote.emptyUsapPool();
342             }
343 
344             stateChangeCode.run();
345 
346             if (zygoteServer.isUsapPoolEnabled()) {
347                 Runnable fpResult =
348                         zygoteServer.fillUsapPool(
349                                 new int[]{mSocket.getFileDescriptor().getInt$()}, false);
350 
351                 if (fpResult != null) {
352                     zygoteServer.setForkChild();
353                     return fpResult;
354                 } else {
355                     Log.i(TAG, "Finished refilling USAP Pool after state change.");
356                 }
357             }
358 
359             mSocketOutStream.writeInt(0);
360 
361             return null;
362         } catch (IOException ioe) {
363             throw new IllegalStateException("Error writing to command socket", ioe);
364         }
365     }
366 
367     /**
368      * Makes the necessary changes to implement a new API blacklist exemption policy, and then
369      * responds to the system server, letting it know that the task has been completed.
370      *
371      * This necessitates a change to the internal state of the Zygote.  As such, if the USAP
372      * pool is enabled all existing USAPs have an incorrect API blacklist exemption list.  To
373      * properly handle this request the pool must be emptied and refilled.  This process can return
374      * a Runnable object that must be returned to ZygoteServer.runSelectLoop to be invoked.
375      *
376      * @param zygoteServer  The server object that received the request
377      * @param exemptions  The new exemption list.
378      * @return A Runnable object representing a new app in any USAPs spawned from here; the
379      *         zygote process will always receive a null value from this function.
380      */
handleApiBlacklistExemptions(ZygoteServer zygoteServer, String[] exemptions)381     private Runnable handleApiBlacklistExemptions(ZygoteServer zygoteServer, String[] exemptions) {
382         return stateChangeWithUsapPoolReset(zygoteServer,
383                 () -> ZygoteInit.setApiBlacklistExemptions(exemptions));
384     }
385 
handleUsapPoolStatusChange(ZygoteServer zygoteServer, boolean newStatus)386     private Runnable handleUsapPoolStatusChange(ZygoteServer zygoteServer, boolean newStatus) {
387         try {
388             Runnable fpResult = zygoteServer.setUsapPoolStatus(newStatus, mSocket);
389 
390             if (fpResult == null) {
391                 mSocketOutStream.writeInt(0);
392             } else {
393                 zygoteServer.setForkChild();
394             }
395 
396             return fpResult;
397         } catch (IOException ioe) {
398             throw new IllegalStateException("Error writing to command socket", ioe);
399         }
400     }
401 
402     /**
403      * Changes the API access log sample rate for the Zygote and processes spawned from it.
404      *
405      * This necessitates a change to the internal state of the Zygote.  As such, if the USAP
406      * pool is enabled all existing USAPs have an incorrect API access log sample rate.  To
407      * properly handle this request the pool must be emptied and refilled.  This process can return
408      * a Runnable object that must be returned to ZygoteServer.runSelectLoop to be invoked.
409      *
410      * @param zygoteServer  The server object that received the request
411      * @param samplingRate  The new sample rate for regular logging
412      * @param statsdSamplingRate  The new sample rate for statslog logging
413      * @return A Runnable object representing a new app in any blastulas spawned from here; the
414      *         zygote process will always receive a null value from this function.
415      */
handleHiddenApiAccessLogSampleRate(ZygoteServer zygoteServer, int samplingRate, int statsdSamplingRate)416     private Runnable handleHiddenApiAccessLogSampleRate(ZygoteServer zygoteServer,
417             int samplingRate, int statsdSamplingRate) {
418         return stateChangeWithUsapPoolReset(zygoteServer, () -> {
419             int maxSamplingRate = Math.max(samplingRate, statsdSamplingRate);
420             ZygoteInit.setHiddenApiAccessLogSampleRate(maxSamplingRate);
421             StatsdHiddenApiUsageLogger.setHiddenApiAccessLogSampleRates(
422                     samplingRate, statsdSamplingRate);
423             ZygoteInit.setHiddenApiUsageLogger(StatsdHiddenApiUsageLogger.getInstance());
424         });
425     }
426 
preload()427     protected void preload() {
428         ZygoteInit.lazyPreload();
429     }
430 
isPreloadComplete()431     protected boolean isPreloadComplete() {
432         return ZygoteInit.isPreloadComplete();
433     }
434 
getSocketOutputStream()435     protected DataOutputStream getSocketOutputStream() {
436         return mSocketOutStream;
437     }
438 
handlePreloadPackage(String packagePath, String libsPath, String libFileName, String cacheKey)439     protected void handlePreloadPackage(String packagePath, String libsPath, String libFileName,
440             String cacheKey) {
441         throw new RuntimeException("Zygote does not support package preloading");
442     }
443 
canPreloadApp()444     protected boolean canPreloadApp() {
445         return false;
446     }
447 
handlePreloadApp(ApplicationInfo aInfo)448     protected void handlePreloadApp(ApplicationInfo aInfo) {
449         throw new RuntimeException("Zygote does not support app preloading");
450     }
451 
452     /**
453      * Closes socket associated with this connection.
454      */
455     @UnsupportedAppUsage
closeSocket()456     void closeSocket() {
457         try {
458             mSocket.close();
459         } catch (IOException ex) {
460             Log.e(TAG, "Exception while closing command "
461                     + "socket in parent", ex);
462         }
463     }
464 
isClosedByPeer()465     boolean isClosedByPeer() {
466         return isEof;
467     }
468 
469     /**
470      * Handles post-fork setup of child proc, closing sockets as appropriate,
471      * reopen stdio as appropriate, and ultimately throwing MethodAndArgsCaller
472      * if successful or returning if failed.
473      *
474      * @param parsedArgs non-null; zygote args
475      * @param pipeFd null-ok; pipe for communication back to Zygote.
476      * @param isZygote whether this new child process is itself a new Zygote.
477      */
handleChildProc(ZygoteArguments parsedArgs, FileDescriptor pipeFd, boolean isZygote)478     private Runnable handleChildProc(ZygoteArguments parsedArgs,
479             FileDescriptor pipeFd, boolean isZygote) {
480         /*
481          * By the time we get here, the native code has closed the two actual Zygote
482          * socket connections, and substituted /dev/null in their place.  The LocalSocket
483          * objects still need to be closed properly.
484          */
485 
486         closeSocket();
487 
488         Zygote.setAppProcessName(parsedArgs, TAG);
489 
490         // End of the postFork event.
491         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
492         if (parsedArgs.mInvokeWith != null) {
493             WrapperInit.execApplication(parsedArgs.mInvokeWith,
494                     parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
495                     VMRuntime.getCurrentInstructionSet(),
496                     pipeFd, parsedArgs.mRemainingArgs);
497 
498             // Should not get here.
499             throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
500         } else {
501             if (!isZygote) {
502                 return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
503                         parsedArgs.mDisabledCompatChanges,
504                         parsedArgs.mRemainingArgs, null /* classLoader */);
505             } else {
506                 return ZygoteInit.childZygoteInit(parsedArgs.mTargetSdkVersion,
507                         parsedArgs.mRemainingArgs, null /* classLoader */);
508             }
509         }
510     }
511 
512     /**
513      * Handles post-fork cleanup of parent proc
514      *
515      * @param pid != 0; pid of child if &gt; 0 or indication of failed fork
516      * if &lt; 0;
517      * @param pipeFd null-ok; pipe for communication with child.
518      */
handleParentProc(int pid, FileDescriptor pipeFd)519     private void handleParentProc(int pid, FileDescriptor pipeFd) {
520         if (pid > 0) {
521             setChildPgid(pid);
522         }
523 
524         boolean usingWrapper = false;
525         if (pipeFd != null && pid > 0) {
526             int innerPid = -1;
527             try {
528                 // Do a busy loop here. We can't guarantee that a failure (and thus an exception
529                 // bail) happens in a timely manner.
530                 final int BYTES_REQUIRED = 4;  // Bytes in an int.
531 
532                 StructPollfd[] fds = new StructPollfd[] {
533                         new StructPollfd()
534                 };
535 
536                 byte[] data = new byte[BYTES_REQUIRED];
537 
538                 int remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS;
539                 int dataIndex = 0;
540                 long startTime = System.nanoTime();
541 
542                 while (dataIndex < data.length && remainingSleepTime > 0) {
543                     fds[0].fd = pipeFd;
544                     fds[0].events = (short) POLLIN;
545                     fds[0].revents = 0;
546                     fds[0].userData = null;
547 
548                     int res = android.system.Os.poll(fds, remainingSleepTime);
549                     long endTime = System.nanoTime();
550                     int elapsedTimeMs =
551                             (int) TimeUnit.MILLISECONDS.convert(
552                                     endTime - startTime,
553                                     TimeUnit.NANOSECONDS);
554                     remainingSleepTime = WRAPPED_PID_TIMEOUT_MILLIS - elapsedTimeMs;
555 
556                     if (res > 0) {
557                         if ((fds[0].revents & POLLIN) != 0) {
558                             // Only read one byte, so as not to block.
559                             int readBytes = android.system.Os.read(pipeFd, data, dataIndex, 1);
560                             if (readBytes < 0) {
561                                 throw new RuntimeException("Some error");
562                             }
563                             dataIndex += readBytes;
564                         } else {
565                             // Error case. revents should contain one of the error bits.
566                             break;
567                         }
568                     } else if (res == 0) {
569                         Log.w(TAG, "Timed out waiting for child.");
570                     }
571                 }
572 
573                 if (dataIndex == data.length) {
574                     DataInputStream is = new DataInputStream(new ByteArrayInputStream(data));
575                     innerPid = is.readInt();
576                 }
577 
578                 if (innerPid == -1) {
579                     Log.w(TAG, "Error reading pid from wrapped process, child may have died");
580                 }
581             } catch (Exception ex) {
582                 Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex);
583             }
584 
585             // Ensure that the pid reported by the wrapped process is either the
586             // child process that we forked, or a descendant of it.
587             if (innerPid > 0) {
588                 int parentPid = innerPid;
589                 while (parentPid > 0 && parentPid != pid) {
590                     parentPid = Process.getParentPid(parentPid);
591                 }
592                 if (parentPid > 0) {
593                     Log.i(TAG, "Wrapped process has pid " + innerPid);
594                     pid = innerPid;
595                     usingWrapper = true;
596                 } else {
597                     Log.w(TAG, "Wrapped process reported a pid that is not a child of "
598                             + "the process that we forked: childPid=" + pid
599                             + " innerPid=" + innerPid);
600                 }
601             }
602         }
603 
604         try {
605             mSocketOutStream.writeInt(pid);
606             mSocketOutStream.writeBoolean(usingWrapper);
607         } catch (IOException ex) {
608             throw new IllegalStateException("Error writing to command socket", ex);
609         }
610     }
611 
setChildPgid(int pid)612     private void setChildPgid(int pid) {
613         // Try to move the new child into the peer's process group.
614         try {
615             Os.setpgid(pid, Os.getpgid(peer.getPid()));
616         } catch (ErrnoException ex) {
617             // This exception is expected in the case where
618             // the peer is not in our session
619             // TODO get rid of this log message in the case where
620             // getsid(0) != getsid(peer.getPid())
621             Log.i(TAG, "Zygote: setpgid failed. This is "
622                 + "normal if peer is not in our session");
623         }
624     }
625 }
626