Merge "Biometric Scheduler Watchdog"

This commit is contained in:
Diya Bera
2022-10-21 17:00:45 +00:00
committed by Android (Google) Code Review
16 changed files with 360 additions and 27 deletions

View File

@@ -768,6 +768,20 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan
}
}
/**
* Schedules a watchdog.
*
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
public void scheduleWatchdog() {
try {
mService.scheduleWatchdog();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void cancelEnrollment(long requestId) {
if (mService != null) {
try {

View File

@@ -172,4 +172,9 @@ interface IFaceService {
// Registers BiometricStateListener.
void registerBiometricStateListener(IBiometricStateListener listener);
// Internal operation used to clear face biometric scheduler.
// Ensures that the scheduler is not stuck.
@EnforcePermission("USE_BIOMETRIC_INTERNAL")
void scheduleWatchdog();
}

View File

@@ -1124,6 +1124,20 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing
return BIOMETRIC_LOCKOUT_NONE;
}
/**
* Schedules a watchdog.
*
* @hide
*/
@RequiresPermission(USE_BIOMETRIC_INTERNAL)
public void scheduleWatchdog() {
try {
mService.scheduleWatchdog();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* @hide
*/

View File

@@ -208,4 +208,9 @@ interface IFingerprintService {
// Sends a power button pressed event to all listeners.
@EnforcePermission("USE_BIOMETRIC_INTERNAL")
oneway void onPowerPressed();
// Internal operation used to clear fingerprint biometric scheduler.
// Ensures that the scheduler is not stuck.
@EnforcePermission("USE_BIOMETRIC_INTERNAL")
void scheduleWatchdog();
}

View File

@@ -106,7 +106,6 @@ import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
@@ -3796,4 +3795,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
}
mListenModels.print(pw);
}
/**
* Schedules a watchdog for the face and fingerprint BiometricScheduler.
* Cancels all operations in the scheduler if it is hung for 10 seconds.
*/
public void startBiometricWatchdog() {
if (mFaceManager != null) {
mFaceManager.scheduleWatchdog();
}
if (mFpm != null) {
mFpm.scheduleWatchdog();
}
}
}

View File

@@ -751,6 +751,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (DEBUG) Log.d(TAG, "keyguardGone");
mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
mKeyguardDisplayManager.hide();
mUpdateMonitor.startBiometricWatchdog();
Trace.endSection();
}

View File

@@ -21,6 +21,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.biometrics.BiometricConstants;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
@@ -293,4 +294,30 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient {
+ ", requestId=" + getRequestId()
+ ", userId=" + getTargetUserId() + "}";
}
/**
* Cancels this ClientMonitor
*/
public void cancel() {
cancelWithoutStarting(mCallback);
}
/**
* Cancels this ClientMonitor without starting
* @param callback
*/
public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) {
Slog.d(TAG, "cancelWithoutStarting: " + this);
final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
try {
ClientMonitorCallbackConverter listener = getListener();
if (listener != null) {
listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
}
} catch (RemoteException e) {
Slog.w(TAG, "Failed to invoke sendError", e);
}
callback.onClientFinished(this, true /* success */);
}
}

View File

@@ -543,4 +543,37 @@ public class BiometricScheduler {
mPendingOperations.clear();
mCurrentOperation = null;
}
/**
* Marks all pending operations as canceling and cancels the current
* operation.
*/
private void clearScheduler() {
if (mCurrentOperation == null) {
return;
}
for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
Slog.d(getTag(), "[Watchdog cancelling pending] "
+ pendingOperation.getClientMonitor());
pendingOperation.markCanceling();
}
Slog.d(getTag(), "[Watchdog cancelling current] "
+ mCurrentOperation.getClientMonitor());
mCurrentOperation.cancel(mHandler, getInternalCallback());
}
/**
* Start the timeout for the watchdog.
*/
public void startWatchdog() {
if (mCurrentOperation == null) {
return;
}
final BiometricSchedulerOperation mOperation = mCurrentOperation;
mHandler.postDelayed(() -> {
if (mOperation == mCurrentOperation) {
clearScheduler();
}
}, 10000);
}
}

View File

@@ -267,7 +267,7 @@ public class BiometricSchedulerOperation {
/** Flags this operation as canceled, if possible, but does not cancel it until started. */
public boolean markCanceling() {
if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) {
if (mState == STATE_WAITING_IN_QUEUE) {
mState = STATE_WAITING_IN_QUEUE_CANCELING;
return true;
}
@@ -287,10 +287,6 @@ public class BiometricSchedulerOperation {
}
final int currentState = mState;
if (!isInterruptable()) {
Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this);
return;
}
if (currentState == STATE_STARTED_CANCELING) {
Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this);
return;
@@ -301,10 +297,10 @@ public class BiometricSchedulerOperation {
|| currentState == STATE_WAITING_IN_QUEUE_CANCELING
|| currentState == STATE_WAITING_FOR_COOKIE) {
Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor);
((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback));
mClientMonitor.cancelWithoutStarting(getWrappedCallback(callback));
} else {
Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor);
((Interruptable) mClientMonitor).cancel();
mClientMonitor.cancel();
}
// forcibly finish this client if the HAL does not acknowledge within the timeout

View File

@@ -183,6 +183,18 @@ public class FaceService extends SystemService {
receiver, opPackageName, disabledFeatures, previewSurface, debugConsent);
}
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
@Override
public void scheduleWatchdog() {
final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
if (provider == null) {
Slog.w(TAG, "Null provider for scheduling watchdog");
return;
}
provider.second.scheduleWatchdog(provider.first);
}
@android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
@Override // Binder call
public long enrollRemotely(int userId, final IBinder token, final byte[] hardwareAuthToken,

View File

@@ -128,4 +128,10 @@ public interface ServiceProvider extends BiometricServiceProvider<FaceSensorProp
@NonNull String opPackageName);
void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args);
/**
* Schedules watchdog for canceling hung operations
* @param sensorId sensor ID of the associated operation
*/
default void scheduleWatchdog(int sensorId) {}
}

View File

@@ -52,6 +52,7 @@ import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.InvalidationRequesterClient;
@@ -661,4 +662,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
void setTestHalEnabled(boolean enabled) {
mTestHalEnabled = enabled;
}
@Override
public void scheduleWatchdog(int sensorId) {
Slog.d(getTag(), "Starting watchdog for face");
final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
if (biometricScheduler == null) {
return;
}
biometricScheduler.startWatchdog();
}
}

View File

@@ -879,6 +879,18 @@ public class FingerprintService extends SystemService {
provider.onPowerPressed();
}
}
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
@Override
public void scheduleWatchdog() {
final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
if (provider == null) {
Slog.w(TAG, "Null provider for scheduling watchdog");
return;
}
provider.second.scheduleWatchdog(provider.first);
}
};
public FingerprintService(Context context) {

View File

@@ -140,4 +140,10 @@ public interface ServiceProvider extends
@NonNull
ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
@NonNull String opPackageName);
/**
* Schedules watchdog for canceling hung operations
* @param sensorId sensor ID of the associated operation
*/
default void scheduleWatchdog(int sensorId) {}
}

View File

@@ -59,6 +59,7 @@ import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -779,4 +780,14 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
}
return null;
}
@Override
public void scheduleWatchdog(int sensorId) {
Slog.d(getTag(), "Starting watchdog for fingerprint");
final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
if (biometricScheduler == null) {
return;
}
biometricScheduler.startWatchdog();
}
}

View File

@@ -16,7 +16,7 @@
package com.android.server.biometrics.sensors;
import static android.testing.TestableLooper.RunWithLooper;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -24,8 +24,10 @@ import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -35,6 +37,7 @@ import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import android.content.Context;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.IBiometricService;
import android.os.Binder;
@@ -63,27 +66,25 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.function.Supplier;
@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class BiometricSchedulerTest {
private static final String TAG = "BiometricSchedulerTest";
private static final int TEST_SENSOR_ID = 1;
private static final int LOG_NUM_RECENT_OPERATIONS = 2;
private BiometricScheduler mScheduler;
private IBinder mToken;
@Mock
private IBiometricService mBiometricService;
@Rule
public final TestableContext mContext =
new TestableContext(InstrumentationRegistry.getContext(), null);
private BiometricScheduler mScheduler;
private IBinder mToken;
@Mock
private IBiometricService mBiometricService;
@Before
public void setUp() {
@@ -323,7 +324,7 @@ public class BiometricSchedulerTest {
client1.getCallback().onClientFinished(client1, true /* success */);
waitForIdle();
verify(callback).onError(anyInt(), anyInt(),
eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
eq(BIOMETRIC_ERROR_CANCELED),
eq(0) /* vendorCode */);
assertNull(mScheduler.getCurrentClient());
assertTrue(client1.isAlreadyDone());
@@ -484,7 +485,7 @@ public class BiometricSchedulerTest {
mScheduler.scheduleClientMonitor(interrupter);
waitForIdle();
verify((Interruptable) interruptableMonitor).cancel();
verify(interruptableMonitor).cancel();
mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */);
}
@@ -500,7 +501,7 @@ public class BiometricSchedulerTest {
mScheduler.scheduleClientMonitor(interrupter);
waitForIdle();
verify((Interruptable) interruptableMonitor, never()).cancel();
verify(interruptableMonitor, never()).cancel();
}
@Test
@@ -514,21 +515,180 @@ public class BiometricSchedulerTest {
assertTrue(client.mDestroyed);
}
@Test
public void testClearBiometricQueue_clearsHungAuthOperation() {
// Creating a hung client
final TestableLooper looper = TestableLooper.get(this);
final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
mScheduler.scheduleClientMonitor(client1, callback1);
waitForIdle();
mScheduler.startWatchdog();
waitForIdle();
//Checking client is hung
verify(callback1).onClientStarted(client1);
verify(callback1, never()).onClientFinished(any(), anyBoolean());
assertNotNull(mScheduler.mCurrentOperation);
assertEquals(0, mScheduler.getCurrentPendingCount());
looper.moveTimeForward(10000);
waitForIdle();
looper.moveTimeForward(3000);
waitForIdle();
// The hung client did not honor this operation, verify onError and authenticated
// were never called.
assertFalse(client1.mOnErrorCalled);
assertFalse(client1.mAuthenticateCalled);
verify(callback1).onClientFinished(client1, false /* success */);
assertNull(mScheduler.mCurrentOperation);
assertEquals(0, mScheduler.getCurrentPendingCount());
}
@Test
public void testAuthWorks_afterClearBiometricQueue() {
// Creating a hung client
final TestableLooper looper = TestableLooper.get(this);
final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
mScheduler.scheduleClientMonitor(client1, callback1);
assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
assertEquals(0, mScheduler.getCurrentPendingCount());
//Checking client is hung
waitForIdle();
verify(callback1, never()).onClientFinished(any(), anyBoolean());
//Start watchdog
mScheduler.startWatchdog();
waitForIdle();
// The watchdog should kick off the cancellation
looper.moveTimeForward(10000);
waitForIdle();
// After 10 seconds the HAL has 3 seconds to respond to a cancel
looper.moveTimeForward(3000);
waitForIdle();
// The hung client did not honor this operation, verify onError and authenticated
// were never called.
assertFalse(client1.mOnErrorCalled);
assertFalse(client1.mAuthenticateCalled);
verify(callback1).onClientFinished(client1, false /* success */);
assertEquals(0, mScheduler.getCurrentPendingCount());
assertNull(mScheduler.mCurrentOperation);
//Run additional auth client
final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext,
lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
mScheduler.scheduleClientMonitor(client2, callback2);
assertEquals(client2, mScheduler.mCurrentOperation.getClientMonitor());
assertEquals(0, mScheduler.getCurrentPendingCount());
//Start watchdog
mScheduler.startWatchdog();
waitForIdle();
mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
mock(ClientMonitorCallback.class));
waitForIdle();
//Ensure auth client passes
verify(callback2).onClientStarted(client2);
client2.getCallback().onClientFinished(client2, true);
waitForIdle();
looper.moveTimeForward(10000);
waitForIdle();
// After 10 seconds the HAL has 3 seconds to respond to a cancel
looper.moveTimeForward(3000);
waitForIdle();
//Asserting auth client passes
assertTrue(client2.isAlreadyDone());
assertNotNull(mScheduler.mCurrentOperation);
}
@Test
public void testClearBiometricQueue_doesNotClearOperationsWhenQueueNotStuck() {
//Creating clients
final TestableLooper looper = TestableLooper.get(this);
final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
mScheduler.scheduleClientMonitor(client1, callback1);
//Start watchdog
mScheduler.startWatchdog();
waitForIdle();
mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
mock(ClientMonitorCallback.class));
mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
mock(ClientMonitorCallback.class));
waitForIdle();
assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
assertEquals(2, mScheduler.getCurrentPendingCount());
verify(callback1, never()).onClientFinished(any(), anyBoolean());
verify(callback1).onClientStarted(client1);
//Client finishes successfully
client1.getCallback().onClientFinished(client1, true);
waitForIdle();
// The watchdog should kick off the cancellation
looper.moveTimeForward(10000);
waitForIdle();
// After 10 seconds the HAL has 3 seconds to respond to a cancel
looper.moveTimeForward(3000);
waitForIdle();
//Watchdog does not clear pending operations
assertEquals(1, mScheduler.getCurrentPendingCount());
assertNotNull(mScheduler.mCurrentOperation);
}
private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception {
return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
}
private void waitForIdle() {
TestableLooper.get(this).processAllMessages();
}
private static class TestAuthenticationClient extends AuthenticationClient<Object> {
boolean mStartedHal = false;
boolean mStoppedHal = false;
boolean mDestroyed = false;
int mNumCancels = 0;
boolean mAuthenticateCalled = false;
boolean mOnErrorCalled = false;
public TestAuthenticationClient(@NonNull Context context,
TestAuthenticationClient(@NonNull Context context,
@NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
@NonNull ClientMonitorCallbackConverter listener) {
this(context, lazyDaemon, token, listener, 1 /* cookie */);
}
TestAuthenticationClient(@NonNull Context context,
@NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
@NonNull ClientMonitorCallbackConverter listener, int cookie) {
super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
false /* restricted */, TAG, cookie, false /* requireConfirmation */,
TEST_SENSOR_ID, mock(BiometricLogger.class), mock(BiometricContext.class),
true /* isStrongBiometric */, null /* taskStackListener */,
mock(LockoutTracker.class), false /* isKeyguard */,
@@ -546,7 +706,19 @@ public class BiometricSchedulerTest {
}
@Override
protected void handleLifecycleAfterAuth(boolean authenticated) {}
protected void handleLifecycleAfterAuth(boolean authenticated) {
}
@Override
public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
boolean authenticated, ArrayList<Byte> hardwareAuthToken) {
mAuthenticateCalled = true;
}
@Override
protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) {
mOnErrorCalled = true;
}
@Override
public boolean wasUserDetected() {
@@ -651,8 +823,4 @@ public class BiometricSchedulerTest {
mDestroyed = true;
}
}
private void waitForIdle() {
TestableLooper.get(this).processAllMessages();
}
}