Ошибки в Java по ГОСТу? Да, в этой статье мы вам расскажем, что это за ГОСТ, какие категории ошибок в нём существуют, какие из них относятся к Java, и даже покажем примеры из реальных проектов.

Предисловие про ГОСТ 71207-2024
В первую очередь, что это за ГОСТ? Возьмём описание из него же:
Настоящий стандарт устанавливает общие требования к внедрению и выполнению статического анализа ПО. Настоящий стандарт устанавливает требования к методам статического анализа, инструментам анализа (статическим анализаторам) и к специалистам, участвующим в анализе.
То есть ГОСТ предъявляет требования к тому, что должен уметь используемый статический анализатор и как им правильно пользоваться.
А кому это нужно? ГОСТ 71207-2024 применяется совместно с ГОСТ 56939-2024 "Разработка безопасного программного обеспечения". В последнем отдельной мерой приводится использование статического анализатора в процессе разработки, но нет чётких требований и критериев. Непонятно, что считать подходящим инструментом. Если инструмент ищет простые ошибки по регулярным выражениям — он тоже считается статическим анализатором? Также в нём нет регламентации того, что считается регулярным использованием. Получается, если я запускаю статический анализатор раз в два года, это считается регулярным использованием? На все эти вопросы (и не только) отвечает ГОСТ 71207-2024. Так что нужно это тем, кто разрабатывает ПО в соответствии с ГОСТ 56939-2024.
Однако этим область применения стандарта не ограничивается. Он может быть полезен тем, кто руководствуется "Методикой выявления уязвимостей и НДВ в программном обеспечении", а также просто решил реализовать в компании более зрелые процессы разработки и тестирования.
Предметом статьи будут примеры того, что в рассматриваемом ГОСТе именуется "Классификация критических ошибок, находимых статическими анализаторами" на языке Java. Причём примеры из реальных проектов. Но сначала давайте определимся с тем, что такое "критическая ошибка".
Подробнее про критические ошибки
Критическая ошибка по ГОСТ 71207-2024 — "ошибка, которая может привести к нарушению безопасности обрабатываемой информации". Синонимом является термин "потенциальная уязвимость".
Но ведь разработка ПО может вестись на разных языках программирования. И если в рамках разработки языка программирования N одно действо будет являться ошибкой, то для другого это может не быть чем-то серьёзным. ГОСТ частично учитывает это. Критические ошибки разделяются по следующим категориям в зависимости от типа языка:
для компилируемых языков (п. 6.3);
для интерпретируемых языков (п. 6.4);
дополнительные категории ошибок для C/C++ (п. 6.5).
Дополнительные категории ошибок для C/C++ появились из-за небольшого изменения в самом ГОСТе, где указано, что для конкретного языка программирования впоследствии может быть добавлено больше критических ошибок. И для C/C++ дополнительные критические ошибки указаны в самом ГОСТе.
А что по Java? Java находится в компилируемом лагере (несмотря на то, что является компилируемо-интерпретируемым языком). С C#, кстати, та же история.
Какие для Java критические ошибки?
Как я и сказал выше, Java изначально принадлежит к компилируемому лагерю. Для таких языков набор критических ошибок следующий (п. 6.4):
ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.);
ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел;
ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти);
ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.);
ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).
И для Java он какое-то время оставался таковым. Вплоть до "Домашнего задания".
"Домашнее задание" — один из этапов испытания статических анализаторов под патронажем ФСТЭК России. Цель — оценить то, как участвующие анализаторы соответствуют ГОСТ 71207-2024 и дать критику самому этапу испытаний, чтобы впоследствии сформировать публичные методы и критерии оценки. Дополнительные ссылки:
Дмитрий Пономарев. Испытания статических анализаторов.
Дмитрий Пономарев. Итоги этапа "Домашнее задание" испытаний статических анализаторов под патронажем ФСТЭК России.
Комментарий Андрея Карпова касательно PVS-Studio в своём телеграм-канале.
Одна из частей "Домашнего задания" — на большом наборе синтетических тестов проверялось умение инструмента находить критические ошибки. Причём находить так, чтобы количество True Positive срабатываний было > 50 %, а количество False Positive < 50 %.
True Positive и False Positive?
True Positive — "правильное" срабатывание анализатора. Ситуация, когда статический анализатор действительно нашёл проблемное место и указал на него.
False Positive — "ложное" срабатывание анализатора. Ситуация, при которой статический анализатор неверно обозначил фрагмент кода как проблемный. Ознакомиться подробнее можно здесь.
В ходе обсуждения участниками первого этапа испытаний было принято решение добавить языку Java дополнительные категории критических ошибок:
ошибки разыменования нулевой ссылки;
ошибки деления на ноль;
ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений;
ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти).
Первое, что бросается в глаза тому, кто знаком с Java — некоторые формулировки не соответствуют специфике этого языка. Отчасти именно для их расшифровки на Java язык и написана эта статья. Но рассуждать абстрактно не так интересно, как увидеть всё своими глазами — то есть, как критические ошибки выглядят в реальных проектах.
Конечно, перечисленные типы ошибок для Java программ не так разрушительны, как в случае C и C++. Например, разыменование нулевой ссылки и деление на ноль, приведут не к неопределённому поведению программы (undefined behavior), а к генерации исключения. Однако этот сбой может привести к остановке нормального функционирования программы, а значит такой код потенциально уязвим к DoS-атаке уровня приложения.
Примечание Андрея Карпова о целевом проценте ложных срабатываний
В ГОСТ 71207-2024 в пункте 8.4, где говорится об оценке качества статического анализатора, упомянуты следующие проценты:
Для данных типов ошибок на наборе тестов, отвечающих требованиям 10.4, статический анализатор должен обеспечивать достижение следующих показателей:
а) долю ошибок первого рода (ложноположительных срабатываний) — не более 50 %;
б) долю ошибок второго рода (ложноотрицательных срабатываний, т. е. пропусков заведомо известных ошибок) — не более 50 %.
К сожалению, выдернутые из контекста, эти "50%" приводят к неправильной интерпретации информации. Тема выходит за рамки этой публикации, и мы постараемся рассмотреть её более подробно отдельно. Однако, поскольку мы уже столкнулись с недопониманиям при общениях на конференциях и вебинарах, следует дать здесь хотя бы краткие пояснения.
К сожалению, в стандарте очень обще сформулировано, что анализатор должен выдавать не более 50% ложных срабатываний (FP). Само по себе это число абстрактное и, скорее всего, недостижимо на практике, если мы просто возьмём и проверим абстрактный проект каким-то анализатором.
Подождите, но как же так?! Ведь:
не просто так в стандарте говорится про 50% ложных срабатываний;
вы сами писали статью "Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний";
некоторые разработчики заявляют, что их статические анализаторы вообще ложных срабатываний не выдают.
Давайте разбираться. Думаю, основная проблема в том, что в ГОСТ 71207-2024 отсутствуют слова про настройку статических анализаторов кода. Вернее, в стандарте говорится про настройку (например, в п. 5.1, 5.4, 5.12), но не в контексте требований (раздел 8) и проверки требований (раздел 10).
На практике, запустив впервые анализатор на проекте, можно получить любой (случайный) процент ложных срабатываний. Скорее всего, их будет больше, чем 50%. Однако даже результат, скажем, в 99% ложных срабатываний, вовсе не означает, что анализатор плох и его не удастся эффективно использовать в проекте. Скорее всего, даже минимальная настройка существенно улучшит картину.
Зачастую количество FP может быть сокращено в разы благодаря следующим действиям:
Настройка детекторов. Например, в общем случае нельзя разыменовывать указатель, который вернула функция
malloc
без предварительной проверки. Однако есть системы, когдаmalloc
заведомо точно не возвращаетNULL
. В этом случае можно настроить этот момент и сразу убрать множество нерелевантных предупреждений.Пометка макросов в C и C++, реализованных особенным образом. Хотя конструкция, смущающая анализатор, по сути, одна, могут быть выданы сотни ложных предупреждений в тех местах, где используется этот макрос.
И так далее.
В упомянутой ранее нашей статье как раз результат в 10–15% процентов ложных срабатываний может быть достигнут благодаря настройке.
Можно выдать менее 50% ложных срабатываний на синтетических квалификационных тестах небольшого размера (п. 10.2.б). Собственно, мы уже это сделали в рамках "Домашнего задания".

Таблица 1. Средний процент ложных срабатываний (FP) для языка Java на квалификационных тестах этапа "Домашнего задания" составил 34,1%.
Однако запуск на синтетических тестах и реальных проектах — это очень разные вещи. Плохо, что в ГОСТ 71207-2024 в методике проведения испытаний на реальных проектах (п.10.2.В и п.10.4) ничего нет сказано настройку анализаторов.
Запуск ненастроенного анализатора на проекте — это лотерея. На практике такой запуск ничего не скажет о качестве и возможностях инструмента. Процент ложных срабатываний в основном будет зависеть от везения, а не от технологий, реализованных в анализаторе.
Впрочем, стандарт и не требует, что анализатор должен быть изначально именно не настроен. Остаётся свобода интерпретации. Собственно, в рамках выработки методологии испытаний, которая сейчас происходит по инициативе ФСТЭК, этот вопрос, думаю, так или иначе будет обсуждён, и процесс оценки будет сформулирован более точно. В течение 2025 года этот момент должен проясниться.
Остаётся вопрос: а что насчёт разработчиков анализаторов, которые заявляют, что количество ложных срабатываний у их решений около 0%? Всё просто:
Это просто маркетинг, а не реальность. "А Вы тоже говорите" (c) анекдот.
Детекторы разные бывают. Можно без ложных срабатываний выявлять такие недостатки кода, как: переход с помощью
goto
выше по коду, указание типаlong
маленькой буквой 10l (надо 10L), отсутствие вswitch
веткиdafault
и т.д. Во многом на таких правилах основаны такие стандарты, как например MISRA C. Это имеет смысл и может повысить общее качество кода, но не имеет отношения к выявлению критических ошибок, которые обсуждаются в этой статье и стандарте.Там не говорится, что "нет ложных срабатываний даже без предварительной настройки".
Примечание. Вы можете больше узнать про эту тему и задать вопросы Андрею Карпову, подписавшись на его Телеграм-канал "Бестиарий программирования".
Примеры критических ошибок
PVS-Studio позволяет выделить предупреждения о критических ошибках среди других, используя соответствующие механизмы их идентификации и фильтрации. См. статью "Фильтрация предупреждений PVS-Studio, выявляющих критические ошибки (согласно классификации ГОСТ Р 71207-2024)".
Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.)
Чувствительные данные — это данные, пришедшие из какого-либо внешнего источника. Вся их опасность заключается в том, что прийти может что угодно. И при попадании в определённые точки программы эти данные могут очень сильно навредить самой программе или данным, с которыми она работает. SQL-инъекция — самый популярный пример подобной ситуации.
Однако, если данные перед попаданием в SQL-запрос будут грамотно очищены либо проверены, опасности не будет. И задача статического анализатора как раз в том, чтобы проверить, были ли очищены данные, пришедшие из какого-либо внешнего источника. И если данные попали в ключевую точку непроверенными, анализатор должен выдать предупреждение.
Срабатывание на код проекта "AutoMQ":
V5330. Possible XSS injection. Potentially tainted data in the 'message' variable might be used to execute a malicious script. HelloSessionServlet.java
public class HelloSessionServlet extends HttpServlet {
....
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException
{
....
String greeting = request.getParameter("greeting"); // <=
if (greeting != null)
{
....
message = "New greeting '" + greeting + "' set in session."; // <=
....
}
....
PrintWriter out = response.getWriter();
out.println("<h1>" + greeting + " from HelloSessionServlet</h1>"); // <=
out.println("<p>" + message + "</p>"); // <=
....
}
}
Из пришедшего request
мы получили строку greeting
. А затем использовали её, записав данные в response
. В чём может быть проблема? В том, что эти данные никак не экранируются и не чистятся. Мы формируем тело страницы, вставляя туда то, что нам передал пользователь. Это открывает пространство для XSS-инъекции. К примеру, такой:
<script>alert("XSS Injection")</script>
Безусловно, в самом проекте это не реальная уязвимость, которую можно эксплуатировать в отношении тех, кто использует jetty. Этот файл — буквально демонстрационный пример из пакета demos
. Однако, во-первых, это никак не отменяет справедливости срабатывания — в коде действительно используются непроверенные данные извне. А во-вторых, демонстрационный статус примера не исключает риска — он также может послужить источником ошибок при заимствовании кода.
Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел
Взглянув лишь на название этой категории ошибок, можно понять: оно пришло неадаптированным из C/C++ :) В Java и беззнакового типа-то нет. Да, есть char
, но он таковой скорее для того, чтобы уместить в себе определённый диапазон символов, и чтобы его байтовое представление соответствовало значениям символов из кодировки UTF-16.
Сама же категория, по сути, лишь про целочисленное переполнение. Но если в C++ переполнение знаковых переменных — это undefined behavior, то в Java значение int
переменной "закольцуется".
Пример:
public static void main(String[] args) {
int i = Integer.MAX_VALUE;
System.out.println(i); // 2_147_483_647
i += 1;
System.out.println(i); // -2_147_483_648
}
То есть, прибавив единицу к максимально возможному int
, мы получили минимально возможный int
. Тот же эффект будет, если пример полностью развернуть. Отняв у минимального int
единицу, мы получим максимально возможный int
.
И хоть в Java поведение определено, это не значит, что оно не может привести к незапланированному (неожиданному) поведению программы с точки зрения разработчика. Поэтому такие ситуации полезно выявлять.
Срабатывание на код проекта Apache Hive:
V6034 Shift by the value of 'j' could be inconsistent with the size of type: 'j' = [0 .. 63]. IoTrace.java(272)
public void logSargResult(int stripeIx, boolean[] rgsToRead) {
....
for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) {
long val = 0;
for (int j = 0; j < 64; ++j) {
int ix = valOffset + j;
if (rgsToRead.length == ix) break;
if (!rgsToRead[ix]) continue;
val = val | (1 << j); // <=
}
....
}
....
}
Мы сдвигаем единицу на количество бит, равное j
. Значение у j
может быть вплоть до 63. А у литерала 1
типом будет int
, то есть он будет представлен 32-битным числом. Чтобы единица не "закольцевалась", нужно объявить её как long
, т.е. при объявлении просто добавить L
литерал: 1L
.
Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти)
И снова не совсем Java-формулировка. Она также пришла неадаптированной из C/C++. Там есть глобальное понятие буфера, представляющего из себя область выделенной памяти. И ошибка выполнение буфера — это выход за её пределы, что по стандарту C++ является UB. То есть это что-то страшное, поскольку поведение неопределённое. А в Java всё чуть скромнее: эта ошибка представляет собой лишь выход за границу массива/коллекции, что чревато простым IndexOutOfBoundsException
. Т.е., безусловно, это неприятная ошибка, но никакой неопределённости не будет. Генерация исключения — ожидаемое и понятное поведение. Тем не менее, находить такое важно и нужно.
Срабатывание на код проекта AutoMQ:
V6025. Possibly index 'type' is out of bounds. ByteBufAlloc.java 151
public static final int MAX_TYPE_NUMBER = 20;
private static final LongAdder[] USAGE_STATS = new LongAdder[MAX_TYPE_NUMBER];
....
public static ByteBuf byteBuffer(int initCapacity, int type) {
try {
if (MEMORY_USAGE_DETECT) {
....
if (type > MAX_TYPE_NUMBER) {
counter = UNKNOWN_USAGE_STATS;
} else {
counter = USAGE_STATS[type]; // <=
....
}
....
}
....
}
}
Ошибка довольно простая. У нас есть массив USAGE_STATS
и его размер MAX_TYPE_NUMBER
. Из-за условия type > MAX_TYPE_NUMBER
в блоке else
индекс type
может принимать значение равное размеру массива. Т.е., если размер массива равен 20, то из-за условия возможно такое, что к массиву мы обратимся по 20 индексу. Это как раз и будет вышеупомянутый IndexOutOfBoundsException
.
Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.)
В целом название достаточно ёмко передаёт саму суть возможной ошибки. Мне здесь особо нечего добавить. Скажу лишь, что нежелательных практик, на которые реагируют наши диагностики в рамках этой категории критических ошибок, может быть много. Далее я приведу несколько из них.
Первое срабатывание на код проекта DBeaver:
V5307. Potentially predictable seed is used in pseudo-random number generator. DataSourceDescriptor.java
public static String generateNewId(DBPDriver driver) {
long rnd = new Random().nextLong(); // <=
if (rnd < 0) rnd = -rnd;
return driver.getId() + "-"
+ Long.toHexString(System.currentTimeMillis())
+ "-"
+ Long.toHexString(rnd);
}
Здесь каждый раз при вызове метода создаётся новый объект Random
. Это приведёт к тому, что генерируемые "случайные" значения не будут достаточно "случайными". В зависимости от большого количества условий, в том числе от конкретной JVM, значения из генерируемой последовательности могут повторяться. Это может быть нежелательным поведением в контексте генерации ID значения. Это не самая лучшая практика. Лучше вынести Random
объект в поле класса.
Второе срабатывание на код проекта DBeaver:
V5314. Use of the 'MD5' hash algorithm is not recommended. Such code may cause the exposure of sensitive data. EditConnectionWizard.java
private boolean checkLockPassword() {
BaseAuthDialog dialog = new BaseAuthDialog(....);
if (dialog.open() == IDialogConstants.OK_ID) {
final String userPassword = dialog.getUserPassword();
if (!CommonUtils.isEmpty(userPassword)) {
try {
final byte[]
md5hash = MessageDigest.getInstance("MD5") // <=
.digest(userPassword.getBytes(....));
final String hexString = CommonUtils.toHexString(md5hash)
.toLowerCase(Locale.ENGLISH)
.trim();
if (hexString.equals(dataSource.getLockPasswordHash())) {
return true;
}
UIUtils.showMessageBox(....);
} catch (Throwable e) {
DBWorkbench.getPlatformUI().showError(....);
}
}
}
return false;
}
Во фрагменте выше происходит проверка введённого пароля. Проблема в том, что для его хеширования применяется алгоритм MD5
. На данный момент он считается устаревшим и не рекомендуется к использованию. В документации присутствует ссылка на материалы OWASP по этому поводу, продублирую её здесь.
Использование алгоритма MD5, так же как и, например, SHA1, не является достаточно безопасной практикой. Лучше использовать более новые и безопасные алгоритмы, например, SHA256.
Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.)
Здесь всё тоже довольно понятно по названию. Под эту классификацию попадают все ошибки, связанные с многопоточностью, которые могут привести к race condition, data race или к выбросу специального исключения.
Несмотря на то, что проблемы многопоточности — это скорее прерогатива динамических анализаторов, какие-то паттерны ошибок можно обнаружить именно статически, имея лишь сам код. В PVS-Studio присутствует соответствующая группа детекторов. Рассмотрим несколько примеров срабатываний.
Срабатывание на код проекта Apache DS:
V6125. The notify method is called potentially outside of synchronized context. Such call will lead to IllegalMonitorStateException. MockSyncReplConsumer.java
public void stopRefreshing() {
stop = true;
// just in case if it is sleeping, wake up the thread
mutex.notify();
}
Вызов метода notify
корректен только на том объекте, по которому происходит синхронизация. Здесь же ни о какой синхронизации по объекту mutex
речи не идёт. Что самое интересное, поле mutex
приватное и инициализируется в самом классе. А геттеров для него никаких нет. То есть вызов этого метода извне, в синхронизированном по этому мьютексу контексте, невозможен.
Срабатывание на код проекта AutoMQ:
V6102. Inconsistent synchronization of the 'mbeans' field. Consider synchronizing the field on all usages. JmxReporter.java
public class JmxReporter implements MetricsReporter {
private final Map<String, KafkaMbean> mbeans = new HashMap<>();
....
@Override
public void reconfigure(Map<String, ?> configs) {
synchronized (LOCK) {
....
mbeans.forEach(....);
}
}
....
@Override
public void init(List<KafkaMetric> metrics) {
synchronized (LOCK) {
for (KafkaMetric metric : metrics)
addAttribute(metric);
mbeans.forEach(....);
}
}
public boolean containsMbean(String mbeanName) {
return mbeans.containsKey(mbeanName); // <=
}
@Override
public void metricChange(KafkaMetric metric) {
synchronized (LOCK) {
String mbeanName = addAttribute(metric);
if (mbeanName != null && mbeanPredicate.test(mbeanName)) {
reregister(mbeans.get(mbeanName));
}
}
}
....
}
Ошибка, с которой мы периодически встречаемся, проверяя Open Source проекты. Очень подозрительное использование mbeans
в несинхронизированном контексте. При условии, что остальные использования происходят в синхронизированных блоках или методах.
С учётом того, что в синхронизированном контексте возможны как запись, так и чтение объекта, может произойти data race, ведь в несинхронизированном контексте также выполняется чтение. И поскольку в этот момент другой поток может изменять его, возможна ситуация, при которой читается объект, находящийся в невалидном состоянии. Или в рамках несинхронизированного контекста произойдёт чтение неактуального значения mbeans
, поскольку по нему отсутствует синхронизация.
Ошибки разыменования нулевых ссылок
Именно с этой категории мы перешли к критическим ошибкам, которые добавились в Java в рамках этапа "Домашнее задание".
Здесь речь идёт про простой NPE. Подобных мы показывали много в наших статьях-проверках.
Срабатывание на код проекта AutoMQ:
V6008. Potential null dereference of 'oldMember' in function 'removeStaticMember'. ConsumerGroup.java 311, ConsumerGroup.java 323
@Override
public void removeMember(String memberId) {
ConsumerGroupMember oldMember = members.remove(memberId);
....
removeStaticMember(oldMember);
....
}
private void removeStaticMember(ConsumerGroupMember oldMember) {
if (oldMember.instanceId() != null) { // <=
staticMembers.remove(oldMember.instanceId());
}
}
Весьма классический NPE. members
представляет собой реализацию интерфейса Map
. Если в members
не найдётся элемента с ключом memberId
, метод remove
вернёт null
. Результат выполнения метода remove
записали в переменную oldMember
, нигде не проверили, что в ней лежит, а потом разыменовали в методе removeStaticMember
.
Что примечательно, AutoMQ — это форк всеми известного проекта Kafka. Мы уже обозревали, что там нашёл наш анализатор. И эта ошибка там была. Через несколько дней после выхода статьи один из читателей создал PR, в рамках которого сообщил об ошибке выше и добавил необходимую проверку. PR приняли, вот ссылка на этот коммит.
Ошибки деления на ноль
Деление на 0 в Java приводит к ArithmeticException
.
Срабатывание из AutoMQ:
V6020. Divide by zero. Denominator range ['0'..'2147483646']. Frequencies.java
/**
* ....
* @param buckets the number of buckets; must be at least 1
* ....
*/
public Frequencies(int buckets,
double min,
double max,
Frequency... frequencies) {
....
if (buckets < 1) {
throw new IllegalArgumentException("Must be at least 1 bucket");
}
if (buckets < frequencies.length) {
throw new IllegalArgumentException("More frequencies than buckets");
}
....
double halfBucketWidth = (max - min) / (buckets - 1) / 2.0; // <=
}
В комментариях сказано, что buckets
должно равняться минимум единице. Из условий в самом теле метода получаем то же самое. При этом, если переменная будет равняться единице, произойдёт деление на 0 при вычислении значения переменной haldBucketWidth
.
Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений
Для глубокой проверки, не происходит ли утечки ресурсов, принято использовать динамический анализ. Однако большинство потенциальных не закрытий ресурсов, соединений и т.д. можно найти и с помощью статических анализаторов ещё на этапе написания кода. Ведь если у объекта не вызвали метод close
и объявили его не в try-with-resource конструкции, откуда взяться закрытию его ресурсов?
Срабатывание из проекта XMage:
V6114 The 'ComputerPlayerMCTS' class contains the 'threadPoolSimulations' Closeable field, but the resources that the field is holding are not released inside the class. ComputerPlayerMCTS.java 39
public class ComputerPlayerMCTS extends ComputerPlayer {
....
private ExecutorService threadPoolSimulations = null;
....
protected void applyMCTS(....) {
if (this.threadPoolSimulations == null) {
....
this.threadPoolSimulations = new ThreadPoolExecutor(....);
}
....
try {
List<Future<Boolean>>
runningTasks = threadPoolSimulations.invokeAll(tasks,
thinkTime,
TimeUnit.SECONDS);
....
}
....
}
....
}
Тут как раз пример подобной ситуации. Анализатор ругается на то, что у ExecutorService
не закрываются его ресурсы. Если перейти в документацию этого класса, можно будет увидеть отдельную пометку про то, что после использования объекта ExecutorService
его ресурсы должны закрываться:
An unused ExecutorService should be shut down to allow reclamation of its resources.
В примере выше после создания объекта и вызова метода invokeAll
закрытия его ресурсов нигде не происходит.
Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти)
Здесь, пожалуй, самая спорная формулировка критической ошибки из всех. Спорная потому, что вся ответственность за вышеперечисленное лежит на JVM и Garbage Collector. Руками в Java никакая память не освобождается. Ну и, как следствие, использование освобождённой памяти тоже невозможно.
Под эту категорию организаторы испытаний подвели использование высвобождённых ресурсов. То есть попытка чтения из закрытого сокета или, к примеру, файлового потока.
Стоит признаться, такого срабатывания в реальном проекте пока не было обнаружено, поскольку соответствующий детектор появился в PVS-Studio совсем недавно. Ну, в таком случае обойдёмся синтетическим примером.
V6128 The use of a 'Closable' object after it has been closed can lead to an exception. Example.java 10
public void appendFileInformation(String path) throws IOException {
FileInputStream is = new FileInputStream(path);
....
is.close(); // <=
....
if (is.available() == 0) { // <=
System.out.println("EOF");
}
....
}
Чтобы не повторяться, сочту корректным сослаться на документацию этой диагностики:
Проблема в том, что при вызове метода
available
будет выброшено исключение типаIOException
с сообщением "Stream Closed". Это связано с тем, что перед условием у переменнойis
вызывается методclose
. Следовательно, ресурсыis
будут освобождены.
Что по итогу?
PVS-Studio очень давно продвигает идеи того, что статический анализ — это хорошо и полезно. Особенно если он применяется грамотно и регулярно. И мы рады, что теперь этот процесс регламентирован специальным стандартом. На протяжении всего своего существования команда разработчиков PVS-Studio продвигала те идеи, отголоски которых нашлись в ГОСТ 71207-2024.
По поводу "Домашнего задания" больших трудностей не возникло. Практически все необходимые диагностики у нас были. Чтобы не осталось каких-либо зазоров, мы разметили существующие детекторы, добавили пару недостающих и доработали некоторые механизмы анализатора.
Остаются небольшие вопросы по поводу того, не стоит ли изменить название категориям критических ошибок Java. Всё-таки некоторые из них частично не соответствую существующему порядку вещей. К примеру, те же понятия "буфера" или "выделения, освобождения, использования освобожденной памяти" немного не вписываются в специфику языка.
Если вы хотите поделиться своим мнением по поводу ГОСТ 71207-2024 или тех моментов "Домашнего задания", что мы осветили, — готовы пообщаться с вами в комментариях.
На этом, пожалуй, пока всё. Если есть желание попробовать PVS-Studio, переходите по этой ссылке.
humanpath
Это инсайд о вопросах на джависта в 2026 году?