1 /*
2  * Copyright (C) 2022 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.logcat;
18 
19 import static android.os.Process.getParentPid;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.ActivityManager;
25 import android.app.ActivityManagerInternal;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.os.Build;
31 import android.os.DeadObjectException;
32 import android.os.Handler;
33 import android.os.ILogd;
34 import android.os.Looper;
35 import android.os.Message;
36 import android.os.RemoteException;
37 import android.os.ServiceManager;
38 import android.os.SystemClock;
39 import android.os.UserHandle;
40 import android.os.logcat.ILogcatManagerService;
41 import android.util.ArrayMap;
42 import android.util.Slog;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.app.ILogAccessDialogCallback;
46 import com.android.internal.util.ArrayUtils;
47 import com.android.server.LocalServices;
48 import com.android.server.SystemService;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Objects;
57 import java.util.function.Supplier;
58 
59 
60 /**
61  * Service responsible for managing the access to Logcat.
62  */
63 public final class LogcatManagerService extends SystemService {
64     private static final String TAG = "LogcatManagerService";
65     private static final boolean DEBUG = false;
66     private static final String TARGET_PACKAGE_NAME = "com.android.systemui";
67     private static final String TARGET_ACTIVITY_NAME =
68             "com.android.systemui.logcat.LogAccessDialogActivity";
69     public static final String EXTRA_CALLBACK = "EXTRA_CALLBACK";
70 
71     /** How long to wait for the user to approve/decline before declining automatically */
72     @VisibleForTesting
73     static final int PENDING_CONFIRMATION_TIMEOUT_MILLIS = Build.IS_DEBUGGABLE ? 70000 : 400000;
74 
75     /**
76      * How long an approved / declined status is valid for.
77      *
78      * After a client has been approved/declined log access, if they try to access logs again within
79      * this timeout, the new request will be automatically approved/declined.
80      * Only after this timeout expires will a new request generate another prompt to the user.
81      **/
82     @VisibleForTesting
83     static final int STATUS_EXPIRATION_TIMEOUT_MILLIS = 60 * 1000;
84 
85     private static final int MSG_LOG_ACCESS_REQUESTED = 0;
86     private static final int MSG_APPROVE_LOG_ACCESS = 1;
87     private static final int MSG_DECLINE_LOG_ACCESS = 2;
88     private static final int MSG_LOG_ACCESS_FINISHED = 3;
89     private static final int MSG_PENDING_TIMEOUT = 4;
90     private static final int MSG_LOG_ACCESS_STATUS_EXPIRED = 5;
91 
92     private static final int STATUS_NEW_REQUEST = 0;
93     private static final int STATUS_PENDING = 1;
94     private static final int STATUS_APPROVED = 2;
95     private static final int STATUS_DECLINED = 3;
96 
97     @IntDef(prefix = {"STATUS_"}, value = {
98             STATUS_NEW_REQUEST,
99             STATUS_PENDING,
100             STATUS_APPROVED,
101             STATUS_DECLINED,
102     })
103     @Retention(RetentionPolicy.SOURCE)
104     public @interface LogAccessRequestStatus {
105     }
106 
107     private final Context mContext;
108     private final Injector mInjector;
109     private final Supplier<Long> mClock;
110     private final BinderService mBinderService;
111     private final LogAccessDialogCallback mDialogCallback;
112     private final Handler mHandler;
113     private ActivityManagerInternal mActivityManagerInternal;
114     private ILogd mLogdService;
115 
116     private static final class LogAccessClient {
117         final int mUid;
118         @NonNull
119         final String mPackageName;
120 
LogAccessClient(int uid, @NonNull String packageName)121         LogAccessClient(int uid, @NonNull String packageName) {
122             mUid = uid;
123             mPackageName = packageName;
124         }
125 
126         @Override
equals(Object o)127         public boolean equals(Object o) {
128             if (this == o) return true;
129             if (!(o instanceof LogAccessClient)) return false;
130             LogAccessClient that = (LogAccessClient) o;
131             return mUid == that.mUid && Objects.equals(mPackageName, that.mPackageName);
132         }
133 
134         @Override
hashCode()135         public int hashCode() {
136             return Objects.hash(mUid, mPackageName);
137         }
138 
139         @Override
toString()140         public String toString() {
141             return "LogAccessClient{"
142                     + "mUid=" + mUid
143                     + ", mPackageName=" + mPackageName
144                     + '}';
145         }
146     }
147 
148     private static final class LogAccessRequest {
149         final int mUid;
150         final int mGid;
151         final int mPid;
152         final int mFd;
153 
LogAccessRequest(int uid, int gid, int pid, int fd)154         private LogAccessRequest(int uid, int gid, int pid, int fd) {
155             mUid = uid;
156             mGid = gid;
157             mPid = pid;
158             mFd = fd;
159         }
160 
161         @Override
equals(Object o)162         public boolean equals(Object o) {
163             if (this == o) return true;
164             if (!(o instanceof LogAccessRequest)) return false;
165             LogAccessRequest that = (LogAccessRequest) o;
166             return mUid == that.mUid && mGid == that.mGid && mPid == that.mPid && mFd == that.mFd;
167         }
168 
169         @Override
hashCode()170         public int hashCode() {
171             return Objects.hash(mUid, mGid, mPid, mFd);
172         }
173 
174         @Override
toString()175         public String toString() {
176             return "LogAccessRequest{"
177                     + "mUid=" + mUid
178                     + ", mGid=" + mGid
179                     + ", mPid=" + mPid
180                     + ", mFd=" + mFd
181                     + '}';
182         }
183     }
184 
185     private static final class LogAccessStatus {
186         @LogAccessRequestStatus
187         int mStatus = STATUS_NEW_REQUEST;
188         final List<LogAccessRequest> mPendingRequests = new ArrayList<>();
189     }
190 
191     private final Map<LogAccessClient, LogAccessStatus> mLogAccessStatus = new ArrayMap<>();
192     private final Map<LogAccessClient, Integer> mActiveLogAccessCount = new ArrayMap<>();
193 
194     private final class BinderService extends ILogcatManagerService.Stub {
195         @Override
startThread(int uid, int gid, int pid, int fd)196         public void startThread(int uid, int gid, int pid, int fd) {
197             final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd);
198             if (DEBUG) {
199                 Slog.d(TAG, "New log access request: " + logAccessRequest);
200             }
201             final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_REQUESTED, logAccessRequest);
202             mHandler.sendMessageAtTime(msg, mClock.get());
203         }
204 
205         @Override
finishThread(int uid, int gid, int pid, int fd)206         public void finishThread(int uid, int gid, int pid, int fd) {
207             final LogAccessRequest logAccessRequest = new LogAccessRequest(uid, gid, pid, fd);
208             if (DEBUG) {
209                 Slog.d(TAG, "Log access finished: " + logAccessRequest);
210             }
211             final Message msg = mHandler.obtainMessage(MSG_LOG_ACCESS_FINISHED, logAccessRequest);
212             mHandler.sendMessageAtTime(msg, mClock.get());
213         }
214     }
215 
216     final class LogAccessDialogCallback extends ILogAccessDialogCallback.Stub {
217         @Override
approveAccessForClient(int uid, @NonNull String packageName)218         public void approveAccessForClient(int uid, @NonNull String packageName) {
219             final LogAccessClient client = new LogAccessClient(uid, packageName);
220             if (DEBUG) {
221                 Slog.d(TAG, "Approving log access for client: " + client);
222             }
223             final Message msg = mHandler.obtainMessage(MSG_APPROVE_LOG_ACCESS, client);
224             mHandler.sendMessageAtTime(msg, mClock.get());
225         }
226 
227         @Override
declineAccessForClient(int uid, @NonNull String packageName)228         public void declineAccessForClient(int uid, @NonNull String packageName) {
229             final LogAccessClient client = new LogAccessClient(uid, packageName);
230             if (DEBUG) {
231                 Slog.d(TAG, "Declining log access for client: " + client);
232             }
233             final Message msg = mHandler.obtainMessage(MSG_DECLINE_LOG_ACCESS, client);
234             mHandler.sendMessageAtTime(msg, mClock.get());
235         }
236     }
237 
getLogdService()238     private ILogd getLogdService() {
239         if (mLogdService == null) {
240             mLogdService = mInjector.getLogdService();
241         }
242         return mLogdService;
243     }
244 
245     private static class LogAccessRequestHandler extends Handler {
246         private final LogcatManagerService mService;
247 
LogAccessRequestHandler(Looper looper, LogcatManagerService service)248         LogAccessRequestHandler(Looper looper, LogcatManagerService service) {
249             super(looper);
250             mService = service;
251         }
252 
253         @Override
handleMessage(Message msg)254         public void handleMessage(Message msg) {
255             switch (msg.what) {
256                 case MSG_LOG_ACCESS_REQUESTED: {
257                     LogAccessRequest request = (LogAccessRequest) msg.obj;
258                     mService.onLogAccessRequested(request);
259                     break;
260                 }
261                 case MSG_APPROVE_LOG_ACCESS: {
262                     LogAccessClient client = (LogAccessClient) msg.obj;
263                     mService.onAccessApprovedForClient(client);
264                     break;
265                 }
266                 case MSG_DECLINE_LOG_ACCESS: {
267                     LogAccessClient client = (LogAccessClient) msg.obj;
268                     mService.onAccessDeclinedForClient(client);
269                     break;
270                 }
271                 case MSG_LOG_ACCESS_FINISHED: {
272                     LogAccessRequest request = (LogAccessRequest) msg.obj;
273                     mService.onLogAccessFinished(request);
274                     break;
275                 }
276                 case MSG_PENDING_TIMEOUT: {
277                     LogAccessClient client = (LogAccessClient) msg.obj;
278                     mService.onPendingTimeoutExpired(client);
279                     break;
280                 }
281                 case MSG_LOG_ACCESS_STATUS_EXPIRED: {
282                     LogAccessClient client = (LogAccessClient) msg.obj;
283                     mService.onAccessStatusExpired(client);
284                     break;
285                 }
286             }
287         }
288     }
289 
290     static class Injector {
createClock()291         protected Supplier<Long> createClock() {
292             return SystemClock::uptimeMillis;
293         }
294 
getLooper()295         protected Looper getLooper() {
296             return Looper.getMainLooper();
297         }
298 
getLogdService()299         protected ILogd getLogdService() {
300             return ILogd.Stub.asInterface(ServiceManager.getService("logd"));
301         }
302     }
303 
LogcatManagerService(Context context)304     public LogcatManagerService(Context context) {
305         this(context, new Injector());
306     }
307 
LogcatManagerService(Context context, Injector injector)308     public LogcatManagerService(Context context, Injector injector) {
309         super(context);
310         mContext = context;
311         mInjector = injector;
312         mClock = injector.createClock();
313         mBinderService = new BinderService();
314         mDialogCallback = new LogAccessDialogCallback();
315         mHandler = new LogAccessRequestHandler(injector.getLooper(), this);
316     }
317 
318     @Override
onStart()319     public void onStart() {
320         try {
321             mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
322             publishBinderService("logcat", mBinderService);
323         } catch (Throwable t) {
324             Slog.e(TAG, "Could not start the LogcatManagerService.", t);
325         }
326     }
327 
328     @VisibleForTesting
getDialogCallback()329     LogAccessDialogCallback getDialogCallback() {
330         return mDialogCallback;
331     }
332 
333     @VisibleForTesting
getBinderService()334     ILogcatManagerService getBinderService() {
335         return mBinderService;
336     }
337 
338     @Nullable
getClientForRequest(LogAccessRequest request)339     private LogAccessClient getClientForRequest(LogAccessRequest request) {
340         final String packageName = getPackageName(request);
341         if (packageName == null) {
342             return null;
343         }
344 
345         return new LogAccessClient(request.mUid, packageName);
346     }
347 
348     /**
349      * Returns the package name.
350      * If we cannot retrieve the package name, it returns null and we decline the full device log
351      * access
352      */
getPackageName(LogAccessRequest request)353     private String getPackageName(LogAccessRequest request) {
354         PackageManager pm = mContext.getPackageManager();
355         if (pm == null) {
356             // Decline the logd access if PackageManager is null
357             Slog.e(TAG, "PackageManager is null, declining the logd access");
358             return null;
359         }
360 
361         String[] packageNames = pm.getPackagesForUid(request.mUid);
362         if (ArrayUtils.isEmpty(packageNames)) {
363             // Decline the logd access if the app name is unknown
364             Slog.e(TAG, "Unknown calling package name, declining the logd access");
365             return null;
366         }
367 
368         if (mActivityManagerInternal != null) {
369             int pid = request.mPid;
370             String packageName = mActivityManagerInternal.getPackageNameByPid(pid);
371             while ((packageName == null || !ArrayUtils.contains(packageNames, packageName))
372                     && pid != -1) {
373                 pid = getParentPid(pid);
374                 packageName = mActivityManagerInternal.getPackageNameByPid(pid);
375             }
376 
377             if (packageName != null && ArrayUtils.contains(packageNames, packageName)) {
378                 return packageName;
379             }
380         }
381 
382         Arrays.sort(packageNames);
383         String firstPackageName = packageNames[0];
384         if (firstPackageName == null || firstPackageName.isEmpty()) {
385             // Decline the logd access if the package name from uid is unknown
386             Slog.e(TAG, "Unknown calling package name, declining the logd access");
387             return null;
388         }
389 
390         return firstPackageName;
391     }
392 
onLogAccessRequested(LogAccessRequest request)393     void onLogAccessRequested(LogAccessRequest request) {
394         final LogAccessClient client = getClientForRequest(request);
395         if (client == null) {
396             declineRequest(request);
397             return;
398         }
399 
400         LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
401         if (logAccessStatus == null) {
402             logAccessStatus = new LogAccessStatus();
403             mLogAccessStatus.put(client, logAccessStatus);
404         }
405 
406         switch (logAccessStatus.mStatus) {
407             case STATUS_NEW_REQUEST:
408                 logAccessStatus.mPendingRequests.add(request);
409                 processNewLogAccessRequest(client);
410                 break;
411             case STATUS_PENDING:
412                 logAccessStatus.mPendingRequests.add(request);
413                 return;
414             case STATUS_APPROVED:
415                 approveRequest(client, request);
416                 break;
417             case STATUS_DECLINED:
418                 declineRequest(request);
419                 break;
420         }
421     }
422 
shouldShowConfirmationDialog(LogAccessClient client)423     private boolean shouldShowConfirmationDialog(LogAccessClient client) {
424         // If the process is foreground, show a dialog for user consent
425         final int procState = mActivityManagerInternal.getUidProcessState(client.mUid);
426         return procState == ActivityManager.PROCESS_STATE_TOP;
427     }
428 
processNewLogAccessRequest(LogAccessClient client)429     private void processNewLogAccessRequest(LogAccessClient client) {
430         boolean isInstrumented = mActivityManagerInternal.getInstrumentationSourceUid(client.mUid)
431                 != android.os.Process.INVALID_UID;
432         // The instrumented apks only run for testing, so we don't check user permission.
433         if (isInstrumented) {
434             onAccessApprovedForClient(client);
435             return;
436         }
437 
438         if (!shouldShowConfirmationDialog(client)) {
439             onAccessDeclinedForClient(client);
440             return;
441         }
442 
443         final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
444         logAccessStatus.mStatus = STATUS_PENDING;
445 
446         mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client),
447                 mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS);
448         final Intent mIntent = createIntent(client);
449         mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
450         mIntent.setComponent(new ComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME));
451         mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM);
452     }
453 
onAccessApprovedForClient(LogAccessClient client)454     void onAccessApprovedForClient(LogAccessClient client) {
455         scheduleStatusExpiry(client);
456 
457         LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
458         if (logAccessStatus != null) {
459             for (LogAccessRequest request : logAccessStatus.mPendingRequests) {
460                 approveRequest(client, request);
461             }
462             logAccessStatus.mStatus = STATUS_APPROVED;
463             logAccessStatus.mPendingRequests.clear();
464         }
465     }
466 
onAccessDeclinedForClient(LogAccessClient client)467     void onAccessDeclinedForClient(LogAccessClient client) {
468         scheduleStatusExpiry(client);
469 
470         LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
471         if (logAccessStatus != null) {
472             for (LogAccessRequest request : logAccessStatus.mPendingRequests) {
473                 declineRequest(request);
474             }
475             logAccessStatus.mStatus = STATUS_DECLINED;
476             logAccessStatus.mPendingRequests.clear();
477         }
478     }
479 
scheduleStatusExpiry(LogAccessClient client)480     private void scheduleStatusExpiry(LogAccessClient client) {
481         mHandler.removeMessages(MSG_PENDING_TIMEOUT, client);
482         mHandler.removeMessages(MSG_LOG_ACCESS_STATUS_EXPIRED, client);
483         mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_LOG_ACCESS_STATUS_EXPIRED, client),
484                 mClock.get() + STATUS_EXPIRATION_TIMEOUT_MILLIS);
485     }
486 
onPendingTimeoutExpired(LogAccessClient client)487     void onPendingTimeoutExpired(LogAccessClient client) {
488         final LogAccessStatus logAccessStatus = mLogAccessStatus.get(client);
489         if (logAccessStatus != null && logAccessStatus.mStatus == STATUS_PENDING) {
490             onAccessDeclinedForClient(client);
491         }
492     }
493 
onAccessStatusExpired(LogAccessClient client)494     void onAccessStatusExpired(LogAccessClient client) {
495         if (DEBUG) {
496             Slog.d(TAG, "Log access status expired for " + client);
497         }
498         mLogAccessStatus.remove(client);
499     }
500 
onLogAccessFinished(LogAccessRequest request)501     void onLogAccessFinished(LogAccessRequest request) {
502         final LogAccessClient client = getClientForRequest(request);
503         final int activeCount = mActiveLogAccessCount.getOrDefault(client, 1) - 1;
504 
505         if (activeCount == 0) {
506             mActiveLogAccessCount.remove(client);
507             if (DEBUG) {
508                 Slog.d(TAG, "Client is no longer accessing logs: " + client);
509             }
510             // TODO This will be used to notify the AppOpsManager that the logd data access
511             // is finished.
512         } else {
513             mActiveLogAccessCount.put(client, activeCount);
514         }
515     }
516 
approveRequest(LogAccessClient client, LogAccessRequest request)517     private void approveRequest(LogAccessClient client, LogAccessRequest request) {
518         if (DEBUG) {
519             Slog.d(TAG, "Approving log access: " + request);
520         }
521         try {
522             try {
523                 getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd);
524             } catch (DeadObjectException e) {
525                 // This can happen if logd restarts, so force getting a new connection
526                 // to logd and try once more.
527                 Slog.w(TAG, "Logd connection no longer valid while approving, trying once more.");
528                 mLogdService = null;
529                 getLogdService().approve(request.mUid, request.mGid, request.mPid, request.mFd);
530             }
531             Integer activeCount = mActiveLogAccessCount.getOrDefault(client, 0);
532             mActiveLogAccessCount.put(client, activeCount + 1);
533         } catch (RemoteException e) {
534             Slog.e(TAG, "Fails to call remote functions", e);
535         }
536     }
537 
declineRequest(LogAccessRequest request)538     private void declineRequest(LogAccessRequest request) {
539         if (DEBUG) {
540             Slog.d(TAG, "Declining log access: " + request);
541         }
542         try {
543             try {
544                 getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd);
545             } catch (DeadObjectException e) {
546                 // This can happen if logd restarts, so force getting a new connection
547                 // to logd and try once more.
548                 Slog.w(TAG, "Logd connection no longer valid while declining, trying once more.");
549                 mLogdService = null;
550                 getLogdService().decline(request.mUid, request.mGid, request.mPid, request.mFd);
551             }
552         } catch (RemoteException e) {
553             Slog.e(TAG, "Fails to call remote functions", e);
554         }
555     }
556 
557     /**
558      * Create the Intent for LogAccessDialogActivity.
559      */
createIntent(LogAccessClient client)560     public Intent createIntent(LogAccessClient client) {
561         final Intent intent = new Intent();
562 
563         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
564 
565         intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName);
566         intent.putExtra(Intent.EXTRA_UID, client.mUid);
567         intent.putExtra(EXTRA_CALLBACK, mDialogCallback.asBinder());
568 
569         return intent;
570     }
571 }
572