Bug 1743983 pt2 - Rewrite the minidump-analyzer in Rust r=gsvelto

Differential Revision: https://phabricator.services.mozilla.com/D208391
This commit is contained in:
Alex Franchuk 2024-09-24 13:36:22 +00:00
parent a92f7373b2
commit 1311d61cb2
13 changed files with 2093 additions and 37 deletions

3
.gitignore vendored
View File

@ -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/

View File

@ -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/

View File

@ -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"

View File

@ -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 <alex.franchuk@gmail.com>"]
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]

View File

@ -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`.

View File

@ -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/. */

View File

@ -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<std::ffi::OsStr> 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<MinidumpBinary, minidumper::Error>,
) -> LoopAction {
result.expect("failed to write minidump");
LoopAction::Exit
}
fn on_message(&self, _kind: u32, _buffer: Vec<u8>) {}
}
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());
}
}

View File

@ -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);

View File

@ -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"]

View File

@ -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<Ctx<'a>>,
}
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<MinidumpMiscInfo>,
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<Fut::Output>
where
R: futures_util::task::Spawn,
I: IntoIterator<Item = Fut>,
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<T>(minidump: &'a Minidump<T>) -> anyhow::Result<Self>
where
T: std::ops::Deref<Target = [u8]>,
{
let system_info = minidump.get_stream::<MinidumpSystemInfo>()?;
let misc_info = minidump.get_stream::<MinidumpMiscInfo>().ok();
let module_list = minidump
.get_stream::<MinidumpModuleList>()
.unwrap_or_default();
let unloaded_module_list = minidump
.get_stream::<MinidumpUnloadedModuleList>()
.unwrap_or_default();
let memory_list = minidump
.get_stream::<MinidumpMemoryList>()
.unwrap_or_default();
let exception = minidump.get_stream::<MinidumpException>()?;
// 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<Item = &'b MinidumpThread<'b>>,
) -> anyhow::Result<Vec<CallStack>> {
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<Item = &MinidumpModule> {
self.context.module_list.by_addr()
}
/// Get all unloaded modules, ordered by address.
pub fn unloaded_modules(&self) -> impl Iterator<Item = &MinidumpUnloadedModule> {
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 { <binary_org_name>: [<code_file filename>...], ... }
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<dyn SymbolProvider + Send + Sync>);
#[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<PathBuf, minidump_unwind::FileError> {
self.0.get_file_path(module, file_kind).await
}
fn stats(&self) -> std::collections::HashMap<String, minidump_unwind::SymbolStats> {
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::<MinidumpThreadList>()?;
// 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::<Vec<_>>();
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::<Vec<_>>(),
"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::<Vec<_>>(),
"threads": call_stacks.iter().map(|call_stack| call_stack_to_json(call_stack, &used_modules)).collect::<Vec<_>>()
});
// 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<usize> {
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::<Vec<_>>()
})
}
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);
}
}
_ => (),
}
}

View File

@ -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<String> {
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::<WINTRUST_FILE_INFO>()
.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::<WINTRUST_CATALOG_INFO>()
.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<F>(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::<WINTRUST_DATA>().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::<WINTRUST_DATA_0>() },
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<Self::Value>;
}
impl HResult for BOOL {
type Value = ();
fn into_option(self) -> Option<Self::Value> {
(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<CertContext> {
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<CertInfo> {
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<u8>,
}
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<OsString> {
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) };
}
}
}

View File

@ -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<u16>);
/// An error indicating that an interior nul byte was found.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct NulError(usize, Vec<u16>);
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<OsStr>) -> Result<Self, NulError> {
let mut v: Vec<u16> = 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<Self::Item> {
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<CString> {
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::<Option<Vec<_>>>()
.map(CString::new)
.and_then(|res| {
if res.is_err() {
log::error!("FfiWideCharIterator provided nul character");
}
res.ok()
})
}

View File

@ -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<Self> {
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::<CERT_STRONG_SIGN_PARA>() 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<Vec<u8>> {
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<CATInfo> {
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<CATALOG_INFO> {
let mut ret = CATALOG_INFO {
cbStruct: std::mem::size_of::<CATALOG_INFO>().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) };
}
}