我敢打赌,每一个Java开发人员在他们的职业生涯初期都首次在Java代码中遇到本机方法时都会感到惊讶。
我还可以肯定,多年来随着了解JVM如何通过JNI处理对本机实现的调用而使惊喜消失了。
这篇文章是关于本机方法的最新经验。 更详细地讲,使用本机方法如何导致JVM静默崩溃,而日志文件中没有任何合理的跟踪。 为了向您介绍经验,我创建了一个小测试用例。
它由一个简单的Java类组成 ,可计算文件的校验和。 为了实现Awesome Performance(TM),我决定使用本机实现来实现校验和计算部分。 该代码简单明了,因此正在运行。 您只需要克隆存储库并启动它,类似于以下示例:
$ ./gradlew jarWithNatives
$ java -jar build/libs/checksum.jar 123.txt
Exiting native method with checksum: 1804289383
Got checksum from native method: 1804289383
该代码似乎按预期工作。 当您发现自己盯着输出时使用的输入文件名略有不同(较长)时,会发现不太简单的部分:
$ 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代码中立即可见:
char dst_filename[MAX_FILE_NAME_LENGTH];
// cut for brevity
sprintf(dst_filename, "%s.digested", src_filename);
从上面可以明显看出,缓冲区只能容纳固定数量的字符。 输入较长时,剩余字符将被写到末尾。 实际上,这将导致堆栈崩溃,并为潜在的黑客攻击或使应用程序处于不可预测的状态打开大门。
对于C开发人员,底层的堆栈保护器机制是众所周知的,但对于Java开发人员,可能需要更多说明。 除了使用更安全的snprintf占用缓冲区长度并且不会超出该长度之外,您还可以要求编译器向堆栈中添加堆栈保护器或内存清理。 可用的安全网因编译器而异,甚至在同一编译器的不同版本之间也存在很大差异,但这是一个示例:
gcc -fstack-protector CheckSumCalculator.c -o CheckSumCalculator.so
使用适当的堆栈保护器编译代码后,运行时库或OS的实现在某些情况下可能会检测到这种情况,并终止程序以防止意外行为。
如下面的示例所示,在未进行清理的情况下编译代码时,
gcc -fno-stack-protector CheckSumCalculator.c -o CheckSumCalculator.so
运行此类代码的结果可能变得完全不可预测。 在某些情况下,代码看起来似乎可以完成,但是在某些情况下,您可能会遇到缓冲区溢出。 尽管在此示例中,使用snprintf并启用清除功能绝对有帮助,但该错误可能比其细微得多,并且不会自动捕获。
回到所谓的安全Java世界,这样的缓冲区溢出可能会破坏内部JVM结构,甚至使提供字符串的任何人都可以执行任意代码。 因此,JVM将保护值添加到内存中,如果在本机方法完成后对这些值进行了修改,则立即终止应用程序。 为什么在没有更详细的错误日志的情况下进行堕胎是一个不同的问题,不在本文的讨论范围之内。
我希望这篇文章在面对突然的JVM死亡甚至没有崩溃日志时能为某人节省一整夜的时间。 在所有平台上甚至都没有出现标准错误流中的“ stack smashed”消息,并且可能需要花费大量时间才能确定发生了什么情况,尤其是在运行没有源代码的第三方本机库的情况下。
翻译自: https://www.javacodegeeks.com/2015/09/stack-smashing-detected.html