Сегодня ночью ShadPS4 присоединится к охоте: проверяем самый популярный эмулятор PS4
20 августа исполнилось 136 лет Говарду Филлипсу Лавкрафту, человеку, создавшему свой собственный жанр "лавкрафтовских ужасов". Многие пытались копировать или развивать его идеи, но получалось только у считанных единиц.
Удачным примером можно назвать популярную игру Bloodborne, мир которой вдохновлён творчеством Лавкрафта, но имеет свою уникальную идентичность. Долгое время геймеры лишь мечтали запустить её на ПК, и вот, наконец, это стало возможным. А всё благодаря нашему сегодняшнему герою — эмулятору ShadPS4.
Добро пожаловать на ПК, добрый охотник. Желаешь проверить на баги эмулятор ShadPS4?
В нашей проверке участвовал код непосредственно ShadPS4 без всяческих сторонних библиотек. За основу взят коммит 24e68ba main-ветки. Ошибки брались из High и Medium уровней группы общего назначения. Комментарии вида `//!` добавлены мной.
Приведённые ссылки на конкретные коммиты добавлены для упрощения верифицирования и исправления найденных ошибок и не имеют цели скомпрометировать или каким-то образом принизить их авторов. Всем контрибуторам проекта большая личная благодарность от фаната Лавкрафта и игр Миядзаки.
В отличие от моей прошлой проверки llvm здесь не будет примеров "кривого мёржа", наложения двух реализаций и прочих артефактов совместной разработки. Охотник никогда не одинок, но всё же их всегда немного. Начнём же охоту, о, я не могу дождаться... хи-хи...
Результаты проверки
Фрагмент N1. Fear your blindness
Предупреждение PVS-Studio: V557 Array overrun is possible. The `3` index is pointing beyond array bound. input_handler.h 397
Выход за границу буфера в чистейшем виде. Весь файл добавили в огромном коммите с горой разных фич и, скорее всего, глаз автора просто замылился.
Так вышло, что пока писалась статья, эту ошибку нашли и исправили, заменив `keys[3] = k1` на `keys[2] = k1`. Но полтора года зловреду всё же удалось прожить в проекте.
Фрагмент N2. No mercy for "Liar"
Предупреждение PVS-Studio: V547 Expression `m_streams.size() >= 0` is always true. Unsigned type value is always >= 0. avplayer_source.cpp 132
Давайте попробуем разобраться, присутствует ли здесь ошибка. Сразу замечу, что `m_streams` всегда был вектором, а значит его `size` всегда был беззнаковым.
Фрагмент менялся в этом коммите: функция `AvPlayerSource::FindStreams` заменила собой `AvPlayerSource::HasStreams`, утащив также часть кода из `AvPlayerSource::Init`.
Если копнуть ещё глубже, то у `AvPlayerSource::HasStreams` тоже был предшественник:
`m_avformat_context` — умный указатель на тип `AVFormatContext`, а поле `nb_streams` объявлено так:
Как видим, в прошлом действия были менее тавтологичны.
Если же поискать использование `AvPlayerSource::FindStreams`, то оно присутствует только в функции `AvPlayerState::ProcessEvent`:
Как итог, всё указывает на то, что разработчик сделал опечатку `>=` при создании функции `AvPlayerSource::HasStreams`, а потом скопипастил её в функцию `AvPlayerSource::FindStreams`.
Фрагмент N3. Let us cleanse these foul streets
Предупреждения PVS-Studio:
V568 It's odd that the argument of sizeof() operator is the `sizeof (int) * 512` expression. aio.cpp 318
V1086 A call of the `memset` function will lead to underflow of the buffer `id_state`. aio.cpp 318
Файл добавлен целиком.
Выражение `sizeof(int) * 512` имеет тип `size_t`, соответственно `sizeof(sizeof(int) * MAX_QUEUE))` будет равно размеру `size_t`. Поскольку его размер обычно 4 или 8 байт, обнулится в итоге только часть массива. С большой долей уверенности можно предположить, что внешний `sizeof` лишний и предполагалось заполнить нулями весь буфер. Однако найти примеры использования с конкретными данными сложно, т. к. читаются элементы массива только в семействе функций `sceKernelAio***`, которые сохраняются и вызывается как указатели. Надеюсь, "охотники" этого мира обратят внимание и либо очистят всё целиком, либо вставят чёткий знак, почему это не нужно.
Фрагмент N4. Beware of "Attack from behind"
Предупреждение PVS-Studio: V595 The `chunkIds` pointer was utilized before it was verified against nullptr. Check lines: 173, 179.
Макрос `LOG_DEBUG` определён в файле `log.h`,и после подстановки код выглядит так:
Как видим, при `chunkIds == nullptr` получаем классическое UB, от которого может спасти только отсутствие в таком случае логера для `Common::Log::Class::Lib_PlayGo`. Но, увы, все логеры создаются в функции main на старте приложения и живут вместе с ним.
Несмотря на то, что логгируется только debug информация, вызов функции `logger::log`, а с ним и потенциальное UB, произойдёт и в release — излишние записи отсекаются уже внутри.
Сам макрос — немного разный по форме, но с разыменованием во всех версиях, — был всегда, а вот код после него добавлен позже.
Судя по тексту коммита, разработчик расширил поддержку системы PlayGo, той самой, что позволяет начать охоту, не дожидаясь скачивания игры целиком. Функция перестала быть просто заглушкой, получив и необходимые проверки, а безусловное разыменование `chunkIds`, видимо, выпало из поля зрения автора. Возможных исправлений тут множество, и они довольно очевидны: всё зависит от стилистики и формата логов, принятых в проекте. Я бы предложил такой вариант:
Фрагмент N5. Oh, Yourself, please, carry on in my stead
Предупреждение PVS-Studio: V570 The same value is assigned twice to the `inComment` variable. text_editor.cpp 2109
Добавлено единым огромным коммитом.
Очевидно, что присваивание `inComment` самой себе здесь бессмысленно, но как это произошло?
Как мне кажется, автор написал декларацию `inComment`, затем скопировал декларатор с инициализатором из верхней строки целиком и всё вставил ниже, не заметив подвоха. Поэтому повторное присваивание здесь просто лишнее. Другой возможный вариант: вместо `=` должно было быть `==` или `!=`, но это не особо вяжется с логикой алгоритма. Хотя, конечно, последнее слово может сказать только сам разработчик или его "боевые товарищи".
Фрагмент N6. Treat "The unseen" with care
Предупреждение PVS-Studio: V634 The priority of the `*` operation is higher than that of the `<<` operation. It`s possible that parentheses should be used in the expression. liverpool_to_vk.cpp 759
Строка:
Равносильна:
Что из-за приоритета операций будет интерпретировано как:
Что равно 1024.
Судя по функции `GetSurfaceFormatTableIndex`, ожидается, что максимальный индекс в `result` как раз меньше 1024:
Что вполне согласуется с документацией AMD из комментария: 64 формата данных и 14 способов их интерпретации шейдерами.
Но смущает `* 1`, как будто хотели написать:
Что тоже равно 1024, однако забыли про приоритет операции и при этом всё равно получили правильный результат.
Все функции и константы добавлены единым коммитом, поэтому проблемы кооперации исключаем.
В итоге ошибки вроде бы и нет, но код получился хрупким, и какой-нибудь неискушённый разработчик в будущем может здесь легко оступиться и пасть перед ужасами приоритета операций.
Фрагмент N7. "Strong foe" waits ahead but "don't give up"
Предупреждение PVS-Studio: V579 The ZydisDecoderDecodeFull function receives the pointer and its size as arguments. It is possibly a mistake. Inspect the third argument. decoder.cpp 29
Ранее файл назывался `Disassembler.cpp`:
Для функции `ZydisDecoderDecodeFull` есть поясняющий комментарий:
Довольно подозрительно выглядит передача в функцию указателя на некий буфер и размера указателя в качестве длины этого буфера.
Путь параметра `length` довольно извилистый, но в какой-то момент он сохраняется в поле структуры:
И далее используется для ограничения парсинга инструкции, например:
Ранее вместо `sizeof(code)` была константа:
Что соответствует максимальной длине инструкции в байтах на х86.
Чтобы текущий код был корректным, должна быть гарантия, что нет инструкций длиннее, чем размер указателя. И мне такую найтись не удалось.
Также в пользу ошибки косвенно говорит пример в проекте Zydis — фреймворке для дисассемблирования, используемом в эмуляторе:
Здесь тоже используется `sizeof`, но для массива с известными во время компиляции границами он действительно даст размер буфера в байтах.
Также можно заметить почти такой же код в истории коммитов и в самом эмуляторе. Его выпилили в 2023 году:
Сразу бросается в глаза точно такая же структура `sizeof(***) - offset` для `length`, что и в Zydis, и такое же взятие размера указателя, что и в текущем коде, что выглядит как чёткий сигнал о реальности ошибки.
Вдобавок в другом, уже почившем эмуляторе PS4, тоже использовался Zydis и функция `ZydisDecoderDecodeFull` с длиной инструкции, равной 15:
Но для полной определённости, конечно же, необходимы пояснения от опытных разработчиков проекта.
Фрагмент N8. Despicable "Metamorphosis" therefore fear "Moon"
Предупреждение PVS-Studio: V610 Undefined behavior. Check the shift operator `<<`. The right operand (`(64 - systemLang - 1)` = [16..63]) is greater than or equal to the length in bits of the promoted left operand. playgo.cpp 233
Функция добавлена целиком вместе с нижележащим куском `scePlayGoInitialize`.
С учётом того, что сейчас эмулятор собирается для архитектуры x86-64, где `int` занимает 4 байта, то при значениях `systemLang` меньше 32 получим переполнение и UB.
Но происходит ли такое в действительности? Отследим происхождение параметра `systemLang`.
Наша функция вызывается только из `scePlayGoInitialize`:
А `system_lang` туда попадает после функции `sceSystemServiceParamGetInt`:
Как видим, небольшие значения `system_lang` вполне возможны. Кроме того, они чаще всего и будут! Ведь по документации каждому поддерживаемому языку соответствует свой номер от 0 до 47. Например, японский — 0 (ожидаемо от Sony), английский — 1, а русский — 8. Так что любая игра, скажем, на японском, гарантировано вызовет переполнение.
Заодно теперь стали понятны условия выше. Вот тут, например, если sdk довольно старой версии, , то вместо канадского французского используется обычный:
Кроме вышеперечисленного, дополнительным аргументом в пользу ошибки служит реализация этой же функции в другой имплементации PlayGo, которая практически идентична, но лишена переполнения:
Фрагмент N9. Time for "Hidden path"
Предупреждение PVS-Studio: V796 It is possible that `break` statement is missing in switch statement. hull_shader_transform.cpp 247
Ранее в этой ветке был безусловный выход:
В тексте коммита говорится:
Ignore when a user contributes to the wrong operand of an LDS inst, for example the data operand of WriteShared* instead of the address operand. This can mistakenly happen due to phi nodes.
Из "ignore when a user contributes to the wrong operand" можно сделать вывод, что нежелательно продолжать выполнение, когда `use.operand != 0`, и здесь действительно пропущен `break`. Если же наши предположения неверны, то разработчику стоило воспользоваться блокнотом и оставить подсказку в виде `[[fallthrough]]` своим менее благословлённым озарением коллегам.
Фрагмент N10. Treat "Lever" with care or you must accept "Ignoring"
Предупреждение PVS-Studio: V547 Expression `compare < 0` is always false. Unsigned type value is never < 0. np_common.cpp 49
Весь файл был добавлен целиком (ранее он располагался в другой папке).
Напомню семантику функции `strncmp`:
The sign of the result is the sign of the difference between the values of the first pair of characters (both interpreted as unsigned char) that differ in the arrays being compared.
Похоже, автор просто опечатался в типе, написав беззнаковый вместо знакового.
Прервав кооперацию в одном мире, заглянем ненадолго в другой. Кроме основного репозитория у проекта ShadPS4 существует довольно популярный форк, заточенный конкретно под Bloodborne: diegolix29/shadPS4.
Он ушёл от upstream на пару тысяч коммитов и, помимо общих болячек, имеет и свои уникальные.
Фрагмент N11. The sky and the cosmos are one
Предупреждение PVS-Studio: V590 Consider inspecting the 'deltaTime <= 0.0f || deltaTime < 0.0001f' expression. The expression is excessive or contains a misprint. controller.cpp 201
Кусок с `MAX_DELTA_TIME` и `orientation` добавлен отдельно.
На первый взгляд кажется, будто вместо `deltaTime < 0.0001f` должно было `deltaTime > 0.0001f`, и это косвенно подтверждается тем, что в upstream репозитории есть похожий фрагмент:
Но тогда бессмысленным становится кусок с `MAX_DELTA_TIME`. Что ж, придётся углубиться в лор вычисления ориентации контролёра.
Сперва посмотрим, что же такое `delta_time`. И, неожиданно, это дельта по времени с момента последнего обновления, например:
Поискав, удалось понять, что вариант из upstream с константой `1.0f` служит защитой от пролагов и пауз эмулятора, при которых в результате получалась бы ерунда.
В форке поступили немного иначе и в таком случае просто обновляют ориентацию, но с фиксированной дельтой `0.1f`. Отсюда вытекает, что проблемное место:
видимо, служит другой цели — параноидальной защите от невалидных данных, поскольку и при отрицательных, и при крайне малых дельтах мы получаем некорректные значения.
В итоге со значительной долей уверенности можно сказать, что левая часть проверки `deltaTime <= 0.0f` — лишняя.
Фрагмент N12. Time for "jump"
Предупреждение PVS-Studio: V1082 Function marked as 'noreturn' may return control. This will result in undefined behavior. ir_emitter.cpp 15
Как видим, `UNREACHABLE_MSG` действительно делал функцию `noreturn`:
Закомментировали эти строки с сообщением "SOTC hacks" (вероятно, SOTC — Shadow Of The Colossus).
В upstream этого изменения нет, соответственно, и анализатору негде было срабатывать.
Здесь особо вопросов нет: очевидно, атрибут просто забыли удалить. Согласно стандарту C++, это — UB, и `[[noreturn]]` всё же нужно убрать.
Фрагмент N13. Treat hunter with care and don't be fooled
Предупреждение PVS-Studio: V501 There are identical sub-expressions 'serial == "CUSA03014"' to the left and to the right of the '||' operator. storage_image_sync.cpp 42
Как видим, сравнение с `CUSA03014` дублируется, и обе копии добавили одновременно.
В upstream вообще нет этого файла, поэтому там нет и срабатывания.
Символично, что `CUSA ID` — это пятизначный серийный номер игр для PS4 и наш дубль `CUSA03014` соответствует "Bloodborne: The Old Hunters Edition".
Также можно заметить, что `CUSA003027` визуально выделяется большей длиной, ломая стройный ряд линий, и не имеет смысла, т. к. шестизначен.
Но просто ли удалить излишние сравнения или заменить ID на другие, может сказать только сам разработчик.
Фрагмент N14. Remember key but fear Malformed thing
Предупреждение PVS-Studio: V766 An item with the same key 'vk::Format::eR8G8B8A8Srgb' has already been added. host_compatibility.cpp 202
Ключ `eR8G8B8A8Srgb` повторяется дважды с разными значениями: `CompatibilityClass::_8BIT` и `CompatibilityClass::_32BIT`. По стандарту C++ не специфицировано, какое из них останется в итоге.
Т. е. значение `8BIT` не совпадает с образцом, да и просто, похоже, лишнее, с учётом того, что в референсной мапе между значениями `VK_FORMAT_R8_SRGB` и `VK_FORMAT_R8_SSCALED` ничего нет.В upstream есть эта мапа, но, естественно, нет второго варианта с 8BIT, который в форке был добавлен позже отдельно от других.
Какой из двух вариантов в итоге нужно оставить, могут уверенно сказать только разработчики.
Заключение
Вот и конец ночи охоты, и пора увидеть свет дня. Пусть и наделённые силой C++, но разработчики остаются людьми и тоже могут совершать ошибки, пропускать опечатки и утрачивать бдительность. В помощь им создано множество инструментов, и в нашей мастерской в том числе. Например, бесплатная версия PVS-Studio для open source проектов.
Если у вас не open source проект, то вы все равно можете попробовать анализатор бесплатно. Вы ведь в курсе, да?