From 24ebc0a09b764ddd6935c213ccd235c8e45260f2 Mon Sep 17 00:00:00 2001 From: Alexander Kuzmenkov <36882414+akuzm@users.noreply.github.com> Date: Wed, 13 May 2026 15:57:19 +0200 Subject: [PATCH 1/2] Fix int overflow in Arrow text body buffer computation The PG text values can be up to 1GB in size, so the computation for extending the Arrow text body buffer could overflow. Switch it to use the Size type. --- tsl/src/nodes/vector_agg/exec.c | 12 ++++--- .../expected/vector_agg_text_overflow.out | 36 +++++++++++++++++++ tsl/test/sql/CMakeLists.txt | 1 + tsl/test/sql/vector_agg_text_overflow.sql | 33 +++++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 tsl/test/expected/vector_agg_text_overflow.out create mode 100644 tsl/test/sql/vector_agg_text_overflow.sql diff --git a/tsl/src/nodes/vector_agg/exec.c b/tsl/src/nodes/vector_agg/exec.c index f3797030db3..2648e4fa60b 100644 --- a/tsl/src/nodes/vector_agg/exec.c +++ b/tsl/src/nodes/vector_agg/exec.c @@ -72,7 +72,7 @@ typedef struct uint64 *restrict validity; - int allocated_body_bytes; + Size allocated_body_bytes; uint8 *restrict body_buffer; uint32 *restrict offset_buffer; @@ -171,8 +171,8 @@ columnar_result_set_row(ColumnarResult *columnar_result, DecompressBatchState co } case DT_ArrowText: { - const int result_bytes = VARSIZE_ANY_EXHDR(datum); - const int required_body_bytes = + const Size result_bytes = VARSIZE_ANY_EXHDR(datum); + const Size required_body_bytes = pad_to_multiple(64, columnar_result->current_offset + result_bytes); if (required_body_bytes > columnar_result->allocated_body_bytes) { @@ -183,8 +183,10 @@ columnar_result_set_row(ColumnarResult *columnar_result, DecompressBatchState co * tuned manually on a few real data sets until this balance * looked somewhat acceptable. */ - const int new_body_bytes = - required_body_bytes * Min(10, Max(1.2, 1.2 * nrows / ((float) row + 1))) + 1; + const Size new_body_bytes = + (Size) (required_body_bytes * + Min(10, Max(1.2, 1.2 * nrows / ((double) row + 1)))) + + 1; Assert(new_body_bytes >= required_body_bytes); columnar_result->body_buffer = repalloc(columnar_result->body_buffer, new_body_bytes); diff --git a/tsl/test/expected/vector_agg_text_overflow.out b/tsl/test/expected/vector_agg_text_overflow.out new file mode 100644 index 00000000000..43c534fa2fa --- /dev/null +++ b/tsl/test/expected/vector_agg_text_overflow.out @@ -0,0 +1,36 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- Test for body_buffer overflow in columnar_result_set_row with +-- DT_ArrowText type. When vectorized text functions produce large +-- results, the body buffer growth computation could overflow int32. +set max_parallel_workers_per_gather to 0; +create table t_textoverflow (time timestamptz, a text); +select table_name from create_hypertable('t_textoverflow', 'time', + chunk_time_interval => interval '1 year'); + table_name +---------------- + t_textoverflow + +insert into t_textoverflow +select '2020-01-01'::timestamptz + i * interval '1 hour', + repeat('x', 100000) +from generate_series(1, 10) i; +alter table t_textoverflow set ( + timescaledb.compress, + timescaledb.compress_orderby = 'time' +); +select count(compress_chunk(c)) from show_chunks('t_textoverflow') c; + count +------- + 1 + +set timescaledb.debug_require_vector_agg to 'require'; +select sum(length(a || a)) from t_textoverflow; + sum +--------- + 2000000 + +reset timescaledb.debug_require_vector_agg; +drop table t_textoverflow cascade; +reset max_parallel_workers_per_gather; diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt index bc8d89d8c76..19cd39ab303 100644 --- a/tsl/test/sql/CMakeLists.txt +++ b/tsl/test/sql/CMakeLists.txt @@ -189,6 +189,7 @@ if(CMAKE_BUILD_TYPE MATCHES Debug) vector_agg_filter.sql vector_agg_grouping.sql vector_agg_text.sql + vector_agg_text_overflow.sql vector_agg_memory.sql vector_agg_modify_hypertable.sql vector_agg_segmentby.sql diff --git a/tsl/test/sql/vector_agg_text_overflow.sql b/tsl/test/sql/vector_agg_text_overflow.sql new file mode 100644 index 00000000000..14a7ea1c8e6 --- /dev/null +++ b/tsl/test/sql/vector_agg_text_overflow.sql @@ -0,0 +1,33 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- Test for body_buffer overflow in columnar_result_set_row with +-- DT_ArrowText type. When vectorized text functions produce large +-- results, the body buffer growth computation could overflow int32. + +set max_parallel_workers_per_gather to 0; + +create table t_textoverflow (time timestamptz, a text); + +select table_name from create_hypertable('t_textoverflow', 'time', + chunk_time_interval => interval '1 year'); + +insert into t_textoverflow +select '2020-01-01'::timestamptz + i * interval '1 hour', + repeat('x', 100000) +from generate_series(1, 10) i; + +alter table t_textoverflow set ( + timescaledb.compress, + timescaledb.compress_orderby = 'time' +); +select count(compress_chunk(c)) from show_chunks('t_textoverflow') c; + +set timescaledb.debug_require_vector_agg to 'require'; +select sum(length(a || a)) from t_textoverflow; +reset timescaledb.debug_require_vector_agg; + +drop table t_textoverflow cascade; + +reset max_parallel_workers_per_gather; From f498af396077bcad92993cf3a00f156b87a4037f Mon Sep 17 00:00:00 2001 From: Alexander Kuzmenkov <36882414+akuzm@users.noreply.github.com> Date: Fri, 29 May 2026 11:10:20 +0200 Subject: [PATCH 2/2] cleanup --- tsl/src/nodes/vector_agg/exec.c | 17 ++++++++--- .../expected/vector_agg_text_overflow.out | 30 +++++++++++++++++-- tsl/test/sql/vector_agg_text_overflow.sql | 28 +++++++++++++++-- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/tsl/src/nodes/vector_agg/exec.c b/tsl/src/nodes/vector_agg/exec.c index 2db6a005485..a147d2bc4d6 100644 --- a/tsl/src/nodes/vector_agg/exec.c +++ b/tsl/src/nodes/vector_agg/exec.c @@ -174,6 +174,16 @@ columnar_result_set_row(ColumnarResult *columnar_result, DecompressBatchState co const Size result_bytes = VARSIZE_ANY_EXHDR(datum); const Size required_body_bytes = pad_to_multiple(64, columnar_result->current_offset + result_bytes); + if (required_body_bytes > MaxAllocSize) + { + /* + * The body buffer is allocated through palloc, so it cannot + * exceed MaxAllocSize. + */ + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("vectorized text result exceeds %zu bytes", (Size) MaxAllocSize))); + } if (required_body_bytes > columnar_result->allocated_body_bytes) { /* @@ -183,10 +193,9 @@ columnar_result_set_row(ColumnarResult *columnar_result, DecompressBatchState co * tuned manually on a few real data sets until this balance * looked somewhat acceptable. */ - const Size new_body_bytes = - (Size) (required_body_bytes * - Min(10, Max(1.2, 1.2 * nrows / ((double) row + 1)))) + - 1; + const double desired_growth_factor = 1.2 * nrows / ((double) row + 1); + const double growth_factor = Min(10, Max(1.2, desired_growth_factor)); + Size new_body_bytes = Min(MaxAllocSize, (Size) required_body_bytes * growth_factor); Assert(new_body_bytes >= required_body_bytes); columnar_result->body_buffer = repalloc(columnar_result->body_buffer, new_body_bytes); diff --git a/tsl/test/expected/vector_agg_text_overflow.out b/tsl/test/expected/vector_agg_text_overflow.out index 43c534fa2fa..71bbb7c0d51 100644 --- a/tsl/test/expected/vector_agg_text_overflow.out +++ b/tsl/test/expected/vector_agg_text_overflow.out @@ -1,9 +1,10 @@ -- This file and its contents are licensed under the Timescale License. -- Please see the included NOTICE for copyright information and -- LICENSE-TIMESCALE for a copy of the license. --- Test for body_buffer overflow in columnar_result_set_row with +-- Tests for body buffer overflow in columnar_result_set_row with -- DT_ArrowText type. When vectorized text functions produce large --- results, the body buffer growth computation could overflow int32. +-- results, the body buffer growth computation could overflow int32, +-- and the total buffer size can exceed MaxAllocSize. set max_parallel_workers_per_gather to 0; create table t_textoverflow (time timestamptz, a text); select table_name from create_hypertable('t_textoverflow', 'time', @@ -33,4 +34,27 @@ select sum(length(a || a)) from t_textoverflow; reset timescaledb.debug_require_vector_agg; drop table t_textoverflow cascade; -reset max_parallel_workers_per_gather; +-- When the actual data doesn't fit into MaxAllocSize, produce an error message +-- instead of erroring out in palloc. +create table t_text_overflow (time timestamptz not null, text_col text not null); +select table_name from create_hypertable('t_text_overflow', 'time', + chunk_time_interval => interval '1 year'); + table_name +----------------- + t_text_overflow + +insert into t_text_overflow +select '2020-01-01'::timestamptz + i * interval '1 hour', 'a' +from generate_series(1, 1000) i; +alter table t_text_overflow set ( + timescaledb.compress, + timescaledb.compress_orderby = 'time' +); +select count(compress_chunk(c)) from show_chunks('t_text_overflow') c; + count +------- + 1 + +set timescaledb.debug_require_vector_agg to 'require'; +select count(text_col || repeat('x', 2000000)) from t_text_overflow; +ERROR: vectorized text result exceeds 1073741823 bytes diff --git a/tsl/test/sql/vector_agg_text_overflow.sql b/tsl/test/sql/vector_agg_text_overflow.sql index 14a7ea1c8e6..a5957e7f74b 100644 --- a/tsl/test/sql/vector_agg_text_overflow.sql +++ b/tsl/test/sql/vector_agg_text_overflow.sql @@ -2,9 +2,10 @@ -- Please see the included NOTICE for copyright information and -- LICENSE-TIMESCALE for a copy of the license. --- Test for body_buffer overflow in columnar_result_set_row with +-- Tests for body buffer overflow in columnar_result_set_row with -- DT_ArrowText type. When vectorized text functions produce large --- results, the body buffer growth computation could overflow int32. +-- results, the body buffer growth computation could overflow int32, +-- and the total buffer size can exceed MaxAllocSize. set max_parallel_workers_per_gather to 0; @@ -30,4 +31,27 @@ reset timescaledb.debug_require_vector_agg; drop table t_textoverflow cascade; +-- When the actual data doesn't fit into MaxAllocSize, produce an error message +-- instead of erroring out in palloc. +create table t_text_overflow (time timestamptz not null, text_col text not null); + +select table_name from create_hypertable('t_text_overflow', 'time', + chunk_time_interval => interval '1 year'); + +insert into t_text_overflow +select '2020-01-01'::timestamptz + i * interval '1 hour', 'a' +from generate_series(1, 1000) i; + +alter table t_text_overflow set ( + timescaledb.compress, + timescaledb.compress_orderby = 'time' +); +select count(compress_chunk(c)) from show_chunks('t_text_overflow') c; + +set timescaledb.debug_require_vector_agg to 'require'; +select count(text_col || repeat('x', 2000000)) from t_text_overflow; +reset timescaledb.debug_require_vector_agg; + +drop table t_text_overflow cascade; + reset max_parallel_workers_per_gather;