max / makenotwork
| 1 | //! Byte fuzz over the whole substitution surface. |
| 2 | //! |
| 3 | //! Row 6 of `astra-soak-overview`: creator templates reach this parser, so the |
| 4 | //! boundary is hostile input and the failure cost is injection or a panic DoS. |
| 5 | //! It is named there as the cheapest target in the tree, which is why it is the |
| 6 | //! one that proves the soak loop before eight more harnesses are written. |
| 7 | //! |
| 8 | //! `substitute` is the entry point rather than `parse_expr`, which is |
| 9 | //! `pub(crate)` and only half the surface. One call runs code-span detection, |
| 10 | //! the marker regex, path lookup, the filter chain and the formatter, so |
| 11 | //! fuzzing the public function covers the private parser and everything the |
| 12 | //! parser hands off to. |
| 13 | //! |
| 14 | //! ## Oracles |
| 15 | //! |
| 16 | //! Not-panicking is the weakest thing a fuzz target can assert, and a target |
| 17 | //! that asserts only that will report clean forever while returning wrong |
| 18 | //! answers. Three real properties are checked besides: |
| 19 | //! |
| 20 | //! 1. **Code-span ranges are usable as byte ranges.** `code_span_ranges` is |
| 21 | //! public API and its output is used to index the input. A range past the |
| 22 | //! end, or one that splits a UTF-8 sequence, is a panic in every caller |
| 23 | //! rather than in this crate. |
| 24 | //! 2. **Text with no marker is returned unchanged.** The engine's whole |
| 25 | //! contract is that it touches `{{ ... }}` and nothing else. |
| 26 | //! 3. **Substitution reaches a fixed point.** Output that still contains a |
| 27 | //! resolvable marker would mean a value could inject a further |
| 28 | //! substitution, which is the injection half of the failure cost. |
| 29 | |
| 30 | |
| 31 | |
| 32 | use fuzz_target; |
| 33 | use ; |
| 34 | |
| 35 | /// A table with one value per `Value` shape, so the filter chain is reachable. |
| 36 | /// |
| 37 | /// Built per call rather than once in a `static`: a `Substituter` holds boxed |
| 38 | /// trait objects, and a target that shares mutable state between runs stops |
| 39 | /// being reproducible from its input alone, which is what makes a crash |
| 40 | /// artifact worth keeping. |
| 41 | |
| 42 | new |
| 43 | .with_value |
| 44 | .with_value |
| 45 | .with_value |
| 46 | .with_value |
| 47 | // No value here contains `{{`, and that is load-bearing for oracle 3. |
| 48 | // |
| 49 | // A value whose own text is a marker breaks the fixed point trivially: |
| 50 | // one pass yields the marker, a second resolves it. That says nothing |
| 51 | // about this crate, because the untrusted input on this path is the |
| 52 | // TEMPLATE, not the table -- `astra-soak-overview` row 6 is creator |
| 53 | // templates, and callers populate values from config they control. The |
| 54 | // first version of this target planted such a value and the fuzzer |
| 55 | // reported it in under a minute, which was the oracle being wrong rather |
| 56 | // than the engine. |
| 57 | // |
| 58 | // Whether hostile values should also be inert is a real question and a |
| 59 | // different one. It needs the caller contract stated first, and if it is |
| 60 | // ever answered "yes" it wants its own target rather than a weakened |
| 61 | // oracle here. |
| 62 | |
| 63 | |
| 64 | fuzz_target! |
| 65 | // Oracle 1. Checked before substitute, because substitute consumes these |
| 66 | // ranges and a bad one panics there instead of here, where the message |
| 67 | // would say which invariant broke. |
| 68 | let ranges = code_span_ranges; |
| 69 | let mut prev_end = 0usize; |
| 70 | for & in &ranges |
| 71 | assert!; |
| 72 | assert! |
| 73 | end <= text.len, |
| 74 | "code span {start}..{end} past len {}", |
| 75 | text.len |
| 76 | ; |
| 77 | assert! |
| 78 | text.is_char_boundary && text.is_char_boundary, |
| 79 | "code span {start}..{end} splits a UTF-8 sequence" |
| 80 | ; |
| 81 | assert! |
| 82 | start >= prev_end, |
| 83 | "code spans overlap or are unsorted at {start}" |
| 84 | ; |
| 85 | prev_end = end; |
| 86 | // The range is what callers slice with. Prove it. |
| 87 | let _ = &text; |
| 88 | |
| 89 | |
| 90 | let s = substituter; |
| 91 | let Ok = s.substitute else |
| 92 | // An unresolved marker is the documented error, not a defect. Nothing |
| 93 | // further is promised about the output in that case. |
| 94 | return; |
| 95 | ; |
| 96 | |
| 97 | // Oracle 2. |
| 98 | if !text.contains |
| 99 | assert_eq!; |
| 100 | |
| 101 | |
| 102 | // Oracle 3, deliberately narrow. |
| 103 | // |
| 104 | // An unconditional fixed point is not true even of correct code. A template |
| 105 | // may legitimately ask for text that looks like a marker: |
| 106 | // |
| 107 | // {{ price | money("{{ other }}") }} -> {{ other }}16.00 |
| 108 | // |
| 109 | // That is the author's literal string, correctly emitted, and substitution |
| 110 | // is one pass by design. Asserting otherwise would demand the engine escape |
| 111 | // its own output, which fights the crate's whole purpose: code spans exist |
| 112 | // so documentation can show template syntax. |
| 113 | // |
| 114 | // So the property is asserted where it does hold. With no quote in the |
| 115 | // input there is no string literal to carry marker text, and anything the |
| 116 | // engine emits must be inert. That still covers a scanner that mis-locates |
| 117 | // a marker, which mangles quoteless input too. |
| 118 | if !text.contains && !text.contains |
| 119 | if let Ok = s.substitute |
| 120 | assert_eq!; |
| 121 | |
| 122 | |
| 123 | ; |
| 124 |