Статьи

Обнаружено разрушение стека

Распечатать Бьюсь об заклад, каждый разработчик Java был удивлен в какой-то момент в начале своей карьеры, когда они впервые столкнулись с нативными методами в коде Java.

Я также уверен, что с годами неожиданность исчезла, когда я начал понимать, как JVM обрабатывает вызовы нативных реализаций через JNI .

Этот пост о недавнем опыте с нативными методами. Или более подробно, как использование нативных методов может привести к сбою JVM без каких-либо разумных следов в файлах журнала. Чтобы провести вас через этот опыт, я создал небольшой тестовый пример.

Он состоит из простого Java-класса , вычисляющего контрольные суммы для файлов. Чтобы достичь Awesome Performance (TM), я решил реализовать часть вычисления контрольной суммы, используя собственную реализацию . Код прост и понятен, и поэтому он выполняется. Вам просто нужно клонировать репозиторий и запустить его, как показано в следующем примере:

1
2
3
4
$ ./gradlew jarWithNatives
$ java -jar build/libs/checksum.jar 123.txt
Exiting native method with checksum: 1804289383
Got checksum from native method: 1804289383

Код, кажется, работает так же, как ожидалось. Не очень простая часть раскрывается, когда вы обнаруживаете, что смотрите на вывод с немного другим (более длинным) именем файла, используемым для ввода:

1
2
3
$ java -jar build/libs/checksum.jar 123456789012.txt
Exiting native method with checksum: 1804289383
*** stack smashing detected ***: java terminated

Таким образом, нативный метод завершил свое выполнение просто отлично, но элемент управления не был возвращен Java. Вместо этого JVM терпит крах без так много как журнал аварий. Вам следует учитывать тот факт, что я тестировал только примеры на Linux и Mac OS X, а на Windows это может отличаться.

Основная проблема не слишком сложна и, вероятно, сразу видна в коде C :

1
2
3
char        dst_filename[MAX_FILE_NAME_LENGTH];
// cut for brevity
sprintf(dst_filename, "%s.digested", src_filename);

Из вышесказанного ясно, что буфер может содержать только фиксированное число символов. При более длинных входах оставшиеся символы будут записаны после его конца. Это действительно приведет к разрушению стека и откроет двери для потенциальных взломов или просто оставит приложение в непредсказуемом состоянии.

Для разработчиков C основной механизм защиты стека хорошо известен, но для разработчиков Java, возможно, потребуется более подробное объяснение. Помимо использования гораздо более безопасного snprintf, который принимает длину буфера и не записывает после этого, вы также можете попросить компилятор добавить средства защиты стека или очистку памяти в скомпилированный код. Доступные сети безопасности будут значительно различаться от компилятора к компилятору и даже между различными версиями одного и того же компилятора, но вот пример:

1
gcc -fstack-protector CheckSumCalculator.c -o CheckSumCalculator.so

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

Когда код скомпилирован без очистки, как в следующем примере,

1
gcc -fno-stack-protector CheckSumCalculator.c -o CheckSumCalculator.so

результаты выполнения такого кода могут стать совершенно непредсказуемыми. В некоторых случаях код может завершиться, казалось бы, нормально, но в некоторых случаях вы можете столкнуться с переполнением буфера. Хотя в этом примере использование snprintf и включение санации определенно помогут, ошибка может быть гораздо более скрытой и не обнаруживаться автоматически.

Возвращаясь к предположительно безопасному миру Java, такое переполнение буфера может повредить внутренние структуры JVM или даже позволить тому, кто предоставил строку, выполнить произвольный код. Таким образом, JVM добавляет защитные значения в память, и если эти значения искажаются после завершения собственного метода, немедленно завершает приложение. Почему аборт выполняется без более детального журнала ошибок — это другой вопрос, который выходит за рамки этого поста.

Я надеюсь, что этот пост спасет кого-то почти всю ночь, когда он столкнется с внезапной смертью JVM даже без журнала аварий. Сообщение «разбитый стек» в стандартном потоке ошибок отсутствует даже на всех платформах, и может потребоваться много-много времени, чтобы выяснить, что произошло, особенно если вы используете стороннюю собственную библиотеку без исходного кода.

Ссылка: Разрушение стека обнаружено от нашего партнера JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .