|
| 1 | +//! `ConfigDSNW`'s attribute-list parser, driven over raw pointers. |
| 2 | +//! |
| 3 | +//! `test_support::parse_attributes_summary_w` wraps |
| 4 | +//! `ffi::setup::parse_attributes_w`, which walks a `*const u16` the Driver |
| 5 | +//! Manager supplied, hunting for the double null that ends the list. It is the |
| 6 | +//! one parser in core whose failure mode is a read past the end of an |
| 7 | +//! allocation rather than a panic, which is what makes AddressSanitizer worth |
| 8 | +//! the nightly toolchain here. |
| 9 | +//! |
| 10 | +//! # Every buffer is terminated |
| 11 | +//! |
| 12 | +//! The parser's safety contract is that the pointer is null, or points to a |
| 13 | +//! valid double-null-terminated `u16` sequence. So each shape below appends that |
| 14 | +//! terminator itself and fuzzes what comes *before* it. Handing the parser an |
| 15 | +//! unterminated buffer would certainly produce an ASAN report, and it would be a |
| 16 | +//! report about this file: the read past the end would be the caller breaking a |
| 17 | +//! contract it agreed to, not the parser exceeding one. What is worth fuzzing is |
| 18 | +//! the walk over a buffer that is terminated but says nothing else sensible. |
| 19 | +//! |
| 20 | +//! The parser bounds each segment at `i16::MAX` code units precisely so a caller |
| 21 | +//! that gets this wrong is contained rather than unbounded, and the |
| 22 | +//! `OverlongSegment` shape reaches that bound from inside a real allocation. |
| 23 | +
|
| 24 | +#![no_main] |
| 25 | + |
| 26 | +use arbitrary::Arbitrary; |
| 27 | +use libfuzzer_sys::fuzz_target; |
| 28 | +use stackable_odbc_core::test_support::parse_attributes_summary_w; |
| 29 | + |
| 30 | +/// One code unit past the parser's own per-segment scan limit, which is |
| 31 | +/// `i16::MAX`. Declared here rather than imported because it is crate-private, |
| 32 | +/// and a copy that drifts makes this shape stop reaching the bound rather than |
| 33 | +/// start failing, so the assertion below checks the bound was actually hit. |
| 34 | +const PAST_SEGMENT_SCAN_LIMIT: usize = i16::MAX as usize + 1; |
| 35 | + |
| 36 | +#[derive(Arbitrary, Debug)] |
| 37 | +enum Shape { |
| 38 | + /// A `u16`-aligned buffer, which is the ordinary case. |
| 39 | + Aligned, |
| 40 | + /// A buffer whose `u16` sequence starts at an odd byte address. |
| 41 | + /// |
| 42 | + /// The Driver Manager promises no alignment, and the parser reads every code |
| 43 | + /// unit with `read_unaligned` for that reason. An aligned read of this |
| 44 | + /// pointer is undefined behaviour, and in a debug build it aborts without |
| 45 | + /// unwinding, which no panic hook can contain. A regression to |
| 46 | + /// `slice::from_raw_parts` would be caught here and nowhere else. |
| 47 | + Unaligned, |
| 48 | + /// A segment longer than the parser will scan, so the scan limit fires with |
| 49 | + /// every read still inside the allocation. |
| 50 | + OverlongSegment, |
| 51 | +} |
| 52 | + |
| 53 | +#[derive(Arbitrary, Debug)] |
| 54 | +struct Input { |
| 55 | + shape: Shape, |
| 56 | + units: Vec<u16>, |
| 57 | +} |
| 58 | + |
| 59 | +fuzz_target!(|input: Input| { |
| 60 | + match input.shape { |
| 61 | + Shape::Aligned => { |
| 62 | + let mut buf = input.units; |
| 63 | + buf.extend_from_slice(&[0, 0]); |
| 64 | + // SAFETY: `buf` is non-empty and ends in two zero `u16`s, so it is a |
| 65 | + // double-null-terminated sequence, and it outlives the call. |
| 66 | + let _ = unsafe { parse_attributes_summary_w(buf.as_ptr()) }; |
| 67 | + } |
| 68 | + |
| 69 | + Shape::Unaligned => { |
| 70 | + // One byte of padding in front, so the `u16` sequence begins at an |
| 71 | + // odd address. Everything after it stays a whole number of code |
| 72 | + // units, which keeps the terminator two aligned-to-the-sequence |
| 73 | + // zeros rather than a split pair. |
| 74 | + let mut bytes = vec![0u8]; |
| 75 | + for unit in &input.units { |
| 76 | + bytes.extend_from_slice(&unit.to_le_bytes()); |
| 77 | + } |
| 78 | + bytes.extend_from_slice(&[0, 0, 0, 0]); |
| 79 | + |
| 80 | + // SAFETY: offset 1 is inside `bytes`, which holds `1 + 2n + 4` |
| 81 | + // bytes, so the sequence from there is `n + 2` whole `u16`s ending |
| 82 | + // in two zeros. The pointer is read only with `read_unaligned`, so |
| 83 | + // the odd address is sound, and `bytes` outlives the call. |
| 84 | + let ptr = unsafe { bytes.as_ptr().add(1) }.cast::<u16>(); |
| 85 | + let _ = unsafe { parse_attributes_summary_w(ptr) }; |
| 86 | + } |
| 87 | + |
| 88 | + Shape::OverlongSegment => { |
| 89 | + // The fuzzed units come first, so their bytes still drive real |
| 90 | + // segments, and the overlong run is appended behind a separator. |
| 91 | + let mut buf = input.units; |
| 92 | + buf.push(0); |
| 93 | + |
| 94 | + // Decided on the prefix alone, before the run and the terminator are |
| 95 | + // appended: those end the list by construction, so a check made |
| 96 | + // after them would be true every time and assert nothing. |
| 97 | + let ends_early = ends_the_list(&buf); |
| 98 | + |
| 99 | + buf.resize(buf.len() + PAST_SEGMENT_SCAN_LIMIT, u16::from(b'A')); |
| 100 | + buf.extend_from_slice(&[0, 0]); |
| 101 | + |
| 102 | + // SAFETY: as the aligned case; `buf` ends in two zero `u16`s. |
| 103 | + let (_, _, syntax_error) = unsafe { parse_attributes_summary_w(buf.as_ptr()) }; |
| 104 | + |
| 105 | + // Where the run is reached at all, the scan limit must have fired. |
| 106 | + assert!( |
| 107 | + syntax_error || ends_early, |
| 108 | + "a segment of {PAST_SEGMENT_SCAN_LIMIT} code units must trip the scan limit" |
| 109 | + ); |
| 110 | + } |
| 111 | + } |
| 112 | +}); |
| 113 | + |
| 114 | +/// Whether the parser stops inside `units` rather than walking off its end. |
| 115 | +/// |
| 116 | +/// It stops at an empty segment, which is a leading null or two consecutive |
| 117 | +/// ones. |
| 118 | +fn ends_the_list(units: &[u16]) -> bool { |
| 119 | + units.first() == Some(&0) || units.windows(2).any(|pair| pair == [0, 0]) |
| 120 | +} |
0 commit comments