Search before asking
Version
Affected: Apache Doris 4.1.4 — BE reports doris-4.1.4-rc04-ad35a140c7f (the newest
released build; images apache/doris:fe-4.1.4 + be-4.1.4, pushed 2026-09-11). Single FE +
single BE, Nereids planner, otherwise default configuration, Linux x86_64.
Also affected: current master. There is no nightly/master Doris image, so I built
apache/doris master commit c4dee4bd5e82c6f0f380e0243cbaf265999e6a6a from source with
apache/doris:build-env-ldb-toolchain-latest and ran the identical battery against a
single-FE/single-BE cluster from that build. The clone's HEAD was verified equal to GitHub's
master HEAD (2026-09-17T03:34:48Z). BE reports doris-0.0.0-Unknown because a local build
carries no version stamp; the shipped default batch_size is 8160 there too.
master reproduces every measurement identically to 4.1.4 — not approximately, the same
counts row for row:
| measurement |
4.1.4 |
master c4dee4bd |
first_value(varchar) wrong @ batch_size 4 / 2 / 1 |
4 / 10 / 9 |
4 / 10 / 9 |
| longest bogus string @ 4 / 2 / 1 |
229 / 525 / 772 |
229 / 525 / 772 |
first_value(int) wrong @ batch_size 38 / 37 / 4 / 1 |
392 / 908 / 15 888 / 17 180 |
392 / 908 / 15 888 / 17 180 |
| onset rule at partition size 10 000 / 5 000 / 2 000 |
bs 38 / 19 / 7 |
38 / 19 / 7 |
2 560 000-row partition @ default batch_size 8160 |
454 720 wrong |
454 720 wrong |
1 280 000-row partition @ default batch_size 8160 |
0 |
0 |
nth_value string length |
total_length=4294967303 |
total_length=4294967296 (2³², message now also prints limit=4294967295) |
and master crashed the BE during the run. Because it is a locally built binary the trace
carries full source lines, which is the clearest statement of the bug available:
*** SIGSEGV invalid permissions for mapped object (@0x7f15c87fd000) received by PID 2333 ***
2# memcpy at ./be/build_Release/../src/glibc-compatibility/memcpy/memcpy_x86_64.cpp:219
3# doris::ColumnStr<unsigned int>::insert_from(doris::IColumn const&, unsigned long)
at ../src/core/column/column_string.h:188
4# doris::IAggregateFunction::insert_result_into_range(char const*, doris::IColumn&, unsigned long, unsigned long) const
at ../src/exprs/aggregate/aggregate_function.h
5# doris::AnalyticSinkLocalState::_insert_result_info(long, long)
at ./be/src/exec/operator/analytic_sink_operator.cpp:404
6# doris::AnalyticSinkLocalState::_get_next_for_unbounded_rows(long, long)
at ./be/src/exec/operator/analytic_sink_operator.cpp:248
8# doris::AnalyticSinkOperatorX::sink_impl(doris::RuntimeState*, doris::Block*, bool)
at ./be/src/exec/operator/analytic_sink_operator.cpp:759
A memcpy inside ColumnStr::insert_from, reached from the analytic sink writing out an
aggregate's saved value — i.e. copying a string through an offset that no longer describes the
row it was saved for.
No fix is in flight: the most recent commit touching analytic_sink_operator.cpp is #67274
(2026-08-31) and the most recent touching aggregate_function_window.h is #64864 (2026-06-26);
neither addresses the saved-pointer invalidation.
What's Wrong?
first_value() over a window silently returns values that are not in the window — and on a
VARCHAR column, values that are not in the table at all. On the same data the BE can also
abort the query with a bogus 4 GB string length, or crash outright.
The trigger is that the analytic operator evicts input rows it believes are no longer needed,
while first_value is still holding a raw pointer plus an absolute row offset into exactly
the columns being evicted.
1. first_value(varchar) returns a string that is nowhere in the table
With PARTITION BY c, first_value(c) must equal the row's own c — every partition contains
one distinct value, so this is an exact oracle needing no reference engine. 20 000 rows, every
value at most 8 characters:
batch_size |
rows wrong |
longest bogus value returned |
| 65535 |
0 |
— |
| 8160 (shipped default) |
0 |
— |
| 8 |
0 |
— |
| 4 |
4 |
229 chars |
| 2 |
10 |
525 chars |
| 1 |
9 |
772 chars |
A sample row: c = 'xyz', so the correct answer is 'xyz', and Doris returns
azczazaczcbczcbczcbczyaxabxxaaxbbxabaxabcayabxacazayxaxcxyzxaxyxxxx… (229 characters)
That is a run of adjacent values read out of the string column through a wrong offset/length
pair — the correct 3-character value is visible inside it.
2. first_value(int) is wrong for most of the table
Same shape on an INT column, ground truth computed in Python (PARTITION BY e with e boolean,
so two partitions of ~10 000 rows):
batch_size |
rows wrong (of 20 000) |
| 65535 / 8160 / 64 / 40 |
0 |
| 39 |
0 |
| 38 |
392 |
| 37 |
908 |
| 4 |
15 888 (79%) |
| 1 |
17 180 (86%) |
No warning, no error — the query simply returns wrong numbers for most rows.
3. It reproduces at the shipped batch_size on an ordinary table
batch_size only makes it cheap to demonstrate. What actually matters is the partition size in
blocks, so the same defect is reached with batch_size left completely alone (shipped default
8160) by making one partition large — the threshold being 257 * 8160 = 2 097 120 rows.
Single partition, nothing configured:
| rows in the partition |
wrong at default batch_size (8160) |
wrong at batch_size = 65535 |
| 160 000 – 1 280 000 |
0 |
0 |
| 2 560 000 |
454 720 (18%) |
0 |
The boundary lands exactly where the 257 * batch_size rule predicts (1 280 000 < 2 097 120 <
2 560 000), and raising batch_size to 65535 moves the threshold out of reach again, which
isolates the block count as the cause.
One caveat worth knowing when reproducing: the block count is produced by the scan, so it
depends on how the table is physically laid out, not only on batch_size. On a table I had
built by repeated INSERT INTO … SELECT doubling (many small rowsets) the same 2 560 000-row
partition was wrong at batch_size = 65535 as well, where a table of the same size written by a
single INSERT … SELECT was clean at 65535. So a fragmented table reaches 257 blocks with fewer
rows, and raising batch_size does not reliably protect it.
4. The exact trigger
The onset is not fuzzy. Corruption begins exactly when
partition_size > 257 * batch_size
which is block_num = 256 in _remove_unused_rows(), plus the + 1 in its guard. Verified by
fixing the partition size and walking batch_size across the predicted boundary:
| partition size |
predicted onset |
bs = pred+1 |
bs = pred |
bs = pred−1 |
| 10 000 |
bs ≤ 38 |
0 wrong |
392 wrong |
908 wrong |
| 5 000 |
bs ≤ 19 |
0 wrong |
392 wrong |
1 424 wrong |
| 2 000 |
bs ≤ 7 |
0 wrong |
1 940 wrong |
4 520 wrong |
Three for three, on the row exactly.
5. The same corruption as an error, and as a crash
nth_value(varchar, 3) over ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING, 20 000 rows,
batch_size = 4:
errCode = 2, detailMessage = [E-3113] string column length is too large:
total_length=4294967303, element_number=4, rows=0
4294967303 is 2³² + 7 — the string length arithmetic wrapped a 32-bit counter, which is the
same wrong offset/length pair that produces the 229-character first_value answers.
The same shape took the BE down on both builds — on 4.1.4 at batch_size = 1, and on the
master build during the battery. Stack from the 4.1.4 BE log (the master one, with full source
lines, is in the Version section):
2# doris::ColumnStr<unsigned int>::insert_from(doris::IColumn const&, unsigned long)
at be/src/core/column/column_string.h:71
3# doris::IAggregateFunction::insert_result_into_range(char const*, doris::IColumn&, unsigned long, unsigned long) const
at be/src/exprs/aggregate/aggregate_function.h:252
4# doris::AnalyticSinkLocalState::_insert_result_info(long, long)
at be/src/exec/operator/analytic_sink_operator.cpp:406
5# doris::AnalyticSinkLocalState::_get_next_for_unbounded_rows(long, long)
at be/src/exec/operator/analytic_sink_operator.cpp:250
6# doris::AnalyticSinkLocalState::_execute_impl(doris::RuntimeState*)
7# doris::AnalyticSinkOperatorX::sink_impl(doris::RuntimeState*, doris::Block*, bool)
Frame 2 is the aggregate's saved value being copied out: to.insert_from(*_ptr, _offset). It is
reading through a pointer/offset that no longer describes the row it was saved for.
What You Expected?
first_value(c) OVER (PARTITION BY c …) must return the row's own c for every row, and
first_value(a) must return the first a of the partition in window order — regardless of
batch_size, partition size, or whether the operator decided to release memory mid-partition.
The answer must not depend on how the input was split into vectorised blocks.
At minimum, a query must not silently return values that do not exist in the input, and must not
crash the backend.
How to Reproduce?
Single FE + single BE, default configuration:
docker network create --subnet=172.30.0.0/24 doris-net
docker run -d --name fe --network doris-net --ip 172.30.0.2 \
-e FE_SERVERS="fe1:172.30.0.2:9010" -e FE_ID=1 -p 9030:9030 apache/doris:fe-4.1.4
docker run -d --name be --network doris-net --ip 172.30.0.3 \
-e FE_SERVERS="fe1:172.30.0.2:9010" -e BE_ADDR="172.30.0.3:9050" apache/doris:be-4.1.4
A. Smallest form — exact oracle, no reference engine needed.
Load 20 000 rows of (id INT, c VARCHAR(255)) where c is a random string of at most 8
characters, then:
SET batch_size = 4; -- 20 000 rows / 4 = 5 000 blocks per scan
SELECT id, c,
first_value(c) OVER (PARTITION BY c ORDER BY c, id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS w
FROM t
HAVING w <> c; -- must be empty; returns rows whose w is 229 chars long
Because the window partitions by c, w can only be c. Any returned row is a wrong answer.
SET batch_size = 65535 on the same table returns nothing, confirming the data is fine.
B. At the shipped batch_size, no session variable touched.
Build (id INT, a INT, g BIGINT, e INT) with e = 0 on every row, so the whole table is one
partition, and grow it past 257 * 8160 = 2 097 120 rows:
-- batch_size left at its shipped default of 8160
SELECT count(*) FROM (
SELECT id, first_value(a) OVER (PARTITION BY e ORDER BY g, id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS w
FROM big
) s
WHERE w <> (SELECT a FROM big ORDER BY g, id LIMIT 1);
-- 1 280 000 rows -> 0 (below 257*8160)
-- 2 560 000 rows -> 454 720 (must be 0)
SET batch_size = 65535 returns 0 for both, which isolates the block count as the cause.
C. The crash / 4 GB length.
On the 20 000-row varchar table:
SET batch_size = 4;
SELECT id, nth_value(c, 3) OVER (PARTITION BY b % 13 ORDER BY c, id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS w
FROM t;
-- [E-3113] string column length is too large: total_length=4294967303
SET batch_size = 1; -- took the BE down in my run
A reproduction script with the ground-truth oracles for all of the above is attached to this
report.
Anything Else?
No response
Are you willing to submit PR?
Code of Conduct
Search before asking
Version
Affected: Apache Doris 4.1.4 — BE reports
doris-4.1.4-rc04-ad35a140c7f(the newestreleased build; images
apache/doris:fe-4.1.4+be-4.1.4, pushed 2026-09-11). Single FE +single BE, Nereids planner, otherwise default configuration, Linux x86_64.
Also affected: current
master. There is no nightly/master Doris image, so I builtapache/dorismaster commitc4dee4bd5e82c6f0f380e0243cbaf265999e6a6afrom source withapache/doris:build-env-ldb-toolchain-latestand ran the identical battery against asingle-FE/single-BE cluster from that build. The clone's HEAD was verified equal to GitHub's
masterHEAD (2026-09-17T03:34:48Z). BE reportsdoris-0.0.0-Unknownbecause a local buildcarries no version stamp; the shipped default
batch_sizeis 8160 there too.master reproduces every measurement identically to 4.1.4 — not approximately, the same
counts row for row:
c4dee4bdfirst_value(varchar)wrong @batch_size4 / 2 / 1first_value(int)wrong @batch_size38 / 37 / 4 / 1bs38 / 19 / 7batch_size8160batch_size8160nth_valuestring lengthtotal_length=4294967303total_length=4294967296(2³², message now also printslimit=4294967295)and master crashed the BE during the run. Because it is a locally built binary the trace
carries full source lines, which is the clearest statement of the bug available:
A
memcpyinsideColumnStr::insert_from, reached from the analytic sink writing out anaggregate's saved value — i.e. copying a string through an offset that no longer describes the
row it was saved for.
No fix is in flight: the most recent commit touching
analytic_sink_operator.cppis #67274(2026-08-31) and the most recent touching
aggregate_function_window.his #64864 (2026-06-26);neither addresses the saved-pointer invalidation.
What's Wrong?
first_value()over a window silently returns values that are not in the window — and on aVARCHARcolumn, values that are not in the table at all. On the same data the BE can alsoabort the query with a bogus 4 GB string length, or crash outright.
The trigger is that the analytic operator evicts input rows it believes are no longer needed,
while
first_valueis still holding a raw pointer plus an absolute row offset into exactlythe columns being evicted.
1.
first_value(varchar)returns a string that is nowhere in the tableWith
PARTITION BY c,first_value(c)must equal the row's ownc— every partition containsone distinct value, so this is an exact oracle needing no reference engine. 20 000 rows, every
value at most 8 characters:
batch_sizeA sample row:
c = 'xyz', so the correct answer is'xyz', and Doris returnsThat is a run of adjacent values read out of the string column through a wrong offset/length
pair — the correct 3-character value is visible inside it.
2.
first_value(int)is wrong for most of the tableSame shape on an INT column, ground truth computed in Python (
PARTITION BY ewitheboolean,so two partitions of ~10 000 rows):
batch_sizeNo warning, no error — the query simply returns wrong numbers for most rows.
3. It reproduces at the shipped
batch_sizeon an ordinary tablebatch_sizeonly makes it cheap to demonstrate. What actually matters is the partition size inblocks, so the same defect is reached with
batch_sizeleft completely alone (shipped default8160) by making one partition large — the threshold being
257 * 8160 = 2 097 120rows.Single partition, nothing configured:
batch_size(8160)batch_size = 65535The boundary lands exactly where the
257 * batch_sizerule predicts (1 280 000 < 2 097 120 <2 560 000), and raising
batch_sizeto 65535 moves the threshold out of reach again, whichisolates the block count as the cause.
One caveat worth knowing when reproducing: the block count is produced by the scan, so it
depends on how the table is physically laid out, not only on
batch_size. On a table I hadbuilt by repeated
INSERT INTO … SELECTdoubling (many small rowsets) the same 2 560 000-rowpartition was wrong at
batch_size = 65535as well, where a table of the same size written by asingle
INSERT … SELECTwas clean at 65535. So a fragmented table reaches 257 blocks with fewerrows, and raising
batch_sizedoes not reliably protect it.4. The exact trigger
The onset is not fuzzy. Corruption begins exactly when
which is
block_num = 256in_remove_unused_rows(), plus the+ 1in its guard. Verified byfixing the partition size and walking
batch_sizeacross the predicted boundary:bs= pred+1bs= predbs= pred−1bs≤ 38bs≤ 19bs≤ 7Three for three, on the row exactly.
5. The same corruption as an error, and as a crash
nth_value(varchar, 3)overROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING, 20 000 rows,batch_size = 4:4294967303is 2³² + 7 — the string length arithmetic wrapped a 32-bit counter, which is thesame wrong offset/length pair that produces the 229-character
first_valueanswers.The same shape took the BE down on both builds — on 4.1.4 at
batch_size = 1, and on themaster build during the battery. Stack from the 4.1.4 BE log (the master one, with full source
lines, is in the
Versionsection):Frame 2 is the aggregate's saved value being copied out:
to.insert_from(*_ptr, _offset). It isreading through a pointer/offset that no longer describes the row it was saved for.
What You Expected?
first_value(c) OVER (PARTITION BY c …)must return the row's owncfor every row, andfirst_value(a)must return the firstaof the partition in window order — regardless ofbatch_size, partition size, or whether the operator decided to release memory mid-partition.The answer must not depend on how the input was split into vectorised blocks.
At minimum, a query must not silently return values that do not exist in the input, and must not
crash the backend.
How to Reproduce?
Single FE + single BE, default configuration:
A. Smallest form — exact oracle, no reference engine needed.
Load 20 000 rows of
(id INT, c VARCHAR(255))wherecis a random string of at most 8characters, then:
Because the window partitions by
c,wcan only bec. Any returned row is a wrong answer.SET batch_size = 65535on the same table returns nothing, confirming the data is fine.B. At the shipped
batch_size, no session variable touched.Build
(id INT, a INT, g BIGINT, e INT)withe = 0on every row, so the whole table is onepartition, and grow it past
257 * 8160 = 2 097 120rows:SET batch_size = 65535returns 0 for both, which isolates the block count as the cause.C. The crash / 4 GB length.
On the 20 000-row varchar table:
A reproduction script with the ground-truth oracles for all of the above is attached to this
report.
Anything Else?
No response
Are you willing to submit PR?
Code of Conduct