Skip to content

Commit c99983e

Browse files
authored
ESQL: Fix usage of already released null block in ValueSourceReaderOperator (#126411) (#126474)
* Add yaml test with reproducer * Fix the bug * Make ComputeBlockLoaderFactory Releasable
1 parent dbdaf16 commit c99983e

File tree

4 files changed

+100
-26
lines changed

4 files changed

+100
-26
lines changed

docs/changelog/126411.yaml

+6
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

+31-25
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

+8-1
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,14 @@ public enum Cap {
704704
/**
705705
* Supercedes {@link Cap#MAKE_NUMBER_OF_CHANNELS_CONSISTENT_WITH_LAYOUT}.
706706
*/
707-
FIX_REPLACE_MISSING_FIELD_WITH_NULL_DUPLICATE_NAME_ID_IN_LAYOUT;
707+
FIX_REPLACE_MISSING_FIELD_WITH_NULL_DUPLICATE_NAME_ID_IN_LAYOUT,
708+
709+
/**
710+
* When creating constant null blocks in {@link org.elasticsearch.compute.lucene.ValuesSourceReaderOperator}, we also handed off
711+
* the ownership of that block - but didn't account for the fact that the caller might close it, leading to double releases
712+
* in some union type queries. C.f. https://github.com/elastic/elasticsearch/issues/125850
713+
*/
714+
FIX_DOUBLY_RELEASED_NULL_BLOCKS_IN_VALUESOURCEREADER;
708715

709716
private final boolean enabled;
710717

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

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

0 commit comments

Comments
 (0)