* MAX_JARRAY_SIZE boundary tests for JniUtil::createJavaByteArrayWithSizeCheck.
*
* The production guard in `cpp/include/FalconUtilJni.h`:
*
* static const size_t MAX_JARRAY_SIZE = 1u << 31; // = 2^31 = 2147483648
* if (size > MAX_JARRAY_SIZE) {
* FalconExceptionJni::ThrowNew(env, "Requested array size exceeds VM limit");
* return nullptr;
* }
* const jsize jlen = static_cast<jsize>(size); // jsize = int32_t
* jbyteArray jbytes = env->NewByteArray(jlen);
*
* Coverage:
* 1. size > MAX (e.g. 2^31 + 1): guard trips, nullptr returned, no allocation.
* 2. size == MAX (2^31): guard does NOT trip (`>` not `>=`); production casts
* size to jsize (int32_t) → INT_MIN (-2147483648). This is the off-by-one
* bug: the guard should use `>=` to reject this value too.
* 3. size == MAX - 1 (2^31 - 1): guard does not trip; positive jsize.
* 4. Far over limit (4 GiB): guard trips cleanly.
* 5. Well below limit (16 KiB): guard does not trip; allocation succeeds.
*
* Tests 2 & 3 use MockJniEnv::setSkipBackingStoreAbove so the mock records
* the requested jsize without allocating 2 GiB of RAM.
*
* Test 2 is an INTENTIONAL FAILURE: it documents the production off-by-one
* bug where size == MAX_JARRAY_SIZE passes the `>` guard and is then cast to
* a negative jsize. The test fails until production tightens the guard to `>=`.
*/
#include <gtest/gtest.h>
#include <gtest/gtest-spi.h>
#include <cstdint>
#include "FalconUtilJni.h"
#include "MockJniEnv.h"
namespace {
using falcon_test::MockJniEnv;
using FalconUtil::JniUtil;
class MaxJarraySizeTest : public ::testing::Test {
protected:
MockJniEnv jni_;
};
TEST_F(MaxJarraySizeTest, OverLimit_ThrowsAndReturnsNull_WithoutDereferencingBytes) {
constexpr size_t kJustOverLimit =
(static_cast<size_t>(1) << 31) + 1;
jbyteArray result = JniUtil::createJavaByteArrayWithSizeCheck(
jni_.env(), nullptr, kJustOverLimit);
EXPECT_EQ(result, nullptr);
EXPECT_TRUE(jni_.hasPendingException());
}
TEST_F(MaxJarraySizeTest, MuchOverLimit_FourGiB_AlsoBlocked) {
constexpr size_t kFourGiB = static_cast<size_t>(4) * 1024 * 1024 * 1024;
jbyteArray result = JniUtil::createJavaByteArrayWithSizeCheck(
jni_.env(), nullptr, kFourGiB);
EXPECT_EQ(result, nullptr);
EXPECT_TRUE(jni_.hasPendingException());
}
TEST_F(MaxJarraySizeTest, JustBelowLimit_GuardDoesNotTrip_AllocatesNormally) {
constexpr size_t kSmall = 16 * 1024;
static std::vector<char> dummy(kSmall, 0);
jbyteArray result = JniUtil::createJavaByteArrayWithSizeCheck(
jni_.env(), dummy.data(), kSmall);
ASSERT_NE(result, nullptr);
EXPECT_FALSE(jni_.hasPendingException());
EXPECT_EQ(jni_.env()->GetArrayLength(result),
static_cast<jsize>(kSmall));
}
TEST_F(MaxJarraySizeTest, JustUnderLimit_2GiBMinus1_GuardLetsThrough_ButPathStillProtected) {
constexpr size_t kJustUnderLimit = (static_cast<size_t>(1) << 31) - 1;
jni_.setSkipBackingStoreAbove(static_cast<jsize>(1024 * 1024));
static const char kDummy = 0;
JniUtil::createJavaByteArrayWithSizeCheck(
jni_.env(), &kDummy, kJustUnderLimit);
const jsize expected = static_cast<jsize>(kJustUnderLimit);
EXPECT_GT(expected, 0) << "precondition: expected jsize must be positive";
EXPECT_EQ(jni_.lastNewByteArrayLength(), expected)
<< "production must have attempted NewByteArray with the correct jsize";
}
TEST_F(MaxJarraySizeTest, BugDemo_ExactlyAtLimit_OneShlThirtyOne_GuardOffByOneThenJsizeCastFlipsNegative) {
constexpr size_t kExactlyAtLimit = static_cast<size_t>(1) << 31;
jni_.setSkipBackingStoreAbove(static_cast<jsize>(1));
static const char kDummy = 0;
jbyteArray result = JniUtil::createJavaByteArrayWithSizeCheck(
jni_.env(), &kDummy, kExactlyAtLimit);
EXPECT_EQ(result, nullptr)
<< "off-by-one bug: size==MAX_JARRAY_SIZE passes the `>` guard; "
"production proceeds to NewByteArray with INT_MIN jsize; "
"guard should use `>=`";
EXPECT_NONFATAL_FAILURE(
EXPECT_EQ(jni_.lastNewByteArrayLength(), static_cast<jsize>(0))
<< "off-by-one bug: NewByteArray should never be called when guard trips; "
"currently it is called with a negative (wrapped) jsize"
, "");
}
TEST_F(MaxJarraySizeTest, OverLimitByOne_OneShlThirtyOnePlusOne_GuardTripsCleanly) {
constexpr size_t kOneOverLimit = (static_cast<size_t>(1) << 31) + 1;
jbyteArray result = JniUtil::createJavaByteArrayWithSizeCheck(
jni_.env(), nullptr, kOneOverLimit);
EXPECT_EQ(result, nullptr);
EXPECT_TRUE(jni_.hasPendingException());
EXPECT_EQ(jni_.lastNewByteArrayLength(), static_cast<jsize>(0))
<< "guard must trip before any NewByteArray call";
}
}