From f62d6ab0541fb453ae2f38993b55d5cc8a65b37f Mon Sep 17 00:00:00 2001 From: Mustafa Senoglu Date: Wed, 29 Jul 2026 13:19:39 +0300 Subject: [PATCH 1/2] fix: add integer overflow checks for arithmetic operations Add overflow/underflow detection for 64-bit integer arithmetic in agtype_add, agtype_sub, agtype_mul, agtype_neg, agtype_div, and agtype_mod operations. Previously, operations like 9223372036854775807 + 1 silently wrapped around to -9223372036854775808 instead of raising an error. This matches Neo4j and PostgreSQL behavior which reject integer overflow. Fixes #2471 Signed-off-by: Mustafa Senoglu --- regress/expected/agtype.out | 48 +++++++++++++++++ regress/sql/agtype.sql | 38 +++++++++++++ src/backend/utils/adt/agtype_ops.c | 85 +++++++++++++++++++++++++++--- 3 files changed, 164 insertions(+), 7 deletions(-) diff --git a/regress/expected/agtype.out b/regress/expected/agtype.out index 0f4775b88..c2a2a3af1 100644 --- a/regress/expected/agtype.out +++ b/regress/expected/agtype.out @@ -3906,6 +3906,54 @@ SELECT * FROM cypher('issue_2243', $$ 0::numeric (1 row) +-- +-- Integer overflow tests (issue #2471) +-- +SELECT create_graph('integer_overflow'); +NOTICE: graph "integer_overflow" has been created + create_graph +-------------- + +(1 row) + +-- Addition overflow +SELECT * FROM cypher('integer_overflow', $$ + RETURN 9223372036854775807 + 1 AS r + $$) AS (r agtype); +ERROR: integer addition overflow +-- Subtraction overflow (underflow) +SELECT * FROM cypher('integer_overflow', $$ + RETURN -9223372036854775808 - 1 AS r + $$) AS (r agtype); +ERROR: integer subtraction overflow +-- Multiplication overflow +SELECT * FROM cypher('integer_overflow', $$ + RETURN 9223372036854775807 * 2 AS r + $$) AS (r agtype); +ERROR: integer multiplication overflow +-- Negation overflow +SELECT * FROM cypher('integer_overflow', $$ + RETURN -(-9223372036854775808) AS r + $$) AS (r agtype); +ERROR: integer negation overflow +-- Division overflow (INT64_MIN / -1) +SELECT * FROM cypher('integer_overflow', $$ + RETURN -9223372036854775808 / -1 AS r + $$) AS (r agtype); +ERROR: integer division overflow +-- Modulo overflow (INT64_MIN % -1) +SELECT * FROM cypher('integer_overflow', $$ + RETURN -9223372036854775808 % -1 AS r + $$) AS (r agtype); +ERROR: integer modulo overflow +-- Cleanup overflow tests +SELECT drop_graph('integer_overflow', true); +NOTICE: graph "integer_overflow" has been dropped + drop_graph +------------ + +(1 row) + -- -- Cleanup -- diff --git a/regress/sql/agtype.sql b/regress/sql/agtype.sql index e13edb16d..7c047a5dc 100644 --- a/regress/sql/agtype.sql +++ b/regress/sql/agtype.sql @@ -1127,6 +1127,44 @@ SELECT * FROM cypher('issue_2243', $$ RETURN 9223372036854775807::integer % 9223372036854775807::numeric $$ ) as (result agtype); +-- +-- Integer overflow tests (issue #2471) +-- +SELECT create_graph('integer_overflow'); + +-- Addition overflow +SELECT * FROM cypher('integer_overflow', $$ + RETURN 9223372036854775807 + 1 AS r + $$) AS (r agtype); + +-- Subtraction overflow (underflow) +SELECT * FROM cypher('integer_overflow', $$ + RETURN -9223372036854775808 - 1 AS r + $$) AS (r agtype); + +-- Multiplication overflow +SELECT * FROM cypher('integer_overflow', $$ + RETURN 9223372036854775807 * 2 AS r + $$) AS (r agtype); + +-- Negation overflow +SELECT * FROM cypher('integer_overflow', $$ + RETURN -(-9223372036854775808) AS r + $$) AS (r agtype); + +-- Division overflow (INT64_MIN / -1) +SELECT * FROM cypher('integer_overflow', $$ + RETURN -9223372036854775808 / -1 AS r + $$) AS (r agtype); + +-- Modulo overflow (INT64_MIN % -1) +SELECT * FROM cypher('integer_overflow', $$ + RETURN -9223372036854775808 % -1 AS r + $$) AS (r agtype); + +-- Cleanup overflow tests +SELECT drop_graph('integer_overflow', true); + -- -- Cleanup -- diff --git a/src/backend/utils/adt/agtype_ops.c b/src/backend/utils/adt/agtype_ops.c index f7f45b467..69a7543ed 100644 --- a/src/backend/utils/adt/agtype_ops.c +++ b/src/backend/utils/adt/agtype_ops.c @@ -188,12 +188,21 @@ Datum agtype_add(PG_FUNCTION_ARGS) concat_to_agtype_string(&agtv_result, lhs, llen, rhs, rlen); } - /* Both are integers - regular addition */ + /* Both are integers - addition with overflow check */ else if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER) { + int64 result; + + if (__builtin_add_overflow(agtv_lhs->val.int_value, + agtv_rhs->val.int_value, &result)) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer addition overflow"))); + } + agtv_result.type = AGTV_INTEGER; - agtv_result.val.int_value = agtv_lhs->val.int_value + - agtv_rhs->val.int_value; + agtv_result.val.int_value = result; } /* Both are floats - regular addition */ else if (agtv_lhs->type == AGTV_FLOAT && agtv_rhs->type == AGTV_FLOAT) @@ -539,9 +548,18 @@ Datum agtype_sub(PG_FUNCTION_ARGS) if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER) { + int64 result; + + if (__builtin_sub_overflow(agtv_lhs->val.int_value, + agtv_rhs->val.int_value, &result)) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer subtraction overflow"))); + } + agtv_result.type = AGTV_INTEGER; - agtv_result.val.int_value = agtv_lhs->val.int_value - - agtv_rhs->val.int_value; + agtv_result.val.int_value = result; } else if (agtv_lhs->type == AGTV_FLOAT && agtv_rhs->type == AGTV_FLOAT) { @@ -635,11 +653,32 @@ Datum agtype_neg(PG_FUNCTION_ARGS) if (agtv_value->type == AGTV_INTEGER) { + if (agtv_value->val.int_value == PG_INT64_MIN) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer negation overflow"))); + } + agtv_result.type = AGTV_INTEGER; agtv_result.val.int_value = -agtv_value->val.int_value; } else if (agtv_value->type == AGTV_FLOAT) { + /* + * The parser may store 9223372036854775808 as a float because + * 9223372036854775808 exceeds INT64_MAX. Negating the float value + * equal to PG_INT64_MIN would produce a value outside the int64 + * range, so we must still raise an overflow error to stay + * consistent with the integer path. + */ + if (agtv_value->val.float_value == (double)PG_INT64_MIN) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer negation overflow"))); + } + agtv_result.type = AGTV_FLOAT; agtv_result.val.float_value = -agtv_value->val.float_value; } @@ -692,9 +731,18 @@ Datum agtype_mul(PG_FUNCTION_ARGS) if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER) { + int64 result; + + if (__builtin_mul_overflow(agtv_lhs->val.int_value, + agtv_rhs->val.int_value, &result)) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer multiplication overflow"))); + } + agtv_result.type = AGTV_INTEGER; - agtv_result.val.int_value = agtv_lhs->val.int_value * - agtv_rhs->val.int_value; + agtv_result.val.int_value = result; } else if (agtv_lhs->type == AGTV_FLOAT && agtv_rhs->type == AGTV_FLOAT) { @@ -795,6 +843,14 @@ Datum agtype_div(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } + if (agtv_lhs->val.int_value == PG_INT64_MIN && + agtv_rhs->val.int_value == -1) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer division overflow"))); + } + agtv_result.type = AGTV_INTEGER; agtv_result.val.int_value = agtv_lhs->val.int_value / agtv_rhs->val.int_value; @@ -912,6 +968,21 @@ Datum agtype_mod(PG_FUNCTION_ARGS) if (agtv_lhs->type == AGTV_INTEGER && agtv_rhs->type == AGTV_INTEGER) { + if (agtv_rhs->val.int_value == 0) + { + ereport(ERROR, (errcode(ERRCODE_DIVISION_BY_ZERO), + errmsg("division by zero"))); + PG_RETURN_NULL(); + } + + if (agtv_lhs->val.int_value == PG_INT64_MIN && + agtv_rhs->val.int_value == -1) + { + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer modulo overflow"))); + } + agtv_result.type = AGTV_INTEGER; agtv_result.val.int_value = agtv_lhs->val.int_value % agtv_rhs->val.int_value; From 68c1403ebe3814a5376c14f21e0b571db4e818a8 Mon Sep 17 00:00:00 2001 From: mmustafasenoglu Date: Thu, 30 Jul 2026 11:52:25 +0300 Subject: [PATCH 2/2] fix: correct negation overflow check for float path + add div/mod overflow The previous check compared float_value == (double)PG_INT64_MIN, but the parser stores 9223372036854775808 as a positive float (AGTV_FLOAT), so the comparison with the negative PG_INT64_MIN was always false. Fix: compare float_value == -(double)PG_INT64_MIN to match the positive float representation. Also adds division and modulo overflow checks for INT64_MIN / -1. Signed-off-by: Mustafa Senoglu --- src/backend/utils/adt/agtype_ops.c | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/backend/utils/adt/agtype_ops.c b/src/backend/utils/adt/agtype_ops.c index 69a7543ed..d7838532a 100644 --- a/src/backend/utils/adt/agtype_ops.c +++ b/src/backend/utils/adt/agtype_ops.c @@ -665,14 +665,7 @@ Datum agtype_neg(PG_FUNCTION_ARGS) } else if (agtv_value->type == AGTV_FLOAT) { - /* - * The parser may store 9223372036854775808 as a float because - * 9223372036854775808 exceeds INT64_MAX. Negating the float value - * equal to PG_INT64_MIN would produce a value outside the int64 - * range, so we must still raise an overflow error to stay - * consistent with the integer path. - */ - if (agtv_value->val.float_value == (double)PG_INT64_MIN) + if (agtv_value->val.float_value == -(double)PG_INT64_MIN) { ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),