@@ -91,7 +91,9 @@ impl ColumnarWalRecord {
9191/// Each value is written as: [type_tag: u8][value_bytes].
9292/// This is more compact than MessagePack for typed columns and enables
9393/// direct replay into the memtable without schema interpretation overhead.
94- pub fn encode_row_for_wal ( values : & [ nodedb_types:: value:: Value ] ) -> Vec < u8 > {
94+ pub fn encode_row_for_wal (
95+ values : & [ nodedb_types:: value:: Value ] ,
96+ ) -> Result < Vec < u8 > , crate :: error:: ColumnarError > {
9597 use nodedb_types:: value:: Value ;
9698
9799 let mut buf = Vec :: with_capacity ( values. len ( ) * 10 ) ; // Rough estimate.
@@ -152,14 +154,63 @@ pub fn encode_row_for_wal(values: &[nodedb_types::value::Value]) -> Vec<u8> {
152154 _ => {
153155 // Geometry and other complex types: serialize as JSON bytes.
154156 buf. push ( 10 ) ;
155- let json = sonic_rs:: to_vec ( value) . unwrap_or_default ( ) ;
157+ let json = sonic_rs:: to_vec ( value) . map_err ( |e| {
158+ crate :: error:: ColumnarError :: Serialization ( format ! (
159+ "failed to serialize value as JSON: {e}"
160+ ) )
161+ } ) ?;
156162 buf. extend_from_slice ( & ( json. len ( ) as u32 ) . to_le_bytes ( ) ) ;
157163 buf. extend_from_slice ( & json) ;
158164 }
159165 }
160166 }
161167
162- buf
168+ Ok ( buf)
169+ }
170+
171+ /// Maximum length for a variable-length field in a WAL record (256 MiB).
172+ /// Prevents OOM from crafted/corrupt records with bogus length prefixes.
173+ const MAX_FIELD_LEN : usize = 256 * 1024 * 1024 ;
174+
175+ /// Read exactly `n` bytes from `data` at `cursor`, advancing cursor.
176+ /// Returns `Err` if not enough bytes remain.
177+ fn read_slice < ' a > (
178+ data : & ' a [ u8 ] ,
179+ cursor : & mut usize ,
180+ n : usize ,
181+ context : & str ,
182+ ) -> Result < & ' a [ u8 ] , crate :: error:: ColumnarError > {
183+ let end = cursor. checked_add ( n) . ok_or_else ( || {
184+ crate :: error:: ColumnarError :: Serialization ( format ! ( "overflow in {context}" ) )
185+ } ) ?;
186+ if end > data. len ( ) {
187+ return Err ( crate :: error:: ColumnarError :: Serialization ( format ! (
188+ "truncated {context}: need {n} bytes at offset {cursor}, have {}" ,
189+ data. len( ) . saturating_sub( * cursor)
190+ ) ) ) ;
191+ }
192+ let slice = & data[ * cursor..end] ;
193+ * cursor = end;
194+ Ok ( slice)
195+ }
196+
197+ /// Read a u32 length prefix, validate it against MAX_FIELD_LEN, then read
198+ /// that many bytes. Returns the payload slice.
199+ fn read_length_prefixed < ' a > (
200+ data : & ' a [ u8 ] ,
201+ cursor : & mut usize ,
202+ context : & str ,
203+ ) -> Result < & ' a [ u8 ] , crate :: error:: ColumnarError > {
204+ let len_bytes = read_slice ( data, cursor, 4 , context) ?;
205+ let len = u32:: from_le_bytes ( len_bytes. try_into ( ) . map_err ( |_| {
206+ crate :: error:: ColumnarError :: Serialization ( format ! ( "truncated {context} len" ) )
207+ } ) ?) as usize ;
208+ if len > MAX_FIELD_LEN {
209+ return Err ( crate :: error:: ColumnarError :: Serialization ( format ! (
210+ "{context} length {len} exceeds maximum {MAX_FIELD_LEN}"
211+ ) ) ) ;
212+ }
213+ read_slice ( data, cursor, len, context)
163214}
164215
165216/// Decode a row from the columnar wire format back into Values.
@@ -172,37 +223,40 @@ pub fn decode_row_from_wal(
172223 let mut cursor = 0 ;
173224
174225 while cursor < data. len ( ) {
175- let tag = data[ cursor] ;
176- cursor += 1 ;
226+ let tag_slice = read_slice ( data, & mut cursor, 1 , "tag" ) ? ;
227+ let tag = tag_slice [ 0 ] ;
177228
178229 let value = match tag {
179230 0 => Value :: Null ,
180231 1 => {
181- let v = i64:: from_le_bytes ( data[ cursor..cursor + 8 ] . try_into ( ) . map_err ( |_| {
232+ let bytes = read_slice ( data, & mut cursor, 8 , "i64" ) ?;
233+ let v = i64:: from_le_bytes ( bytes. try_into ( ) . map_err ( |_| {
182234 crate :: error:: ColumnarError :: Serialization ( "truncated i64" . into ( ) )
183235 } ) ?) ;
184- cursor += 8 ;
185236 Value :: Integer ( v)
186237 }
187238 2 => {
188- let v = f64:: from_le_bytes ( data[ cursor..cursor + 8 ] . try_into ( ) . map_err ( |_| {
239+ let bytes = read_slice ( data, & mut cursor, 8 , "f64" ) ?;
240+ let v = f64:: from_le_bytes ( bytes. try_into ( ) . map_err ( |_| {
189241 crate :: error:: ColumnarError :: Serialization ( "truncated f64" . into ( ) )
190242 } ) ?) ;
191- cursor += 8 ;
192243 Value :: Float ( v)
193244 }
194245 3 => {
195- let v = data[ cursor] != 0 ;
196- cursor += 1 ;
197- Value :: Bool ( v)
246+ let bytes = read_slice ( data, & mut cursor, 1 , "bool" ) ?;
247+ Value :: Bool ( bytes[ 0 ] != 0 )
198248 }
199249 4 | 5 | 8 => {
200- let len = u32:: from_le_bytes ( data[ cursor..cursor + 4 ] . try_into ( ) . map_err ( |_| {
201- crate :: error:: ColumnarError :: Serialization ( "truncated len" . into ( ) )
202- } ) ?) as usize ;
203- cursor += 4 ;
204- let bytes = & data[ cursor..cursor + len] ;
205- cursor += len;
250+ let bytes = read_length_prefixed (
251+ data,
252+ & mut cursor,
253+ match tag {
254+ 4 => "string" ,
255+ 5 => "bytes" ,
256+ 8 => "uuid" ,
257+ _ => unreachable ! ( ) ,
258+ } ,
259+ ) ?;
206260 match tag {
207261 4 => Value :: String ( String :: from_utf8_lossy ( bytes) . into_owned ( ) ) ,
208262 5 => Value :: Bytes ( bytes. to_vec ( ) ) ,
@@ -211,43 +265,41 @@ pub fn decode_row_from_wal(
211265 }
212266 }
213267 6 => {
214- let micros =
215- i64:: from_le_bytes ( data[ cursor..cursor + 8 ] . try_into ( ) . map_err ( |_| {
216- crate :: error:: ColumnarError :: Serialization ( "truncated timestamp" . into ( ) )
217- } ) ?) ;
218- cursor += 8 ;
268+ let bytes = read_slice ( data, & mut cursor, 8 , "timestamp" ) ?;
269+ let micros = i64:: from_le_bytes ( bytes. try_into ( ) . map_err ( |_| {
270+ crate :: error:: ColumnarError :: Serialization ( "truncated timestamp" . into ( ) )
271+ } ) ?) ;
219272 Value :: DateTime ( nodedb_types:: datetime:: NdbDateTime :: from_micros ( micros) )
220273 }
221274 7 => {
222- let mut bytes = [ 0u8 ; 16 ] ;
223- bytes . copy_from_slice ( & data [ cursor..cursor + 16 ] ) ;
224- cursor += 16 ;
225- Value :: Decimal ( rust_decimal:: Decimal :: deserialize ( bytes ) )
275+ let bytes = read_slice ( data , & mut cursor , 16 , "decimal" ) ? ;
276+ let mut arr = [ 0u8 ; 16 ] ;
277+ arr . copy_from_slice ( bytes ) ;
278+ Value :: Decimal ( rust_decimal:: Decimal :: deserialize ( arr ) )
226279 }
227280 9 => {
228- let count =
229- u32:: from_le_bytes ( data[ cursor..cursor + 4 ] . try_into ( ) . map_err ( |_| {
230- crate :: error:: ColumnarError :: Serialization ( "truncated vector count" . into ( ) )
231- } ) ?) as usize ;
232- cursor += 4 ;
281+ let count_bytes = read_slice ( data, & mut cursor, 4 , "vector count" ) ?;
282+ let count = u32:: from_le_bytes ( count_bytes. try_into ( ) . map_err ( |_| {
283+ crate :: error:: ColumnarError :: Serialization ( "truncated vector count" . into ( ) )
284+ } ) ?) as usize ;
285+ if count > MAX_FIELD_LEN / 4 {
286+ return Err ( crate :: error:: ColumnarError :: Serialization ( format ! (
287+ "vector count {count} exceeds maximum {}" ,
288+ MAX_FIELD_LEN / 4
289+ ) ) ) ;
290+ }
233291 let mut arr = Vec :: with_capacity ( count) ;
234292 for _ in 0 ..count {
235- let f =
236- f32:: from_le_bytes ( data[ cursor..cursor + 4 ] . try_into ( ) . map_err ( |_| {
237- crate :: error:: ColumnarError :: Serialization ( "truncated f32" . into ( ) )
238- } ) ?) ;
239- cursor += 4 ;
293+ let fb = read_slice ( data, & mut cursor, 4 , "vector f32" ) ?;
294+ let f = f32:: from_le_bytes ( fb. try_into ( ) . map_err ( |_| {
295+ crate :: error:: ColumnarError :: Serialization ( "truncated f32" . into ( ) )
296+ } ) ?) ;
240297 arr. push ( Value :: Float ( f as f64 ) ) ;
241298 }
242299 Value :: Array ( arr)
243300 }
244301 10 => {
245- let len = u32:: from_le_bytes ( data[ cursor..cursor + 4 ] . try_into ( ) . map_err ( |_| {
246- crate :: error:: ColumnarError :: Serialization ( "truncated json len" . into ( ) )
247- } ) ?) as usize ;
248- cursor += 4 ;
249- let json_bytes = & data[ cursor..cursor + len] ;
250- cursor += len;
302+ let json_bytes = read_length_prefixed ( data, & mut cursor, "json" ) ?;
251303 sonic_rs:: from_slice ( json_bytes) . unwrap_or ( Value :: Null )
252304 }
253305 _ => {
@@ -316,7 +368,7 @@ mod tests {
316368 Value :: Array ( vec![ Value :: Float ( 1.0 ) , Value :: Float ( 2.0 ) ] ) ,
317369 ] ;
318370
319- let encoded = encode_row_for_wal ( & values) ;
371+ let encoded = encode_row_for_wal ( & values) . expect ( "encode" ) ;
320372 let decoded = decode_row_from_wal ( & encoded) . expect ( "decode" ) ;
321373
322374 assert_eq ! ( decoded. len( ) , values. len( ) ) ;
@@ -335,4 +387,70 @@ mod tests {
335387 ) ;
336388 assert_eq ! ( decoded[ 8 ] , Value :: Null ) ;
337389 }
390+
391+ #[ test]
392+ fn decode_truncated_i64_returns_error ( ) {
393+ // Tag 1 (i64) requires 8 payload bytes; supply none.
394+ // Today the slice index `data[cursor..cursor+8]` panics with an index
395+ // out-of-bounds. After the fix, `try_into()` returns the
396+ // Serialization error instead.
397+ let result = decode_row_from_wal ( & [ 1 ] ) ;
398+ assert ! (
399+ result. is_err( ) ,
400+ "truncated i64 payload must return Err, not panic"
401+ ) ;
402+ }
403+
404+ #[ test]
405+ fn decode_truncated_string_returns_error ( ) {
406+ // Tag 4 (string): length prefix says 255 bytes but the slice ends
407+ // immediately after the 4-byte length field. The read of
408+ // `data[cursor..cursor+255]` panics today; after the fix it errors.
409+ let input = {
410+ let mut v = vec ! [ 4u8 ] ; // tag = string
411+ v. extend_from_slice ( & 255u32 . to_le_bytes ( ) ) ; // len = 255
412+ // no payload bytes follow
413+ v
414+ } ;
415+ let result = decode_row_from_wal ( & input) ;
416+ assert ! (
417+ result. is_err( ) ,
418+ "truncated string payload must return Err, not panic"
419+ ) ;
420+ }
421+
422+ #[ test]
423+ fn decode_huge_vector_count_returns_error ( ) {
424+ // Tag 9 (vector array): count = 0x7FFFFFFF. After reading the count,
425+ // the very first iteration tries to read 4 bytes of f32 from an empty
426+ // slice, which panics today. After the fix the loop errors out cleanly
427+ // before any allocation proportional to count is attempted.
428+ let input = {
429+ let mut v = vec ! [ 9u8 ] ; // tag = vector array
430+ v. extend_from_slice ( & 0x7FFF_FFFFu32 . to_le_bytes ( ) ) ; // count
431+ // no f32 bytes follow
432+ v
433+ } ;
434+ let result = decode_row_from_wal ( & input) ;
435+ assert ! (
436+ result. is_err( ) ,
437+ "huge vector count with no payload must return Err, not panic or OOM"
438+ ) ;
439+ }
440+
441+ #[ test]
442+ fn decode_truncated_decimal_returns_error ( ) {
443+ // Tag 7 (Decimal) requires 16 bytes; supply only 4.
444+ // `data[cursor..cursor+16]` panics today; after the fix it errors.
445+ let input = {
446+ let mut v = vec ! [ 7u8 ] ; // tag = decimal
447+ v. extend_from_slice ( & [ 0u8 ; 4 ] ) ; // only 4 bytes, need 16
448+ v
449+ } ;
450+ let result = decode_row_from_wal ( & input) ;
451+ assert ! (
452+ result. is_err( ) ,
453+ "truncated decimal payload must return Err, not panic"
454+ ) ;
455+ }
338456}
0 commit comments