From 5dcc542fa7e835d8dded8a60e9730afd1448aa08 Mon Sep 17 00:00:00 2001 From: jha Date: Mon, 2 Feb 2026 16:25:10 +0100 Subject: [PATCH 1/3] fix(codec/delimiter): fix panic in decode if delimiter is longer than payload --- framez/src/codec/delimiter.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framez/src/codec/delimiter.rs b/framez/src/codec/delimiter.rs index 8164f04..7020c0c 100644 --- a/framez/src/codec/delimiter.rs +++ b/framez/src/codec/delimiter.rs @@ -56,7 +56,7 @@ impl<'buf> Decoder<'buf> for Delimiter<'_> { } Some(last_byte) => { while self.seen < src.len() { - if src[self.seen] == *last_byte { + if src[self.seen] == *last_byte && self.delimiter.len() <= self.seen + 1 { let src_delimiter = &src[self.seen + 1 - self.delimiter.len()..self.seen + 1]; @@ -202,8 +202,9 @@ mod test { b"Hey".to_vec(), ]; - let decoder = Delimiter::new(b"###"); - let encoder = Delimiter::new(b"###"); + // TODO: use delimiters with different lengths in the fuzzer + let decoder = Delimiter::new(b"######"); + let encoder = Delimiter::new(b"######"); let map = |item: &[u8]| item.to_vec(); sink_stream!(encoder, decoder, items, map); From b9534d4d3a91a4172350865e07325ed114ed2180 Mon Sep 17 00:00:00 2001 From: "Jad K. Haddad" Date: Thu, 26 Feb 2026 13:58:13 +0100 Subject: [PATCH 2/3] feat(codec/delimiter)!: Delimiter now uses a single byte as a delimiter instead of a slice Signed-off-by: Jad K. Haddad --- framez/src/codec/delimiter.rs | 79 +++++++++++++++-------------------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/framez/src/codec/delimiter.rs b/framez/src/codec/delimiter.rs index 7020c0c..100ce42 100644 --- a/framez/src/codec/delimiter.rs +++ b/framez/src/codec/delimiter.rs @@ -14,68 +14,53 @@ use crate::{ /// This codec tracks progress using an internal state of the underlying buffer, and it must not be used across multiple framing sessions. #[derive(Debug, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Delimiter<'a> { +pub struct Delimiter { /// The delimiter to search for. - delimiter: &'a [u8], + delimiter: u8, /// The number of bytes of the slice that have been seen so far. seen: usize, } -impl<'a> Delimiter<'a> { +impl Delimiter { /// Creates a new [`Delimiter`] with the given `delimiter`. #[inline] - pub const fn new(delimiter: &'a [u8]) -> Self { + pub const fn new(delimiter: u8) -> Self { Self { delimiter, seen: 0 } } /// Returns the delimiter to search for. #[inline] - pub const fn delimiter(&self) -> &'a [u8] { + pub const fn delimiter(&self) -> u8 { self.delimiter } } -impl DecodeError for Delimiter<'_> { +impl DecodeError for Delimiter { type Error = Infallible; } -impl<'buf> Decoder<'buf> for Delimiter<'_> { +impl<'buf> Decoder<'buf> for Delimiter { type Item = &'buf [u8]; fn decode(&mut self, src: &'buf mut [u8]) -> Result, Self::Error> { - if src.len() < self.delimiter.len() { + if src.is_empty() { return Ok(None); } - match self.delimiter.last() { - None => { - let bytes = &src[..self.seen + 1]; + while self.seen < src.len() { + if src[self.seen] == self.delimiter { + let bytes = &src[..self.seen]; let item = (bytes, self.seen + 1); - Ok(Some(item)) - } - Some(last_byte) => { - while self.seen < src.len() { - if src[self.seen] == *last_byte && self.delimiter.len() <= self.seen + 1 { - let src_delimiter = - &src[self.seen + 1 - self.delimiter.len()..self.seen + 1]; - - if src_delimiter == self.delimiter { - let bytes = &src[..self.seen + 1 - self.delimiter.len()]; - let item = (bytes, self.seen + 1); - - self.seen = 0; - - return Ok(Some(item)); - } - } + self.seen = 0; - self.seen += 1; - } - - Ok(None) + return Ok(Some(item)); } + + self.seen += 1; } + + Ok(None) } } @@ -97,18 +82,18 @@ impl core::fmt::Display for DelimiterEncodeError { impl core::error::Error for DelimiterEncodeError {} -impl Encoder<&[u8]> for Delimiter<'_> { +impl Encoder<&[u8]> for Delimiter { type Error = DelimiterEncodeError; fn encode(&mut self, item: &[u8], dst: &mut [u8]) -> Result { - let size = item.len() + self.delimiter.len(); + let size = item.len() + 1; if dst.len() < size { return Err(DelimiterEncodeError::BufferTooSmall); } dst[..item.len()].copy_from_slice(item); - dst[item.len()..size].copy_from_slice(self.delimiter); + dst[item.len()..size].copy_from_slice(&[self.delimiter]); Ok(size) } @@ -134,17 +119,17 @@ mod test { // cspell: disable let items: &[&[u8]] = &[ - b"jh asjd##ppppppppppppppp##", - b"k hb##jsjuwjal kadj##jsadhjiu##w", - b"##jal kadjjsadhjiuwqens ##", + b"jh asjd#ppppppppppppppp#", + b"k hb#jsjuwjal kadj#jsadhjiu#w", + b"#jal kadjjsadhjiuwqens #", b"nd ", - b"yxxcjajsdi##askdn as", - b"jdasd##iouqw es", - b"sd##k", + b"yxxcjajsdi#askdn as", + b"jdasd#iouqw es", + b"sd#k", ]; // cspell: enable - let decoder = Delimiter::new(b"##"); + let decoder = Delimiter::new(b'#'); let expected: &[&[u8]] = &[]; framed_read!(items, expected, decoder, 1, BufferTooSmall); @@ -165,7 +150,7 @@ mod test { // cspell: disable let expected: &[&[u8]] = &[b"jh asjd"]; - framed_read!(items, expected, decoder, 16, BufferTooSmall); + framed_read!(items, expected, decoder, 14, BufferTooSmall); let expected: &[&[u8]] = &[ b"jh asjd", @@ -200,11 +185,13 @@ mod test { b"Hei".to_vec(), b"sup".to_vec(), b"Hey".to_vec(), + b"He".to_vec(), + b"H".to_vec(), + b"".to_vec(), ]; - // TODO: use delimiters with different lengths in the fuzzer - let decoder = Delimiter::new(b"######"); - let encoder = Delimiter::new(b"######"); + let decoder = Delimiter::new(b'#'); + let encoder = Delimiter::new(b'#'); let map = |item: &[u8]| item.to_vec(); sink_stream!(encoder, decoder, items, map); From 452aec070de8d27128c1cdc99b2e75cf83269335 Mon Sep 17 00:00:00 2001 From: "Jad K. Haddad" Date: Thu, 26 Feb 2026 13:58:35 +0100 Subject: [PATCH 3/3] chore(fuzz): updated the fuzz functions and fuzzed locally Signed-off-by: Jad K. Haddad --- fuzz/Cargo.lock | 7 +++-- fuzz/Cargo.toml | 1 + fuzz/fuzz_targets/decode.rs | 7 ++++- fuzz/fuzz_targets/encode.rs | 7 ++++- fuzz/fuzz_targets/send_receive.rs | 52 ++++++++++++++++++------------- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index a84793e..7a0f1f8 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -19,9 +19,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "autocfg" @@ -107,7 +107,7 @@ dependencies = [ [[package]] name = "framez" -version = "0.2.1" +version = "0.3.1" dependencies = [ "embedded-io-async", "futures", @@ -117,6 +117,7 @@ dependencies = [ name = "framez-fuzz" version = "0.0.0" dependencies = [ + "arbitrary", "embedded-io-adapters", "framez", "heapless", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e1c945c..8ac9393 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -15,6 +15,7 @@ embedded-io-adapters = { version = "0.6.1", default-features = false, features = ] } heapless = { version = "0.8.0", default-features = false } framez = { path = "../framez", default-features = false } +arbitrary = "1.4.2" [workspace] members = ["."] diff --git a/fuzz/fuzz_targets/decode.rs b/fuzz/fuzz_targets/decode.rs index c3463e0..c70611a 100644 --- a/fuzz/fuzz_targets/decode.rs +++ b/fuzz/fuzz_targets/decode.rs @@ -6,6 +6,7 @@ #![no_main] +use arbitrary::Unstructured; use framez::{ codec::{ bytes::Bytes, @@ -20,7 +21,11 @@ fuzz_target!(|data: &[u8]| { let buf = &mut [0_u8; 64]; let data = &mut std::vec::Vec::from(data); - let mut codec = Delimiter::new(b"#"); + let delimiter = Unstructured::new(data) + .arbitrary::() + .expect("Failed to generate delimiter"); + + let mut codec = Delimiter::new(delimiter); let _ = codec.decode(data).expect("Must be Infallible"); let mut codec = Bytes::new(); diff --git a/fuzz/fuzz_targets/encode.rs b/fuzz/fuzz_targets/encode.rs index bd9a821..1b10c6e 100644 --- a/fuzz/fuzz_targets/encode.rs +++ b/fuzz/fuzz_targets/encode.rs @@ -6,6 +6,7 @@ #![no_main] +use arbitrary::Unstructured; use framez::{ codec::{ bytes::Bytes, @@ -19,7 +20,11 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { let buf = &mut [0_u8; 64]; - let mut codec = Delimiter::new(b"#"); + let delimiter = Unstructured::new(data) + .arbitrary::() + .expect("Failed to generate delimiter"); + + let mut codec = Delimiter::new(delimiter); let _ = codec.encode(data, buf); let mut codec = Bytes::new(); diff --git a/fuzz/fuzz_targets/send_receive.rs b/fuzz/fuzz_targets/send_receive.rs index ec83b95..01e7eda 100644 --- a/fuzz/fuzz_targets/send_receive.rs +++ b/fuzz/fuzz_targets/send_receive.rs @@ -13,6 +13,7 @@ use std::{ fmt::{Debug, Display}, }; +use arbitrary::Unstructured; use embedded_io_adapters::tokio_1::FromTokio; use framez::{ codec::{ @@ -27,27 +28,36 @@ use libfuzzer_sys::fuzz_target; use tokio::runtime::Runtime; fuzz_target!(|data: &[u8]| { - Runtime::new().expect("Runtime must build").block_on(async { - fuzz(data, Delimiter::new(b"#"), Delimiter::new(b"#"), |data| { - (!data.contains(&b'#')).then_some(data).ok_or(()) - }) - .await - .unwrap(); - - fuzz(data, Lines::new(), Lines::new(), |data| { - (!data.contains(&b'\n')).then_some(data).ok_or(()) - }) - .await - .unwrap(); - - fuzz(data, StrLines::new(), StrLines::new(), |data| { - (!data.contains(&b'\n')).then_some(data).ok_or(())?; - - str::from_utf8(data).map_err(|_| ()) - }) - .await - .unwrap(); - }); + Runtime::new() + .expect("Runtime must build") + .block_on(async move { + let delimiter = Unstructured::new(data) + .arbitrary::() + .expect("Failed to generate delimiter"); + + fuzz( + data, + Delimiter::new(delimiter), + Delimiter::new(delimiter), + |data| (!data.contains(&delimiter)).then_some(data).ok_or(()), + ) + .await + .unwrap(); + + fuzz(data, Lines::new(), Lines::new(), |data| { + (!data.contains(&b'\n')).then_some(data).ok_or(()) + }) + .await + .unwrap(); + + fuzz(data, StrLines::new(), StrLines::new(), |data| { + (!data.contains(&b'\n')).then_some(data).ok_or(())?; + + str::from_utf8(data).map_err(|_| ()) + }) + .await + .unwrap(); + }); }); // Note: Bytes can not be fuzzed like this