Разработка
Pixel не дал позвонить 911: разбор ошибки в Android и Microsoft Teams
Что приложение Microsoft Teams делает не так и как это влияет на службу экстренных вызовов Android?
29 ноября пользователь Reddit KitchenPicture5849 опубликовал душераздирающую историю о том, как телефон Pixel не смог позвонить в службу экстренной помощи. Пользователь, которому принадлежит Google Pixel 3 под управлением Android 11, попытался набрать 911, чтобы получить медицинскую помощь для своей бабушки в связи с подозрением на инсульт. После совершения звонка телефон «завис», и что он «не мог ничего делать, кроме как переходить по приложениям с экстренным телефонным звонком, работающим в фоновом режиме».
К счастью, у бабушки пользователя была стационарная связь, поэтому пользователь в конце концов смог позвонить в службу 911, но, тем не менее, ситуация была для пользователя очень тревожной. Общественность явно согласилась с ним, поскольку это сообщение в /r/GooglePixel стало самым популярным постом в сабреддите за все время. После того, как о публикации стало известно, пользователь Reddit воспроизвел ошибку и заявил, что через пять минут после звонка не было ответа от служб экстренной помощи или доказательств того, что звонок состоялся — как из лога телефона на устройстве, так и оператора связи (Verizon).
Команда поддержки Google связалась с пользователем Reddit, чтобы диагностировать проблему, и 8 декабря представитель компании наконец публично ответил на этот вопрос. Согласно расследованию Google, проблема может возникнуть, когда пользователь установил приложение Microsoft Teams, но не вошел в систему, что может привести к «непреднамеренному взаимодействию между приложением Microsoft Teams и базовой операционной системой Android». Вот полное заявление Google, опубликованное в аккаунте PixelCommunity:
Понятно, что открытие того, что стороннее приложение, такое как Microsoft Teams, может вмешиваться в такой критический процесс, как набор экстренного номера, многих пользователей расстроило и смутило. Ни при каких обстоятельствах нельзя блокировать экстренный набор номера на устройстве. По закону операторы связи обязаны направлять экстренные вызовы в местные службы экстренной помощи, даже если у устройства нет SIM-карты в их сети.
Так что же на самом деле произошло в этом случае и что именно Google и Microsoft делают для смягчения этой проблемы? Благодаря моему другу Кубе Войцеховски (@Za_Raczke в Твиттере) я нашел подсказки, чтобы собрать вещи воедино. С его помощью я смог найти куски кода, которые вызывают эту проблему, и выяснил, что приложение Microsoft Teams делает не так и как это повлияло на службу экстренных вызовов Android.
Слишком много PhoneAccount
Проблема может быть прослежена до методов adjustAttemptsforEmergency и sortSimPhoneAccountsForEmergency в классе CreateConnectionProcessor. Метод adjustAttemptsforEmergency решает, какой экземпляр PhoneAccount должен обрабатывать экстренный вызов. Согласно документации Google, «приложения, которые могут совершать звонки и хотят, чтобы эти звонки были интегрированы в набор номера и пользовательский интерфейс во время звонка, должны создать экземпляр класса [PhoneAccount] и зарегистрировать его в системе с помощью TelecomManager». По сути, многие приложения Android с функцией телефонных звонков — будь то через стандартную платформу дозвона или настраиваемую службу, подключающуюся к удаленной конечной точке — создают свой экземпляр PhoneAccount. Сюда входит приложение Microsoft Teams, которое использует серверную часть Skype для голосовых вызовов.
Поскольку Microsoft Teams управляет своим собственным Connection вместо телефонного приложения по умолчанию, приложение регистрирует PhoneAccount с флагом CAPABILITY_SELF_MANAGED. Однако, поскольку приложение Teams не может обрабатывать экстренные вызовы, оно не регистрируется с флагом CAPABILITY_PLACE_EMERGENCY_CALLS. Важность этих двух констант станет ясна вскоре.
Если мы вернемся к классу adjustAttemptsForEmergency, первое, что мы заметим, это то, что класс получает список всех зарегистрированных экземпляров PhoneAccount.
Сюда входят экземпляры без определения CAPABILITY_PLACE_EMERGENCY_CALLS, в том числе экземпляры, созданные приложением Microsoft Teams. Следующий блок кода добавляет резервную учетную запись телефона для экстренных случаев на случай, если список экземпляров PhoneAccount пуст, поэтому мы можем двигаться дальше. Мы также можем пропустить следующий блок, который касается тестирования экстренных вызовов через службу тестового подключения.
Однако затем adjustAttemptsForEmergency пытается получить «предпочтительный для пользователя» экземпляр PhoneAccount, который, по мнению службы Telephony, должен обрабатывать экстренные вызовы.
Однако и на этом все не заканчивается, так как Android затем сортирует список PhoneAccount в случае, если существует несколько экземпляров с определенным CAPABILITY_PLACE_EMERGENCY_CALLS, и один из них имеет более высокий рейтинг, чем «предпочитаемый пользователем» PhoneAccount.
Возможно, вы уже увидели одну проблему — sortSimPhoneAccountsforEmergency вызывается со списком, который содержит все экземпляры PhoneAccount, даже те, у которых нет CAPABILITY_PLACE_EMERGENCY_CALLS. Microsoft Teams даже не должно быть в списке, и тем не менее, это так. Однако, если бы это была основная проблема в этом сбое, то каждый телефон Android с установленной Microsoft Teams не смог бы набрать 911, а это явно не так.
Однако внутри метода sortSimPhoneAccountsforEmergency есть код, который при определенных обстоятельствах может привести к целочисленной ошибке переполнения/потери значимости. Этот метод сортирует список PhoneAccount в возрастающем порядке, выполняя несколько сравнений между каждой учетной записью телефона в списке. Эти сравнения включают проверку того, какой PhoneAccount поддерживает CAPABILITY_SIM_SUBSCRIPTION, что является вышеупомянутой «предпочтительной для пользователя» учетной записью, и какая учетная запись связана с действительным идентификатором подписки и индексом слота SIM-карты. Если два сравниваемых экземпляра PhoneAccount одинаковы, метод затем упорядочит список по имени пакета, метке и, наконец, по хэш-коду. Даже если два сравниваемых экземпляра PhoneAccount имеют одинаковые возможности, имя пакета и метку, они могут иметь разные значения хэш-кода.
Проблема с этой строкой в том виде, в котором она написана, в том, что она может привести к целочисленному переполнению, если значение account1.hashCode() — account2.hashCode() меньше Integer.MIN_VALUE или больше Integer.MAX_VALUE. Обычно существует не так много PhoneAccount для сортировки, и метод обычно никогда не достигает этого блока кода, поскольку другие сравнения, вероятно, уже преуспевают в ранжировании двух экземпляров PhoneAccount, поэтому шансы добиться целочисленного переполнения/потери значимости в этом вычислении действительно очень малы.
Но что, если бы для этого метода были потенциально десятки повторяющихся экземпляров PhoneAccount? В этом случае вероятность получения целочисленного переполнения/потери значимости возрастает. Даже не анализируя декомпилированный код приложения Microsoft Teams, легко убедиться, что в приложении есть ошибка, которая приводит к избыточной регистрации экземпляров PhoneAccount. Каждый раз, когда приложение Microsoft Teams установлено, но пользователь не вошел в систему, каждый холодный запуск приложения приводит к созданию другого экземпляра PhoneAccount.
Я не знаю, в каком выпуске приложения Microsoft Teams впервые появилось такое поведение, но я могу подтвердить, что оно проявляется в версиях 1416/1.0.0.2021163901 и 1416/1.0.0.2021183702 приложения. Первый был загружен в APKMirror 28 октября 2021 года, примерно за месяц до того, как пользователь Reddit обнаружил ошибку. Это последняя версия в Play Store.
Если вы хотите воспроизвести это поведение самостоятельно, установите одну из упомянутых мной версий Teams и выполните следующую команду оболочки ADB, чтобы просмотреть список PhoneAccount:
dumpsys telecom | sed -n ‘/PhoneAccountRegistrar/,/Analytics/p’
Удаление, а затем повторная установка приложения приведет к удалению всех созданных им экземпляров PhoneAccount, что является одним из шагов по смягчению последствий, которые рекомендует Google.
Я также не знаю, как именно пользователь Reddit получил на своем устройстве столько экземпляров PhoneAccount, созданных Microsoft Teams. Я не так часто использую Microsoft Teams, но, судя по тому, что я читал в Интернете, были проблемы, когда приложение часто выводило пользователей из системы. Я также читал отчеты о том, что предприятия могут устанавливать политику для периодического выхода пользователя из системы по соображениям безопасности.
Череда досадных событий
После проверки декомпилированной версии приложения Microsoft Teams мы смогли определить, почему новый экземпляр PhoneAccount появляется каждый раз при перезапуске приложения. Мы обнаружили, что, когда пользователь не вошел в систему, новый, случайно сгенерированный UUID используется для создания экземпляра PhoneAccount, который добавляется в TelecomManager в Android. Это означает, что каждый раз при перезапуске или сбое приложения Teams создается новый UUID для пользователей, которые не вошли в систему, и, таким образом, новый PhoneAccount добавляется в TelecomManager в Android. Поскольку в Teams есть boot broadcast receiver, это также происходит при каждой перезагрузке телефона.
Соответствующий псевдокод:
String userId = MS_Teams_Current_User_ID; if (StringUtils.isEmptyOrWhiteSpace(userId)) { userId = UUID.randomUUID().toString(); } TelecomManager.registerPhoneAccount(PhoneAccount.builder(new PhoneAccountHandle(componentName, userId), applicationName).setCapabilities(capabilities).build());
Здесь userId + componentName составляют PhoneAccountHandle, который представляет собой уникальный идентификатор, используемый для сравнения разных PhoneAccount в AOSP. UserId используется в Teams для идентификации сеанса Teams и не используется явно за пределами приложения.
У пользователей, вошедших в приложение Teams, не будет дублирующих экземпляров PhoneAccount, поскольку userId будет таким же, как и уже зарегистрированный PhoneAccount. Это также объясняет, почему в случае, когда я установил и сразу вошел в Teams, все еще оставалось два экземпляра PhoneAccount, созданных Teams: один был построен со случайно сгенерированным UUID, а другой — с userId учетной записи Teams.
Вот трассировка стека, показывающая, как Teams вызывает метод registerPhoneAccount в PhoneAccountRegistrar при перезапуске приложения.
В любом случае, обилие повторяющихся экземпляров PhoneAccount повышает вероятность возникновения целочисленной ошибки переполнения/потери значимости, когда пользователь пытается совершить экстренный вызов. Я не могу определить, является ли целочисленная ошибка переполнения/потери тем этапом, на котором все идет не так, без изучения того, как эта ошибка влияет на остальную часть стека телефонии. Это требует дальнейшего анализа.
Два патча, и еще больше впереди
Я вполне уверен, что именно из-за этой ошибки целочисленного переполнения/потери значимости все начинает идти не так, поскольку это именно то, на что обращено внимание при изменении кода. Это изменение кода, озаглавленное «Исправить целочисленное переполнение/недостаточное заполнение, вызванное сортировкой дублирующихся телефонных учетных записей во время попытки экстренного вызова», что интересно, было представлено инженером Samsung еще 25 ноября, за четыре дня до того, как появилось обсуждение в Reddit. К сожалению, связанный отчет об ошибке не является общедоступным, поэтому мы не можем понять, как инженеры обнаружили эту проблему. Таким образом, мы не знаем наверняка, был ли отчет об ошибке вызван той же проблемой Microsoft Teams с дублированием PhoneAccount, но это было бы чертовски интересным совпадением, если бы это было не так.
В любом случае это исправление кода решает проблему целочисленного переполнения/потери значимости простым способом. Оно заменяет простое вычитание, используемое для сравнения хэш-кодов, с Integer.compare, которое возвращает только -1, 0 или 1. Стоит отметить, что приложение Teams регистрирует PhoneAccounts только в том случае, если оно работает на уровне API 28+ (Android 9 Pie и более поздние версии), но изменение кода, которое реализовало метод sortSimPhoneAccountsforEmergency в том виде, в котором он был изначально написан, было передано в AOSP в середине 2019 года и было включено в ветку android10-release, поэтому эта ошибка затрагивает только устройства Android под управлением Android 10+.
Однако это не единственное изменение кода, направленное на решение этой проблемы. Другое на днях было внесено в общедоступную главную ветку AOSP. По словам сотрудника Google, отправившего патч, это изменение кода «представляет собой cherry-pick от внутреннего геррита». Он добавляет новые строки в метод adjustAttemptsForEmergency, чтобы отфильтровать экземпляры PhoneAccount, которые являются самоуправляемыми, т. е с определенным CAPABILITY_SELF_MANAGED. Это предотвратит передачу экземпляров PhoneAccount, подобных созданным Microsoft Teams, методу sortSimPhoneAccountsforEmergency, что разумно, поскольку только телефонная служба по умолчанию должна обрабатывать экстренные вызовы.
Google заявляет, что компания представит «обновление платформы Android для экосистемы Android 4 января». Служба Telecomm не входит ни в один из 23 модулей Mainline, перечисленных Google, поэтому я не думаю, что она будет исправлена через Google Play System Update. Таким образом, исправления могут быть доставлены на устройства как часть исправления безопасности 2022–01–01, которое должно начать развертываться на устройствах, когда 3 января 2022 года, в первый понедельник месяца, будет опубликован бюллетень по безопасности Android за январь 2022 года.
В любом случае скоро будет обновление, и не только от Google. Microsoft развернет новую версию Teams, которая, вероятно, решит проблему с созданием дублирующихся экземпляров PhoneAccount. Microsoft может устранить эту ошибку на своей стороне, создав UUID один раз и прочитав его из SharedPreferences.
Я не думаю, что большинству пользователей стоит опасаться этой ошибки, потому что для ее возникновения требуется очень специфический набор обстоятельств. И даже когда эти обстоятельства встречаются, это в основном неудача, если они срабатывают. Если вы обеспокоены этим, обязательно следуйте инструкциям Google в этом сообщении.