UserAwareBiometricScheduler lifecycle fix.

Fixed an issue where UserAwareBiometricScheduler queue would get stuck
indefinitely(causing face/fingerprint to fail until reboot).

Testing steps to reproduce

Setup.
* Have multiple users with multipler biometrics.

1. Add a property in a biometric HAL(Call it biometric A) to hang on auth operation
2. Set property to hang when authenticating(biometric A).
3. Unlock device with alternative biometric(biometric B)/pin/pattern/pass
4. Unset property to stop biometric(biometric A) from hanging
5. Rinse and repeat 1-4 until a hang occurs

Test: Manual (See above)
Test: atest UserAwareBiometricSchedulerTest
Fixes: 234625004
Change-Id: Ia6055ef3314ddaf986d5dfb4b642a5aba183053d
This commit is contained in:
Joshua McCloskey
2022-06-09 22:05:16 +00:00
committed by Joshua Mccloskey
parent cc5019f605
commit 30524ebfec
2 changed files with 66 additions and 4 deletions

View File

@@ -67,6 +67,14 @@ public class UserAwareBiometricScheduler extends BiometricScheduler {
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) {
mHandler.post(() -> {
Slog.d(getTag(), "[Client finished] " + clientMonitor + ", success: " + success);
// Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible
// for that the queue will wait indefinitely until the field is cleared.
if (clientMonitor instanceof StopUserClient<?> && !success) {
Slog.w(getTag(),
"StopUserClient failed(), is the HAL stuck? Clearing mStopUserClient");
mStopUserClient = null;
}
if (mCurrentOperation != null && mCurrentOperation.isFor(mOwner)) {
mCurrentOperation = null;
} else {
@@ -166,4 +174,9 @@ public class UserAwareBiometricScheduler extends BiometricScheduler {
mStopUserClient.onUserStopped();
mStopUserClient = null;
}
@VisibleForTesting
@Nullable public StopUserClient<?> getStopUserClient() {
return mStopUserClient;
}
}

View File

@@ -80,6 +80,11 @@ public class UserAwareBiometricSchedulerTest {
@Mock
private BiometricContext mBiometricContext;
private boolean mShouldFailStopUser = false;
private final StopUserClientShouldFail mStopUserClientShouldFail =
() -> {
return mShouldFailStopUser;
};
private final TestUserStartedCallback mUserStartedCallback = new TestUserStartedCallback();
private final TestUserStoppedCallback mUserStoppedCallback = new TestUserStoppedCallback();
private int mCurrentUserId = UserHandle.USER_NULL;
@@ -88,6 +93,7 @@ public class UserAwareBiometricSchedulerTest {
@Before
public void setUp() {
mShouldFailStopUser = false;
mHandler = new Handler(TestableLooper.get(this).getLooper());
mScheduler = new UserAwareBiometricScheduler(TAG,
mHandler,
@@ -101,7 +107,7 @@ public class UserAwareBiometricSchedulerTest {
public StopUserClient<?> getStopUserClient(int userId) {
return new TestStopUserClient(mContext, Object::new, mToken, userId,
TEST_SENSOR_ID, mBiometricLogger, mBiometricContext,
mUserStoppedCallback);
mUserStoppedCallback, mStopUserClientShouldFail);
}
@NonNull
@@ -240,6 +246,36 @@ public class UserAwareBiometricSchedulerTest {
assertThat(mUserStoppedCallback.mNumInvocations).isEqualTo(1);
}
@Test
public void testStartUser_failsClearsStopUserClient() {
// When a stop user client fails, check that mStopUserClient
// is set to null to prevent the scheduler from getting stuck.
BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
when(nextClient.getTargetUserId()).thenReturn(10);
mScheduler.scheduleClientMonitor(nextClient);
waitForIdle();
verify(nextClient).start(any());
// finish first operation
mScheduler.getInternalCallback().onClientFinished(nextClient, true /* success */);
waitForIdle();
// schedule second operation but swap out the current operation
// before it runs so that it's not current when it's completion callback runs
nextClient = mock(BaseClientMonitor.class);
when(nextClient.getTargetUserId()).thenReturn(11);
mUserStartedCallback.mAfterStart = () -> mScheduler.mCurrentOperation = null;
mShouldFailStopUser = true;
mScheduler.scheduleClientMonitor(nextClient);
waitForIdle();
assertThat(mUserStartedCallback.mStartedUsers).containsExactly(10, 11).inOrder();
assertThat(mUserStoppedCallback.mNumInvocations).isEqualTo(0);
assertThat(mScheduler.getStopUserClient()).isEqualTo(null);
}
private void waitForIdle() {
TestableLooper.get(this).processAllMessages();
}
@@ -268,13 +304,19 @@ public class UserAwareBiometricSchedulerTest {
}
}
private static class TestStopUserClient extends StopUserClient<Object> {
private interface StopUserClientShouldFail {
boolean shouldFail();
}
private class TestStopUserClient extends StopUserClient<Object> {
private StopUserClientShouldFail mShouldFailClient;
public TestStopUserClient(@NonNull Context context,
@NonNull Supplier<Object> lazyDaemon, @Nullable IBinder token, int userId,
int sensorId, @NonNull BiometricLogger logger,
@NonNull BiometricContext biometricContext,
@NonNull UserStoppedCallback callback) {
@NonNull UserStoppedCallback callback, StopUserClientShouldFail shouldFail) {
super(context, lazyDaemon, token, userId, sensorId, logger, biometricContext, callback);
mShouldFailClient = shouldFail;
}
@Override
@@ -285,7 +327,14 @@ public class UserAwareBiometricSchedulerTest {
@Override
public void start(@NonNull ClientMonitorCallback callback) {
super.start(callback);
onUserStopped();
if (mShouldFailClient.shouldFail()) {
getCallback().onClientFinished(this, false /* success */);
// When the above fails, it means that the HAL has died, in this case we
// need to ensure the UserSwitchCallback correctly returns the NULL user handle.
mCurrentUserId = UserHandle.USER_NULL;
} else {
onUserStopped();
}
}
@Override