1 /*
2  * Copyright (C) 2019 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.server.om;
18 
19 import static android.content.Context.IDMAP_SERVICE;
20 
21 import static com.android.server.om.OverlayManagerService.TAG;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.os.FabricatedOverlayInfo;
26 import android.os.FabricatedOverlayInternal;
27 import android.os.IBinder;
28 import android.os.IIdmap2;
29 import android.os.RemoteException;
30 import android.os.ServiceManager;
31 import android.os.SystemClock;
32 import android.os.SystemService;
33 import android.text.TextUtils;
34 import android.util.Slog;
35 
36 import com.android.server.FgThread;
37 
38 import java.io.File;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.concurrent.TimeoutException;
43 import java.util.concurrent.atomic.AtomicInteger;
44 
45 /**
46  * To prevent idmap2d from continuously running, the idmap daemon will terminate after 10 seconds
47  * without a transaction.
48  **/
49 class IdmapDaemon {
50     // The amount of time in milliseconds to wait after a transaction to the idmap service is made
51     // before stopping the service.
52     private static final int SERVICE_TIMEOUT_MS = 10000;
53 
54     // The device may enter CPU sleep while waiting for the service startup, and in that mode
55     // the uptime doesn't increment. Thus, we need to have two timeouts: a smaller one for the
56     // uptime and a longer one for the wall time in case when the device never advances the uptime,
57     // so the watchdog won't get triggered.
58 
59     // The amount of uptime in milliseconds to wait when attempting to connect to idmap service.
60     private static final int SERVICE_CONNECT_UPTIME_TIMEOUT_MS = 5000;
61     // The amount of wall time in milliseconds to wait.
62     private static final int SERVICE_CONNECT_WALLTIME_TIMEOUT_MS = 30000;
63     private static final int SERVICE_CONNECT_INTERVAL_SLEEP_MS = 5;
64 
65     private static final String IDMAP_DAEMON = "idmap2d";
66 
67     private static IdmapDaemon sInstance;
68     private volatile IIdmap2 mService;
69     private final AtomicInteger mOpenedCount = new AtomicInteger();
70     private final Object mIdmapToken = new Object();
71 
72     /**
73      * An {@link AutoCloseable} connection to the idmap service. When the connection is closed or
74      * finalized, the idmap service will be stopped after a period of time unless another connection
75      * to the service is open.
76      **/
77     private class Connection implements AutoCloseable {
78         @Nullable
79         private final IIdmap2 mIdmap2;
80         private boolean mOpened = true;
81 
Connection(IIdmap2 idmap2)82         private Connection(IIdmap2 idmap2) {
83             synchronized (mIdmapToken) {
84                 mOpenedCount.incrementAndGet();
85                 mIdmap2 = idmap2;
86             }
87         }
88 
89         @Override
close()90         public void close() {
91             synchronized (mIdmapToken) {
92                 if (!mOpened) {
93                     return;
94                 }
95 
96                 mOpened = false;
97                 if (mOpenedCount.decrementAndGet() != 0) {
98                     // Only post the callback to stop the service if the service does not have an
99                     // open connection.
100                     return;
101                 }
102 
103                 FgThread.getHandler().postDelayed(() -> {
104                     synchronized (mIdmapToken) {
105                         // Only stop the service if the service does not have an open connection.
106                         if (mService == null || mOpenedCount.get() != 0) {
107                             return;
108                         }
109 
110                         stopIdmapService();
111                         mService = null;
112                     }
113                 }, mIdmapToken, SERVICE_TIMEOUT_MS);
114             }
115         }
116 
117         @Nullable
getIdmap2()118         public IIdmap2 getIdmap2() {
119             return mIdmap2;
120         }
121     }
122 
getInstance()123     static IdmapDaemon getInstance() {
124         if (sInstance == null) {
125             sInstance = new IdmapDaemon();
126         }
127         return sInstance;
128     }
129 
createIdmap(@onNull String targetPath, @NonNull String overlayPath, @Nullable String overlayName, int policies, boolean enforce, int userId)130     String createIdmap(@NonNull String targetPath, @NonNull String overlayPath,
131             @Nullable String overlayName, int policies, boolean enforce, int userId)
132             throws TimeoutException, RemoteException {
133         try (Connection c = connect()) {
134             final IIdmap2 idmap2 = c.getIdmap2();
135             if (idmap2 == null) {
136                 Slog.w(TAG, "idmap2d service is not ready for createIdmap(\"" + targetPath
137                         + "\", \"" + overlayPath + "\", \"" + overlayName + "\", " + policies + ", "
138                         + enforce + ", " + userId + ")");
139                 return null;
140             }
141 
142             return idmap2.createIdmap(targetPath, overlayPath, TextUtils.emptyIfNull(overlayName),
143                     policies, enforce, userId);
144         }
145     }
146 
removeIdmap(String overlayPath, int userId)147     boolean removeIdmap(String overlayPath, int userId) throws TimeoutException, RemoteException {
148         try (Connection c = connect()) {
149             final IIdmap2 idmap2 = c.getIdmap2();
150             if (idmap2 == null) {
151                 Slog.w(TAG, "idmap2d service is not ready for removeIdmap(\"" + overlayPath
152                         + "\", " + userId + ")");
153                 return false;
154             }
155 
156             return idmap2.removeIdmap(overlayPath, userId);
157         }
158     }
159 
verifyIdmap(@onNull String targetPath, @NonNull String overlayPath, @Nullable String overlayName, int policies, boolean enforce, int userId)160     boolean verifyIdmap(@NonNull String targetPath, @NonNull String overlayPath,
161             @Nullable String overlayName, int policies, boolean enforce, int userId)
162             throws Exception {
163         try (Connection c = connect()) {
164             final IIdmap2 idmap2 = c.getIdmap2();
165             if (idmap2 == null) {
166                 Slog.w(TAG, "idmap2d service is not ready for verifyIdmap(\"" + targetPath
167                         + "\", \"" + overlayPath + "\", \"" + overlayName + "\", " + policies + ", "
168                         + enforce + ", " + userId + ")");
169                 return false;
170             }
171 
172             return idmap2.verifyIdmap(targetPath, overlayPath, TextUtils.emptyIfNull(overlayName),
173                     policies, enforce, userId);
174         }
175     }
176 
idmapExists(String overlayPath, int userId)177     boolean idmapExists(String overlayPath, int userId) {
178         try (Connection c = connect()) {
179             final IIdmap2 idmap2 = c.getIdmap2();
180             if (idmap2 == null) {
181                 Slog.w(TAG, "idmap2d service is not ready for idmapExists(\"" + overlayPath
182                         + "\", " + userId + ")");
183                 return false;
184             }
185 
186             return new File(idmap2.getIdmapPath(overlayPath, userId)).isFile();
187         } catch (Exception e) {
188             Slog.wtf(TAG, "failed to check if idmap exists for " + overlayPath, e);
189             return false;
190         }
191     }
192 
createFabricatedOverlay(@onNull FabricatedOverlayInternal overlay)193     FabricatedOverlayInfo createFabricatedOverlay(@NonNull FabricatedOverlayInternal overlay) {
194         try (Connection c = connect()) {
195             final IIdmap2 idmap2 = c.getIdmap2();
196             if (idmap2 == null) {
197                 Slog.w(TAG, "idmap2d service is not ready for createFabricatedOverlay()");
198                 return null;
199             }
200 
201             return idmap2.createFabricatedOverlay(overlay);
202         } catch (Exception e) {
203             Slog.wtf(TAG, "failed to fabricate overlay " + overlay, e);
204             return null;
205         }
206     }
207 
deleteFabricatedOverlay(@onNull String path)208     boolean deleteFabricatedOverlay(@NonNull String path) {
209         try (Connection c = connect()) {
210             final IIdmap2 idmap2 = c.getIdmap2();
211             if (idmap2 == null) {
212                 Slog.w(TAG, "idmap2d service is not ready for deleteFabricatedOverlay(\"" + path
213                         + "\")");
214                 return false;
215             }
216 
217             return idmap2.deleteFabricatedOverlay(path);
218         } catch (Exception e) {
219             Slog.wtf(TAG, "failed to delete fabricated overlay '" + path + "'", e);
220             return false;
221         }
222     }
223 
getFabricatedOverlayInfos()224     synchronized List<FabricatedOverlayInfo> getFabricatedOverlayInfos() {
225         final ArrayList<FabricatedOverlayInfo> allInfos = new ArrayList<>();
226         Connection c = null;
227         int iteratorId = -1;
228         try {
229             c = connect();
230             final IIdmap2 service = c.getIdmap2();
231             if (service == null) {
232                 Slog.w(TAG, "idmap2d service is not ready for getFabricatedOverlayInfos()");
233                 return Collections.emptyList();
234             }
235 
236             iteratorId = service.acquireFabricatedOverlayIterator();
237             List<FabricatedOverlayInfo> infos;
238             while (!(infos = service.nextFabricatedOverlayInfos(iteratorId)).isEmpty()) {
239                 allInfos.addAll(infos);
240             }
241             return allInfos;
242         } catch (Exception e) {
243             Slog.wtf(TAG, "failed to get all fabricated overlays", e);
244         } finally {
245             try {
246                 if (c.getIdmap2() != null && iteratorId != -1) {
247                     c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId);
248                 }
249             } catch (RemoteException e) {
250                 // ignore
251             }
252             c.close();
253         }
254         return allInfos;
255     }
256 
dumpIdmap(@onNull String overlayPath)257     String dumpIdmap(@NonNull String overlayPath) {
258         try (Connection c = connect()) {
259             final IIdmap2 service = c.getIdmap2();
260             if (service == null) {
261                 final String dumpText = "idmap2d service is not ready for dumpIdmap()";
262                 Slog.w(TAG, dumpText);
263                 return dumpText;
264             }
265             String dump = service.dumpIdmap(overlayPath);
266             return TextUtils.nullIfEmpty(dump);
267         } catch (Exception e) {
268             Slog.wtf(TAG, "failed to dump idmap", e);
269             return null;
270         }
271     }
272 
273     @Nullable
getIdmapService()274     private IBinder getIdmapService() throws TimeoutException, RemoteException {
275         try {
276             SystemService.start(IDMAP_DAEMON);
277         } catch (RuntimeException e) {
278             Slog.wtf(TAG, "Failed to enable idmap2 daemon", e);
279             if (e.getMessage().contains("failed to set system property")) {
280                 return null;
281             }
282         }
283 
284         long uptimeMillis = SystemClock.uptimeMillis();
285         final long endUptimeMillis = uptimeMillis + SERVICE_CONNECT_UPTIME_TIMEOUT_MS;
286         long walltimeMillis = SystemClock.elapsedRealtime();
287         final long endWalltimeMillis = walltimeMillis + SERVICE_CONNECT_WALLTIME_TIMEOUT_MS;
288 
289         do {
290             final IBinder binder = ServiceManager.getService(IDMAP_SERVICE);
291             if (binder != null) {
292                 binder.linkToDeath(
293                         () -> Slog.w(TAG,
294                                 TextUtils.formatSimple("service '%s' died", IDMAP_SERVICE)), 0);
295                 return binder;
296             }
297             SystemClock.sleep(SERVICE_CONNECT_INTERVAL_SLEEP_MS);
298         } while ((uptimeMillis = SystemClock.uptimeMillis()) <= endUptimeMillis
299                 && (walltimeMillis = SystemClock.elapsedRealtime()) <= endWalltimeMillis);
300 
301         throw new TimeoutException(
302                 TextUtils.formatSimple("Failed to connect to '%s' in %d/%d ms (spent %d/%d ms)",
303                         IDMAP_SERVICE, SERVICE_CONNECT_UPTIME_TIMEOUT_MS,
304                         SERVICE_CONNECT_WALLTIME_TIMEOUT_MS,
305                         uptimeMillis - endUptimeMillis + SERVICE_CONNECT_UPTIME_TIMEOUT_MS,
306                         walltimeMillis - endWalltimeMillis + SERVICE_CONNECT_WALLTIME_TIMEOUT_MS));
307     }
308 
stopIdmapService()309     private static void stopIdmapService() {
310         try {
311             SystemService.stop(IDMAP_DAEMON);
312         } catch (RuntimeException e) {
313             // If the idmap daemon cannot be disabled for some reason, it is okay
314             // since we already finished invoking idmap.
315             Slog.w(TAG, "Failed to disable idmap2 daemon", e);
316         }
317     }
318 
319     @NonNull
connect()320     private Connection connect() throws TimeoutException, RemoteException {
321         synchronized (mIdmapToken) {
322             FgThread.getHandler().removeCallbacksAndMessages(mIdmapToken);
323             if (mService != null) {
324                 // Not enough time has passed to stop the idmap service. Reuse the existing
325                 // interface.
326                 return new Connection(mService);
327             }
328 
329             IBinder binder = getIdmapService();
330             if (binder == null) {
331                 return new Connection(null);
332             }
333 
334             mService = IIdmap2.Stub.asInterface(binder);
335             return new Connection(mService);
336         }
337     }
338 }
339