From 1311d61cb217a535ac44b45aa2be7cfd49ab8b52 Mon Sep 17 00:00:00 2001 From: Alex Franchuk Date: Tue, 24 Sep 2024 13:36:22 +0000 Subject: [PATCH] Bug 1743983 pt2 - Rewrite the minidump-analyzer in Rust r=gsvelto Differential Revision: https://phabricator.services.mozilla.com/D208391 --- .gitignore | 3 + .hgignore | 3 + .../analyzer-test/Cargo.lock | 662 ++++++++++++++++++ .../analyzer-test/Cargo.toml | 16 + .../minidump-analyzer/analyzer-test/README.md | 6 + .../analyzer-test/src/lib.rs | 3 + .../analyzer-test/tests/analyze.rs | 169 +++++ .../analyzer-test/tests/client.rs | 47 ++ .../crashreporter/minidump-analyzer/moz.build | 38 +- .../minidump-analyzer/src/main.rs | 494 +++++++++++++ .../minidump-analyzer/src/windows/mod.rs | 433 ++++++++++++ .../minidump-analyzer/src/windows/strings.rs | 83 +++ .../minidump-analyzer/src/windows/wintrust.rs | 173 +++++ 13 files changed, 2093 insertions(+), 37 deletions(-) create mode 100644 toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.lock create mode 100644 toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.toml create mode 100644 toolkit/crashreporter/minidump-analyzer/analyzer-test/README.md create mode 100644 toolkit/crashreporter/minidump-analyzer/analyzer-test/src/lib.rs create mode 100644 toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/analyze.rs create mode 100644 toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/client.rs create mode 100644 toolkit/crashreporter/minidump-analyzer/src/windows/mod.rs create mode 100644 toolkit/crashreporter/minidump-analyzer/src/windows/strings.rs create mode 100644 toolkit/crashreporter/minidump-analyzer/src/windows/wintrust.rs diff --git a/.gitignore b/.gitignore index 05c1009eb5e8..b32e3b1075ae 100644 --- a/.gitignore +++ b/.gitignore @@ -351,3 +351,6 @@ mobile/android/annotations/bin/ # Ignore generated log files under media/libvpx media/libvpx/config/**/config.log + +# Ignore generated files resulting from building the minidump analyzer tests. +toolkit/crashreporter/minidump-analyzer/analyzer-test/target/ diff --git a/.hgignore b/.hgignore index f6ebd9f4d8ec..994382f9b296 100644 --- a/.hgignore +++ b/.hgignore @@ -351,3 +351,6 @@ toolkit/themes/shared/design-system/node_modules/ # Ignore generated log files under media/libvpx ^media/libvpx/config/.*/config.log + +# Ignore generated files resulting from building the minidump analyzer tests. +^toolkit/crashreporter/minidump-analyzer/analyzer-test/target/ diff --git a/toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.lock b/toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.lock new file mode 100644 index 000000000000..731a8f15e729 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.lock @@ -0,0 +1,662 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crash-context" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85cef661eeca0c6675116310936972c520ebb0a33ddef16fd7efc957f4c1288" +dependencies = [ + "cfg-if", + "libc", + "mach2", +] + +[[package]] +name = "crash-handler" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed56890cb467e9ff5e6fe95254571beebb301990b5c7df5e97a7b203e4f0c4f0" +dependencies = [ + "cfg-if", + "crash-context", + "libc", + "mach2", + "parking_lot", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minidump-analyzer-test" +version = "0.1.0" +dependencies = [ + "crash-handler", + "json", + "minidumper", + "sadness-generator", + "tempfile", +] + +[[package]] +name = "minidump-common" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb6eaf88cc770fa58e6ae721cf2e40c2ca6a4c942ae8c7aa324d680bd3c6717" +dependencies = [ + "bitflags 2.5.0", + "debugid", + "num-derive", + "num-traits", + "range-map", + "scroll", + "smart-default", +] + +[[package]] +name = "minidump-writer" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" +dependencies = [ + "bitflags 2.5.0", + "byteorder", + "cfg-if", + "crash-context", + "goblin", + "libc", + "log", + "mach2", + "memmap2", + "memoffset", + "minidump-common", + "nix", + "procfs-core", + "scroll", + "tempfile", + "thiserror", +] + +[[package]] +name = "minidumper" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b99697a8483221ca2d163aedcbee44f3851c4f5916ab0e96ae6397fb6b6deb2" +dependencies = [ + "cfg-if", + "crash-context", + "libc", + "log", + "minidump-writer", + "parking_lot", + "polling", + "scroll", + "thiserror", + "uds", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.0", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs-core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" +dependencies = [ + "bitflags 2.5.0", + "hex", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "range-map" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "sadness-generator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdaf5eed5d14ebb5d32d864b419e760c9ddf123bcde880a30a90063494592d7" +dependencies = [ + "libc", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" + +[[package]] +name = "uds" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" +dependencies = [ + "libc", +] + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "uuid" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.toml b/toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.toml new file mode 100644 index 000000000000..e0bba7f6ddcb --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/analyzer-test/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "minidump-analyzer-test" +description = "Tests minidump-analyzer using crates which we won't vendor." +version = "0.1.0" +authors = ["Alex Franchuk "] +license = "MPL-2.0" +edition = "2018" + +[dev-dependencies] +crash-handler = "0.6" +minidumper = "0.8" +json = "0.12.4" +sadness-generator = "0.5.0" +tempfile = "3.3.0" + +[workspace] diff --git a/toolkit/crashreporter/minidump-analyzer/analyzer-test/README.md b/toolkit/crashreporter/minidump-analyzer/analyzer-test/README.md new file mode 100644 index 000000000000..951ce177eb5a --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/analyzer-test/README.md @@ -0,0 +1,6 @@ +To run the tests, execute: + +`MINIDUMP_ANALYZER=/path/to/minidump-analyzer cargo test` + +You likely want to set `MINIDUMP_ANALYZER` to `OBJDIR/dist/bin/minidump-analyzer` after running +`mach build`. diff --git a/toolkit/crashreporter/minidump-analyzer/analyzer-test/src/lib.rs b/toolkit/crashreporter/minidump-analyzer/analyzer-test/src/lib.rs new file mode 100644 index 000000000000..daa3e8897c2f --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/analyzer-test/src/lib.rs @@ -0,0 +1,3 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ diff --git a/toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/analyze.rs b/toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/analyze.rs new file mode 100644 index 000000000000..ce3498e34b9d --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/analyze.rs @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::fs::File; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command}; +use tempfile::tempdir; + +/// Child failure types. +/// +/// These must correspond to tests defined in `client.rs` (per the `Display` implementation). +#[derive(Debug, Clone, Copy)] +enum FailureType { + RaiseAbort, +} + +impl FailureType { + pub fn test_name(&self) -> &str { + match self { + FailureType::RaiseAbort => "raise_abort", + } + } +} + +impl AsRef for FailureType { + fn as_ref(&self) -> &std::ffi::OsStr { + self.test_name().as_ref() + } +} + +impl std::fmt::Display for FailureType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(self.test_name()) + } +} + +fn start_child(failure: FailureType) -> Child { + Command::new("cargo") + .args([ + "test", + "--package", + env!("CARGO_PKG_NAME"), + "--test", + "client", + "--", + "--include-ignored", + ]) + .arg(failure) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .expect("failed to execute child") +} + +// XXX: minidumper is somewhat slow to establish the connection, making the test slow. +fn write_minidump(minidump_file: &Path, failure: FailureType) { + use minidumper::{LoopAction, MinidumpBinary, Server, ServerHandler}; + use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + + struct Handler { + minidump_file: PathBuf, + } + + impl ServerHandler for Handler { + fn create_minidump_file(&self) -> std::io::Result<(File, PathBuf)> { + let f = File::create(&self.minidump_file)?; + let p = self.minidump_file.clone(); + Ok((f, p)) + } + + fn on_minidump_created( + &self, + result: Result, + ) -> LoopAction { + result.expect("failed to write minidump"); + LoopAction::Exit + } + + fn on_message(&self, _kind: u32, _buffer: Vec) {} + } + + let mut server = + Server::with_name(&failure.to_string()).expect("failed to create minidumper server"); + let mut child = start_child(failure); + + /// Maximum time we want to wait for the child to execute and crash. + const CHILD_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); + + // Run the server. + let shutdown = AtomicBool::default(); + std::thread::scope(|s| { + let handle = s.spawn(|| { + std::thread::park_timeout(CHILD_TIMEOUT); + shutdown.store(true, Relaxed); + }); + server + .run( + Box::new(Handler { + minidump_file: minidump_file.into(), + }), + &shutdown, + None, + ) + .expect("minidumper server failure"); + handle.thread().unpark(); + }); + drop(child.kill()); + if !minidump_file.exists() { + // Likely a timeout occurred + panic!("expected child process to crash within {:?}", CHILD_TIMEOUT); + } +} + +#[test] +fn analyze_basic_minidump() { + let dir = tempdir().expect("failed to create temporary directory"); + let minidump_file = dir.path().join("mini.dump"); + let extra_file = dir.path().join("mini.extra"); + + let Some(analyzer) = std::env::var_os("MINIDUMP_ANALYZER") else { + panic!("Specify the path to the minidump analyzer binary as the MINIDUMP_ANALYZER environment variable."); + }; + + // Create minidump from test. + write_minidump(&minidump_file, FailureType::RaiseAbort); + + // Create empty extra file + { + let mut extra = File::create(&extra_file).expect("failed to create extra json file"); + write!(&mut extra, "{{}}").expect("failed to write to extra json file"); + } + + // Run minidump-analyzer + { + let output = Command::new(analyzer) + .env("RUST_BACKTRACE", "1") + .arg(&minidump_file) + .output() + .expect("failed to run minidump-analyzer"); + assert!( + output.status.success(), + "stderr:\n{}", + std::str::from_utf8(&output.stderr).unwrap() + ); + } + + // Check the output JSON + // The stack trace will actually be in cargo. It forks and execs the test program; there is no + // clean way to make it just exec one or to directly address the binary (without creating a new + // crate). + { + let mut extra_content = String::new(); + File::open(extra_file) + .expect("failed to open extra json file") + .read_to_string(&mut extra_content) + .expect("failed to read extra json file"); + + let extra = json::parse(&extra_content).expect("failed to parse extra json"); + let stack_traces = &extra["StackTraces"]; + assert!(stack_traces.is_object()); + let threads = &stack_traces["threads"]; + assert!(threads.is_array() && threads.len() == 1); + assert!(threads[0].is_object()); + let frames = &threads[0]["frames"]; + assert!(frames.is_array() && !frames.is_empty()); + } +} diff --git a/toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/client.rs b/toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/client.rs new file mode 100644 index 000000000000..52543ade35e9 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/analyzer-test/tests/client.rs @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Tests in this file are not intended to be run by default, they are just entrypoints for +//! creating minidumps. + +use crash_handler::{CrashContext, CrashEvent, CrashEventResult, CrashHandler}; + +struct TestCrashClient { + client: minidumper::Client, +} + +impl TestCrashClient { + pub fn new(name: &str) -> Self { + TestCrashClient { + client: minidumper::Client::with_name(name) + .expect("failed to create minidumper client"), + } + } +} + +unsafe impl CrashEvent for TestCrashClient { + fn on_crash(&self, context: &CrashContext) -> CrashEventResult { + CrashEventResult::Handled(self.client.request_dump(context).is_ok()) + } +} + +pub fn handle_crashes(name: &str) -> CrashHandler { + CrashHandler::attach(Box::new(TestCrashClient::new(name))) + .expect("failed to install crash handler") +} + +macro_rules! sadness_test { + ( $name:ident ) => { + #[test] + #[ignore] + fn $name() { + let _handler = handle_crashes(stringify!($name)); + unsafe { + sadness_generator::$name(); + } + } + }; +} + +sadness_test!(raise_abort); diff --git a/toolkit/crashreporter/minidump-analyzer/moz.build b/toolkit/crashreporter/minidump-analyzer/moz.build index 912e830ab43f..3848b5b15c9e 100644 --- a/toolkit/crashreporter/minidump-analyzer/moz.build +++ b/toolkit/crashreporter/minidump-analyzer/moz.build @@ -4,40 +4,4 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -GeckoProgram("minidump-analyzer", linkage=None) - -if CONFIG["OS_TARGET"] == "WINNT": - DEFINES["UNICODE"] = True - DEFINES["_UNICODE"] = True - - if CONFIG["TARGET_CPU"] == "x86_64": - UNIFIED_SOURCES += [ - "MozStackFrameSymbolizer.cpp", - "Win64ModuleUnwindMetadata.cpp", - ] - - OS_LIBS += ["dbghelp", "imagehlp"] - -if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"): - # This allows us to use wmain as the entry point on mingw - LDFLAGS += [ - "-municode", - ] - -UNIFIED_SOURCES += [ - "minidump-analyzer.cpp", -] - -USE_LIBS += [ - "breakpad_processor", - "jsoncpp", -] - -LOCAL_INCLUDES += [ - "/toolkit/components/jsoncpp/include", -] - -if CONFIG["OS_TARGET"] != "WINNT": - DisableStlWrapping() - -include("/toolkit/crashreporter/crashreporter.mozbuild") +RUST_PROGRAMS = ["minidump-analyzer"] diff --git a/toolkit/crashreporter/minidump-analyzer/src/main.rs b/toolkit/crashreporter/minidump-analyzer/src/main.rs index e69de29bb2d1..8526dca3d3d9 100644 --- a/toolkit/crashreporter/minidump-analyzer/src/main.rs +++ b/toolkit/crashreporter/minidump-analyzer/src/main.rs @@ -0,0 +1,494 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use anyhow::Context; +use clap::Parser; +use futures_executor::{block_on, ThreadPool}; +use minidump::{ + system_info::Cpu, Minidump, MinidumpException, MinidumpMemoryList, MinidumpMiscInfo, + MinidumpModule, MinidumpModuleList, MinidumpSystemInfo, MinidumpThread, MinidumpThreadList, + MinidumpUnloadedModule, MinidumpUnloadedModuleList, Module, UnifiedMemoryList, +}; +use minidump_unwind::{ + symbols::debuginfo::DebugInfoSymbolProvider, symbols::SymbolProvider, walk_stack, CallStack, + CallStackInfo, SystemInfo, +}; +use std::fmt::Debug; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use std::sync::Arc; + +use serde_json::{json, Value as JsonValue}; + +#[cfg(windows)] +mod windows; + +/// Analyze a minidump file to augment a corresponding .extra file with stack trace information. +#[derive(Parser, Debug)] +#[clap(version)] +struct Args { + /// Generate all stacks, rather than just those of the crashing thread. + #[clap(long = "full")] + all_stacks: bool, + + /// The minidump file to analyze. + minidump: PathBuf, +} + +impl Args { + /// Get the extra file. + /// + /// This file is derived from the minidump file path. + pub fn extra_file(&self) -> PathBuf { + let mut ret = self.minidump.clone(); + ret.set_extension("extra"); + ret + } +} + +mod processor { + use super::*; + + pub struct Processor<'a> { + runtime: ThreadPool, + // We create a Context abstraction to easily spawn tokio tasks (which must be 'static). + context: Arc>, + } + + struct Ctx<'a> { + module_list: MinidumpModuleList, + unloaded_module_list: MinidumpUnloadedModuleList, + memory_list: UnifiedMemoryList<'a>, + system_info: MinidumpSystemInfo, + processor_system_info: SystemInfo, + exception: MinidumpException<'a>, + misc_info: Option, + symbol_provider: BoxedSymbolProvider, + } + + /// Concurrently execute the given futures, returning a Vec of the results. + async fn concurrently<'a, I, Fut, R>(runtime: &R, iter: I) -> Vec + where + R: futures_util::task::Spawn, + I: IntoIterator, + Fut: std::future::Future + Send + 'a, + // It's possible, though very obtuse, to support `'a` on the Output. We don't need it + // though, so we keep it `'static` to simplify things. + Fut::Output: Send + 'static, + { + use futures_util::{ + future::{join_all, BoxFuture, FutureExt}, + task::SpawnExt, + }; + + join_all(iter.into_iter().map(|f| { + let fut: BoxFuture<'a, Fut::Output> = f.boxed(); + // Safety: It is safe to transmute to a static lifetime because we await the output of + // the future while the `'a` lifetime is guaranteed to be valid (before exit from this + // function). + let fut: BoxFuture<'static, Fut::Output> = unsafe { std::mem::transmute(fut) }; + runtime.spawn_with_handle(fut).expect("spawn failed") + })) + .await + } + + impl<'a> Processor<'a> { + pub fn new(minidump: &'a Minidump) -> anyhow::Result + where + T: std::ops::Deref, + { + let system_info = minidump.get_stream::()?; + let misc_info = minidump.get_stream::().ok(); + let module_list = minidump + .get_stream::() + .unwrap_or_default(); + let unloaded_module_list = minidump + .get_stream::() + .unwrap_or_default(); + let memory_list = minidump + .get_stream::() + .unwrap_or_default(); + let exception = minidump.get_stream::()?; + + // TODO Something like SystemInfo::current() to get the active system's info? + let processor_system_info = SystemInfo { + os: system_info.os, + os_version: None, + os_build: None, + cpu: system_info.cpu, + cpu_info: None, + cpu_microcode_version: None, + cpu_count: 1, + }; + + let symbol_provider = BoxedSymbolProvider(match system_info.cpu { + // DebugInfoSymbolProvider only supports x86_64 and Arm64 right now + Cpu::X86_64 | Cpu::Arm64 => Box::new(block_on(DebugInfoSymbolProvider::new( + &system_info, + &module_list, + ))), + _ => Box::new(breakpad_symbols::Symbolizer::new( + breakpad_symbols::SimpleSymbolSupplier::new(vec![]), + )), + }); + + Ok(Processor { + runtime: ThreadPool::new()?, + context: Arc::new(Ctx { + module_list, + unloaded_module_list, + memory_list: UnifiedMemoryList::Memory(memory_list), + system_info, + processor_system_info, + exception, + misc_info, + symbol_provider, + }), + }) + } + + /// Get the minidump system info. + pub fn system_info(&self) -> &MinidumpSystemInfo { + &self.context.system_info + } + + /// Get the minidump exception. + pub fn exception(&self) -> &MinidumpException { + &self.context.exception + } + + /// Get call stacks for the given threads. + /// + /// Call stacks will be concurrently calculated. + pub fn thread_call_stacks<'b>( + &self, + threads: impl IntoIterator>, + ) -> anyhow::Result> { + Ok(block_on(concurrently( + &self.runtime, + threads + .into_iter() + .map(|thread| self.context.thread_call_stack(thread)), + )) + .into_iter() + .collect()) + } + + /// Get all modules, ordered by address. + #[cfg(windows)] + pub fn ordered_modules(&self) -> impl Iterator { + self.context.module_list.by_addr() + } + + /// Get all unloaded modules, ordered by address. + pub fn unloaded_modules(&self) -> impl Iterator { + self.context.unloaded_module_list.by_addr() + } + + /// Get the index of the main module. + /// + /// Returns `None` when no main module exists (only when there are modules). + pub fn main_module(&self) -> Option<&MinidumpModule> { + self.context.module_list.main_module() + } + + /// Get the json representation of module signature information. + #[cfg(windows)] + pub fn module_signature_info(&self) -> JsonValue { + // JSON with structure { : [...], ... } + let mut ret = json!({}); + for module in self + .ordered_modules() + .map(|m| m as &dyn Module) + .chain(self.unloaded_modules().map(|m| m as &dyn Module)) + { + let code_file = module.code_file(); + let code_file_path: &std::path::Path = code_file.as_ref().as_ref(); + if let Some(org_name) = windows::binary_org_name(code_file_path) { + let entry = &mut ret[org_name]; + + if entry.is_null() { + *entry = json!([]); + } + entry.as_array_mut().unwrap().push( + code_file_path + .file_name() + .map(|s| s.to_string_lossy()) + .into(), + ); + } else { + log::warn!("couldn't get binary org name for {code_file}"); + } + } + + ret + } + + /// Get the json representation of module signature information. + /// + /// This is currently unimplemented and returns null. + #[cfg(unix)] + pub fn module_signature_info(&self) -> JsonValue { + JsonValue::Null + } + } + + impl Ctx<'_> { + /// Compute the call stack for a single thread. + pub async fn thread_call_stack(&self, thread: &MinidumpThread<'_>) -> CallStack { + let context = if thread.raw.thread_id == self.exception.get_crashing_thread_id() { + self.exception + .context(&self.system_info, self.misc_info.as_ref()) + } else { + thread.context(&self.system_info, self.misc_info.as_ref()) + } + .map(|c| c.into_owned()); + let stack_memory = thread.stack_memory(&self.memory_list); + let Some(mut call_stack) = context.map(CallStack::with_context) else { + return CallStack::with_info(thread.raw.thread_id, CallStackInfo::MissingContext); + }; + + walk_stack( + 0, + (), + &mut call_stack, + stack_memory, + &self.module_list, + &self.processor_system_info, + &self.symbol_provider, + ) + .await; + + call_stack + } + } +} + +struct BoxedSymbolProvider(Box); + +#[async_trait::async_trait] +impl SymbolProvider for BoxedSymbolProvider { + async fn fill_symbol( + &self, + module: &(dyn Module + Sync), + frame: &mut (dyn minidump_unwind::FrameSymbolizer + Send), + ) -> Result<(), minidump_unwind::FillSymbolError> { + self.0.fill_symbol(module, frame).await + } + + async fn walk_frame( + &self, + module: &(dyn Module + Sync), + walker: &mut (dyn minidump_unwind::FrameWalker + Send), + ) -> Option<()> { + self.0.walk_frame(module, walker).await + } + + async fn get_file_path( + &self, + module: &(dyn Module + Sync), + file_kind: minidump_unwind::FileKind, + ) -> Result { + self.0.get_file_path(module, file_kind).await + } + + fn stats(&self) -> std::collections::HashMap { + self.0.stats() + } + + fn pending_stats(&self) -> minidump_unwind::PendingSymbolStats { + self.0.pending_stats() + } +} + +use processor::Processor; + +pub fn main() { + env_logger::init(); + + if let Err(e) = try_main() { + eprintln!("{e}"); + std::process::exit(1); + } +} + +fn try_main() -> anyhow::Result<()> { + let args = Args::parse(); + let extra_file = args.extra_file(); + + log::info!("minidump file path: {}", args.minidump.display()); + log::info!("extra file path: {}", extra_file.display()); + + let minidump = Minidump::read_path(&args.minidump).context("while reading minidump")?; + + let mut extra_json: JsonValue = { + let mut extra_file_content = String::new(); + File::open(&extra_file) + .context("while opening extra file")? + .read_to_string(&mut extra_file_content) + .context("while reading extra file")?; + + serde_json::from_str(&extra_file_content).context("while parsing extra file JSON")? + }; + + // Read relevant information from the minidump. + let proc = Processor::new(&minidump)?; + let thread_list = minidump.get_stream::()?; + + // Derive additional arguments used in stack walking. + let crashing_thread = thread_list + .get_thread(proc.exception().get_crashing_thread_id()) + .ok_or(anyhow::anyhow!( + "exception thread id missing in thread list" + ))?; + + let (crashing_thread_idx, call_stacks) = if args.all_stacks { + ( + thread_list + .threads + .iter() + .position(|t| t.raw.thread_id == crashing_thread.raw.thread_id) + .expect("get_thread() returned a thread that doesn't exist"), + proc.thread_call_stacks(&thread_list.threads)?, + ) + } else { + (0, proc.thread_call_stacks([crashing_thread])?) + }; + + let crash_type = proc + .exception() + .get_crash_reason(proc.system_info().os, proc.system_info().cpu) + .to_string(); + let crash_address = proc + .exception() + .get_crash_address(proc.system_info().os, proc.system_info().cpu); + + let used_modules = { + let mut v = call_stacks + .iter() + .flat_map(|call_stack| call_stack.frames.iter()) + .filter_map(|frame| frame.module.as_ref()) + // Always include the main module. + .chain(proc.main_module()) + .collect::>(); + v.sort_by_key(|m| m.base_address()); + v.dedup_by_key(|m| m.base_address()); + v + }; + + extra_json["StackTraces"] = json!({ + "status": call_stack_status(&call_stacks), + "crash_info": { + "type": crash_type, + "address": format!("{crash_address:#x}"), + "crashing_thread": crashing_thread_idx + // TODO: "assertion" when there's no crash indicator + }, + "main_module": proc.main_module().and_then(|m| module_index(&used_modules, m)), + "modules": used_modules.iter().map(|module| { + let code_file = module.code_file(); + let code_file_path: &std::path::Path = code_file.as_ref().as_ref(); + json!({ + "base_addr": format!("{:#x}", module.base_address()), + "end_addr": format!("{:#x}", module.base_address() + module.size()), + "filename": code_file_path.file_name().map(|s| s.to_string_lossy()), + "code_id": module.code_identifier().as_ref().map(|id| id.as_str()), + "debug_file": module.debug_file().as_deref(), + "debug_id": module.debug_identifier().map(|debug| debug.breakpad().to_string()), + "version": module.version().as_deref() + }) + }).collect::>(), + "unloaded_modules": proc.unloaded_modules().map(|module| { + let code_file = module.code_file(); + let code_file_path: &std::path::Path = code_file.as_ref().as_ref(); + json!({ + "base_addr": format!("{:#x}", module.base_address()), + "end_addr": format!("{:#x}", module.base_address() + module.size()), + "filename": code_file_path.file_name().map(|s| s.to_string_lossy()), + "code_id": module.code_identifier().as_ref().map(|id| id.as_str()), + }) + }).collect::>(), + "threads": call_stacks.iter().map(|call_stack| call_stack_to_json(call_stack, &used_modules)).collect::>() + }); + + // StackTraces should not have null values (upstream processing expects the values to be + // omitted). + remove_nulls(&mut extra_json["StackTraces"]); + + let module_signature_info = proc.module_signature_info(); + if !module_signature_info.is_null() { + // ModuleSignatureInfo is sent as a crash annotation so must be string. This differs from + // StackTraces which isn't actually sent (it's just read and removed by the crash + // reporter client). + extra_json["ModuleSignatureInfo"] = serde_json::to_string(&module_signature_info) + .unwrap() + .into(); + } + + std::fs::write(&extra_file, extra_json.to_string()) + .context("while writing modified extra file")?; + + Ok(()) +} + +/// Get the index of `needle` in `modules`. +fn module_index(modules: &[&MinidumpModule], needle: &MinidumpModule) -> Option { + modules + .iter() + .position(|o| o.base_address() == needle.base_address()) +} + +/// Convert a call stack to json (in a form appropriate for the extra json file). +fn call_stack_to_json(call_stack: &CallStack, modules: &[&MinidumpModule]) -> JsonValue { + json!({ + "frames": call_stack.frames.iter().map(|frame| { + json!({ + "ip": format!("{:#x}", frame.instruction), + "module_index": frame.module.as_ref().and_then(|m| module_index(modules, m)), + "trust": frame.trust.as_str(), + }) + }).collect::>() + }) +} + +fn call_stack_status(stacks: &[CallStack]) -> JsonValue { + let mut error_string = String::new(); + + for (_i, s) in stacks.iter().enumerate() { + match s.info { + CallStackInfo::Ok | CallStackInfo::DumpThreadSkipped => (), + CallStackInfo::UnsupportedCpu => { + // If the CPU is unsupported, it ought to be the same error for every thread. + error_string = "unsupported cpu".into(); + break; + } + // We ignore these errors as they are permissible wrt the overall status. + CallStackInfo::MissingContext | CallStackInfo::MissingMemory => (), + } + } + if error_string.is_empty() { + "OK".into() + } else { + error_string.into() + } +} + +/// Remove all object entries which have null values. +fn remove_nulls(value: &mut JsonValue) { + match value { + JsonValue::Array(vals) => { + for v in vals { + remove_nulls(v); + } + } + JsonValue::Object(kvs) => { + kvs.retain(|_, v| !v.is_null()); + for v in kvs.values_mut() { + remove_nulls(v); + } + } + _ => (), + } +} diff --git a/toolkit/crashreporter/minidump-analyzer/src/windows/mod.rs b/toolkit/crashreporter/minidump-analyzer/src/windows/mod.rs new file mode 100644 index 000000000000..b88af7bd7788 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/src/windows/mod.rs @@ -0,0 +1,433 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::ffi::{c_void, OsString}; +use std::fs::OpenOptions; +use std::os::windows::{ffi::OsStringExt, fs::OpenOptionsExt, io::AsRawHandle}; +use std::path::Path; +use windows_sys::Win32::{ + Foundation::{BOOL, HWND, INVALID_HANDLE_VALUE}, + Security::Cryptography::*, + Security::WinTrust::*, + Storage::FileSystem::{ + FILE_FLAG_SEQUENTIAL_SCAN, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, + }, + UI::WindowsAndMessaging::CharUpperBuffW, +}; + +// Our windows-targets doesn't link wintrust correctly. +#[link(name = "wintrust", kind = "static")] +extern "C" {} + +type DWORD = u32; + +mod strings; +mod wintrust; + +use strings::WideString; + +pub fn binary_org_name(binary: &Path) -> Option { + log::trace!("binary_org_name({})", binary.display()); + let binary_wide = match WideString::new(binary) { + Err(e) => { + log::error!("failed to create wide string of binary path: {e}"); + return None; + } + Ok(s) => s, + }; + + // Verify trust for the binary and get the certificate context. + let mut cert_store = CertStore::default(); + let mut crypt_msg = CryptMsg::default(); + + let result = unsafe { + CryptQueryObject( + CERT_QUERY_OBJECT_FILE, + binary_wide.pcwstr() as *const _, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_FORMAT_FLAG_BINARY, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut *cert_store, + &mut *crypt_msg, + std::ptr::null_mut(), + ) + }; + let verified = if result != 0 { + // We got a result. + let mut file_info = WINTRUST_FILE_INFO { + cbStruct: std::mem::size_of::() + .try_into() + .unwrap(), + pcwszFilePath: binary_wide.pcwstr(), + hFile: 0, + pgKnownSubject: std::ptr::null_mut(), + }; + verify_trust(|data| { + data.dwUnionChoice = WTD_CHOICE_FILE; + data.Anonymous = WINTRUST_DATA_0 { + pFile: &mut file_info, + }; + }) + } else { + log::debug!("checking catalogs for binary org name"); + // We didn't find anything in the binary, so try catalogs. + let cat_admin = wintrust::CATAdmin::acquire()?; + + log::trace!("acquired CATAdmin"); + + // Hash the binary + let file = match OpenOptions::new() + .read(true) + .share_mode(FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE) + .custom_flags(FILE_FLAG_SEQUENTIAL_SCAN) + .open(binary) + { + Err(e) => { + log::error!("failed to open file {}: {e}", binary.display()); + return None; + } + Ok(v) => v, + }; + + let mut file_hash = cat_admin.calculate_file_hash(&file)?; + + log::trace!("{} hashed to {file_hash:?}", binary.display()); + + // Now query the catalog system to see if any catalogs reference a binary with our hash. + let catalog_info = cat_admin + .catalog_from_hash(&mut file_hash) + .and_then(|h| h.get_info())?; + + log::trace!("found catalog info"); + + unsafe { + CryptQueryObject( + CERT_QUERY_OBJECT_FILE, + catalog_info.wszCatalogFile.as_ptr() as *const _, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_FORMAT_FLAG_BINARY, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut *cert_store, + &mut *crypt_msg, + std::ptr::null_mut(), + ) + } + .into_option()?; + + // WINTRUST_CATALOG_INFO::pcwszMemberTag is commonly set to the string + // representation of the file hash, so we build that here. + let mut strlength = DWORD::default(); + let file_hash_to_string = |buf: *mut u16, len: *mut DWORD| { + unsafe { + CryptBinaryToStringW( + file_hash.as_ptr(), + file_hash.len() as DWORD, + CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF, + buf, + len, + ) + } + .into_option() + }; + file_hash_to_string(std::ptr::null_mut(), &mut strlength as *mut _)?; + let mut file_hash_string = vec![0u16; strlength as usize]; + file_hash_to_string(file_hash_string.as_mut_ptr(), &mut strlength as *mut _)?; + + // Ensure the string is uppercase for WinVerifyTrust + unsafe { + CharUpperBuffW( + file_hash_string.as_mut_ptr(), + file_hash_string.len().try_into().unwrap(), + ) + }; + + let mut info = WINTRUST_CATALOG_INFO { + cbStruct: std::mem::size_of::() + .try_into() + .unwrap(), + dwCatalogVersion: 0, + pcwszCatalogFilePath: catalog_info.wszCatalogFile.as_ptr(), + pcwszMemberTag: file_hash_string.as_ptr(), + pcwszMemberFilePath: binary_wide.pcwstr(), + hMemberFile: file.as_raw_handle() as _, + // These two weren't used in Authenticode.cpp, though we have the information. + pbCalculatedFileHash: std::ptr::null_mut(), + cbCalculatedFileHash: 0, + pcCatalogContext: std::ptr::null_mut(), + hCatAdmin: *cat_admin, + }; + + verify_trust(|data| { + data.dwUnionChoice = WTD_CHOICE_CATALOG; + data.Anonymous = WINTRUST_DATA_0 { + pCatalog: &mut info, + }; + }) + }; + + if !verified { + log::warn!("could not verify trust for {}", binary.display()); + return None; + } + + let cert_context = crypt_msg + .get_certificate_info() + .and_then(|info| cert_store.find_certificate(&info))?; + + cert_context + .get_name_string() + .and_then(|oss| oss.into_string().ok()) +} + +/// Call WinVerifyTrust, first calling `setup` on the `WINTRUST_DATA`. This is expected to set the +/// `dwUnionChoice` and `u` members. +fn verify_trust(setup: F) -> bool +where + F: FnOnce(&mut WINTRUST_DATA), +{ + let mut guid = WINTRUST_ACTION_GENERIC_VERIFY_V2; + let mut data = WINTRUST_DATA { + cbStruct: std::mem::size_of::().try_into().unwrap(), + pPolicyCallbackData: std::ptr::null_mut(), + pSIPClientData: std::ptr::null_mut(), + dwUIChoice: WTD_UI_NONE, + fdwRevocationChecks: WTD_REVOKE_NONE, + // setup should set these two fields + dwUnionChoice: Default::default(), + Anonymous: unsafe { std::mem::zeroed::() }, + dwStateAction: WTD_STATEACTION_VERIFY, + hWVTStateData: 0, + pwszURLReference: std::ptr::null_mut(), + dwProvFlags: WTD_CACHE_ONLY_URL_RETRIEVAL, + dwUIContext: 0, + pSignatureSettings: std::ptr::null_mut(), + }; + setup(&mut data); + + let result = unsafe { + WinVerifyTrust( + INVALID_HANDLE_VALUE as HWND, + &mut guid, + &mut data as *mut WINTRUST_DATA as *mut _, + ) + }; + + data.dwStateAction = WTD_STATEACTION_CLOSE; + unsafe { + WinVerifyTrust( + INVALID_HANDLE_VALUE as HWND, + &mut guid, + &mut data as *mut WINTRUST_DATA as *mut _, + ) + }; + + result == 0 +} + +/// Convenience trait for handling windows results. +trait HResult { + type Value; + + fn into_option(self) -> Option; +} + +impl HResult for BOOL { + type Value = (); + + fn into_option(self) -> Option { + (self != 0).then_some(()) + } +} + +/// A certificate store handle. +struct CertStore(HCERTSTORE); + +impl Default for CertStore { + fn default() -> Self { + CertStore(std::ptr::null_mut()) + } +} + +impl CertStore { + pub fn find_certificate(&self, cert_info: &CertInfo) -> Option { + let ctx = unsafe { + CertFindCertificateInStore( + self.0, + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, + CERT_FIND_SUBJECT_CERT, + cert_info.as_ptr() as *const _, + std::ptr::null_mut(), + ) + }; + if ctx.is_null() { + None + } else { + Some(CertContext(ctx)) + } + } +} + +impl std::ops::Deref for CertStore { + type Target = HCERTSTORE; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for CertStore { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for CertStore { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { CertCloseStore(self.0, 0) }; + } + } +} + +/// A crypt message handle. +type HCRYPTMSG = *mut c_void; +struct CryptMsg(HCRYPTMSG); + +impl Default for CryptMsg { + fn default() -> Self { + CryptMsg(std::ptr::null_mut()) + } +} + +impl CryptMsg { + pub fn get_certificate_info(&self) -> Option { + let mut cert_info_length: DWORD = 0; + unsafe { + CryptMsgGetParam( + self.0, + CMSG_SIGNER_CERT_INFO_PARAM, + 0, + std::ptr::null_mut(), + &mut cert_info_length, + ) + } + .into_option()?; + + let mut buffer = vec![0u8; cert_info_length as usize]; + + unsafe { + CryptMsgGetParam( + self.0, + CMSG_SIGNER_CERT_INFO_PARAM, + 0, + buffer.as_mut_ptr() as *mut _, + &mut cert_info_length, + ) + } + .into_option()?; + buffer.resize(cert_info_length as usize, 0); + + Some(CertInfo { buffer }) + } +} + +impl std::ops::Deref for CryptMsg { + type Target = HCRYPTMSG; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for CryptMsg { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for CryptMsg { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { CryptMsgClose(self.0) }; + } + } +} + +/// Certificate information. +struct CertInfo { + buffer: Vec, +} + +impl CertInfo { + pub fn as_ptr(&self) -> *const u8 { + self.buffer.as_ptr() + } +} + +/// A certificate context. +#[allow(non_camel_case_types)] +type PCCERT_CONTEXT = *const CERT_CONTEXT; +struct CertContext(PCCERT_CONTEXT); + +impl CertContext { + pub fn get_name_string(&self) -> Option { + let char_count = unsafe { + CertGetNameStringW( + self.0, + CERT_NAME_SIMPLE_DISPLAY_TYPE, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + 0, + ) + }; + if char_count <= 1 { + return None; + } + let mut name = vec![0u16; char_count as usize]; + let char_count = unsafe { + CertGetNameStringW( + self.0, + CERT_NAME_SIMPLE_DISPLAY_TYPE, + 0, + std::ptr::null_mut(), + name.as_mut_ptr(), + char_count, + ) + }; + assert!(char_count > 1); + + // Subtract one for the null termination byte. + Some(OsString::from_wide(&name[0..(char_count as usize - 1)])) + } +} + +impl std::ops::Deref for CertContext { + type Target = PCCERT_CONTEXT; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for CertContext { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for CertContext { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { CertFreeCertificateContext(self.0) }; + } + } +} diff --git a/toolkit/crashreporter/minidump-analyzer/src/windows/strings.rs b/toolkit/crashreporter/minidump-analyzer/src/windows/strings.rs new file mode 100644 index 000000000000..643f1b731c8c --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/src/windows/strings.rs @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub use std::ffi::CString; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; + +/// Windows wide strings. +/// +/// These are utf16 encoded with a terminating nul character (0). +#[derive(Debug)] +pub struct WideString(Vec); + +/// An error indicating that an interior nul byte was found. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct NulError(usize, Vec); + +impl std::fmt::Display for NulError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "nul byte found in provided data at position: {}", self.0) + } +} + +impl std::error::Error for NulError {} + +impl WideString { + pub fn new(os_str: impl AsRef) -> Result { + let mut v: Vec = os_str.as_ref().encode_wide().collect(); + if let Some(p) = v.iter().position(|c| *c == 0) { + Err(NulError(p, v)) + } else { + v.push(0); + Ok(WideString(v)) + } + } + + pub fn pcwstr(&self) -> windows_sys::core::PCWSTR { + self.0.as_ptr() + } +} + +/// Iterator over wide characters in a wide string. +/// +/// This is useful for wide string constants. +#[derive(Debug)] +pub struct FfiWideCharIterator(*const u16); + +impl FfiWideCharIterator { + pub fn new(ptr: *const u16) -> Self { + FfiWideCharIterator(ptr) + } +} + +impl Iterator for FfiWideCharIterator { + type Item = u16; + + fn next(&mut self) -> Option { + let c = unsafe { self.0.read() }; + if c == 0 { + None + } else { + self.0 = unsafe { self.0.add(1) }; + Some(c) + } + } +} + +/// Convert a utf16 ptr to an ascii CString. +pub fn utf16_ptr_to_ascii(ptr: *const u16) -> Option { + char::decode_utf16(FfiWideCharIterator::new(ptr)) + // Using try_into() accepts extended ascii as well; we don't care much about the + // distinction here, it'll still be a valid conversion. + .map(|res| res.ok().and_then(|c| c.try_into().ok())) + .collect::>>() + .map(CString::new) + .and_then(|res| { + if res.is_err() { + log::error!("FfiWideCharIterator provided nul character"); + } + res.ok() + }) +} diff --git a/toolkit/crashreporter/minidump-analyzer/src/windows/wintrust.rs b/toolkit/crashreporter/minidump-analyzer/src/windows/wintrust.rs new file mode 100644 index 000000000000..f4a995c8a365 --- /dev/null +++ b/toolkit/crashreporter/minidump-analyzer/src/windows/wintrust.rs @@ -0,0 +1,173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::strings::utf16_ptr_to_ascii; +use super::HResult; +use std::fs::File; +use std::os::windows::io::AsRawHandle; +use windows_sys::Win32::{ + Foundation::{GetLastError, BOOL, ERROR_INSUFFICIENT_BUFFER, HANDLE, MAX_PATH}, + Security::Cryptography::{ + szOID_CERT_STRONG_SIGN_OS_1, szOID_CERT_STRONG_SIGN_OS_CURRENT, + Catalog::{ + CryptCATAdminAcquireContext2, CryptCATAdminCalcHashFromFileHandle2, + CryptCATAdminEnumCatalogFromHash, CryptCATAdminReleaseCatalogContext, + CryptCATAdminReleaseContext, CryptCATCatalogInfoFromContext, CATALOG_INFO, + }, + BCRYPT_SHA256_ALGORITHM, CERT_STRONG_SIGN_OID_INFO_CHOICE, CERT_STRONG_SIGN_PARA, + CERT_STRONG_SIGN_PARA_0, + }, +}; + +pub type HCATADMIN = HANDLE; +pub type HCATINFO = HANDLE; + +/// A catalog admin handle. +pub struct CATAdmin(HCATADMIN); + +impl Default for CATAdmin { + fn default() -> Self { + CATAdmin(0) + } +} + +impl CATAdmin { + /// Acquire a handle. + pub fn acquire() -> Option { + let mut ret = Self::default(); + // Annoyingly, szOID_CERT_STRONG_SIGN_OS_CURRENT is a wide string, but all other such + // constants are C strings. + let oid_string = utf16_ptr_to_ascii(szOID_CERT_STRONG_SIGN_OS_CURRENT); + let policy = CERT_STRONG_SIGN_PARA { + cbSize: std::mem::size_of::() as u32, + dwInfoChoice: CERT_STRONG_SIGN_OID_INFO_CHOICE, + Anonymous: CERT_STRONG_SIGN_PARA_0 { + pszOID: oid_string + .as_ref() + .map(|c| c.as_ptr() as *mut u8) + .unwrap_or(szOID_CERT_STRONG_SIGN_OS_1 as *mut u8), + }, + }; + unsafe { + CryptCATAdminAcquireContext2( + &mut *ret, + std::ptr::null(), + BCRYPT_SHA256_ALGORITHM, + &policy as *const _, + 0, + ) + } + .into_option()?; + Some(ret) + } + + /// Calculate the hash of the given file. + pub fn calculate_file_hash(&self, file: &File) -> Option> { + let calc_hash = |size: *mut u32, dest: *mut u8| -> BOOL { + unsafe { + CryptCATAdminCalcHashFromFileHandle2( + self.0, + file.as_raw_handle() as _, + size, + dest, + 0, + ) + } + }; + + // First call to retrieve the hash size. + let mut size: u32 = 0; + calc_hash(&mut size, std::ptr::null_mut()) + .into_option() + // If ERROR_INSUFFICIENT_BUFFER is the last error, `size` has been set. + .or_else(|| (unsafe { GetLastError() } == ERROR_INSUFFICIENT_BUFFER).then_some(()))?; + + // Second call to get the hash. + let mut hash = vec![0; size as usize]; + calc_hash(&mut size as *mut _, hash.as_mut_ptr()).into_option()?; + + Some(hash) + } + + /// Find the first catalog that contains the given hash. + pub fn catalog_from_hash(&self, hash: &mut [u8]) -> Option { + let ptr = unsafe { + CryptCATAdminEnumCatalogFromHash( + self.0, + hash.as_mut_ptr(), + hash.len().try_into().unwrap(), + 0, + std::ptr::null_mut(), + ) + }; + if ptr == 0 { + None + } else { + Some(CATInfo { + cat_admin_context: self, + handle: ptr, + }) + } + } +} + +impl std::ops::Deref for CATAdmin { + type Target = HCATADMIN; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for CATAdmin { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for CATAdmin { + fn drop(&mut self) { + if self.0 != 0 { + unsafe { CryptCATAdminReleaseContext(self.0, 0) }; + } + } +} + +pub struct CATInfo<'a> { + cat_admin_context: &'a CATAdmin, + handle: HCATINFO, +} + +impl CATInfo<'_> { + pub fn get_info(&self) -> Option { + let mut ret = CATALOG_INFO { + cbStruct: std::mem::size_of::().try_into().unwrap(), + wszCatalogFile: [0u16; MAX_PATH as usize], + }; + unsafe { CryptCATCatalogInfoFromContext(self.handle, &mut ret as *mut _, 0) } + .into_option()?; + Some(ret) + } +} + +impl std::ops::Deref for CATInfo<'_> { + type Target = HCATINFO; + + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +impl std::ops::DerefMut for CATInfo<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.handle + } +} + +impl Drop for CATInfo<'_> { + fn drop(&mut self) { + // Unwrap because this function must exist if we got a handle. + unsafe { CryptCATAdminReleaseCatalogContext(**self.cat_admin_context, self.handle, 0) }; + } +}