Skip to content

Commit 6774ba4

Browse files
committed
ESQL: Fix usage of already released null block in ValueSourceReaderOperator (#126411)
* Add yaml test with reproducer * Fix the bug * Make ComputeBlockLoaderFactory Releasable
1 parent 112859b commit 6774ba4

File tree

4 files changed

+100
-26
lines changed

4 files changed

+100
-26
lines changed

docs/changelog/126411.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 126411
2+
summary: Fix usage of already released null block in `ValueSourceReaderOperator`
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 125850

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,8 @@ private void loadFromSingleLeaf(Block[] blocks, int shard, int segment, BlockLoa
220220
positionFieldWork(shard, segment, firstDoc);
221221
StoredFieldsSpec storedFieldsSpec = StoredFieldsSpec.NO_REQUIREMENTS;
222222
List<RowStrideReaderWork> rowStrideReaders = new ArrayList<>(fields.length);
223-
ComputeBlockLoaderFactory loaderBlockFactory = new ComputeBlockLoaderFactory(blockFactory, docs.count());
224223
LeafReaderContext ctx = ctx(shard, segment);
225-
try {
224+
try (ComputeBlockLoaderFactory loaderBlockFactory = new ComputeBlockLoaderFactory(blockFactory, docs.count())) {
226225
for (int f = 0; f < fields.length; f++) {
227226
FieldWork field = fields[f];
228227
BlockLoader.ColumnAtATimeReader columnAtATime = field.columnAtATime(ctx);
@@ -345,27 +344,28 @@ void run() throws IOException {
345344
builders[f] = new Block.Builder[shardContexts.size()];
346345
converters[f] = new BlockLoader[shardContexts.size()];
347346
}
348-
ComputeBlockLoaderFactory loaderBlockFactory = new ComputeBlockLoaderFactory(blockFactory, docs.getPositionCount());
349-
int p = forwards[0];
350-
int shard = shards.getInt(p);
351-
int segment = segments.getInt(p);
352-
int firstDoc = docs.getInt(p);
353-
positionFieldWork(shard, segment, firstDoc);
354-
LeafReaderContext ctx = ctx(shard, segment);
355-
fieldsMoved(ctx, shard);
356-
verifyBuilders(loaderBlockFactory, shard);
357-
read(firstDoc, shard);
358-
for (int i = 1; i < forwards.length; i++) {
359-
p = forwards[i];
360-
shard = shards.getInt(p);
361-
segment = segments.getInt(p);
362-
boolean changedSegment = positionFieldWorkDocGuarteedAscending(shard, segment);
363-
if (changedSegment) {
364-
ctx = ctx(shard, segment);
365-
fieldsMoved(ctx, shard);
366-
}
347+
try (ComputeBlockLoaderFactory loaderBlockFactory = new ComputeBlockLoaderFactory(blockFactory, docs.getPositionCount())) {
348+
int p = forwards[0];
349+
int shard = shards.getInt(p);
350+
int segment = segments.getInt(p);
351+
int firstDoc = docs.getInt(p);
352+
positionFieldWork(shard, segment, firstDoc);
353+
LeafReaderContext ctx = ctx(shard, segment);
354+
fieldsMoved(ctx, shard);
367355
verifyBuilders(loaderBlockFactory, shard);
368-
read(docs.getInt(p), shard);
356+
read(firstDoc, shard);
357+
for (int i = 1; i < forwards.length; i++) {
358+
p = forwards[i];
359+
shard = shards.getInt(p);
360+
segment = segments.getInt(p);
361+
boolean changedSegment = positionFieldWorkDocGuarteedAscending(shard, segment);
362+
if (changedSegment) {
363+
ctx = ctx(shard, segment);
364+
fieldsMoved(ctx, shard);
365+
}
366+
verifyBuilders(loaderBlockFactory, shard);
367+
read(docs.getInt(p), shard);
368+
}
369369
}
370370
for (int f = 0; f < target.length; f++) {
371371
for (int s = 0; s < shardContexts.size(); s++) {
@@ -614,7 +614,7 @@ public String toString() {
614614
}
615615
}
616616

617-
private static class ComputeBlockLoaderFactory implements BlockLoader.BlockFactory {
617+
private static class ComputeBlockLoaderFactory implements BlockLoader.BlockFactory, Releasable {
618618
private final BlockFactory factory;
619619
private final int pageSize;
620620
private Block nullBlock;
@@ -683,12 +683,18 @@ public BlockLoader.Builder nulls(int expectedCount) {
683683
public Block constantNulls() {
684684
if (nullBlock == null) {
685685
nullBlock = factory.newConstantNullBlock(pageSize);
686-
} else {
687-
nullBlock.incRef();
688686
}
687+
nullBlock.incRef();
689688
return nullBlock;
690689
}
691690

691+
@Override
692+
public void close() {
693+
if (nullBlock != null) {
694+
nullBlock.close();
695+
}
696+
}
697+
692698
@Override
693699
public BytesRefBlock constantBytes(BytesRef value) {
694700
return factory.newConstantBytesRefBlockWith(value, pageSize);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,14 @@ public enum Cap {
870870
/**
871871
* Supercedes {@link Cap#MAKE_NUMBER_OF_CHANNELS_CONSISTENT_WITH_LAYOUT}.
872872
*/
873-
FIX_REPLACE_MISSING_FIELD_WITH_NULL_DUPLICATE_NAME_ID_IN_LAYOUT;
873+
FIX_REPLACE_MISSING_FIELD_WITH_NULL_DUPLICATE_NAME_ID_IN_LAYOUT,
874+
875+
/**
876+
* When creating constant null blocks in {@link org.elasticsearch.compute.lucene.ValuesSourceReaderOperator}, we also handed off
877+
* the ownership of that block - but didn't account for the fact that the caller might close it, leading to double releases
878+
* in some union type queries. C.f. https://github.com/elastic/elasticsearch/issues/125850
879+
*/
880+
FIX_DOUBLY_RELEASED_NULL_BLOCKS_IN_VALUESOURCEREADER;
874881

875882
private final boolean enabled;
876883

x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,58 @@
335335
esql.query:
336336
body:
337337
query: 'FROM test_grok | KEEP name | WHERE last_name == "Facello" | EVAL name = concat("1 ", last_name) | GROK name "%{NUMBER:foo} %{WORD:foo}"'
338+
---
339+
"union types with null blocks from missing fields #125850":
340+
- requires:
341+
test_runner_features: [allowed_warnings_regex, capabilities]
342+
capabilities:
343+
- method: POST
344+
path: /_query
345+
parameters: []
346+
capabilities: [fix_doubly_released_null_blocks_in_valuesourcereader]
347+
reason: "fixed handing out already closed null block references in ValueSourceReader"
348+
- do:
349+
indices.create:
350+
index: test1
351+
body:
352+
mappings:
353+
properties:
354+
truefalse1 :
355+
type : boolean
356+
truefalse2 :
357+
type: boolean
358+
- do:
359+
indices.create:
360+
index: test2
361+
body:
362+
mappings:
363+
properties:
364+
truefalse1 :
365+
type : keyword
366+
truefalse2 :
367+
type: keyword
368+
- do:
369+
bulk:
370+
refresh: true
371+
body:
372+
- { "index": { "_index": "test1" } }
373+
- { "truefalse1": null}
374+
- { "index": { "_index": "test2" } }
375+
- { "truefalse1": null }
376+
377+
- do:
378+
allowed_warnings_regex:
379+
- "No limit defined, adding default limit of \\[.*\\]"
380+
381+
esql.query:
382+
body:
383+
query: 'FROM test* | eval t1 = truefalse1::boolean, t2 = truefalse2::boolean | keep t1, t2'
384+
- match: { columns.0.name: t1 }
385+
- match: { columns.0.type: boolean }
386+
- match: { columns.1.name: t2 }
387+
- match: { columns.1.type: boolean }
388+
- length: { values: 2 }
389+
- match: { values.0.0: null }
390+
- match: { values.0.1: null }
391+
- match: { values.1.0: null }
392+
- match: { values.1.1: null }

0 commit comments

Comments
 (0)