Эмулятор – это приложение, способное имитировать запуск программы, предназначенной для одной платформы, на другой. Примером эмулятора является GPCS4, предназначенный для запуска игр для PS4 на PC. Недавно состоялся первый релиз GPCS4, и мы решили проверить этот проект. Давайте посмотрим, какие ошибки удалось найти PVS-Studio в исходном коде этого эмулятора.
О проекте
GPCS4 – это эмулятор PlayStation 4, написанный на C и C++.
Изначально целью автора проекта было исследовать архитектуру PS4. Однако проект быстро развивался, и уже в начале 2020 года разработчикам GPCS4 удалось запустить на своём эмуляторе игру We are Doomed. Это был первый успешный стабильный запуск на PC игры для PS4. Впрочем, сам игровой процесс пока что был далёк от идеала: игра тормозила, картинка дёргалась. Тем не менее, основной разработчик проекта полон энтузиазма и продолжает совершенствовать эмулятор.
В конце апреля 2022 года состоялся первый релиз GPCS4. Я скачал и проверил версию 0.1.0 проекта. К слову, на момент публикации статьи уже вышел релиз 0.2.1 – проект развивается очень быстро. Давайте перейдём к ошибкам и недочётам, которые удалось найти анализатору PVS-Studio в первом релизе проекта GPCS4.
Потерянный break
V796 [CWE-484] It is possible that ‘break’ statement is missing in switch statement. AudioOut.cpp 137
В данном фрагменте кода для случая SCE_AUDIO_OUT_PARAM_FORMAT_FLOAT_MONO потерян break. Из-за этого количество каналов будет выставлено неправильно.
Проверка указателя после использования
V595 The ‘m_moduleData’ pointer was utilized before it was verified against nullptr. Check lines: 49, 53. ELFMapper.cpp 49
В приведённом фрагменте указатель m_moduleData сначала разыменовывается, а затем в цикле do-while сравнивается с nullptr.
Внимательные читатели могут возразить: «Может быть, на вход функции всегда приходит валидный указатель? А потом в цикле do-while этот указатель модифицируется и может стать нулевым? Тогда и ошибки тут нет». В данном случае это не так. Во-первых, из-за условия while (false) выполнится ровно одна итерация цикла. Во-вторых, указатель m_moduleData не модифицируется.
Другое возражение может заключаться в том, что взятие ссылки – это безопасно. Ведь использоваться эта ссылка будет, только если указатель валидный. Но нет, такой код содержит неопределённое поведение. Это ошибка. Скорее всего, нужно сделать проверку указателя до его первого разыменования:
Повторное присвоение
V519 [CWE-563] The ‘* memoryType’ variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 54, 55. sce_kernel_memory.cpp 55
Как можно догадаться по названию LOG_SCE_DUMMY_IMPL, реализация метода sceKernelGetDirectMemoryType ещё будет меняться. И всё же, два присваивания по одному и тому же адресу memoryType выглядят странно. Возможно, так получилось в результате неудачного слияния кода.
Переполнение буфера
V512 [CWE-119] A call of the ‘memset’ function will lead to overflow of the buffer ‘param->reserved’. sce_gnm_draw.cpp 420
V531 [CWE-131] It is odd that a sizeof() operator is multiplied by sizeof(). sce_gnm_draw.cpp 420
Иногда бывает, что на одну и ту же строчку кода указывают сразу несколько диагностик PVS-Studio. Так получилось и в этом примере. В данном фрагменте кода в функцию memset передаётся неправильное значение третьим аргументом. Выражение sizeof(param->reserved) уже вернёт размер массива param->reserved. Умножение на sizeof(uint32_t) увеличит это значение в 4 раза, и значение получится неправильным. Поэтому в результате вызова memset произойдёт выход за границу массива param->reserved. Нужно убрать лишнее умножение:
Всего анализатор обнаружил 20 подобных переполнений, приведу ещё один пример:
V512 [CWE-119] A call of the ‘memset’ function will lead to overflow of the buffer ‘initParam->reserved’. sce_gnm_dispatch.cpp 16
В этом фрагменте кода происходит выход за границу массива initParam->reserved.
Учимся считать до семи, или ещё одно переполнение буфера
V557 [CWE-787] Array overrun is possible. The ‘dynamicStateCount ++’ index is pointing beyond array bound. VltGraphics.cpp 157
Анализатор предупреждает, что может произойти переполнение массива dynamicStates. В данном фрагменте кода есть 4 проверки:
- if (state.useDynamicDepthBias())
- if (state.useDynamicDepthBounds())
- if (state.useDynamicBlendConstants())
- if (state.useDynamicStencilRef())
Каждая из них – это проверка одного из независимых флагов. Например, проверка if (state.useDynamicDepthBias()):
Получается, все эти 4 проверки могут быть истинными одновременно. Тогда выполнится 7 строк вида ‘dynamicStates[dynamicStateCount++] = ….’. На седьмой такой строке произойдёт обращение к dynamicStates[6]. Это выход за границу массива.
Для исправления нужно выделить память на 7 элементов:
Неправильное использование флага
V547 [CWE-570] Expression ‘nOldFlag & VMPF_NOACCESS’ is always false. PlatMemory.cpp 22
Функция GetProtectFlag конвертирует флаг с правами доступа к файлу из одного формата в другой. Однако делает это некорректно. Программист не учёл, что значение VMPF_NOACCESS равно нулю. Из-за этого условие if (nOldFlag & VMPF_NOACCESS) всегда ложное и функция никогда не вернёт значение PAGE_NOACCESS.
Кроме того, функция GetProtectFlag неправильно конвертирует не только флаг VMPF_NOACCESS, но и другие. Например, флаг VMPF_CPU_EXEC будет сконвертирован во флаг PAGE_EXECUTE_READWRITE.
Когда я думал, как это исправить, первой мыслью было написать что-то в таком роде:
Однако в данном случае такой подход не работает. Дело в том, что PAGE_NOACCESS, PAGE_READONLY и остальные – это Windows-флаги со своей спецификой. Например, среди них нет флага PAGE_WRITE. Считается, что если есть права на запись, то как минимум есть права еще и на чтение. По тем же причинам нет флага PAGE_EXECUTE_WRITE.
Кроме того, побитовое «ИЛИ» двух Windows-флагов не даёт в результате флаг, соответствующий сумме прав: PAGE_READONLY | PAGE_EXECUTE != PAGE_EXECUTE_READ. Поэтому нужно перебирать все возможные комбинации флагов:
Лишняя проверка
V547 [CWE-571] Expression ‘retAddress’ is always true. Memory.cpp 373
В этом фрагменте кода указатель retAddress проверяется дважды. Сначала делается проверка if (!retAddress). Если указатель нулевой, то выполнение перейдёт к следующей итерации цикла while. Иначе указатель retAddress ненулевой. Поэтому вторая проверка if (retAddress) всегда истинная, и её можно убрать.
Ещё одно всегда истинное условие
V547 [CWE-571] Expression ‘pipeConfig == kPipeConfigP16’ is always true. GnmDepthRenderTarget.h 170
В данном фрагменте кода анализатор нашёл всегда истинное условие if (pipeConfig == kPipeConfigP16). Давайте разберёмся, почему это так.
Если вызов функции getPipeConfig вернул значение, не равное kPipeConfigP16, то первое условие будет верным и выполнение программы не достигнет проверки if (pipeConfig == kPipeConfigP16).
Получается, что вторая проверка этой переменной либо не выполняется, либо всегда истинна. Но не стоит спешить и убирать её. Возможно, первое условие было добавлено временно и будет убрано в дальнейшем.
Ошибка копипасты
V517 [CWE-570] The use of ‘if (A) {…} else if (A) {…}’ pattern was detected. There is a probability of logical error presence. Check lines: 469, 475. GnmGpuAddress.cpp 469
Не обошлось и без ошибок копипасты. В данном фрагменте кода дважды написана одна и та же проверка newArrayMode == Gnm::kArrayMode2dTiledThin.
Сложно сказать, как именно нужно это поправить. Скорее всего, вторая проверка должна быть несколько другой. А может быть, она лишняя, и её можно убрать.
Почему лучше избегать сложных выражений?
V732 [CWE-480] Unary minus operator does not modify a bool type value. Consider using the ‘!’ operator. GnmRenderTarget.h 237
Похоже, что программист ожидал следующего поведения при вычислении выражения:
- пусть переменная type меньше 7;
- тогда выражение type < 7 равно true;
- затем к true применяется унарный минус, получается -1;
- значение -1 приводится к unsigned char, получается 0b1111’1111.
Однако на самом деле происходит следующее:
- пусть переменная type меньше 7;
- тогда выражение type < 7 равно true;
- затем к true применяется унарный минус, получается 1;
- значение 1 приводится к unsigned char, получается 0b0000’0001.
Впрочем, следующая операция & 1 приводит к одному и тому же результату. По этой счастливой случайности, код целиком работает так, как ожидает программист. Тем не менее, стоит поправить этот код. Попробуем понять, какое значение присваивается переменной v3 в зависимости от значения type.
Первый случай: переменная type больше или равна 7.
- Тогда выражение type < 7 равно false.
- К false применяется унарный минус, получается false.
- False приводится к unsigned char, получается 0b0000’0000.
- Побитовое «И» с 0 всегда даёт 0, поэтому в результате получаем 0.
Второй случай: переменная type меньше 7.
- Как уже выяснили раньше, выражение (uint8_t) — (type < 7) равно 1.
- В данном случае есть смысл вычислять выражение 0x43u >> type.
- Для удобства запишем бинарное представление числа 0x43 = 0b0100’0011.
- Нас интересует только младший бит, потому что к результату выражения 0x43u >> type применится побитовое «И» с 1.
- Если type равен 0, 1 или 6, то младший бит будет равен 1, и результат всего выражения будет 1. Во всех других случаях получится 0.
Итого, если type равен 0, 1 или 6, то в переменную v3 будет записано значение 1, а во всех остальных случаях – значение 0. Стоит заменить сложное выражение на более простое и понятное (type == 0) || (type == 1) || (type == 6). Предлагаю следующий код:
Здесь я также заменил числа 0, 1 и 6 на соответствующие именованные значения перечисления и записал подвыражения в табличном виде.
Краевой случай в операторе перемещения
V794 The assignment operator should be protected from the case of ‘this == &other’. VltShader.cpp 39
Если при вызове такого оператора окажется, что ‘this == &other’, то все поля текущего объекта будут очищены и данные потеряются. Такое поведение некорректно, нужно добавить проверку. Исправленный код:
Повторное присвоение как повод для рефакторинга
V1048 [CWE-1164] The ‘retVal’ variable was assigned the same value. Module.cpp 129
В данном фрагменте кода есть повторное присваивание значения true в переменную retVal. Давайте разберемся, почему так происходит. Для этого рассмотрим все возможные модификации переменной retVal до присваивания, указанного анализатором.
- Переменная retVal инициализируется значением false.
- Если вызов функции isEncodedSymbol вернул false, то переменной retVal присваивается значение true и прерывается цикл do-while.
- Переменной retVal присваивается результат вызова функции decodeSymbol. После этого если retVal == false, то цикл do-while прерывается.
- То же самое происходит и с двумя вызовами функции getModNameFromId. Если любой из вызовов вернёт false, то цикл do-while прерывается.
Заметим, что если цикл do-while был досрочно прерван, то и указанное анализатором присваивание не выполнится. Это значит, подозрительное присваивание retVal == true будет выполнено, только если все рассмотренные выше вызовы функций вернули true. Поэтому переменная retVal уже равна true, и присваивание не имеет смысла.
А зачем вообще использовать конструкцию ‘do … while(false)’? Дело в том, что такая конструкция позволяет сделать ранний выход из функции с одним return. К функциям с одним return, в свою очередь, с большей вероятностью будет применена NRVO – named return value optimization. Эта оптимизация компилятора позволяет избежать лишнего копирования или перемещения возвращаемого объекта, конструируя его сразу на месте вызова функции. В данном случае функция возвращает легковесный тип bool, поэтому выигрыш от NRVO будет незначительным. Кроме того, современные компиляторы умеют применять NRVO и к функциям с несколькими return, если во всех return возвращается один и тот же объект.
Метод getSymbolInfo не содержит ошибки и работает так, как задумал программист. Однако стоит провести рефакторинг метода getSymbolInfo и убрать цикл do-while вместе с переменной retVal. Предлагаю следующий код:
Здесь я сделал следующее:
- убрал цикл do-while и лишнюю переменную retVal;
- каждую проверку переменной retVal заменил на проверку результата вызова соответствующей функции;
- каждый break цикла do-while заменил на возврат нужного значения true / false. Какое именно значение вернуть, знаем из анализа переменной retVal, который провели ранее.
На мой взгляд, такой код проще читать и поддерживать.
Заключение
Конечно, это не все ошибки и недочёты, которые мы нашли в GPCS4. Некоторые случаи было довольно сложно описать, поэтому я не включил их в статью.
Мы желаем разработчикам GPCS4 успехов в дальнейшем развитии эмулятора и рекомендуем проверить текущую версию проекта анализатором PVS-Studio. Для этого достаточно скачать дистрибутив анализатора и запросить бесплатную лицензию для Open Source проектов. Если вас заинтересовал статический анализ в целом и PVS-Studio в частности – самое время попробовать. Вы можете проверить GPCS4 вслед за нами, а можете проверить свой собственный проект:) Спасибо за внимание!
Статья опубликована с разрешения автора. Ссылка на оригинал статьи.