Язык программирования Java
Ядро языка
Реализации языка программирования Java обеспечивают строгую безопасность памяти даже при наличии состояний гонки в параллельном коде. Это предотвращает возникновение широкого спектра уязвимостей безопасности, если только не используются некоторые низкоуровневые возможности; см. Низкоуровневые возможности виртуальной машины.
Повышение надёжности при чтении массивов
Внешние форматы данных часто включают массивы, и данные хранятся в виде целого числа, указывающего количество элементов массива, за которым следует это количество элементов в файле или единице данных протокола. Указанная длина может быть намного больше, чем фактически доступно в источнике данных.
Чтобы избежать выделения чрезмерно больших объёмов данных, вы можете выделить небольшой массив изначально и увеличивать его по мере чтения данных, реализуя политику экспоненциального роста. См. функцию readBytes(InputStream, int) в Инкрементальное чтение байтового массива.
static byte[] readBytes(InputStream in, int length) throws IOException {
final int startSize = 65536;
byte[] b = new byte[Math.min(length, startSize)];
int filled = 0;
while (true) {
int remaining = b.length - filled;
readFully(in, b, filled, remaining);
if (b.length == length) {
break;
}
filled = b.length;
if (length - b.length <= b.length) {
// Allocate final length. Condition avoids overflow.
b = Arrays.copyOf(b, length);
} else {
b = Arrays.copyOf(b, b.length * 2);
}
}
return b;
}
static void readFully(InputStream in,byte[] b, int off, int len)
throws IOException {
int startlen = len;
while (len > 0) {
int count = in.read(b, off, len);
if (count < 0) {
throw new EOFException();
}
off += count;
len -= count;
}
}
При чтении данных в массивы, хеш-карты или хеш-наборы используйте конструктор по умолчанию и не указывайте подсказку размера. Вы можете просто добавлять элементы в коллекцию по мере их чтения.
Управление ресурсами
В отличие от C++, Java не предлагает деструкторы, которые могли бы освобождать ресурсы предсказуемым образом. Всё управление ресурсами должно выполняться вручную в месте использования. (Финализаторы обычно непригодны для управления ресурсами, особенно в высокопроизводительном коде; см. Финализаторы.)
Первый вариант — конструкция try-finally, как показано в Управление ресурсами с помощью блока try-finally. Код в блоке finally должен быть максимально коротким и не должен генерировать исключения.
try-finallyInputStream in = new BufferedInputStream(new FileInputStream(path));
try {
readFile(in);
} finally {
in.close();
}
Обратите внимание, что выделение ресурса происходит за пределами блока try, и в блоке finally нет проверки на null. (Оба являются распространёнными артефактами, возникающими из шаблонов кода IDE.)
Если объект ресурса создаётся заново и реализует интерфейс java.lang.AutoCloseable, можно использовать код из Управление ресурсами с использованием конструкции try-с-ресурсами. Компилятор Java автоматически вставит вызов метода close() в синтетический блок finally.
try-с-ресурсамиtry (InputStream in = new BufferedInputStream(new FileInputStream(path))) {
readFile(in);
}
Для совместимости с конструкцией try-с-ресурсами новые классы должны называть метод освобождения ресурса close() и реализовывать интерфейс AutoCloseable (последнее нарушает обратную совместимость с Java 6). Однако использование конструкции try-с-ресурсами с объектами, которые не были выделены заново, в лучшем случае неудобно, и явный блок finally обычно является лучшим подходом.
В целом, лучше всего проектировать программный интерфейс таким образом, чтобы методы освобождения ресурсов, такие как close(), не могли генерировать исключения (ни проверяемые, ни непроверяемые), но это не должно быть причиной игнорировать фактические ошибочные ситуации.
Финализаторы
Финализаторы можно использовать как крайнее средство для освобождения ресурсов, которые иначе могли бы утечь. Финализация непредсказуема, затратна, и между исчезновением последней ссылки на объект и выполнением финализатора может пройти значительное время. Как правило, требуется ручное управление ресурсами; см. Управление ресурсами.
Финализаторы должны быть очень короткими и должны освобождать только нативные или другие внешние ресурсы, непосредственно удерживаемые финализируемым объектом. В общем случае они должны использовать синхронизацию: финализация обязательно происходит в отдельном потоке, поскольку она по своей природе параллельна. Может быть несколько потоков финализации, и, несмотря на то, что каждый объект финализируется не более одного раза, финализатор не должен предполагать, что он имеет исключительный доступ к финализируемому объекту (через указатель this).
Финализаторы не должны освобождать ресурсы, удерживаемые другими объектами, особенно если эти объекты имеют собственные финализаторы. В частности, очень плохая идея определять финализатор только для вызова метода освобождения ресурсов другого объекта или для перезаписи некоторых полей-указателей.
Финализаторы не гарантированно запускаются вообще. Например, виртуальная машина (или машина под ней) может аварийно завершиться, предотвращая их выполнение.
Объекты с финализаторами собираются сборщиком мусора значительно позже, чем объекты без них, поэтому использование финализаторов для обнуления ключевых материалов (чтобы сократить их время жизни в незашифрованном виде в памяти) может дать противоположный эффект, удерживая объекты значительно дольше и препятствуя их перезаписи в нормальном ходе выполнения программы.
По той же причине код, который выделяет объекты с финализаторами с высокой скоростью, в конечном итоге завершится ошибкой (вероятно, с исключением java.lang.OutOfMemoryError), поскольку виртуальная машина имеет конечные ресурсы для отслеживания объектов, ожидающих финализации. Для решения этой проблемы может потребоваться переработка объектов с финализаторами.
Замечания в этом разделе относятся к финализаторам, которые реализуются путём переопределения метода finalize(), а также к пользовательской финализации с использованием очередей ссылок.
Восстановление после исключений и ошибок
Исключения Java бывают трёх видов, все в конечном счёте происходят от java.lang.Throwable:
-
Исключения времени выполнения не должны объявляться явно и могут быть явно выброшены из любого кода, при вызове кода, который их генерирует, или при возникновении ошибочной ситуации во время выполнения, такой как деление на ноль или попытка выхода за границы массива. Эти исключения происходят от класса
java.lang.RuntimeException(возможно, косвенно). -
Проверяемые исключения должны явно объявляться функциями, которые их генерируют или распространяют. Они похожи на исключения времени выполнения в других аспектах, за исключением того, что не существует языковой конструкции для их генерации (кроме самого оператора
throw). Проверяемые исключения существуют только на уровне языка Java и проверяются только во время компиляции. Во время выполнения виртуальная машина не знает о них и позволяет генерировать исключения из любого кода. Проверяемые исключения должны происходить (возможно, косвенно) от классаjava.lang.Exception, но не отjava.lang.RuntimeException. -
Ошибки — это исключения, которые обычно отражают серьёзные ошибочные ситуации. Они могут быть выброшены в любой точке программы и не требуют объявления (в отличие от проверяемых исключений). В общем случае невозможно восстановиться после таких ошибок; подробнее об этом ниже, в Сложность перехвата ошибок. Классы ошибок происходят (возможно, косвенно) от
java.lang.Errorили отjava.lang.Throwable, но не отjava.lang.Exception.
Обычно предполагается, что ошибок времени выполнения можно избежать аккуратным программированием (например, не делить на ноль). Проверяемые исключения ожидаемо перехватываются по мере их возникновения (например, когда неожиданно отсутствует входной файл). Ошибки невозможно предсказать, они могут произойти в любой момент и отражают то, что что-то пошло не так сверх всех ожиданий.
Сложность перехвата ошибок
Ошибки (то есть исключения, которые не происходят (косвенно) от java.lang.Exception) обладают тем свойством, что их перехват проблематичен. Для этого есть несколько причин:
-
Ошибка отражает неудачную проверку согласованности, например,
java.lang.AssertionError. -
Ошибка может произойти в любой момент, что приводит к несогласованности из-за наполовину обновлённых объектов. Примеры:
java.lang.ThreadDeath,java.lang.OutOfMemoryErrorиjava.lang.StackOverflowError. -
Ошибка указывает на то, что виртуальная машина не смогла обеспечить некоторые семантические гарантии языка программирования Java.
java.lang.ExceptionInInitializerError— пример такой ошибки: она может оставить после себя наполовину инициализированный класс.
В общем случае, если выброшена ошибка, виртуальную машину следует перезапустить как можно скорее, поскольку она находится в несогласованном состоянии. Продолжение работы как прежде может иметь непредсказуемые последствия. Однако существуют законные причины для перехвата ошибок, поскольку их неперехват приводит к ещё большим проблемам.
Код должен быть написан таким образом, чтобы избегать возникновения ошибок. Пример см. в Повышение надёжности при чтении массивов.
Обычно необходимо регистрировать ошибки. В противном случае нигде не может остаться следа проблемы, что делает диагностику связанных сбоев очень сложной. Следовательно, если вы перехватываете java.lang.Exception для регистрации и подавления всех неожиданных исключений (например, в цикле диспетчеризации запросов), вам следует рассмотреть возможность переключения на java.lang.Throwable, чтобы также охватить ошибки.
Другая причина в основном относится к таким циклам диспетчеризации запросов: если вы не перехватываете ошибки, цикл перестаёт выполняться, что приводит к отказу в обслуживании.
Однако, если это возможно, перехват ошибок должен быть связан со способом сигнализации о необходимости перезапуска виртуальной машины.
Низкоуровневые возможности виртуальной машины
Рефлексия и закрытые части
Метод setAccessible(boolean) класса java.lang.reflect.AccessibleObject позволяет программе отключать определённые языком правила доступа для конкретных конструкторов, методов или полей. После отключения проверок доступа любой код может использовать объекты java.lang.reflect.Constructor, java.lang.reflect.Method или java.lang.reflect.Field для доступа к соответствующей сущности Java без дополнительных проверок разрешений. Это нарушает инкапсуляцию и может подорвать стабильность виртуальной машины. (В отличие от этого, без использования метода setAccessible(boolean) этого не должно происходить, поскольку все определённые языком проверки всё ещё применяются.)
По возможности этой возможности следует избегать.
Java Native Interface (JNI)
Java Native Interface позволяет из Java-кода вызывать функции, специально написанные для этой цели, обычно на C или C++.
Переход между миром Java и миром C не полностью проверяется по типам, и C-код может легко нарушить семантику виртуальной машины Java. Поэтому при использовании этой функциональности требуется особая осторожность.
Чтобы обеспечить умеренный уровень типобезопасности, рекомендуется повторно создавать заголовочный файл для конкретного класса с помощью javah в процессе сборки, включать его в реализацию и использовать параметр -Wmissing-declarations.
В идеале требуемые данные должны напрямую передаваться в статические методы JNI и возвращаться из них, и код (сторона C) не должен иметь дело с доступом к полям Java (или даже методам).
При использовании GetPrimitiveArrayCritical или GetStringCritical убедитесь, что между операциями получения и освобождения выполняется очень мало обработки. Не обращайтесь к файловой системе или сети и не выполняйте блокировок, так как это может привести к блокировкам. При обработке больших строк или массивов рассмотрите возможность разбиения вычислений на несколько подчастей, чтобы не препятствовать достижению JVM безопасной точки в течение длительного времени.
При необходимости вы можете использовать тип Java long для хранения C-указателя в поле Java-класса. На стороне C при приведении между значением jlong и указателем на стороне C
Не следует пытаться выполнять арифметику указателей на стороне Java (то есть следует рассматривать значения long, содержащие указатели, как непрозрачные). При передаче среза массива в нативный код следуйте соглашению Java и передавайте его как базовый массив, целочисленное смещение начала среза и целочисленную длину среза. На нативной стороне проверьте комбинацию смещения/длины на соответствие фактической длине массива и используйте смещение для вычисления указателя на начало массива.
JNIEXPORT jint JNICALL Java_sum
(JNIEnv *jEnv, jclass clazz, jbyteArray buffer, jint offset, jint length)
{
assert(sizeof(jint) == sizeof(unsigned));
if (offset < 0 || length < 0) {
(*jEnv)->ThrowNew(jEnv, arrayIndexOutOfBoundsExceptionClass,
"negative offset/length");
return 0;
}
unsigned uoffset = offset;
unsigned ulength = length;
// This cannot overflow because of the check above.
unsigned totallength = uoffset + ulength;
unsigned actuallength = (*jEnv)->GetArrayLength(jEnv, buffer);
if (totallength > actuallength) {
(*jEnv)->ThrowNew(jEnv, arrayIndexOutOfBoundsExceptionClass,
"offset + length too large");
return 0;
}
unsigned char *ptr = (*jEnv)->GetPrimitiveArrayCritical(jEnv, buffer, 0);
if (ptr == NULL) {
return 0;
}
unsigned long long sum = 0;
for (unsigned char *p = ptr + uoffset, *end = p + ulength; p != end; ++p) {
sum += *p;
}
(*jEnv)->ReleasePrimitiveArrayCritical(jEnv, buffer, ptr, 0);
return sum;
}
В любом случае классы, ссылающиеся на нативные ресурсы, должны быть объявлены как final и не должны быть сериализуемыми или клонируемыми. Инициализация и изменение состояния, используемого нативной стороной, должны тщательно контролироваться. В противном случае может быть возможно создать объект с несогласованным нативным состоянием, что приведёт к сбою (или хуже) при его использовании (или, возможно, только при финализации) позже. Если вам нужны как наследование Java, так и нативные ресурсы, рассмотрите возможность переноса нативного состояния в отдельный класс и хранения только ссылки на объекты этого класса. Таким образом, проблем с клонированием и сериализацией в большинстве случаев можно избежать.
Если с объектом связаны нативные ресурсы, класс должен иметь явный метод освобождения ресурсов (Управление ресурсами) и финализатор (Финализаторы) как крайнее средство. Необходимость финализации означает, что требуется минимальный объём синхронизации. Код на нативной стороне должен проверять, что объект не находится в закрытом/освобождённом состоянии.
Многие функции JNI создают локальные ссылки. По умолчанию они существуют до возврата из метода, реализованного через JNI. Если вы создаёте много таких ссылок (например, в цикле), возможно, вам придётся освобождать их с помощью DeleteLocalRef или начать использовать PushLocalFrame и PopLocalFrame. Глобальные ссылки должны быть освобождены с помощью DeleteGlobalRef, иначе возникнет утечка памяти, как и в случае с malloc и free.
При генерации исключений с помощью Throw или ThrowNew имейте в виду, что эти функции возвращают управление обычным образом. Вам нужно вручную вернуть управление виртуальной машине Java.
Технически указатель JNIEnv не обязательно является константным в течение времени жизни вашего JNI-модуля. Поэтому хранение его в глобальной переменной неверно. Особенно если вы имеете дело с обратными вызовами, возможно, вам придётся хранить указатель в переменной, локальной для потока (определённой с помощью __thread). Однако лучше избегать сложности обратных вызовов в Java-код.
Имейте в виду, что C/C и Java — это разные языки, несмотря на очень похожий синтаксис выражений. Модель памяти Java гораздо более строгая, чем модели памяти C или C, и нативный код требует более тщательной синхронизации, обычно с использованием средств JVM или мьютексов POSIX threads. Переполнение целых чисел в Java определено, а в C/C++ — нет (для типов jint и jlong).
Взаимодействие с менеджером безопасности
Платформа Java в значительной степени реализована на самом языке Java. Поэтому в одной и той же JVM выполняется код, который является частью установки Java и считается доверенным, но также может быть код из недоверенных источников, ограниченный песочницей Java (в разной степени). Менеджер безопасности проводит разграничение между полностью доверенным, частично доверенным и недоверенным кодом.
Проверки типобезопасности и доступности, предоставляемые языком Java и JVM, были бы достаточны для реализации песочницы. Однако только некоторые Java API используют такой подход, основанный на возможностях. (Библиотека Java SE содержит множество открытых классов с открытыми конструкторами, которые могут нарушить любую политику безопасности, например java.io.FileOutputStream.) Вместо этого критически важная функциональность защищена инспекцией стека: при проверке безопасности стек просматривается сверху (самый вложенный) вниз. Проверка безопасности завершается неудачей, если встречается кадр стека для метода, класс которого не имеет разрешения, требуемого проверкой безопасности.
Этот простой подход не позволил бы недоверенному коду (которому не хватает определённых разрешений) вызывать доверенный код, пока последний сохраняет доверие. Такие переходы доверия желательны, поскольку они позволяют использовать Java как язык реализации для большинства частей платформы Java, включая код, связанный с безопасностью. Поэтому существует механизм пометки определённых кадров стека как доверенных (Повторное получение привилегий).
Теоретически возможно запустить виртуальную машину Java с менеджером безопасности, который работает совершенно иначе, но большая часть кода ожидает поведения, очень близкого к поведению платформы по умолчанию (включая многие классы, которые являются частью реализации OpenJDK).
Совместимость с менеджером безопасности
Большая часть кода может выполняться без каких-либо дополнительных разрешений с минимальными изменениями. Следующие рекомендации помогут повысить совместимость с ограничительным менеджером безопасности.
-
При получении системных свойств с помощью
System.getProperty(String)или аналогичных методов перехватывайте исключенияSecurityExceptionи обрабатывайте свойство как неустановленное. -
Избегайте ненужного доступа к файловой системе или сети.
-
Избегайте явной загрузки классов. Доступ к подходящему загрузчику классов может быть недоступен при выполнении кода как недоверенного.
Если реализуемая вами функциональность абсолютно требует привилегированного доступа и эту функциональность необходимо использовать из недоверенного кода (надеемся, ограниченным и безопасным образом), см. Повторное получение привилегий.
Активация менеджера безопасности
Обычная команда для запуска Java-приложения, java, не активирует менеджер безопасности. Поэтому виртуальная машина не применяет никаких ограничений песочницы, даже если это явно запрошено кодом (например, как описано в Снижение доверия к коду).
Параметр -Djava.security.manager активирует менеджер безопасности с довольно ограничительной политикой по умолчанию. С очень разрешительной политикой большая часть Java-кода будет работать без изменений. Предполагая, что политика из Наиболее разрешительный файл политики OpenJDK сохранена в файле grant-all.policy, эту политику можно активировать с помощью параметра -Djava.security.policy=grant-all.policy (в дополнение к параметру -Djava.security.manager).
grant {
permission java.security.AllPermission;
};
С этой наиболее разрешительной политикой менеджер безопасности всё ещё активен, и явные запросы на снижение привилегий будут выполняться.
Снижение доверия к коду
Пример Использование менеджера безопасности для выполнения кода с пониженными привилегиями показывает, как выполнить фрагмент кода с пониженными привилегиями.
Permissions permissions = new Permissions();
ProtectionDomain protectionDomain =
new ProtectionDomain(null, permissions);
AccessControlContext context = new AccessControlContext(
new ProtectionDomain[] { protectionDomain });
// This is expected to succeed.
try (FileInputStream in = new FileInputStream(path)) {
System.out.format("FileInputStream: %s%n", in);
}
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
// This code runs with reduced privileges and is
// expected to fail.
try (FileInputStream in = new FileInputStream(path)) {
System.out.format("FileInputStream: %s%n", in);
}
return null;
}
}, context);
В приведённом выше примере в объект permissions не добавляются никакие дополнительные разрешения. Если такие разрешения необходимы, можно использовать код, подобный следующему (который предоставляет разрешение на чтение всех файлов в текущем каталоге):
permissions.add(new FilePermission(
System.getProperty("user.dir") + "/-", "read"));
|
Вызовы методов Приведённый выше пример кода не препятствует вызванному коду вызывать методы Аргумент |
Об активации менеджера безопасности см. Активация менеджера безопасности. К сожалению, это влияет на виртуальную машину в целом, поэтому сделать это из библиотеки невозможно.
Повторное получение привилегий
Обычно, когда доверенный код вызывается из недоверенного, он теряет свои привилегии (из-за недоверенных кадров стека, видимых при инспекции стека). Семейство методов java.security.AccessController.doPrivileged() предоставляет контролируемый обратный ход из недоверенного кода в доверенный.
|
По своей природе эта возможность может подорвать модель безопасности Java и песочницу. Её следует использовать очень осторожно. Большинство уязвимостей песочницы можно проследить до её неправильного использования. |
По сути, методы doPrivileged() заставляют инспекцию стека завершаться в месте их вызова. Недоверенный код, расположенный ниже по стеку вызовов, становится невидимым для проверок безопасности.
Следующие операции являются распространёнными и безопасными для выполнения с повышенными привилегиями.
-
Чтение пользовательских системных свойств с фиксированными именами, особенно если значение не передаётся недоверенному коду. (Пути файловой системы, включая пути установки, имена хостов и имена пользователей иногда считаются частной информацией и требуют защиты.)
-
Чтение из файловой системы по фиксированным путям, определённым либо во время компиляции, либо с помощью системного свойства. Опять же, раскрытие содержимого файла вызывающему коду может быть проблематичным.
-
Доступ к сетевым ресурсам по фиксированному адресу, имени или URL, полученным из системного свойства или файла конфигурации, несмотря на возможные утечки информации.
Пример Использование менеджера безопасности для выполнения кода с повышенными привилегиями показывает, как запросить дополнительные привилегии.
// This is expected to fail.
try {
System.out.println(System.getProperty("user.home"));
} catch (SecurityException e) {
e.printStackTrace(System.err);
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// This should work.
System.out.println(System.getProperty("user.home"));
return null;
}
});
Очевидно, это работает только в том случае, если класс, содержащий вызов doPrivileged(), помечен как доверенный (обычно потому, что он загружен из доверенного загрузчика классов).
При написании кода, который выполняется с повышенными привилегиями, убедитесь, что вы следуете приведённым ниже правилам.
-
Сделайте привилегированный код как можно меньшим. Выполняйте как можно больше вычислений до и после привилегированного участка кода, даже если это означает, что вам придётся определить новый класс для передачи данных.
-
Убедитесь, что вы либо контролируете входные данные для привилегированного кода, либо что входные данные безвредны и не могут повлиять на свойства безопасности привилегированного кода.
-
Данные, возвращаемые или записываемые привилегированным кодом, должны быть либо ограничены (то есть недоступны для недоверенного кода), либо безвредны. В противном случае результатом могут быть утечки конфиденциальности или раскрытие информации, влияющее на свойства безопасности.
Если код на более позднем этапе вызывает обратно недоверенный код (или выполняет другие действия под управлением недоверенного вызывающего), вы должны получить исходный контекст безопасности и восстановить его перед выполнением обратного вызова, как в Восстановление привилегий при вызове обратных вызовов. (В этом примере, конечно, было бы гораздо лучше вынести вызов обратного вызова за пределы привилегированного участка кода.)
interface Callback<T> {
T call(boolean flag);
}
class CallbackInvoker<T> {
private final AccessControlContext context;
Callback<T> callback;
CallbackInvoker(Callback<T> callback) {
context = AccessController.getContext();
this.callback = callback;
}
public T invoke() {
// Obtain increased privileges.
return AccessController.doPrivileged(new PrivilegedAction<T>() {
@Override
public T run() {
// This operation would fail without
// additional privileges.
final boolean flag = Boolean.getBoolean("some.property");
// Restore the original privileges.
return AccessController.doPrivileged(
new PrivilegedAction<T>() {
@Override
public T run() {
return callback.call(flag);
}
}, context);
}
});
}
}
Want to help? Learn how to contribute to Fedora Docs ›