diff --git a/core/vm.cpp b/core/vm.cpp index 27629fd67..faf6d54c2 100644 --- a/core/vm.cpp +++ b/core/vm.cpp @@ -2559,36 +2559,45 @@ class Interpreter { case BOP_SHIFT_L: { if (rhs.v.d < 0) throw makeError(ast.location, "shift by negative exponent."); - int64_t long_l = lhs.v.d; - int64_t long_r = rhs.v.d; + + int64_t long_l = safeDoubleToInt64(lhs.v.d, ast.location); + int64_t long_r = safeDoubleToInt64(rhs.v.d, ast.location); long_r = long_r % 64; + + // Additional safety check for left shifts to prevent undefined behavior + if (long_r >= 1 && long_l >= (1LL << (63 - long_r))) { + throw makeError(ast.location, + "numeric value outside safe integer range for bitwise operation."); + } + scratch = makeNumber(long_l << long_r); } break; case BOP_SHIFT_R: { if (rhs.v.d < 0) throw makeError(ast.location, "shift by negative exponent."); - int64_t long_l = lhs.v.d; - int64_t long_r = rhs.v.d; + + int64_t long_l = safeDoubleToInt64(lhs.v.d, ast.location); + int64_t long_r = safeDoubleToInt64(rhs.v.d, ast.location); long_r = long_r % 64; scratch = makeNumber(long_l >> long_r); } break; case BOP_BITWISE_AND: { - int64_t long_l = lhs.v.d; - int64_t long_r = rhs.v.d; + int64_t long_l = safeDoubleToInt64(lhs.v.d, ast.location); + int64_t long_r = safeDoubleToInt64(rhs.v.d, ast.location); scratch = makeNumber(long_l & long_r); } break; case BOP_BITWISE_XOR: { - int64_t long_l = lhs.v.d; - int64_t long_r = rhs.v.d; + int64_t long_l = safeDoubleToInt64(lhs.v.d, ast.location); + int64_t long_r = safeDoubleToInt64(rhs.v.d, ast.location); scratch = makeNumber(long_l ^ long_r); } break; case BOP_BITWISE_OR: { - int64_t long_l = lhs.v.d; - int64_t long_r = rhs.v.d; + int64_t long_l = safeDoubleToInt64(lhs.v.d, ast.location); + int64_t long_r = safeDoubleToInt64(rhs.v.d, ast.location); scratch = makeNumber(long_l | long_r); } break; @@ -3406,4 +3415,21 @@ std::vector jsonnet_vm_execute_stream(Allocator *alloc, const AST * return vm.manifestStream(string_output); } +inline int64_t safeDoubleToInt64(double value, const internal::LocationRange& loc) { + if (std::isnan(value) || std::isinf(value)) { + throw internal::StaticError(loc, "numeric value is not finite"); + } + + // Constants for safe double-to-int conversion + // Jsonnet uses IEEE 754 doubles, which precisely represent integers in the range [-2^53, 2^53]. + constexpr int64_t DOUBLE_MAX_SAFE_INTEGER = 1LL << 53; + constexpr int64_t DOUBLE_MIN_SAFE_INTEGER = -(1LL << 53); + + // Check if the value is within the safe integer range + if (value < DOUBLE_MIN_SAFE_INTEGER || value > DOUBLE_MAX_SAFE_INTEGER) { + throw internal::StaticError(loc, "numeric value outside safe integer range for bitwise operation."); + } + return static_cast(value); +} + } // namespace jsonnet::internal diff --git a/core/vm.h b/core/vm.h index 830972eb1..78b55028c 100644 --- a/core/vm.h +++ b/core/vm.h @@ -129,6 +129,25 @@ std::vector jsonnet_vm_execute_stream( double gc_min_objects, double gc_growth_trigger, const VmNativeCallbackMap &natives, JsonnetImportCallback *import_callback, void *import_callback_ctx, bool string_output); +/** Safely converts a double to an int64_t, with range and validity checks. + * + * This function is used primarily for bitwise operations which require integer operands. + * It performs two safety checks: + * 1. Verifies the value is finite (not NaN or Infinity) + * 2. Ensures the value is within the safe integer range [-2^53, 2^53] + * + * The safe integer range limitation is necessary because IEEE 754 double precision + * floating point numbers can only precisely represent integers in the range [-2^53, 2^53]. + * Beyond this range, precision is lost, which would lead to unpredictable results + * in bitwise operations that depend on exact bit patterns. + * + * \param value The double value to convert + * \param loc The location in source code (for error reporting) + * \throws StaticError if value is not finite or outside the safe integer range + * \returns The value converted to int64_t + */ +int64_t safeDoubleToInt64(double value, const LocationRange& loc); + } // namespace jsonnet::internal #endif diff --git a/test_suite/error.integer_conversion.jsonnet b/test_suite/error.integer_conversion.jsonnet new file mode 100644 index 000000000..546f4b92e --- /dev/null +++ b/test_suite/error.integer_conversion.jsonnet @@ -0,0 +1,3 @@ +// Value just beyond MAX_SAFE_INTEGER (2^53) +local beyond_max = 9007199254740994; // 2^53 + 1 +beyond_max << 1 // Should throw error "numeric value outside safe integer range for bitwise operation" diff --git a/test_suite/error.integer_conversion.jsonnet.golden b/test_suite/error.integer_conversion.jsonnet.golden new file mode 100644 index 000000000..275de8634 --- /dev/null +++ b/test_suite/error.integer_conversion.jsonnet.golden @@ -0,0 +1 @@ +STATIC ERROR: error.integer_conversion.jsonnet:3:1-16: numeric value outside safe integer range for bitwise operation. diff --git a/test_suite/error.integer_left_shift.jsonnet b/test_suite/error.integer_left_shift.jsonnet new file mode 100644 index 000000000..40ac93d5d --- /dev/null +++ b/test_suite/error.integer_left_shift.jsonnet @@ -0,0 +1,3 @@ +// Test that left-shifting a value that would exceed int64_t range throws an error +local large_value = 1 << 62; // 2^62 +large_value << 2 // Would be 2^64, exceeding signed 64-bit integer range diff --git a/test_suite/error.integer_left_shift.jsonnet.golden b/test_suite/error.integer_left_shift.jsonnet.golden new file mode 100644 index 000000000..f3cae8b45 --- /dev/null +++ b/test_suite/error.integer_left_shift.jsonnet.golden @@ -0,0 +1 @@ +STATIC ERROR: error.integer_left_shift.jsonnet:3:1-17: numeric value outside safe integer range for bitwise operation. diff --git a/test_suite/error.integer_left_shift_runtime.jsonnet b/test_suite/error.integer_left_shift_runtime.jsonnet new file mode 100644 index 000000000..f4b54fe16 --- /dev/null +++ b/test_suite/error.integer_left_shift_runtime.jsonnet @@ -0,0 +1,2 @@ +local large_value = 4503599627370496; +large_value << 11 diff --git a/test_suite/error.integer_left_shift_runtime.jsonnet.golden b/test_suite/error.integer_left_shift_runtime.jsonnet.golden new file mode 100644 index 000000000..696f974ad --- /dev/null +++ b/test_suite/error.integer_left_shift_runtime.jsonnet.golden @@ -0,0 +1,2 @@ +RUNTIME ERROR: numeric value outside safe integer range for bitwise operation. + error.integer_left_shift_runtime.jsonnet:2:1-18 diff --git a/test_suite/safe_integer_conversion.jsonnet b/test_suite/safe_integer_conversion.jsonnet new file mode 100644 index 000000000..3e9709542 --- /dev/null +++ b/test_suite/safe_integer_conversion.jsonnet @@ -0,0 +1,44 @@ +// Test values at boundary of safe integer range +local max_safe = 9007199254740992; // 2^53 +local min_safe = -9007199254740992; // -2^53 + +std.assertEqual(max_safe & 1, 0) && // Check 2^53 +std.assertEqual(min_safe & 1, 0) && // Check -2^53 +std.assertEqual((max_safe - 1) & 1, 1) && // Check 2^53 - 1 +std.assertEqual((min_safe + 1) & 1, 1) && // Check -2^53 + 1 + +std.assertEqual(~(max_safe - 1), min_safe) && // ~(2^53 - 1) == -2^53 +std.assertEqual(~(min_safe + 1), max_safe - 2) && // ~(-2^53 + 1) == 2^53 - 2 + +// Test basic values +std.assertEqual(~0, -1) && +std.assertEqual(~1, -2) && +std.assertEqual(~(-1), 0) && + +// Test shift operations with large values at safe boundary +// (2^53 - 1) right shift by 4 bits +std.assertEqual((max_safe - 1) >> 4, 562949953421311) && +// MAX_SAFE_INTEGER (2^53) right shift by 1 bit +std.assertEqual(max_safe >> 1, 4503599627370496) && // 2^52 +// MIN_SAFE_INTEGER (-2^53) right shift by 1 bit +std.assertEqual(min_safe >> 1, -4503599627370496) && // -2^52 + +// Cannot left shift 2^53 without potential overflow/loss of precision issues +// depending on the shift amount, but can shift smaller numbers up to it. +// (2^52) left shift by 1 bit (result is 2^53) +std.assertEqual((max_safe >> 1) << 1, max_safe) && +// (-2^52) left shift by 1 bit (result is -2^53) +std.assertEqual((min_safe >> 1) << 1, min_safe) && + +// Test larger values within safe range +std.assertEqual(~123456789, -123456790) && +std.assertEqual(~(-987654321), 987654320) && + +// Test other bitwise operations +std.assertEqual(123 & 456, 72) && +std.assertEqual(123 | 456, 507) && +std.assertEqual(123 ^ 456, 435) && +std.assertEqual(123 << 2, 492) && +std.assertEqual(123 >> 2, 30) && + +true diff --git a/test_suite/safe_integer_conversion.jsonnet.fmt.golden b/test_suite/safe_integer_conversion.jsonnet.fmt.golden new file mode 100644 index 000000000..cd508e323 --- /dev/null +++ b/test_suite/safe_integer_conversion.jsonnet.fmt.golden @@ -0,0 +1,44 @@ +// Test values at boundary of safe integer range +local max_safe = 9007199254740992; // 2^53 +local min_safe = -9007199254740992; // -2^53 + +std.assertEqual(max_safe & 1, 0) && // Check 2^53 +std.assertEqual(min_safe & 1, 0) && // Check -2^53 +std.assertEqual((max_safe - 1) & 1, 1) && // Check 2^53 - 1 +std.assertEqual((min_safe + 1) & 1, 1) && // Check -2^53 + 1 + +std.assertEqual(~(max_safe - 1), min_safe) && // ~(2^53 - 1) == -2^53 +std.assertEqual(~(min_safe + 1), max_safe - 2) && // ~(-2^53 + 1) == 2^53 - 2 + +// Test basic values +std.assertEqual(~0, -1) && +std.assertEqual(~1, -2) && +std.assertEqual(~(-1), 0) && + +// Test shift operations with large values at safe boundary +// (2^53 - 1) right shift by 4 bits +std.assertEqual((max_safe - 1) >> 4, 562949953421311) && +// MAX_SAFE_INTEGER (2^53) right shift by 1 bit +std.assertEqual(max_safe >> 1, 4503599627370496) && // 2^52 +// MIN_SAFE_INTEGER (-2^53) right shift by 1 bit +std.assertEqual(min_safe >> 1, -4503599627370496) && // -2^52 + +// Cannot left shift 2^53 without potential overflow/loss of precision issues +// depending on the shift amount, but can shift smaller numbers up to it. +// (2^52) left shift by 1 bit (result is 2^53) +std.assertEqual((max_safe >> 1) << 1, max_safe) && +// (-2^52) left shift by 1 bit (result is -2^53) +std.assertEqual((min_safe >> 1) << 1, min_safe) && + +// Test larger values within safe range +std.assertEqual(~123456789, -123456790) && +std.assertEqual(~(-987654321), 987654320) && + +// Test other bitwise operations +std.assertEqual(123 & 456, 72) && +std.assertEqual(123 | 456, 507) && +std.assertEqual(123 ^ 456, 435) && +std.assertEqual(123 << 2, 492) && +std.assertEqual(123 >> 2, 30) && + +true diff --git a/test_suite/safe_integer_conversion.jsonnet.golden b/test_suite/safe_integer_conversion.jsonnet.golden new file mode 100644 index 000000000..27ba77dda --- /dev/null +++ b/test_suite/safe_integer_conversion.jsonnet.golden @@ -0,0 +1 @@ +true