decode_payload.rs
| 1 | //! Fuzzes the payload decoders directly, behind the AEAD. |
| 2 | //! |
| 3 | //! ```sh |
| 4 | //! cargo +nightly fuzz run decode_payload |
| 5 | //! ``` |
| 6 | //! |
| 7 | //! This is where the structural parsing lives — point counts that disagree with |
| 8 | //! the payload length, unknown enum discriminants, arithmetic on attacker-chosen |
| 9 | //! lengths. Reaching it through a sealed datagram would require forging a |
| 10 | //! Poly1305 tag, so it gets its own target with the crypto stripped away. In |
| 11 | //! production only a client holding a valid token can reach this code, which |
| 12 | //! bounds the blast radius but does not make it safe to panic in. |
| 13 | |
| 14 | #![no_main] |
| 15 | |
| 16 | use libfuzzer_sys::fuzz_target; |
| 17 | use otproto::{Message, MsgType}; |
| 18 | |
| 19 | fuzz_target!(|data: &[u8]| { |
| 20 | // First byte picks the message type; the rest is the payload. |
| 21 | let Some((&ty, payload)) = data.split_first() else { |
| 22 | return; |
| 23 | }; |
| 24 | let Ok(ty) = MsgType::try_from(ty & 0x0F) else { |
| 25 | return; |
| 26 | }; |
| 27 | |
| 28 | if let Ok(msg) = Message::decode_payload(ty, payload) { |
| 29 | assert_eq!(msg.msg_type(), ty); |
| 30 | // Anything that decodes must re-encode to the same *length*, and |
| 31 | // `payload_len` must agree without allocating. Byte equality is not |
| 32 | // asserted because reserved bytes are ignored on decode and written as |
| 33 | // zero on encode — deliberately, so a later version can populate them. |
| 34 | let re = msg.encode_payload(); |
| 35 | assert_eq!(re.len(), payload.len()); |
| 36 | assert_eq!(msg.payload_len(), payload.len()); |
| 37 | } |
| 38 | }); |
| 39 |