| 1 | #![no_main] |
| 2 | use libfuzzer_sys::fuzz_target; |
| 3 | use onestore::{ |
| 4 | Arena, Reference, RevisionIndex, Section, Store, |
| 5 | protected::{Key, Limits, UnlockedSection}, |
| 6 | }; |
| 7 | use std::{ops::Range, sync::LazyLock}; |
| 8 | |
| 9 | type Source = (Vec<u8>, String, Vec<Range<usize>>, Key); |
| 10 | |
| 11 | static SOURCES: LazyLock<Vec<Source>> = LazyLock::new(|| { |
| 12 | [ |
| 13 | (&include_bytes!("../../corpus/native-encrypted/encrypted-01/notebook/synthetic.one")[..], include_str!("../../corpus/native-encrypted/manifest.json")), |
| 14 | (&include_bytes!("../../corpus/native-protected-boundaries/notebook/synthetic.one")[..], include_str!("../../corpus/native-protected-boundaries/manifest.json")), |
| 15 | ].into_iter().map(|(bytes, manifest)| { |
| 16 | let manifest: serde_json::Value = serde_json::from_str(manifest).unwrap(); |
| 17 | let store = Store::parse(bytes).unwrap(); |
| 18 | let mut ranges: Vec<_> = store.lists.values().flat_map(|list| &list.nodes).filter_map(|node| { |
| 19 | let Reference::Data(chunk) = node.reference? else { return None; }; |
| 20 | let start = usize::try_from(chunk.offset).unwrap(); |
| 21 | let end = start + usize::try_from(chunk.length).unwrap(); |
| 22 | (start < end).then_some(start..end) |
| 23 | }).collect(); |
| 24 | ranges.sort_by_key(|range| (range.start, range.end)); |
| 25 | ranges.dedup(); |
| 26 | let password = manifest["password"].as_str().unwrap().to_owned(); |
| 27 | let key = Key::open(bytes, &password).unwrap(); |
| 28 | (bytes.to_vec(), password, ranges, key) |
| 29 | }).collect() |
| 30 | }); |
| 31 | |
| 32 | fuzz_target!(|data: &[u8]| { |
| 33 | if data.len() < 8 { |
| 34 | return; |
| 35 | } |
| 36 | let (source, password, ranges, key) = &SOURCES[usize::from(data[0]) % SOURCES.len()]; |
| 37 | let mut bytes = source.clone(); |
| 38 | let mut password = password.clone(); |
| 39 | let mode = data[1] % 3; |
| 40 | if mode == 2 { |
| 41 | password.push_str(&String::from_utf8_lossy(&data[8..])); |
| 42 | } else { |
| 43 | let range = if mode == 0 { |
| 44 | 0..bytes.len() |
| 45 | } else { |
| 46 | ranges[usize::from(u16::from_le_bytes([data[2], data[3]])) % ranges.len()].clone() |
| 47 | }; |
| 48 | let offset = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize % range.len(); |
| 49 | let count = (range.len() - offset).min(data.len() - 8); |
| 50 | bytes[range.start + offset..range.start + offset + count] |
| 51 | .copy_from_slice(&data[8..8 + count]); |
| 52 | } |
| 53 | // The kept-open section decodes the same bytes under the section's key. |
| 54 | if let Ok(mut section) = Section::unlock(&Arena::default(), bytes.clone(), key) { |
| 55 | for (space, ..) in section.pages().unwrap_or_default() { |
| 56 | let _ = section.page(space); |
| 57 | } |
| 58 | } |
| 59 | let Ok(store) = Store::parse(&bytes) else { |
| 60 | return; |
| 61 | }; |
| 62 | let Ok(index) = RevisionIndex::parse(&store) else { |
| 63 | return; |
| 64 | }; |
| 65 | let result = UnlockedSection::open( |
| 66 | &index, |
| 67 | &password, |
| 68 | Limits { |
| 69 | kdf_rounds: 200000, |
| 70 | decoded_bytes: 4 * 1024 * 1024, |
| 71 | object_visits: 20000, |
| 72 | }, |
| 73 | ); |
| 74 | if mode == 2 && data.len() > 8 { |
| 75 | assert!(result.is_err()); |
| 76 | } |
| 77 | if let Ok(unlocked) = result { |
| 78 | let document = unlocked.document().unwrap(); |
| 79 | let _ = document.pages(); |
| 80 | for revision in document.spaces.values().flat_map(|s| s.revisions.values()) { |
| 81 | for (id, node) in &revision.nodes { |
| 82 | if matches!(node.kind, onestore::document::Kind::RichText { .. }) { |
| 83 | let _ = revision.text_runs(*id); |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | }); |