This is an automated email from the ASF dual-hosted git repository.

jrgemignani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git


The following commit(s) were added to refs/heads/master by this push:
     new fafdaa1c Fix stack overflow and precision loss in toFloatList() 
conversion (#2451)
fafdaa1c is described below

commit fafdaa1c0ef5f4ab8ff09482f20a382b845dc9e5
Author: Greg Felice <[email protected]>
AuthorDate: Thu Jul 2 10:39:37 2026 -0400

    Fix stack overflow and precision loss in toFloatList() conversion (#2451)
    
    toFloatList()'s AGTV_FLOAT branch formatted each element with
    sprintf(buffer, "%f", ...) into a fixed 64-byte stack buffer and then
    re-parsed the string back into a float. This had two defects:
    
      1. Stack overflow. "%f" prints the full integer part with no width
         limit, so a large magnitude overflows the 64-byte buffer. The value
         is query-reachable: RETURN toFloatList([1.0e308]) needs ~317 bytes
         (309 integer digits + ".000000") and smashes the stack. This is the
         issue reported in #2410.
    
      2. Precision loss. "%f" emits only 6 fractional digits, so the
         format-and-reparse round trip was lossy -- toFloatList([0.123456789])
         returned 0.123457.
    
    The element is already a float8, so the whole format/reparse step is
    unnecessary. Assign elem->val.float_value directly. This removes the
    stack buffer entirely (no magic buffer size to justify) and fixes both
    the overflow and the precision loss at once.
    
    Also harden toStringList(): its "%.*g"/"%ld" conversions use bounded
    formats and were never overflow-prone, but switch them from sprintf to
    snprintf as defensive depth.
    
    Add regression coverage to regress/sql/expr.sql for both the large
    magnitude case (no overflow) and precision preservation.
    
    This reimplements the fix originally proposed by David Christensen in
    #2410, whose report identified the sprintf overflow.
    
    Co-authored-by: David Christensen <[email protected]>
---
 regress/expected/expr.out      | 20 ++++++++++++++++++++
 regress/sql/expr.sql           | 10 ++++++++++
 src/backend/utils/adt/agtype.c | 19 +++++++++++--------
 3 files changed, 41 insertions(+), 8 deletions(-)

diff --git a/regress/expected/expr.out b/regress/expected/expr.out
index d6b9ac15..806a6f65 100644
--- a/regress/expected/expr.out
+++ b/regress/expected/expr.out
@@ -3548,6 +3548,26 @@ $$) AS (toFloatList agtype);
  [1.20002]
 (1 row)
 
+-- large magnitudes must not overflow the conversion (regression: unbounded
+-- sprintf into a fixed stack buffer overflowed for values like 1.0e308)
+SELECT * FROM cypher('expr', $$
+    RETURN toFloatList([1.0e308, -1.0e308])
+$$) AS (toFloatList agtype);
+    tofloatlist    
+-------------------
+ [1e+308, -1e+308]
+(1 row)
+
+-- precision must be preserved (regression: "%f" format truncated to 6 digits,
+-- so 0.123456789 came back as 0.123457)
+SELECT * FROM cypher('expr', $$
+    RETURN toFloatList([0.123456789])
+$$) AS (toFloatList agtype);
+  tofloatlist  
+---------------
+ [0.123456789]
+(1 row)
+
 -- should return null
 SELECT * FROM cypher('expr', $$
     RETURN toFloatList(['true'])
diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql
index 251349ce..d4d900a1 100644
--- a/regress/sql/expr.sql
+++ b/regress/sql/expr.sql
@@ -1520,6 +1520,16 @@ $$) AS (toFloatList agtype);
 SELECT * FROM cypher('expr', $$
     RETURN toFloatList([1.20002])
 $$) AS (toFloatList agtype);
+-- large magnitudes must not overflow the conversion (regression: unbounded
+-- sprintf into a fixed stack buffer overflowed for values like 1.0e308)
+SELECT * FROM cypher('expr', $$
+    RETURN toFloatList([1.0e308, -1.0e308])
+$$) AS (toFloatList agtype);
+-- precision must be preserved (regression: "%f" format truncated to 6 digits,
+-- so 0.123456789 came back as 0.123457)
+SELECT * FROM cypher('expr', $$
+    RETURN toFloatList([0.123456789])
+$$) AS (toFloatList agtype);
 -- should return null
 SELECT * FROM cypher('expr', $$
     RETURN toFloatList(['true'])
diff --git a/src/backend/utils/adt/agtype.c b/src/backend/utils/adt/agtype.c
index bf69bf1f..0e1a7963 100644
--- a/src/backend/utils/adt/agtype.c
+++ b/src/backend/utils/adt/agtype.c
@@ -7099,8 +7099,6 @@ Datum age_tofloatlist(PG_FUNCTION_ARGS)
     int count;
     int i;
     bool is_valid = false;
-    float8 float_num;
-    char buffer[64];
 
     /* check for null */
     if (PG_ARGISNULL(0))
@@ -7160,11 +7158,16 @@ Datum age_tofloatlist(PG_FUNCTION_ARGS)
 
         case AGTV_FLOAT:
 
+            /*
+             * The element is already a float8, so assign it directly. The
+             * previous approach formatted it to a string with sprintf() and
+             * re-parsed it: that both overflowed a fixed 64-byte stack buffer
+             * for large magnitudes (e.g. 1.0e308 needs ~317 chars) and lost
+             * precision, since "%f" truncates to 6 fractional digits. Direct
+             * assignment avoids both problems.
+             */
             float_elem.type = AGTV_FLOAT;
-            float_num = elem->val.float_value;
-            sprintf(buffer, "%f", float_num);
-            string = buffer;
-            float_elem.val.float_value = float8in_internal_null(string, NULL, 
"double precision", string, &is_valid);
+            float_elem.val.float_value = elem->val.float_value;
             agis_result.res = push_agtype_value(&agis_result.parse_state, 
WAGT_ELEM, &float_elem);
 
             break;
@@ -8146,7 +8149,7 @@ Datum age_tostringlist(PG_FUNCTION_ARGS)
 
         case AGTV_FLOAT:
 
-            sprintf(buffer, "%.*g", DBL_DIG, elem->val.float_value);
+            snprintf(buffer, sizeof(buffer), "%.*g", DBL_DIG, 
elem->val.float_value);
             string_elem.val.string.val = pstrdup(buffer);
             string_elem.val.string.len = strlen(buffer);
 
@@ -8157,7 +8160,7 @@ Datum age_tostringlist(PG_FUNCTION_ARGS)
 
         case AGTV_INTEGER:
 
-            sprintf(buffer, "%ld", elem->val.int_value);
+            snprintf(buffer, sizeof(buffer), "%ld", elem->val.int_value);
             string_elem.val.string.val = pstrdup(buffer);
             string_elem.val.string.len = strlen(buffer);
 

Reply via email to