1use std::borrow::Cow;
4use std::collections::HashSet;
5use std::num::TryFromIntError;
6use std::ops::Range;
7use std::str::Utf8Error;
8
9use minidump::format::{
10 CvSignature, MINIDUMP_LOCATION_DESCRIPTOR, MINIDUMP_STREAM_TYPE as StreamType,
11};
12use minidump::{
13 Endian, Error as MinidumpError, Minidump, MinidumpMemoryList, MinidumpModuleList,
14 MinidumpThreadList,
15};
16use num_traits::FromPrimitive;
17use relay_event_schema::processor::{FieldAttrs, Pii, ValueType};
18use utf16string::{Utf16Error, WStr};
19
20use crate::{PiiAttachmentsProcessor, ScrubEncodings};
21
22#[derive(Debug, thiserror::Error)]
24pub enum ScrubMinidumpError {
25 #[error("failed to parse minidump")]
27 InvalidMinidump(#[from] MinidumpError),
28
29 #[error("invalid memory address")]
31 InvalidAddress,
32
33 #[error("minidump offsets out of usize range")]
35 OutOfRange,
36
37 #[error("string decoding error")]
39 Decoding,
40}
41
42impl From<TryFromIntError> for ScrubMinidumpError {
43 fn from(_source: TryFromIntError) -> Self {
44 Self::OutOfRange
45 }
46}
47
48impl From<Utf16Error> for ScrubMinidumpError {
49 fn from(_source: Utf16Error) -> Self {
50 Self::Decoding
51 }
52}
53
54impl From<Utf8Error> for ScrubMinidumpError {
55 fn from(_source: Utf8Error) -> Self {
56 Self::Decoding
57 }
58}
59
60#[derive(Debug, Clone, Eq, PartialEq, Hash)]
66enum MinidumpItem {
67 StackMemory(Range<usize>),
69 NonStackMemory(Range<usize>),
71 LinuxEnviron(Range<usize>),
75 LinuxCmdLine(Range<usize>),
79 CodeModuleName(Range<usize>),
81 DebugModuleName(Range<usize>),
83}
84
85struct MinidumpData<'a> {
87 data: &'a [u8],
88 minidump: Minidump<'a, &'a [u8]>,
89}
90
91impl<'a> MinidumpData<'a> {
92 fn parse(data: &'a [u8]) -> Result<Self, ScrubMinidumpError> {
97 let minidump = Minidump::read(data).map_err(ScrubMinidumpError::InvalidMinidump)?;
98 Ok(Self { data, minidump })
99 }
100
101 fn offset(&self, slice: &[u8]) -> Option<usize> {
107 let base = self.data.as_ptr() as usize;
108 let pointer = slice.as_ptr() as usize;
109
110 if pointer > base {
111 Some(pointer - base)
112 } else {
113 None
114 }
115 }
116
117 fn slice_range(&self, slice: &[u8]) -> Option<Range<usize>> {
119 let start = self.offset(slice)?;
120 let end = start + slice.len();
121 Some(start..end)
122 }
123
124 fn location_range(
128 &self,
129 location: MINIDUMP_LOCATION_DESCRIPTOR,
130 ) -> Result<Range<usize>, ScrubMinidumpError> {
131 let start: usize = location.rva.try_into()?;
132 let len: usize = location.data_size.try_into()?;
133 Ok(start..start + len)
134 }
135
136 fn raw_stream_range(
138 &self,
139 stream_type: StreamType,
140 ) -> Result<Option<Range<usize>>, ScrubMinidumpError> {
141 let range = match self.minidump.get_raw_stream(stream_type.into()) {
142 Ok(stream) => Some(
143 self.slice_range(stream)
144 .ok_or(ScrubMinidumpError::InvalidAddress)?,
145 ),
146 Err(MinidumpError::StreamNotFound) => None,
147 Err(e) => return Err(ScrubMinidumpError::InvalidMinidump(e)),
148 };
149 Ok(range)
150 }
151
152 fn items(&self) -> Result<Vec<MinidumpItem>, ScrubMinidumpError> {
154 let mut items = Vec::new();
155
156 let thread_list: MinidumpThreadList = self.minidump.get_stream()?;
157 let stack_memory_rvas: HashSet<u32> = thread_list
158 .threads
159 .iter()
160 .map(|t| t.raw.stack.memory.rva)
161 .collect();
162
163 let mem_list: MinidumpMemoryList = self.minidump.get_stream()?;
165
166 for mem in mem_list.iter() {
167 if stack_memory_rvas.contains(&mem.desc.memory.rva) {
168 items.push(MinidumpItem::StackMemory(
169 self.location_range(mem.desc.memory)?,
170 ));
171 } else {
172 items.push(MinidumpItem::NonStackMemory(
173 self.location_range(mem.desc.memory)?,
174 ));
175 }
176 }
177
178 if let Some(range) = self.raw_stream_range(StreamType::LinuxEnviron)? {
179 items.push(MinidumpItem::LinuxEnviron(range));
180 }
181 if let Some(range) = self.raw_stream_range(StreamType::LinuxCmdLine)? {
182 items.push(MinidumpItem::LinuxCmdLine(range));
183 }
184
185 let mod_list: MinidumpModuleList = self.minidump.get_stream()?;
186 let mut rvas = HashSet::new();
187 for module in mod_list.iter() {
188 let rva: usize = module.raw.module_name_rva.try_into()?;
189 if !rvas.insert(rva) {
190 continue;
191 }
192 let len_bytes = self
193 .data
194 .get(rva..)
195 .ok_or(ScrubMinidumpError::InvalidAddress)?;
196 let len: usize = u32_from_bytes(len_bytes, self.minidump.endian)?.try_into()?;
197 let start: usize = rva + 4;
198 items.push(MinidumpItem::CodeModuleName(start..start + len));
199
200 let codeview_loc = module.raw.cv_record;
202 let cv_start: usize = codeview_loc.rva.try_into()?;
203 let cv_len: usize = codeview_loc.data_size.try_into()?;
204 let signature_bytes = self
205 .data
206 .get(cv_start..)
207 .ok_or(ScrubMinidumpError::InvalidAddress)?;
208 let signature = u32_from_bytes(signature_bytes, self.minidump.endian)?;
209 match CvSignature::from_u32(signature) {
210 Some(CvSignature::Pdb70) => {
211 let offset: usize = 4 + (4 + 2 + 2 + 8) + 4; items.push(MinidumpItem::DebugModuleName(
213 (cv_start + offset)..(cv_start + cv_len),
214 ));
215 }
216 Some(CvSignature::Pdb20) => {
217 let offset: usize = 4 + 4 + 4 + 4; items.push(MinidumpItem::DebugModuleName(
219 (cv_start + offset)..(cv_start + cv_len),
220 ));
221 }
222 _ => {}
223 }
224 }
225
226 Ok(items)
227 }
228}
229
230fn u32_from_bytes(bytes: &[u8], endian: Endian) -> Result<u32, ScrubMinidumpError> {
235 let mut buf = [0u8; 4];
236 buf.copy_from_slice(bytes.get(..4).ok_or(ScrubMinidumpError::InvalidAddress)?);
237 match endian {
238 Endian::Little => Ok(u32::from_le_bytes(buf)),
239 Endian::Big => Ok(u32::from_be_bytes(buf)),
240 }
241}
242
243impl PiiAttachmentsProcessor<'_> {
244 pub fn scrub_minidump(
255 &self,
256 filename: &str,
257 data: &mut [u8],
258 ) -> Result<bool, ScrubMinidumpError> {
259 let file_state = self.state(filename, ValueType::Minidump);
260 let items = MinidumpData::parse(data)?.items()?;
261 let mut changed = false;
262
263 for item in items {
264 match item {
265 MinidumpItem::StackMemory(range) => {
266 let slice = data
269 .get_mut(range)
270 .ok_or(ScrubMinidumpError::InvalidAddress)?;
271
272 let attrs = Cow::Owned(FieldAttrs::new().pii(Pii::Maybe));
273 let state = file_state.enter_borrowed(
274 "stack_memory",
275 Some(attrs),
276 ValueType::Binary | ValueType::StackMemory,
277 );
278 changed |= self.scrub_bytes(slice, &state, ScrubEncodings::All);
279 }
280 MinidumpItem::NonStackMemory(range) => {
281 let slice = data
282 .get_mut(range)
283 .ok_or(ScrubMinidumpError::InvalidAddress)?;
284 let attrs = Cow::Owned(FieldAttrs::new().pii(Pii::True));
285 let state = file_state.enter_borrowed(
286 "heap_memory",
287 Some(attrs),
288 ValueType::Binary | ValueType::HeapMemory,
289 );
290 changed |= self.scrub_bytes(slice, &state, ScrubEncodings::All);
291 }
292 MinidumpItem::LinuxEnviron(range) | MinidumpItem::LinuxCmdLine(range) => {
293 let slice = data
294 .get_mut(range)
295 .ok_or(ScrubMinidumpError::InvalidAddress)?;
296 let attrs = Cow::Owned(FieldAttrs::new().pii(Pii::True));
297 let state = file_state.enter_borrowed("", Some(attrs), Some(ValueType::Binary));
298 changed |= self.scrub_bytes(slice, &state, ScrubEncodings::All);
299 }
300 MinidumpItem::CodeModuleName(range) => {
301 let slice = data
302 .get_mut(range)
303 .ok_or(ScrubMinidumpError::InvalidAddress)?;
304 let attrs = Cow::Owned(FieldAttrs::new().pii(Pii::True));
305 let state = file_state.enter_borrowed(
307 "code_file",
308 Some(attrs),
309 Some(ValueType::String),
310 );
311 let wstr = WStr::from_utf16le_mut(slice)?; changed |= self.scrub_utf16_filepath(wstr, &state);
313 }
314 MinidumpItem::DebugModuleName(range) => {
315 let slice = data
316 .get_mut(range)
317 .ok_or(ScrubMinidumpError::InvalidAddress)?;
318 let attrs = Cow::Owned(FieldAttrs::new().pii(Pii::True));
319 let state = file_state.enter_borrowed(
321 "debug_file",
322 Some(attrs),
323 Some(ValueType::String),
324 );
325 let s = std::str::from_utf8_mut(slice)?;
326 changed |= self.scrub_utf8_filepath(s, &state);
327 }
328 };
329 }
330
331 Ok(changed)
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use minidump::format::RVA;
338 use minidump::{MinidumpModule, Module};
339
340 use super::*;
341 use crate::config::PiiConfig;
342
343 struct TestScrubber {
344 orig_dump: Minidump<'static, &'static [u8]>,
345 _scrubbed_data: Vec<u8>,
346 scrubbed_dump: Minidump<'static, &'static [u8]>,
347 }
348
349 impl TestScrubber {
350 fn new(filename: &str, orig_data: &'static [u8], json: serde_json::Value) -> Self {
351 let orig_dump = Minidump::read(orig_data).expect("original minidump failed to parse");
352 let mut scrubbed_data = Vec::from(orig_data);
353
354 let config = serde_json::from_value::<PiiConfig>(json).expect("invalid config json");
355 let processor = PiiAttachmentsProcessor::new(config.compiled());
356 processor
357 .scrub_minidump(filename, scrubbed_data.as_mut_slice())
358 .expect("scrubbing failed");
359
360 let slice =
365 unsafe { std::mem::transmute::<&[u8], &'static [u8]>(scrubbed_data.as_slice()) };
366 let scrubbed_dump = Minidump::read(slice).expect("scrubbed minidump failed to parse");
367 Self {
368 orig_dump,
369 _scrubbed_data: scrubbed_data,
370 scrubbed_dump,
371 }
372 }
373 }
374
375 enum Which {
376 Original,
377 Scrubbed,
378 }
379
380 enum MemRegion {
381 Stack,
382 Heap,
383 }
384
385 impl TestScrubber {
386 fn main_module(&self, which: Which) -> MinidumpModule {
387 let dump = match which {
388 Which::Original => &self.orig_dump,
389 Which::Scrubbed => &self.scrubbed_dump,
390 };
391 let modules: MinidumpModuleList = dump.get_stream().unwrap();
392 modules.main_module().unwrap().clone()
393 }
394
395 fn other_modules(&self, which: Which) -> Vec<MinidumpModule> {
396 let dump = match which {
397 Which::Original => &self.orig_dump,
398 Which::Scrubbed => &self.scrubbed_dump,
399 };
400 let modules: MinidumpModuleList = dump.get_stream().unwrap();
401 let mut iter = modules.iter();
402 iter.next(); iter.cloned().collect()
404 }
405
406 fn memory_regions<'slf>(&'slf self, which: Which, region: MemRegion) -> Vec<&'slf [u8]> {
408 let dump: &'slf Minidump<&'static [u8]> = match which {
409 Which::Original => &self.orig_dump,
410 Which::Scrubbed => &self.scrubbed_dump,
411 };
412
413 let thread_list: MinidumpThreadList = dump.get_stream().unwrap();
414 let stack_rvas: Vec<RVA> = thread_list
415 .threads
416 .iter()
417 .map(|t| t.raw.stack.memory.rva)
418 .collect();
419
420 let mem_list: MinidumpMemoryList<'slf> = dump.get_stream().unwrap();
426 mem_list
427 .iter()
428 .filter(|mem| match region {
429 MemRegion::Stack => stack_rvas.contains(&mem.desc.memory.rva),
430 MemRegion::Heap => !stack_rvas.contains(&mem.desc.memory.rva),
431 })
432 .map(|mem| unsafe { std::mem::transmute(mem.bytes) })
433 .collect()
434 }
435
436 fn stacks(&self, which: Which) -> Vec<&[u8]> {
438 self.memory_regions(which, MemRegion::Stack)
439 }
440
441 fn heaps(&self, which: Which) -> Vec<&[u8]> {
443 self.memory_regions(which, MemRegion::Heap)
444 }
445
446 fn environ(&self, which: Which) -> &[u8] {
450 let dump = match which {
451 Which::Original => &self.orig_dump,
452 Which::Scrubbed => &self.scrubbed_dump,
453 };
454 dump.get_raw_stream(StreamType::LinuxEnviron.into())
455 .unwrap()
456 }
457 }
458
459 #[test]
460 fn test_module_list_removed_win() {
461 let scrubber = TestScrubber::new(
462 "windows.dmp",
463 include_bytes!("../../tests/fixtures/windows.dmp"),
464 serde_json::json!(
465 {
466 "applications": {
467 "debug_file": ["@anything:mask"],
468 "$attachments.'windows.dmp'.code_file": ["@anything:mask"]
469 }
470 }
471 ),
472 );
473
474 let main = scrubber.main_module(Which::Original);
475 assert_eq!(
476 main.code_file(),
477 "C:\\projects\\breakpad-tools\\windows\\Release\\crash.exe"
478 );
479 assert_eq!(
480 main.debug_file().unwrap(),
481 "C:\\projects\\breakpad-tools\\windows\\Release\\crash.pdb"
482 );
483
484 let main = scrubber.main_module(Which::Scrubbed);
485 assert_eq!(
486 main.code_file(),
487 "******************************************\\crash.exe"
488 );
489 assert_eq!(
490 main.debug_file().unwrap(),
491 "******************************************\\crash.pdb"
492 );
493
494 let modules = scrubber.other_modules(Which::Original);
495 for module in modules {
496 assert!(
497 module.code_file().starts_with("C:\\Windows\\System32\\"),
498 "code file without full path"
499 );
500 assert!(module.debug_file().unwrap().ends_with(".pdb"));
501 }
502
503 let modules = scrubber.other_modules(Which::Scrubbed);
504 for module in modules {
505 assert!(
506 module.code_file().starts_with("*******************\\"),
507 "code file path not scrubbed"
508 );
509 assert!(module.debug_file().unwrap().ends_with(".pdb"));
510 }
511 }
512
513 #[test]
514 fn test_module_list_removed_lin() {
515 let scrubber = TestScrubber::new(
516 "linux.dmp",
517 include_bytes!("../../tests/fixtures/linux.dmp"),
518 serde_json::json!(
519 {
520 "applications": {
521 "debug_file": ["@anything:mask"],
522 "$attachments.*.code_file": ["@anything:mask"]
523 }
524 }
525 ),
526 );
527
528 let main = scrubber.main_module(Which::Original);
529 assert_eq!(main.code_file(), "/work/linux/build/crash");
530 assert_eq!(main.debug_file().unwrap(), "/work/linux/build/crash");
531
532 let main = scrubber.main_module(Which::Scrubbed);
533 assert_eq!(main.code_file(), "*****************/crash");
534 assert_eq!(main.debug_file().unwrap(), "*****************/crash");
535
536 let modules = scrubber.other_modules(Which::Original);
537 for module in modules {
538 assert!(
539 module.code_file().matches('/').count() > 1
540 || module.code_file() == "linux-gate.so",
541 "code file does not contain path"
542 );
543 assert!(
544 module.debug_file().unwrap().matches('/').count() > 1
545 || module.debug_file().unwrap() == "linux-gate.so",
546 "debug file does not contain a path"
547 );
548 }
549
550 let modules = scrubber.other_modules(Which::Scrubbed);
551 for module in modules {
552 assert!(
553 module.code_file().matches('/').count() == 1
554 || module.code_file() == "linux-gate.so",
555 "code file not scrubbed"
556 );
557 assert!(
558 module.debug_file().unwrap().matches('/').count() == 1
559 || module.debug_file().unwrap() == "linux-gate.so",
560 "scrubbed debug file contains a path"
561 );
562 }
563 }
564
565 #[test]
566 fn test_module_list_removed_mac() {
567 let scrubber = TestScrubber::new(
568 "macos.dmp",
569 include_bytes!("../../tests/fixtures/macos.dmp"),
570 serde_json::json!(
571 {
572 "applications": {
573 "debug_file": ["@anything:mask"],
574 "$attachments.*.code_file": ["@anything:mask"]
575 }
576 }
577 ),
578 );
579
580 let main = scrubber.main_module(Which::Original);
581 assert_eq!(
582 main.code_file(),
583 "/Users/travis/build/getsentry/breakpad-tools/macos/build/./crash"
584 );
585 assert_eq!(main.debug_file().unwrap(), "crash");
586
587 let main = scrubber.main_module(Which::Scrubbed);
588 assert_eq!(
589 main.code_file(),
590 "**********************************************************/crash"
591 );
592 assert_eq!(main.debug_file().unwrap(), "crash");
593
594 let modules = scrubber.other_modules(Which::Original);
595 for module in modules {
596 assert!(
597 module.code_file().matches('/').count() > 1,
598 "code file does not contain path"
599 );
600 assert!(
601 module.debug_file().unwrap().matches('/').count() == 0,
602 "debug file contains a path"
603 );
604 }
605
606 let modules = scrubber.other_modules(Which::Scrubbed);
607 for module in modules {
608 assert!(
609 module.code_file().matches('/').count() == 1,
610 "code file not scrubbed"
611 );
612 assert!(
613 module.debug_file().unwrap().matches('/').count() == 0,
614 "scrubbed debug file contains a path"
615 );
616 }
617 }
618
619 #[test]
620 fn test_module_list_selectors() {
621 let scrubber = TestScrubber::new(
623 "linux.dmp",
624 include_bytes!("../../tests/fixtures/linux.dmp"),
625 serde_json::json!(
626 {
627 "applications": {
628 "$string": ["@anything:mask"],
629 }
630 }
631 ),
632 );
633 let main = scrubber.main_module(Which::Scrubbed);
634 assert_eq!(main.code_file(), "*****************/crash");
635 assert_eq!(main.debug_file().unwrap(), "*****************/crash");
636 }
637
638 #[test]
639 fn test_stack_scrubbing_backwards_compatible_selector() {
640 let scrubber = TestScrubber::new(
643 "linux.dmp",
644 include_bytes!("../../tests/fixtures/linux.dmp"),
645 serde_json::json!(
646 {
647 "applications": {
648 "$stack_memory": ["@anything:mask"],
649 }
650 }
651 ),
652 );
653 for stack in scrubber.stacks(Which::Scrubbed) {
654 assert!(stack.iter().all(|b| *b == b'*'));
655 }
656 }
657
658 #[test]
659 fn test_stack_scrubbing_path_item_selector() {
660 let scrubber = TestScrubber::new(
661 "linux.dmp",
662 include_bytes!("../../tests/fixtures/linux.dmp"),
663 serde_json::json!(
664 {
665 "applications": {
666 "$minidump.stack_memory": ["@anything:mask"],
667 }
668 }
669 ),
670 );
671 for stack in scrubber.stacks(Which::Scrubbed) {
672 assert!(stack.iter().all(|b| *b == b'*'));
673 }
674 }
675
676 #[test]
677 #[should_panic]
678 fn test_stack_scrubbing_valuetype_selector() {
679 let scrubber = TestScrubber::new(
682 "linux.dmp",
683 include_bytes!("../../tests/fixtures/linux.dmp"),
684 serde_json::json!(
685 {
686 "applications": {
687 "$minidump.$binary": ["@anything:mask"],
688 }
689 }
690 ),
691 );
692 for stack in scrubber.stacks(Which::Scrubbed) {
693 assert!(stack.iter().all(|b| *b == b'*'));
694 }
695 }
696
697 #[test]
698 fn test_stack_scrubbing_valuetype_not_fully_qualified() {
699 let scrubber = TestScrubber::new(
701 "linux.dmp",
702 include_bytes!("../../tests/fixtures/linux.dmp"),
703 serde_json::json!(
704 {
705 "applications": {
706 "$binary": ["@anything:mask"],
707 }
708 }
709 ),
710 );
711 for (scrubbed_stack, original_stack) in scrubber
712 .stacks(Which::Scrubbed)
713 .iter()
714 .zip(scrubber.stacks(Which::Original).iter())
715 {
716 assert_eq!(scrubbed_stack, original_stack);
717 }
718 }
719
720 #[test]
721 #[should_panic]
722 fn test_stack_scrubbing_wildcard() {
723 let scrubber = TestScrubber::new(
726 "linux.dmp",
727 include_bytes!("../../tests/fixtures/linux.dmp"),
728 serde_json::json!(
729 {
730 "applications": {
731 "$minidump.*": ["@anything:mask"],
732 }
733 }
734 ),
735 );
736 for (scrubbed_stack, original_stack) in scrubber
737 .stacks(Which::Scrubbed)
738 .iter()
739 .zip(scrubber.stacks(Which::Original).iter())
740 {
741 assert_eq!(scrubbed_stack, original_stack);
742 }
743 }
744
745 #[test]
746 fn test_stack_scrubbing_deep_wildcard() {
747 let scrubber = TestScrubber::new(
749 "linux.dmp",
750 include_bytes!("../../tests/fixtures/linux.dmp"),
751 serde_json::json!(
752 {
753 "applications": {
754 "$attachments.**": ["@anything:mask"],
755 }
756 }
757 ),
758 );
759 for (scrubbed_stack, original_stack) in scrubber
760 .stacks(Which::Scrubbed)
761 .iter()
762 .zip(scrubber.stacks(Which::Original).iter())
763 {
764 assert_eq!(scrubbed_stack, original_stack);
765 }
766 }
767
768 #[test]
769 fn test_stack_scrubbing_binary_not_stack() {
770 let scrubber = TestScrubber::new(
771 "linux.dmp",
772 include_bytes!("../../tests/fixtures/linux.dmp"),
773 serde_json::json!(
774 {
775 "applications": {
776 "$binary && !stack_memory": ["@anything:mask"],
777 }
778 }
779 ),
780 );
781 for (scrubbed_stack, original_stack) in scrubber
782 .stacks(Which::Scrubbed)
783 .iter()
784 .zip(scrubber.stacks(Which::Original).iter())
785 {
786 assert_eq!(scrubbed_stack, original_stack);
787 }
788 for heap in scrubber.heaps(Which::Scrubbed) {
789 assert!(heap.iter().all(|b| *b == b'*'));
790 }
791 }
792
793 #[test]
794 fn test_linux_environ_valuetype() {
795 let scrubber = TestScrubber::new(
797 "linux.dmp",
798 include_bytes!("../../tests/fixtures/linux.dmp"),
799 serde_json::json!(
800 {
801 "applications": {
802 "$binary": ["@anything:mask"],
803 }
804 }
805 ),
806 );
807 let environ = scrubber.environ(Which::Scrubbed);
808 assert!(environ.iter().all(|b| *b == b'*'));
809 }
810}