3232import io .questdb .client .cutlass .qwp .protocol .QwpConstants ;
3333import io .questdb .client .std .MemoryTag ;
3434import io .questdb .client .std .Unsafe ;
35+ import io .questdb .client .test .tools .TestUtils ;
3536import org .junit .Assert ;
3637import org .junit .Test ;
3738
@@ -51,25 +52,28 @@ public class QwpResultBatchDecoderHardeningTest {
5152 * append nulls until OOM (or AIOOBE for negative ids cast from a high varint).
5253 */
5354 @ Test
54- public void testHugeSchemaIdIsRejected () {
55- QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
56- QwpBatchBuffer buffer = new QwpBatchBuffer (256 );
57- long staging = Unsafe .malloc (256 , MemoryTag .NATIVE_DEFAULT );
58- try {
59- // schema_id = 1_000_000_000, well above the 65_535 cap.
60- int len = writeMinimalResultBatch (staging , /*schemaId=*/ 1_000_000_000L );
61- buffer .copyFromPayload (staging , len );
55+ public void testHugeSchemaIdIsRejected () throws Exception {
56+ TestUtils .assertMemoryLeak (() -> {
57+ QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
58+ QwpBatchBuffer buffer = new QwpBatchBuffer (256 );
59+ long staging = Unsafe .malloc (256 , MemoryTag .NATIVE_DEFAULT );
6260 try {
63- decoder .decode (buffer );
64- Assert .fail ("decoder must reject huge schema_id" );
65- } catch (QwpDecodeException expected ) {
66- Assert .assertTrue ("error message should mention schema_id: " + expected .getMessage (),
67- expected .getMessage ().contains ("schema_id" ));
61+ // schema_id = 1_000_000_000, well above the 65_535 cap.
62+ int len = writeMinimalResultBatch (staging , /*schemaId=*/ 1_000_000_000L );
63+ buffer .copyFromPayload (staging , len );
64+ try {
65+ decoder .decode (buffer );
66+ Assert .fail ("decoder must reject huge schema_id" );
67+ } catch (QwpDecodeException expected ) {
68+ Assert .assertTrue ("error message should mention schema_id: " + expected .getMessage (),
69+ expected .getMessage ().contains ("schema_id" ));
70+ }
71+ } finally {
72+ Unsafe .free (staging , 256 , MemoryTag .NATIVE_DEFAULT );
73+ buffer .close ();
74+ decoder .close ();
6875 }
69- } finally {
70- Unsafe .free (staging , 256 , MemoryTag .NATIVE_DEFAULT );
71- buffer .close ();
72- }
76+ });
7377 }
7478
7579 /**
@@ -78,32 +82,35 @@ public void testHugeSchemaIdIsRejected() {
7882 * rejected, not silently passed to {@code getQuick(negativeIndex)}.
7983 */
8084 @ Test
81- public void testNegativeSchemaIdIsRejected () {
82- QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
83- QwpBatchBuffer buffer = new QwpBatchBuffer (256 );
84- long staging = Unsafe .malloc (256 , MemoryTag .NATIVE_DEFAULT );
85- try {
86- // 5-byte varint encoding 0x80000000 (which casts to Integer.MIN_VALUE).
87- // varint bytes for 0x80000000:
88- // value bits 7..0: 0x00 -> byte: 0x80 (continuation)
89- // value bits 14..8: 0x00 -> byte: 0x80
90- // value bits 21..15:0x00 -> byte: 0x80
91- // value bits 28..22:0x00 -> byte: 0x80
92- // value bits 35..29:0x08 -> byte: 0x08 (no continuation)
93- int len = writeMinimalResultBatchWithRawSchemaIdVarint (
94- staging , new byte []{(byte ) 0x80 , (byte ) 0x80 , (byte ) 0x80 , (byte ) 0x80 , (byte ) 0x08 });
95- buffer .copyFromPayload (staging , len );
85+ public void testNegativeSchemaIdIsRejected () throws Exception {
86+ TestUtils .assertMemoryLeak (() -> {
87+ QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
88+ QwpBatchBuffer buffer = new QwpBatchBuffer (256 );
89+ long staging = Unsafe .malloc (256 , MemoryTag .NATIVE_DEFAULT );
9690 try {
97- decoder .decode (buffer );
98- Assert .fail ("decoder must reject huge/negative schema_id" );
99- } catch (QwpDecodeException expected ) {
100- Assert .assertTrue ("error message should mention schema_id: " + expected .getMessage (),
101- expected .getMessage ().contains ("schema_id" ));
91+ // 5-byte varint encoding 0x80000000 (which casts to Integer.MIN_VALUE).
92+ // varint bytes for 0x80000000:
93+ // value bits 7..0: 0x00 -> byte: 0x80 (continuation)
94+ // value bits 14..8: 0x00 -> byte: 0x80
95+ // value bits 21..15:0x00 -> byte: 0x80
96+ // value bits 28..22:0x00 -> byte: 0x80
97+ // value bits 35..29:0x08 -> byte: 0x08 (no continuation)
98+ int len = writeMinimalResultBatchWithRawSchemaIdVarint (
99+ staging , new byte []{(byte ) 0x80 , (byte ) 0x80 , (byte ) 0x80 , (byte ) 0x80 , (byte ) 0x08 });
100+ buffer .copyFromPayload (staging , len );
101+ try {
102+ decoder .decode (buffer );
103+ Assert .fail ("decoder must reject huge/negative schema_id" );
104+ } catch (QwpDecodeException expected ) {
105+ Assert .assertTrue ("error message should mention schema_id: " + expected .getMessage (),
106+ expected .getMessage ().contains ("schema_id" ));
107+ }
108+ } finally {
109+ Unsafe .free (staging , 256 , MemoryTag .NATIVE_DEFAULT );
110+ buffer .close ();
111+ decoder .close ();
102112 }
103- } finally {
104- Unsafe .free (staging , 256 , MemoryTag .NATIVE_DEFAULT );
105- buffer .close ();
106- }
113+ });
107114 }
108115
109116 /**
@@ -114,32 +121,34 @@ public void testNegativeSchemaIdIsRejected() {
114121 * detects the overrun and reports a bounded error.
115122 */
116123 @ Test
117- public void testQueryErrorMsgLenOverrunIsRejected () {
118- // Frame contents:
119- // 12 bytes header (uninspected by decodeError)
120- // 1 byte msg_kind
121- // 8 bytes request_id
122- // 1 byte status
123- // 2 bytes msgLen (we set 0xFFFF)
124- // 0 bytes of actual message body
125- // Total payload: 24 bytes; msgLen would otherwise force reading 65535 bytes.
126- int payloadLen = 12 + 1 + 8 + 1 + 2 ;
127- long buf = Unsafe .malloc (payloadLen , MemoryTag .NATIVE_DEFAULT );
128- try {
129- // Zero out
130- for (int i = 0 ; i < payloadLen ; i ++) Unsafe .getUnsafe ().putByte (buf + i , (byte ) 0 );
131- // Write an obviously bogus msgLen at the right offset (header + msg_kind + reqId + status).
132- long msgLenOffset = buf + 12 + 1 + 8 + 1 ;
133- Unsafe .getUnsafe ().putShort (msgLenOffset , (short ) 0xFFFF );
124+ public void testQueryErrorMsgLenOverrunIsRejected () throws Exception {
125+ TestUtils .assertMemoryLeak (() -> {
126+ // Frame contents:
127+ // 12 bytes header (uninspected by decodeError)
128+ // 1 byte msg_kind
129+ // 8 bytes request_id
130+ // 1 byte status
131+ // 2 bytes msgLen (we set 0xFFFF)
132+ // 0 bytes of actual message body
133+ // Total payload: 24 bytes; msgLen would otherwise force reading 65535 bytes.
134+ int payloadLen = 12 + 1 + 8 + 1 + 2 ;
135+ long buf = Unsafe .malloc (payloadLen , MemoryTag .NATIVE_DEFAULT );
136+ try {
137+ // Zero out
138+ for (int i = 0 ; i < payloadLen ; i ++) Unsafe .getUnsafe ().putByte (buf + i , (byte ) 0 );
139+ // Write an obviously bogus msgLen at the right offset (header + msg_kind + reqId + status).
140+ long msgLenOffset = buf + 12 + 1 + 8 + 1 ;
141+ Unsafe .getUnsafe ().putShort (msgLenOffset , (short ) 0xFFFF );
134142
135- QueryEvent ev = QwpEgressIoThread .decodeError (buf , payloadLen );
136- Assert .assertEquals (QueryEvent .KIND_ERROR , ev .kind );
137- Assert .assertNotNull (ev .errorMessage );
138- Assert .assertTrue ("error must mention msg_len overrun: " + ev .errorMessage ,
139- ev .errorMessage .contains ("msg_len" ) && ev .errorMessage .contains ("exceeds" ));
140- } finally {
141- Unsafe .free (buf , payloadLen , MemoryTag .NATIVE_DEFAULT );
142- }
143+ QueryEvent ev = QwpEgressIoThread .decodeError (buf , payloadLen );
144+ Assert .assertEquals (QueryEvent .KIND_ERROR , ev .kind );
145+ Assert .assertNotNull (ev .errorMessage );
146+ Assert .assertTrue ("error must mention msg_len overrun: " + ev .errorMessage ,
147+ ev .errorMessage .contains ("msg_len" ) && ev .errorMessage .contains ("exceeds" ));
148+ } finally {
149+ Unsafe .free (buf , payloadLen , MemoryTag .NATIVE_DEFAULT );
150+ }
151+ });
143152 }
144153
145154 /**
@@ -148,28 +157,30 @@ public void testQueryErrorMsgLenOverrunIsRejected() {
148157 * confirming a real defensive guard, not a broken decoder.
149158 */
150159 @ Test
151- public void testQueryErrorValidMessageDecodes () {
152- byte [] msgBytes = "boom" .getBytes (java .nio .charset .StandardCharsets .UTF_8 );
153- int payloadLen = 12 + 1 + 8 + 1 + 2 + msgBytes .length ;
154- long buf = Unsafe .malloc (payloadLen , MemoryTag .NATIVE_DEFAULT );
155- try {
156- for (int i = 0 ; i < payloadLen ; i ++) Unsafe .getUnsafe ().putByte (buf + i , (byte ) 0 );
157- long statusOffset = buf + 12 + 1 + 8 ;
158- Unsafe .getUnsafe ().putByte (statusOffset , (byte ) 0x05 );
159- long msgLenOffset = statusOffset + 1 ;
160- Unsafe .getUnsafe ().putShort (msgLenOffset , (short ) msgBytes .length );
161- long bytesOffset = msgLenOffset + 2 ;
162- for (int i = 0 ; i < msgBytes .length ; i ++) {
163- Unsafe .getUnsafe ().putByte (bytesOffset + i , msgBytes [i ]);
164- }
160+ public void testQueryErrorValidMessageDecodes () throws Exception {
161+ TestUtils .assertMemoryLeak (() -> {
162+ byte [] msgBytes = "boom" .getBytes (java .nio .charset .StandardCharsets .UTF_8 );
163+ int payloadLen = 12 + 1 + 8 + 1 + 2 + msgBytes .length ;
164+ long buf = Unsafe .malloc (payloadLen , MemoryTag .NATIVE_DEFAULT );
165+ try {
166+ for (int i = 0 ; i < payloadLen ; i ++) Unsafe .getUnsafe ().putByte (buf + i , (byte ) 0 );
167+ long statusOffset = buf + 12 + 1 + 8 ;
168+ Unsafe .getUnsafe ().putByte (statusOffset , (byte ) 0x05 );
169+ long msgLenOffset = statusOffset + 1 ;
170+ Unsafe .getUnsafe ().putShort (msgLenOffset , (short ) msgBytes .length );
171+ long bytesOffset = msgLenOffset + 2 ;
172+ for (int i = 0 ; i < msgBytes .length ; i ++) {
173+ Unsafe .getUnsafe ().putByte (bytesOffset + i , msgBytes [i ]);
174+ }
165175
166- QueryEvent ev = QwpEgressIoThread .decodeError (buf , payloadLen );
167- Assert .assertEquals (QueryEvent .KIND_ERROR , ev .kind );
168- Assert .assertEquals ((byte ) 0x05 , ev .errorStatus );
169- Assert .assertEquals ("boom" , ev .errorMessage );
170- } finally {
171- Unsafe .free (buf , payloadLen , MemoryTag .NATIVE_DEFAULT );
172- }
176+ QueryEvent ev = QwpEgressIoThread .decodeError (buf , payloadLen );
177+ Assert .assertEquals (QueryEvent .KIND_ERROR , ev .kind );
178+ Assert .assertEquals ((byte ) 0x05 , ev .errorStatus );
179+ Assert .assertEquals ("boom" , ev .errorMessage );
180+ } finally {
181+ Unsafe .free (buf , payloadLen , MemoryTag .NATIVE_DEFAULT );
182+ }
183+ });
173184 }
174185
175186 /**
@@ -180,24 +191,27 @@ public void testQueryErrorValidMessageDecodes() {
180191 * memory backwards.
181192 */
182193 @ Test
183- public void testStringColumnNegativeTotalBytesIsRejected () {
184- QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
185- QwpBatchBuffer buffer = new QwpBatchBuffer (512 );
186- long staging = Unsafe .malloc (512 , MemoryTag .NATIVE_DEFAULT );
187- try {
188- int len = writeStringResultBatch (staging , /*nonNull=*/ 1 , /*totalBytes=*/ -1 );
189- buffer .copyFromPayload (staging , len );
194+ public void testStringColumnNegativeTotalBytesIsRejected () throws Exception {
195+ TestUtils .assertMemoryLeak (() -> {
196+ QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
197+ QwpBatchBuffer buffer = new QwpBatchBuffer (512 );
198+ long staging = Unsafe .malloc (512 , MemoryTag .NATIVE_DEFAULT );
190199 try {
191- decoder .decode (buffer );
192- Assert .fail ("decoder must reject negative totalBytes" );
193- } catch (QwpDecodeException expected ) {
194- Assert .assertTrue ("error message should describe invalid total bytes: " + expected .getMessage (),
195- expected .getMessage ().contains ("total bytes" ));
200+ int len = writeStringResultBatch (staging , /*nonNull=*/ 1 , /*totalBytes=*/ -1 );
201+ buffer .copyFromPayload (staging , len );
202+ try {
203+ decoder .decode (buffer );
204+ Assert .fail ("decoder must reject negative totalBytes" );
205+ } catch (QwpDecodeException expected ) {
206+ Assert .assertTrue ("error message should describe invalid total bytes: " + expected .getMessage (),
207+ expected .getMessage ().contains ("total bytes" ));
208+ }
209+ } finally {
210+ Unsafe .free (staging , 512 , MemoryTag .NATIVE_DEFAULT );
211+ buffer .close ();
212+ decoder .close ();
196213 }
197- } finally {
198- Unsafe .free (staging , 512 , MemoryTag .NATIVE_DEFAULT );
199- buffer .close ();
200- }
214+ });
201215 }
202216
203217 /**
@@ -206,19 +220,22 @@ public void testStringColumnNegativeTotalBytesIsRejected() {
206220 * the negative-value rejection above is testing the right code path.
207221 */
208222 @ Test
209- public void testStringColumnValidTotalBytesIsAccepted () throws QwpDecodeException {
210- QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
211- QwpBatchBuffer buffer = new QwpBatchBuffer (512 );
212- long staging = Unsafe .malloc (512 , MemoryTag .NATIVE_DEFAULT );
213- try {
214- int len = writeStringResultBatch (staging , /*nonNull=*/ 1 , /*totalBytes=*/ 5 );
215- buffer .copyFromPayload (staging , len );
216- decoder .decode (buffer );
217- // no exception => the decoder accepts the valid wire bytes
218- } finally {
219- Unsafe .free (staging , 512 , MemoryTag .NATIVE_DEFAULT );
220- buffer .close ();
221- }
223+ public void testStringColumnValidTotalBytesIsAccepted () throws Exception {
224+ TestUtils .assertMemoryLeak (() -> {
225+ QwpResultBatchDecoder decoder = new QwpResultBatchDecoder ();
226+ QwpBatchBuffer buffer = new QwpBatchBuffer (512 );
227+ long staging = Unsafe .malloc (512 , MemoryTag .NATIVE_DEFAULT );
228+ try {
229+ int len = writeStringResultBatch (staging , /*nonNull=*/ 1 , /*totalBytes=*/ 5 );
230+ buffer .copyFromPayload (staging , len );
231+ decoder .decode (buffer );
232+ // no exception => the decoder accepts the valid wire bytes
233+ } finally {
234+ Unsafe .free (staging , 512 , MemoryTag .NATIVE_DEFAULT );
235+ buffer .close ();
236+ decoder .close ();
237+ }
238+ });
222239 }
223240
224241 // -----------------------------------------------------------------------
0 commit comments