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 amount of time in milliseconds to wait when attempting to connect to idmap service.
55     private static final int SERVICE_CONNECT_TIMEOUT_MS = 5000;
56     private static final int SERVICE_CONNECT_INTERVAL_SLEEP_MS = 5;
57 
58     private static final String IDMAP_DAEMON = "idmap2d";
59 
60     private static IdmapDaemon sInstance;
61     private volatile IIdmap2 mService;
62     private final AtomicInteger mOpenedCount = new AtomicInteger();
63     private final Object mIdmapToken = new Object();
64 
65     /**
66      * An {@link AutoCloseable} connection to the idmap service. When the connection is closed or
67      * finalized, the idmap service will be stopped after a period of time unless another connection
68      * to the service is open.
69      **/
70     private class Connection implements AutoCloseable {
71         @Nullable
72         private final IIdmap2 mIdmap2;
73         private boolean mOpened = true;
74 
Connection(IIdmap2 idmap2)75         private Connection(IIdmap2 idmap2) {
76             synchronized (mIdmapToken) {
77                 mOpenedCount.incrementAndGet();
78                 mIdmap2 = idmap2;
79             }
80         }
81 
82         @Override
close()83         public void close() {
84             synchronized (mIdmapToken) {
85                 if (!mOpened) {
86                     return;
87                 }
88 
89                 mOpened = false;
90                 if (mOpenedCount.decrementAndGet() != 0) {
91                     // Only post the callback to stop the service if the service does not have an
92                     // open connection.
93                     return;
94                 }
95 
96                 FgThread.getHandler().postDelayed(() -> {
97                     synchronized (mIdmapToken) {
98                         // Only stop the service if the service does not have an open connection.
99                         if (mService == null || mOpenedCount.get() != 0) {
100                             return;
101                         }
102 
103                         stopIdmapService();
104                         mService = null;
105                     }
106                 }, mIdmapToken, SERVICE_TIMEOUT_MS);
107             }
108         }
109 
110         @Nullable
getIdmap2()111         public IIdmap2 getIdmap2() {
112             return mIdmap2;
113         }
114     }
115 
getInstance()116     static IdmapDaemon getInstance() {
117         if (sInstance == null) {
118             sInstance = new IdmapDaemon();
119         }
120         return sInstance;
121     }
122 
createIdmap(@onNull String targetPath, @NonNull String overlayPath, @Nullable String overlayName, int policies, boolean enforce, int userId)123     String createIdmap(@NonNull String targetPath, @NonNull String overlayPath,
124             @Nullable String overlayName, int policies, boolean enforce, int userId)
125             throws TimeoutException, RemoteException {
126         try (Connection c = connect()) {
127             final IIdmap2 idmap2 = c.getIdmap2();
128             if (idmap2 == null) {
129                 Slog.w(TAG, "idmap2d service is not ready for createIdmap(\"" + targetPath
130                         + "\", \"" + overlayPath + "\", \"" + overlayName + "\", " + policies + ", "
131                         + enforce + ", " + userId + ")");
132                 return null;
133             }
134 
135             return idmap2.createIdmap(targetPath, overlayPath, TextUtils.emptyIfNull(overlayName),
136                     policies, enforce, userId);
137         }
138     }
139 
removeIdmap(String overlayPath, int userId)140     boolean removeIdmap(String overlayPath, int userId) throws TimeoutException, RemoteException {
141         try (Connection c = connect()) {
142             final IIdmap2 idmap2 = c.getIdmap2();
143             if (idmap2 == null) {
144                 Slog.w(TAG, "idmap2d service is not ready for removeIdmap(\"" + overlayPath
145                         + "\", " + userId + ")");
146                 return false;
147             }
148 
149             return idmap2.removeIdmap(overlayPath, userId);
150         }
151     }
152 
verifyIdmap(@onNull String targetPath, @NonNull String overlayPath, @Nullable String overlayName, int policies, boolean enforce, int userId)153     boolean verifyIdmap(@NonNull String targetPath, @NonNull String overlayPath,
154             @Nullable String overlayName, int policies, boolean enforce, int userId)
155             throws Exception {
156         try (Connection c = connect()) {
157             final IIdmap2 idmap2 = c.getIdmap2();
158             if (idmap2 == null) {
159                 Slog.w(TAG, "idmap2d service is not ready for verifyIdmap(\"" + targetPath
160                         + "\", \"" + overlayPath + "\", \"" + overlayName + "\", " + policies + ", "
161                         + enforce + ", " + userId + ")");
162                 return false;
163             }
164 
165             return idmap2.verifyIdmap(targetPath, overlayPath, TextUtils.emptyIfNull(overlayName),
166                     policies, enforce, userId);
167         }
168     }
169 
idmapExists(String overlayPath, int userId)170     boolean idmapExists(String overlayPath, int userId) {
171         try (Connection c = connect()) {
172             final IIdmap2 idmap2 = c.getIdmap2();
173             if (idmap2 == null) {
174                 Slog.w(TAG, "idmap2d service is not ready for idmapExists(\"" + overlayPath
175                         + "\", " + userId + ")");
176                 return false;
177             }
178 
179             return new File(idmap2.getIdmapPath(overlayPath, userId)).isFile();
180         } catch (Exception e) {
181             Slog.wtf(TAG, "failed to check if idmap exists for " + overlayPath, e);
182             return false;
183         }
184     }
185 
createFabricatedOverlay(@onNull FabricatedOverlayInternal overlay)186     FabricatedOverlayInfo createFabricatedOverlay(@NonNull FabricatedOverlayInternal overlay) {
187         try (Connection c = connect()) {
188             final IIdmap2 idmap2 = c.getIdmap2();
189             if (idmap2 == null) {
190                 Slog.w(TAG, "idmap2d service is not ready for createFabricatedOverlay()");
191                 return null;
192             }
193 
194             return idmap2.createFabricatedOverlay(overlay);
195         } catch (Exception e) {
196             Slog.wtf(TAG, "failed to fabricate overlay " + overlay, e);
197             return null;
198         }
199     }
200 
deleteFabricatedOverlay(@onNull String path)201     boolean deleteFabricatedOverlay(@NonNull String path) {
202         try (Connection c = connect()) {
203             final IIdmap2 idmap2 = c.getIdmap2();
204             if (idmap2 == null) {
205                 Slog.w(TAG, "idmap2d service is not ready for deleteFabricatedOverlay(\"" + path
206                         + "\")");
207                 return false;
208             }
209 
210             return idmap2.deleteFabricatedOverlay(path);
211         } catch (Exception e) {
212             Slog.wtf(TAG, "failed to delete fabricated overlay '" + path + "'", e);
213             return false;
214         }
215     }
216 
getFabricatedOverlayInfos()217     synchronized List<FabricatedOverlayInfo> getFabricatedOverlayInfos() {
218         final ArrayList<FabricatedOverlayInfo> allInfos = new ArrayList<>();
219         Connection c = null;
220         int iteratorId = -1;
221         try {
222             c = connect();
223             final IIdmap2 service = c.getIdmap2();
224             if (service == null) {
225                 Slog.w(TAG, "idmap2d service is not ready for getFabricatedOverlayInfos()");
226                 return Collections.emptyList();
227             }
228 
229             iteratorId = service.acquireFabricatedOverlayIterator();
230             List<FabricatedOverlayInfo> infos;
231             while (!(infos = service.nextFabricatedOverlayInfos(iteratorId)).isEmpty()) {
232                 allInfos.addAll(infos);
233             }
234             return allInfos;
235         } catch (Exception e) {
236             Slog.wtf(TAG, "failed to get all fabricated overlays", e);
237         } finally {
238             try {
239                 if (c.getIdmap2() != null && iteratorId != -1) {
240                     c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId);
241                 }
242             } catch (RemoteException e) {
243                 // ignore
244             }
245             c.close();
246         }
247         return allInfos;
248     }
249 
dumpIdmap(@onNull String overlayPath)250     String dumpIdmap(@NonNull String overlayPath) {
251         try (Connection c = connect()) {
252             final IIdmap2 service = c.getIdmap2();
253             if (service == null) {
254                 final String dumpText = "idmap2d service is not ready for dumpIdmap()";
255                 Slog.w(TAG, dumpText);
256                 return dumpText;
257             }
258             String dump = service.dumpIdmap(overlayPath);
259             return TextUtils.nullIfEmpty(dump);
260         } catch (Exception e) {
261             Slog.wtf(TAG, "failed to dump idmap", e);
262             return null;
263         }
264     }
265 
266     @Nullable
getIdmapService()267     private IBinder getIdmapService() throws TimeoutException, RemoteException {
268         try {
269             SystemService.start(IDMAP_DAEMON);
270         } catch (RuntimeException e) {
271             Slog.wtf(TAG, "Failed to enable idmap2 daemon", e);
272             if (e.getMessage().contains("failed to set system property")) {
273                 return null;
274             }
275         }
276 
277         final long endMillis = SystemClock.uptimeMillis() + SERVICE_CONNECT_TIMEOUT_MS;
278         do {
279             final IBinder binder = ServiceManager.getService(IDMAP_SERVICE);
280             if (binder != null) {
281                 binder.linkToDeath(
282                         () -> Slog.w(TAG, String.format("service '%s' died", IDMAP_SERVICE)), 0);
283                 return binder;
284             }
285             SystemClock.sleep(SERVICE_CONNECT_INTERVAL_SLEEP_MS);
286         } while (SystemClock.uptimeMillis() <= endMillis);
287 
288         throw new TimeoutException(
289             String.format("Failed to connect to '%s' in %d milliseconds", IDMAP_SERVICE,
290                     SERVICE_CONNECT_TIMEOUT_MS));
291     }
292 
stopIdmapService()293     private static void stopIdmapService() {
294         try {
295             SystemService.stop(IDMAP_DAEMON);
296         } catch (RuntimeException e) {
297             // If the idmap daemon cannot be disabled for some reason, it is okay
298             // since we already finished invoking idmap.
299             Slog.w(TAG, "Failed to disable idmap2 daemon", e);
300         }
301     }
302 
303     @NonNull
connect()304     private Connection connect() throws TimeoutException, RemoteException {
305         synchronized (mIdmapToken) {
306             FgThread.getHandler().removeCallbacksAndMessages(mIdmapToken);
307             if (mService != null) {
308                 // Not enough time has passed to stop the idmap service. Reuse the existing
309                 // interface.
310                 return new Connection(mService);
311             }
312 
313             IBinder binder = getIdmapService();
314             if (binder == null) {
315                 return new Connection(null);
316             }
317 
318             mService = IIdmap2.Stub.asInterface(binder);
319             return new Connection(mService);
320         }
321     }
322 }
323