Skip to content

Commit 5a1bf1b

Browse files
committed
GH-1230: fix out-of-bounds write in ClobConsumer buffer growth
1 parent 7f1f9f5 commit 5a1bf1b

2 files changed

Lines changed: 161 additions & 2 deletions

File tree

adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/ClobConsumer.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ public void consume(ResultSet resultSet) throws SQLException {
8383
String str = clob.getSubString(read, readSize);
8484
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
8585

86-
while ((dataBuffer.writerIndex() + bytes.length) > dataBuffer.capacity()) {
86+
while (dataBuffer.capacity() < startIndex + totalBytes + bytes.length) {
8787
vector.reallocDataBuffer();
88+
dataBuffer = vector.getDataBuffer();
8889
}
8990
MemoryUtil.copyToMemory(
9091
bytes, 0, dataBuffer.memoryAddress() + startIndex + totalBytes, bytes.length);
@@ -126,8 +127,9 @@ public void consume(ResultSet resultSet) throws SQLException {
126127
String str = clob.getSubString(read, readSize);
127128
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
128129

129-
while ((dataBuffer.writerIndex() + bytes.length) > dataBuffer.capacity()) {
130+
while (dataBuffer.capacity() < startIndex + totalBytes + bytes.length) {
130131
vector.reallocDataBuffer();
132+
dataBuffer = vector.getDataBuffer();
131133
}
132134
MemoryUtil.copyToMemory(
133135
bytes, 0, dataBuffer.memoryAddress() + startIndex + totalBytes, bytes.length);
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.arrow.adapter.jdbc.consumer;
18+
19+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.Reader;
25+
import java.nio.charset.StandardCharsets;
26+
import java.sql.Clob;
27+
import java.sql.SQLException;
28+
import org.apache.arrow.adapter.jdbc.ResultSetUtility;
29+
import org.apache.arrow.vector.BaseValueVector;
30+
import org.apache.arrow.vector.VarCharVector;
31+
import org.junit.jupiter.api.Test;
32+
33+
public class ClobConsumerTest extends AbstractConsumerTest {
34+
35+
private static final int INITIAL_VALUE_ALLOCATION = BaseValueVector.INITIAL_VALUE_ALLOCATION;
36+
private static final int DEFAULT_RECORD_BYTE_COUNT = 8;
37+
38+
private void assertConsume(boolean nullable, String value) throws SQLException, IOException {
39+
try (final VarCharVector vector = new VarCharVector("clob", allocator)) {
40+
ClobConsumer consumer = ClobConsumer.createConsumer(vector, 0, nullable);
41+
consumer.consume(new SingleClobResultSet(value));
42+
assertEquals(0, vector.getLastSet());
43+
assertArrayEquals(value.getBytes(StandardCharsets.UTF_8), vector.get(0));
44+
}
45+
}
46+
47+
@Test
48+
public void testConsumeSmallClob() throws SQLException, IOException {
49+
assertConsume(false, repeat('a', DEFAULT_RECORD_BYTE_COUNT));
50+
}
51+
52+
@Test
53+
public void testConsumeClobLargerThanDataBuffer() throws SQLException, IOException {
54+
// A CLOB whose bytes exceed the VarCharVector's initial data-buffer capacity must grow the
55+
// data buffer, not write past it.
56+
assertConsume(false, repeat('a', INITIAL_VALUE_ALLOCATION * DEFAULT_RECORD_BYTE_COUNT * 4));
57+
}
58+
59+
private static String repeat(char c, int count) {
60+
char[] chars = new char[count];
61+
java.util.Arrays.fill(chars, c);
62+
return new String(chars);
63+
}
64+
65+
/** A {@link java.sql.ResultSet} that returns a single non-null CLOB for any column. */
66+
private static class SingleClobResultSet extends ResultSetUtility.ThrowingResultSet {
67+
private final Clob clob;
68+
69+
SingleClobResultSet(String value) {
70+
this.clob = new StringClob(value);
71+
}
72+
73+
@Override
74+
public Clob getClob(int columnIndex) {
75+
return clob;
76+
}
77+
78+
@Override
79+
public boolean wasNull() {
80+
return false;
81+
}
82+
}
83+
84+
/** Minimal read-only {@link Clob} backed by a {@link String}. */
85+
private static class StringClob implements Clob {
86+
private final String value;
87+
88+
StringClob(String value) {
89+
this.value = value;
90+
}
91+
92+
@Override
93+
public long length() {
94+
return value.length();
95+
}
96+
97+
@Override
98+
public String getSubString(long pos, int length) {
99+
int start = (int) (pos - 1);
100+
int end = Math.min(value.length(), start + length);
101+
return value.substring(start, end);
102+
}
103+
104+
@Override
105+
public Reader getCharacterStream() {
106+
throw new UnsupportedOperationException();
107+
}
108+
109+
@Override
110+
public InputStream getAsciiStream() {
111+
throw new UnsupportedOperationException();
112+
}
113+
114+
@Override
115+
public long position(String searchstr, long start) {
116+
throw new UnsupportedOperationException();
117+
}
118+
119+
@Override
120+
public long position(Clob searchstr, long start) {
121+
throw new UnsupportedOperationException();
122+
}
123+
124+
@Override
125+
public int setString(long pos, String str) {
126+
throw new UnsupportedOperationException();
127+
}
128+
129+
@Override
130+
public int setString(long pos, String str, int offset, int len) {
131+
throw new UnsupportedOperationException();
132+
}
133+
134+
@Override
135+
public java.io.OutputStream setAsciiStream(long pos) {
136+
throw new UnsupportedOperationException();
137+
}
138+
139+
@Override
140+
public java.io.Writer setCharacterStream(long pos) {
141+
throw new UnsupportedOperationException();
142+
}
143+
144+
@Override
145+
public void truncate(long len) {
146+
throw new UnsupportedOperationException();
147+
}
148+
149+
@Override
150+
public void free() {}
151+
152+
@Override
153+
public Reader getCharacterStream(long pos, long length) {
154+
throw new UnsupportedOperationException();
155+
}
156+
}
157+
}

0 commit comments

Comments
 (0)