Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5fc69102 authored by Ahaan Ugale's avatar Ahaan Ugale
Browse files

Grant HotwordDetectionService RECORD_AUDIO and CAPTURE_AUDIO_HOTWORD.

HotwordDetectionService is an isolated service which ordinarily cannot
hold permissions. An isolated service has its own uid separate from the
owning package.
To allow it to access mic audio, we dynamically override the permissions
check based on the currently bound HotwordDetectionService.

Bug: 190011174
Test: manual - sample app can read audio (with a few other wip changes)
Test: atest CtsVoiceInteractionTestCases
Change-Id: I5abc809546184ef13cb9eb009f916afae6bdf1af
parent 1e70989c
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
@@ -16,9 +16,12 @@

package android.service.voice;

import android.annotation.Nullable;
import android.os.Bundle;
import android.os.IBinder;

import com.android.internal.annotations.Immutable;


/**
 * @hide
@@ -46,4 +49,38 @@ public abstract class VoiceInteractionManagerInternal {
     * Returns whether the given package is currently in an active session
     */
    public abstract boolean hasActiveSession(String packageName);

    /**
     * Gets the identity of the currently active HotwordDetectionService.
     *
     * @see HotwordDetectionServiceIdentity
     */
    @Nullable
    public abstract HotwordDetectionServiceIdentity getHotwordDetectionServiceIdentity();

    /**
     * Provides the uids of the currently active
     * {@link android.service.voice.HotwordDetectionService} and its owning package. The
     * HotwordDetectionService is an isolated service, so it has a separate uid.
     */
    @Immutable
    public static class HotwordDetectionServiceIdentity {
        private final int mIsolatedUid;
        private final int mOwnerUid;

        public HotwordDetectionServiceIdentity(int isolatedUid, int ownerUid) {
            mIsolatedUid = isolatedUid;
            mOwnerUid = ownerUid;
        }

        /** Gets the uid of the currently active isolated process hosting the service. */
        public int getIsolatedUid() {
            return mIsolatedUid;
        }

        /** Gets the uid of the package that provides the HotwordDetectionService. */
        public int getOwnerUid() {
            return mOwnerUid;
        }
    }
}
 No newline at end of file
+67 −25
Original line number Diff line number Diff line
@@ -17,7 +17,9 @@
package com.android.server.pm.permission;

import static android.Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY;
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_ERRORED;
@@ -148,6 +150,7 @@ import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.parsing.PackageInfoUtils;
import com.android.server.pm.parsing.pkg.AndroidPackage;
import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider;
import com.android.server.pm.permission.PermissionManagerServiceInternal.OnRuntimePermissionStateChangedListener;
import com.android.server.policy.PermissionPolicyInternal;
import com.android.server.policy.SoftRestrictedPermissionPolicy;
@@ -308,6 +311,9 @@ public class PermissionManagerService extends IPermissionManager.Stub {
    @NonNull
    private final OnPermissionChangeListeners mOnPermissionChangeListeners;

    @Nullable
    private HotwordDetectionServiceProvider mHotwordDetectionServiceProvider;

    // TODO: Take a look at the methods defined in the callback.
    // The callback was initially created to support the split between permission
    // manager and the package manager. However, it's started to be used for other
@@ -5200,6 +5206,16 @@ public class PermissionManagerService extends IPermissionManager.Stub {
        public int[] getGidsForUid(int uid) {
            return PermissionManagerService.this.getGidsForUid(uid);
        }

        @Override
        public void setHotwordDetectionServiceProvider(HotwordDetectionServiceProvider provider) {
            mHotwordDetectionServiceProvider = provider;
        }

        @Override
        public HotwordDetectionServiceProvider getHotwordDetectionServiceProvider() {
            return mHotwordDetectionServiceProvider;
        }
    }

    /**
@@ -5476,10 +5492,13 @@ public class PermissionManagerService extends IPermissionManager.Stub {

        private final @NonNull Context mContext;
        private final @NonNull AppOpsManager mAppOpsManager;
        private final @NonNull PermissionManagerServiceInternal mPermissionManagerServiceInternal;

        PermissionCheckerService(@NonNull Context context) {
            mContext = context;
            mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
            mPermissionManagerServiceInternal =
                    LocalServices.getService(PermissionManagerServiceInternal.class);
        }

        @Override
@@ -5492,8 +5511,9 @@ public class PermissionManagerService extends IPermissionManager.Stub {
            Objects.requireNonNull(attributionSourceState);
            final AttributionSource attributionSource = new AttributionSource(
                    attributionSourceState);
            final int result = checkPermission(mContext, permission, attributionSource, message,
                    forDataDelivery, startDataDelivery, fromDatasource, attributedOp);
            final int result = checkPermission(mContext, mPermissionManagerServiceInternal,
                    permission, attributionSource, message, forDataDelivery, startDataDelivery,
                    fromDatasource, attributedOp);
            // Finish any started op if some step in the attribution chain failed.
            if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED) {
                if (attributedOp == AppOpsManager.OP_NONE) {
@@ -5581,10 +5601,11 @@ public class PermissionManagerService extends IPermissionManager.Stub {
        }

        @PermissionCheckerManager.PermissionResult
        private static int checkPermission(@NonNull Context context, @NonNull String permission,
                @NonNull AttributionSource attributionSource, @Nullable String message,
                boolean forDataDelivery, boolean startDataDelivery, boolean fromDatasource,
                int attributedOp) {
        private static int checkPermission(@NonNull Context context,
                @NonNull PermissionManagerServiceInternal permissionManagerServiceInt,
                @NonNull String permission, @NonNull AttributionSource attributionSource,
                @Nullable String message, boolean forDataDelivery, boolean startDataDelivery,
                boolean fromDatasource, int attributedOp) {
            PermissionInfo permissionInfo = sPlatformPermissions.get(permission);

            if (permissionInfo == null) {
@@ -5601,22 +5622,25 @@ public class PermissionManagerService extends IPermissionManager.Stub {
            }

            if (permissionInfo.isAppOp()) {
                return checkAppOpPermission(context, permission, attributionSource, message,
                        forDataDelivery, fromDatasource);
                return checkAppOpPermission(context, permissionManagerServiceInt, permission,
                        attributionSource, message, forDataDelivery, fromDatasource);
            }
            if (permissionInfo.isRuntime()) {
                return checkRuntimePermission(context, permission, attributionSource, message,
                        forDataDelivery, startDataDelivery, fromDatasource, attributedOp);
                return checkRuntimePermission(context, permissionManagerServiceInt, permission,
                        attributionSource, message, forDataDelivery, startDataDelivery,
                        fromDatasource, attributedOp);
            }

            if (!fromDatasource && !checkPermission(context, permission, attributionSource.getUid(),
            if (!fromDatasource && !checkPermission(context, permissionManagerServiceInt,
                    permission, attributionSource.getUid(),
                    attributionSource.getRenouncedPermissions())) {
                return PermissionChecker.PERMISSION_HARD_DENIED;
            }

            if (attributionSource.getNext() != null) {
                return checkPermission(context, permission, attributionSource.getNext(), message,
                        forDataDelivery, startDataDelivery, /*fromDatasource*/ false, attributedOp);
                return checkPermission(context, permissionManagerServiceInt, permission,
                        attributionSource.getNext(), message, forDataDelivery, startDataDelivery,
                        /*fromDatasource*/ false, attributedOp);
            }

            return PermissionChecker.PERMISSION_GRANTED;
@@ -5624,6 +5648,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {

        @PermissionCheckerManager.PermissionResult
        private static int checkAppOpPermission(@NonNull Context context,
                @NonNull PermissionManagerServiceInternal permissionManagerServiceInt,
                @NonNull String permission, @NonNull AttributionSource attributionSource,
                @Nullable String message, boolean forDataDelivery, boolean fromDatasource) {
            final int op = AppOpsManager.permissionToOpCode(permission);
@@ -5667,13 +5692,13 @@ public class PermissionManagerService extends IPermissionManager.Stub {
                        return PermissionChecker.PERMISSION_HARD_DENIED;
                    }
                    case AppOpsManager.MODE_DEFAULT: {
                        if (!skipCurrentChecks && !checkPermission(context, permission,
                                attributionSource.getUid(), attributionSource
                                        .getRenouncedPermissions())) {
                        if (!skipCurrentChecks && !checkPermission(context,
                                permissionManagerServiceInt, permission, attributionSource.getUid(),
                                attributionSource.getRenouncedPermissions())) {
                            return PermissionChecker.PERMISSION_HARD_DENIED;
                        }
                        if (next != null && !checkPermission(context, permission,
                                next.getUid(), next.getRenouncedPermissions())) {
                        if (next != null && !checkPermission(context, permissionManagerServiceInt,
                                permission, next.getUid(), next.getRenouncedPermissions())) {
                            return PermissionChecker.PERMISSION_HARD_DENIED;
                        }
                    }
@@ -5688,6 +5713,7 @@ public class PermissionManagerService extends IPermissionManager.Stub {
        }

        private static int checkRuntimePermission(@NonNull Context context,
                @NonNull PermissionManagerServiceInternal permissionManagerServiceInt,
                @NonNull String permission, @NonNull AttributionSource attributionSource,
                @Nullable String message, boolean forDataDelivery, boolean startDataDelivery,
                boolean fromDatasource, int attributedOp) {
@@ -5712,13 +5738,13 @@ public class PermissionManagerService extends IPermissionManager.Stub {
                }

                // If we already checked the permission for this one, skip the work
                if (!skipCurrentChecks && !checkPermission(context, permission,
                        current.getUid(), current.getRenouncedPermissions())) {
                if (!skipCurrentChecks && !checkPermission(context, permissionManagerServiceInt,
                        permission, current.getUid(), current.getRenouncedPermissions())) {
                    return PermissionChecker.PERMISSION_HARD_DENIED;
                }

                if (next != null && !checkPermission(context, permission,
                        next.getUid(), next.getRenouncedPermissions())) {
                if (next != null && !checkPermission(context, permissionManagerServiceInt,
                        permission, next.getUid(), next.getRenouncedPermissions())) {
                    return PermissionChecker.PERMISSION_HARD_DENIED;
                }

@@ -5773,10 +5799,26 @@ public class PermissionManagerService extends IPermissionManager.Stub {
            }
        }

        private static boolean checkPermission(@NonNull Context context, @NonNull String permission,
                int uid, @NonNull Set<String> renouncedPermissions) {
            final boolean permissionGranted = context.checkPermission(permission, /*pid*/ -1,
        private static boolean checkPermission(@NonNull Context context,
                @NonNull PermissionManagerServiceInternal permissionManagerServiceInt,
                @NonNull String permission, int uid, @NonNull Set<String> renouncedPermissions) {
            boolean permissionGranted = context.checkPermission(permission, /*pid*/ -1,
                    uid) == PackageManager.PERMISSION_GRANTED;

            // Override certain permissions checks for the HotwordDetectionService. This service is
            // an isolated service, which ordinarily cannot hold permissions.
            // There's probably a cleaner, more generalizable way to do this. For now, this is
            // the only use case for this, so simply override here.
            if (!permissionGranted
                    && Process.isIsolated(uid) // simple check which fails-fast for the common case
                    && (permission.equals(RECORD_AUDIO)
                    || permission.equals(CAPTURE_AUDIO_HOTWORD))) {
                HotwordDetectionServiceProvider hotwordServiceProvider =
                        permissionManagerServiceInt.getHotwordDetectionServiceProvider();
                permissionGranted = hotwordServiceProvider != null
                        && uid == hotwordServiceProvider.getUid();
            }

            if (permissionGranted && renouncedPermissions.contains(permission)
                    && context.checkPermission(Manifest.permission.RENOUNCE_PERMISSIONS,
                    /*pid*/ -1, uid) == PackageManager.PERMISSION_GRANTED) {
+24 −0
Original line number Diff line number Diff line
@@ -428,4 +428,28 @@ public interface PermissionManagerServiceInternal extends PermissionManagerInter
            }
        }
    }

    /**
     * Sets the provider of the currently active HotwordDetectionService.
     *
     * @see HotwordDetectionServiceProvider
     */
    void setHotwordDetectionServiceProvider(@Nullable HotwordDetectionServiceProvider provider);

    /**
     * Gets the provider of the currently active HotwordDetectionService.
     *
     * @see HotwordDetectionServiceProvider
     */
    @Nullable
    HotwordDetectionServiceProvider getHotwordDetectionServiceProvider();

    /**
     * Provides the uid of the currently active
     * {@link android.service.voice.HotwordDetectionService}, which should be granted RECORD_AUDIO
     * and CAPTURE_AUDIO_HOTWORD permissions.
     */
    interface HotwordDetectionServiceProvider {
        int getUid();
    }
}
+33 −5
Original line number Diff line number Diff line
@@ -33,7 +33,10 @@ import android.content.pm.ResolveInfo;
import android.location.LocationManagerInternal;
import android.net.Uri;
import android.os.IBinder;
import android.os.Process;
import android.os.UserHandle;
import android.service.voice.VoiceInteractionManagerInternal;
import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -73,6 +76,9 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat
    @NonNull
    private final RoleManager mRoleManager;

    @NonNull
    private final VoiceInteractionManagerInternal mVoiceInteractionManagerInternal;

    /**
     * The locking policy around the location tags is a bit special. Since we want to
     * avoid grabbing the lock on every op note we are taking the approach where the
@@ -96,6 +102,8 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat
    public AppOpsPolicy(@NonNull Context context) {
        mContext = context;
        mRoleManager = mContext.getSystemService(RoleManager.class);
        mVoiceInteractionManagerInternal = LocalServices.getService(
                VoiceInteractionManagerInternal.class);

        final LocationManagerInternal locationManagerInternal = LocalServices.getService(
                LocationManagerInternal.class);
@@ -145,7 +153,7 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat
    public int checkOperation(int code, int uid, String packageName,
            @Nullable String attributionTag, boolean raw,
            QuintFunction<Integer, Integer, String, String, Boolean, Integer> superImpl) {
        return superImpl.apply(code, uid, packageName, attributionTag, raw);
        return superImpl.apply(code, resolveUid(code, uid), packageName, attributionTag, raw);
    }

    @Override
@@ -159,8 +167,8 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat
            @Nullable String attributionTag, boolean shouldCollectAsyncNotedOp, @Nullable
            String message, boolean shouldCollectMessage, @NonNull HeptFunction<Integer, Integer,
                    String, String, Boolean, String, Boolean, SyncNotedAppOp> superImpl) {
        return superImpl.apply(resolveDatasourceOp(code, uid, packageName, attributionTag), uid,
                packageName, attributionTag, shouldCollectAsyncNotedOp,
        return superImpl.apply(resolveDatasourceOp(code, uid, packageName, attributionTag),
                resolveUid(code, uid), packageName, attributionTag, shouldCollectAsyncNotedOp,
                message, shouldCollectMessage);
    }

@@ -185,8 +193,9 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat
                    String, Boolean, Boolean, String, Boolean, Integer, Integer,
            SyncNotedAppOp> superImpl) {
        return superImpl.apply(token, resolveDatasourceOp(code, uid, packageName, attributionTag),
                uid, packageName, attributionTag, startIfModeDefault, shouldCollectAsyncNotedOp,
                message, shouldCollectMessage, attributionFlags, attributionChainId);
                resolveUid(code, uid), packageName, attributionTag, startIfModeDefault,
                shouldCollectAsyncNotedOp, message, shouldCollectMessage, attributionFlags,
                attributionChainId);
    }

    @Override
@@ -387,4 +396,23 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat
        }
        return code;
    }

    private int resolveUid(int code, int uid) {
        // The HotwordDetectionService is an isolated service, which ordinarily cannot hold
        // permissions. So we allow it to assume the owning package identity for certain
        // operations.
        // Note: The package name coming from the audio server is already the one for the owning
        // package, so we don't need to modify it.
        if (Process.isIsolated(uid) // simple check which fails-fast for the common case
                && (code == AppOpsManager.OP_RECORD_AUDIO
                || code == AppOpsManager.OP_RECORD_AUDIO_HOTWORD)) {
            final HotwordDetectionServiceIdentity hotwordDetectionServiceIdentity =
                    mVoiceInteractionManagerInternal.getHotwordDetectionServiceIdentity();
            if (hotwordDetectionServiceIdentity != null
                    && uid == hotwordDetectionServiceIdentity.getIsolatedUid()) {
                uid = hotwordDetectionServiceIdentity.getOwnerUid();
            }
        }
        return uid;
    }
}
+22 −3
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
@@ -48,6 +49,7 @@ import android.service.voice.HotwordRejectedResult;
import android.service.voice.IDspHotwordDetectionCallback;
import android.service.voice.IHotwordDetectionService;
import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
import android.util.Pair;
import android.util.Slog;
import android.view.contentcapture.IContentCaptureManager;
@@ -56,6 +58,8 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ServiceConnector;
import com.android.server.LocalServices;
import com.android.server.pm.permission.PermissionManagerServiceInternal;

import java.io.Closeable;
import java.io.IOException;
@@ -94,20 +98,24 @@ final class HotwordDetectionConnection {
    private final AtomicBoolean mUpdateStateFinish = new AtomicBoolean(false);

    final Object mLock;
    final int mVoiceInteractionServiceUid;
    final ComponentName mDetectionComponentName;
    final int mUser;
    final Context mContext;
    final @NonNull ServiceConnector<IHotwordDetectionService> mRemoteHotwordDetectionService;
    boolean mBound;
    volatile HotwordDetectionServiceIdentity mIdentity;

    @GuardedBy("mLock")
    private ParcelFileDescriptor mCurrentAudioSink;

    HotwordDetectionConnection(Object lock, Context context, ComponentName serviceName,
            int userId, boolean bindInstantServiceAllowed, @Nullable PersistableBundle options,
            @Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) {
    HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid,
            ComponentName serviceName, int userId, boolean bindInstantServiceAllowed,
            @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory,
            IHotwordRecognitionStatusCallback callback) {
        mLock = lock;
        mContext = context;
        mVoiceInteractionServiceUid = voiceInteractionServiceUid;
        mDetectionComponentName = serviceName;
        mUser = userId;
        final Intent intent = new Intent(HotwordDetectionService.SERVICE_INTERFACE);
@@ -153,7 +161,15 @@ final class HotwordDetectionConnection {
                public void sendResult(Bundle bundle) throws RemoteException {
                    if (DEBUG) {
                        Slog.d(TAG, "updateState finish");
                    }
                        Slog.d(TAG, "updating hotword UID " + Binder.getCallingUid());
                    }
                    // TODO: Do this earlier than this callback and have the provider point to the
                    // current state stored in VoiceInteractionManagerServiceImpl.
                    final int uid = Binder.getCallingUid();
                    LocalServices.getService(PermissionManagerServiceInternal.class)
                            .setHotwordDetectionServiceProvider(() -> uid);
                    mIdentity =
                            new HotwordDetectionServiceIdentity(uid, mVoiceInteractionServiceUid);
                    future.complete(null);
                    try {
                        if (mUpdateStateFinish.getAndSet(true)) {
@@ -224,6 +240,9 @@ final class HotwordDetectionConnection {
        if (mBound) {
            mRemoteHotwordDetectionService.unbind();
            mBound = false;
            LocalServices.getService(PermissionManagerServiceInternal.class)
                    .setHotwordDetectionServiceProvider(null);
            mIdentity = null;
        }
    }

Loading