Skip to main content

relay_pii/
minidumps.rs

1//! Minidump scrubbing.
2
3use 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/// An error returned from [`PiiAttachmentsProcessor::scrub_minidump`].
23#[derive(Debug, thiserror::Error)]
24pub enum ScrubMinidumpError {
25    /// Failed to parse open or parse the minidump.
26    #[error("failed to parse minidump")]
27    InvalidMinidump(#[from] MinidumpError),
28
29    /// The minidump contains an invalid memory address.
30    #[error("invalid memory address")]
31    InvalidAddress,
32
33    /// Minidump offsets out of usize range.
34    #[error("minidump offsets out of usize range")]
35    OutOfRange,
36
37    /// A UTF-8 or prefix string in the minidump could not be decoded.
38    #[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/// Items of the minidump which we are interested in.
61///
62/// For our own convenience we like to be able to identify which areas of the minidump we
63/// have.  This locates the data using [Range] slices since we can not take references to
64/// the original data where we construct these.
65#[derive(Debug, Clone, Eq, PartialEq, Hash)]
66enum MinidumpItem {
67    /// Stack memory region.
68    StackMemory(Range<usize>),
69    /// Memory region not associated with a stack stack/thread.
70    NonStackMemory(Range<usize>),
71    /// The Linux environ block.
72    ///
73    /// This is a NULL-byte separated list of `KEY=value` pairs.
74    LinuxEnviron(Range<usize>),
75    /// The Linux cmdline block.
76    ///
77    /// This is a NULL-byte separated list of arguments.
78    LinuxCmdLine(Range<usize>),
79    /// This is a UTF-16LE encoded pathname of a code module.
80    CodeModuleName(Range<usize>),
81    /// This is a UTF-16LE encoded pathname of a debug file.
82    DebugModuleName(Range<usize>),
83}
84
85/// Internal struct to keep a minidump and it's raw data together.
86struct MinidumpData<'a> {
87    data: &'a [u8],
88    minidump: Minidump<'a, &'a [u8]>,
89}
90
91impl<'a> MinidumpData<'a> {
92    /// Parses raw minidump data into the readable `Minidump` struct.
93    ///
94    /// This does only read the stream index, individual streams might still be corrupt even
95    /// when parsing this succeeds.
96    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    /// Returns the offset of a given slice into the minidump data.
102    ///
103    /// In minidump parlance this is also known as the RVA or Relative Virtual Address.
104    /// E.g. if all the raw minidump data is `data` and you have `&data[start..end]` this
105    /// returns you `start`.
106    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    /// Returns the `Range` in the raw minidump data of a slice in the minidump data.
118    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    /// Returns the `Range` in the raw minidump data of a `MINIDUMP_LOCATION_DESCRIPTOR`.
125    ///
126    /// This allows you to create a slice of the data specified in the location descriptor.
127    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    /// Returns the range of a raw stream, if the stream is preset.
137    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    /// Extracts all items we care about.
153    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        // NOTE: Scrubbing fails if the minidump is large and has a MinidumpMemory64List instead.
164        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            // Try to get the raw debug name range.  Minidump API only give us an owned version.
201            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; // cv_sig + sig GUID + age
212                    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; // cv_sig + cv_offset + sig + age
218                    items.push(MinidumpItem::DebugModuleName(
219                        (cv_start + offset)..(cv_start + cv_len),
220                    ));
221                }
222                _ => {}
223            }
224        }
225
226        Ok(items)
227    }
228}
229
230/// Read a u32 from the start of a byte-slice.
231///
232/// This function is exceedingly close in functionality to `bytes.pread_with(0, endian)` from scroll
233/// directly, only differing in the error type.
234fn 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    /// Applies PII rules to the given minidump.
245    ///
246    /// This function selectively opens minidump streams in order to avoid destroying the stack
247    /// memory required for minidump processing. It visits:
248    ///
249    ///  1. All stack memory regions with `ValueType::StackMemory`
250    ///  2. All other memory regions with `ValueType::HeapMemory`
251    ///  3. Linux auxiliary streams with `ValueType::Binary`
252    ///
253    /// Returns `true`, if the minidump was modified.
254    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                    // IMPORTANT: The stack is PII::Maybe to avoid accidentally scrubbing it
267                    // with highly generic selectors.
268                    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                    // Mirrors decisions made on NativeImagePath type
306                    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)?; // TODO: Consider making this lossy?
312                    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                    // Mirrors decisions made on NativeImagePath type
320                    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            // We could let scrubbed_dump just consume the Vec<[u8]>, but that would give
361            // both our dumps different types which is awkward to work with.  So we store
362            // the Vec separately to keep the slice alive and pretend we give Minidump a
363            // &'static [u8].
364            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(); // remove main module
403            iter.cloned().collect()
404        }
405
406        /// Returns the raw stack or heap memory regions.
407        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            // These bytes are kept alive by our struct itself, so returning them with the
421            // lifetime of our struct is fine.  The lifetimes on the Minidump::MemoryRegions
422            // iterator are currenty wrong and assumes we keep a reference to the
423            // MinidumpMemoryList, hence we need to transmute this.  See
424            // https://github.com/luser/rust-minidump/pull/111
425            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        /// Returns the raw stack memory regions.
437        fn stacks(&self, which: Which) -> Vec<&[u8]> {
438            self.memory_regions(which, MemRegion::Stack)
439        }
440
441        /// Returns the raw heap memory regions.
442        fn heaps(&self, which: Which) -> Vec<&[u8]> {
443            self.memory_regions(which, MemRegion::Heap)
444        }
445
446        /// Returns the Linux environ region.
447        ///
448        /// Panics if there is no such region.
449        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        // Since scrubbing the module list is safe, it should be scrubbed by valuetype.
622        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        // Some users already use this bare selector, that's all we care about for backwards
641        // compatibility.
642        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        // This should work, but is known to fail currently because the selector logic never
680        // considers a selector containing $binary as specific.
681        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        // Not fully qualified valuetype should not touch the stack
700        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        // Wildcard should not touch the stack.  However currently wildcards are considered
724        // specific selectors so they do.  This is a known issue.
725        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        // Wildcard should not touch the stack
748        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        // The linux environ should be scrubbed for any $binary
796        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}