diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/minidump-writer | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/minidump-writer')
72 files changed, 13227 insertions, 0 deletions
diff --git a/third_party/rust/minidump-writer/.cargo-checksum.json b/third_party/rust/minidump-writer/.cargo-checksum.json new file mode 100644 index 0000000000..461af91038 --- /dev/null +++ b/third_party/rust/minidump-writer/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"bd47a727b96e7175b86cb9384fa9a4174ef6172869ad535374c38825fd2c3b38","Cargo.lock":"9bf0a7576a9f71f2a06a1ebf886af125fcd11fb771cb6491804ad2e1c07bf408","Cargo.toml":"34acafdc4e13be804e847b771c2a9da6928d161bddab467f4d9a4085c79e9210","LICENSE":"1ecdd8e8977af83c07c5f97bec87b47d27059b7ea323ca3160fbfa2314f5d99c","README.md":"ba247891c565089e59211c414755ed4693b81371233fa3b398a501b545f6b962","build.rs":"689cd32a441f5011f694a9f86bc03bc27c2a09bcb4130f47e04fe00bb069b1b5","deny.toml":"d0eab3030d061ba5bf0b7b11ce3001cc2e5b49ea0add047602edd334fdceb5bc","examples/synthetic.rs":"cd13bd0bba64a1d8c8c326bb1ce1818bead6904e5708418fd09edeaef1437c24","release.toml":"f554067378aec602383b96e5fa63427136533a7dd00137fd0664b279fb8fcc56","src/bin/test.rs":"3b6b59e7205b2f51184d169b7902b52cf9c8a863bb27f91a45f4466e1f0d1359","src/dir_section.rs":"b7c83b16acefb4327b406612e1a667dffb91f86bec40f9330171dba8d10346ff","src/lib.rs":"6f393273ea6b354f412e70c9a4925fe2e646bc477591962c96a61aed9fde4e3b","src/linux.rs":"65d1249e80a3c3b6ab8c4361891b5ef35fe68f91253f6378ae03a0ad59ecd807","src/linux/android.rs":"9f2fd633de0d545b400c479a70f000cb9d04bb6e322f62098f4a7ebe7299f051","src/linux/app_memory.rs":"5f093e4ed0aecc6086366a9c09658761fdd3b0e6e9ff2111690719e56612df64","src/linux/auxv_reader.rs":"e9c75b0093c845da8dd321c6582e65678772c57658358213098aa6267f3679e1","src/linux/crash_context.rs":"069d949c7a9fc23a1e8710f7c018cacf71af2326a2dd6ca0283d492c5253107a","src/linux/crash_context/aarch64.rs":"158d02200b25a7ca029ff5132ba3b7f76a05c9976ff529a5658523aff370e4d2","src/linux/crash_context/arm.rs":"a4f41aae015937d3dccdf759df84bc657cf457d8baaf07711617d17f4b40f6f1","src/linux/crash_context/x86.rs":"d464680ee9df8a7f11dae2bb199cc4efb66dfc790a87c9b773c3a9b44b9339d8","src/linux/crash_context/x86_64.rs":"dceabbcf4d3c4ff5ad7658daa7dec3349cad9b61091385bcd868de6b11eb4957","src/linux/dso_debug.rs":"39beabf0598fe6bfaf0edd068bbe20442b45a7c0567588e978e8d9f5c331888b","src/linux/dumper_cpu_info.rs":"76558ffc85386e416bbfc49adc550f61fb206dfbfb0b6a25f620a5fc91f32bc1","src/linux/dumper_cpu_info/arm.rs":"6fc140181f3a32504c3b66f0235136e730424be830602c87cfbeb1dfb4c1cec3","src/linux/dumper_cpu_info/x86_mips.rs":"1b7f554190c2c0f0575e36995aae5211a0fc9f29f3949ac7521a9d148566f6e9","src/linux/errors.rs":"f0bc2a400a16d322f8d0dd5c76afa61fa32cf8a86f1b4e937298f767de031cb8","src/linux/maps_reader.rs":"0fde4c432f718e38d3ff35abcf00d3d3b478836fec2113ee11a8a1419921fe7b","src/linux/minidump_writer.rs":"b85eadb008c2f92d3d86c2c44c10bd139f793a0988d79aa51969848773bfb0fc","src/linux/ptrace_dumper.rs":"a2d6a06bb088caad1254746d842cb2c8d4fed918cfc318ec562aa160af3526d1","src/linux/sections.rs":"98aabd7e4b0542201783af90cafdc66bb3845585d0ed24af7476c1ea3ab40fb9","src/linux/sections/app_memory.rs":"52fdfe9622a395763fef94c5fea924935fcd86103c3eb1b0fb8d7ea61fd3e783","src/linux/sections/exception_stream.rs":"e1b73b5168506f8804479206ef0941aca5f792500cc56c5c17db55cd6b273a99","src/linux/sections/handle_data_stream.rs":"3aae030c009543142555d493f9273e3445454b18085dfdb3b44bc83d43b510dd","src/linux/sections/mappings.rs":"3b3bdf51e8e11f5b579d51b1a06244f1785cdfcdda3ee835472e5ab58756e71c","src/linux/sections/memory_info_list_stream.rs":"bd927824e859e2fcbc85ef9d5e3d8b7be13c4918a636f70f0712070d6c65b002","src/linux/sections/memory_list_stream.rs":"f2d5b33fb4167b502dc8e74371d50b4ad66e0b48f541fe16bcbf5fb62491bf2d","src/linux/sections/systeminfo_stream.rs":"02373c97d4caeb66f4601c066d430ad2929b5584eaa0af3be1dc51ef818e5cb8","src/linux/sections/thread_list_stream.rs":"9ea966c00a878ef1a6e6781005ddf9909c18177f2c455cb250c4ac191040fc69","src/linux/sections/thread_names_stream.rs":"2a123335626e941cf0a3349f9327c3e7606af593a8370ef5f71325012107b91f","src/linux/thread_info.rs":"b57a989a97418821a40c019d8edcb3143b43d3f76d7172d55a05001c9bfc210f","src/linux/thread_info/aarch64.rs":"fcbdd6b9185f00b4f42eef27076a8c9edab8afcaf22e1c005e32702be16e2264","src/linux/thread_info/arm.rs":"cf927d9f0ea48e07a874e8d6049e447c0f12a0715ed166448c3b07a410dd958a","src/linux/thread_info/mips.rs":"332d7888bb08a36d3b77023a4a0385193a07b1c6c5bf0e91aacb5d948ba8b903","src/linux/thread_info/x86.rs":"a759b2298f0831298c56e49717b4f188c213ebeb30b38be23af5bcbe6fd96369","src/mac.rs":"4671ad90c433db559ec633c880c3fe083f38a2e185ef4fc99577318526076519","src/mac/errors.rs":"696473d1187a0f003409e3ac0fc83a02cafbad8451a62bda868b08acc36e7443","src/mac/mach.rs":"1ae13287b6d76075473b0179b2a106bd2422c5234561770c0fedb5597c4128e9","src/mac/minidump_writer.rs":"d6f07685779c45f6b773d4163965f5dc5e0a73421937d9ec6b8e4d3320d23e85","src/mac/streams.rs":"82e1b9fefa26d98f1c0efdd98de0aed9a9ee9edf3e4b4f636212c73f8d3e464e","src/mac/streams/breakpad_info.rs":"9aafbc3f376050e39a2295e77a1b764223403ffdd47bded39efa9450a53b5a06","src/mac/streams/exception.rs":"316cf3b9ccb6b1bb3b431b4e927e4d68201f845d80e31fcaa6b80bcc30748147","src/mac/streams/memory_list.rs":"e507a1ada858ef535e3e211d91195901e9976ba0ff6cf601919d7cacb48c5e9f","src/mac/streams/misc_info.rs":"ed9fe394ded145cae62094adf3a9e454e23bfd5b6fab7871f0cf62c6a5e79017","src/mac/streams/module_list.rs":"0d735c07d162311d386aa40d5a80ebcbdc644df0dfc811d7c4b85a2fae51c5f9","src/mac/streams/system_info.rs":"4bd0d171c9d7c97afbacf6fd144d5554f0e46d47ae78f00b85331d6d1a1d24b3","src/mac/streams/thread_list.rs":"2b003e3409daebbd38ebc8a49a2c24e7a75b296d28f58a2c85114cf611174c1b","src/mac/streams/thread_names.rs":"2c8ca3a98b0c4ac9bd0373721324f32038569b70eb5b47bafe7de4cc3f76483b","src/mac/task_dumper.rs":"b22f42b11bb7e9af5976ec04cbf9f8bf4d1d70c3c7738b1072e7bd41e8d5af3c","src/mem_writer.rs":"44d594e4b8a4bedf28c61f89c34f13be42deb9a8820a1d3267dd9572d65d0c97","src/minidump_cpu.rs":"ccb3dc179699159883e539e29285f8f6ba936afb8ce8980dbcb5060a80b6618c","src/minidump_format.rs":"9d5940d71da3a543efa90279e287e0dbbe303de386a4d5aab15e8ccfdd556116","src/windows.rs":"7aae8747519b203f1889c369cb1122ff308d255e9514cf9d4d3f09257a3bb96c","src/windows/errors.rs":"9b8752122784417ed48b9c3fccc9bdabc6c4e6285cdb77c79723cba56cb0de82","src/windows/ffi.rs":"24a6f99de9f25ae7bc80f2763d8c5b97e65699682a99ec6265ebc435c3310999","src/windows/minidump_writer.rs":"1c89d43c31f59c9091e84155d7062bf85d4674c54899a4e92b4ba2bd5fcd1354","tests/common/mod.rs":"457d8c26f41d2c883c42ede5f7f2b604368581ef299f41a18b5d8c4ee7974c21","tests/linux_minidump_writer.rs":"128273f06bad3bcc316ea4cd3ba7ef1bc74f1fb349c5d38129fcfdf30ddc5037","tests/mac_minidump_writer.rs":"355e28635e9f8ccc831627e3323fe37907915047cd525f07eefd15d658cf4823","tests/ptrace_dumper.rs":"c4ec7f903cbe2181891951680efa9aaf21875b42184068436b3a17f412ece227","tests/task_dumper.rs":"3881899e4cfef0dae772d16b1a783497eebd2b58025394e0819cb03a0960d480","tests/windows_minidump_writer.rs":"138551179c4e610141d56ba99305a0fc05531c8e9924805dd071d380338f0f74"},"package":"017101620fe5d413ac2d98224ab8b1fff0d4bacaf2803c130ad4a6db3e5d3e70"}
\ No newline at end of file diff --git a/third_party/rust/minidump-writer/CHANGELOG.md b/third_party/rust/minidump-writer/CHANGELOG.md new file mode 100644 index 0000000000..770d621be3 --- /dev/null +++ b/third_party/rust/minidump-writer/CHANGELOG.md @@ -0,0 +1,119 @@ +<!-- markdownlint-disable blanks-around-headings blanks-around-lists no-duplicate-heading --> + +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +<!-- next-header --> +## [Unreleased] - ReleaseDate +## [0.8.3] - 2023-11-07 +### Added +- [PR#94](https://github.com/rust-minidump/minidump-writer/pull/94) added support for writing [file information](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_handle_descriptor) for every file open in the process the dump is being performed for into the [`MINIDUMP_HANDLE_DATA_STREAM`](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_handle_data_stream) stream. +- [PR#90](https://github.com/rust-minidump/minidump-writer/pull/90) added support for including the `/proc/<pid>/limits` file in the [`MozLinuxLimits`](https://docs.rs/minidump-common/latest/minidump_common/format/enum.MINIDUMP_STREAM_TYPE.html#variant.MozLinuxLimits) stream. This information can be used together with the file information described above to diagnose situations where the process was killed by the kernel due to file handle limits being hit. Thanks [@lissyx](https://github.com/lissyx)! + +### Changed +- [PR#94](https://github.com/rust-minidump/minidump-writer/pull/94) updated several dependencies to align with `minidump-common`, which was also bumped. + +## [0.8.2] - 2023-09-21 +### Added +- [PR#86](https://github.com/rust-minidump/minidump-writer/pull/86) added support for `i686-android-linux`. + +### Fixed +- [PR#85](https://github.com/rust-minidump/minidump-writer/pull/85) removed the dependency on `chrono`. +- [PR#89](https://github.com/rust-minidump/minidump-writer/pull/89) resolved [#88](https://github.com/rust-minidump/minidump-writer/issues/88) by merging ranges that were mapped, but had 1 or more unmapped ranges in between them. + +### Changed +- [PR#87](https://github.com/rust-minidump/minidump-writer/pull/87) updated some dependencies. + +## [0.8.1] - 2023-06-21 +### Added +- [PR#70](https://github.com/rust-minidump/minidump-writer/pull/70) resolved [#8](https://github.com/rust-minidump/minidump-writer/issues/8) by adding support for writing `MemoryInfoListStream` on Linux/Android targets, this allows minidump consumers to process minidumps more easily without needing to parse and understand Linux-specific information. Thanks [@afranchuk](https://github.com/afranchuk)! +- [PR#81](https://github.com/rust-minidump/minidump-writer/pull/81) stabilized `arm` and `aarch64` support for `unknown-linux` and `linux-android`, as well as adding support for `x86_64-linux-android`. + +### Changed +- [PR#70](https://github.com/rust-minidump/minidump-writer/pull/70) replaced the custom reading of procfs information used when generating a minidump on Linux to use the `procfs` crate instead, removing a bunch of code. +- [PR#80](https://github.com/rust-minidump/minidump-writer/pull/80) along with [PR#84](https://github.com/rust-minidump/minidump-writer/pull/84) replaced `procfs` with `procfs-core`, removing unneeded dependencies such as `windows-sys`. + +### Fixed +- [PR#78](https://github.com/rust-minidump/minidump-writer/pull/78) resolved [#24](https://github.com/rust-minidump/minidump-writer/issues/24) by ignoring guard pages when dumping the stack to the minidump in the event of a stack overflow. +- [PR#83](https://github.com/rust-minidump/minidump-writer/pull/83) resolved [#82](https://github.com/rust-minidump/minidump-writer/issues/82) by correctly aligning a structure. + +## [0.8.0] - 2023-04-03 +### Removed +- [PR#77](https://github.com/rust-minidump/minidump-writer/pull/77) removed the dependency on `winapi`, all bindings are either part of `minidump-writer` or `crash-context` now. + +### Changed +- [PR#77](https://github.com/rust-minidump/minidump-writer/pull/77) closed [#67](https://github.com/rust-minidump/minidump-writer/issues/67) by allowing the user to specify the `MinidumpType` flags when creating a minidump. + +### Fixed +- [PR#68](https://github.com/rust-minidump/minidump-writer/pull/68) resolved [#29](https://github.com/rust-minidump/minidump-writer/issues/29) by ignoring the bening `ESRCH` error when detaching pthreads. Thanks [@afranchuk](https://github.com/afranchuk)! +- [PR#74](https://github.com/rust-minidump/minidump-writer/pull/74) resolved [#73](https://github.com/rust-minidump/minidump-writer/issues/73) by ensuring the `NT_GNU_BUILD_ID` section had the proper correct `GNU` name before using it as the build identifier. + +## [0.7.0] - 2022-11-17 +### Changed +- [PR#65](https://github.com/rust-minidump/minidump-writer/pull/65) updated `crash-context` to 0.5, which has support for a custom `capture_context` to replace `RtlCaptureContext` on Windows, due to improper bindings and deficiencies, resolving [#63](https://github.com/rust-minidump/minidump-writer/issues/63). +- [PR#65](https://github.com/rust-minidump/minidump-writer/pull/65) replaced _most_ of the custom bindings from [PR#60](https://github.com/rust-minidump/minidump-writer/pull/60) with bindings from either `crash-context` or `winapi`. + +## [0.6.0] - 2022-11-15 +### Changed +- [PR#60](https://github.com/rust-minidump/minidump-writer/pull/60) removed the dependency on `windows-sys` due the massive version churn, resolving [#58](https://github.com/rust-minidump/minidump-writer/issues/58). This should allow projects to more easily integrate this crate into their project without introducing multiple versions of transitive dependencies. +- [PR#62](https://github.com/rust-minidump/minidump-writer/pull/62) replaced `MDExceptionCodeLinux` with `minidump_common::ExceptionCodeLinux`. +- [PR#64](https://github.com/rust-minidump/minidump-writer/pull/64) updated dependencies. + +## [0.5.0] - 2022-10-21 +### Changed +- [PR#53](https://github.com/rust-minidump/minidump-writer/pull/53) made the `mem_writer` and `dir_section` modules public. Thanks [@sage-msft](https://github.com/sage-msft)! +- [PR#55](https://github.com/rust-minidump/minidump-writer/pull/55) bumped `nix`, `minidump-common`, `minidump`, `minidump-processor` and `dump_syms`. Thanks +[@sfackler](https://github.com/sfackler)! +- [PR#57](https://github.com/rust-minidump/minidump-writer/pull/57) bumped `windows-sys` to 0.42. + +### Removed +- [PR#56](https://github.com/rust-minidump/minidump-writer/pull/56) removed the writing of the [HandleOperationListStream](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_handle_operation_list) stream, as it was essentially untested and was of dubious usefulness. + +## [0.4.0] - 2022-07-21 +### Changed +- [PR#50](https://github.com/rust-minidump/minidump-writer/pull/50) updated `minidump-common` and `crash-context`. + +### Fixed +- [PR#50](https://github.com/rust-minidump/minidump-writer/pull/50) resolved [#33](https://github.com/rust-minidump/minidump-writer/issues/33) by encoding the full exception info in the `exception_information` field of the exception stream. +- [PR#50](https://github.com/rust-minidump/minidump-writer/pull/50) resolved [#34](https://github.com/rust-minidump/minidump-writer/issues/34) by unwrapping `EXC_CRASH` exceptions to retrieve the wrapped exception. + +## [0.3.1] - 2022-07-18 +### Fixed +- [PR#47](https://github.com/rust-minidump/minidump-writer/pull/47) resolved [#46](https://github.com/rust-minidump/minidump-writer/issues/46) by handling the special case of `dyld`. + +## [0.3.0] - 2022-07-15 +### Fixed +- [PR#42](https://github.com/rust-minidump/minidump-writer/pull/42) resolved [#41](https://github.com/rust-minidump/minidump-writer/issues/41) by capping the VM read of task memory to avoid a syscall failure, as well as made it so that if an error does occur when reading the module's file path, the module is still written to the minidump, as the file path is less important than the UUID in terms of module identification. +- [PR#44](https://github.com/rust-minidump/minidump-writer/pull/44) resolved [#43](https://github.com/rust-minidump/minidump-writer/issues/43) by correctly calculating the base address of each loaded module. The bug was inherited from Breakpad. +- [PR#44](https://github.com/rust-minidump/minidump-writer/pull/44) and [PR#45](https://github.com/rust-minidump/minidump-writer/pull/45) resolved [#37](https://github.com/rust-minidump/minidump-writer/issues/37) by making the `crash_context::CrashContext` optional on MacOS and Windows, to make creating a minidump without necessarily having an actual crash more convenient for users. + +## [0.2.1] - 2022-05-25 +### Added +- [PR#32](https://github.com/rust-minidump/minidump-writer/pull/32) resolved [#23](https://github.com/rust-minidump/minidump-writer/issues/23) by adding support for the thread names stream on MacOS. + +## [0.2.0] - 2022-05-23 +### Added +- [PR#21](https://github.com/rust-minidump/minidump-writer/pull/21) added an initial implementation for `x86_64-apple-darwin` and `aarch64-apple-darwin` + +## [0.1.0] - 2022-04-26 +### Added +- Initial release, including basic support for `x86_64-unknown-linux-gnu/musl` and `x86_64-pc-windows-msvc` + +<!-- next-url --> +[Unreleased]: https://github.com/rust-minidump/minidump-writer/compare/0.8.3...HEAD +[0.8.3]: https://github.com/rust-minidump/minidump-writer/compare/0.8.2...0.8.3 +[0.8.2]: https://github.com/rust-minidump/minidump-writer/compare/0.8.1...0.8.2 +[0.8.1]: https://github.com/rust-minidump/minidump-writer/compare/0.8.0...0.8.1 +[0.8.0]: https://github.com/rust-minidump/minidump-writer/compare/0.7.0...0.8.0 +[0.7.0]: https://github.com/rust-minidump/minidump-writer/compare/0.6.0...0.7.0 +[0.6.0]: https://github.com/rust-minidump/minidump-writer/compare/0.5.0...0.6.0 +[0.5.0]: https://github.com/rust-minidump/minidump-writer/compare/0.4.0...0.5.0 +[0.4.0]: https://github.com/rust-minidump/minidump-writer/compare/0.3.1...0.4.0 +[0.3.1]: https://github.com/rust-minidump/minidump-writer/compare/0.3.0...0.3.1 +[0.3.0]: https://github.com/rust-minidump/minidump-writer/compare/0.2.1...0.3.0 +[0.2.1]: https://github.com/rust-minidump/minidump-writer/compare/0.2.0...0.2.1 +[0.2.0]: https://github.com/rust-minidump/minidump-writer/compare/0.1.0...0.2.0 +[0.1.0]: https://github.com/rust-minidump/minidump-writer/releases/tag/0.1.0 diff --git a/third_party/rust/minidump-writer/Cargo.lock b/third_party/rust/minidump-writer/Cargo.lock new file mode 100644 index 0000000000..514186142d --- /dev/null +++ b/third_party/rust/minidump-writer/Cargo.lock @@ -0,0 +1,1851 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "breakpad-symbols" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b002797414ffc34425bdf5b21a9e50d102013292625749eeba0a59923176ab05" +dependencies = [ + "async-trait", + "cachemap2", + "circular", + "debugid", + "futures-util", + "minidump-common", + "nom", + "range-map", + "thiserror", + "tracing", +] + +[[package]] +name = "brownstone" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5839ee4f953e811bfdcf223f509cb2c6a3e1447959b0bff459405575bc17f22" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cab" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6b4de23c7d39c0631fd3cc952d87951c86c75a13812d7247cb7a896e7b3551" +dependencies = [ + "byteorder", + "flate2", + "lzxd", + "time", +] + +[[package]] +name = "cachemap2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bba2f68a9fefca870fed897de7c655f9d5c1eaf1cd9517db96c9a3861f648b" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "circular" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c76f98bdfc7f66172e6c7065f981ebb576ffc903fe4c0561d9f0c2509226dc6" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[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 = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[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 = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dmsort" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0bc8fbe9441c17c9f46f75dfe27fa1ddb6c68a461ccaed0481419219d4f10d3" + +[[package]] +name = "dump_syms" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0640a83419dce0966981ccf5f41359cf858ce786314e55af12bff22e782585d5" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "cab", + "crossbeam", + "dirs", + "goblin 0.6.1", + "hashbrown", + "log", + "lzma-rs", + "num_cpus", + "once_cell", + "regex", + "serde", + "serde_json", + "sha2", + "symbolic", + "url", + "uuid", +] + +[[package]] +name = "elementtree" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3efd4742acf458718a6456e0adf0b4d734d6b783e452bbf1ac36bf31f4085cb3" +dependencies = [ + "string_cache", +] + +[[package]] +name = "elsa" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e0aca8dce8856e420195bd13b6a64de3334235ccc9214e824b86b12bf26283" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + +[[package]] +name = "goblin" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "goblin" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27c1b4369c2cd341b5de549380158b105a04c331be5db9110eef7b6d2742134" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", + "serde", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indent_write" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "joinery" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[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 = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzxd" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784462f20dddd9dfdb45de963fa4ad4a288cb10a7889ac5d2c34fb6481c6b213" + +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minidump" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c671544a05d0e8daa3018c8fb6687c11935c4ae8f122de8f2386c2896b4e9b8" +dependencies = [ + "debugid", + "encoding_rs", + "memmap2 0.8.0", + "minidump-common", + "num-traits", + "range-map", + "scroll", + "thiserror", + "time", + "tracing", + "uuid", +] + +[[package]] +name = "minidump-common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc11dfb55b3b7b5684fb16d98e0fc9d1e93a64d6b00bf383eabfc4541aaac2" +dependencies = [ + "bitflags 2.4.0", + "debugid", + "num-derive", + "num-traits", + "range-map", + "scroll", + "smart-default", +] + +[[package]] +name = "minidump-processor" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b49bde7c0ae9a7142c540c27c7fc29db2288fd9614f11a9ce57badeb74af43" +dependencies = [ + "async-trait", + "breakpad-symbols", + "debugid", + "futures-util", + "memmap2 0.8.0", + "minidump", + "minidump-common", + "minidump-unwind", + "scroll", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "minidump-unwind" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63aef4cd2e018881680b152296ae28e674242823faa1767b417b6669a1896cdc" +dependencies = [ + "async-trait", + "breakpad-symbols", + "cachemap2", + "futures-util", + "memmap2 0.8.0", + "minidump", + "minidump-common", + "scroll", + "symbolic-cfi", + "symbolic-common", + "symbolic-debuginfo", + "symbolic-demangle", + "tracing", +] + +[[package]] +name = "minidump-writer" +version = "0.8.3" +dependencies = [ + "bitflags 2.4.0", + "byteorder", + "cfg-if", + "crash-context", + "dump_syms", + "futures", + "goblin 0.7.1", + "libc", + "mach2", + "memmap2 0.8.0", + "memoffset", + "minidump", + "minidump-common", + "minidump-processor", + "minidump-unwind", + "nix", + "procfs-core", + "scroll", + "similar-asserts", + "tempfile", + "thiserror", + "uuid", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "msvc-demangler" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb67c6dd0fa9b00619c41c5700b6f92d5f418be49b45ddb9970fbd4569df3c8" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom-supreme" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd3ae6c901f1959588759ff51c95d24b491ecb9ff91aa9c2ef4acc5b1dcab27" +dependencies = [ + "brownstone", + "indent_write", + "joinery", + "memchr", + "nom", +] + +[[package]] +name = "num-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[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 0.3.5", + "smallvec", + "windows-targets 0.48.0", +] + +[[package]] +name = "pdb" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82040a392923abe6279c00ab4aff62d5250d1c8555dc780e4b02783a7aa74863" +dependencies = [ + "fallible-iterator", + "scroll", + "uuid", +] + +[[package]] +name = "pdb-addr2line" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e89a9f2f40b2389ba6da0814c8044bf942bece03dffa1514f84e3b525f4f9a" +dependencies = [ + "bitflags 1.3.2", + "elsa", + "maybe-owned", + "pdb", + "range-collections", + "thiserror", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[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.4.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 = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "range-collections" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fdfd79629e2b44a1d34b4d227957174cb858e6b86ee45fad114edbcfc903ab" +dependencies = [ + "binary-merge", + "inplace-vec-builder", + "smallvec", +] + +[[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.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[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 = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "serde_json" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +dependencies = [ + "console", + "similar", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[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 2.0.18", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "symbolic" +version = "12.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d311bfa722c01294e838091c455ed4e63c96ea7b8fb65b7fd7acdc72a4b0309" +dependencies = [ + "symbolic-cfi", + "symbolic-common", + "symbolic-debuginfo", + "symbolic-demangle", +] + +[[package]] +name = "symbolic-cfi" +version = "12.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d568f889388e2d96b4b3c258a9e36ebe4ad95bd941ee245719c30cbc2c1ad51" +dependencies = [ + "symbolic-common", + "symbolic-debuginfo", + "thiserror", +] + +[[package]] +name = "symbolic-common" +version = "12.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb6682826c7186b16c5c0ed2a68f419609f8af62f070c688871caae4911432d" +dependencies = [ + "debugid", + "memmap2 0.5.10", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-debuginfo" +version = "12.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222363f4ca5fb00cdd4915afba4a6aa18549d3438b27c048db2c41f0ea7a1e58" +dependencies = [ + "bitvec", + "debugid", + "dmsort", + "elementtree", + "elsa", + "fallible-iterator", + "flate2", + "gimli", + "goblin 0.6.1", + "lazy_static", + "nom", + "nom-supreme", + "once_cell", + "parking_lot", + "pdb-addr2line", + "regex", + "scroll", + "serde", + "serde_json", + "smallvec", + "symbolic-common", + "symbolic-ppdb", + "thiserror", + "wasmparser", + "zip", +] + +[[package]] +name = "symbolic-demangle" +version = "12.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7597986728aebeae5b865266b6158b134a4d61f078049552b5c897bb0a61985" +dependencies = [ + "cc", + "cpp_demangle", + "msvc-demangler", + "rustc-demangle", + "symbolic-common", +] + +[[package]] +name = "symbolic-ppdb" +version = "12.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e3c9b6cc654de90c05d841af02f3dd37415278538fa23534cbcb58a1a74ae8b" +dependencies = [ + "flate2", + "indexmap", + "serde_json", + "symbolic-common", + "thiserror", + "uuid", + "watto", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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 = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", +] + +[[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 2.0.18", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasmparser" +version = "0.102.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48134de3d7598219ab9eaf6b91b15d8e50d31da76b8519fe4ecfcec2cf35104b" +dependencies = [ + "indexmap", + "url", +] + +[[package]] +name = "watto" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6746b5315e417144282a047ebb82260d45c92d09bf653fa9ec975e3809be942b" +dependencies = [ + "leb128", + "thiserror", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[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_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/third_party/rust/minidump-writer/Cargo.toml b/third_party/rust/minidump-writer/Cargo.toml new file mode 100644 index 0000000000..6b33b5eebf --- /dev/null +++ b/third_party/rust/minidump-writer/Cargo.toml @@ -0,0 +1,105 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "minidump-writer" +version = "0.8.3" +authors = ["Martin Sirringhaus"] +description = "Rust rewrite of Breakpad's minidump_writer" +homepage = "https://github.com/rust-minidump/minidump-writer" +readme = "README.md" +license = "MIT" +repository = "https://github.com/rust-minidump/minidump-writer" + +[dependencies.bitflags] +version = "2.4" + +[dependencies.byteorder] +version = "1.4" + +[dependencies.cfg-if] +version = "1.0" + +[dependencies.crash-context] +version = "0.6" + +[dependencies.memoffset] +version = "0.9" + +[dependencies.minidump-common] +version = "0.19.1" + +[dependencies.scroll] +version = "0.11" + +[dependencies.tempfile] +version = "3.8" + +[dependencies.thiserror] +version = "1.0" + +[dev-dependencies.futures] +version = "0.3" +features = ["executor"] + +[dev-dependencies.memmap2] +version = "0.8" + +[dev-dependencies.minidump] +version = "0.19" + +[target."cfg(any(target_os = \"linux\", target_os = \"android\"))".dependencies.nix] +version = "0.27" +features = [ + "mman", + "process", + "ptrace", + "user", +] +default-features = false + +[target."cfg(any(target_os = \"linux\", target_os = \"android\"))".dependencies.procfs-core] +version = "0.16" +default-features = false + +[target."cfg(target_os = \"macos\")".dependencies.mach2] +version = "0.4" + +[target."cfg(target_os = \"macos\")".dev-dependencies.dump_syms] +version = "2.2" +default-features = false + +[target."cfg(target_os = \"macos\")".dev-dependencies.minidump-processor] +version = "0.19" +default-features = false + +[target."cfg(target_os = \"macos\")".dev-dependencies.minidump-unwind] +version = "0.19" +features = ["debuginfo"] + +[target."cfg(target_os = \"macos\")".dev-dependencies.similar-asserts] +version = "1.5" + +[target."cfg(target_os = \"macos\")".dev-dependencies.uuid] +version = "1.4" + +[target."cfg(target_os = \"windows\")".dependencies.bitflags] +version = "2.4" + +[target."cfg(unix)".dependencies.goblin] +version = "0.7.1" + +[target."cfg(unix)".dependencies.libc] +version = "0.2" + +[target."cfg(unix)".dependencies.memmap2] +version = "0.8" diff --git a/third_party/rust/minidump-writer/LICENSE b/third_party/rust/minidump-writer/LICENSE new file mode 100644 index 0000000000..6f9ec1352e --- /dev/null +++ b/third_party/rust/minidump-writer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/rust/minidump-writer/README.md b/third_party/rust/minidump-writer/README.md new file mode 100644 index 0000000000..20962c913f --- /dev/null +++ b/third_party/rust/minidump-writer/README.md @@ -0,0 +1,155 @@ +<div align="center"> + +# `minidump-writer` + +**Rust rewrite of Breakpad's minidump_writer (client)** + +[![Rust CI](https://github.com/rust-minidump/minidump-writer/actions/workflows/ci.yml/badge.svg)](https://github.com/rust-minidump/minidump-writer/actions/workflows/ci.yml) +[![crates.io](https://img.shields.io/crates/v/minidump-writer.svg)](https://crates.io/crates/minidump-writer) +[![docs.rs](https://docs.rs/minidump-writer/badge.svg)](https://docs.rs/minidump-writer) + +</div> + +This project is currently being very actively brought up from nothing, and is really ultimately many separate client implementations for different platforms. + +## Usage / Examples + +The primary use case of this crate is for creating a minidump for an **external** process (ie a process other than the one that writes the minidump) as writing minidumps from within a crashing process is inherently unreliable. That being said, there are scenarios where creating a minidump can be useful outside of a crash scenario thus each supported platforms has a way to generate a minidump for a local process as well. + +For more information on how to dump an external process you can check out the documentation or code for the [minidumper](https://docs.rs/minidumper/latest/minidumper/) crate. + +### Linux + +#### Local process + +```rust +fn write_minidump() { + // At a minimum, the crashdump writer needs to know the process and thread that you want to dump + let mut writer = minidump_writer::minidump_writer::MinidumpWriter::new( + std::process::id() as _, + // This gets the current thread, but you could get the id for any thread + // in the current process + unsafe { libc::syscall(libc::SYS_gettid) } as i32 + ); + + // If provided with a full [crash_context::CrashContext](https://docs.rs/crash-context/latest/crash_context/struct.CrashContext.html), + // the crash will contain more info on the crash cause, such as the signal + //writer.set_crash_context(minidump_writer::crash_context::CrashContext { inner: crash_context }); + + // Here we could add more context or modify how the minidump is written, eg + // Add application specific memory blocks to the minidump + //writer.set_app_memory() + // Sanitize stack memory before it is written to the minidump by replacing + // non-pointer values with a sentinel value + //writer.sanitize_stack(); + + let mut minidump_file = std::fs::File::create("example_dump.mdmp").expect("failed to create file"); + writer.dump(&mut minidump_file).expect("failed to write minidump"); +} +``` + +#### External process + +```rust +fn write_minidump(crash_context: crash_context::CrashContext) { + // At a minimum, the crashdump writer needs to know the process and thread that the crash occurred in + let mut writer = minidump_writer::minidump_writer::MinidumpWriter::new(crash_context.pid, crash_context.tid); + + // If provided with a full [crash_context::CrashContext](https://docs.rs/crash-context/latest/crash_context/struct.CrashContext.html), + // the crash will contain more info on the crash cause, such as the signal + writer.set_crash_context(minidump_writer::crash_context::CrashContext { inner: crash_context }); + + // Here we could add more context or modify how the minidump is written, eg + // Add application specific memory blocks to the minidump + //writer.set_app_memory() + // Sanitize stack memory before it is written to the minidump by replacing + // non-pointer values with a sentinel value + //writer.sanitize_stack(); + + let mut minidump_file = std::fs::File::create("example_dump.mdmp").expect("failed to create file"); + writer.dump(&mut minidump_file).expect("failed to write minidump"); +} +``` + +### Windows + +#### Local process + +```rust +fn write_minidump() { + let mut minidump_file = std::fs::File::create("example_dump.mdmp").expect("failed to create file"); + + // Attempts to the write the minidump + minidump_writer::minidump_writer::MinidumpWriter::dump_local_context( + // The exception code, presumably one of STATUS_*. Defaults to STATUS_NONCONTINUABLE_EXCEPTION if not specified + None, + // If not specified, uses the current thread as the "crashing" thread, + // so this is equivalent to passing `None`, but it could be any thread + // in the process + Some(unsafe { windows_sys::Win32::System::Threading::GetCurrentThreadId() }), + &mut minidump_file, + ).expect("failed to write minidump");; +} +``` + +#### External process + +```rust +fn write_minidump(crash_context: crash_context::CrashContext) { + use std::io::{Read, Seek}; + + // Create the file to write the minidump to. Unlike MacOS and Linux, the + // system call used to write the minidump only supports outputting to a file + let mut minidump_file = std::fs::File::create("example_dump.mdmp").expect("failed to create file"); + // Attempts to the write the minidump for the crash context + minidump_writer::minidump_writer::MinidumpWriter::dump_crash_context(crash_context, &mut minidump_file).expect("failed to write minidump");; + + let mut minidump_contents = Vec::with_capacity(minidump_file.stream_position().expect("failed to get stream length") as usize); + minidump_file.rewind().expect("failed to rewind minidump file"); + + minidump_file.read_to_end(&mut minidump_contents).expect("failed to read minidump"); +} +``` + +### MacOS + +#### Local process + +```rust +fn write_minidump() { + // Defaults to dumping the current process and thread. + let mut writer = minidump_writer::minidump_writer::MinidumpWriter::new(None, None)?; + + let mut minidump_file = std::fs::File::create("example_dump.mdmp").expect("failed to create file"); + writer.dump(&mut minidump_file).expect("failed to write minidump"); +} +``` + +#### External process + +```rust +fn write_minidump(crash_context: crash_context::CrashContext) { + let mut writer = minidump_writer::minidump_writer::MinidumpWriter::with_crash_context(crash_context)?; + + let mut minidump_file = std::fs::File::create("example_dump.mdmp").expect("failed to create file"); + writer.dump(&mut minidump_file).expect("failed to write minidump"); +} +``` + +## Client Statuses + +- ✅ Usable, but care should be taken in production environments +- ⚠️ Implemented (ie compiles), but untested and needs more work to be usable +- ⭕️ Unimplemented, but could be implemented in the future +- ❌ Unimplemented, and unlikely to ever be implemented + +| Arch | unknown-linux-gnu | unknown-linux-musl | linux-android | pc-windows-msvc | apple-darwin | apple-ios | +----------- | ----------------- | ------------------ | ------------- | --------------- | ------------ | --------- | +`x86_64` | ✅ | ✅ | ⚠️ | ✅ | ✅ | ⭕️ | +`i686` | ✅ | ✅ | ❌ | ⭕️ | ❌ | ❌ | +`arm` | ⚠️ | ⚠️ | ⚠️ | ⭕️ | ❌ | ❌ | +`aarch64` | ⚠️ | ⚠️ | ⚠️ | ⭕️ | ✅ | ⭕️ | +`mips` | ⭕️ | ⭕️ | ❌ | ❌ | ❌ | ❌ | +`mips64` | ⭕️ | ⭕️ | ❌ | ❌ | ❌ | ❌ | +`powerpc` | ⭕️ | ⭕️ | ❌ | ❌ | ❌ | ❌ | +`powerpc64` | ⭕️ | ⭕️ | ❌ | ❌ | ❌ | ❌ | diff --git a/third_party/rust/minidump-writer/build.rs b/third_party/rust/minidump-writer/build.rs new file mode 100644 index 0000000000..23efce46c1 --- /dev/null +++ b/third_party/rust/minidump-writer/build.rs @@ -0,0 +1,5 @@ +fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + println!("cargo:rustc-link-lib=dylib=dbghelp"); + } +} diff --git a/third_party/rust/minidump-writer/deny.toml b/third_party/rust/minidump-writer/deny.toml new file mode 100644 index 0000000000..3cadd53b23 --- /dev/null +++ b/third_party/rust/minidump-writer/deny.toml @@ -0,0 +1,23 @@ +targets = [ + { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-musl" }, + { triple = "x86_64-pc-windows-msvc" }, + { triple = "x86_64-apple-darwin" }, + { triple = "aarch64-apple-darwin" }, + { triple = "aarch64-linux-android" }, + { triple = "x86_64-linux-android" }, +] + +[advisories] +ignore = [] + +[bans] +multiple-versions = "deny" +skip = [ + # scroll still depends on 1.0 + { name = "syn", version = "=1.0.109" }, +] + +[licenses] +allow = ["MIT", "Apache-2.0"] +exceptions = [{ allow = ["Unicode-DFS-2016"], name = "unicode-ident" }] diff --git a/third_party/rust/minidump-writer/examples/synthetic.rs b/third_party/rust/minidump-writer/examples/synthetic.rs new file mode 100644 index 0000000000..58ddc7d268 --- /dev/null +++ b/third_party/rust/minidump-writer/examples/synthetic.rs @@ -0,0 +1,36 @@ +//! Emits default minidump with no streams to specified path + +use std::fs::File; + +use minidump_writer::{ + dir_section::DirSection, + mem_writer::{Buffer, MemoryWriter}, + minidump_format::{MDRawHeader, MD_HEADER_SIGNATURE, MD_HEADER_VERSION}, +}; + +// usage: `cargo run --example synthetic /tmp/micro-minidump.dmp` +fn main() { + let output_path = std::env::args() + .nth(1) + .expect("missing argument: output file path"); + + let num_writers = 0u32; + let buffer_capacity = 32; + + let mut destination = File::create(output_path).expect("failed to create file"); + let mut buffer = Buffer::with_capacity(buffer_capacity); + let mut header_section = MemoryWriter::<MDRawHeader>::alloc(&mut buffer).unwrap(); + let mut dir_section = DirSection::new(&mut buffer, num_writers, &mut destination).unwrap(); + + let header = MDRawHeader { + signature: MD_HEADER_SIGNATURE, + version: MD_HEADER_VERSION, + stream_count: num_writers, + stream_directory_rva: dir_section.position(), + checksum: 0, + time_date_stamp: 0u32, + flags: 0, + }; + header_section.set_value(&mut buffer, header).unwrap(); + dir_section.write_to_file(&mut buffer, None).unwrap(); +} diff --git a/third_party/rust/minidump-writer/release.toml b/third_party/rust/minidump-writer/release.toml new file mode 100644 index 0000000000..25de3f42d9 --- /dev/null +++ b/third_party/rust/minidump-writer/release.toml @@ -0,0 +1,10 @@ +pre-release-commit-message = "Release {{version}}" +tag-message = "Release {{version}}" +tag-name = "{{version}}" +pre-release-replacements = [ + { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}" }, + { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}" }, + { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" }, + { file = "CHANGELOG.md", search = "<!-- next-header -->", replace = "<!-- next-header -->\n## [Unreleased] - ReleaseDate" }, + { file = "CHANGELOG.md", search = "<!-- next-url -->", replace = "<!-- next-url -->\n[Unreleased]: https://github.com/rust-minidump/minidump-writer/compare/{{tag_name}}...HEAD" }, +] diff --git a/third_party/rust/minidump-writer/src/bin/test.rs b/third_party/rust/minidump-writer/src/bin/test.rs new file mode 100644 index 0000000000..85b6fa6a93 --- /dev/null +++ b/third_party/rust/minidump-writer/src/bin/test.rs @@ -0,0 +1,446 @@ +// This binary shouldn't be under /src, but under /tests, but that is +// currently not possible (https://github.com/rust-lang/cargo/issues/4356) + +type Error = Box<dyn std::error::Error + std::marker::Send + std::marker::Sync>; +pub type Result<T> = std::result::Result<T, Error>; + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod linux { + use super::*; + use minidump_writer::{ + ptrace_dumper::{PtraceDumper, AT_SYSINFO_EHDR}, + LINUX_GATE_LIBRARY_NAME, + }; + use nix::{ + sys::mman::{mmap, MapFlags, ProtFlags}, + unistd::getppid, + }; + use std::os::fd::BorrowedFd; + + macro_rules! test { + ($x:expr, $errmsg:expr) => { + if $x { + Ok(()) + } else { + Err($errmsg) + } + }; + } + + fn test_setup() -> Result<()> { + let ppid = getppid(); + PtraceDumper::new(ppid.as_raw())?; + Ok(()) + } + + fn test_thread_list() -> Result<()> { + let ppid = getppid(); + let dumper = PtraceDumper::new(ppid.as_raw())?; + test!(!dumper.threads.is_empty(), "No threads")?; + test!( + dumper + .threads + .iter() + .filter(|x| x.tid == ppid.as_raw()) + .count() + == 1, + "Thread found multiple times" + )?; + Ok(()) + } + + fn test_copy_from_process(stack_var: usize, heap_var: usize) -> Result<()> { + let ppid = getppid().as_raw(); + let mut dumper = PtraceDumper::new(ppid)?; + dumper.suspend_threads()?; + let stack_res = PtraceDumper::copy_from_process(ppid, stack_var as *mut libc::c_void, 1)?; + + let expected_stack: libc::c_long = 0x11223344; + test!( + stack_res == expected_stack.to_ne_bytes(), + "stack var not correct" + )?; + + let heap_res = PtraceDumper::copy_from_process(ppid, heap_var as *mut libc::c_void, 1)?; + let expected_heap: libc::c_long = 0x55667788; + test!( + heap_res == expected_heap.to_ne_bytes(), + "heap var not correct" + )?; + dumper.resume_threads()?; + Ok(()) + } + + fn test_find_mappings(addr1: usize, addr2: usize) -> Result<()> { + let ppid = getppid(); + let dumper = PtraceDumper::new(ppid.as_raw())?; + dumper + .find_mapping(addr1) + .ok_or("No mapping for addr1 found")?; + + dumper + .find_mapping(addr2) + .ok_or("No mapping for addr2 found")?; + + test!(dumper.find_mapping(0).is_none(), "NULL found")?; + Ok(()) + } + + fn test_file_id() -> Result<()> { + let ppid = getppid().as_raw(); + let exe_link = format!("/proc/{}/exe", ppid); + let exe_name = std::fs::read_link(exe_link)?.into_os_string(); + let mut dumper = PtraceDumper::new(getppid().as_raw())?; + let mut found_exe = None; + for (idx, mapping) in dumper.mappings.iter().enumerate() { + if mapping.name.as_ref().map(|x| x.into()).as_ref() == Some(&exe_name) { + found_exe = Some(idx); + break; + } + } + let idx = found_exe.unwrap(); + let id = dumper.elf_identifier_for_mapping_index(idx)?; + assert!(!id.is_empty()); + assert!(id.iter().any(|&x| x > 0)); + Ok(()) + } + + fn test_merged_mappings(path: String, mapped_mem: usize, mem_size: usize) -> Result<()> { + // Now check that PtraceDumper interpreted the mappings properly. + let dumper = PtraceDumper::new(getppid().as_raw())?; + let mut mapping_count = 0; + for map in &dumper.mappings { + if map + .name + .as_ref() + .map_or(false, |name| name.to_string_lossy().starts_with(&path)) + { + mapping_count += 1; + // This mapping should encompass the entire original mapped + // range. + assert_eq!(map.start_address, mapped_mem); + assert_eq!(map.size, mem_size); + assert_eq!(0, map.offset); + } + } + assert_eq!(1, mapping_count); + Ok(()) + } + + fn test_linux_gate_mapping_id() -> Result<()> { + let ppid = getppid().as_raw(); + let mut dumper = PtraceDumper::new(ppid)?; + let mut found_linux_gate = false; + for mut mapping in dumper.mappings.clone() { + if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { + found_linux_gate = true; + dumper.suspend_threads()?; + let id = PtraceDumper::elf_identifier_for_mapping(&mut mapping, ppid)?; + test!(!id.is_empty(), "id-vec is empty")?; + test!(id.iter().any(|&x| x > 0), "all id elements are 0")?; + dumper.resume_threads()?; + break; + } + } + test!(found_linux_gate, "found no linux_gate")?; + Ok(()) + } + + fn test_mappings_include_linux_gate() -> Result<()> { + let ppid = getppid().as_raw(); + let dumper = PtraceDumper::new(ppid)?; + let linux_gate_loc = dumper.auxv[&AT_SYSINFO_EHDR]; + test!(linux_gate_loc != 0, "linux_gate_loc == 0")?; + let mut found_linux_gate = false; + for mapping in &dumper.mappings { + if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { + found_linux_gate = true; + test!( + linux_gate_loc == mapping.start_address.try_into()?, + "linux_gate_loc != start_address" + )?; + + // This doesn't work here, as we do not test via "fork()", so the addresses are different + // let ll = mapping.start_address as *const u8; + // for idx in 0..header::SELFMAG { + // let mag = unsafe { std::ptr::read(ll.offset(idx as isize)) == header::ELFMAG[idx] }; + // test!( + // mag, + // format!("ll: {} != ELFMAG: {} at {}", mag, header::ELFMAG[idx], idx) + // )?; + // } + break; + } + } + test!(found_linux_gate, "found no linux_gate")?; + Ok(()) + } + + fn spawn_and_wait(num: usize) -> Result<()> { + // One less than the requested amount, as the main thread counts as well + for _ in 1..num { + std::thread::spawn(|| { + println!("1"); + loop { + std::thread::park(); + } + }); + } + println!("1"); + loop { + std::thread::park(); + } + } + + fn spawn_name_wait(num: usize) -> Result<()> { + // One less than the requested amount, as the main thread counts as well + for id in 1..num { + std::thread::Builder::new() + .name(format!("thread_{}", id)) + .spawn(|| { + println!("1"); + loop { + std::thread::park(); + } + })?; + } + println!("1"); + loop { + std::thread::park(); + } + } + + fn spawn_mmap_wait() -> Result<()> { + let page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE).unwrap(); + let memory_size = std::num::NonZeroUsize::new(page_size.unwrap() as usize).unwrap(); + // Get some memory to be mapped by the child-process + let mapped_mem = unsafe { + mmap::<BorrowedFd>( + None, + memory_size, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_PRIVATE | MapFlags::MAP_ANON, + None, + 0, + ) + .unwrap() + }; + + println!("{} {}", mapped_mem as usize, memory_size); + loop { + std::thread::park(); + } + } + + fn spawn_alloc_wait() -> Result<()> { + let page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE).unwrap(); + let memory_size = page_size.unwrap() as usize; + + let mut values = Vec::<u8>::with_capacity(memory_size); + for idx in 0..memory_size { + values.push((idx % 255) as u8); + } + + println!("{:p} {}", values.as_ptr(), memory_size); + loop { + std::thread::park(); + } + } + + fn create_files_wait(num: usize) -> Result<()> { + let mut file_array = Vec::<tempfile::NamedTempFile>::with_capacity(num); + for id in 0..num { + let file = tempfile::Builder::new() + .prefix("test_file") + .suffix::<str>(id.to_string().as_ref()) + .tempfile() + .unwrap(); + file_array.push(file); + println!("1"); + } + println!("1"); + loop { + std::thread::park(); + // This shouldn't be executed, but we put it here to ensure that + // all the files within the array are kept open. + println!("{}", file_array.len()); + } + } + + pub(super) fn real_main(args: Vec<String>) -> Result<()> { + match args.len() { + 1 => match args[0].as_ref() { + "file_id" => test_file_id(), + "setup" => test_setup(), + "thread_list" => test_thread_list(), + "mappings_include_linux_gate" => test_mappings_include_linux_gate(), + "linux_gate_mapping_id" => test_linux_gate_mapping_id(), + "spawn_mmap_wait" => spawn_mmap_wait(), + "spawn_alloc_wait" => spawn_alloc_wait(), + _ => Err("Len 1: Unknown test option".into()), + }, + 2 => match args[0].as_ref() { + "spawn_and_wait" => { + let num_of_threads: usize = args[1].parse().unwrap(); + spawn_and_wait(num_of_threads) + } + "spawn_name_wait" => { + let num_of_threads: usize = args[1].parse().unwrap(); + spawn_name_wait(num_of_threads) + } + "create_files_wait" => { + let num_of_files: usize = args[1].parse().unwrap(); + create_files_wait(num_of_files) + } + _ => Err(format!("Len 2: Unknown test option: {}", args[0]).into()), + }, + 3 => { + if args[0] == "find_mappings" { + let addr1: usize = args[1].parse().unwrap(); + let addr2: usize = args[2].parse().unwrap(); + test_find_mappings(addr1, addr2) + } else if args[0] == "copy_from_process" { + let stack_var: usize = args[1].parse().unwrap(); + let heap_var: usize = args[2].parse().unwrap(); + test_copy_from_process(stack_var, heap_var) + } else { + Err(format!("Len 3: Unknown test option: {}", args[0]).into()) + } + } + 4 => { + if args[0] == "merged_mappings" { + let path = &args[1]; + let mapped_mem: usize = args[2].parse().unwrap(); + let mem_size: usize = args[3].parse().unwrap(); + test_merged_mappings(path.to_string(), mapped_mem, mem_size) + } else { + Err(format!("Len 4: Unknown test option: {}", args[0]).into()) + } + } + _ => Err("Unknown test option".into()), + } + } +} + +#[cfg(target_os = "windows")] +mod windows { + use super::*; + use std::mem; + + #[link(name = "kernel32")] + extern "system" { + pub fn GetCurrentProcessId() -> u32; + pub fn GetCurrentThreadId() -> u32; + pub fn GetCurrentThread() -> isize; + pub fn GetThreadContext(thread: isize, context: *mut crash_context::CONTEXT) -> i32; + } + + #[inline(never)] + pub(super) fn real_main(args: Vec<String>) -> Result<()> { + let exception_code = u32::from_str_radix(&args[0], 16).unwrap(); + + // Generate the exception and communicate back where the exception pointers + // are + unsafe { + let mut exception_record: crash_context::EXCEPTION_RECORD = mem::zeroed(); + let mut exception_context = std::mem::MaybeUninit::uninit(); + + let pid = GetCurrentProcessId(); + let tid = GetCurrentThreadId(); + + GetThreadContext(GetCurrentThread(), exception_context.as_mut_ptr()); + + let mut exception_context = exception_context.assume_init(); + + let exception_ptrs = crash_context::EXCEPTION_POINTERS { + ExceptionRecord: &mut exception_record, + ContextRecord: &mut exception_context, + }; + + exception_record.ExceptionCode = exception_code as _; + + let exc_ptr_addr = &exception_ptrs as *const _ as usize; + + println!("{pid} {exc_ptr_addr} {tid} {exception_code:x}"); + + // Wait until we're killed + loop { + std::thread::park(); + } + } + } +} + +#[cfg(target_os = "macos")] +mod mac { + use super::*; + use std::time::Duration; + + #[inline(never)] + pub(super) fn real_main(args: Vec<String>) -> Result<()> { + let port_name = args.get(0).ok_or("mach port name not specified")?; + let exception: u32 = args.get(1).ok_or("exception code not specified")?.parse()?; + + let client = + crash_context::ipc::Client::create(&std::ffi::CString::new(port_name.clone())?)?; + + std::thread::Builder::new() + .name("test-thread".to_owned()) + .spawn(move || { + #[inline(never)] + fn wait_until_killed(client: crash_context::ipc::Client, exception: u32) { + // SAFETY: syscalls + let cc = unsafe { + crash_context::CrashContext { + task: mach2::traps::mach_task_self(), + thread: mach2::mach_init::mach_thread_self(), + handler_thread: mach2::port::MACH_PORT_NULL, + exception: Some(crash_context::ExceptionInfo { + kind: exception, + code: 0, + subcode: None, + }), + } + }; + + // Send the crash context to the server and wait for it to + // finish dumping, we should be killed shortly afterwards + client + .send_crash_context( + &cc, + Some(Duration::from_secs(2)), + Some(Duration::from_secs(5)), + ) + .expect("failed to send crash context/receive ack"); + + // Wait until we're killed + loop { + std::thread::park(); + } + } + + wait_until_killed(client, exception) + }) + .unwrap() + .join() + .unwrap(); + + Ok(()) + } +} + +fn main() -> Result<()> { + let args: Vec<_> = std::env::args().skip(1).collect(); + + cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "android"))] { + linux::real_main(args) + } else if #[cfg(target_os = "windows")] { + windows::real_main(args) + } else if #[cfg(target_os = "macos")] { + mac::real_main(args) + } else { + unimplemented!(); + } + } +} diff --git a/third_party/rust/minidump-writer/src/dir_section.rs b/third_party/rust/minidump-writer/src/dir_section.rs new file mode 100644 index 0000000000..ced5a2d156 --- /dev/null +++ b/third_party/rust/minidump-writer/src/dir_section.rs @@ -0,0 +1,103 @@ +use crate::{ + mem_writer::{Buffer, MemoryArrayWriter, MemoryWriterError}, + minidump_format::MDRawDirectory, +}; +use std::io::{Error, Seek, Write}; + +pub type DumpBuf = Buffer; + +#[derive(Debug, thiserror::Error)] +pub enum FileWriterError { + #[error("IO error")] + IOError(#[from] Error), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +/// Utility that wraps writing minidump directory entries to an I/O stream, generally +/// a [`std::fs::File`]. +#[derive(Debug)] +pub struct DirSection<'a, W> +where + W: Write + Seek, +{ + curr_idx: usize, + section: MemoryArrayWriter<MDRawDirectory>, + /// If we have to append to some file, we have to know where we currently are + destination_start_offset: u64, + destination: &'a mut W, + last_position_written_to_file: u64, +} + +impl<'a, W> DirSection<'a, W> +where + W: Write + Seek, +{ + pub fn new( + buffer: &mut DumpBuf, + index_length: u32, + destination: &'a mut W, + ) -> std::result::Result<Self, FileWriterError> { + let dir_section = + MemoryArrayWriter::<MDRawDirectory>::alloc_array(buffer, index_length as usize)?; + + Ok(Self { + curr_idx: 0, + section: dir_section, + destination_start_offset: destination.stream_position()?, + destination, + last_position_written_to_file: 0, + }) + } + + #[inline] + pub fn position(&self) -> u32 { + self.section.position + } + + pub fn dump_dir_entry( + &mut self, + buffer: &mut DumpBuf, + dirent: MDRawDirectory, + ) -> std::result::Result<(), FileWriterError> { + self.section.set_value_at(buffer, dirent, self.curr_idx)?; + + // Now write it to file + + // First get all the positions + let curr_file_pos = self.destination.stream_position()?; + let idx_pos = self.section.location_of_index(self.curr_idx); + self.curr_idx += 1; + + self.destination.seek(std::io::SeekFrom::Start( + self.destination_start_offset + idx_pos.rva as u64, + ))?; + let start = idx_pos.rva as usize; + let end = (idx_pos.rva + idx_pos.data_size) as usize; + self.destination.write_all(&buffer[start..end])?; + + // Reset file-position + self.destination + .seek(std::io::SeekFrom::Start(curr_file_pos))?; + + Ok(()) + } + + /// Writes 2 things to file: + /// 1. The given dirent into the dir section in the header (if any is given) + /// 2. Everything in the in-memory buffer that was added since the last call to this function + pub fn write_to_file( + &mut self, + buffer: &mut DumpBuf, + dirent: Option<MDRawDirectory>, + ) -> std::result::Result<(), FileWriterError> { + if let Some(dirent) = dirent { + self.dump_dir_entry(buffer, dirent)?; + } + + let start_pos = self.last_position_written_to_file as usize; + self.destination.write_all(&buffer[start_pos..])?; + self.last_position_written_to_file = buffer.position(); + Ok(()) + } +} diff --git a/third_party/rust/minidump-writer/src/lib.rs b/third_party/rust/minidump-writer/src/lib.rs new file mode 100644 index 0000000000..a76291d0ae --- /dev/null +++ b/third_party/rust/minidump-writer/src/lib.rs @@ -0,0 +1,21 @@ +cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "android"))] { + mod linux; + + pub use linux::*; + } else if #[cfg(target_os = "windows")] { + mod windows; + + pub use windows::*; + } else if #[cfg(target_os = "macos")] { + mod mac; + + pub use mac::*; + } +} + +pub mod minidump_cpu; +pub mod minidump_format; + +pub mod dir_section; +pub mod mem_writer; diff --git a/third_party/rust/minidump-writer/src/linux.rs b/third_party/rust/minidump-writer/src/linux.rs new file mode 100644 index 0000000000..b4c5b21131 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux.rs @@ -0,0 +1,18 @@ +// `WriterError` is large and clippy doesn't like that, but not a huge deal atm +#![allow(clippy::result_large_err)] + +#[cfg(target_os = "android")] +mod android; +pub mod app_memory; +pub(crate) mod auxv_reader; +pub mod crash_context; +mod dso_debug; +mod dumper_cpu_info; +pub mod errors; +pub mod maps_reader; +pub mod minidump_writer; +pub mod ptrace_dumper; +pub(crate) mod sections; +pub mod thread_info; + +pub use maps_reader::LINUX_GATE_LIBRARY_NAME; diff --git a/third_party/rust/minidump-writer/src/linux/android.rs b/third_party/rust/minidump-writer/src/linux/android.rs new file mode 100644 index 0000000000..05e7d4acb9 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/android.rs @@ -0,0 +1,146 @@ +use crate::errors::AndroidError; +use crate::maps_reader::MappingInfo; +use crate::ptrace_dumper::PtraceDumper; +use crate::thread_info::Pid; +use goblin::elf; +#[cfg(target_pointer_width = "32")] +use goblin::elf::dynamic::dyn32::{Dyn, SIZEOF_DYN}; +#[cfg(target_pointer_width = "64")] +use goblin::elf::dynamic::dyn64::{Dyn, SIZEOF_DYN}; +#[cfg(target_pointer_width = "32")] +use goblin::elf::header::header32 as elf_header; +#[cfg(target_pointer_width = "64")] +use goblin::elf::header::header64 as elf_header; +#[cfg(target_pointer_width = "32")] +use goblin::elf::program_header::program_header32::ProgramHeader; +#[cfg(target_pointer_width = "64")] +use goblin::elf::program_header::program_header64::ProgramHeader; +use std::ffi::c_void; + +type Result<T> = std::result::Result<T, AndroidError>; + +// From /usr/include/elf.h of the android SDK +// #define DT_ANDROID_REL (DT_LOOS + 2) +// #define DT_ANDROID_RELSZ (DT_LOOS + 3) +// #define DT_ANDROID_RELA (DT_LOOS + 4) +// #define DT_ANDROID_RELASZ (DT_LOOS + 5) +#[cfg(target_pointer_width = "64")] +const DT_ANDROID_REL: u64 = elf::dynamic::DT_LOOS + 2; +#[cfg(target_pointer_width = "64")] +const DT_ANDROID_RELA: u64 = elf::dynamic::DT_LOOS + 4; +#[cfg(target_pointer_width = "32")] +const DT_ANDROID_REL: u32 = (elf::dynamic::DT_LOOS + 2) as u32; +#[cfg(target_pointer_width = "32")] +const DT_ANDROID_RELA: u32 = (elf::dynamic::DT_LOOS + 4) as u32; + +struct DynVaddresses { + min_vaddr: usize, + dyn_vaddr: usize, + dyn_count: usize, +} + +fn has_android_packed_relocations(pid: Pid, load_bias: usize, vaddrs: DynVaddresses) -> Result<()> { + let dyn_addr = load_bias + vaddrs.dyn_vaddr; + for idx in 0..vaddrs.dyn_count { + let addr = (dyn_addr + SIZEOF_DYN * idx) as *mut c_void; + let dyn_data = PtraceDumper::copy_from_process(pid, addr, SIZEOF_DYN)?; + // TODO: Couldn't find a nice way to use goblin for that, to avoid the unsafe-block + let dyn_obj: Dyn; + unsafe { + dyn_obj = std::mem::transmute::<[u8; SIZEOF_DYN], Dyn>(dyn_data.as_slice().try_into()?); + } + + if dyn_obj.d_tag == DT_ANDROID_REL || dyn_obj.d_tag == DT_ANDROID_RELA { + return Ok(()); + } + } + Err(AndroidError::NoRelFound) +} + +fn get_effective_load_bias(pid: Pid, ehdr: &elf_header::Header, address: usize) -> usize { + let ph = parse_loaded_elf_program_headers(pid, ehdr, address); + // If |min_vaddr| is non-zero and we find Android packed relocation tags, + // return the effective load bias. + + if ph.min_vaddr != 0 { + let load_bias = address - ph.min_vaddr; + if has_android_packed_relocations(pid, load_bias, ph).is_ok() { + return load_bias; + } + } + // Either |min_vaddr| is zero, or it is non-zero but we did not find the + // expected Android packed relocations tags. + address +} + +fn parse_loaded_elf_program_headers( + pid: Pid, + ehdr: &elf_header::Header, + address: usize, +) -> DynVaddresses { + let phdr_addr = address + ehdr.e_phoff as usize; + let mut min_vaddr = usize::MAX; + let mut dyn_vaddr = 0; + let mut dyn_count = 0; + + let phdr_opt = PtraceDumper::copy_from_process( + pid, + phdr_addr as *mut c_void, + elf_header::SIZEOF_EHDR * ehdr.e_phnum as usize, + ); + if let Ok(ph_data) = phdr_opt { + // TODO: The original C code doesn't have error-handling here at all. + // We silently ignore "not parsable" for now, but might bubble it up. + // TODO2: `from_bytes` might panic, `parse()` would return a Result<>, so maybe better + // to switch to that at some point. + for phdr in ProgramHeader::from_bytes(&ph_data, ehdr.e_phnum as usize) { + let p_vaddr = phdr.p_vaddr as usize; + if phdr.p_type == elf::program_header::PT_LOAD && p_vaddr < min_vaddr { + min_vaddr = p_vaddr; + } + + if phdr.p_type == elf::program_header::PT_DYNAMIC { + dyn_vaddr = p_vaddr; + dyn_count = phdr.p_memsz as usize / SIZEOF_DYN; + } + } + } + + DynVaddresses { + min_vaddr, + dyn_vaddr, + dyn_count, + } +} + +pub fn late_process_mappings(pid: Pid, mappings: &mut [MappingInfo]) -> Result<()> { + // Only consider exec mappings that indicate a file path was mapped, and + // where the ELF header indicates a mapped shared library. + for map in mappings + .iter_mut() + .filter(|m| m.is_executable() && m.name_is_path()) + { + let ehdr_opt = PtraceDumper::copy_from_process( + pid, + map.start_address as *mut c_void, + elf_header::SIZEOF_EHDR, + ) + .ok() + .and_then(|x| elf_header::Header::parse(&x).ok()); + + if let Some(ehdr) = ehdr_opt { + if ehdr.e_type == elf_header::ET_DYN { + // Compute the effective load bias for this mapped library, and update + // the mapping to hold that rather than |start_addr|, at the same time + // adjusting |size| to account for the change in |start_addr|. Where + // the library does not contain Android packed relocations, + // GetEffectiveLoadBias() returns |start_addr| and the mapping entry + // is not changed. + let load_bias = get_effective_load_bias(pid, &ehdr, map.start_address); + map.size += map.start_address - load_bias; + map.start_address = load_bias; + } + } + } + Ok(()) +} diff --git a/third_party/rust/minidump-writer/src/linux/app_memory.rs b/third_party/rust/minidump-writer/src/linux/app_memory.rs new file mode 100644 index 0000000000..80e77d5bb0 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/app_memory.rs @@ -0,0 +1,9 @@ +// These entries store a list of memory regions that the client wants included +// in the minidump. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct AppMemory { + pub ptr: usize, + pub length: usize, +} + +pub type AppMemoryList = Vec<AppMemory>; diff --git a/third_party/rust/minidump-writer/src/linux/auxv_reader.rs b/third_party/rust/minidump-writer/src/linux/auxv_reader.rs new file mode 100644 index 0000000000..3ed09e5aeb --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/auxv_reader.rs @@ -0,0 +1,115 @@ +// This file is heavily based on https://bitbucket.org/marshallpierce/rust-auxv +// Thus I'm keeping the original MIT-license copyright here: +// Copyright 2017 Marshall Pierce +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be in…substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +use crate::errors::AuxvReaderError; +use byteorder::{NativeEndian, ReadBytesExt}; +use std::fs::File; +use std::io::{BufReader, Read}; + +pub type Result<T> = std::result::Result<T, AuxvReaderError>; + +/// The type used in auxv keys and values. +#[cfg(target_pointer_width = "32")] +pub type AuxvType = u32; +/// The type used in auxv keys and values. +#[cfg(target_pointer_width = "64")] +pub type AuxvType = u64; + +/// An auxv key-value pair. +#[derive(Debug, PartialEq, Eq)] +pub struct AuxvPair { + pub key: AuxvType, + pub value: AuxvType, +} + +/// An iterator across auxv pairs from procfs. +pub struct ProcfsAuxvIter { + pair_size: usize, + buf: Vec<u8>, + input: BufReader<File>, + keep_going: bool, +} + +impl ProcfsAuxvIter { + pub fn new(input: BufReader<File>) -> Self { + let pair_size = 2 * std::mem::size_of::<AuxvType>(); + let buf: Vec<u8> = Vec::with_capacity(pair_size); + + Self { + pair_size, + buf, + input, + keep_going: true, + } + } +} + +impl Iterator for ProcfsAuxvIter { + type Item = Result<AuxvPair>; + fn next(&mut self) -> Option<Self::Item> { + if !self.keep_going { + return None; + } + // assume something will fail + self.keep_going = false; + + self.buf = vec![0; self.pair_size]; + + let mut read_bytes: usize = 0; + while read_bytes < self.pair_size { + // read exactly buf's len of bytes. + match self.input.read(&mut self.buf[read_bytes..]) { + Ok(n) => { + if n == 0 { + // should not hit EOF before AT_NULL + return Some(Err(AuxvReaderError::InvalidFormat)); + } + + read_bytes += n; + } + Err(x) => return Some(Err(x.into())), + } + } + + let mut reader = &self.buf[..]; + let aux_key = match read_long(&mut reader) { + Ok(x) => x, + Err(x) => return Some(Err(x.into())), + }; + let aux_val = match read_long(&mut reader) { + Ok(x) => x, + Err(x) => return Some(Err(x.into())), + }; + + let at_null; + #[cfg(any(target_arch = "arm", all(target_os = "android", target_arch = "x86")))] + { + at_null = 0; + } + #[cfg(not(any(target_arch = "arm", all(target_os = "android", target_arch = "x86"))))] + { + at_null = libc::AT_NULL; + } + + if aux_key == at_null { + return None; + } + + self.keep_going = true; + Some(Ok(AuxvPair { + key: aux_key, + value: aux_val, + })) + } +} + +fn read_long(reader: &mut dyn Read) -> std::io::Result<AuxvType> { + match std::mem::size_of::<AuxvType>() { + 4 => reader.read_u32::<NativeEndian>().map(|u| u as AuxvType), + 8 => reader.read_u64::<NativeEndian>().map(|u| u as AuxvType), + x => panic!("Unexpected type width: {}", x), + } +} diff --git a/third_party/rust/minidump-writer/src/linux/crash_context.rs b/third_party/rust/minidump-writer/src/linux/crash_context.rs new file mode 100644 index 0000000000..f7a554d110 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/crash_context.rs @@ -0,0 +1,19 @@ +//! Minidump defines register structures which are different from the raw +//! structures which we get from the kernel. These are platform specific +//! functions to juggle the `ucontext_t` and user structures into minidump format. + +pub struct CrashContext { + pub inner: crash_context::CrashContext, +} + +cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + mod x86_64; + } else if #[cfg(target_arch = "x86")] { + mod x86; + } else if #[cfg(target_arch = "aarch64")] { + mod aarch64; + } else if #[cfg(target_arch = "arm")] { + mod arm; + } +} diff --git a/third_party/rust/minidump-writer/src/linux/crash_context/aarch64.rs b/third_party/rust/minidump-writer/src/linux/crash_context/aarch64.rs new file mode 100644 index 0000000000..c53c37720c --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/crash_context/aarch64.rs @@ -0,0 +1,34 @@ +use super::CrashContext; +use crate::{ + minidump_cpu::{RawContextCPU, FP_REG_COUNT, GP_REG_COUNT}, + minidump_format::format, +}; + +impl CrashContext { + pub fn get_instruction_pointer(&self) -> usize { + self.inner.context.uc_mcontext.pc as usize + } + + pub fn get_stack_pointer(&self) -> usize { + self.inner.context.uc_mcontext.sp as usize + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = format::ContextFlagsArm64Old::CONTEXT_ARM64_OLD_FULL.bits() as u64; + + { + let gregs = &self.inner.context.uc_mcontext; + out.cpsr = gregs.pstate as u32; + out.iregs[..GP_REG_COUNT].copy_from_slice(&gregs.regs[..GP_REG_COUNT]); + out.sp = gregs.sp; + out.pc = gregs.pc; + } + + { + let fs = &self.inner.float_state; + out.fpsr = fs.fpsr; + out.fpcr = fs.fpcr; + out.float_regs[..FP_REG_COUNT].copy_from_slice(&fs.vregs[..FP_REG_COUNT]); + } + } +} diff --git a/third_party/rust/minidump-writer/src/linux/crash_context/arm.rs b/third_party/rust/minidump-writer/src/linux/crash_context/arm.rs new file mode 100644 index 0000000000..e4e40216e2 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/crash_context/arm.rs @@ -0,0 +1,47 @@ +use super::CrashContext; +use crate::minidump_cpu::RawContextCPU; + +impl CrashContext { + pub fn get_instruction_pointer(&self) -> usize { + self.inner.context.uc_mcontext.arm_pc as usize + } + + pub fn get_stack_pointer(&self) -> usize { + self.inner.context.uc_mcontext.arm_sp as usize + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = + crate::minidump_format::format::ContextFlagsArm::CONTEXT_ARM_FULL.bits(); + + { + let iregs = &mut out.iregs; + let gregs = &self.inner.context.uc_mcontext; + iregs[0] = gregs.arm_r0; + iregs[1] = gregs.arm_r1; + iregs[2] = gregs.arm_r2; + iregs[3] = gregs.arm_r3; + iregs[4] = gregs.arm_r4; + iregs[5] = gregs.arm_r5; + iregs[6] = gregs.arm_r6; + iregs[7] = gregs.arm_r7; + iregs[8] = gregs.arm_r8; + iregs[9] = gregs.arm_r9; + iregs[10] = gregs.arm_r10; + + iregs[11] = gregs.arm_fp; + iregs[12] = gregs.arm_ip; + iregs[13] = gregs.arm_sp; + iregs[14] = gregs.arm_lr; + iregs[15] = gregs.arm_pc; + + out.cpsr = gregs.arm_cpsr; + } + + // TODO: this todo has been in breakpad for years.... + // TODO: fix this after fixing ExceptionHandler + //out.float_save.fpscr = 0; + //out.float_save.regs = [0; MD_FLOATINGSAVEAREA_ARM_FPR_COUNT]; + //out.float_save.extra = [0; MD_FLOATINGSAVEAREA_ARM_FPEXTRA_COUNT]; + } +} diff --git a/third_party/rust/minidump-writer/src/linux/crash_context/x86.rs b/third_party/rust/minidump-writer/src/linux/crash_context/x86.rs new file mode 100644 index 0000000000..5a2e43eed3 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/crash_context/x86.rs @@ -0,0 +1,62 @@ +use super::CrashContext; +use crate::{minidump_cpu::RawContextCPU, minidump_format::format::ContextFlagsX86}; +use libc::{ + REG_CS, REG_DS, REG_EAX, REG_EBP, REG_EBX, REG_ECX, REG_EDI, REG_EDX, REG_EFL, REG_EIP, REG_ES, + REG_ESI, REG_ESP, REG_FS, REG_GS, REG_SS, REG_UESP, +}; +impl CrashContext { + pub fn get_instruction_pointer(&self) -> usize { + self.inner.context.uc_mcontext.gregs[REG_EIP as usize] as usize + } + + pub fn get_stack_pointer(&self) -> usize { + self.inner.context.uc_mcontext.gregs[REG_ESP as usize] as usize + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = ContextFlagsX86::CONTEXT_X86_FULL.bits() + | ContextFlagsX86::CONTEXT_X86_FLOATING_POINT.bits(); + + { + let gregs = &self.inner.context.uc_mcontext.gregs; + out.gs = gregs[REG_GS as usize] as u32; + out.fs = gregs[REG_FS as usize] as u32; + out.es = gregs[REG_ES as usize] as u32; + out.ds = gregs[REG_DS as usize] as u32; + + out.edi = gregs[REG_EDI as usize] as u32; + out.esi = gregs[REG_ESI as usize] as u32; + out.ebx = gregs[REG_EBX as usize] as u32; + out.edx = gregs[REG_EDX as usize] as u32; + out.ecx = gregs[REG_ECX as usize] as u32; + out.eax = gregs[REG_EAX as usize] as u32; + + out.ebp = gregs[REG_EBP as usize] as u32; + out.eip = gregs[REG_EIP as usize] as u32; + out.cs = gregs[REG_CS as usize] as u32; + out.eflags = gregs[REG_EFL as usize] as u32; + out.esp = gregs[REG_UESP as usize] as u32; + out.ss = gregs[REG_SS as usize] as u32; + } + + { + let fs = &self.inner.float_state; + let out = &mut out.float_save; + out.control_word = fs.cw; + out.status_word = fs.sw; + out.tag_word = fs.tag; + out.error_offset = fs.ipoff; + out.error_selector = fs.cssel; + out.data_offset = fs.dataoff; + out.data_selector = fs.datasel; + + debug_assert_eq!(fs._st.len() * std::mem::size_of::<libc::_libc_fpreg>(), 80); + out.register_area.copy_from_slice(unsafe { + std::slice::from_raw_parts( + fs._st.as_ptr().cast(), + fs._st.len() * std::mem::size_of::<libc::_libc_fpreg>(), + ) + }); + } + } +} diff --git a/third_party/rust/minidump-writer/src/linux/crash_context/x86_64.rs b/third_party/rust/minidump-writer/src/linux/crash_context/x86_64.rs new file mode 100644 index 0000000000..e559692679 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/crash_context/x86_64.rs @@ -0,0 +1,78 @@ +use super::CrashContext; +use crate::{ + minidump_cpu::RawContextCPU, minidump_format::format, thread_info::copy_u32_registers, +}; +use libc::{ + REG_CSGSFS, REG_EFL, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15, REG_R8, REG_R9, + REG_RAX, REG_RBP, REG_RBX, REG_RCX, REG_RDI, REG_RDX, REG_RIP, REG_RSI, REG_RSP, +}; +use scroll::Pwrite; + +impl CrashContext { + pub fn get_instruction_pointer(&self) -> usize { + self.inner.context.uc_mcontext.gregs[REG_RIP as usize] as usize + } + + pub fn get_stack_pointer(&self) -> usize { + self.inner.context.uc_mcontext.gregs[REG_RSP as usize] as usize + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = format::ContextFlagsAmd64::CONTEXT_AMD64_FULL.bits(); + + { + let gregs = &self.inner.context.uc_mcontext.gregs; + out.cs = (gregs[REG_CSGSFS as usize] & 0xffff) as u16; + + out.fs = ((gregs[REG_CSGSFS as usize] >> 32) & 0xffff) as u16; + out.gs = ((gregs[REG_CSGSFS as usize] >> 16) & 0xffff) as u16; + + out.eflags = gregs[REG_EFL as usize] as u32; + + out.rax = gregs[REG_RAX as usize] as u64; + out.rcx = gregs[REG_RCX as usize] as u64; + out.rdx = gregs[REG_RDX as usize] as u64; + out.rbx = gregs[REG_RBX as usize] as u64; + + out.rsp = gregs[REG_RSP as usize] as u64; + out.rbp = gregs[REG_RBP as usize] as u64; + out.rsi = gregs[REG_RSI as usize] as u64; + out.rdi = gregs[REG_RDI as usize] as u64; + out.r8 = gregs[REG_R8 as usize] as u64; + out.r9 = gregs[REG_R9 as usize] as u64; + out.r10 = gregs[REG_R10 as usize] as u64; + out.r11 = gregs[REG_R11 as usize] as u64; + out.r12 = gregs[REG_R12 as usize] as u64; + out.r13 = gregs[REG_R13 as usize] as u64; + out.r14 = gregs[REG_R14 as usize] as u64; + out.r15 = gregs[REG_R15 as usize] as u64; + + out.rip = gregs[REG_RIP as usize] as u64; + } + + { + let fs = &self.inner.float_state; + + let mut float_save = format::XMM_SAVE_AREA32 { + control_word: fs.cwd, + status_word: fs.swd, + tag_word: fs.ftw as u8, + error_opcode: fs.fop, + error_offset: fs.rip as u32, + data_offset: fs.rdp as u32, + error_selector: 0, // We don't have this. + data_selector: 0, // We don't have this. + mx_csr: fs.mxcsr, + mx_csr_mask: fs.mxcr_mask, + ..Default::default() + }; + + copy_u32_registers(&mut float_save.float_registers, &fs.st_space); + copy_u32_registers(&mut float_save.xmm_registers, &fs.xmm_space); + + out.float_save + .pwrite_with(float_save, 0, scroll::Endian::Little) + .expect("this is impossible"); + } + } +} diff --git a/third_party/rust/minidump-writer/src/linux/dso_debug.rs b/third_party/rust/minidump-writer/src/linux/dso_debug.rs new file mode 100644 index 0000000000..b9f466261f --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/dso_debug.rs @@ -0,0 +1,273 @@ +use crate::{ + linux::{auxv_reader::AuxvType, errors::SectionDsoDebugError, ptrace_dumper::PtraceDumper}, + mem_writer::{write_string_to_location, Buffer, MemoryArrayWriter, MemoryWriter}, + minidump_format::*, +}; +use std::collections::HashMap; + +type Result<T> = std::result::Result<T, SectionDsoDebugError>; + +cfg_if::cfg_if! { + if #[cfg(target_pointer_width = "32")] { + use goblin::elf::program_header::program_header32::SIZEOF_PHDR; + } else if #[cfg(target_pointer_width = "64")] { + use goblin::elf::program_header::program_header64::SIZEOF_PHDR; + } +} + +cfg_if::cfg_if! { + if #[cfg(all(target_pointer_width = "64", target_arch = "arm"))] { + type ElfAddr = u64; + } else if #[cfg(all(target_pointer_width = "64", not(target_arch = "arm")))] { + type ElfAddr = libc::Elf64_Addr; + } else if #[cfg(all(target_pointer_width = "32", target_arch = "arm"))] { + type ElfAddr = u32; + } else if #[cfg(all(target_pointer_width = "32", not(target_arch = "arm")))] { + type ElfAddr = libc::Elf32_Addr; + } +} + +// COPY from <link.h> +#[derive(Debug, Clone, Default)] +#[repr(C)] +pub struct LinkMap { + /* These first few members are part of the protocol with the debugger. + This is the same format used in SVR4. */ + l_addr: ElfAddr, /* Difference between the address in the ELF + file and the addresses in memory. */ + l_name: usize, /* Absolute file name object was found in. WAS: `char*` */ + l_ld: usize, /* Dynamic section of the shared object. WAS: `ElfW(Dyn) *` */ + l_next: usize, /* Chain of loaded objects. WAS: `struct link_map *` */ + l_prev: usize, /* Chain of loaded objects. WAS: `struct link_map *` */ +} + +// COPY from <link.h> +/// This state value describes the mapping change taking place when +/// the `r_brk' address is called. +#[derive(Debug, Clone, Default)] +#[allow(non_camel_case_types, unused)] +#[repr(C)] +enum RState { + /// Mapping change is complete. + #[default] + RT_CONSISTENT, + /// Beginning to add a new object. + RT_ADD, + /// Beginning to remove an object mapping. + RT_DELETE, +} + +// COPY from <link.h> +#[derive(Debug, Clone, Default)] +#[repr(C)] +pub struct RDebug { + r_version: libc::c_int, /* Version number for this protocol. */ + r_map: usize, /* Head of the chain of loaded objects. WAS: `struct link_map *` */ + + /* This is the address of a function internal to the run-time linker, + that will always be called when the linker begins to map in a + library or unmap it, and again when the mapping change is complete. + The debugger can set a breakpoint at this address if it wants to + notice shared object mapping changes. */ + r_brk: ElfAddr, + r_state: RState, + r_ldbase: ElfAddr, /* Base address the linker is loaded at. */ +} + +pub fn write_dso_debug_stream( + buffer: &mut Buffer, + blamed_thread: i32, + auxv: &HashMap<AuxvType, AuxvType>, +) -> Result<MDRawDirectory> { + let at_phnum; + let at_phdr; + #[cfg(any(target_arch = "arm", all(target_os = "android", target_arch = "x86")))] + { + at_phdr = 3; + at_phnum = 5; + } + #[cfg(not(any(target_arch = "arm", all(target_os = "android", target_arch = "x86"))))] + { + at_phdr = libc::AT_PHDR; + at_phnum = libc::AT_PHNUM; + } + let phnum_max = *auxv + .get(&at_phnum) + .ok_or(SectionDsoDebugError::CouldNotFind("AT_PHNUM in auxv"))? + as usize; + let phdr = *auxv + .get(&at_phdr) + .ok_or(SectionDsoDebugError::CouldNotFind("AT_PHDR in auxv"))? as usize; + + let ph = PtraceDumper::copy_from_process( + blamed_thread, + phdr as *mut libc::c_void, + SIZEOF_PHDR * phnum_max, + )?; + let program_headers; + #[cfg(target_pointer_width = "64")] + { + program_headers = goblin::elf::program_header::program_header64::ProgramHeader::from_bytes( + &ph, phnum_max, + ); + } + #[cfg(target_pointer_width = "32")] + { + program_headers = goblin::elf::program_header::program_header32::ProgramHeader::from_bytes( + &ph, phnum_max, + ); + }; + + // Assume the program base is at the beginning of the same page as the PHDR + let mut base = phdr & !0xfff; + let mut dyn_addr = 0; + // Search for the program PT_DYNAMIC segment + for ph in program_headers { + // Adjust base address with the virtual address of the PT_LOAD segment + // corresponding to offset 0 + if ph.p_type == goblin::elf::program_header::PT_LOAD && ph.p_offset == 0 { + base -= ph.p_vaddr as usize; + } + if ph.p_type == goblin::elf::program_header::PT_DYNAMIC { + dyn_addr = ph.p_vaddr; + } + } + + if dyn_addr == 0 { + return Err(SectionDsoDebugError::CouldNotFind( + "dyn_addr in program headers", + )); + } + + dyn_addr += base as ElfAddr; + + let dyn_size = std::mem::size_of::<goblin::elf::Dyn>(); + let mut r_debug = 0usize; + let mut dynamic_length = 0usize; + + // The dynamic linker makes information available that helps gdb find all + // DSOs loaded into the program. If this information is indeed available, + // dump it to a MD_LINUX_DSO_DEBUG stream. + loop { + let dyn_data = PtraceDumper::copy_from_process( + blamed_thread, + (dyn_addr as usize + dynamic_length) as *mut libc::c_void, + dyn_size, + )?; + dynamic_length += dyn_size; + + // goblin::elf::Dyn doesn't have padding bytes + let (head, body, _tail) = unsafe { dyn_data.align_to::<goblin::elf::Dyn>() }; + assert!(head.is_empty(), "Data was not aligned"); + let dyn_struct = &body[0]; + + // #ifdef __mips__ + // const int32_t debug_tag = DT_MIPS_RLD_MAP; + // #else + // const int32_t debug_tag = DT_DEBUG; + // #endif + let debug_tag = goblin::elf::dynamic::DT_DEBUG; + if dyn_struct.d_tag == debug_tag { + r_debug = dyn_struct.d_val as usize; + } else if dyn_struct.d_tag == goblin::elf::dynamic::DT_NULL { + break; + } + } + + // The "r_map" field of that r_debug struct contains a linked list of all + // loaded DSOs. + // Our list of DSOs potentially is different from the ones in the crashing + // process. So, we have to be careful to never dereference pointers + // directly. Instead, we use CopyFromProcess() everywhere. + // See <link.h> for a more detailed discussion of the how the dynamic + // loader communicates with debuggers. + + let debug_entry_data = PtraceDumper::copy_from_process( + blamed_thread, + r_debug as *mut libc::c_void, + std::mem::size_of::<RDebug>(), + )?; + + // goblin::elf::Dyn doesn't have padding bytes + let (head, body, _tail) = unsafe { debug_entry_data.align_to::<RDebug>() }; + assert!(head.is_empty(), "Data was not aligned"); + let debug_entry = &body[0]; + + // Count the number of loaded DSOs + let mut dso_vec = Vec::new(); + let mut curr_map = debug_entry.r_map; + while curr_map != 0 { + let link_map_data = PtraceDumper::copy_from_process( + blamed_thread, + curr_map as *mut libc::c_void, + std::mem::size_of::<LinkMap>(), + )?; + + // LinkMap is repr(C) and doesn't have padding bytes, so this should be safe + let (head, body, _tail) = unsafe { link_map_data.align_to::<LinkMap>() }; + assert!(head.is_empty(), "Data was not aligned"); + let map = &body[0]; + + curr_map = map.l_next; + dso_vec.push(map.clone()); + } + + let mut linkmap_rva = u32::MAX; + if !dso_vec.is_empty() { + // If we have at least one DSO, create an array of MDRawLinkMap + // entries in the minidump file. + let mut linkmap = MemoryArrayWriter::<MDRawLinkMap>::alloc_array(buffer, dso_vec.len())?; + linkmap_rva = linkmap.location().rva; + + // Iterate over DSOs and write their information to mini dump + for (idx, map) in dso_vec.iter().enumerate() { + let mut filename = String::new(); + if map.l_name > 0 { + let filename_data = PtraceDumper::copy_from_process( + blamed_thread, + map.l_name as *mut libc::c_void, + 256, + )?; + + // C - string is NULL-terminated + if let Some(name) = filename_data.splitn(2, |x| *x == b'\0').next() { + filename = String::from_utf8(name.to_vec())?; + } + } + let location = write_string_to_location(buffer, &filename)?; + let entry = MDRawLinkMap { + addr: map.l_addr, + name: location.rva, + ld: map.l_ld as ElfAddr, + }; + + linkmap.set_value_at(buffer, entry, idx)?; + } + } + + // Write MD_LINUX_DSO_DEBUG record + let debug = MDRawDebug { + version: debug_entry.r_version as u32, + map: linkmap_rva, + dso_count: dso_vec.len() as u32, + brk: debug_entry.r_brk, + ldbase: debug_entry.r_ldbase, + dynamic: dyn_addr, + }; + let debug_loc = MemoryWriter::<MDRawDebug>::alloc_with_val(buffer, debug)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::LinuxDsoDebug as u32, + location: debug_loc.location(), + }; + + dirent.location.data_size += dynamic_length as u32; + let dso_debug_data = PtraceDumper::copy_from_process( + blamed_thread, + dyn_addr as *mut libc::c_void, + dynamic_length, + )?; + MemoryArrayWriter::write_bytes(buffer, &dso_debug_data); + + Ok(dirent) +} diff --git a/third_party/rust/minidump-writer/src/linux/dumper_cpu_info.rs b/third_party/rust/minidump-writer/src/linux/dumper_cpu_info.rs new file mode 100644 index 0000000000..72da20fe30 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/dumper_cpu_info.rs @@ -0,0 +1,72 @@ +cfg_if::cfg_if! { + if #[cfg(any( + target_arch = "x86_64", + target_arch = "x86", + target_arch = "mips", + target_arch = "mips64" + ))] + { + pub mod x86_mips; + pub use x86_mips as imp; + } else if #[cfg(any( + target_arch = "arm", + target_arch = "aarch64", + ))] + { + pub mod arm; + pub use arm as imp; + } +} + +pub use imp::write_cpu_information; + +use crate::minidump_format::PlatformId; +use nix::sys::utsname::uname; + +/// Retrieves the [`MDOSPlatform`] and synthesized version information +pub fn os_information() -> (PlatformId, String) { + let platform_id = if cfg!(target_os = "android") { + PlatformId::Android + } else { + PlatformId::Linux + }; + + // This is quite unfortunate, but the primary reason that uname could fail + // would be if it failed to fill out the nodename (hostname) field, even + // though we don't care about that particular field at all + let info = uname().map_or_else( + |_e| { + let os = if platform_id == PlatformId::Linux { + "Linux" + } else { + "Android" + }; + + let machine = if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "x86") { + "x86" + } else if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "arm") { + "arm" + } else { + "<unknown>" + }; + + // TODO: Fallback to other sources of information, eg /etc/os-release + format!("{os} <unknown> <unknown> {machine}") + }, + |info| { + format!( + "{} {} {} {}", + info.sysname().to_str().unwrap_or("<unknown>"), + info.release().to_str().unwrap_or("<unknown>"), + info.version().to_str().unwrap_or("<unknown>"), + info.machine().to_str().unwrap_or("<unknown>"), + ) + }, + ); + + (platform_id, info) +} diff --git a/third_party/rust/minidump-writer/src/linux/dumper_cpu_info/arm.rs b/third_party/rust/minidump-writer/src/linux/dumper_cpu_info/arm.rs new file mode 100644 index 0000000000..886920f560 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/dumper_cpu_info/arm.rs @@ -0,0 +1,404 @@ +use crate::{errors::CpuInfoError, minidump_format::*}; +use scroll::Pwrite; +use std::{ + collections::HashSet, + fs::File, + io::{BufRead, BufReader, Read}, + path, +}; + +type Result<T> = std::result::Result<T, CpuInfoError>; + +pub fn parse_cpus_from_sysfile(file: &mut File) -> Result<HashSet<u32>> { + let mut res = HashSet::new(); + let mut content = String::new(); + file.read_to_string(&mut content)?; + // Expected format: comma-separated list of items, where each + // item can be a decimal integer, or two decimal integers separated + // by a dash. + // E.g.: + // 0 + // 0,1,2,3 + // 0-3 + // 1,10-23 + for items in content.split(',') { + let items = items.trim(); + if items.is_empty() { + continue; + } + let cores: std::result::Result<Vec<_>, _> = + items.split('-').map(|x| x.parse::<u32>()).collect(); + let cores = cores?; + match cores.as_slice() { + [x] => { + res.insert(*x); + } + [x, y] => { + for core in *x..=*y { + res.insert(core); + } + } + _ => { + return Err(CpuInfoError::UnparsableCores(format!("{:?}", cores))); + } + } + } + Ok(res) +} + +struct CpuInfoEntry { + field: &'static str, + format: char, + bit_lshift: u8, + bit_length: u8, +} + +impl CpuInfoEntry { + fn new(field: &'static str, format: char, bit_lshift: u8, bit_length: u8) -> Self { + CpuInfoEntry { + field, + format, + bit_lshift, + bit_length, + } + } +} + +/// Retrieves the hardware capabilities from the 'Features' field. +#[cfg(target_arch = "arm")] +fn parse_features(val: &str) -> u32 { + struct CpuFeaturesEntry { + tag: &'static str, + hwcaps: u32, + } + + impl CpuFeaturesEntry { + fn new(tag: &'static str, hwcaps: u32) -> Self { + CpuFeaturesEntry { tag, hwcaps } + } + } + + // The ELF hwcaps are listed in the "Features" entry as textual tags. + // This table is used to rebuild them. + let cpu_features_entries = [ + CpuFeaturesEntry::new("swp", MDCPUInformationARMElfHwCaps::HWCAP_SWP.bits()), + CpuFeaturesEntry::new("half", MDCPUInformationARMElfHwCaps::HWCAP_HALF.bits()), + CpuFeaturesEntry::new("thumb", MDCPUInformationARMElfHwCaps::HWCAP_THUMB.bits()), + CpuFeaturesEntry::new("bit26", MDCPUInformationARMElfHwCaps::HWCAP_26BIT.bits()), + CpuFeaturesEntry::new( + "fastmult", + MDCPUInformationARMElfHwCaps::HWCAP_FAST_MULT.bits(), + ), + CpuFeaturesEntry::new("fpa", MDCPUInformationARMElfHwCaps::HWCAP_FPA.bits()), + CpuFeaturesEntry::new("vfp", MDCPUInformationARMElfHwCaps::HWCAP_VFP.bits()), + CpuFeaturesEntry::new("edsp", MDCPUInformationARMElfHwCaps::HWCAP_EDSP.bits()), + CpuFeaturesEntry::new("java", MDCPUInformationARMElfHwCaps::HWCAP_JAVA.bits()), + CpuFeaturesEntry::new("iwmmxt", MDCPUInformationARMElfHwCaps::HWCAP_IWMMXT.bits()), + CpuFeaturesEntry::new("crunch", MDCPUInformationARMElfHwCaps::HWCAP_CRUNCH.bits()), + CpuFeaturesEntry::new( + "thumbee", + MDCPUInformationARMElfHwCaps::HWCAP_THUMBEE.bits(), + ), + CpuFeaturesEntry::new("neon", MDCPUInformationARMElfHwCaps::HWCAP_NEON.bits()), + CpuFeaturesEntry::new("vfpv3", MDCPUInformationARMElfHwCaps::HWCAP_VFPv3.bits()), + CpuFeaturesEntry::new( + "vfpv3d16", + MDCPUInformationARMElfHwCaps::HWCAP_VFPv3D16.bits(), + ), + CpuFeaturesEntry::new("tls", MDCPUInformationARMElfHwCaps::HWCAP_TLS.bits()), + CpuFeaturesEntry::new("vfpv4", MDCPUInformationARMElfHwCaps::HWCAP_VFPv4.bits()), + CpuFeaturesEntry::new("idiva", MDCPUInformationARMElfHwCaps::HWCAP_IDIVA.bits()), + CpuFeaturesEntry::new("idivt", MDCPUInformationARMElfHwCaps::HWCAP_IDIVT.bits()), + CpuFeaturesEntry::new("idiv", MDCPUInformationARMElfHwCaps::HWCAP_IDIV.bits()), + ]; + + let mut ehwc = 0; + // Parse each space-separated tag. + for tag in val.split_whitespace() { + for entry in &cpu_features_entries { + if entry.tag == tag { + ehwc |= entry.hwcaps; + break; + } + } + } + + ehwc +} + +/// Stub for aarch64, always 0 +#[cfg(target_arch = "aarch64")] +fn parse_features(_val: &str) -> u32 { + 0 +} + +pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { + // The CPUID value is broken up in several entries in /proc/cpuinfo. + // This table is used to rebuild it from the entries. + let cpu_id_entries = [ + CpuInfoEntry::new("CPU implementer", 'x', 24, 8), + CpuInfoEntry::new("CPU variant", 'x', 20, 4), + CpuInfoEntry::new("CPU part", 'x', 4, 12), + CpuInfoEntry::new("CPU revision", 'd', 0, 4), + ]; + + // processor_architecture should always be set, do this first + if cfg!(target_arch = "aarch64") { + sys_info.processor_architecture = + MDCPUArchitecture::PROCESSOR_ARCHITECTURE_ARM64_OLD as u16; + } else { + sys_info.processor_architecture = MDCPUArchitecture::PROCESSOR_ARCHITECTURE_ARM as u16; + } + + // /proc/cpuinfo is not readable under various sandboxed environments + // (e.g. Android services with the android:isolatedProcess attribute) + // prepare for this by setting default values now, which will be + // returned when this happens. + // + // Note: Bogus values are used to distinguish between failures (to + // read /sys and /proc files) and really badly configured kernels. + sys_info.number_of_processors = 0; + sys_info.processor_level = 1; // There is no ARMv1 + sys_info.processor_revision = 42; + + // Counting the number of CPUs involves parsing two sysfs files, + // because the content of /proc/cpuinfo will only mirror the number + // of 'online' cores, and thus will vary with time. + // See http://www.kernel.org/doc/Documentation/cputopology.txt + if let Ok(mut present_file) = File::open("/sys/devices/system/cpu/present") { + // Ignore unparsable content + let cpus_present = parse_cpus_from_sysfile(&mut present_file).unwrap_or_default(); + + if let Ok(mut possible_file) = File::open("/sys/devices/system/cpu/possible") { + // Ignore unparsable content + let cpus_possible = parse_cpus_from_sysfile(&mut possible_file).unwrap_or_default(); + let intersection = cpus_present.intersection(&cpus_possible).count(); + let cpu_count = std::cmp::min(255, intersection) as u8; + sys_info.number_of_processors = cpu_count; + } + } + + // Parse /proc/cpuinfo to reconstruct the CPUID value, as well + // as the ELF hwcaps field. For the latter, it would be easier to + // read /proc/self/auxv but unfortunately, this file is not always + // readable from regular Android applications on later versions + // (>= 4.1) of the Android platform. + + let cpuinfo_file = match File::open(path::PathBuf::from("/proc/cpuinfo")) { + Ok(x) => x, + Err(_) => { + // Do not return Error here to allow the minidump generation + // to happen properly. + return Ok(()); + } + }; + + let mut cpuid = 0; + let mut elf_hwcaps = 0; + + for line in BufReader::new(cpuinfo_file).lines() { + let line = line?; + // Expected format: <field-name> <space>+ ':' <space> <value> + // Note that: + // - empty lines happen. + // - <field-name> can contain spaces. + // - some fields have an empty <value> + if line.trim().is_empty() { + continue; + } + + let (field, value) = if let Some(ind) = line.find(':') { + (&line[..ind], Some(&line[ind + 1..])) + } else { + (line.as_str(), None) + }; + + if let Some(val) = value { + for entry in &cpu_id_entries { + if field != entry.field { + continue; + } + + let rr = if val.starts_with("0x") || entry.format == 'x' { + usize::from_str_radix(val.trim_start_matches("0x"), 16) + } else { + val.parse() + }; + + if let Ok(mut result) = rr { + result &= (1 << entry.bit_length) - 1; + result <<= entry.bit_lshift; + cpuid |= result as u32; + } + } + } + + if cfg!(target_arch = "arm") { + // Get the architecture version from the "Processor" field. + // Note that it is also available in the "CPU architecture" field, + // however, some existing kernels are misconfigured and will report + // invalid values here (e.g. 6, while the CPU is ARMv7-A based). + // The "Processor" field doesn't have this issue. + if field == "Processor" { + // Expected format: <text> (v<level><endian>) + // Where <text> is some text like "ARMv7 Processor rev 2" + // and <level> is a decimal corresponding to the ARM + // architecture number. <endian> is either 'l' or 'b' + // and corresponds to the endianess, it is ignored here. + if let Some(val) = value.and_then(|v| v.split_whitespace().last()) { + // val is now something like "(v7l)" + sys_info.processor_level = val[2..val.len() - 2].parse::<u16>().unwrap_or(5); + } + } + } else { + // aarch64 + // The aarch64 architecture does not provide the architecture level + // in the Processor field, so we instead check the "CPU architecture" + // field. + if field == "CPU architecture" { + sys_info.processor_level = match value.and_then(|v| v.parse::<u16>().ok()) { + Some(v) => v, + None => { + continue; + } + }; + } + } + + // Rebuild the ELF hwcaps from the 'Features' field. + if field == "Features" { + if let Some(val) = value { + elf_hwcaps = parse_features(val); + } + } + } + + // The sys_info.cpu field is just a byte array, but in arm's case it is + // actually + // minidump_common::format::ARMCpuInfo { + // pub cpuid: u32, + // pub elf_hwcaps: u32, + // } + sys_info + .cpu + .data + .pwrite_with(cpuid, 0, scroll::Endian::Little) + .expect("impossible"); + sys_info + .cpu + .data + .pwrite_with( + elf_hwcaps, + std::mem::size_of::<u32>(), + scroll::Endian::Little, + ) + .expect("impossible"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + // In tests we can have access to std + extern crate std; + use std::io::Write; + + fn new_file(content: &str) -> File { + let mut file = tempfile::Builder::new() + .prefix("cpu_sets") + .tempfile() + .unwrap(); + write!(file, "{}", content).unwrap(); + std::fs::File::open(file).unwrap() + } + + #[test] + fn test_empty_count() { + let mut file = new_file(""); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to parse empty file"); + assert_eq!(set.len(), 0); + } + + #[test] + fn test_one_cpu() { + let mut file = new_file("10"); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to file"); + assert_eq!(set, [10,].iter().copied().collect()); + } + + #[test] + fn test_one_cpu_newline() { + let mut file = new_file("10\n"); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to file"); + assert_eq!(set, [10,].iter().copied().collect()); + } + + #[test] + fn test_two_cpus() { + let mut file = new_file("1,10\n"); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to file"); + assert_eq!(set, [1, 10].iter().copied().collect()); + } + + #[test] + fn test_two_cpus_with_range() { + let mut file = new_file("1-2\n"); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to file"); + assert_eq!(set, [1, 2].iter().copied().collect()); + } + + #[test] + fn test_ten_cpus_with_range() { + let mut file = new_file("9-18\n"); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to file"); + assert_eq!(set, (9..=18).collect()); + } + + #[test] + fn test_multiple_items() { + let mut file = new_file("0, 2-4, 128\n"); + let set = parse_cpus_from_sysfile(&mut file).expect("Failed to file"); + assert_eq!(set, [0, 2, 3, 4, 128].iter().copied().collect()); + } + + #[test] + fn test_intersects_with() { + let mut file1 = new_file("9-19\n"); + let mut set1 = parse_cpus_from_sysfile(&mut file1).expect("Failed to file"); + assert_eq!(set1, (9..=19).collect()); + + let mut file2 = new_file("16-24\n"); + let set2 = parse_cpus_from_sysfile(&mut file2).expect("Failed to file"); + assert_eq!(set2, (16..=24).collect()); + + set1 = set1.intersection(&set2).copied().collect(); + assert_eq!(set1, (16..=19).collect()); + } + + #[test] + fn test_intersects_with_discontinuous() { + let mut file1 = new_file("0, 2-4, 7, 10\n"); + let mut set1 = parse_cpus_from_sysfile(&mut file1).expect("Failed to file"); + assert_eq!(set1, [0, 2, 3, 4, 7, 10].iter().copied().collect()); + + let mut file2 = new_file("0-2, 5, 8-10\n"); + let set2 = parse_cpus_from_sysfile(&mut file2).expect("Failed to file"); + assert_eq!(set2, [0, 1, 2, 5, 8, 9, 10].iter().copied().collect()); + + set1 = set1.intersection(&set2).copied().collect(); + assert_eq!(set1, [0, 2, 10].iter().copied().collect()); + } + + #[test] + fn test_bad_input() { + let mut file = new_file("abc\n"); + let _set = parse_cpus_from_sysfile(&mut file).expect_err("Did not fail to parse"); + } + + #[test] + fn test_bad_input_range() { + let mut file = new_file("1-abc\n"); + let _set = parse_cpus_from_sysfile(&mut file).expect_err("Did not fail to parse"); + } +} diff --git a/third_party/rust/minidump-writer/src/linux/dumper_cpu_info/x86_mips.rs b/third_party/rust/minidump-writer/src/linux/dumper_cpu_info/x86_mips.rs new file mode 100644 index 0000000000..cefba4fd25 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/dumper_cpu_info/x86_mips.rs @@ -0,0 +1,115 @@ +use crate::errors::CpuInfoError; +use crate::minidump_format::*; +use std::io::{BufRead, BufReader}; +use std::path; + +type Result<T> = std::result::Result<T, CpuInfoError>; + +struct CpuInfoEntry { + info_name: &'static str, + value: i32, + found: bool, +} + +impl CpuInfoEntry { + fn new(info_name: &'static str, value: i32, found: bool) -> Self { + CpuInfoEntry { + info_name, + value, + found, + } + } +} + +pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { + let vendor_id_name = "vendor_id"; + let mut cpu_info_table = [ + CpuInfoEntry::new("processor", -1, false), + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] + CpuInfoEntry::new("model", 0, false), + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] + CpuInfoEntry::new("stepping", 0, false), + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] + CpuInfoEntry::new("cpu family", 0, false), + ]; + + // processor_architecture should always be set, do this first + sys_info.processor_architecture = if cfg!(target_arch = "mips") { + MDCPUArchitecture::PROCESSOR_ARCHITECTURE_MIPS + } else if cfg!(target_arch = "mips64") { + MDCPUArchitecture::PROCESSOR_ARCHITECTURE_MIPS64 + } else if cfg!(target_arch = "x86") { + MDCPUArchitecture::PROCESSOR_ARCHITECTURE_INTEL + } else { + MDCPUArchitecture::PROCESSOR_ARCHITECTURE_AMD64 + } as u16; + + let cpuinfo_file = std::fs::File::open(path::PathBuf::from("/proc/cpuinfo"))?; + + let mut vendor_id = String::new(); + for line in BufReader::new(cpuinfo_file).lines() { + let line = line?; + // Expected format: <field-name> <space>+ ':' <space> <value> + // Note that: + // - empty lines happen. + // - <field-name> can contain spaces. + // - some fields have an empty <value> + if line.trim().is_empty() { + continue; + } + + let mut liter = line.split(':').map(|x| x.trim()); + let field = liter.next().unwrap(); // guaranteed to have at least one item + let value = if let Some(val) = liter.next() { + val + } else { + continue; + }; + + let mut is_first_entry = true; + for entry in cpu_info_table.iter_mut() { + if !is_first_entry && entry.found { + // except for the 'processor' field, ignore repeated values. + continue; + } + is_first_entry = false; + if field == entry.info_name { + if let Ok(v) = value.parse() { + entry.value = v; + entry.found = true; + } else { + continue; + } + } + + // special case for vendor_id + if field == vendor_id_name && !value.is_empty() { + vendor_id = value.to_owned(); + } + } + } + // make sure we got everything we wanted + if !cpu_info_table.iter().all(|x| x.found) { + return Err(CpuInfoError::NotAllProcEntriesFound); + } + // cpu_info_table[0] holds the last cpu id listed in /proc/cpuinfo, + // assuming this is the highest id, change it to the number of CPUs + // by adding one. + cpu_info_table[0].value += 1; + + sys_info.number_of_processors = cpu_info_table[0].value as u8; // TODO: might not work on special machines with LOTS of CPUs + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] + { + sys_info.processor_level = cpu_info_table[3].value as u16; + sys_info.processor_revision = + (cpu_info_table[1].value << 8 | cpu_info_table[2].value) as u16; + } + if !vendor_id.is_empty() { + let vendor_id = vendor_id.as_bytes(); + // The vendor_id is the first 12 (3 * size_of::<u32>()) bytes + let vendor_len = std::cmp::min(3 * std::mem::size_of::<u32>(), vendor_id.len()); + sys_info.cpu.data[..vendor_len].copy_from_slice(&vendor_id[..vendor_len]); + } + + Ok(()) +} diff --git a/third_party/rust/minidump-writer/src/linux/errors.rs b/third_party/rust/minidump-writer/src/linux/errors.rs new file mode 100644 index 0000000000..b666fefa2b --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/errors.rs @@ -0,0 +1,253 @@ +use crate::dir_section::FileWriterError; +use crate::maps_reader::MappingInfo; +use crate::mem_writer::MemoryWriterError; +use crate::thread_info::Pid; +use goblin; +use nix::errno::Errno; +use std::ffi::OsString; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum InitError { + #[error("IO error for file {0}")] + IOError(String, #[source] std::io::Error), + #[error("No auxv entry found for PID {0}")] + NoAuxvEntryFound(Pid), + #[error("crash thread does not reference principal mapping")] + PrincipalMappingNotReferenced, + #[error("Failed Android specific late init")] + AndroidLateInitError(#[from] AndroidError), + #[error("Failed to read the page size")] + PageSizeError(#[from] Errno), +} + +#[derive(Error, Debug)] +pub enum MapsReaderError { + // parse_from_line() + #[error("Map entry malformed: No {0} found")] + MapEntryMalformed(&'static str), + #[error("Couldn't parse address")] + UnparsableInteger(#[from] std::num::ParseIntError), + #[error("Linux gate location doesn't fit in the required integer type")] + LinuxGateNotConvertable(#[from] std::num::TryFromIntError), + + // get_mmap() + #[error("Not safe to open mapping {}", .0.to_string_lossy())] + NotSafeToOpenMapping(OsString), + #[error("IO Error")] + FileError(#[from] std::io::Error), + #[error("Mmapped file empty or not an ELF file")] + MmapSanityCheckFailed, + #[error("Symlink does not match ({0} vs. {1})")] + SymlinkError(std::path::PathBuf, std::path::PathBuf), + + // fixup_deleted_file() + #[error("Couldn't parse as ELF file")] + ELFParsingFailed(#[from] goblin::error::Error), + #[error("An anonymous mapping has no associated file")] + AnonymousMapping, + #[error("No soname found (filename: {})", .0.to_string_lossy())] + NoSoName(OsString), +} + +#[derive(Debug, Error)] +pub enum AuxvReaderError { + #[error("Invalid auxv format (should not hit EOF before AT_NULL)")] + InvalidFormat, + #[error("IO Error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +pub enum CpuInfoError { + #[error("IO error for file /proc/cpuinfo")] + IOError(#[from] std::io::Error), + #[error("Not all entries of /proc/cpuinfo found!")] + NotAllProcEntriesFound, + #[error("Couldn't parse core from file")] + UnparsableInteger(#[from] std::num::ParseIntError), + #[error("Couldn't parse cores: {0}")] + UnparsableCores(String), +} + +#[derive(Error, Debug)] +pub enum ThreadInfoError { + #[error("Index out of bounds: Got {0}, only have {1}")] + IndexOutOfBounds(usize, usize), + #[error("Either ppid ({1}) or tgid ({2}) not found in {0}")] + InvalidPid(String, Pid, Pid), + #[error("IO error")] + IOError(#[from] std::io::Error), + #[error("Couldn't parse address")] + UnparsableInteger(#[from] std::num::ParseIntError), + #[error("nix::ptrace() error")] + PtraceError(#[from] nix::Error), + #[error("Invalid line in /proc/{0}/status: {1}")] + InvalidProcStatusFile(Pid, String), +} + +#[derive(Debug, Error)] +pub enum AndroidError { + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] DumperError), + #[error("Failed slice conversion")] + TryFromSliceError(#[from] std::array::TryFromSliceError), + #[error("No Android rel found")] + NoRelFound, +} + +#[derive(Debug, Error)] +pub enum DumperError { + #[error("Failed to get PAGE_SIZE from system")] + SysConfError(#[from] nix::Error), + #[error("wait::waitpid(Pid={0}) failed")] + WaitPidError(Pid, #[source] nix::Error), + #[error("nix::ptrace::attach(Pid={0}) failed")] + PtraceAttachError(Pid, #[source] nix::Error), + #[error("nix::ptrace::detach(Pid={0}) failed")] + PtraceDetachError(Pid, #[source] nix::Error), + #[error("Copy from process {0} failed (source {1}, offset: {2}, length: {3})")] + CopyFromProcessError(Pid, usize, usize, usize, #[source] nix::Error), + #[error("Skipped thread {0} due to it being part of the seccomp sandbox's trusted code")] + DetachSkippedThread(Pid), + #[error("No threads left to suspend out of {0}")] + SuspendNoThreadsLeft(usize), + #[error("No mapping for stack pointer found")] + NoStackPointerMapping, + #[error("Failed slice conversion")] + TryFromSliceError(#[from] std::array::TryFromSliceError), + #[error("Couldn't parse as ELF file")] + ELFParsingFailed(#[from] goblin::error::Error), + #[error("No build-id found")] + NoBuildIDFound, + #[error("Not safe to open mapping: {}", .0.to_string_lossy())] + NotSafeToOpenMapping(OsString), + #[error("Failed integer conversion")] + TryFromIntError(#[from] std::num::TryFromIntError), + #[error("Maps reader error")] + MapsReaderError(#[from] MapsReaderError), +} + +#[derive(Debug, Error)] +pub enum SectionAppMemoryError { + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] DumperError), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +#[derive(Debug, Error)] +pub enum SectionExceptionStreamError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +#[derive(Debug, Error)] +pub enum SectionHandleDataStreamError { + #[error("Failed to access file")] + IOError(#[from] std::io::Error), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed integer conversion")] + TryFromIntError(#[from] std::num::TryFromIntError), +} + +#[derive(Debug, Error)] +pub enum SectionMappingsError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to get effective path of mapping ({0:?})")] + GetEffectivePathError(MappingInfo, #[source] MapsReaderError), +} + +#[derive(Debug, Error)] +pub enum SectionMemInfoListError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to read from procfs")] + ProcfsError(#[from] procfs_core::ProcError), +} + +#[derive(Debug, Error)] +pub enum SectionMemListError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +#[derive(Debug, Error)] +pub enum SectionSystemInfoError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to get CPU Info")] + CpuInfoError(#[from] CpuInfoError), +} + +#[derive(Debug, Error)] +pub enum SectionThreadListError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed integer conversion")] + TryFromIntError(#[from] std::num::TryFromIntError), + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] DumperError), + #[error("Failed to get thread info")] + ThreadInfoError(#[from] ThreadInfoError), + #[error("Failed to write to memory buffer")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +pub enum SectionThreadNamesError { + #[error("Failed integer conversion")] + TryFromIntError(#[from] std::num::TryFromIntError), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to write to memory buffer")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +pub enum SectionDsoDebugError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Could not find: {0}")] + CouldNotFind(&'static str), + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] DumperError), + #[error("Failed to copy memory from process")] + FromUTF8Error(#[from] std::string::FromUtf8Error), +} + +#[derive(Debug, Error)] +pub enum WriterError { + #[error("Error during init phase")] + InitError(#[from] InitError), + #[error(transparent)] + DumperError(#[from] DumperError), + #[error("Failed when writing section AppMemory")] + SectionAppMemoryError(#[from] SectionAppMemoryError), + #[error("Failed when writing section ExceptionStream")] + SectionExceptionStreamError(#[from] SectionExceptionStreamError), + #[error("Failed when writing section HandleDataStream")] + SectionHandleDataStreamError(#[from] SectionHandleDataStreamError), + #[error("Failed when writing section MappingsError")] + SectionMappingsError(#[from] SectionMappingsError), + #[error("Failed when writing section MemList")] + SectionMemListError(#[from] SectionMemListError), + #[error("Failed when writing section SystemInfo")] + SectionSystemInfoError(#[from] SectionSystemInfoError), + #[error("Failed when writing section MemoryInfoList")] + SectionMemoryInfoListError(#[from] SectionMemInfoListError), + #[error("Failed when writing section ThreadList")] + SectionThreadListError(#[from] SectionThreadListError), + #[error("Failed when writing section ThreadNameList")] + SectionThreadNamesError(#[from] SectionThreadNamesError), + #[error("Failed when writing section DsoDebug")] + SectionDsoDebugError(#[from] SectionDsoDebugError), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to write to file")] + FileWriterError(#[from] FileWriterError), + #[error("Failed to get current timestamp when writing header of minidump")] + SystemTimeError(#[from] std::time::SystemTimeError), +} diff --git a/third_party/rust/minidump-writer/src/linux/maps_reader.rs b/third_party/rust/minidump-writer/src/linux/maps_reader.rs new file mode 100644 index 0000000000..4d0d3b5aaa --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/maps_reader.rs @@ -0,0 +1,658 @@ +use crate::auxv_reader::AuxvType; +use crate::errors::MapsReaderError; +use crate::thread_info::Pid; +use byteorder::{NativeEndian, ReadBytesExt}; +use goblin::elf; +use memmap2::{Mmap, MmapOptions}; +use procfs_core::process::{MMPermissions, MMapPath, MemoryMaps}; +use std::ffi::{OsStr, OsString}; +use std::os::unix::ffi::OsStrExt; +use std::{fs::File, mem::size_of, path::PathBuf}; + +pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so"; +pub const DELETED_SUFFIX: &[u8] = b" (deleted)"; + +type Result<T> = std::result::Result<T, MapsReaderError>; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SystemMappingInfo { + pub start_address: usize, + pub end_address: usize, +} + +// One of these is produced for each mapping in the process (i.e. line in +// /proc/$x/maps). +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MappingInfo { + // On Android, relocation packing can mean that the reported start + // address of the mapping must be adjusted by a bias in order to + // compensate for the compression of the relocation section. The + // following two members hold (after LateInit) the adjusted mapping + // range. See crbug.com/606972 for more information. + pub start_address: usize, + pub size: usize, + // When Android relocation packing causes |start_addr| and |size| to + // be modified with a load bias, we need to remember the unbiased + // address range. The following structure holds the original mapping + // address range as reported by the operating system. + pub system_mapping_info: SystemMappingInfo, + pub offset: usize, // offset into the backed file. + pub permissions: MMPermissions, // read, write and execute permissions. + pub name: Option<OsString>, + // pub elf_obj: Option<elf::Elf>, +} + +#[derive(Debug)] +pub struct MappingEntry { + pub mapping: MappingInfo, + pub identifier: Vec<u8>, +} + +// A list of <MappingInfo, GUID> +pub type MappingList = Vec<MappingEntry>; + +#[derive(Debug)] +pub enum MappingInfoParsingResult { + SkipLine, + Success(MappingInfo), +} + +fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { + match pathname { + Some(x) => x.as_bytes().contains(&b'/'), + None => false, + } +} + +impl MappingInfo { + /// Return whether the `name` field is a path (contains a `/`). + pub fn name_is_path(&self) -> bool { + is_mapping_a_path(self.name.as_deref()) + } + + pub fn is_empty_page(&self) -> bool { + (self.offset == 0) && (self.permissions == MMPermissions::PRIVATE) && self.name.is_none() + } + + pub fn end_address(&self) -> usize { + self.start_address + self.size + } + + pub fn aggregate(memory_maps: MemoryMaps, linux_gate_loc: AuxvType) -> Result<Vec<Self>> { + let mut infos = Vec::<Self>::new(); + + for mm in memory_maps { + let start_address: usize = mm.address.0.try_into()?; + let end_address: usize = mm.address.1.try_into()?; + let mut offset: usize = mm.offset.try_into()?; + + let mut pathname: Option<OsString> = match mm.pathname { + MMapPath::Path(p) => Some(p.into()), + MMapPath::Heap => Some("[heap]".into()), + MMapPath::Stack => Some("[stack]".into()), + MMapPath::TStack(i) => Some(format!("[stack:{i}]").into()), + MMapPath::Vdso => Some("[vdso]".into()), + MMapPath::Vvar => Some("[vvar]".into()), + MMapPath::Vsyscall => Some("[vsyscall]".into()), + MMapPath::Rollup => Some("[rollup]".into()), + MMapPath::Vsys(i) => Some(format!("/SYSV{i:x}").into()), + MMapPath::Other(n) => Some(format!("[{n}]").into()), + MMapPath::Anonymous => None, + }; + + let is_path = is_mapping_a_path(pathname.as_deref()); + + if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? { + pathname = Some(LINUX_GATE_LIBRARY_NAME.into()); + offset = 0; + } + + if let Some(prev_module) = infos.last_mut() { + if (start_address == prev_module.end_address()) + && pathname.is_some() + && (pathname == prev_module.name) + { + // Merge adjacent mappings into one module, assuming they're a single + // library mapped by the dynamic linker. + prev_module.system_mapping_info.end_address = end_address; + prev_module.size = end_address - prev_module.start_address; + prev_module.permissions |= mm.perms; + continue; + } else if (start_address == prev_module.end_address()) + && prev_module.is_executable() + && prev_module.name_is_path() + && ((offset == 0) || (offset == prev_module.end_address())) + && (mm.perms == MMPermissions::PRIVATE) + { + // Also merge mappings that result from address ranges that the + // linker reserved but which a loaded library did not use. These + // appear as an anonymous private mapping with no access flags set + // and which directly follow an executable mapping. + prev_module.size = end_address - prev_module.start_address; + continue; + } + } + + // Sometimes the unused ranges reserved but the linker appear within the library. + // If we detect an empty page that is adjacent to two mappings of the same library + // we fold the three mappings together. + if let Some(previous_modules) = infos.rchunks_exact_mut(2).next() { + let empty_page = if let Some(prev_module) = previous_modules.last() { + let prev_prev_module = previous_modules.first().unwrap(); + prev_prev_module.name_is_path() + && (prev_prev_module.end_address() == prev_module.start_address) + && prev_module.is_empty_page() + && (prev_module.end_address() == start_address) + } else { + false + }; + + if empty_page { + let prev_prev_module = previous_modules.first_mut().unwrap(); + + if pathname == prev_prev_module.name { + prev_prev_module.system_mapping_info.end_address = end_address; + prev_prev_module.size = end_address - prev_prev_module.start_address; + prev_prev_module.permissions |= mm.perms; + infos.pop(); + continue; + } + } + } + + infos.push(MappingInfo { + start_address, + size: end_address - start_address, + system_mapping_info: SystemMappingInfo { + start_address, + end_address, + }, + offset, + permissions: mm.perms, + name: pathname, + }); + } + Ok(infos) + } + + pub fn get_mmap(name: &Option<OsString>, offset: usize) -> Result<Mmap> { + if !MappingInfo::is_mapped_file_safe_to_open(name) { + return Err(MapsReaderError::NotSafeToOpenMapping( + name.clone().unwrap_or_default(), + )); + } + + // Not doing this as root_prefix is always "" at the moment + // if (!dumper.GetMappingAbsolutePath(mapping, filename)) + let filename = name.clone().unwrap_or_default(); + let mapped_file = unsafe { + MmapOptions::new() + .offset(offset.try_into()?) // try_into() to work for both 32 and 64 bit + .map(&File::open(filename)?)? + }; + + if mapped_file.is_empty() || mapped_file.len() < elf::header::SELFMAG { + return Err(MapsReaderError::MmapSanityCheckFailed); + } + Ok(mapped_file) + } + + /// Check whether the mapping refers to a deleted file, and if so try to find the file + /// elsewhere and return that path. + /// + /// Currently this only supports fixing a deleted file that was the main exe of the given + /// `pid`. + /// + /// Returns a tuple, where the first element is the file path (which is possibly different than + /// `self.name`), and the second element is the original file path if a different path was + /// used. If no mapping name exists, returns an error. + pub fn fixup_deleted_file(&self, pid: Pid) -> Result<(OsString, Option<&OsStr>)> { + // Check for ' (deleted)' in |path|. + // |path| has to be at least as long as "/x (deleted)". + let Some(path) = &self.name else { + return Err(MapsReaderError::AnonymousMapping); + }; + + let Some(old_path) = path.as_bytes().strip_suffix(DELETED_SUFFIX) else { + return Ok((path.clone(), None)); + }; + + // Check |path| against the /proc/pid/exe 'symlink'. + let exe_link = format!("/proc/{}/exe", pid); + let link_path = std::fs::read_link(&exe_link)?; + + // This is a no-op for now (until we want to support root_prefix for chroot-envs) + // if (!GetMappingAbsolutePath(new_mapping, new_path)) + // return false; + + if &link_path != path { + return Err(MapsReaderError::SymlinkError( + PathBuf::from(path), + link_path, + )); + } + + // Check to see if someone actually named their executable 'foo (deleted)'. + + // This makes currently no sense, as exe_link == new_path + // if let (Some(exe_stat), Some(new_path_stat)) = (nix::stat::stat(exe_link), nix::stat::stat(new_path)) { + // if exe_stat.st_dev == new_path_stat.st_dev && exe_stat.st_ino == new_path_stat.st_ino { + // return Err("".into()); + // } + // } + Ok((exe_link.into(), Some(OsStr::from_bytes(old_path)))) + } + + pub fn stack_has_pointer_to_mapping(&self, stack_copy: &[u8], sp_offset: usize) -> bool { + // Loop over all stack words that would have been on the stack in + // the target process (i.e. are word aligned, and at addresses >= + // the stack pointer). Regardless of the alignment of |stack_copy|, + // the memory starting at |stack_copy| + |offset| represents an + // aligned word in the target process. + let low_addr = self.system_mapping_info.start_address; + let high_addr = self.system_mapping_info.end_address; + let mut offset = (sp_offset + size_of::<usize>() - 1) & !(size_of::<usize>() - 1); + while offset <= stack_copy.len() - size_of::<usize>() { + let addr = match std::mem::size_of::<usize>() { + 4 => stack_copy[offset..] + .as_ref() + .read_u32::<NativeEndian>() + .map(|u| u as usize), + 8 => stack_copy[offset..] + .as_ref() + .read_u64::<NativeEndian>() + .map(|u| u as usize), + x => panic!("Unexpected type width: {}", x), + }; + if let Ok(addr) = addr { + if low_addr <= addr && addr <= high_addr { + return true; + } + offset += size_of::<usize>(); + } else { + break; + } + } + false + } + + pub fn is_mapped_file_safe_to_open(name: &Option<OsString>) -> bool { + // It is unsafe to attempt to open a mapped file that lives under /dev, + // because the semantics of the open may be driver-specific so we'd risk + // hanging the crash dumper. And a file in /dev/ almost certainly has no + // ELF file identifier anyways. + if let Some(name) = name { + if name.as_bytes().starts_with(b"/dev/") { + return false; + } + } + true + } + + fn elf_file_so_name(&self) -> Result<String> { + // Find the shared object name (SONAME) by examining the ELF information + // for |mapping|. If the SONAME is found copy it into the passed buffer + // |soname| and return true. The size of the buffer is |soname_size|. + let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?; + + let elf_obj = elf::Elf::parse(&mapped_file)?; + + let soname = elf_obj.soname.ok_or_else(|| { + MapsReaderError::NoSoName(self.name.clone().unwrap_or_else(|| "None".into())) + })?; + Ok(soname.to_string()) + } + + pub fn get_mapping_effective_path_and_name(&self) -> Result<(PathBuf, String)> { + let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default()); + + // Tools such as minidump_stackwalk use the name of the module to look up + // symbols produced by dump_syms. dump_syms will prefer to use a module's + // DT_SONAME as the module name, if one exists, and will fall back to the + // filesystem name of the module. + + // Just use the filesystem name if no SONAME is present. + let file_name = if let Ok(name) = self.elf_file_so_name() { + name + } else { + // file_path := /path/to/libname.so + // file_name := libname.so + let file_name = file_path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + return Ok((file_path, file_name)); + }; + + if self.is_executable() && self.offset != 0 { + // If an executable is mapped from a non-zero offset, this is likely because + // the executable was loaded directly from inside an archive file (e.g., an + // apk on Android). + // In this case, we append the file_name to the mapped archive path: + // file_name := libname.so + // file_path := /path/to/ARCHIVE.APK/libname.so + file_path.push(&file_name); + } else { + // Otherwise, replace the basename with the SONAME. + file_path.set_file_name(&file_name); + } + + Ok((file_path, file_name)) + } + + pub fn is_contained_in(&self, user_mapping_list: &MappingList) -> bool { + for user in user_mapping_list { + // Ignore any mappings that are wholly contained within + // mappings in the mapping_info_ list. + if self.start_address >= user.mapping.start_address + && (self.start_address + self.size) + <= (user.mapping.start_address + user.mapping.size) + { + return true; + } + } + false + } + + pub fn is_interesting(&self) -> bool { + // only want modules with filenames. + self.name.is_some() && + // Only want to include one mapping per shared lib. + // Avoid filtering executable mappings. + (self.offset == 0 || self.is_executable()) && + // big enough to get a signature for. + self.size >= 4096 + } + + pub fn contains_address(&self, address: usize) -> bool { + self.system_mapping_info.start_address <= address + && address < self.system_mapping_info.end_address + } + + pub fn is_executable(&self) -> bool { + self.permissions.contains(MMPermissions::EXECUTE) + } + + pub fn is_readable(&self) -> bool { + self.permissions.contains(MMPermissions::READ) + } + + pub fn is_writable(&self) -> bool { + self.permissions.contains(MMPermissions::WRITE) + } +} + +#[cfg(test)] +#[cfg(target_pointer_width = "64")] // All addresses are 64 bit and I'm currently too lazy to adjust it to work for both +mod tests { + use super::*; + use procfs_core::FromRead; + + fn get_mappings_for(map: &str, linux_gate_loc: u64) -> Vec<MappingInfo> { + MappingInfo::aggregate( + MemoryMaps::from_read(map.as_bytes()).expect("failed to read mapping info"), + linux_gate_loc, + ) + .unwrap_or_default() + } + + const LINES: &str = "\ +5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat +5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat +559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat +559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat +559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat +559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap] +7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 +7efd968f5000-7efd9694a000 r--p 00000000 00:31 5004638 /usr/lib/locale/en_US.utf8/LC_CTYPE +7efd9694a000-7efd96bc2000 r--p 00000000 00:31 5004373 /usr/lib/locale/en_US.utf8/LC_COLLATE +7efd96bc2000-7efd96bc4000 rw-p 00000000 00:00 0 +7efd96bc4000-7efd96bea000 r--p 00000000 00:31 4996104 /lib64/libc-2.32.so +7efd96bea000-7efd96d39000 r-xp 00026000 00:31 4996104 /lib64/libc-2.32.so +7efd96d39000-7efd96d85000 r--p 00175000 00:31 4996104 /lib64/libc-2.32.so +7efd96d85000-7efd96d86000 ---p 001c1000 00:31 4996104 /lib64/libc-2.32.so +7efd96d86000-7efd96d89000 r--p 001c1000 00:31 4996104 /lib64/libc-2.32.so +7efd96d89000-7efd96d8c000 rw-p 001c4000 00:31 4996104 /lib64/libc-2.32.so +7efd96d8c000-7efd96d92000 ---p 00000000 00:00 0 +7efd96da0000-7efd96da1000 r--p 00000000 00:31 5004379 /usr/lib/locale/en_US.utf8/LC_NUMERIC +7efd96da1000-7efd96da2000 r--p 00000000 00:31 5004382 /usr/lib/locale/en_US.utf8/LC_TIME +7efd96da2000-7efd96da3000 r--p 00000000 00:31 5004377 /usr/lib/locale/en_US.utf8/LC_MONETARY +7efd96da3000-7efd96da4000 r--p 00000000 00:31 5004376 /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES +7efd96da4000-7efd96da5000 r--p 00000000 00:31 5004380 /usr/lib/locale/en_US.utf8/LC_PAPER +7efd96da5000-7efd96da6000 r--p 00000000 00:31 5004378 /usr/lib/locale/en_US.utf8/LC_NAME +7efd96da6000-7efd96da7000 r--p 00000000 00:31 5004372 /usr/lib/locale/en_US.utf8/LC_ADDRESS +7efd96da7000-7efd96da8000 r--p 00000000 00:31 5004381 /usr/lib/locale/en_US.utf8/LC_TELEPHONE +7efd96da8000-7efd96da9000 r--p 00000000 00:31 5004375 /usr/lib/locale/en_US.utf8/LC_MEASUREMENT +7efd96da9000-7efd96db0000 r--s 00000000 00:31 5004639 /usr/lib64/gconv/gconv-modules.cache +7efd96db0000-7efd96db1000 r--p 00000000 00:31 5004374 /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION +7efd96db1000-7efd96db2000 r--p 00000000 00:31 4996100 /lib64/ld-2.32.so +7efd96db2000-7efd96dd3000 r-xp 00001000 00:31 4996100 /lib64/ld-2.32.so +7efd96dd3000-7efd96ddc000 r--p 00022000 00:31 4996100 /lib64/ld-2.32.so +7efd96ddc000-7efd96ddd000 r--p 0002a000 00:31 4996100 /lib64/ld-2.32.so +7efd96ddd000-7efd96ddf000 rw-p 0002b000 00:31 4996100 /lib64/ld-2.32.so +7ffc6dfda000-7ffc6dffb000 rw-p 00000000 00:00 0 [stack] +7ffc6e0f3000-7ffc6e0f7000 r--p 00000000 00:00 0 [vvar] +7ffc6e0f7000-7ffc6e0f9000 r-xp 00000000 00:00 0 [vdso] +ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]"; + const LINUX_GATE_LOC: u64 = 0x7ffc6e0f7000; + + fn get_all_mappings() -> Vec<MappingInfo> { + get_mappings_for(LINES, LINUX_GATE_LOC) + } + + #[test] + fn test_merged() { + // Only /usr/bin/cat and [heap] + let mappings = get_mappings_for( + "\ +5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat +5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat +559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat +559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat +559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat +559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap] +7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 ", + 0x7ffc6e0f7000, + ); + + assert_eq!(mappings.len(), 3); + let cat_map = MappingInfo { + start_address: 0x5597483fc000, + size: 40960, + system_mapping_info: SystemMappingInfo { + start_address: 0x5597483fc000, + end_address: 0x559748406000, + }, + offset: 0, + permissions: MMPermissions::READ + | MMPermissions::WRITE + | MMPermissions::EXECUTE + | MMPermissions::PRIVATE, + name: Some("/usr/bin/cat".into()), + }; + + assert_eq!(mappings[0], cat_map); + + let heap_map = MappingInfo { + start_address: 0x559749b0e000, + size: 135168, + system_mapping_info: SystemMappingInfo { + start_address: 0x559749b0e000, + end_address: 0x559749b2f000, + }, + offset: 0, + permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::PRIVATE, + name: Some("[heap]".into()), + }; + + assert_eq!(mappings[1], heap_map); + + let empty_map = MappingInfo { + start_address: 0x7efd968d3000, + size: 139264, + system_mapping_info: SystemMappingInfo { + start_address: 0x7efd968d3000, + end_address: 0x7efd968f5000, + }, + offset: 0, + permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::PRIVATE, + name: None, + }; + + assert_eq!(mappings[2], empty_map); + } + + #[test] + fn test_linux_gate_parsing() { + let mappings = get_all_mappings(); + + let gate_map = MappingInfo { + start_address: 0x7ffc6e0f7000, + size: 8192, + system_mapping_info: SystemMappingInfo { + start_address: 0x7ffc6e0f7000, + end_address: 0x7ffc6e0f9000, + }, + offset: 0, + permissions: MMPermissions::READ | MMPermissions::EXECUTE | MMPermissions::PRIVATE, + name: Some("linux-gate.so".into()), + }; + + assert_eq!(mappings[21], gate_map); + } + + #[test] + fn test_reading_all() { + let mappings = get_all_mappings(); + + let found_items: Vec<Option<OsString>> = vec![ + Some("/usr/bin/cat".into()), + Some("[heap]".into()), + None, + Some("/usr/lib/locale/en_US.utf8/LC_CTYPE".into()), + Some("/usr/lib/locale/en_US.utf8/LC_COLLATE".into()), + None, + Some("/lib64/libc-2.32.so".into()), + // The original shows a None here, but this is an address ranges that the + // linker reserved but which a loaded library did not use. These + // appear as an anonymous private mapping with no access flags set + // and which directly follow an executable mapping. + Some("/usr/lib/locale/en_US.utf8/LC_NUMERIC".into()), + Some("/usr/lib/locale/en_US.utf8/LC_TIME".into()), + Some("/usr/lib/locale/en_US.utf8/LC_MONETARY".into()), + Some("/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES".into()), + Some("/usr/lib/locale/en_US.utf8/LC_PAPER".into()), + Some("/usr/lib/locale/en_US.utf8/LC_NAME".into()), + Some("/usr/lib/locale/en_US.utf8/LC_ADDRESS".into()), + Some("/usr/lib/locale/en_US.utf8/LC_TELEPHONE".into()), + Some("/usr/lib/locale/en_US.utf8/LC_MEASUREMENT".into()), + Some("/usr/lib64/gconv/gconv-modules.cache".into()), + Some("/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION".into()), + Some("/lib64/ld-2.32.so".into()), + Some("[stack]".into()), + Some("[vvar]".into()), + // This is rewritten from [vdso] to linux-gate.so + Some("linux-gate.so".into()), + Some("[vsyscall]".into()), + ]; + + assert_eq!( + mappings.iter().map(|x| x.name.clone()).collect::<Vec<_>>(), + found_items + ); + } + + #[test] + fn test_merged_reserved_mappings() { + let mappings = get_all_mappings(); + + let gate_map = MappingInfo { + start_address: 0x7efd96bc4000, + size: 1892352, // Merged the anonymous area after in this mapping, so its bigger.. + system_mapping_info: SystemMappingInfo { + start_address: 0x7efd96bc4000, + end_address: 0x7efd96d8c000, // ..but this is not visible here + }, + offset: 0, + permissions: MMPermissions::READ + | MMPermissions::WRITE + | MMPermissions::EXECUTE + | MMPermissions::PRIVATE, + name: Some("/lib64/libc-2.32.so".into()), + }; + + assert_eq!(mappings[6], gate_map); + } + + #[test] + fn test_merged_reserved_mappings_within_module() { + let mappings = get_mappings_for( + "\ +9b4a0000-9b931000 r--p 00000000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so +9b931000-9bcae000 ---p 00000000 00:00 0 +9bcae000-a116b000 r-xp 00490000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so +a116b000-a4562000 r--p 0594d000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so +a4562000-a4563000 ---p 00000000 00:00 0 +a4563000-a4840000 r--p 08d44000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so +a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so", + 0xa4876000, + ); + + let gate_map = MappingInfo { + start_address: 0x9b4a0000, + size: 155004928, // Merged the anonymous area after in this mapping, so its bigger.. + system_mapping_info: SystemMappingInfo { + start_address: 0x9b4a0000, + end_address: 0xa4873000, + }, + offset: 0, + permissions: MMPermissions::READ + | MMPermissions::WRITE + | MMPermissions::EXECUTE + | MMPermissions::PRIVATE, + name: Some("/data/app/org.mozilla.firefox-1/lib/x86/libxul.so".into()), + }; + + assert_eq!(mappings[0], gate_map); + } + + #[test] + fn test_get_mapping_effective_name() { + let mappings = get_mappings_for( + "\ +7f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so +7f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so +7f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so +7f0b97b73000-7f0b97b74000 rw-p 00001000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", + 0x7ffe091bf000, + ); + assert_eq!(mappings.len(), 1); + + let (file_path, file_name) = mappings[0] + .get_mapping_effective_path_and_name() + .expect("Couldn't get effective name for mapping"); + assert_eq!(file_name, "libmozgtk.so"); + assert_eq!(file_path, PathBuf::from("/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so")); + } + + #[test] + fn test_whitespaces_in_name() { + let mappings = get_mappings_for( + "\ +10000000-20000000 r--p 00000000 00:3e 27136458 libmoz gtk.so +20000000-30000000 r--p 00000000 00:3e 27136458 libmozgtk.so (deleted) +30000000-40000000 r--p 00000000 00:3e 27136458 \"libmoz gtk.so (deleted)\" +30000000-40000000 r--p 00000000 00:3e 27136458 ", + 0x7ffe091bf000, + ); + + assert_eq!(mappings.len(), 4); + assert_eq!(mappings[0].name, Some("libmoz gtk.so".into())); + assert_eq!(mappings[1].name, Some("libmozgtk.so (deleted)".into())); + assert_eq!( + mappings[2].name, + Some("\"libmoz gtk.so (deleted)\"".into()) + ); + assert_eq!(mappings[3].name, None); + } +} diff --git a/third_party/rust/minidump-writer/src/linux/minidump_writer.rs b/third_party/rust/minidump-writer/src/linux/minidump_writer.rs new file mode 100644 index 0000000000..da395b53f5 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/minidump_writer.rs @@ -0,0 +1,361 @@ +use crate::{ + dir_section::{DirSection, DumpBuf}, + linux::{ + app_memory::AppMemoryList, + crash_context::CrashContext, + dso_debug, + errors::{InitError, WriterError}, + maps_reader::{MappingInfo, MappingList}, + ptrace_dumper::PtraceDumper, + sections::*, + thread_info::Pid, + }, + mem_writer::{Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError}, + minidump_format::*, +}; +use std::io::{Seek, Write}; + +pub enum CrashingThreadContext { + None, + CrashContext(MDLocationDescriptor), + CrashContextPlusAddress((MDLocationDescriptor, usize)), +} + +pub struct MinidumpWriter { + pub process_id: Pid, + pub blamed_thread: Pid, + pub minidump_size_limit: Option<u64>, + pub skip_stacks_if_mapping_unreferenced: bool, + pub principal_mapping_address: Option<usize>, + pub user_mapping_list: MappingList, + pub app_memory: AppMemoryList, + pub memory_blocks: Vec<MDMemoryDescriptor>, + pub principal_mapping: Option<MappingInfo>, + pub sanitize_stack: bool, + pub crash_context: Option<CrashContext>, + pub crashing_thread_context: CrashingThreadContext, +} + +// This doesn't work yet: +// https://github.com/rust-lang/rust/issues/43408 +// fn write<T: Sized, P: AsRef<Path>>(path: P, value: T) -> Result<()> { +// let mut file = std::fs::File::open(path)?; +// let bytes: [u8; size_of::<T>()] = unsafe { transmute(value) }; +// file.write_all(&bytes)?; +// Ok(()) +// } + +type Result<T> = std::result::Result<T, WriterError>; + +impl MinidumpWriter { + pub fn new(process: Pid, blamed_thread: Pid) -> Self { + Self { + process_id: process, + blamed_thread, + minidump_size_limit: None, + skip_stacks_if_mapping_unreferenced: false, + principal_mapping_address: None, + user_mapping_list: MappingList::new(), + app_memory: AppMemoryList::new(), + memory_blocks: Vec::new(), + principal_mapping: None, + sanitize_stack: false, + crash_context: None, + crashing_thread_context: CrashingThreadContext::None, + } + } + + pub fn set_minidump_size_limit(&mut self, limit: u64) -> &mut Self { + self.minidump_size_limit = Some(limit); + self + } + + pub fn set_user_mapping_list(&mut self, user_mapping_list: MappingList) -> &mut Self { + self.user_mapping_list = user_mapping_list; + self + } + + pub fn set_principal_mapping_address(&mut self, principal_mapping_address: usize) -> &mut Self { + self.principal_mapping_address = Some(principal_mapping_address); + self + } + + pub fn set_app_memory(&mut self, app_memory: AppMemoryList) -> &mut Self { + self.app_memory = app_memory; + self + } + + pub fn set_crash_context(&mut self, crash_context: CrashContext) -> &mut Self { + self.crash_context = Some(crash_context); + self + } + + pub fn skip_stacks_if_mapping_unreferenced(&mut self) -> &mut Self { + self.skip_stacks_if_mapping_unreferenced = true; // Off by default + self + } + + pub fn sanitize_stack(&mut self) -> &mut Self { + self.sanitize_stack = true; // Off by default + self + } + + /// Generates a minidump and writes to the destination provided. Returns the in-memory + /// version of the minidump as well. + pub fn dump(&mut self, destination: &mut (impl Write + Seek)) -> Result<Vec<u8>> { + let mut dumper = PtraceDumper::new(self.process_id)?; + dumper.suspend_threads()?; + dumper.late_init()?; + + if self.skip_stacks_if_mapping_unreferenced { + if let Some(address) = self.principal_mapping_address { + self.principal_mapping = dumper.find_mapping_no_bias(address).cloned(); + } + + if !self.crash_thread_references_principal_mapping(&dumper) { + return Err(InitError::PrincipalMappingNotReferenced.into()); + } + } + + let mut buffer = Buffer::with_capacity(0); + self.generate_dump(&mut buffer, &mut dumper, destination)?; + + // dumper would resume threads in drop() automatically, + // but in case there is an error, we want to catch it + dumper.resume_threads()?; + + Ok(buffer.into()) + } + + fn crash_thread_references_principal_mapping(&self, dumper: &PtraceDumper) -> bool { + if self.crash_context.is_none() || self.principal_mapping.is_none() { + return false; + } + + let low_addr = self + .principal_mapping + .as_ref() + .unwrap() + .system_mapping_info + .start_address; + let high_addr = self + .principal_mapping + .as_ref() + .unwrap() + .system_mapping_info + .end_address; + + let pc = self + .crash_context + .as_ref() + .unwrap() + .get_instruction_pointer(); + let stack_pointer = self.crash_context.as_ref().unwrap().get_stack_pointer(); + + if pc >= low_addr && pc < high_addr { + return true; + } + + let (valid_stack_pointer, stack_len) = match dumper.get_stack_info(stack_pointer) { + Ok(x) => x, + Err(_) => { + return false; + } + }; + + let stack_copy = match PtraceDumper::copy_from_process( + self.blamed_thread, + valid_stack_pointer as *mut libc::c_void, + stack_len, + ) { + Ok(x) => x, + Err(_) => { + return false; + } + }; + + let sp_offset = stack_pointer.saturating_sub(valid_stack_pointer); + self.principal_mapping + .as_ref() + .unwrap() + .stack_has_pointer_to_mapping(&stack_copy, sp_offset) + } + + fn generate_dump( + &mut self, + buffer: &mut DumpBuf, + dumper: &mut PtraceDumper, + destination: &mut (impl Write + Seek), + ) -> Result<()> { + // A minidump file contains a number of tagged streams. This is the number + // of streams which we write. + let num_writers = 17u32; + + let mut header_section = MemoryWriter::<MDRawHeader>::alloc(buffer)?; + + let mut dir_section = DirSection::new(buffer, num_writers, destination)?; + + let header = MDRawHeader { + signature: MD_HEADER_SIGNATURE, + version: MD_HEADER_VERSION, + stream_count: num_writers, + // header.get()->stream_directory_rva = dir.position(); + stream_directory_rva: dir_section.position(), + checksum: 0, /* Can be 0. In fact, that's all that's + * been found in minidump files. */ + time_date_stamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs() as u32, // TODO: This is not Y2038 safe, but thats how its currently defined as + flags: 0, + }; + header_section.set_value(buffer, header)?; + + // Ensure the header gets flushed. If we crash somewhere below, + // we should have a mostly-intact dump + dir_section.write_to_file(buffer, None)?; + + let dirent = thread_list_stream::write(self, buffer, dumper)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = mappings::write(self, buffer, dumper)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + app_memory::write(self, buffer)?; + // Write section to file + dir_section.write_to_file(buffer, None)?; + + let dirent = memory_list_stream::write(self, buffer)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = exception_stream::write(self, buffer)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = systeminfo_stream::write(buffer)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = memory_info_list_stream::write(self, buffer)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, "/proc/cpuinfo") { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxCpuInfo as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, &format!("/proc/{}/status", self.blamed_thread)) + { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxProcStatus as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self + .write_file(buffer, "/etc/lsb-release") + .or_else(|_| self.write_file(buffer, "/etc/os-release")) + { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxLsbRelease as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, &format!("/proc/{}/cmdline", self.blamed_thread)) + { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxCmdLine as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, &format!("/proc/{}/environ", self.blamed_thread)) + { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxEnviron as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, &format!("/proc/{}/auxv", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxAuxv as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, &format!("/proc/{}/maps", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxMaps as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = dso_debug::write_dso_debug_stream(buffer, self.process_id, &dumper.auxv) + .unwrap_or_default(); + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_file(buffer, &format!("/proc/{}/limits", self.blamed_thread)) + { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::MozLinuxLimits as u32, + location, + }, + Err(_) => Default::default(), + }; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = thread_names_stream::write(buffer, dumper)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + + // This section is optional, so we ignore errors when writing it + if let Ok(dirent) = handle_data_stream::write(self, buffer) { + let _ = dir_section.write_to_file(buffer, Some(dirent)); + } + + // If you add more directory entries, don't forget to update num_writers, above. + Ok(()) + } + + #[allow(clippy::unused_self)] + fn write_file( + &self, + buffer: &mut DumpBuf, + filename: &str, + ) -> std::result::Result<MDLocationDescriptor, MemoryWriterError> { + let content = std::fs::read(filename)?; + + let section = MemoryArrayWriter::write_bytes(buffer, &content); + Ok(section.location()) + } +} diff --git a/third_party/rust/minidump-writer/src/linux/ptrace_dumper.rs b/third_party/rust/minidump-writer/src/linux/ptrace_dumper.rs new file mode 100644 index 0000000000..f75499bcdd --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/ptrace_dumper.rs @@ -0,0 +1,607 @@ +#[cfg(target_os = "android")] +use crate::linux::android::late_process_mappings; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +use crate::thread_info; +use crate::{ + linux::{ + auxv_reader::{AuxvType, ProcfsAuxvIter}, + errors::{DumperError, InitError, ThreadInfoError}, + maps_reader::MappingInfo, + thread_info::{Pid, ThreadInfo}, + LINUX_GATE_LIBRARY_NAME, + }, + minidump_format::GUID, +}; +use goblin::elf; +use nix::{ + errno::Errno, + sys::{ptrace, wait}, +}; +use procfs_core::process::MMPermissions; +use std::{collections::HashMap, ffi::c_void, io::BufReader, path, result::Result}; + +#[derive(Debug, Clone)] +pub struct Thread { + pub tid: Pid, + pub name: Option<String>, +} + +#[derive(Debug)] +pub struct PtraceDumper { + pub pid: Pid, + threads_suspended: bool, + pub threads: Vec<Thread>, + pub auxv: HashMap<AuxvType, AuxvType>, + pub mappings: Vec<MappingInfo>, + pub page_size: usize, +} + +#[cfg(target_pointer_width = "32")] +pub const AT_SYSINFO_EHDR: u32 = 33; +#[cfg(target_pointer_width = "64")] +pub const AT_SYSINFO_EHDR: u64 = 33; + +impl Drop for PtraceDumper { + fn drop(&mut self) { + // Always try to resume all threads (e.g. in case of error) + let _ = self.resume_threads(); + } +} + +/// PTRACE_DETACH the given pid. +/// +/// This handles special errno cases (ESRCH) which we won't consider errors. +fn ptrace_detach(child: Pid) -> Result<(), DumperError> { + let pid = nix::unistd::Pid::from_raw(child); + ptrace::detach(pid, None).or_else(|e| { + // errno is set to ESRCH if the pid no longer exists, but we don't want to error in that + // case. + if e == nix::Error::ESRCH { + Ok(()) + } else { + Err(DumperError::PtraceDetachError(child, e)) + } + }) +} + +impl PtraceDumper { + /// Constructs a dumper for extracting information of a given process + /// with a process ID of |pid|. + pub fn new(pid: Pid) -> Result<Self, InitError> { + let mut dumper = PtraceDumper { + pid, + threads_suspended: false, + threads: Vec::new(), + auxv: HashMap::new(), + mappings: Vec::new(), + page_size: 0, + }; + dumper.init()?; + Ok(dumper) + } + + // TODO: late_init for chromeos and android + pub fn init(&mut self) -> Result<(), InitError> { + self.read_auxv()?; + self.enumerate_threads()?; + self.enumerate_mappings()?; + self.page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)? + .expect("page size apparently unlimited: doesn't make sense.") + as usize; + + Ok(()) + } + + #[cfg_attr(not(target_os = "android"), allow(clippy::unused_self))] + pub fn late_init(&mut self) -> Result<(), InitError> { + #[cfg(target_os = "android")] + { + late_process_mappings(self.pid, &mut self.mappings)?; + } + Ok(()) + } + + /// Copies content of |length| bytes from a given process |child|, + /// starting from |src|, into |dest|. This method uses ptrace to extract + /// the content from the target process. Always returns true. + pub fn copy_from_process( + child: Pid, + src: *mut c_void, + num_of_bytes: usize, + ) -> Result<Vec<u8>, DumperError> { + use DumperError::CopyFromProcessError as CFPE; + let pid = nix::unistd::Pid::from_raw(child); + let mut res = Vec::new(); + let mut idx = 0usize; + while idx < num_of_bytes { + let word = ptrace::read(pid, (src as usize + idx) as *mut c_void) + .map_err(|e| CFPE(child, src as usize, idx, num_of_bytes, e))?; + res.append(&mut word.to_ne_bytes().to_vec()); + idx += std::mem::size_of::<libc::c_long>(); + } + Ok(res) + } + + /// Suspends a thread by attaching to it. + pub fn suspend_thread(child: Pid) -> Result<(), DumperError> { + use DumperError::PtraceAttachError as AttachErr; + + let pid = nix::unistd::Pid::from_raw(child); + // This may fail if the thread has just died or debugged. + ptrace::attach(pid).map_err(|e| AttachErr(child, e))?; + loop { + match wait::waitpid(pid, Some(wait::WaitPidFlag::__WALL)) { + Ok(_) => break, + Err(_e @ Errno::EINTR) => continue, + Err(e) => { + ptrace_detach(child)?; + return Err(DumperError::WaitPidError(child, e)); + } + } + } + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + // On x86, the stack pointer is NULL or -1, when executing trusted code in + // the seccomp sandbox. Not only does this cause difficulties down the line + // when trying to dump the thread's stack, it also results in the minidumps + // containing information about the trusted threads. This information is + // generally completely meaningless and just pollutes the minidumps. + // We thus test the stack pointer and exclude any threads that are part of + // the seccomp sandbox's trusted code. + let skip_thread; + let regs = thread_info::ThreadInfo::getregs(pid.into()); + if let Ok(regs) = regs { + #[cfg(target_arch = "x86_64")] + { + skip_thread = regs.rsp == 0; + } + #[cfg(target_arch = "x86")] + { + skip_thread = regs.esp == 0; + } + } else { + skip_thread = true; + } + if skip_thread { + ptrace_detach(child)?; + return Err(DumperError::DetachSkippedThread(child)); + } + } + Ok(()) + } + + /// Resumes a thread by detaching from it. + pub fn resume_thread(child: Pid) -> Result<(), DumperError> { + ptrace_detach(child) + } + + pub fn suspend_threads(&mut self) -> Result<(), DumperError> { + let threads_count = self.threads.len(); + // Iterate over all threads and try to suspend them. + // If the thread either disappeared before we could attach to it, or if + // it was part of the seccomp sandbox's trusted code, it is OK to + // silently drop it from the minidump. + self.threads.retain(|x| Self::suspend_thread(x.tid).is_ok()); + + if self.threads.is_empty() { + Err(DumperError::SuspendNoThreadsLeft(threads_count)) + } else { + self.threads_suspended = true; + Ok(()) + } + } + + pub fn resume_threads(&mut self) -> Result<(), DumperError> { + let mut result = Ok(()); + if self.threads_suspended { + for thread in &self.threads { + match Self::resume_thread(thread.tid) { + Ok(_) => {} + x => { + result = x; + } + } + } + } + self.threads_suspended = false; + result + } + + /// Parse /proc/$pid/task to list all the threads of the process identified by + /// pid. + fn enumerate_threads(&mut self) -> Result<(), InitError> { + let pid = self.pid; + let filename = format!("/proc/{}/task", pid); + let task_path = path::PathBuf::from(&filename); + if task_path.is_dir() { + std::fs::read_dir(task_path) + .map_err(|e| InitError::IOError(filename, e))? + .filter_map(|entry| entry.ok()) // Filter out bad entries + .filter_map(|entry| { + entry + .file_name() // Parse name to Pid, filter out those that are unparsable + .to_str() + .and_then(|name| name.parse::<Pid>().ok()) + }) + .map(|tid| { + // Read the thread-name (if there is any) + let name = std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid)) + // NOTE: This is a bit wasteful as it does two allocations in order to trim, but leaving it for now + .map(|s| s.trim_end().to_string()) + .ok(); + (tid, name) + }) + .for_each(|(tid, name)| self.threads.push(Thread { tid, name })); + } + Ok(()) + } + + fn read_auxv(&mut self) -> Result<(), InitError> { + let filename = format!("/proc/{}/auxv", self.pid); + let auxv_path = path::PathBuf::from(&filename); + let auxv_file = + std::fs::File::open(auxv_path).map_err(|e| InitError::IOError(filename, e))?; + let input = BufReader::new(auxv_file); + let reader = ProcfsAuxvIter::new(input); + self.auxv = reader + .filter_map(Result::ok) + .map(|x| (x.key, x.value)) + .collect(); + + if self.auxv.is_empty() { + Err(InitError::NoAuxvEntryFound(self.pid)) + } else { + Ok(()) + } + } + + fn enumerate_mappings(&mut self) -> Result<(), InitError> { + // linux_gate_loc is the beginning of the kernel's mapping of + // linux-gate.so in the process. It doesn't actually show up in the + // maps list as a filename, but it can be found using the AT_SYSINFO_EHDR + // aux vector entry, which gives the information necessary to special + // case its entry when creating the list of mappings. + // See http://www.trilithium.com/johan/2005/08/linux-gate/ for more + // information. + let linux_gate_loc = *self.auxv.get(&AT_SYSINFO_EHDR).unwrap_or(&0); + // Although the initial executable is usually the first mapping, it's not + // guaranteed (see http://crosbug.com/25355); therefore, try to use the + // actual entry point to find the mapping. + let at_entry; + #[cfg(any(target_arch = "arm", all(target_os = "android", target_arch = "x86")))] + { + at_entry = 9; + } + #[cfg(not(any(target_arch = "arm", all(target_os = "android", target_arch = "x86"))))] + { + at_entry = libc::AT_ENTRY; + } + + let entry_point_loc = *self.auxv.get(&at_entry).unwrap_or(&0); + let filename = format!("/proc/{}/maps", self.pid); + let errmap = |e| InitError::IOError(filename.clone(), e); + let maps_path = path::PathBuf::from(&filename); + let maps_file = std::fs::File::open(maps_path).map_err(errmap)?; + + use procfs_core::FromRead; + self.mappings = procfs_core::process::MemoryMaps::from_read(maps_file) + .ok() + .and_then(|maps| MappingInfo::aggregate(maps, linux_gate_loc).ok()) + .unwrap_or_default(); + + if entry_point_loc != 0 { + let mut swap_idx = None; + for (idx, module) in self.mappings.iter().enumerate() { + // If this module contains the entry-point, and it's not already the first + // one, then we need to make it be first. This is because the minidump + // format assumes the first module is the one that corresponds to the main + // executable (as codified in + // processor/minidump.cc:MinidumpModuleList::GetMainModule()). + if entry_point_loc >= module.start_address.try_into().unwrap() + && entry_point_loc < (module.start_address + module.size).try_into().unwrap() + { + swap_idx = Some(idx); + break; + } + } + if let Some(idx) = swap_idx { + self.mappings.swap(0, idx); + } + } + Ok(()) + } + + /// Read thread info from /proc/$pid/status. + /// Fill out the |tgid|, |ppid| and |pid| members of |info|. If unavailable, + /// these members are set to -1. Returns true if all three members are + /// available. + pub fn get_thread_info_by_index(&self, index: usize) -> Result<ThreadInfo, ThreadInfoError> { + if index > self.threads.len() { + return Err(ThreadInfoError::IndexOutOfBounds(index, self.threads.len())); + } + + ThreadInfo::create(self.pid, self.threads[index].tid) + } + + // Returns a valid stack pointer and the mapping that contains the stack. + // The stack pointer will usually point within this mapping, but it might + // not in case of stack overflows, hence the returned pointer might be + // different from the one that was passed in. + pub fn get_stack_info(&self, int_stack_pointer: usize) -> Result<(usize, usize), DumperError> { + // Round the stack pointer to the nearest page, this will cause us to + // capture data below the stack pointer which might still be relevant. + let mut stack_pointer = int_stack_pointer & !(self.page_size - 1); + let mut mapping = self.find_mapping(stack_pointer); + + // The guard page has been 1 MiB in size since kernel 4.12, older + // kernels used a 4 KiB one instead. + let guard_page_max_addr = stack_pointer + (1024 * 1024); + + // If we found no mapping, or the mapping we found has no permissions + // then we might have hit a guard page, try looking for a mapping in + // addresses past the stack pointer. Stack grows towards lower addresses + // on the platforms we care about so the stack should appear after the + // guard page. + while !Self::may_be_stack(mapping) && (stack_pointer <= guard_page_max_addr) { + stack_pointer += self.page_size; + mapping = self.find_mapping(stack_pointer); + } + + mapping + .map(|mapping| { + let valid_stack_pointer = if mapping.contains_address(stack_pointer) { + stack_pointer + } else { + mapping.start_address + }; + + let stack_len = mapping.size - (valid_stack_pointer - mapping.start_address); + (valid_stack_pointer, stack_len) + }) + .ok_or(DumperError::NoStackPointerMapping) + } + + fn may_be_stack(mapping: Option<&MappingInfo>) -> bool { + if let Some(mapping) = mapping { + return mapping + .permissions + .intersects(MMPermissions::READ | MMPermissions::WRITE); + } + + false + } + + pub fn sanitize_stack_copy( + &self, + stack_copy: &mut [u8], + stack_pointer: usize, + sp_offset: usize, + ) -> Result<(), DumperError> { + // We optimize the search for containing mappings in three ways: + // 1) We expect that pointers into the stack mapping will be common, so + // we cache that address range. + // 2) The last referenced mapping is a reasonable predictor for the next + // referenced mapping, so we test that first. + // 3) We precompute a bitfield based upon bits 32:32-n of the start and + // stop addresses, and use that to short circuit any values that can + // not be pointers. (n=11) + let defaced; + #[cfg(target_pointer_width = "64")] + { + defaced = 0x0defaced0defacedusize.to_ne_bytes(); + } + #[cfg(target_pointer_width = "32")] + { + defaced = 0x0defacedusize.to_ne_bytes(); + }; + // the bitfield length is 2^test_bits long. + let test_bits = 11; + // byte length of the corresponding array. + let array_size: usize = 1 << (test_bits - 3); + let array_mask = array_size - 1; + // The amount to right shift pointers by. This captures the top bits + // on 32 bit architectures. On 64 bit architectures this would be + // uninformative so we take the same range of bits. + let shift = 32 - 11; + // let MappingInfo* last_hit_mapping = nullptr; + // let MappingInfo* hit_mapping = nullptr; + let stack_mapping = self.find_mapping_no_bias(stack_pointer); + let mut last_hit_mapping: Option<&MappingInfo> = None; + // The magnitude below which integers are considered to be to be + // 'small', and not constitute a PII risk. These are included to + // avoid eliding useful register values. + let small_int_magnitude: isize = 4096; + + let mut could_hit_mapping = vec![0; array_size]; + // Initialize the bitfield such that if the (pointer >> shift)'th + // bit, modulo the bitfield size, is not set then there does not + // exist a mapping in mappings that would contain that pointer. + for mapping in &self.mappings { + if !mapping.is_executable() { + continue; + } + // For each mapping, work out the (unmodulo'ed) range of bits to + // set. + let mut start = mapping.start_address; + let mut end = start + mapping.size; + start >>= shift; + end >>= shift; + for bit in start..=end { + // Set each bit in the range, applying the modulus. + could_hit_mapping[(bit >> 3) & array_mask] |= 1 << (bit & 7); + } + } + + // Zero memory that is below the current stack pointer. + let offset = + (sp_offset + std::mem::size_of::<usize>() - 1) & !(std::mem::size_of::<usize>() - 1); + for x in &mut stack_copy[0..offset] { + *x = 0; + } + let mut chunks = stack_copy[offset..].chunks_exact_mut(std::mem::size_of::<usize>()); + + // Apply sanitization to each complete pointer-aligned word in the + // stack. + for sp in &mut chunks { + let addr = usize::from_ne_bytes(sp.to_vec().as_slice().try_into()?); + let addr_signed = isize::from_ne_bytes(sp.to_vec().as_slice().try_into()?); + + if addr <= small_int_magnitude as usize && addr_signed >= -small_int_magnitude { + continue; + } + + if let Some(stack_map) = stack_mapping { + if stack_map.contains_address(addr) { + continue; + } + } + if let Some(last_hit) = last_hit_mapping { + if last_hit.contains_address(addr) { + continue; + } + } + + let test = addr >> shift; + if could_hit_mapping[(test >> 3) & array_mask] & (1 << (test & 7)) != 0 { + if let Some(hit_mapping) = self.find_mapping_no_bias(addr) { + if hit_mapping.is_executable() { + last_hit_mapping = Some(hit_mapping); + continue; + } + } + } + sp.copy_from_slice(&defaced); + } + // Zero any partial word at the top of the stack, if alignment is + // such that that is required. + for sp in chunks.into_remainder() { + *sp = 0; + } + Ok(()) + } + + // Find the mapping which the given memory address falls in. + pub fn find_mapping(&self, address: usize) -> Option<&MappingInfo> { + self.mappings + .iter() + .find(|map| address >= map.start_address && address - map.start_address < map.size) + } + + // Find the mapping which the given memory address falls in. Uses the + // unadjusted mapping address range from the kernel, rather than the + // biased range. + pub fn find_mapping_no_bias(&self, address: usize) -> Option<&MappingInfo> { + self.mappings.iter().find(|map| { + address >= map.system_mapping_info.start_address + && address < map.system_mapping_info.end_address + }) + } + + fn parse_build_id<'data>( + elf_obj: &elf::Elf<'data>, + mem_slice: &'data [u8], + ) -> Option<&'data [u8]> { + if let Some(mut notes) = elf_obj.iter_note_headers(mem_slice) { + while let Some(Ok(note)) = notes.next() { + if (note.name == "GNU") && (note.n_type == elf::note::NT_GNU_BUILD_ID) { + return Some(note.desc); + } + } + } + if let Some(mut notes) = elf_obj.iter_note_sections(mem_slice, Some(".note.gnu.build-id")) { + while let Some(Ok(note)) = notes.next() { + if (note.name == "GNU") && (note.n_type == elf::note::NT_GNU_BUILD_ID) { + return Some(note.desc); + } + } + } + None + } + + pub fn elf_file_identifier_from_mapped_file(mem_slice: &[u8]) -> Result<Vec<u8>, DumperError> { + let elf_obj = elf::Elf::parse(mem_slice)?; + + if let Some(build_id) = Self::parse_build_id(&elf_obj, mem_slice) { + // Look for a build id note first. + Ok(build_id.to_vec()) + } else { + // Fall back on hashing the first page of the text section. + + // Attempt to locate the .text section of an ELF binary and generate + // a simple hash by XORing the first page worth of bytes into |result|. + for section in elf_obj.section_headers { + if section.sh_type != elf::section_header::SHT_PROGBITS { + continue; + } + if section.sh_flags & u64::from(elf::section_header::SHF_ALLOC) != 0 + && section.sh_flags & u64::from(elf::section_header::SHF_EXECINSTR) != 0 + { + let text_section = + &mem_slice[section.sh_offset as usize..][..section.sh_size as usize]; + // Only provide mem::size_of(MDGUID) bytes to keep identifiers produced by this + // function backwards-compatible. + let max_len = std::cmp::min(text_section.len(), 4096); + let mut result = vec![0u8; std::mem::size_of::<GUID>()]; + let mut offset = 0; + while offset < max_len { + for idx in 0..std::mem::size_of::<GUID>() { + if offset + idx >= text_section.len() { + break; + } + result[idx] ^= text_section[offset + idx]; + } + offset += std::mem::size_of::<GUID>(); + } + return Ok(result); + } + } + Err(DumperError::NoBuildIDFound) + } + } + + pub fn elf_identifier_for_mapping_index(&mut self, idx: usize) -> Result<Vec<u8>, DumperError> { + assert!(idx < self.mappings.len()); + + Self::elf_identifier_for_mapping(&mut self.mappings[idx], self.pid) + } + + pub fn elf_identifier_for_mapping( + mapping: &mut MappingInfo, + pid: Pid, + ) -> Result<Vec<u8>, DumperError> { + if !MappingInfo::is_mapped_file_safe_to_open(&mapping.name) { + return Err(DumperError::NotSafeToOpenMapping( + mapping.name.clone().unwrap_or_default(), + )); + } + + // Special-case linux-gate because it's not a real file. + if mapping.name.as_deref() == Some(LINUX_GATE_LIBRARY_NAME.as_ref()) { + if pid == std::process::id().try_into()? { + let mem_slice = unsafe { + std::slice::from_raw_parts(mapping.start_address as *const u8, mapping.size) + }; + return Self::elf_file_identifier_from_mapped_file(mem_slice); + } else { + let mem_slice = Self::copy_from_process( + pid, + mapping.start_address as *mut libc::c_void, + mapping.size, + )?; + return Self::elf_file_identifier_from_mapped_file(&mem_slice); + } + } + + let (filename, old_name) = mapping.fixup_deleted_file(pid)?; + + let mem_slice = MappingInfo::get_mmap(&Some(filename), mapping.offset)?; + let build_id = Self::elf_file_identifier_from_mapped_file(&mem_slice)?; + + // This means we switched from "/my/binary" to "/proc/1234/exe", change the mapping to + // remove the " (deleted)" portion. + if let Some(old_name) = old_name { + mapping.name = Some(old_name.into()); + } + Ok(build_id) + } +} diff --git a/third_party/rust/minidump-writer/src/linux/sections.rs b/third_party/rust/minidump-writer/src/linux/sections.rs new file mode 100644 index 0000000000..88d19f510e --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections.rs @@ -0,0 +1,20 @@ +pub mod app_memory; +pub mod exception_stream; +pub mod handle_data_stream; +pub mod mappings; +pub mod memory_info_list_stream; +pub mod memory_list_stream; +pub mod systeminfo_stream; +pub mod thread_list_stream; +pub mod thread_names_stream; + +use crate::{ + dir_section::DumpBuf, + errors::{self}, + linux::{ + minidump_writer::{self, MinidumpWriter}, + ptrace_dumper::PtraceDumper, + }, + mem_writer::*, + minidump_format::*, +}; diff --git a/third_party/rust/minidump-writer/src/linux/sections/app_memory.rs b/third_party/rust/minidump-writer/src/linux/sections/app_memory.rs new file mode 100644 index 0000000000..6d4a2e908f --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/app_memory.rs @@ -0,0 +1,23 @@ +use super::*; + +/// Write application-provided memory regions. +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, +) -> Result<(), errors::SectionAppMemoryError> { + for app_memory in &config.app_memory { + let data_copy = PtraceDumper::copy_from_process( + config.blamed_thread, + app_memory.ptr as *mut libc::c_void, + app_memory.length, + )?; + + let section = MemoryArrayWriter::write_bytes(buffer, &data_copy); + let desc = MDMemoryDescriptor { + start_of_memory_range: app_memory.ptr as u64, + memory: section.location(), + }; + config.memory_blocks.push(desc); + } + Ok(()) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/exception_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/exception_stream.rs new file mode 100644 index 0000000000..f7edda8d4c --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/exception_stream.rs @@ -0,0 +1,50 @@ +use super::minidump_writer::CrashingThreadContext; +use super::*; +use minidump_common::errors::ExceptionCodeLinux; + +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, +) -> Result<MDRawDirectory, errors::SectionExceptionStreamError> { + let exception = if let Some(context) = &config.crash_context { + MDException { + exception_code: context.inner.siginfo.ssi_signo, + exception_flags: context.inner.siginfo.ssi_code as u32, + exception_address: context.inner.siginfo.ssi_addr, + ..Default::default() + } + } else { + let addr = match &config.crashing_thread_context { + CrashingThreadContext::CrashContextPlusAddress((_, addr)) => *addr, + _ => 0, + }; + MDException { + exception_code: ExceptionCodeLinux::DUMP_REQUESTED as u32, + exception_address: addr as u64, + ..Default::default() + } + }; + + let thread_context = match config.crashing_thread_context { + CrashingThreadContext::CrashContextPlusAddress((ctx, _)) + | CrashingThreadContext::CrashContext(ctx) => ctx, + CrashingThreadContext::None => MDLocationDescriptor { + data_size: 0, + rva: 0, + }, + }; + + let stream = MDRawExceptionStream { + thread_id: config.blamed_thread as u32, + exception_record: exception, + __align: 0, + thread_context, + }; + let exc = MemoryWriter::alloc_with_val(buffer, stream)?; + let dirent = MDRawDirectory { + stream_type: MDStreamType::ExceptionStream as u32, + location: exc.location(), + }; + + Ok(dirent) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/handle_data_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/handle_data_stream.rs new file mode 100644 index 0000000000..b41c542d78 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/handle_data_stream.rs @@ -0,0 +1,84 @@ +use std::{ + ffi::{CString, OsString}, + fs::{self, DirEntry}, + mem::{self}, + os::unix::prelude::OsStrExt, + path::{Path, PathBuf}, +}; + +use crate::mem_writer::MemoryWriter; + +use super::*; + +fn file_stat(path: &Path) -> Option<libc::stat> { + let c_path = CString::new(path.as_os_str().as_bytes()).ok()?; + let mut stat = unsafe { std::mem::zeroed::<libc::stat>() }; + let result = unsafe { libc::stat(c_path.as_ptr(), &mut stat) }; + + if result == 0 { + Some(stat) + } else { + None + } +} + +fn direntry_to_descriptor(buffer: &mut DumpBuf, entry: &DirEntry) -> Option<MDRawHandleDescriptor> { + let handle = filename_to_fd(&entry.file_name())?; + let realpath = fs::read_link(entry.path()).ok()?; + let path_rva = write_string_to_location(buffer, realpath.to_string_lossy().as_ref()).ok()?; + let stat = file_stat(&entry.path())?; + + // TODO: We store the contents of `st_mode` into the `attributes` field, but + // we could also store a human-readable string of the file type inside + // `type_name_rva`. We might move this missing information (and + // more) inside a custom `MINIDUMP_HANDLE_OBJECT_INFORMATION_TYPE` blob. + // That would make this conversion loss-less. + Some(MDRawHandleDescriptor { + handle, + type_name_rva: 0, + object_name_rva: path_rva.rva, + attributes: stat.st_mode, + granted_access: 0, + handle_count: 0, + pointer_count: 0, + }) +} + +fn filename_to_fd(filename: &OsString) -> Option<u64> { + let filename = filename.to_string_lossy(); + filename.parse::<u64>().ok() +} + +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, +) -> Result<MDRawDirectory, errors::SectionHandleDataStreamError> { + let proc_fd_path = PathBuf::from(format!("/proc/{}/fd", config.process_id)); + let proc_fd_iter = fs::read_dir(proc_fd_path)?; + let descriptors: Vec<_> = proc_fd_iter + .filter_map(|entry| entry.ok()) + .filter_map(|entry| direntry_to_descriptor(buffer, &entry)) + .collect(); + let number_of_descriptors = descriptors.len() as u32; + + let stream_header = MemoryWriter::<MDRawHandleDataStream>::alloc_with_val( + buffer, + MDRawHandleDataStream { + size_of_header: mem::size_of::<MDRawHandleDataStream>() as u32, + size_of_descriptor: mem::size_of::<MDRawHandleDescriptor>() as u32, + number_of_descriptors, + reserved: 0, + }, + )?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::HandleDataStream as u32, + location: stream_header.location(), + }; + + let descriptor_list = + MemoryArrayWriter::<MDRawHandleDescriptor>::alloc_from_iter(buffer, descriptors)?; + + dirent.location.data_size += descriptor_list.location().data_size; + Ok(dirent) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/mappings.rs b/third_party/rust/minidump-writer/src/linux/sections/mappings.rs new file mode 100644 index 0000000000..de19c54068 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/mappings.rs @@ -0,0 +1,98 @@ +use super::*; +use crate::linux::maps_reader::MappingInfo; + +/// Write information about the mappings in effect. Because we are using the +/// minidump format, the information about the mappings is pretty limited. +/// Because of this, we also include the full, unparsed, /proc/$x/maps file in +/// another stream in the file. +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, + dumper: &mut PtraceDumper, +) -> Result<MDRawDirectory, errors::SectionMappingsError> { + let mut modules = Vec::new(); + + // First write all the mappings from the dumper + for map_idx in 0..dumper.mappings.len() { + // If the mapping is uninteresting, or if + // there is caller-provided information about this mapping + // in the user_mapping_list list, skip it + + if !dumper.mappings[map_idx].is_interesting() + || dumper.mappings[map_idx].is_contained_in(&config.user_mapping_list) + { + continue; + } + // Note: elf_identifier_for_mapping_index() can manipulate the |mapping.name|. + let identifier = dumper + .elf_identifier_for_mapping_index(map_idx) + .unwrap_or_default(); + + // If the identifier is all 0, its an uninteresting mapping (bmc#1676109) + if identifier.is_empty() || identifier.iter().all(|&x| x == 0) { + continue; + } + + let module = fill_raw_module(buffer, &dumper.mappings[map_idx], &identifier)?; + modules.push(module); + } + + // Next write all the mappings provided by the caller + for user in &config.user_mapping_list { + // GUID was provided by caller. + let module = fill_raw_module(buffer, &user.mapping, &user.identifier)?; + modules.push(module); + } + + let list_header = MemoryWriter::<u32>::alloc_with_val(buffer, modules.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ModuleListStream as u32, + location: list_header.location(), + }; + + if !modules.is_empty() { + let mapping_list = MemoryArrayWriter::<MDRawModule>::alloc_from_iter(buffer, modules)?; + dirent.location.data_size += mapping_list.location().data_size; + } + + Ok(dirent) +} + +fn fill_raw_module( + buffer: &mut DumpBuf, + mapping: &MappingInfo, + identifier: &[u8], +) -> Result<MDRawModule, errors::SectionMappingsError> { + let cv_record = if identifier.is_empty() { + // Just zeroes + Default::default() + } else { + let cv_signature = crate::minidump_format::format::CvSignature::Elf as u32; + let array_size = std::mem::size_of_val(&cv_signature) + identifier.len(); + + let mut sig_section = MemoryArrayWriter::<u8>::alloc_array(buffer, array_size)?; + for (index, val) in cv_signature + .to_ne_bytes() + .iter() + .chain(identifier.iter()) + .enumerate() + { + sig_section.set_value_at(buffer, *val, index)?; + } + sig_section.location() + }; + + let (file_path, _) = mapping + .get_mapping_effective_path_and_name() + .map_err(|e| errors::SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?; + let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?; + + Ok(MDRawModule { + base_of_image: mapping.start_address as u64, + size_of_image: mapping.size as u32, + cv_record, + module_name_rva: name_header.rva, + ..Default::default() + }) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/memory_info_list_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/memory_info_list_stream.rs new file mode 100644 index 0000000000..c3cd728c54 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/memory_info_list_stream.rs @@ -0,0 +1,68 @@ +use super::*; +use minidump_common::format::{MemoryProtection, MemoryState, MemoryType}; +use procfs_core::{process::MMPermissions, FromRead}; + +/// Write a MemoryInfoListStream using information from procfs. +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, +) -> Result<MDRawDirectory, errors::SectionMemInfoListError> { + let maps = procfs_core::process::MemoryMaps::from_file(std::path::PathBuf::from(format!( + "/proc/{}/maps", + config.blamed_thread + )))?; + + let list_header = MemoryWriter::alloc_with_val( + buffer, + MDMemoryInfoList { + size_of_header: std::mem::size_of::<MDMemoryInfoList>() as u32, + size_of_entry: std::mem::size_of::<MDMemoryInfo>() as u32, + number_of_entries: maps.len() as u64, + }, + )?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::MemoryInfoListStream as u32, + location: list_header.location(), + }; + + let block_list = MemoryArrayWriter::<MDMemoryInfo>::alloc_from_iter( + buffer, + maps.iter().map(|mm| MDMemoryInfo { + base_address: mm.address.0, + allocation_base: mm.address.0, + allocation_protection: get_memory_protection(mm.perms).bits(), + __alignment1: 0, + region_size: mm.address.1 - mm.address.0, + state: MemoryState::MEM_COMMIT.bits(), + protection: get_memory_protection(mm.perms).bits(), + _type: if mm.perms.contains(MMPermissions::PRIVATE) { + MemoryType::MEM_PRIVATE + } else { + MemoryType::MEM_MAPPED + } + .bits(), + __alignment2: 0, + }), + )?; + + dirent.location.data_size += block_list.location().data_size; + + Ok(dirent) +} + +fn get_memory_protection(permissions: MMPermissions) -> MemoryProtection { + let read = permissions.contains(MMPermissions::READ); + let write = permissions.contains(MMPermissions::WRITE); + let exec = permissions.contains(MMPermissions::EXECUTE); + match (read, write, exec) { + (false, false, false) => MemoryProtection::PAGE_NOACCESS, + (false, false, true) => MemoryProtection::PAGE_EXECUTE, + (true, false, false) => MemoryProtection::PAGE_READONLY, + (true, false, true) => MemoryProtection::PAGE_EXECUTE_READ, + // No support for write-only + (true | false, true, false) => MemoryProtection::PAGE_READWRITE, + // No support for execute+write-only + (true | false, true, true) => MemoryProtection::PAGE_EXECUTE_READWRITE, + } +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/memory_list_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/memory_list_stream.rs new file mode 100644 index 0000000000..7f49779204 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/memory_list_stream.rs @@ -0,0 +1,21 @@ +use super::*; + +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, +) -> Result<MDRawDirectory, errors::SectionMemListError> { + let list_header = + MemoryWriter::<u32>::alloc_with_val(buffer, config.memory_blocks.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::MemoryListStream as u32, + location: list_header.location(), + }; + + let block_list = + MemoryArrayWriter::<MDMemoryDescriptor>::alloc_from_array(buffer, &config.memory_blocks)?; + + dirent.location.data_size += block_list.location().data_size; + + Ok(dirent) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/systeminfo_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/systeminfo_stream.rs new file mode 100644 index 0000000000..a298c00d15 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/systeminfo_stream.rs @@ -0,0 +1,23 @@ +use super::*; +use crate::linux::dumper_cpu_info as dci; + +pub fn write(buffer: &mut DumpBuf) -> Result<MDRawDirectory, errors::SectionSystemInfoError> { + let mut info_section = MemoryWriter::<MDRawSystemInfo>::alloc(buffer)?; + let dirent = MDRawDirectory { + stream_type: MDStreamType::SystemInfoStream as u32, + location: info_section.location(), + }; + + let (platform_id, os_version) = dci::os_information(); + let os_version_loc = write_string_to_location(buffer, &os_version)?; + + // SAFETY: POD + let mut info = unsafe { std::mem::zeroed::<MDRawSystemInfo>() }; + info.platform_id = platform_id as u32; + info.csd_version_rva = os_version_loc.rva; + + dci::write_cpu_information(&mut info)?; + + info_section.set_value(buffer, info)?; + Ok(dirent) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/thread_list_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/thread_list_stream.rs new file mode 100644 index 0000000000..648aef9869 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/thread_list_stream.rs @@ -0,0 +1,232 @@ +use std::cmp::min; + +use super::*; +use crate::{minidump_cpu::RawContextCPU, minidump_writer::CrashingThreadContext}; + +// The following kLimit* constants are for when minidump_size_limit_ is set +// and the minidump size might exceed it. +// +// Estimate for how big each thread's stack will be (in bytes). +const LIMIT_AVERAGE_THREAD_STACK_LENGTH: usize = 8 * 1024; +// Number of threads whose stack size we don't want to limit. These base +// threads will simply be the first N threads returned by the dumper (although +// the crashing thread will never be limited). Threads beyond this count are +// the extra threads. +const LIMIT_BASE_THREAD_COUNT: usize = 20; +// Maximum stack size to dump for any extra thread (in bytes). +const LIMIT_MAX_EXTRA_THREAD_STACK_LEN: usize = 2 * 1024; +// Make sure this number of additional bytes can fit in the minidump +// (exclude the stack data). +const LIMIT_MINIDUMP_FUDGE_FACTOR: u64 = 64 * 1024; + +#[derive(Debug, Clone, Copy)] +enum MaxStackLen { + None, + Len(usize), +} + +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, + dumper: &PtraceDumper, +) -> Result<MDRawDirectory, errors::SectionThreadListError> { + let num_threads = dumper.threads.len(); + // Memory looks like this: + // <num_threads><thread_1><thread_2>... + + let list_header = MemoryWriter::<u32>::alloc_with_val(buffer, num_threads as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ThreadListStream as u32, + location: list_header.location(), + }; + + let mut thread_list = MemoryArrayWriter::<MDRawThread>::alloc_array(buffer, num_threads)?; + dirent.location.data_size += thread_list.location().data_size; + // If there's a minidump size limit, check if it might be exceeded. Since + // most of the space is filled with stack data, just check against that. + // If this expects to exceed the limit, set extra_thread_stack_len such + // that any thread beyond the first kLimitBaseThreadCount threads will + // have only kLimitMaxExtraThreadStackLen bytes dumped. + let mut extra_thread_stack_len = MaxStackLen::None; // default to no maximum + if let Some(minidump_size_limit) = config.minidump_size_limit { + let estimated_total_stack_size = (num_threads * LIMIT_AVERAGE_THREAD_STACK_LENGTH) as u64; + let curr_pos = buffer.position(); + let estimated_minidump_size = + curr_pos + estimated_total_stack_size + LIMIT_MINIDUMP_FUDGE_FACTOR; + if estimated_minidump_size > minidump_size_limit { + extra_thread_stack_len = MaxStackLen::Len(LIMIT_MAX_EXTRA_THREAD_STACK_LEN); + } + } + + for (idx, item) in dumper.threads.clone().iter().enumerate() { + let mut thread = MDRawThread { + thread_id: item.tid.try_into()?, + suspend_count: 0, + priority_class: 0, + priority: 0, + teb: 0, + stack: MDMemoryDescriptor::default(), + thread_context: MDLocationDescriptor::default(), + }; + + // We have a different source of information for the crashing thread. If + // we used the actual state of the thread we would find it running in the + // signal handler with the alternative stack, which would be deeply + // unhelpful. + if config.crash_context.is_some() && thread.thread_id == config.blamed_thread as u32 { + let crash_context = config.crash_context.as_ref().unwrap(); + let instruction_ptr = crash_context.get_instruction_pointer(); + let stack_pointer = crash_context.get_stack_pointer(); + fill_thread_stack( + config, + buffer, + dumper, + &mut thread, + instruction_ptr, + stack_pointer, + MaxStackLen::None, + )?; + // Copy 256 bytes around crashing instruction pointer to minidump. + let ip_memory_size: usize = 256; + // Bound it to the upper and lower bounds of the memory map + // it's contained within. If it's not in mapped memory, + // don't bother trying to write it. + for mapping in &dumper.mappings { + if instruction_ptr < mapping.start_address + || instruction_ptr >= mapping.start_address + mapping.size + { + continue; + } + // Try to get 128 bytes before and after the IP, but + // settle for whatever's available. + let mut ip_memory_d = MDMemoryDescriptor { + start_of_memory_range: std::cmp::max( + mapping.start_address, + instruction_ptr - ip_memory_size / 2, + ) as u64, + ..Default::default() + }; + + let end_of_range = std::cmp::min( + mapping.start_address + mapping.size, + instruction_ptr + ip_memory_size / 2, + ) as u64; + ip_memory_d.memory.data_size = + (end_of_range - ip_memory_d.start_of_memory_range) as u32; + + let memory_copy = PtraceDumper::copy_from_process( + thread.thread_id as i32, + ip_memory_d.start_of_memory_range as *mut libc::c_void, + ip_memory_d.memory.data_size as usize, + )?; + + let mem_section = MemoryArrayWriter::alloc_from_array(buffer, &memory_copy)?; + ip_memory_d.memory = mem_section.location(); + config.memory_blocks.push(ip_memory_d); + + break; + } + // let cpu = MemoryWriter::alloc(buffer, &memory_copy)?; + let mut cpu: RawContextCPU = Default::default(); + let crash_context = config.crash_context.as_ref().unwrap(); + crash_context.fill_cpu_context(&mut cpu); + let cpu_section = MemoryWriter::alloc_with_val(buffer, cpu)?; + thread.thread_context = cpu_section.location(); + + config.crashing_thread_context = + CrashingThreadContext::CrashContext(cpu_section.location()); + } else { + let info = dumper.get_thread_info_by_index(idx)?; + let max_stack_len = + if config.minidump_size_limit.is_some() && idx >= LIMIT_BASE_THREAD_COUNT { + extra_thread_stack_len + } else { + MaxStackLen::None // default to no maximum for this thread + }; + let instruction_ptr = info.get_instruction_pointer(); + fill_thread_stack( + config, + buffer, + dumper, + &mut thread, + instruction_ptr, + info.stack_pointer, + max_stack_len, + )?; + + let mut cpu = RawContextCPU::default(); + info.fill_cpu_context(&mut cpu); + let cpu_section = MemoryWriter::<RawContextCPU>::alloc_with_val(buffer, cpu)?; + thread.thread_context = cpu_section.location(); + if item.tid == config.blamed_thread { + // This is the crashing thread of a live process, but + // no context was provided, so set the crash address + // while the instruction pointer is already here. + config.crashing_thread_context = CrashingThreadContext::CrashContextPlusAddress(( + cpu_section.location(), + instruction_ptr, + )); + } + } + thread_list.set_value_at(buffer, thread, idx)?; + } + Ok(dirent) +} + +fn fill_thread_stack( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, + dumper: &PtraceDumper, + thread: &mut MDRawThread, + instruction_ptr: usize, + stack_ptr: usize, + max_stack_len: MaxStackLen, +) -> Result<(), errors::SectionThreadListError> { + thread.stack.start_of_memory_range = stack_ptr.try_into()?; + thread.stack.memory.data_size = 0; + thread.stack.memory.rva = buffer.position() as u32; + + if let Ok((valid_stack_ptr, stack_len)) = dumper.get_stack_info(stack_ptr) { + let stack_len = if let MaxStackLen::Len(max_stack_len) = max_stack_len { + min(stack_len, max_stack_len) + } else { + stack_len + }; + + let mut stack_bytes = PtraceDumper::copy_from_process( + thread.thread_id.try_into()?, + valid_stack_ptr as *mut libc::c_void, + stack_len, + )?; + let stack_pointer_offset = stack_ptr.saturating_sub(valid_stack_ptr); + if config.skip_stacks_if_mapping_unreferenced { + if let Some(principal_mapping) = &config.principal_mapping { + let low_addr = principal_mapping.system_mapping_info.start_address; + let high_addr = principal_mapping.system_mapping_info.end_address; + if (instruction_ptr < low_addr || instruction_ptr > high_addr) + && !principal_mapping + .stack_has_pointer_to_mapping(&stack_bytes, stack_pointer_offset) + { + return Ok(()); + } + } else { + return Ok(()); + } + } + + if config.sanitize_stack { + dumper.sanitize_stack_copy(&mut stack_bytes, stack_ptr, stack_pointer_offset)?; + } + + let stack_location = MDLocationDescriptor { + data_size: stack_bytes.len() as u32, + rva: buffer.position() as u32, + }; + buffer.write_all(&stack_bytes); + thread.stack.start_of_memory_range = valid_stack_ptr as u64; + thread.stack.memory = stack_location; + config.memory_blocks.push(thread.stack); + } + Ok(()) +} diff --git a/third_party/rust/minidump-writer/src/linux/sections/thread_names_stream.rs b/third_party/rust/minidump-writer/src/linux/sections/thread_names_stream.rs new file mode 100644 index 0000000000..bd8682b28a --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/sections/thread_names_stream.rs @@ -0,0 +1,33 @@ +use super::*; + +pub fn write( + buffer: &mut DumpBuf, + dumper: &PtraceDumper, +) -> Result<MDRawDirectory, errors::SectionThreadNamesError> { + // Only count threads that have a name + let num_threads = dumper.threads.iter().filter(|t| t.name.is_some()).count(); + // Memory looks like this: + // <num_threads><thread_1><thread_2>... + + let list_header = MemoryWriter::<u32>::alloc_with_val(buffer, num_threads as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ThreadNamesStream as u32, + location: list_header.location(), + }; + + let mut thread_list = MemoryArrayWriter::<MDRawThreadName>::alloc_array(buffer, num_threads)?; + dirent.location.data_size += thread_list.location().data_size; + + for (idx, item) in dumper.threads.iter().enumerate() { + if let Some(name) = &item.name { + let pos = write_string_to_location(buffer, name)?; + let thread = MDRawThreadName { + thread_id: item.tid.try_into()?, + thread_name_rva: pos.rva.into(), + }; + thread_list.set_value_at(buffer, thread, idx)?; + } + } + Ok(dirent) +} diff --git a/third_party/rust/minidump-writer/src/linux/thread_info.rs b/third_party/rust/minidump-writer/src/linux/thread_info.rs new file mode 100644 index 0000000000..5bb1f9e8fb --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/thread_info.rs @@ -0,0 +1,165 @@ +use crate::errors::ThreadInfoError; +use nix::{errno::Errno, sys::ptrace, unistd}; +use std::{ + io::{self, BufRead}, + path, +}; + +type Result<T> = std::result::Result<T, ThreadInfoError>; + +pub type Pid = i32; + +cfg_if::cfg_if! { + if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { + mod x86; + pub type ThreadInfo = x86::ThreadInfoX86; + } else if #[cfg(target_arch = "arm")] { + mod arm; + pub type ThreadInfo = arm::ThreadInfoArm; + } else if #[cfg(target_arch = "aarch64")] { + mod aarch64; + pub type ThreadInfo = aarch64::ThreadInfoAarch64; + } else if #[cfg(target_arch = "mips")] { + mod mips; + pub type ThreadInfo = mips::ThreadInfoMips; + } +} + +#[derive(Debug)] +#[allow(non_camel_case_types, dead_code)] +enum NT_Elf { + NT_NONE = 0, + NT_PRSTATUS = 1, + NT_PRFPREGSET = 2, + //NT_PRPSINFO = 3, + //NT_TASKSTRUCT = 4, + //NT_AUXV = 6, + NT_ARM_VFP = 0x400, // ARM VFP/NEON registers +} + +#[inline] +pub fn to_u128(slice: &[u32]) -> &[u128] { + unsafe { std::slice::from_raw_parts(slice.as_ptr().cast(), slice.len().saturating_div(4)) } +} + +#[inline] +pub fn copy_registers(dst: &mut [u128], src: &[u128]) { + let to_copy = std::cmp::min(dst.len(), src.len()); + dst[..to_copy].copy_from_slice(&src[..to_copy]); +} + +#[inline] +pub fn copy_u32_registers(dst: &mut [u128], src: &[u32]) { + copy_registers(dst, to_u128(src)); +} + +trait CommonThreadInfo { + fn get_ppid_and_tgid(tid: Pid) -> Result<(Pid, Pid)> { + let mut ppid = -1; + let mut tgid = -1; + + let status_path = path::PathBuf::from(format!("/proc/{}/status", tid)); + let status_file = std::fs::File::open(status_path)?; + for line in io::BufReader::new(status_file).lines() { + let l = line?; + let start = l + .get(0..6) + .ok_or_else(|| ThreadInfoError::InvalidProcStatusFile(tid, l.clone()))?; + match start { + "Tgid:\t" => { + tgid = l + .get(6..) + .ok_or_else(|| ThreadInfoError::InvalidProcStatusFile(tid, l.clone()))? + .parse::<Pid>()?; + } + "PPid:\t" => { + ppid = l + .get(6..) + .ok_or_else(|| ThreadInfoError::InvalidProcStatusFile(tid, l.clone()))? + .parse::<Pid>()?; + } + _ => continue, + } + } + if ppid == -1 || tgid == -1 { + return Err(ThreadInfoError::InvalidPid( + format!("/proc/{}/status", tid), + ppid, + tgid, + )); + } + Ok((ppid, tgid)) + } + + /// SLIGHTLY MODIFIED COPY FROM CRATE nix + /// Function for ptrace requests that return values from the data field. + /// Some ptrace get requests populate structs or larger elements than `c_long` + /// and therefore use the data field to return values. This function handles these + /// requests. + fn ptrace_get_data<T>( + request: ptrace::RequestType, + flag: Option<NT_Elf>, + pid: nix::unistd::Pid, + ) -> Result<T> { + let mut data = std::mem::MaybeUninit::uninit(); + let res = unsafe { + libc::ptrace( + request, + libc::pid_t::from(pid), + flag.unwrap_or(NT_Elf::NT_NONE), + data.as_mut_ptr(), + ) + }; + Errno::result(res)?; + Ok(unsafe { data.assume_init() }) + } + + /// SLIGHTLY MODIFIED COPY FROM CRATE nix + /// Function for ptrace requests that return values from the data field. + /// Some ptrace get requests populate structs or larger elements than `c_long` + /// and therefore use the data field to return values. This function handles these + /// requests. + fn ptrace_get_data_via_io<T>( + request: ptrace::RequestType, + flag: Option<NT_Elf>, + pid: nix::unistd::Pid, + ) -> Result<T> { + let mut data = std::mem::MaybeUninit::<T>::uninit(); + let io = libc::iovec { + iov_base: data.as_mut_ptr().cast(), + iov_len: std::mem::size_of::<T>(), + }; + let res = unsafe { + libc::ptrace( + request, + libc::pid_t::from(pid), + flag.unwrap_or(NT_Elf::NT_NONE), + &io as *const _, + ) + }; + Errno::result(res)?; + Ok(unsafe { data.assume_init() }) + } + + /// COPY FROM CRATE nix BECAUSE ITS NOT PUBLIC + fn ptrace_peek( + request: ptrace::RequestType, + pid: unistd::Pid, + addr: ptrace::AddressType, + data: *mut libc::c_void, + ) -> nix::Result<libc::c_long> { + let ret = unsafe { + Errno::clear(); + libc::ptrace(request, libc::pid_t::from(pid), addr, data) + }; + match Errno::result(ret) { + Ok(..) | Err(Errno::UnknownErrno) => Ok(ret), + err @ Err(..) => err, + } + } +} +impl ThreadInfo { + pub fn create(pid: Pid, tid: Pid) -> std::result::Result<Self, ThreadInfoError> { + Self::create_impl(pid, tid) + } +} diff --git a/third_party/rust/minidump-writer/src/linux/thread_info/aarch64.rs b/third_party/rust/minidump-writer/src/linux/thread_info/aarch64.rs new file mode 100644 index 0000000000..3eff56d8fa --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/thread_info/aarch64.rs @@ -0,0 +1,104 @@ +use super::{CommonThreadInfo, NT_Elf, Pid}; +use crate::{ + errors::ThreadInfoError, + minidump_cpu::{RawContextCPU, FP_REG_COUNT, GP_REG_COUNT}, +}; +use nix::sys::ptrace; + +/// https://github.com/rust-lang/libc/pull/2719 +#[derive(Debug)] +#[allow(non_camel_case_types)] +pub struct user_fpsimd_struct { + pub vregs: [u128; 32], + pub fpsr: u32, + pub fpcr: u32, +} + +type Result<T> = std::result::Result<T, ThreadInfoError>; + +#[cfg(target_arch = "aarch64")] +#[derive(Debug)] +pub struct ThreadInfoAarch64 { + pub stack_pointer: usize, + pub tgid: Pid, // thread group id + pub ppid: Pid, // parent process + pub regs: libc::user_regs_struct, + pub fpregs: user_fpsimd_struct, +} + +impl CommonThreadInfo for ThreadInfoAarch64 {} + +impl ThreadInfoAarch64 { + pub fn get_instruction_pointer(&self) -> usize { + self.regs.pc as usize + } + + // nix currently doesn't support PTRACE_GETREGSET, so we have to do it ourselves + fn getregset(pid: Pid) -> Result<libc::user_regs_struct> { + Self::ptrace_get_data_via_io( + 0x4204 as ptrace::RequestType, // PTRACE_GETREGSET + Some(NT_Elf::NT_PRSTATUS), + nix::unistd::Pid::from_raw(pid), + ) + } + + fn getregs(pid: Pid) -> Result<libc::user_regs_struct> { + // TODO: nix restricts PTRACE_GETREGS to arm android for some reason + Self::ptrace_get_data( + 12 as ptrace::RequestType, // PTRACE_GETREGS + None, + nix::unistd::Pid::from_raw(pid), + ) + } + + // nix currently doesn't support PTRACE_GETREGSET, so we have to do it ourselves + fn getfpregset(pid: Pid) -> Result<user_fpsimd_struct> { + Self::ptrace_get_data_via_io( + 0x4204 as ptrace::RequestType, // PTRACE_GETREGSET + Some(NT_Elf::NT_PRFPREGSET), + nix::unistd::Pid::from_raw(pid), + ) + } + + // nix currently doesn't support PTRACE_GETFPREGS, so we have to do it ourselves + fn getfpregs(pid: Pid) -> Result<user_fpsimd_struct> { + Self::ptrace_get_data( + 14 as ptrace::RequestType, // PTRACE_GETFPREGS + None, + nix::unistd::Pid::from_raw(pid), + ) + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = + minidump_common::format::ContextFlagsArm64Old::CONTEXT_ARM64_OLD_FULL.bits() as u64; + + out.cpsr = self.regs.pstate as u32; + out.iregs[..GP_REG_COUNT].copy_from_slice(&self.regs.regs[..GP_REG_COUNT]); + out.sp = self.regs.sp; + // Note that in breakpad this was the last member of the iregs field + // which was 33 in length, but in rust-minidump it is its own separate + // field instead + out.pc = self.regs.pc; + + out.fpsr = self.fpregs.fpsr; + out.fpcr = self.fpregs.fpcr; + out.float_regs[..FP_REG_COUNT].copy_from_slice(&self.fpregs.vregs[..FP_REG_COUNT]); + } + + pub fn create_impl(_pid: Pid, tid: Pid) -> Result<Self> { + let (ppid, tgid) = Self::get_ppid_and_tgid(tid)?; + let regs = Self::getregset(tid).or_else(|_| Self::getregs(tid))?; + let fpregs = Self::getfpregset(tid).or_else(|_| Self::getfpregs(tid))?; + + let stack_pointer = regs.sp as usize; + + Ok(Self { + stack_pointer, + tgid, + ppid, + regs, + fpregs, + }) + } +} diff --git a/third_party/rust/minidump-writer/src/linux/thread_info/arm.rs b/third_party/rust/minidump-writer/src/linux/thread_info/arm.rs new file mode 100644 index 0000000000..954aec61cf --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/thread_info/arm.rs @@ -0,0 +1,81 @@ +use super::{CommonThreadInfo, NT_Elf, Pid}; +use crate::{errors::ThreadInfoError, minidump_cpu::RawContextCPU}; +use nix::sys::ptrace; + +type Result<T> = std::result::Result<T, ThreadInfoError>; + +// Not defined by libc because this works only for cores support VFP +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug, Eq, Hash, PartialEq, Copy, Clone, Default)] +pub struct user_fpregs_struct { + pub fpregs: [u64; 32], + pub fpscr: u32, +} + +#[repr(C)] +#[derive(Debug, Eq, Hash, PartialEq, Copy, Clone, Default)] +pub struct user_regs_struct { + uregs: [u32; 18], +} + +#[derive(Debug)] +pub struct ThreadInfoArm { + pub stack_pointer: usize, + pub tgid: Pid, // thread group id + pub ppid: Pid, // parent process + pub regs: user_regs_struct, + pub fpregs: user_fpregs_struct, +} + +impl CommonThreadInfo for ThreadInfoArm {} + +impl ThreadInfoArm { + // nix currently doesn't support PTRACE_GETFPREGS, so we have to do it ourselves + fn getfpregs(pid: Pid) -> Result<user_fpregs_struct> { + Self::ptrace_get_data_via_io( + 0x4204 as ptrace::RequestType, // PTRACE_GETREGSET + Some(NT_Elf::NT_ARM_VFP), + nix::unistd::Pid::from_raw(pid), + ) + } + + // nix currently doesn't support PTRACE_GETREGS, so we have to do it ourselves + fn getregs(pid: Pid) -> Result<user_regs_struct> { + Self::ptrace_get_data::<user_regs_struct>( + ptrace::Request::PTRACE_GETREGS as ptrace::RequestType, + None, + nix::unistd::Pid::from_raw(pid), + ) + } + + pub fn get_instruction_pointer(&self) -> usize { + self.regs.uregs[15] as usize + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = + crate::minidump_format::format::ContextFlagsArm::CONTEXT_ARM_FULL.bits(); + + out.iregs.copy_from_slice(&self.regs.uregs[..16]); + out.cpsr = self.regs.uregs[16]; + out.float_save.fpscr = self.fpregs.fpscr as u64; + out.float_save.regs = self.fpregs.fpregs; + } + + pub fn create_impl(_pid: Pid, tid: Pid) -> Result<Self> { + let (ppid, tgid) = Self::get_ppid_and_tgid(tid)?; + let regs = Self::getregs(tid)?; + let fpregs = Self::getfpregs(tid).unwrap_or(Default::default()); + + let stack_pointer = regs.uregs[13] as usize; + + Ok(ThreadInfoArm { + stack_pointer, + tgid, + ppid, + regs, + fpregs, + }) + } +} diff --git a/third_party/rust/minidump-writer/src/linux/thread_info/mips.rs b/third_party/rust/minidump-writer/src/linux/thread_info/mips.rs new file mode 100644 index 0000000000..ff263f5f7b --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/thread_info/mips.rs @@ -0,0 +1,56 @@ +use super::Pid; +use crate::errors::ThreadInfoError; +use libc; + +type Result<T> = std::result::Result<T, ThreadInfoError>; + +#[derive(Debug)] +pub struct ThreadInfoMips { + pub stack_pointer: libc::c_ulonglong, + pub tgid: Pid, // thread group id + pub ppid: Pid, // parent process + // Use the structure defined in <sys/ucontext.h> + pub mcontext: libc::mcontext_t, +} + +impl ThreadInfoMips { + pub fn get_instruction_pointer(&self) -> libc::c_ulonglong { + self.mcontext.pc + } + + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + // #if _MIPS_SIM == _ABI64 + // out->context_flags = MD_CONTEXT_MIPS64_FULL; + // #elif _MIPS_SIM == _ABIO32 + // out->context_flags = MD_CONTEXT_MIPS_FULL; + for idx in 0..MD_CONTEXT_MIPS_GPR_COUNT { + out.iregs[idx] = self.mcontext.gregs[idx]; + } + + out.mdhi = self.mcontext.mdhi; + out.mdlo = self.mcontext.mdlo; + out.dsp_control = self.mcontext.dsp; + + out.hi[0] = self.mcontext.hi1; + out.lo[0] = self.mcontext.lo1; + out.hi[1] = self.mcontext.hi2; + out.lo[1] = self.mcontext.lo2; + out.hi[2] = self.mcontext.hi3; + out.lo[2] = self.mcontext.lo3; + + out.epc = self.mcontext.pc; + out.badvaddr = 0; // Not stored in mcontext + out.status = 0; // Not stored in mcontext + out.cause = 0; // Not stored in mcontext + + for idx in 0..MD_FLOATINGSAVEAREA_MIPS_FPR_COUNT { + out.float_save.regs[idx] = self.mcontext.fpregs.fp_r.fp_fregs[idx]._fp_fregs; + } + + out.float_save.fpcsr = mcontext.fpc_csr; + + // #if _MIPS_SIM == _ABIO32 + // out.float_save.fir = self.mcontext.fpc_eir; + // #endif + } +} diff --git a/third_party/rust/minidump-writer/src/linux/thread_info/x86.rs b/third_party/rust/minidump-writer/src/linux/thread_info/x86.rs new file mode 100644 index 0000000000..aa1ea71ba5 --- /dev/null +++ b/third_party/rust/minidump-writer/src/linux/thread_info/x86.rs @@ -0,0 +1,393 @@ +use super::{CommonThreadInfo, NT_Elf, Pid}; +use crate::{errors::ThreadInfoError, minidump_cpu::RawContextCPU, minidump_format::format}; +use core::mem::size_of_val; +#[cfg(all(not(target_os = "android"), target_arch = "x86"))] +use libc::user_fpxregs_struct; +#[cfg(not(all(target_os = "android", target_arch = "x86")))] +use libc::{user, user_fpregs_struct, user_regs_struct}; +use nix::sys::ptrace; +use scroll::Pwrite; + +type Result<T> = std::result::Result<T, ThreadInfoError>; + +// Not defined by libc on Android +#[cfg(all(target_os = "android", target_arch = "x86"))] +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct user_regs_struct { + pub ebx: libc::c_long, + pub ecx: libc::c_long, + pub edx: libc::c_long, + pub esi: libc::c_long, + pub edi: libc::c_long, + pub ebp: libc::c_long, + pub eax: libc::c_long, + pub xds: libc::c_long, + pub xes: libc::c_long, + pub xfs: libc::c_long, + pub xgs: libc::c_long, + pub orig_eax: libc::c_long, + pub eip: libc::c_long, + pub xcs: libc::c_long, + pub eflags: libc::c_long, + pub esp: libc::c_long, + pub xss: libc::c_long, +} + +// Not defined by libc on Android +#[cfg(all(target_os = "android", target_arch = "x86"))] +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct user_fpxregs_struct { + pub cwd: libc::c_ushort, + pub swd: libc::c_ushort, + pub twd: libc::c_ushort, + pub fop: libc::c_ushort, + pub fip: libc::c_long, + pub fcs: libc::c_long, + pub foo: libc::c_long, + pub fos: libc::c_long, + pub mxcsr: libc::c_long, + __reserved: libc::c_long, + pub st_space: [libc::c_long; 32], + pub xmm_space: [libc::c_long; 32], + padding: [libc::c_long; 56], +} + +// Not defined by libc on Android +#[cfg(all(target_os = "android", target_arch = "x86"))] +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct user_fpregs_struct { + pub cwd: libc::c_long, + pub swd: libc::c_long, + pub twd: libc::c_long, + pub fip: libc::c_long, + pub fcs: libc::c_long, + pub foo: libc::c_long, + pub fos: libc::c_long, + pub st_space: [libc::c_long; 20], +} + +#[cfg(all(target_os = "android", target_arch = "x86"))] +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct user { + pub regs: user_regs_struct, + pub u_fpvalid: libc::c_long, + pub i387: user_fpregs_struct, + pub u_tsize: libc::c_ulong, + pub u_dsize: libc::c_ulong, + pub u_ssize: libc::c_ulong, + pub start_code: libc::c_ulong, + pub start_stack: libc::c_ulong, + pub signal: libc::c_long, + __reserved: libc::c_int, + pub u_ar0: *mut user_regs_struct, + pub u_fpstate: *mut user_fpregs_struct, + pub magic: libc::c_ulong, + pub u_comm: [libc::c_char; 32], + pub u_debugreg: [libc::c_int; 8], +} + +const NUM_DEBUG_REGISTERS: usize = 8; + +pub struct ThreadInfoX86 { + pub stack_pointer: usize, + pub tgid: Pid, // thread group id + pub ppid: Pid, // parent process + pub regs: user_regs_struct, + pub fpregs: user_fpregs_struct, + #[cfg(target_arch = "x86_64")] + pub dregs: [libc::c_ulonglong; NUM_DEBUG_REGISTERS], + #[cfg(target_arch = "x86")] + pub dregs: [libc::c_int; NUM_DEBUG_REGISTERS], + #[cfg(target_arch = "x86")] + pub fpxregs: user_fpxregs_struct, +} + +impl CommonThreadInfo for ThreadInfoX86 {} + +impl ThreadInfoX86 { + // nix currently doesn't support PTRACE_GETREGSET, so we have to do it ourselves + fn getregset(pid: Pid) -> Result<user_regs_struct> { + Self::ptrace_get_data_via_io( + 0x4204 as ptrace::RequestType, // PTRACE_GETREGSET + Some(NT_Elf::NT_PRSTATUS), + nix::unistd::Pid::from_raw(pid), + ) + } + + pub fn getregs(pid: Pid) -> Result<user_regs_struct> { + // TODO: nix restricts PTRACE_GETREGS to arm android for some reason + Self::ptrace_get_data( + 12 as ptrace::RequestType, // PTRACE_GETREGS + None, + nix::unistd::Pid::from_raw(pid), + ) + } + + // nix currently doesn't support PTRACE_GETREGSET, so we have to do it ourselves + fn getfpregset(pid: Pid) -> Result<user_fpregs_struct> { + Self::ptrace_get_data_via_io( + 0x4204 as ptrace::RequestType, // PTRACE_GETREGSET + Some(NT_Elf::NT_PRFPREGSET), + nix::unistd::Pid::from_raw(pid), + ) + } + + // nix currently doesn't support PTRACE_GETFPREGS, so we have to do it ourselves + fn getfpregs(pid: Pid) -> Result<user_fpregs_struct> { + Self::ptrace_get_data( + 14 as ptrace::RequestType, // PTRACE_GETFPREGS + None, + nix::unistd::Pid::from_raw(pid), + ) + } + + // nix currently doesn't support PTRACE_GETFPXREGS, so we have to do it ourselves + #[cfg(target_arch = "x86")] + fn getfpxregs(pid: Pid) -> Result<user_fpxregs_struct> { + Self::ptrace_get_data( + 18 as ptrace::RequestType, // PTRACE_GETFPXREGS + None, + nix::unistd::Pid::from_raw(pid), + ) + } + + fn peek_user(pid: Pid, addr: ptrace::AddressType) -> nix::Result<libc::c_long> { + Self::ptrace_peek( + ptrace::Request::PTRACE_PEEKUSER as ptrace::RequestType, + nix::unistd::Pid::from_raw(pid), + addr, + std::ptr::null_mut(), + ) + } + + pub fn create_impl(_pid: Pid, tid: Pid) -> Result<Self> { + let (ppid, tgid) = Self::get_ppid_and_tgid(tid)?; + let regs = Self::getregset(tid).or_else(|_| Self::getregs(tid))?; + let fpregs = Self::getfpregset(tid).or_else(|_| Self::getfpregs(tid))?; + #[cfg(target_arch = "x86")] + let fpxregs: user_fpxregs_struct; + #[cfg(target_arch = "x86")] + { + if cfg!(target_feature = "fxsr") { + fpxregs = Self::getfpxregs(tid)?; + } else { + fpxregs = unsafe { std::mem::zeroed() }; + } + } + + #[cfg(target_arch = "x86_64")] + let mut dregs: [libc::c_ulonglong; NUM_DEBUG_REGISTERS] = [0; NUM_DEBUG_REGISTERS]; + #[cfg(target_arch = "x86")] + let mut dregs: [libc::c_int; NUM_DEBUG_REGISTERS] = [0; NUM_DEBUG_REGISTERS]; + + let debug_offset = memoffset::offset_of!(user, u_debugreg); + let elem_offset = size_of_val(&dregs[0]); + for (idx, dreg) in dregs.iter_mut().enumerate() { + let chunk = Self::peek_user( + tid, + (debug_offset + idx * elem_offset) as ptrace::AddressType, + )?; + #[cfg(target_arch = "x86_64")] + { + *dreg = chunk as u64; // libc / ptrace is very messy wrt int types used... + } + #[cfg(target_arch = "x86")] + { + *dreg = chunk as i32; // libc / ptrace is very messy wrt int types used... + } + } + + #[cfg(target_arch = "x86_64")] + let stack_pointer = regs.rsp as usize; + #[cfg(target_arch = "x86")] + let stack_pointer = regs.esp as usize; + + Ok(Self { + stack_pointer, + tgid, + ppid, + regs, + fpregs, + dregs, + #[cfg(target_arch = "x86")] + fpxregs, + }) + } + + #[cfg(target_arch = "x86_64")] + pub fn get_instruction_pointer(&self) -> usize { + self.regs.rip as usize + } + + #[cfg(target_arch = "x86")] + pub fn get_instruction_pointer(&self) -> usize { + self.regs.eip as usize + } + + #[cfg(target_arch = "x86_64")] + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + use format::ContextFlagsAmd64; + + out.context_flags = ContextFlagsAmd64::CONTEXT_AMD64_FULL.bits() + | ContextFlagsAmd64::CONTEXT_AMD64_SEGMENTS.bits(); + + out.cs = self.regs.cs as u16; // TODO: This is u64, do we loose information by doing this? + + out.ds = self.regs.ds as u16; // TODO: This is u64, do we loose information by doing this? + out.es = self.regs.es as u16; // TODO: This is u64, do we loose information by doing this? + out.fs = self.regs.fs as u16; // TODO: This is u64, do we loose information by doing this? + out.gs = self.regs.gs as u16; // TODO: This is u64, do we loose information by doing this? + + out.ss = self.regs.ss as u16; // TODO: This is u64, do we loose information by doing this? + out.eflags = self.regs.eflags as u32; // TODO: This is u64, do we loose information by doing this? + + out.dr0 = self.dregs[0]; + out.dr1 = self.dregs[1]; + out.dr2 = self.dregs[2]; + out.dr3 = self.dregs[3]; + // 4 and 5 deliberatly omitted because they aren't included in the minidump + // format. + out.dr6 = self.dregs[6]; + out.dr7 = self.dregs[7]; + + out.rax = self.regs.rax; + out.rcx = self.regs.rcx; + out.rdx = self.regs.rdx; + out.rbx = self.regs.rbx; + + out.rsp = self.regs.rsp; + + out.rbp = self.regs.rbp; + out.rsi = self.regs.rsi; + out.rdi = self.regs.rdi; + out.r8 = self.regs.r8; + out.r9 = self.regs.r9; + out.r10 = self.regs.r10; + out.r11 = self.regs.r11; + out.r12 = self.regs.r12; + out.r13 = self.regs.r13; + out.r14 = self.regs.r14; + out.r15 = self.regs.r15; + + out.rip = self.regs.rip; + + { + let fs = &self.fpregs; + let mut float_save = crate::minidump_cpu::FloatStateCPU { + control_word: fs.cwd, + status_word: fs.swd, + tag_word: fs.ftw as u8, + error_opcode: fs.fop, + error_offset: fs.rip as u32, + data_offset: fs.rdp as u32, + error_selector: 0, // We don't have this. + data_selector: 0, // We don't have this. + mx_csr: fs.mxcsr, + mx_csr_mask: fs.mxcr_mask, + ..Default::default() + }; + + super::copy_u32_registers(&mut float_save.float_registers, &fs.st_space); + super::copy_u32_registers(&mut float_save.xmm_registers, &fs.xmm_space); + + out.float_save + .pwrite_with(float_save, 0, scroll::Endian::Little) + .expect("this is impossible"); + } + } + + #[cfg(target_arch = "x86")] + pub fn fill_cpu_context(&self, out: &mut RawContextCPU) { + out.context_flags = format::ContextFlagsX86::CONTEXT_X86_ALL.bits(); + + out.dr0 = self.dregs[0] as u32; + out.dr3 = self.dregs[3] as u32; + out.dr1 = self.dregs[1] as u32; + out.dr2 = self.dregs[2] as u32; + // 4 and 5 deliberatly omitted because they aren't included in the minidump + // format. + out.dr6 = self.dregs[6] as u32; + out.dr7 = self.dregs[7] as u32; + + out.gs = self.regs.xgs as u32; + out.fs = self.regs.xfs as u32; + out.es = self.regs.xes as u32; + out.ds = self.regs.xds as u32; + + out.edi = self.regs.edi as u32; + out.esi = self.regs.esi as u32; + out.ebx = self.regs.ebx as u32; + out.edx = self.regs.edx as u32; + out.ecx = self.regs.ecx as u32; + out.eax = self.regs.eax as u32; + + out.ebp = self.regs.ebp as u32; + out.eip = self.regs.eip as u32; + out.cs = self.regs.xcs as u32; + out.eflags = self.regs.eflags as u32; + out.esp = self.regs.esp as u32; + out.ss = self.regs.xss as u32; + + out.float_save.control_word = self.fpregs.cwd as u32; + out.float_save.status_word = self.fpregs.swd as u32; + out.float_save.tag_word = self.fpregs.twd as u32; + out.float_save.error_offset = self.fpregs.fip as u32; + out.float_save.error_selector = self.fpregs.fcs as u32; + out.float_save.data_offset = self.fpregs.foo as u32; + out.float_save.data_selector = self.fpregs.fos as u32; + + { + let ra = &mut out.float_save.register_area; + // 8 registers * 10 bytes per register. + for (idx, block) in self.fpregs.st_space.iter().enumerate() { + let offset = idx * std::mem::size_of::<u32>(); + if offset >= ra.len() { + break; + } + + ra.pwrite_with(block, offset, scroll::Endian::Little) + .expect("this is impossible"); + } + } + + #[allow(unused_assignments)] + { + let mut offset = 0; + macro_rules! write_er { + ($reg:expr) => { + offset += out + .extended_registers + .pwrite_with($reg, offset, scroll::Endian::Little) + .unwrap() + }; + } + + // This matches the Intel fpsave format. + write_er!(self.fpregs.cwd as u16); + write_er!(self.fpregs.swd as u16); + write_er!(self.fpregs.twd as u16); + write_er!(self.fpxregs.fop); + write_er!(self.fpxregs.fip); + write_er!(self.fpxregs.fcs); + write_er!(self.fpregs.foo); + write_er!(self.fpregs.fos); + write_er!(self.fpxregs.mxcsr); + + offset = 32; + + for val in &self.fpxregs.st_space { + write_er!(val); + } + + debug_assert_eq!(offset, 160); + + for val in &self.fpxregs.xmm_space { + write_er!(val); + } + } + } +} diff --git a/third_party/rust/minidump-writer/src/mac.rs b/third_party/rust/minidump-writer/src/mac.rs new file mode 100644 index 0000000000..745a5a1eae --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac.rs @@ -0,0 +1,13 @@ +#![allow(unsafe_code)] + +#[cfg(target_pointer_width = "32")] +compile_error!("Various MacOS FFI bindings assume we are on a 64-bit architechture"); + +/// Re-export of the mach2 library for users who want to call mach specific functions +pub use mach2; + +pub mod errors; +pub mod mach; +pub mod minidump_writer; +mod streams; +pub mod task_dumper; diff --git a/third_party/rust/minidump-writer/src/mac/errors.rs b/third_party/rust/minidump-writer/src/mac/errors.rs new file mode 100644 index 0000000000..96ddb88cad --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/errors.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WriterError { + #[error(transparent)] + TaskDumpError(#[from] crate::mac::task_dumper::TaskDumpError), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] crate::mem_writer::MemoryWriterError), + #[error("Failed to write to file")] + FileWriterError(#[from] crate::dir_section::FileWriterError), + #[error("Attempted to write an exception stream with no crash context")] + NoCrashContext, +} diff --git a/third_party/rust/minidump-writer/src/mac/mach.rs b/third_party/rust/minidump-writer/src/mac/mach.rs new file mode 100644 index 0000000000..f95211dc64 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/mach.rs @@ -0,0 +1,670 @@ +//! Contains various helpers to improve and expand on the bindings provided +//! by `mach2` + +// Just exports all of the mach functions we use into a flat list +pub use mach2::{ + kern_return::{kern_return_t, KERN_SUCCESS}, + port::mach_port_name_t, + task::{self, task_threads}, + task_info, + thread_act::thread_get_state, + traps::mach_task_self, + vm::{mach_vm_deallocate, mach_vm_read, mach_vm_region_recurse}, + vm_region::vm_region_submap_info_64, +}; + +/// A Mach kernel error. +/// +/// See <usr/include/mach/kern_return.h>. +#[derive(thiserror::Error, Debug)] +pub enum KernelError { + #[error("specified address is not currently valid")] + InvalidAddress = 1, + #[error("specified memory is valid, but does not permit the required forms of access")] + ProtectionFailure = 2, + #[error("the address range specified is already in use, or no address range of the size specified could be found")] + NoSpace = 3, + #[error("the function requested was not applicable to this type of argument, or an argument is invalid")] + InvalidArgument = 4, + #[error("the function could not be performed")] + Failure = 5, + #[error("system resource could not be allocated to fulfill this request")] + ResourceShortage = 6, + #[error("the task in question does not hold receive rights for the port argument")] + NotReceiver = 7, + #[error("bogus access restriction")] + NoAccess = 8, + #[error( + "during a page fault, the target address refers to a memory object that has been destroyed" + )] + MemoryFailure = 9, + #[error( + "during a page fault, the memory object indicated that the data could not be returned" + )] + MemoryError = 10, + #[error("the receive right is already a member of the portset")] + AlreadyInSet = 11, + #[error("the receive right is not a member of a port set")] + NotInSet = 12, + #[error("the name already denotes a right in the task")] + NameExists = 13, + #[error("the operation was aborted")] + Aborted = 14, + #[error("the name doesn't denote a right in the task")] + InvalidName = 15, + #[error("target task isn't an active task")] + InvalidTask = 16, + #[error("the name denotes a right, but not an appropriate right")] + InvalidRight = 17, + #[error("a blatant range error")] + InvalidValue = 18, + #[error("operation would overflow limit on user-references")] + UserRefsOverflow = 19, + #[error("the supplied port capability is improper")] + InvalidCapability = 20, + #[error("the task already has send or receive rights for the port under another name")] + RightExists = 21, + #[error("target host isn't actually a host")] + InvalidHost = 22, + #[error("an attempt was made to supply 'precious' data for memory that is already present in a memory object")] + MemoryPresent = 23, + // These 2 are errors which should only ever be seen by the kernel itself + //MemoryDataMoved = 24, + //MemoryRestartCopy = 25, + #[error("an argument applied to assert processor set privilege was not a processor set control port")] + InvalidProcessorSet = 26, + #[error("the specified scheduling attributes exceed the thread's limits")] + PolicyLimit = 27, + #[error("the specified scheduling policy is not currently enabled for the processor set")] + InvalidPolicy = 28, + #[error("the external memory manager failed to initialize the memory object")] + InvalidObject = 29, + #[error( + "a thread is attempting to wait for an event for which there is already a waiting thread" + )] + AlreadyWaiting = 30, + #[error("an attempt was made to destroy the default processor set")] + DefaultSet = 31, + #[error("an attempt was made to fetch an exception port that is protected, or to abort a thread while processing a protected exception")] + ExceptionProtected = 32, + #[error("a ledger was required but not supplied")] + InvalidLedger = 33, + #[error("the port was not a memory cache control port")] + InvalidMemoryControl = 34, + #[error("an argument supplied to assert security privilege was not a host security port")] + InvalidSecurity = 35, + #[error("thread_depress_abort was called on a thread which was not currently depressed")] + NotDepressed = 36, + #[error("object has been terminated and is no longer available")] + Terminated = 37, + #[error("lock set has been destroyed and is no longer available")] + LockSetDestroyed = 38, + #[error("the thread holding the lock terminated before releasing the lock")] + LockUnstable = 39, + #[error("the lock is already owned by another thread")] + LockOwned = 40, + #[error("the lock is already owned by the calling thread")] + LockOwnedSelf = 41, + #[error("semaphore has been destroyed and is no longer available")] + SemaphoreDestroyed = 42, + #[error("return from RPC indicating the target server was terminated before it successfully replied")] + RpcServerTerminated = 43, + #[error("terminate an orphaned activation")] + RpcTerminateOrphan = 44, + #[error("allow an orphaned activation to continue executing")] + RpcContinueOrphan = 45, + #[error("empty thread activation (No thread linked to it)")] + NotSupported = 46, + #[error("remote node down or inaccessible")] + NodeDown = 47, + #[error("a signalled thread was not actually waiting")] + NotWaiting = 48, + #[error("some thread-oriented operation (semaphore_wait) timed out")] + OperationTimedOut = 49, + #[error("during a page fault, indicates that the page was rejected as a result of a signature check")] + CodesignError = 50, + #[error("the requested property cannot be changed at this time")] + PoicyStatic = 51, + #[error("the provided buffer is of insufficient size for the requested data")] + InsufficientBufferSize = 52, + #[error("denied by security policy")] + Denied = 53, + #[error("the KC on which the function is operating is missing")] + MissingKC = 54, + #[error("the KC on which the function is operating is invalid")] + InvalidKC = 55, + #[error("a search or query operation did not return a result")] + NotFound = 56, +} + +impl From<mach2::kern_return::kern_return_t> for KernelError { + fn from(kr: mach2::kern_return::kern_return_t) -> Self { + use mach2::kern_return::*; + + match kr { + KERN_INVALID_ADDRESS => Self::InvalidAddress, + KERN_PROTECTION_FAILURE => Self::ProtectionFailure, + KERN_NO_SPACE => Self::NoSpace, + KERN_INVALID_ARGUMENT => Self::InvalidArgument, + KERN_FAILURE => Self::Failure, + KERN_RESOURCE_SHORTAGE => Self::ResourceShortage, + KERN_NOT_RECEIVER => Self::NotReceiver, + KERN_NO_ACCESS => Self::NoAccess, + KERN_MEMORY_FAILURE => Self::MemoryFailure, + KERN_MEMORY_ERROR => Self::MemoryError, + KERN_ALREADY_IN_SET => Self::AlreadyInSet, + KERN_NAME_EXISTS => Self::NameExists, + KERN_INVALID_NAME => Self::InvalidName, + KERN_INVALID_TASK => Self::InvalidTask, + KERN_INVALID_RIGHT => Self::InvalidRight, + KERN_INVALID_VALUE => Self::InvalidValue, + KERN_UREFS_OVERFLOW => Self::UserRefsOverflow, + KERN_INVALID_CAPABILITY => Self::InvalidCapability, + KERN_RIGHT_EXISTS => Self::RightExists, + KERN_INVALID_HOST => Self::InvalidHost, + KERN_MEMORY_PRESENT => Self::MemoryPresent, + KERN_INVALID_PROCESSOR_SET => Self::InvalidProcessorSet, + KERN_POLICY_LIMIT => Self::PolicyLimit, + KERN_INVALID_POLICY => Self::InvalidPolicy, + KERN_INVALID_OBJECT => Self::InvalidObject, + KERN_ALREADY_WAITING => Self::AlreadyWaiting, + KERN_DEFAULT_SET => Self::DefaultSet, + KERN_EXCEPTION_PROTECTED => Self::ExceptionProtected, + KERN_INVALID_LEDGER => Self::InvalidLedger, + KERN_INVALID_MEMORY_CONTROL => Self::InvalidMemoryControl, + KERN_INVALID_SECURITY => Self::InvalidSecurity, + KERN_NOT_DEPRESSED => Self::NotDepressed, + KERN_TERMINATED => Self::Terminated, + KERN_LOCK_SET_DESTROYED => Self::LockSetDestroyed, + KERN_LOCK_UNSTABLE => Self::LockUnstable, + KERN_LOCK_OWNED => Self::LockOwned, + KERN_LOCK_OWNED_SELF => Self::LockOwnedSelf, + KERN_SEMAPHORE_DESTROYED => Self::SemaphoreDestroyed, + KERN_RPC_SERVER_TERMINATED => Self::RpcServerTerminated, + KERN_RPC_TERMINATE_ORPHAN => Self::RpcTerminateOrphan, + KERN_RPC_CONTINUE_ORPHAN => Self::RpcContinueOrphan, + KERN_NOT_SUPPORTED => Self::NotSupported, + KERN_NODE_DOWN => Self::NodeDown, + KERN_NOT_WAITING => Self::NotWaiting, + KERN_OPERATION_TIMED_OUT => Self::OperationTimedOut, + KERN_CODESIGN_ERROR => Self::CodesignError, + KERN_POLICY_STATIC => Self::PoicyStatic, + 52 => Self::InsufficientBufferSize, + 53 => Self::Denied, + 54 => Self::MissingKC, + 55 => Self::InvalidKC, + 56 => Self::NotFound, + // This should never happen given a result from a mach call, but + // in that case we just use `Failure` as the mach header itself + // describes it as a catch all + _ => Self::Failure, + } + } +} + +// From /usr/include/mach/machine/thread_state.h +pub const THREAD_STATE_MAX: usize = 1296; + +cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + /// x86_THREAD_STATE64 in /usr/include/mach/i386/thread_status.h + pub const THREAD_STATE_FLAVOR: u32 = 4; + + pub type ArchThreadState = mach2::structs::x86_thread_state64_t; + } else if #[cfg(target_arch = "aarch64")] { + /// ARM_THREAD_STATE64 in /usr/include/mach/arm/thread_status.h + pub const THREAD_STATE_FLAVOR: u32 = 6; + + // Missing from mach2 atm + // _STRUCT_ARM_THREAD_STATE64 from /usr/include/mach/arm/_structs.h + #[repr(C)] + pub struct Arm64ThreadState { + pub x: [u64; 29], + pub fp: u64, + pub lr: u64, + pub sp: u64, + pub pc: u64, + pub cpsr: u32, + __pad: u32, + } + + pub type ArchThreadState = Arm64ThreadState; + } else { + compile_error!("unsupported target arch"); + } +} + +#[repr(C, align(8))] +pub struct ThreadState { + pub state: [u32; THREAD_STATE_MAX], + pub state_size: u32, +} + +impl Default for ThreadState { + fn default() -> Self { + Self { + state: [0u32; THREAD_STATE_MAX], + state_size: (THREAD_STATE_MAX * std::mem::size_of::<u32>()) as u32, + } + } +} + +impl ThreadState { + /// Gets the program counter + #[inline] + pub fn pc(&self) -> u64 { + cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + self.arch_state().__rip + } else if #[cfg(target_arch = "aarch64")] { + self.arch_state().pc + } + } + } + + /// Gets the stack pointer + #[inline] + pub fn sp(&self) -> u64 { + cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + self.arch_state().__rsp + } else if #[cfg(target_arch = "aarch64")] { + self.arch_state().sp + } + } + } + + /// Converts the raw binary blob into the architecture specific state + #[inline] + pub fn arch_state(&self) -> &ArchThreadState { + // SAFETY: hoping the kernel isn't lying + unsafe { &*(self.state.as_ptr().cast()) } + } +} + +/// Minimal trait that just pairs a structure that can be filled out by +/// [`mach2::task::task_info`] with the "flavor" that tells it the info we +/// actually want to retrieve +pub trait TaskInfo { + /// One of the `MACH_*_TASK` integers. I assume it's very bad if you implement + /// this trait and provide the wrong flavor for the struct + const FLAVOR: u32; +} + +/// Minimal trait that just pairs a structure that can be filled out by +/// [`thread_info`] with the "flavor" that tells it the info we +/// actually want to retrieve +pub trait ThreadInfo { + /// One of the `THREAD_*` integers. I assume it's very bad if you implement + /// this trait and provide the wrong flavor for the struct + const FLAVOR: u32; +} + +/// <usr/include/mach-o/loader.h>, the file type for the main executable image +pub const MH_EXECUTE: u32 = 0x2; +/// <usr/include/mach-o/loader.h>, the file type dyld, the dynamic loader +pub const MH_DYLINKER: u32 = 0x7; +// usr/include/mach-o/loader.h, magic number for MachHeader +pub const MH_MAGIC_64: u32 = 0xfeedfacf; + +/// Load command constants from usr/include/mach-o/loader.h +#[repr(u32)] +#[derive(Debug)] +pub enum LoadCommandKind { + /// Command to map a segment + Segment = 0x19, + /// Dynamically linked shared lib ident + IdDylib = 0xd, + /// Image uuid + Uuid = 0x1b, + /// Load a dynamic linker. Should only be on MH_EXECUTE (main executable) + /// images when the dynamic linker is overriden + LoadDylinker = 0xe, + /// Dynamic linker identification + IdDylinker = 0xf, +} + +impl LoadCommandKind { + #[inline] + fn from_u32(kind: u32) -> Option<Self> { + Some(if kind == Self::Segment as u32 { + Self::Segment + } else if kind == Self::IdDylib as u32 { + Self::IdDylib + } else if kind == Self::Uuid as u32 { + Self::Uuid + } else if kind == Self::LoadDylinker as u32 { + Self::LoadDylinker + } else if kind == Self::IdDylinker as u32 { + Self::IdDylinker + } else { + return None; + }) + } +} + +/// The header at the beginning of every (valid) Mach image +/// +/// <usr/include/mach-o/loader.h> +#[repr(C)] +#[derive(Clone)] +pub struct MachHeader { + /// Mach magic number identifier, this is used to validate the header is valid + pub magic: u32, + /// `cpu_type_t` cpu specifier + pub cpu_type: i32, + /// `cpu_subtype_t` machine specifier + pub cpu_sub_type: i32, + /// Type of file, eg. [`MH_EXECUTE`] for the main executable + pub file_type: u32, + /// Number of load commands for the image + pub num_commands: u32, + /// Size in bytes of all of the load commands + pub size_commands: u32, + pub flags: u32, + __reserved: u32, +} + +/// Every load command is a variable sized struct depending on its type, but +/// they all include the fields in this struct at the beginning +/// +/// <usr/include/mach-o/loader.h> +#[repr(C)] +pub struct LoadCommandBase { + /// Type of load command `LC_*` + pub cmd: u32, + /// Total size of the command in bytes + pub cmd_size: u32, +} + +/// The 64-bit segment load command indicates that a part of this file is to be +/// mapped into a 64-bit task's address space. If the 64-bit segment has +/// sections then section_64 structures directly follow the 64-bit segment +/// command and their size is reflected in `cmdsize`. +#[repr(C)] +pub struct SegmentCommand64 { + cmd: u32, + pub cmd_size: u32, + /// String name of the section + pub segment_name: [u8; 16], + /// Memory address the segment is mapped to + pub vm_addr: u64, + /// Total size of the segment + pub vm_size: u64, + /// File offset of the segment + pub file_off: u64, + /// Amount mapped from the file + pub file_size: u64, + /// Maximum VM protection + pub max_prot: i32, + /// Initial VM protection + pub init_prot: i32, + /// Number of sections in the segment + pub num_sections: u32, + pub flags: u32, +} + +/// Dynamically linked shared libraries are identified by two things. The +/// pathname (the name of the library as found for execution), and the +/// compatibility version number. The pathname must match and the compatibility +/// number in the user of the library must be greater than or equal to the +/// library being used. The time stamp is used to record the time a library was +/// built and copied into user so it can be use to determined if the library used +/// at runtime is exactly the same as used to built the program. +#[repr(C)] +#[derive(Debug)] +pub struct Dylib { + /// Offset from the load command start to the pathname + pub name: u32, + /// Library's build time stamp + pub timestamp: u32, + /// Library's current version number + pub current_version: u32, + /// Library's compatibility version number + pub compatibility_version: u32, +} + +/// A dynamically linked shared library (filetype == MH_DYLIB in the mach header) +/// contains a dylib_command (cmd == LC_ID_DYLIB) to identify the library. +/// An object that uses a dynamically linked shared library also contains a +/// dylib_command (cmd == LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB, or +/// LC_REEXPORT_DYLIB) for each library it uses. +#[repr(C)] +pub struct DylibCommand { + cmd: u32, + /// Total size of the command in bytes, including pathname string + pub cmd_size: u32, + /// Library identification + pub dylib: Dylib, +} + +/// A program that uses a dynamic linker contains a dylinker_command to identify +/// the name of the dynamic linker (LC_LOAD_DYLINKER). And a dynamic linker +/// contains a dylinker_command to identify the dynamic linker (LC_ID_DYLINKER). +/// A file can have at most one of these. +/// This struct is also used for the LC_DYLD_ENVIRONMENT load command and +/// contains string for dyld to treat like environment variable. +#[repr(C)] +struct DylinkerCommandRepr { + /// LC_ID_DYLINKER, LC_LOAD_DYLINKER or LC_DYLD_ENVIRONMENT + cmd: u32, + /// includes pathname string + cmd_size: u32, + /// Dynamic linker's path name, an offset from the load command address + name: u32, +} + +pub struct DylinkerCommand<'buf> { + /// LC_ID_DYLINKER, LC_LOAD_DYLINKER or LC_DYLD_ENVIRONMENT + pub cmd: u32, + /// includes pathname string + pub cmd_size: u32, + /// The offset from the load command where the path was read + pub name_offset: u32, + /// Dynamic linker's path name + pub name: &'buf str, +} + +/// The uuid load command contains a single 128-bit unique random number that +/// identifies an object produced by the static link editor. +#[repr(C)] +pub struct UuidCommand { + cmd: u32, + pub cmd_size: u32, + /// The UUID. The components are in big-endian regardless of the host architecture + pub uuid: [u8; 16], +} + +/// A block of load commands for a particular image +pub struct LoadCommands { + /// The block of memory containing all of the load commands + pub buffer: Vec<u8>, + /// The number of actual load commmands that _should_ be in the buffer + pub count: u32, +} + +impl LoadCommands { + /// Retrieves an iterator over the load commands in the contained buffer + #[inline] + pub fn iter(&self) -> LoadCommandsIter<'_> { + LoadCommandsIter { + buffer: &self.buffer, + count: self.count, + } + } +} + +/// A single load command +pub enum LoadCommand<'buf> { + Segment(&'buf SegmentCommand64), + Dylib(&'buf DylibCommand), + Uuid(&'buf UuidCommand), + DylinkerCommand(DylinkerCommand<'buf>), +} + +pub struct LoadCommandsIter<'buf> { + buffer: &'buf [u8], + count: u32, +} + +impl<'buf> Iterator for LoadCommandsIter<'buf> { + type Item = LoadCommand<'buf>; + + fn next(&mut self) -> Option<Self::Item> { + // SAFETY: we're interpreting raw bytes as C structs, we try and be safe + unsafe { + loop { + if self.count == 0 || self.buffer.len() < std::mem::size_of::<LoadCommandBase>() { + return None; + } + + let header = &*(self.buffer.as_ptr().cast::<LoadCommandBase>()); + + // This would mean we've been lied to by the MachHeader and either + // the size_commands field was too small, or the num_command was + // too large + if header.cmd_size as usize > self.buffer.len() { + return None; + } + + let cmd = LoadCommandKind::from_u32(header.cmd).and_then(|kind| { + Some(match kind { + LoadCommandKind::Segment => LoadCommand::Segment( + &*(self.buffer.as_ptr().cast::<SegmentCommand64>()), + ), + LoadCommandKind::IdDylib => { + LoadCommand::Dylib(&*(self.buffer.as_ptr().cast::<DylibCommand>())) + } + LoadCommandKind::Uuid => { + LoadCommand::Uuid(&*(self.buffer.as_ptr().cast::<UuidCommand>())) + } + LoadCommandKind::LoadDylinker | LoadCommandKind::IdDylinker => { + let dcr = &*(self.buffer.as_ptr().cast::<DylinkerCommandRepr>()); + + let nul = self.buffer[dcr.name as usize..header.cmd_size as usize] + .iter() + .position(|c| *c == 0)?; + + LoadCommand::DylinkerCommand(DylinkerCommand { + cmd: dcr.cmd, + cmd_size: dcr.cmd_size, + name_offset: dcr.name, + name: std::str::from_utf8( + &self.buffer[dcr.name as usize..dcr.name as usize + nul], + ) + .ok()?, + }) + } + }) + }); + + self.count -= 1; + self.buffer = &self.buffer[header.cmd_size as usize..]; + + if let Some(cmd) = cmd { + return Some(cmd); + } + } + } + } + + fn size_hint(&self) -> (usize, Option<usize>) { + let sz = self.count as usize; + (sz, Some(sz)) + } +} + +/// Retrieves an integer sysctl by name. Returns the default value if retrieval +/// fails. +pub fn sysctl_by_name<T: Sized + Default>(name: &[u8]) -> T { + let mut out = T::default(); + let mut len = std::mem::size_of_val(&out); + + // SAFETY: syscall + unsafe { + if libc::sysctlbyname( + name.as_ptr().cast(), + (&mut out as *mut T).cast(), + &mut len, + std::ptr::null_mut(), + 0, + ) != 0 + { + // log? + T::default() + } else { + out + } + } +} + +/// Retrieves an `i32` sysctl by name and casts it to the specified integer type. +/// Returns the default value if retrieval fails or the value is out of bounds of +/// the specified integer type. +pub fn int_sysctl_by_name<T: TryFrom<i32> + Default>(name: &[u8]) -> T { + let val = sysctl_by_name::<i32>(name); + T::try_from(val).unwrap_or_default() +} + +/// Retrieves a string sysctl by name. Returns an empty string if the retrieval +/// fails or the string can't be converted to utf-8. +pub fn sysctl_string(name: &[u8]) -> String { + let mut buf_len = 0; + + // SAFETY: syscalls + let string_buf = unsafe { + // Retrieve the size of the string (including null terminator) + if libc::sysctlbyname( + name.as_ptr().cast(), + std::ptr::null_mut(), + &mut buf_len, + std::ptr::null_mut(), + 0, + ) != 0 + || buf_len <= 1 + { + return String::new(); + } + + let mut buff = Vec::new(); + buff.resize(buf_len, 0); + + if libc::sysctlbyname( + name.as_ptr().cast(), + buff.as_mut_ptr().cast(), + &mut buf_len, + std::ptr::null_mut(), + 0, + ) != 0 + { + return String::new(); + } + + buff.pop(); // remove null terminator + buff + }; + + String::from_utf8(string_buf).unwrap_or_default() +} + +extern "C" { + /// From <usr/include/mach/mach_traps.h>, this retrieves the normal PID for + /// the specified task as the syscalls from BSD use PIDs, not mach ports. + /// + /// This seems to be marked as "obsolete" in the header, but of course being + /// Apple, there is no mention of a replacement function or when/if it might + /// eventually disappear. + pub fn pid_for_task(task: mach_port_name_t, pid: *mut i32) -> kern_return_t; + + /// Fomr <user/include/mach/thread_act.h>, this retrieves thread info for the + /// for the specified thread. + /// + /// Note that the info_size parameter is actually the size of the thread_info / 4 + /// as it is the number of words in the thread info + pub fn thread_info( + thread: u32, + flavor: u32, + thread_info: *mut i32, + info_size: *mut u32, + ) -> kern_return_t; +} diff --git a/third_party/rust/minidump-writer/src/mac/minidump_writer.rs b/third_party/rust/minidump-writer/src/mac/minidump_writer.rs new file mode 100644 index 0000000000..b05662fd21 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/minidump_writer.rs @@ -0,0 +1,187 @@ +use crate::{ + dir_section::{DirSection, DumpBuf}, + mac::{errors::WriterError, task_dumper::TaskDumper}, + mem_writer::*, + minidump_format::{self, MDMemoryDescriptor, MDRawDirectory, MDRawHeader}, +}; +use std::io::{Seek, Write}; + +pub use mach2::mach_types::{task_t, thread_t}; + +type Result<T> = std::result::Result<T, WriterError>; + +pub struct MinidumpWriter { + /// The crash context as captured by an exception handler + pub(crate) crash_context: Option<crash_context::CrashContext>, + /// List of raw blocks of memory we've written into the stream. These are + /// referenced by other streams (eg thread list) + pub(crate) memory_blocks: Vec<MDMemoryDescriptor>, + /// The task being dumped + pub(crate) task: task_t, + /// The handler thread, so it can be ignored/deprioritized + pub(crate) handler_thread: thread_t, +} + +impl MinidumpWriter { + /// Creates a minidump writer for the specified mach task (process) and + /// handler thread. If not specified, defaults to the current task and thread. + /// + /// ``` + /// use minidump_writer::{minidump_writer::MinidumpWriter, mach2}; + /// + /// // Note that this is the same as specifying `None` for both the task and + /// // handler thread, this is just meant to illustrate how you can setup + /// // a MinidumpWriter manually instead of using a `CrashContext` + /// // SAFETY: syscalls + /// let mdw = unsafe { + /// MinidumpWriter::new( + /// Some(mach2::traps::mach_task_self()), + /// Some(mach2::mach_init::mach_thread_self()), + /// ) + /// }; + /// ``` + pub fn new(task: Option<task_t>, handler_thread: Option<thread_t>) -> Self { + Self { + crash_context: None, + memory_blocks: Vec::new(), + task: task.unwrap_or_else(|| { + // SAFETY: syscall + unsafe { mach2::traps::mach_task_self() } + }), + handler_thread: handler_thread.unwrap_or_else(|| { + // SAFETY: syscall + unsafe { mach2::mach_init::mach_thread_self() } + }), + } + } + + /// Creates a minidump writer with the specified crash context, presumably + /// for another task + pub fn with_crash_context(crash_context: crash_context::CrashContext) -> Self { + let task = crash_context.task; + let handler_thread = crash_context.handler_thread; + + Self { + crash_context: Some(crash_context), + memory_blocks: Vec::new(), + task, + handler_thread, + } + } + + /// Writes a minidump to the specified destination, returning the raw minidump + /// contents upon success + pub fn dump(&mut self, destination: &mut (impl Write + Seek)) -> Result<Vec<u8>> { + let writers = { + #[allow(clippy::type_complexity)] + let mut writers: Vec< + Box<dyn FnMut(&mut Self, &mut DumpBuf, &TaskDumper) -> Result<MDRawDirectory>>, + > = vec![ + Box::new(|mw, buffer, dumper| mw.write_thread_list(buffer, dumper)), + Box::new(|mw, buffer, dumper| mw.write_memory_list(buffer, dumper)), + Box::new(|mw, buffer, dumper| mw.write_system_info(buffer, dumper)), + Box::new(|mw, buffer, dumper| mw.write_module_list(buffer, dumper)), + Box::new(|mw, buffer, dumper| mw.write_misc_info(buffer, dumper)), + Box::new(|mw, buffer, dumper| mw.write_breakpad_info(buffer, dumper)), + Box::new(|mw, buffer, dumper| mw.write_thread_names(buffer, dumper)), + ]; + + // Exception stream needs to be the last entry in this array as it may + // be omitted in the case where the minidump is written without an + // exception. + if self + .crash_context + .as_ref() + .and_then(|cc| cc.exception.as_ref()) + .is_some() + { + writers.push(Box::new(|mw, buffer, dumper| { + mw.write_exception(buffer, dumper) + })); + } + + writers + }; + + let num_writers = writers.len() as u32; + let mut buffer = Buffer::with_capacity(0); + + let mut header_section = MemoryWriter::<MDRawHeader>::alloc(&mut buffer)?; + let mut dir_section = DirSection::new(&mut buffer, num_writers, destination)?; + + let header = MDRawHeader { + signature: minidump_format::MD_HEADER_SIGNATURE, + version: minidump_format::MD_HEADER_VERSION, + stream_count: num_writers, + stream_directory_rva: dir_section.position(), + checksum: 0, /* Can be 0. In fact, that's all that's + * been found in minidump files. */ + time_date_stamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as u32, // TODO: This is not Y2038 safe, but thats how its currently defined as + flags: 0, + }; + header_section.set_value(&mut buffer, header)?; + + // Ensure the header gets flushed. If we crash somewhere below, + // we should have a mostly-intact dump + dir_section.write_to_file(&mut buffer, None)?; + + let dumper = super::task_dumper::TaskDumper::new(self.task); + + for mut writer in writers { + let dirent = writer(self, &mut buffer, &dumper)?; + dir_section.write_to_file(&mut buffer, Some(dirent))?; + } + + Ok(buffer.into()) + } + + /// Retrieves the list of active threads in the target process, except + /// the handler thread if it is known, to simplify dump analysis + #[inline] + pub(crate) fn threads(&self, dumper: &TaskDumper) -> ActiveThreads { + ActiveThreads { + threads: dumper.read_threads().unwrap_or_default(), + handler_thread: self.handler_thread, + i: 0, + } + } +} + +pub(crate) struct ActiveThreads { + threads: &'static [u32], + handler_thread: u32, + i: usize, +} + +impl ActiveThreads { + #[inline] + pub(crate) fn len(&self) -> usize { + let mut len = self.threads.len(); + + if self.handler_thread != mach2::port::MACH_PORT_NULL { + len -= 1; + } + + len + } +} + +impl Iterator for ActiveThreads { + type Item = u32; + + fn next(&mut self) -> Option<Self::Item> { + while self.i < self.threads.len() { + let i = self.i; + self.i += 1; + + if self.threads[i] != self.handler_thread { + return Some(self.threads[i]); + } + } + + None + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams.rs b/third_party/rust/minidump-writer/src/mac/streams.rs new file mode 100644 index 0000000000..bec3b22597 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams.rs @@ -0,0 +1,16 @@ +mod breakpad_info; +mod exception; +mod memory_list; +mod misc_info; +mod module_list; +mod system_info; +mod thread_list; +mod thread_names; + +use super::{ + errors::WriterError, + mach, + minidump_writer::MinidumpWriter, + task_dumper::{self, ImageInfo, TaskDumpError, TaskDumper}, +}; +use crate::{dir_section::DumpBuf, mem_writer::*, minidump_format::*}; diff --git a/third_party/rust/minidump-writer/src/mac/streams/breakpad_info.rs b/third_party/rust/minidump-writer/src/mac/streams/breakpad_info.rs new file mode 100644 index 0000000000..5196a95cac --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/breakpad_info.rs @@ -0,0 +1,34 @@ +use super::*; +use format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO as BreakpadInfo}; + +impl MinidumpWriter { + /// Writes the [`BreakpadInfo`] stream. + /// + /// For MacOS the primary use of this stream is to differentiate between + /// the thread that actually raised an exception, and the thread on which + /// the exception port was listening, so that the exception port (handler) + /// thread can be deprioritized/ignored when analyzing the minidump. + pub(crate) fn write_breakpad_info( + &mut self, + buffer: &mut DumpBuf, + _dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + let bp_section = MemoryWriter::<BreakpadInfo>::alloc_with_val( + buffer, + BreakpadInfo { + validity: BreakpadInfoValid::DumpThreadId.bits() + | BreakpadInfoValid::RequestingThreadId.bits(), + // The thread where the exception port handled the exception, might + // be useful to ignore/deprioritize when processing the minidump + dump_thread_id: self.handler_thread, + // The actual thread where the exception was thrown + requesting_thread_id: self.crash_context.as_ref().map(|cc| cc.thread).unwrap_or(0), + }, + )?; + + Ok(MDRawDirectory { + stream_type: MDStreamType::BreakpadInfoStream as u32, + location: bp_section.location(), + }) + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/exception.rs b/third_party/rust/minidump-writer/src/mac/streams/exception.rs new file mode 100644 index 0000000000..e594dd8d95 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/exception.rs @@ -0,0 +1,176 @@ +use super::*; + +use mach2::exception_types as et; + +impl MinidumpWriter { + /// Writes the [`minidump_common::format::MINIDUMP_EXCEPTION_STREAM`] stream. + /// + /// This stream is optional on MacOS as a user requested minidump could + /// choose not to specify the exception information. + pub(crate) fn write_exception( + &mut self, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + // This shouldn't fail since we won't be writing this stream if the crash context is + // not present + let crash_context = self + .crash_context + .as_ref() + .ok_or(WriterError::NoCrashContext)?; + + let thread_state = dumper.read_thread_state(crash_context.thread).ok(); + + let thread_context = if let Some(ts) = &thread_state { + let mut cpu = Default::default(); + Self::fill_cpu_context(ts, &mut cpu); + MemoryWriter::alloc_with_val(buffer, cpu) + .map(|mw| mw.location()) + .ok() + } else { + None + }; + + let exception_record = crash_context + .exception + .as_ref() + .map(|exc| { + let code = exc.code as u64; + + // `EXC_CRASH` exceptions wrap other exceptions, so we want to + // retrieve the _actual_ exception + let wrapped_exc = if exc.kind as u32 == et::EXC_CRASH { + recover_exc_crash_wrapped_exception(code) + } else { + None + }; + + // For EXC_RESOURCE and EXC_GUARD crashes Crashpad records the + // uppermost 32 bits of the exception code in the exception flags, + // as they are the most interesting for those exceptions. Neither + // of these exceptions can be wrapped by an `EXC_CRASH` + // + // EXC_GUARD + // code: + // +-------------------+----------------+--------------+ + // |[63:61] guard type | [60:32] flavor | [31:0] target| + // +-------------------+----------------+--------------+ + // + // EXC_RESOURCE + // code: + // +--------------------------------------------------------+ + // |[63:61] resource type | [60:58] flavor | [57:32] unused | + // +--------------------------------------------------------+ + let exception_code = + if exc.kind as u32 == et::EXC_RESOURCE || exc.kind as u32 == et::EXC_GUARD { + (code >> 32) as u32 + } else if let Some(wrapped) = wrapped_exc { + wrapped.code + } else { + // For all other exceptions types, the value in the code + // _should_ never exceed 32 bits, crashpad does an actual + // range check here, but since we don't really log anything + // else at the moment I'll punt that for now + // TODO: log/do something if exc.code > u32::MAX + code as u32 + }; + + let exception_kind = if let Some(wrapped) = wrapped_exc { + wrapped.kind + } else { + exc.kind + }; + + let exception_address = + if exception_kind == et::EXC_BAD_ACCESS && exc.subcode.is_some() { + exc.subcode.unwrap_or_default() + } else if let Some(ts) = thread_state { + ts.pc() + } else { + 0 + }; + + // The naming is confusing here, but it is how it is + let mut md_exc = MDException { + exception_code: exception_kind, + exception_flags: exception_code, + exception_address, + ..Default::default() + }; + + // Now append the (mostly) original information to the "ancillary" + // exception_information at the end. This allows a minidump parser + // to recover the full exception information for the crash, rather + // than only using the (potentially) truncated information we + // just set in `exception_code` and `exception_flags` + md_exc.exception_information[0] = exception_kind as u64; + md_exc.exception_information[1] = code; + + md_exc.number_parameters = if let Some(subcode) = exc.subcode { + md_exc.exception_information[2] = subcode; + 3 + } else { + 2 + }; + + md_exc + }) + .unwrap_or_default(); + + let stream = MDRawExceptionStream { + thread_id: crash_context.thread, + exception_record, + thread_context: thread_context.unwrap_or_default(), + __align: 0, + }; + + let exc_section = MemoryWriter::<MDRawExceptionStream>::alloc_with_val(buffer, stream)?; + + Ok(MDRawDirectory { + stream_type: MDStreamType::ExceptionStream as u32, + location: exc_section.location(), + }) + } +} + +/// [`et::EXC_CRASH`] is a wrapper exception around another exception, but not +/// all exceptions can be wrapped by it, so this function validates that the +/// `EXC_CRASH` is actually valid +#[inline] +fn is_valid_exc_crash(exc_code: u64) -> bool { + let wrapped = ((exc_code >> 20) & 0xf) as u32; + + !( + wrapped == et::EXC_CRASH // EXC_CRASH can't wrap another one + || wrapped == et::EXC_RESOURCE // EXC_RESOURCE would lose information + || wrapped == et::EXC_GUARD // EXC_GUARD would lose information + || wrapped == et::EXC_CORPSE_NOTIFY + // cannot be wrapped + ) +} + +/// The details for an exception wrapped by an `EXC_CRASH` +#[derive(Copy, Clone)] +struct WrappedException { + /// The `EXC_*` that was wrapped + kind: u32, + /// The code of the wrapped exception, for all exceptions other than + /// `EXC_RESOURCE` and `EXC_GUARD` this _should_ never exceed 32 bits, and + /// is one of the reasons that `EXC_CRASH` cannot wrap those 2 exceptions + code: u32, + /// The Unix signal number that the original exception was converted into + _signal: u8, +} + +/// Unwraps an `EXC_CRASH` exception code to the inner exception it wraps. +/// +/// Will return `None` if the specified code is wrapping an exception that +/// should not be possible to be wrapped in an `EXC_CRASH` +#[inline] +fn recover_exc_crash_wrapped_exception(code: u64) -> Option<WrappedException> { + is_valid_exc_crash(code).then(|| WrappedException { + kind: ((code >> 20) & 0xf) as u32, + code: (code & 0xfffff) as u32, + _signal: ((code >> 24) & 0xff) as u8, + }) +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/memory_list.rs b/third_party/rust/minidump-writer/src/mac/streams/memory_list.rs new file mode 100644 index 0000000000..47a37fbfd6 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/memory_list.rs @@ -0,0 +1,72 @@ +use super::*; + +impl MinidumpWriter { + /// Writes the [`MDStreamType::MemoryListStream`]. The memory blocks that are + /// written into this stream are the raw thread contexts that were retrieved + /// and added by [`Self::write_thread_list`] + pub(crate) fn write_memory_list( + &mut self, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + // Include some memory around the instruction pointer if the crash was + // due to an exception + if let Some(cc) = &self.crash_context { + if cc.exception.is_some() { + const IP_MEM_SIZE: u64 = 256; + + let get_ip_block = |tid| -> Option<std::ops::Range<u64>> { + let thread_state = dumper.read_thread_state(tid).ok()?; + + let ip = thread_state.pc(); + + // Bound it to the upper and lower bounds of the region + // it's contained within. If it's not in a known memory region, + // don't bother trying to write it. + let region = dumper.get_vm_region(ip).ok()?; + + if ip < region.range.start || ip > region.range.end { + return None; + } + + // Try to get IP_MEM_SIZE / 2 bytes before and after the IP, but + // settle for whatever's available. + let start = std::cmp::max(region.range.start, ip - IP_MEM_SIZE / 2); + let end = std::cmp::min(ip + IP_MEM_SIZE / 2, region.range.end); + + Some(start..end) + }; + + if let Some(ip_range) = get_ip_block(cc.thread) { + let size = ip_range.end - ip_range.start; + let stack_buffer = + dumper.read_task_memory(ip_range.start as _, size as usize)?; + let ip_location = MDLocationDescriptor { + data_size: size as u32, + rva: buffer.position() as u32, + }; + buffer.write_all(&stack_buffer); + + self.memory_blocks.push(MDMemoryDescriptor { + start_of_memory_range: ip_range.start, + memory: ip_location, + }); + } + } + } + + let list_header = + MemoryWriter::<u32>::alloc_with_val(buffer, self.memory_blocks.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::MemoryListStream as u32, + location: list_header.location(), + }; + + let block_list = + MemoryArrayWriter::<MDMemoryDescriptor>::alloc_from_array(buffer, &self.memory_blocks)?; + + dirent.location.data_size += block_list.location().data_size; + Ok(dirent) + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/misc_info.rs b/third_party/rust/minidump-writer/src/mac/streams/misc_info.rs new file mode 100644 index 0000000000..629b94cee6 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/misc_info.rs @@ -0,0 +1,179 @@ +use super::*; +use format::{MiscInfoFlags, MINIDUMP_MISC_INFO_2 as MDRawMiscInfo}; +use std::time::Duration; + +/// From <usr/include/mach/time_value.h> +#[repr(C)] +#[derive(Copy, Clone)] +struct TimeValue { + seconds: i32, + microseconds: i32, +} + +impl From<TimeValue> for Duration { + fn from(tv: TimeValue) -> Self { + let mut seconds = tv.seconds as u64; + let mut microseconds = tv.microseconds as u32; + // This _probably_ will never happen, but this will avoid a panic in + // Duration::new() if it does + if tv.microseconds >= 1000000 { + seconds += 1; + microseconds -= 1000000; + } + + Duration::new(seconds, microseconds * 1000) + } +} + +/// From <usr/include/mach/task_info.h>, this includes basic information about +/// a task. +#[repr(C, packed(4))] +struct MachTaskBasicInfo { + /// Virtual memory size in bytes + virtual_size: u64, + /// Resident memory size in bytes + resident_size: u64, + /// Maximum resident memory size in bytes + resident_size_max: u64, + /// Total user run time for terminated threads + user_time: TimeValue, + /// Total system run time for terminated threads + system_time: TimeValue, + /// Default policy for new threads + policy: i32, + /// Suspend count for task + suspend_count: i32, +} + +impl mach::TaskInfo for MachTaskBasicInfo { + const FLAVOR: u32 = mach::task_info::MACH_TASK_BASIC_INFO; +} + +/// From <usr/include/mach/task_info.h>, this includes times for currently +/// live threads in the task. +#[repr(C, packed(4))] +struct TaskThreadsTimeInfo { + /// Total user run time for live threads + user_time: TimeValue, + /// total system run time for live threads + system_time: TimeValue, +} + +impl mach::TaskInfo for TaskThreadsTimeInfo { + const FLAVOR: u32 = mach::task_info::TASK_THREAD_TIMES_INFO; +} + +impl MinidumpWriter { + /// Writes the [`MDStreamType::MiscInfoStream`] stream. + /// + /// On MacOS, we write a [`minidump_common::format::MINIDUMP_MISC_INFO_2`] + /// to this stream, which includes the start time of the process at second + /// granularity, and the (approximate) amount of time spent in user and + /// system (kernel) time for the lifetime of the task. We attempt to also + /// retrieve power ie CPU usage statistics, though this information is only + /// currently available on x86_64, not aarch64 at the moment. + pub(crate) fn write_misc_info( + &mut self, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + let mut info_section = MemoryWriter::<MDRawMiscInfo>::alloc(buffer)?; + let dirent = MDRawDirectory { + stream_type: MDStreamType::MiscInfoStream as u32, + location: info_section.location(), + }; + + let pid = dumper.pid_for_task()?; + + let mut misc_info = MDRawMiscInfo { + size_of_info: std::mem::size_of::<MDRawMiscInfo>() as u32, + flags1: MiscInfoFlags::MINIDUMP_MISC1_PROCESS_ID.bits() + | MiscInfoFlags::MINIDUMP_MISC1_PROCESS_TIMES.bits() + | MiscInfoFlags::MINIDUMP_MISC1_PROCESSOR_POWER_INFO.bits(), + process_id: pid as u32, + process_create_time: 0, + process_user_time: 0, + process_kernel_time: 0, + processor_max_mhz: 0, + processor_current_mhz: 0, + processor_mhz_limit: 0, + processor_max_idle_state: 0, + processor_current_idle_state: 0, + }; + + // Note that both Breakpad and Crashpad use `sysctl CTL_KERN, KERN_PROC, KERN_PROC_PID` + // to retrieve the process start time, but none of the structures that + // are filled in by that call are in libc at the moment, and `proc_pidinfo` + // seems to work just fine, so using that instead. + // + // SAFETY: syscall + misc_info.process_create_time = unsafe { + // Breakpad was using an old method to retrieve this, let's try the + // BSD method instead which is already implemented in libc + let mut proc_info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::uninit(); + let size = std::mem::size_of::<libc::proc_bsdinfo>() as i32; + if libc::proc_pidinfo( + pid, + libc::PROC_PIDTBSDINFO, + 0, + proc_info.as_mut_ptr().cast(), + size, + ) == size + { + let proc_info = proc_info.assume_init(); + + proc_info.pbi_start_tvsec as u32 + } else { + 0 + } + }; + + // Note that Breakpad is using `getrusage` to retrieve this information, + // however that is wrong, as it can only retrieve the process usage information + // for the current or children processes, not an external process, so + // we use the Crashpad method, which is itself based off of the XNU + // method of retrieving the process times + // https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/kern/kern_resource.c#L1215 + + // The basic task info keeps the timings for all of the terminated threads + let basic_info = dumper.task_info::<MachTaskBasicInfo>().ok(); + + // THe thread times info keeps the timings for all of the living threads + let thread_times_info = dumper.task_info::<TaskThreadsTimeInfo>().ok(); + + let user_time = basic_info + .as_ref() + .map(|bi| Duration::from(bi.user_time)) + .unwrap_or_default() + + thread_times_info + .as_ref() + .map(|tt| Duration::from(tt.user_time)) + .unwrap_or_default(); + let system_time = basic_info + .as_ref() + .map(|bi| Duration::from(bi.system_time)) + .unwrap_or_default() + + thread_times_info + .as_ref() + .map(|tt| Duration::from(tt.system_time)) + .unwrap_or_default(); + + misc_info.process_user_time = user_time.as_secs() as u32; + misc_info.process_kernel_time = system_time.as_secs() as u32; + + // Note that neither of these two keys are present on aarch64, at least atm + let max: u64 = mach::sysctl_by_name(b"hw.cpufrequency_max\0"); + let freq: u64 = mach::sysctl_by_name(b"hw.cpufrequency\0"); + + let max = (max / 1000 * 1000) as u32; + let current = (freq / 1000 * 1000) as u32; + + misc_info.processor_max_mhz = max; + misc_info.processor_mhz_limit = max; + misc_info.processor_current_mhz = current; + + info_section.set_value(buffer, misc_info)?; + + Ok(dirent) + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/module_list.rs b/third_party/rust/minidump-writer/src/mac/streams/module_list.rs new file mode 100644 index 0000000000..2b4d13ea74 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/module_list.rs @@ -0,0 +1,414 @@ +use super::*; + +struct ImageLoadInfo { + /// The preferred load address of the TEXT segment + vm_addr: u64, + /// The size of the TEXT segment + vm_size: u64, + /// The difference between the images preferred and actual load address + slide: isize, +} + +struct ImageDetails { + /// Unique identifier for the module + uuid: [u8; 16], + /// The load info for the image indicating the range of addresses it covers + load_info: ImageLoadInfo, + /// Path to the module on the local filesystem. Note that as of MacOS 11.0.1 + /// for system libraries, this path won't actually exist on the filesystem. + /// This data is more useful as human readable information in a minidump, + /// but is not required, as the real identifier is the UUID + file_path: Option<String>, + /// Version information, not present for the main executable + version: Option<u32>, +} + +impl MinidumpWriter { + /// Writes the [`MDStreamType::ModuleListStream`] to the minidump, which is + /// the last of all loaded modules (images) in the process. + /// + /// Notably, this includes the UUID of the image which is needed to look up + /// debug symbols for the module, as well as the address range covered by + /// the module to know which debug symbols are used to resolve which instruction + /// addresses + pub(crate) fn write_module_list( + &mut self, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + // The list of modules is pretty critical information, but there could + // still be useful information in the minidump without them if we can't + // retrieve them for some reason + let modules = self + .write_loaded_modules(buffer, dumper) + .unwrap_or_default(); + + let list_header = MemoryWriter::<u32>::alloc_with_val(buffer, modules.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ModuleListStream as u32, + location: list_header.location(), + }; + + if !modules.is_empty() { + let mapping_list = MemoryArrayWriter::<MDRawModule>::alloc_from_iter(buffer, modules)?; + dirent.location.data_size += mapping_list.location().data_size; + } + + Ok(dirent) + } + + fn write_loaded_modules( + &self, + buf: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<Vec<MDRawModule>, WriterError> { + let (all_images_info, mut images) = dumper.read_images()?; + + // Apparently MacOS will happily list the same image multiple times + // for some reason, so sort the images by load address and remove all + // of the duplicates + images.sort(); + images.dedup(); + + let mut modules = Vec::with_capacity(images.len()); + + for image in images { + if let Ok(image_details) = self.read_image(image, dumper) { + let is_main_executable = image_details.version.is_none(); + + if let Ok(module) = self.write_module(image_details, buf) { + // We want to keep the modules sorted by their load address except + // in the case of the main executable image which we want to put + // first, as it is most likely the culprit, or at least generally + // the most interesting module for human and machine inspectors + if is_main_executable { + modules.insert(0, module); + } else { + modules.push(module) + }; + } + } + } + + if !modules + .get(0) + .map(|rm| rm.version_info.signature != format::VS_FFI_SIGNATURE) + .unwrap_or_default() + { + Err(TaskDumpError::NoExecutableImage.into()) + } else { + // Crashpad also has code for loading the dyld info from the all images + // array above, but AFAICT (and from crashpad's own comments) this will + // never actually happen. It's more robust in the face of changes from + // Apple, which considering their penchant for changings things often + // and not actually documenting anything, is fair, but if that ever + // happens we can just...change the code. + if let Ok(dyld_image) = self.read_dyld(&all_images_info, dumper) { + if let Ok(module) = self.write_module(dyld_image, buf) { + modules.push(module); + } + } + + Ok(modules) + } + } + + /// Obtains important image metadata by traversing the image's load commands + /// + /// # Errors + /// + /// The image's load commands cannot be traversed, or a required load command + /// is missing + fn read_image( + &self, + image: ImageInfo, + dumper: &TaskDumper, + ) -> Result<ImageDetails, TaskDumpError> { + let mut load_info = None; + let mut version = None; + let mut uuid = None; + + { + let load_commands = dumper.read_load_commands(&image)?; + + for lc in load_commands.iter() { + match lc { + mach::LoadCommand::Segment(seg) if load_info.is_none() => { + if &seg.segment_name[..7] == b"__TEXT\0" { + let slide = image.load_address as isize - seg.vm_addr as isize; + + load_info = Some(ImageLoadInfo { + vm_addr: seg.vm_addr, + vm_size: seg.vm_size, + slide, + }); + } + } + mach::LoadCommand::Dylib(dylib) if version.is_none() => { + version = Some(dylib.dylib.current_version); + } + mach::LoadCommand::Uuid(img_id) if uuid.is_none() => { + uuid = Some(img_id.uuid); + } + _ => {} + } + + if load_info.is_some() && version.is_some() && uuid.is_some() { + break; + } + } + } + + let load_info = load_info.ok_or(TaskDumpError::MissingLoadCommand { + name: "LC_SEGMENT_64", + id: mach::LoadCommandKind::Segment, + })?; + let uuid = uuid.ok_or(TaskDumpError::MissingLoadCommand { + name: "LC_UUID", + id: mach::LoadCommandKind::Uuid, + })?; + + let file_path = if image.file_path != 0 { + dumper + .read_string(image.file_path, None) + .unwrap_or_default() + } else { + None + }; + + Ok(ImageDetails { + uuid, + load_info, + file_path, + version, + }) + } + + /// Reads the dynamic linker, which is similar but + fn read_dyld( + &self, + all_images: &task_dumper::AllImagesInfo, + dumper: &TaskDumper, + ) -> Result<ImageDetails, TaskDumpError> { + let image = ImageInfo { + load_address: all_images.dyld_image_load_address, + file_path: 0, + file_mod_date: 0, + }; + + let mut load_info = None; + let mut version = None; + let mut uuid = None; + let mut file_path = None; + + { + let load_commands = dumper.read_load_commands(&image)?; + + for lc in load_commands.iter() { + match lc { + mach::LoadCommand::Segment(seg) if load_info.is_none() => { + if &seg.segment_name[..7] == b"__TEXT\0" { + let slide = image.load_address as isize - seg.vm_addr as isize; + + load_info = Some(ImageLoadInfo { + vm_addr: seg.vm_addr, + vm_size: seg.vm_size, + slide, + }); + } + } + mach::LoadCommand::Dylib(dylib) if version.is_none() => { + version = Some(dylib.dylib.current_version); + } + mach::LoadCommand::Uuid(img_id) if uuid.is_none() => { + uuid = Some(img_id.uuid); + } + mach::LoadCommand::DylinkerCommand(dy_cmd) if file_path.is_none() => { + file_path = Some(dy_cmd.name.to_owned()); + } + _ => {} + } + + if load_info.is_some() && version.is_some() && uuid.is_some() && file_path.is_some() + { + break; + } + } + } + + let load_info = load_info.ok_or(TaskDumpError::MissingLoadCommand { + name: "LC_SEGMENT_64", + id: mach::LoadCommandKind::Segment, + })?; + let uuid = uuid.ok_or(TaskDumpError::MissingLoadCommand { + name: "LC_UUID", + id: mach::LoadCommandKind::Uuid, + })?; + + Ok(ImageDetails { + uuid, + load_info, + file_path, + version, + }) + } + + fn write_module( + &self, + image: ImageDetails, + buf: &mut DumpBuf, + ) -> Result<MDRawModule, WriterError> { + let file_path = image.file_path.as_deref().unwrap_or_default(); + let module_name = write_string_to_location(buf, file_path)?; + + let mut raw_module = MDRawModule { + base_of_image: (image.load_info.vm_addr as isize + image.load_info.slide) as u64, + size_of_image: image.load_info.vm_size as u32, + module_name_rva: module_name.rva, + ..Default::default() + }; + + // Version info is not available for the main executable image since + // it doesn't issue a LC_ID_DYLIB load command + if let Some(version) = image.version { + raw_module.version_info.signature = format::VS_FFI_SIGNATURE; + raw_module.version_info.struct_version = format::VS_FFI_STRUCVERSION; + + // Convert MAC dylib version format, which is a 32 bit number, to the + // format used by minidump. + raw_module.version_info.file_version_hi = version >> 16; + raw_module.version_info.file_version_lo = ((version & 0xff00) << 8) | (version & 0xff); + } + + let module_name = if let Some(sep_index) = file_path.rfind('/') { + &file_path[sep_index + 1..] + } else if file_path.is_empty() { + "<Unknown>" + } else { + file_path + }; + + #[derive(scroll::Pwrite, scroll::SizeWith)] + struct CvInfoPdb { + cv_signature: u32, + signature: format::GUID, + age: u32, + } + + let cv = MemoryWriter::alloc_with_val( + buf, + CvInfoPdb { + cv_signature: format::CvSignature::Pdb70 as u32, + age: 0, + signature: image.uuid.into(), + }, + )?; + + // Note that we don't use write_string_to_location here as the module + // name is a simple 8-bit string, not 16-bit like most other strings + // in the minidump, and is directly part of the record itself, not an rva + buf.write_all(module_name.as_bytes()); + buf.write_all(&[0]); // null terminator + + let mut cv_location = cv.location(); + cv_location.data_size += module_name.len() as u32 + 1; + raw_module.cv_record = cv_location; + + Ok(raw_module) + } +} + +#[cfg(test)] +// The libc functions used here are all marked as deprecated, saying you +// should use the mach2 crate, however, the mach2 crate does not expose +// any of these functions so... +#[allow(deprecated)] +mod test { + use super::*; + + // This function isn't declared in libc nor mach2. And is also undocumented + // by apple, I know, SHOCKING + extern "C" { + fn getsegmentdata( + header: *const libc::mach_header, + segname: *const u8, + size: &mut u64, + ) -> *const u8; + } + + /// Tests that the images we write as modules to the minidump are consistent + /// with those reported by the kernel. The kernel function used as the source + /// of truth can only be used to obtain info for the current process, which + /// is why they aren't used in the actual implementation as we want to handle + /// both the local and intra-process scenarios + #[test] + fn images_match() { + let mdw = MinidumpWriter::new(None, None); + let td = TaskDumper::new(mdw.task); + + let (all_images, images) = td.read_images().unwrap(); + + let actual_image_count = unsafe { libc::_dyld_image_count() } as u32; + + assert_eq!(actual_image_count, images.len() as u32); + + for index in 0..actual_image_count { + let expected_img_hdr = unsafe { libc::_dyld_get_image_header(index) }; + + let actual_img = &images[index as usize]; + + assert_eq!(actual_img.load_address, expected_img_hdr as u64); + + let mut expect_segment_size = 0; + let expect_segment_data = unsafe { + getsegmentdata( + expected_img_hdr, + b"__TEXT\0".as_ptr(), + &mut expect_segment_size, + ) + }; + + let actual_img_details = mdw + .read_image(*actual_img, &td) + .expect("failed to get image details"); + + let expected_image_name = + unsafe { std::ffi::CStr::from_ptr(libc::_dyld_get_image_name(index)) }; + + let expected_slide = unsafe { libc::_dyld_get_image_vmaddr_slide(index) }; + assert_eq!( + expected_slide, actual_img_details.load_info.slide, + "image {index}({expected_image_name:?}) slide is incorrect" + ); + + // The segment pointer has already been adjusted by the slide + assert_eq!( + expect_segment_data as u64, + (actual_img_details.load_info.vm_addr as isize + actual_img_details.load_info.slide) + as u64, + "image {index}({expected_image_name:?}) TEXT address is incorrect" + ); + assert_eq!( + expect_segment_size, actual_img_details.load_info.vm_size, + "image {index}({expected_image_name:?}) TEXT size is incorrect" + ); + + assert_eq!( + expected_image_name.to_str().unwrap(), + actual_img_details.file_path.unwrap() + ); + } + + let dyld = mdw + .read_dyld(&all_images, &td) + .expect("failed to read dyld"); + + // If the user overrides the dynamic linker and runs this test it will + // fail, but that's kind of on you, person reading this comment wondering + // why the test fails. Or Apple changed the path in whatever MacOS version + // in which case, please file a PR! + assert_eq!("/usr/lib/dyld", dyld.file_path.as_deref().unwrap()); + assert!(dyld.load_info.vm_size > 0); + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/system_info.rs b/third_party/rust/minidump-writer/src/mac/streams/system_info.rs new file mode 100644 index 0000000000..aac2de573f --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/system_info.rs @@ -0,0 +1,200 @@ +use super::*; +use crate::minidump_format::*; + +/// Retrieve the OS version information. +/// +/// Note that this only works on 10.13.4+, but that release is over 4 years old +/// and 1 version behind the latest unsupported release at the time of this writing +/// +/// Note that Breakpad/Crashpad use a private API in CoreFoundation to do this +/// via _CFCopySystemVersionDictionary->_kCFSystemVersionProductVersionKey +fn os_version() -> (u32, u32, u32) { + let vers = mach::sysctl_string(b"kern.osproductversion\0"); + + let inner = || { + let mut it = vers.split('.'); + + let major: u32 = it.next()?.parse().ok()?; + let minor: u32 = it.next()?.parse().ok()?; + let patch: u32 = it.next().and_then(|p| p.parse().ok()).unwrap_or_default(); + + Some((major, minor, patch)) + }; + + inner().unwrap_or_default() +} + +/// Retrieves the OS build version. +/// +/// Note that Breakpad/Crashpad use a private API in CoreFoundation to do this +/// via _CFCopySystemVersionDictionary->_kCFSystemVersionBuildVersionKey. I have +/// no idea how long this has been the case, but the same information can be +/// retrieved via `sysctlbyname` via the `kern.osversion` key as seen by comparing +/// its value versus the output of the `sw_vers -buildVersion` command +#[inline] +fn build_version() -> String { + mach::sysctl_string(b"kern.osversion\0") +} + +/// Retrieves more detailed information on the cpu. +/// +/// Note that this function is only implemented on `x86_64` as Apple doesn't +/// expose similar info on `aarch64` (or at least, not via the same mechanisms) +fn read_cpu_info(cpu: &mut format::CPU_INFORMATION) { + if !cfg!(target_arch = "x86_64") { + return; + } + + let mut md_feats: u64 = 1 << 2 /*PF_COMPARE_EXCHANGE_DOUBLE*/; + let features: u64 = mach::sysctl_by_name(b"machdep.cpu.feature_bits\0"); + + // Map the cpuid feature to its equivalent minidump cpu feature. + // See https://en.wikipedia.org/wiki/CPUID for where the values for the + // various cpuid bits come from, and + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent + // for where the bits for the the minidump come from + macro_rules! map_feature { + ($set:expr, $cpuid_bit:expr, $md_bit:expr) => { + if $set & (1 << $cpuid_bit) != 0 { + md_feats |= 1 << $md_bit; + } + }; + } + + map_feature!( + features, 4, /*TSC*/ + 8 /* PF_RDTSC_INSTRUCTION_AVAILABLE */ + ); + map_feature!(features, 6 /*PAE*/, 9 /* PF_PAE_ENABLED */); + map_feature!( + features, 23, /*MMX*/ + 3 /* PF_MMX_INSTRUCTIONS_AVAILABLE */ + ); + map_feature!( + features, 25, /*SSE*/ + 6 /* PF_XMMI_INSTRUCTIONS_AVAILABLE */ + ); + map_feature!( + features, 26, /*SSE2*/ + 10 /* PF_XMMI64_INSTRUCTIONS_AVAILABLE */ + ); + map_feature!( + features, 32, /*SSE3*/ + 13 /* PF_SSE3_INSTRUCTIONS_AVAILABLE */ + ); + map_feature!( + features, 45, /*CX16*/ + 14 /* PF_COMPARE_EXCHANGE128 */ + ); + map_feature!(features, 58 /*XSAVE*/, 17 /* PF_XSAVE_ENABLED */); + map_feature!( + features, 62, /*RDRAND*/ + 28 /* PF_RDRAND_INSTRUCTION_AVAILABLE */ + ); + + let ext_features: u64 = mach::sysctl_by_name(b"machdep.cpu.extfeature_bits\0"); + + map_feature!( + ext_features, + 27, /* RDTSCP */ + 32 /* PF_RDTSCP_INSTRUCTION_AVAILABLE */ + ); + map_feature!( + ext_features, + 31, /* 3DNOW */ + 7 /* PF_3DNOW_INSTRUCTIONS_AVAILABLE */ + ); + + let leaf_features: u32 = mach::sysctl_by_name(b"machdep.cpu.leaf7_feature_bits\0"); + map_feature!( + leaf_features, + 0, /* F7_FSGSBASE */ + 22 /* PF_RDWRFSGSBASE_AVAILABLE */ + ); + + // In newer production kernels, NX is always enabled. + // See 10.15.0 xnu-6153.11.26/osfmk/x86_64/pmap.c nx_enabled. + md_feats |= 1 << 12 /* PF_NX_ENABLED */; + + // All CPUs that Apple is known to have shipped should support DAZ. + md_feats |= 1 << 11 /* PF_SSE_DAZ_MODE_AVAILABLE */; + + // minidump_common::format::OtherCpuInfo is just 2 adjacent u64's, we only + // set the first, so just do a direct write to the bytes + cpu.data[..std::mem::size_of::<u64>()].copy_from_slice(&md_feats.to_ne_bytes()); +} + +impl MinidumpWriter { + /// Writes the [`MDStreamType::SystemInfoStream`] stream. + /// + /// On MacOS we includes basic CPU information, though some of it is not + /// available on `aarch64` at the time of this writing, as well as kernel + /// version information. + pub(crate) fn write_system_info( + &mut self, + buffer: &mut DumpBuf, + _dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + let mut info_section = MemoryWriter::<MDRawSystemInfo>::alloc(buffer)?; + let dirent = MDRawDirectory { + stream_type: MDStreamType::SystemInfoStream as u32, + location: info_section.location(), + }; + + let number_of_processors: u8 = mach::int_sysctl_by_name(b"hw.ncpu\0"); + // SAFETY: POD buffer + let mut cpu: format::CPU_INFORMATION = unsafe { std::mem::zeroed() }; + read_cpu_info(&mut cpu); + + cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + let processor_architecture = MDCPUArchitecture::PROCESSOR_ARCHITECTURE_AMD64; + + // machdep.cpu.family and machdep.cpu.model already take the extended family + // and model IDs into account. See 10.9.2 xnu-2422.90.20/osfmk/i386/cpuid.c + // cpuid_set_generic_info(). + let processor_level: u16 = mach::int_sysctl_by_name(b"machdep.cpu.family\0"); + let model: u8 = mach::int_sysctl_by_name(b"machdep.cpu.model\0"); + let stepping: u8 = mach::int_sysctl_by_name(b"machdep.cpu.stepping\0"); + + let processor_revision = ((model as u16) << 8) | stepping as u16; + } else if #[cfg(target_arch = "aarch64")] { + let processor_architecture = MDCPUArchitecture::PROCESSOR_ARCHITECTURE_ARM64_OLD; + + let family: u32 = mach::sysctl_by_name(b"hw.cpufamily\0"); + + let processor_level = (family & 0xffff0000 >> 16) as u16; + let processor_revision = (family & 0x0000ffff) as u16; + } else { + compile_error!("unsupported target architecture"); + } + } + + let (major_version, minor_version, build_number) = os_version(); + let os_version_loc = write_string_to_location(buffer, &build_version())?; + + let info = MDRawSystemInfo { + // CPU + processor_architecture: processor_architecture as u16, + processor_level, + processor_revision, + number_of_processors, + cpu, + + // OS + platform_id: PlatformId::MacOs as u32, + product_type: 1, // VER_NT_WORKSTATION, could also be VER_NT_SERVER but...seriously? + major_version, + minor_version, + build_number, + csd_version_rva: os_version_loc.rva, + + suite_mask: 0, + reserved2: 0, + }; + + info_section.set_value(buffer, info)?; + + Ok(dirent) + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/thread_list.rs b/third_party/rust/minidump-writer/src/mac/streams/thread_list.rs new file mode 100644 index 0000000000..180bb2f665 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/thread_list.rs @@ -0,0 +1,219 @@ +use super::*; +use crate::minidump_cpu::RawContextCPU; + +impl MinidumpWriter { + /// Writes the [`MDStreamType::ThreadListStream`] which is an array of + /// [`miniduimp_common::format::MINIDUMP_THREAD`] + pub(crate) fn write_thread_list( + &mut self, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + let threads = self.threads(dumper); + + let list_header = MemoryWriter::<u32>::alloc_with_val(buffer, threads.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ThreadListStream as u32, + location: list_header.location(), + }; + + let mut thread_list = MemoryArrayWriter::<MDRawThread>::alloc_array(buffer, threads.len())?; + dirent.location.data_size += thread_list.location().data_size; + + for (i, tid) in threads.enumerate() { + let thread = self.write_thread(tid, buffer, dumper)?; + thread_list.set_value_at(buffer, thread, i)?; + } + + Ok(dirent) + } + + fn write_thread( + &mut self, + tid: u32, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawThread, WriterError> { + let mut thread = MDRawThread { + thread_id: tid, + suspend_count: 0, + priority_class: 0, + priority: 0, + teb: 0, + stack: MDMemoryDescriptor::default(), + thread_context: MDLocationDescriptor::default(), + }; + + let thread_state = dumper.read_thread_state(tid)?; + + self.write_stack_from_start_address(thread_state.sp(), &mut thread, buffer, dumper)?; + + let mut cpu: RawContextCPU = Default::default(); + Self::fill_cpu_context(&thread_state, &mut cpu); + let cpu_section = MemoryWriter::alloc_with_val(buffer, cpu)?; + thread.thread_context = cpu_section.location(); + Ok(thread) + } + + fn write_stack_from_start_address( + &mut self, + start: u64, + thread: &mut MDRawThread, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<(), WriterError> { + thread.stack.start_of_memory_range = start; + thread.stack.memory.data_size = 0; + thread.stack.memory.rva = buffer.position() as u32; + + let stack_size = self.calculate_stack_size(start, dumper); + + // In some situations the stack address for the thread can come back 0. + // In these cases we skip over the threads in question and stuff the + // stack with a clearly borked value. + // + // In other cases, notably a stack overflow, we might fail to read the + // stack eg. InvalidAddress in which case we use a different borked + // value to indicate the different failure + let stack_location = if stack_size != 0 { + dumper + .read_task_memory(start, stack_size) + .map(|stack_buffer| { + let stack_location = MDLocationDescriptor { + data_size: stack_buffer.len() as u32, + rva: buffer.position() as u32, + }; + buffer.write_all(&stack_buffer); + stack_location + }) + .ok() + } else { + None + }; + + thread.stack.memory = stack_location.unwrap_or_else(|| { + let borked = if stack_size == 0 { + 0xdeadbeef + } else { + 0xdeaddead + }; + + thread.stack.start_of_memory_range = borked; + + let stack_location = MDLocationDescriptor { + data_size: 16, + rva: buffer.position() as u32, + }; + buffer.write_all(&borked.to_ne_bytes()); + buffer.write_all(&borked.to_ne_bytes()); + stack_location + }); + + // Add the stack memory as a raw block of memory, this is written to + // the minidump as part of the memory list stream + self.memory_blocks.push(thread.stack); + Ok(()) + } + + fn calculate_stack_size(&self, start_address: u64, dumper: &TaskDumper) -> usize { + if start_address == 0 { + return 0; + } + + let mut region = if let Ok(region) = dumper.get_vm_region(start_address) { + region + } else { + return 0; + }; + + // Failure or stack corruption, since mach_vm_region had to go + // higher in the process address space to find a valid region. + if start_address < region.range.start { + return 0; + } + + let root_range_start = region.range.start; + let mut stack_size = region.range.end - region.range.start; + + // If the user tag is VM_MEMORY_STACK, look for more readable regions with + // the same tag placed immediately above the computed stack region. Under + // some circumstances, the stack for thread 0 winds up broken up into + // multiple distinct abutting regions. This can happen for several reasons, + // including user code that calls setrlimit(RLIMIT_STACK, ...) or changes + // the access on stack pages by calling mprotect. + if region.info.user_tag == mach2::vm_statistics::VM_MEMORY_STACK { + loop { + let proposed_next_region_base = region.range.end; + + region = if let Ok(reg) = dumper.get_vm_region(region.range.end) { + reg + } else { + break; + }; + + if region.range.start != proposed_next_region_base + || region.info.user_tag != mach2::vm_statistics::VM_MEMORY_STACK + || (region.info.protection & mach2::vm_prot::VM_PROT_READ) == 0 + { + break; + } + + stack_size += region.range.end - region.range.start; + } + } + + (root_range_start + stack_size - start_address) as usize + } + + pub(crate) fn fill_cpu_context( + thread_state: &crate::mac::mach::ThreadState, + out: &mut RawContextCPU, + ) { + let ts = thread_state.arch_state(); + + cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + out.context_flags = format::ContextFlagsCpu::CONTEXT_AMD64.bits(); + + out.rax = ts.__rax; + out.rbx = ts.__rbx; + out.rcx = ts.__rcx; + out.rdx = ts.__rdx; + out.rdi = ts.__rdi; + out.rsi = ts.__rsi; + out.rbp = ts.__rbp; + out.rsp = ts.__rsp; + out.r8 = ts.__r8; + out.r9 = ts.__r9; + out.r10 = ts.__r10; + out.r11 = ts.__r11; + out.r12 = ts.__r12; + out.r13 = ts.__r13; + out.r14 = ts.__r14; + out.r15 = ts.__r15; + out.rip = ts.__rip; + // according to AMD's software developer guide, bits above 18 are + // not used in the flags register. Since the minidump format + // specifies 32 bits for the flags register, we can truncate safely + // with no loss. + out.eflags = ts.__rflags as _; + out.cs = ts.__cs as u16; + out.fs = ts.__fs as u16; + out.gs = ts.__gs as u16; + } else if #[cfg(target_arch = "aarch64")] { + // This is kind of a lie as we don't actually include the full float state..? + out.context_flags = format::ContextFlagsArm64Old::CONTEXT_ARM64_OLD_FULL.bits() as u64; + + out.cpsr = ts.cpsr; + out.iregs[..29].copy_from_slice(&ts.x[..29]); + out.iregs[29] = ts.fp; + out.iregs[30] = ts.lr; + out.sp = ts.sp; + out.pc = ts.pc; + } else { + compile_error!("unsupported target arch"); + } + } + } +} diff --git a/third_party/rust/minidump-writer/src/mac/streams/thread_names.rs b/third_party/rust/minidump-writer/src/mac/streams/thread_names.rs new file mode 100644 index 0000000000..42242a6397 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/streams/thread_names.rs @@ -0,0 +1,79 @@ +use super::*; + +impl MinidumpWriter { + /// Writes the [`MDStreamType::ThreadNamesStream`] which is an array of + /// [`miniduimp_common::format::MINIDUMP_THREAD`] + pub(crate) fn write_thread_names( + &mut self, + buffer: &mut DumpBuf, + dumper: &TaskDumper, + ) -> Result<MDRawDirectory, WriterError> { + let threads = self.threads(dumper); + + let list_header = MemoryWriter::<u32>::alloc_with_val(buffer, threads.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ThreadNamesStream as u32, + location: list_header.location(), + }; + + let mut names = MemoryArrayWriter::<MDRawThreadName>::alloc_array(buffer, threads.len())?; + dirent.location.data_size += names.location().data_size; + + for (i, tid) in threads.enumerate() { + // It's unfortunate if we can't grab a thread name, but it's also + // not a critical failure + let name_loc = match Self::write_thread_name(buffer, dumper, tid) { + Ok(loc) => loc, + Err(_err) => { + // TODO: log error + write_string_to_location(buffer, "")? + } + }; + + let thread = MDRawThreadName { + thread_id: tid, + thread_name_rva: name_loc.rva.into(), + }; + + names.set_value_at(buffer, thread, i)?; + } + + Ok(dirent) + } + + /// Attempts to retrieve and write the threadname, returning the threa names + /// location if successful + fn write_thread_name( + buffer: &mut Buffer, + dumper: &TaskDumper, + tid: u32, + ) -> Result<MDLocationDescriptor, WriterError> { + // As noted in usr/include/mach/thread_info.h, the THREAD_EXTENDED_INFO + // return is exactly the same as proc_pidinfo(..., proc_threadinfo) + impl mach::ThreadInfo for libc::proc_threadinfo { + const FLAVOR: u32 = 5; // THREAD_EXTENDED_INFO + } + + let thread_info: libc::proc_threadinfo = dumper.thread_info(tid)?; + + let name = std::str::from_utf8( + // SAFETY: This is an initialized block of static size + unsafe { + std::slice::from_raw_parts( + thread_info.pth_name.as_ptr().cast(), + thread_info.pth_name.len(), + ) + }, + ) + .unwrap_or_default(); + + // Ignore the null terminator + let tname = match name.find('\0') { + Some(i) => &name[..i], + None => name, + }; + + Ok(write_string_to_location(buffer, tname)?) + } +} diff --git a/third_party/rust/minidump-writer/src/mac/task_dumper.rs b/third_party/rust/minidump-writer/src/mac/task_dumper.rs new file mode 100644 index 0000000000..013d432d26 --- /dev/null +++ b/third_party/rust/minidump-writer/src/mac/task_dumper.rs @@ -0,0 +1,462 @@ +use crate::mac::mach; +use mach2::mach_types as mt; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TaskDumpError { + #[error("kernel error {syscall} {error})")] + Kernel { + syscall: &'static str, + error: mach::KernelError, + }, + #[error("detected an invalid mach image header")] + InvalidMachHeader, + #[error(transparent)] + NonUtf8String(#[from] std::string::FromUtf8Error), + #[error("unable to find the main executable image for the process")] + NoExecutableImage, + #[error("expected load command {name}({id:?}) was not found for an image")] + MissingLoadCommand { + name: &'static str, + id: mach::LoadCommandKind, + }, +} + +/// Wraps a mach call in a Result +macro_rules! mach_call { + ($call:expr) => {{ + // SAFETY: syscall + let kr = unsafe { $call }; + if kr == mach::KERN_SUCCESS { + Ok(()) + } else { + // This is ugly, improvements to the macro welcome! + let mut syscall = stringify!($call); + if let Some(i) = syscall.find('(') { + syscall = &syscall[..i]; + } + Err(TaskDumpError::Kernel { + syscall, + error: kr.into(), + }) + } + }}; +} + +/// `dyld_all_image_infos` from <usr/include/mach-o/dyld_images.h> +/// +/// This struct is truncated as we only need a couple of fields at the beginning +/// of the struct +#[repr(C)] +#[derive(Copy, Clone)] +pub struct AllImagesInfo { + // VERSION 1 + pub version: u32, + /// The number of [`ImageInfo`] structs at that following address + info_array_count: u32, + /// The address in the process where the array of [`ImageInfo`] structs is + info_array_addr: u64, + /// A function pointer, unused + _notification: u64, + /// Unused + _process_detached_from_shared_region: bool, + // VERSION 2 + lib_system_initialized: bool, + // Note that crashpad adds a 32-bit int here to get proper alignment when + // building on 32-bit targets...but we explicitly don't care about 32-bit + // targets since Apple doesn't + pub dyld_image_load_address: u64, +} + +/// `dyld_image_info` from <usr/include/mach-o/dyld_images.h> +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ImageInfo { + /// The address in the process where the image is loaded + pub load_address: u64, + /// The address in the process where the image's file path can be read + pub file_path: u64, + /// Timestamp for when the image's file was last modified + pub file_mod_date: u64, +} + +impl PartialEq for ImageInfo { + fn eq(&self, o: &Self) -> bool { + self.load_address == o.load_address + } +} + +impl Eq for ImageInfo {} + +impl Ord for ImageInfo { + fn cmp(&self, o: &Self) -> std::cmp::Ordering { + self.load_address.cmp(&o.load_address) + } +} + +impl PartialOrd for ImageInfo { + fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(o)) + } +} + +/// Describes a region of virtual memory +pub struct VMRegionInfo { + pub info: mach::vm_region_submap_info_64, + pub range: std::ops::Range<u64>, +} + +/// Similarly to PtraceDumper for Linux, this provides access to information +/// for a task (MacOS process) +pub struct TaskDumper { + task: mt::task_t, + page_size: i64, +} + +impl TaskDumper { + /// Constructs a [`TaskDumper`] for the specified task + pub fn new(task: mt::task_t) -> Self { + Self { + task, + // SAFETY: syscall + page_size: unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as i64, + } + } + + /// Reads a block of memory from the task + /// + /// # Errors + /// + /// The syscall to read the task's memory fails for some reason, eg bad address. + pub fn read_task_memory<T>(&self, address: u64, count: usize) -> Result<Vec<T>, TaskDumpError> + where + T: Sized + Clone, + { + let length = (count * std::mem::size_of::<T>()) as u64; + + // use the negative of the page size for the mask to find the page address + let page_address = address & (-self.page_size as u64); + let last_page_address = + (address + length + (self.page_size - 1) as u64) & (-self.page_size as u64); + + let page_size = last_page_address - page_address; + let mut local_start = 0; + let mut local_length = 0; + + mach_call!(mach::mach_vm_read( + self.task, + page_address, + page_size, + &mut local_start, + &mut local_length + ))?; + + let mut buffer = Vec::with_capacity(count); + + // SAFETY: this is safe as long as the kernel has not lied to us + let task_buffer = unsafe { + std::slice::from_raw_parts( + (local_start as *const u8) + .offset((address - page_address) as isize) + .cast(), + count, + ) + }; + buffer.extend_from_slice(task_buffer); + + // Don't worry about the return here, if something goes wrong there's probably + // not much we can do about it, and we have what we want anyways + let _res = mach_call!(mach::mach_vm_deallocate( + mach::mach_task_self(), + local_start as u64, // vm_read returns a pointer, but vm_deallocate takes a integer address :-/ + local_length as u64, // vm_read and vm_deallocate use different sizes :-/ + )); + + Ok(buffer) + } + + /// Reads a null terminated string starting at the specified address. This + /// is a specialization of [`read_task_memory`] since strings can span VM + /// regions. + /// + /// If not specified, the string is capped at 8k which should never be close + /// to being hit in normal scenarios, at least for "system" strings, which is + /// all this interface is used to retrieve + /// + /// # Errors + /// + /// Fails if the address cannot be read for some reason, or the string is + /// not utf-8. + pub fn read_string( + &self, + addr: u64, + expected_size: Option<usize>, + ) -> Result<Option<String>, TaskDumpError> { + // The problem is we don't know how much to read until we know how long + // the string is. And we don't know how long the string is, until we've read + // the memory! So, we'll try to read kMaxStringLength bytes + // (or as many bytes as we can until we reach the end of the vm region). + let get_region_size = || -> Result<u64, TaskDumpError> { + let region = self.get_vm_region(addr)?; + + let mut size_to_end = region.range.end - addr; + + // If the remaining is less than 4k, check if the next region is + // contiguous, and extend the memory that could contain the string + // to include it + if size_to_end < 4 * 1024 { + let maybe_adjacent = self.get_vm_region(region.range.end)?; + + if maybe_adjacent.range.start == region.range.end { + size_to_end += maybe_adjacent.range.end - maybe_adjacent.range.start; + } + } + + Ok(size_to_end) + }; + + if let Ok(size_to_end) = get_region_size() { + let mut bytes = self.read_task_memory( + addr, + std::cmp::min(size_to_end as usize, expected_size.unwrap_or(8 * 1024)), + )?; + + // Find the null terminator and truncate our string + if let Some(null_pos) = bytes.iter().position(|c| *c == 0) { + bytes.resize(null_pos, 0); + } + + Ok(String::from_utf8(bytes).map(Some)?) + } else { + Ok(None) + } + } + + /// Retrives information on the virtual memory region the specified address + /// is located within. + /// + /// # Errors + /// + /// The syscall to retrieve the VM region information fails for some reason, + /// eg. a bad address. + pub fn get_vm_region(&self, addr: u64) -> Result<VMRegionInfo, TaskDumpError> { + let mut region_base = addr; + let mut region_size = 0; + let mut nesting_level = 0; + let mut submap_info = std::mem::MaybeUninit::<mach::vm_region_submap_info_64>::uninit(); + + // <user/include/mach/vm_region.h> + const VM_REGION_SUBMAP_INFO_COUNT_64: u32 = + (std::mem::size_of::<mach::vm_region_submap_info_64>() / std::mem::size_of::<u32>()) + as u32; + + let mut info_count = VM_REGION_SUBMAP_INFO_COUNT_64; + + mach_call!(mach::mach_vm_region_recurse( + self.task, + &mut region_base, + &mut region_size, + &mut nesting_level, + submap_info.as_mut_ptr().cast(), + &mut info_count, + ))?; + + Ok(VMRegionInfo { + // SAFETY: this will be valid if the syscall succeeded + info: unsafe { submap_info.assume_init() }, + range: region_base..region_base + region_size, + }) + } + + /// Retrieves the state of the specified thread. The state is an architecture + /// specific block of CPU context ie register state. + /// + /// # Errors + /// + /// The specified thread id is invalid, or the thread is in a task that is + /// compiled for a different architecture than this local task. + pub fn read_thread_state(&self, tid: u32) -> Result<mach::ThreadState, TaskDumpError> { + let mut thread_state = mach::ThreadState::default(); + + mach_call!(mach::thread_get_state( + tid, + mach::THREAD_STATE_FLAVOR as i32, + thread_state.state.as_mut_ptr(), + &mut thread_state.state_size, + ))?; + + Ok(thread_state) + } + + /// Reads the specified task information. + /// + /// # Errors + /// + /// The syscall to receive the task information failed for some reason, eg. + /// the specified type and the flavor are mismatched and considered invalid. + pub fn task_info<T: mach::TaskInfo>(&self) -> Result<T, TaskDumpError> { + let mut info = std::mem::MaybeUninit::<T>::uninit(); + let mut count = (std::mem::size_of::<T>() / std::mem::size_of::<u32>()) as u32; + + mach_call!(mach::task::task_info( + self.task, + T::FLAVOR, + info.as_mut_ptr().cast(), + &mut count + ))?; + + // SAFETY: this will be initialized if the call succeeded + unsafe { Ok(info.assume_init()) } + } + + /// Reads the specified task information. + /// + /// # Errors + /// + /// The syscall to receive the task information failed for some reason, eg. + /// the specified type and the flavor are mismatched and considered invalid, + /// or the thread no longer exists + pub fn thread_info<T: mach::ThreadInfo>(&self, tid: u32) -> Result<T, TaskDumpError> { + let mut thread_info = std::mem::MaybeUninit::<T>::uninit(); + let mut count = (std::mem::size_of::<T>() / std::mem::size_of::<u32>()) as u32; + + mach_call!(mach::thread_info( + tid, + T::FLAVOR, + thread_info.as_mut_ptr().cast(), + &mut count, + ))?; + + // SAFETY: this will be initialized if the call succeeded + unsafe { Ok(thread_info.assume_init()) } + } + + /// Retrieves all of the images loaded in the task. + /// + /// Note that there may be multiple images with the same load address. + /// + /// # Errors + /// + /// The syscall to retrieve the location of the loaded images fails, or + /// the syscall to read the loaded images from the process memory fails + pub fn read_images(&self) -> Result<(AllImagesInfo, Vec<ImageInfo>), TaskDumpError> { + impl mach::TaskInfo for mach::task_info::task_dyld_info { + const FLAVOR: u32 = mach::task_info::TASK_DYLD_INFO; + } + + // Retrieve the address at which the list of loaded images is located + // within the task + let all_images_addr = { + let dyld_info = self.task_info::<mach::task_info::task_dyld_info>()?; + dyld_info.all_image_info_addr + }; + + // Here we make the assumption that dyld loaded at the same address in + // the crashed process vs. this one. This is an assumption made in + // "dyld_debug.c" and is said to be nearly always valid. + let dyld_all_info_buf = + self.read_task_memory::<u8>(all_images_addr, std::mem::size_of::<AllImagesInfo>())?; + // SAFETY: this is fine as long as the kernel isn't lying to us + let all_images_info: &AllImagesInfo = unsafe { &*(dyld_all_info_buf.as_ptr().cast()) }; + + let images = self.read_task_memory::<ImageInfo>( + all_images_info.info_array_addr, + all_images_info.info_array_count as usize, + )?; + + Ok((*all_images_info, images)) + } + + /// Retrieves the main executable image for the task. + /// + /// Note that this method is currently only used for tests due to deficiencies + /// in `otool` + /// + /// # Errors + /// + /// Any of the errors that apply to [`Self::read_images`] apply here, in + /// addition to not being able to find the main executable image + pub fn read_executable_image(&self) -> Result<ImageInfo, TaskDumpError> { + let (_, images) = self.read_images()?; + + for img in images { + let mach_header = self.read_task_memory::<mach::MachHeader>(img.load_address, 1)?; + + let header = &mach_header[0]; + + if header.magic != mach::MH_MAGIC_64 { + return Err(TaskDumpError::InvalidMachHeader); + } + + if header.file_type == mach::MH_EXECUTE { + return Ok(img); + } + } + + Err(TaskDumpError::NoExecutableImage) + } + + /// Retrieves the load commands for the specified image + /// + /// # Errors + /// + /// We fail to read the image header for the specified image, the header we + /// read is determined to be invalid, or we fail to read the block of memory + /// containing the load commands themselves. + pub fn read_load_commands(&self, img: &ImageInfo) -> Result<mach::LoadCommands, TaskDumpError> { + let mach_header = self.read_task_memory::<mach::MachHeader>(img.load_address, 1)?; + + let header = &mach_header[0]; + + if header.magic != mach::MH_MAGIC_64 { + return Err(TaskDumpError::InvalidMachHeader); + } + + // Read the load commands which immediately follow the image header from + // the task memory. Note that load commands vary in size so we need to + // retrieve the memory as a raw byte buffer that we can then iterate + // through and step according to the size of each load command + let load_commands_buf = self.read_task_memory::<u8>( + img.load_address + std::mem::size_of::<mach::MachHeader>() as u64, + header.size_commands as usize, + )?; + + Ok(mach::LoadCommands { + buffer: load_commands_buf, + count: header.num_commands, + }) + } + + /// Gets a list of all of the thread ids in the task + /// + /// # Errors + /// + /// The syscall to retrieve the list of threads fails + pub fn read_threads(&self) -> Result<&'static [u32], TaskDumpError> { + let mut threads = std::ptr::null_mut(); + let mut thread_count = 0; + + mach_call!(mach::task_threads( + self.task, + &mut threads, + &mut thread_count + ))?; + + Ok( + // SAFETY: This should be valid if the call succeeded + unsafe { std::slice::from_raw_parts(threads, thread_count as usize) }, + ) + } + + /// Retrieves the PID for the task + /// + /// # Errors + /// + /// Presumably the only way this would fail would be if the task we are + /// dumping disappears. + pub fn pid_for_task(&self) -> Result<i32, TaskDumpError> { + let mut pid = 0; + mach_call!(mach::pid_for_task(self.task, &mut pid))?; + Ok(pid) + } +} diff --git a/third_party/rust/minidump-writer/src/mem_writer.rs b/third_party/rust/minidump-writer/src/mem_writer.rs new file mode 100644 index 0000000000..a703d2b11e --- /dev/null +++ b/third_party/rust/minidump-writer/src/mem_writer.rs @@ -0,0 +1,272 @@ +use crate::minidump_format::{MDLocationDescriptor, MDRVA}; +use scroll::ctx::{SizeWith, TryIntoCtx}; + +#[derive(Debug, thiserror::Error)] +pub enum MemoryWriterError { + #[error("IO error when writing to DumpBuf")] + IOError(#[from] std::io::Error), + #[error("Failed integer conversion")] + TryFromIntError(#[from] std::num::TryFromIntError), + #[error("Failed to write to buffer")] + Scroll(#[from] scroll::Error), +} + +type WriteResult<T> = std::result::Result<T, MemoryWriterError>; + +macro_rules! size { + ($t:ty) => { + <$t>::size_with(&scroll::Endian::Little) + }; +} + +pub struct Buffer { + inner: Vec<u8>, +} + +impl Buffer { + pub fn with_capacity(cap: usize) -> Self { + Self { + inner: Vec::with_capacity(cap), + } + } + + #[inline] + pub fn position(&self) -> u64 { + self.inner.len() as u64 + } + + #[inline] + #[must_use] + fn reserve(&mut self, len: usize) -> usize { + let mark = self.inner.len(); + self.inner.resize(self.inner.len() + len, 0); + mark + } + + #[inline] + fn write<N, E>(&mut self, val: N) -> Result<usize, E> + where + N: TryIntoCtx<scroll::Endian, Error = E> + SizeWith<scroll::Endian>, + E: From<scroll::Error>, + { + self.write_at(self.inner.len(), val) + } + + fn write_at<N, E>(&mut self, offset: usize, val: N) -> Result<usize, E> + where + N: TryIntoCtx<scroll::Endian, Error = E> + SizeWith<scroll::Endian>, + E: From<scroll::Error>, + { + let to_write = size!(N); + let remainder = self.inner.len() - offset; + if remainder < to_write { + self.inner + .resize(self.inner.len() + to_write - remainder, 0); + } + + let dst = &mut self.inner[offset..offset + to_write]; + val.try_into_ctx(dst, scroll::Endian::Little) + } + + #[inline] + pub fn write_all(&mut self, buffer: &[u8]) { + self.inner.extend_from_slice(buffer); + } +} + +impl From<Buffer> for Vec<u8> { + fn from(b: Buffer) -> Self { + b.inner + } +} + +impl std::ops::Deref for Buffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug)] +pub struct MemoryWriter<T> { + pub position: MDRVA, + pub size: usize, + phantom: std::marker::PhantomData<T>, +} + +impl<T> MemoryWriter<T> +where + T: TryIntoCtx<scroll::Endian, Error = scroll::Error> + SizeWith<scroll::Endian>, +{ + /// Create a slot for a type T in the buffer, we can fill right now with real values. + pub fn alloc_with_val(buffer: &mut Buffer, val: T) -> WriteResult<Self> { + // Mark the position as we may overwrite later + let position = buffer.position(); + let size = buffer.write(val)?; + + Ok(Self { + position: position as u32, + size, + phantom: std::marker::PhantomData, + }) + } + + /// Create a slot for a type T in the buffer, we can fill later with real values. + pub fn alloc(buffer: &mut Buffer) -> WriteResult<Self> { + let size = size!(T); + let position = buffer.reserve(size) as u32; + + Ok(Self { + position, + size, + phantom: std::marker::PhantomData, + }) + } + + /// Write actual values in the buffer-slot we got during `alloc()` + #[inline] + pub fn set_value(&mut self, buffer: &mut Buffer, val: T) -> WriteResult<()> { + Ok(buffer.write_at(self.position as usize, val).map(|_sz| ())?) + } + + #[inline] + pub fn location(&self) -> MDLocationDescriptor { + MDLocationDescriptor { + data_size: size!(T) as u32, + rva: self.position, + } + } +} + +#[derive(Debug)] +pub struct MemoryArrayWriter<T> { + pub position: MDRVA, + array_size: usize, + phantom: std::marker::PhantomData<T>, +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +impl MemoryArrayWriter<u8> { + #[inline] + pub fn write_bytes(buffer: &mut Buffer, slice: &[u8]) -> Self { + let position = buffer.position(); + buffer.write_all(slice); + + Self { + position: position as u32, + array_size: slice.len(), + phantom: std::marker::PhantomData, + } + } +} + +impl<T> MemoryArrayWriter<T> +where + T: TryIntoCtx<scroll::Endian, Error = scroll::Error> + SizeWith<scroll::Endian> + Copy, +{ + pub fn alloc_from_array(buffer: &mut Buffer, array: &[T]) -> WriteResult<Self> { + let array_size = array.len(); + let position = buffer.reserve(array_size * size!(T)); + + for (idx, val) in array.iter().enumerate() { + buffer.write_at(position + idx * size!(T), *val)?; + } + + Ok(Self { + position: position as u32, + array_size, + phantom: std::marker::PhantomData, + }) + } +} + +impl<T> MemoryArrayWriter<T> +where + T: TryIntoCtx<scroll::Endian, Error = scroll::Error> + SizeWith<scroll::Endian>, +{ + /// Create a slot for a type T in the buffer, we can fill in the values in one go. + pub fn alloc_from_iter<I>( + buffer: &mut Buffer, + iter: impl IntoIterator<Item = T, IntoIter = I>, + ) -> WriteResult<Self> + where + I: std::iter::ExactSizeIterator<Item = T>, + { + let iter = iter.into_iter(); + let array_size = iter.len(); + let size = size!(T); + let position = buffer.reserve(array_size * size); + + for (idx, val) in iter.enumerate() { + buffer.write_at(position + idx * size, val)?; + } + + Ok(Self { + position: position as u32, + array_size, + phantom: std::marker::PhantomData, + }) + } + + /// Create a slot for a type T in the buffer, we can fill later with real values. + /// This function fills it with `Default::default()`, which is less performant than + /// using uninitialized memory, but safe. + pub fn alloc_array(buffer: &mut Buffer, array_size: usize) -> WriteResult<Self> { + let position = buffer.reserve(array_size * size!(T)); + + Ok(Self { + position: position as u32, + array_size, + phantom: std::marker::PhantomData, + }) + } + + /// Write actual values in the buffer-slot we got during `alloc()` + #[inline] + pub fn set_value_at(&mut self, buffer: &mut Buffer, val: T, index: usize) -> WriteResult<()> { + Ok(buffer + .write_at(self.position as usize + size!(T) * index, val) + .map(|_sz| ())?) + } + + #[inline] + pub fn location(&self) -> MDLocationDescriptor { + MDLocationDescriptor { + data_size: (self.array_size * size!(T)) as u32, + rva: self.position, + } + } + + #[inline] + pub fn location_of_index(&self, idx: usize) -> MDLocationDescriptor { + MDLocationDescriptor { + data_size: size!(T) as u32, + rva: self.position + (size!(T) * idx) as u32, + } + } +} + +pub fn write_string_to_location( + buffer: &mut Buffer, + text: &str, +) -> WriteResult<MDLocationDescriptor> { + let letters: Vec<u16> = text.encode_utf16().collect(); + + // First write size of the string (x letters in u16, times the size of u16) + let text_header = MemoryWriter::<u32>::alloc_with_val( + buffer, + (letters.len() * std::mem::size_of::<u16>()).try_into()?, + )?; + + // Then write utf-16 letters after that + let mut text_section = MemoryArrayWriter::<u16>::alloc_array(buffer, letters.len())?; + for (index, letter) in letters.iter().enumerate() { + text_section.set_value_at(buffer, *letter, index)?; + } + + let mut location = text_header.location(); + location.data_size += text_section.location().data_size; + + Ok(location) +} diff --git a/third_party/rust/minidump-writer/src/minidump_cpu.rs b/third_party/rust/minidump-writer/src/minidump_cpu.rs new file mode 100644 index 0000000000..6afc94029e --- /dev/null +++ b/third_party/rust/minidump-writer/src/minidump_cpu.rs @@ -0,0 +1,26 @@ +cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + pub type RawContextCPU = minidump_common::format::CONTEXT_AMD64; + pub type FloatStateCPU = minidump_common::format::XMM_SAVE_AREA32; + } else if #[cfg(target_arch = "x86")] { + pub type RawContextCPU = minidump_common::format::CONTEXT_X86; + pub type FloatStateCPU = minidump_common::format::FLOATING_SAVE_AREA_X86; + } else if #[cfg(target_arch = "arm")] { + pub type RawContextCPU = minidump_common::format::CONTEXT_ARM; + pub type FloatStateCPU = minidump_common::format::FLOATING_SAVE_AREA_ARM; + } else if #[cfg(target_arch = "aarch64")] { + /// This is the number of general purpose registers _not_ counting + /// the stack pointer + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) const GP_REG_COUNT: usize = 31; + /// The number of floating point registers in the floating point save area + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) const FP_REG_COUNT: usize = 32; + + pub type RawContextCPU = minidump_common::format::CONTEXT_ARM64_OLD; + } else if #[cfg(target_arch = "mips")] { + compile_error!("flesh me out"); + } else { + compile_error!("unsupported target architecture"); + } +} diff --git a/third_party/rust/minidump-writer/src/minidump_format.rs b/third_party/rust/minidump-writer/src/minidump_format.rs new file mode 100644 index 0000000000..668ac332a9 --- /dev/null +++ b/third_party/rust/minidump-writer/src/minidump_format.rs @@ -0,0 +1,43 @@ +pub use minidump_common::format::{ + self, ArmElfHwCaps as MDCPUInformationARMElfHwCaps, PlatformId, + ProcessorArchitecture as MDCPUArchitecture, GUID, MINIDUMP_DIRECTORY as MDRawDirectory, + MINIDUMP_EXCEPTION as MDException, MINIDUMP_EXCEPTION_STREAM as MDRawExceptionStream, + MINIDUMP_HANDLE_DATA_STREAM as MDRawHandleDataStream, + MINIDUMP_HANDLE_DESCRIPTOR as MDRawHandleDescriptor, MINIDUMP_HEADER as MDRawHeader, + MINIDUMP_LOCATION_DESCRIPTOR as MDLocationDescriptor, + MINIDUMP_MEMORY_DESCRIPTOR as MDMemoryDescriptor, MINIDUMP_MEMORY_INFO as MDMemoryInfo, + MINIDUMP_MEMORY_INFO_LIST as MDMemoryInfoList, MINIDUMP_MODULE as MDRawModule, + MINIDUMP_SIGNATURE as MD_HEADER_SIGNATURE, MINIDUMP_STREAM_TYPE as MDStreamType, + MINIDUMP_SYSTEM_INFO as MDRawSystemInfo, MINIDUMP_THREAD as MDRawThread, + MINIDUMP_THREAD_NAME as MDRawThreadName, MINIDUMP_VERSION as MD_HEADER_VERSION, + VS_FIXEDFILEINFO as MDVSFixedFileInfo, +}; + +/* An MDRVA is an offset into the minidump file. The beginning of the + * MDRawHeader is at offset 0. */ +pub type MDRVA = u32; + +pub type MDRawThreadList = Vec<MDRawThread>; + +cfg_if::cfg_if! { + if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { + pub use format::X86CpuInfo as MDCPUInformation; + } else if #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] { + pub use format::ARMCpuInfo as MDCPUInformation; + } else if #[cfg(target_arch = "mips")] { + pub struct MDCPUInformation { + pub cpuid: [u64; 2], + _padding: [u32; 2], + } + } +} + +cfg_if::cfg_if! { + if #[cfg(target_pointer_width = "64")] { + pub use format::LINK_MAP_64 as MDRawLinkMap; + pub use format::DSO_DEBUG_64 as MDRawDebug; + } else if #[cfg(target_pointer_width = "32")] { + pub use format::LINK_MAP_32 as MDRawLinkMap; + pub use format::DSO_DEBUG_32 as MDRawDebug; + } +} diff --git a/third_party/rust/minidump-writer/src/windows.rs b/third_party/rust/minidump-writer/src/windows.rs new file mode 100644 index 0000000000..34b72444c1 --- /dev/null +++ b/third_party/rust/minidump-writer/src/windows.rs @@ -0,0 +1,5 @@ +pub mod errors; +mod ffi; +pub mod minidump_writer; + +pub use ffi::MinidumpType; diff --git a/third_party/rust/minidump-writer/src/windows/errors.rs b/third_party/rust/minidump-writer/src/windows/errors.rs new file mode 100644 index 0000000000..a2ba6c9b66 --- /dev/null +++ b/third_party/rust/minidump-writer/src/windows/errors.rs @@ -0,0 +1,13 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Scroll(#[from] scroll::Error), + #[error("Failed to open thread")] + ThreadOpen(#[source] std::io::Error), + #[error("Failed to suspend thread")] + ThreadSuspend(#[source] std::io::Error), + #[error("Failed to get thread context")] + ThreadContext(#[source] std::io::Error), +} diff --git a/third_party/rust/minidump-writer/src/windows/ffi.rs b/third_party/rust/minidump-writer/src/windows/ffi.rs new file mode 100644 index 0000000000..933228f8e6 --- /dev/null +++ b/third_party/rust/minidump-writer/src/windows/ffi.rs @@ -0,0 +1,449 @@ +//! Contains bindings for [`MiniDumpWriteDump`](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump) +//! and related structures, as they are not present in `winapi` and we don't want +//! to depend on `windows-sys` due to version churn. +//! +//! Also has a binding for [`GetThreadContext`](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadcontext) +//! as the `CONTEXT` structures in `winapi` are not correctly aligned which can +//! cause crashes or bad data, so the [`crash_context::ffi::CONTEXT`] is used +//! instead. See [#63](https://github.com/rust-minidump/minidump-writer/issues/63) + +#![allow( + non_snake_case, + non_camel_case_types, + non_upper_case_globals, + clippy::upper_case_acronyms +)] + +pub use crash_context::{capture_context, CONTEXT, EXCEPTION_POINTERS, EXCEPTION_RECORD}; + +pub type HANDLE = isize; +pub type BOOL = i32; +pub const FALSE: BOOL = 0; + +pub type Hresult = i32; +pub const STATUS_NONCONTINUABLE_EXCEPTION: i32 = -1073741787; + +pub type PROCESS_ACCESS_RIGHTS = u32; +pub const PROCESS_ALL_ACCESS: PROCESS_ACCESS_RIGHTS = 2097151; + +pub type THREAD_ACCESS_RIGHTS = u32; +pub const THREAD_SUSPEND_RESUME: THREAD_ACCESS_RIGHTS = 2; +pub const THREAD_GET_CONTEXT: THREAD_ACCESS_RIGHTS = 8; +pub const THREAD_QUERY_INFORMATION: THREAD_ACCESS_RIGHTS = 64; + +bitflags::bitflags! { + /// <https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type> + #[derive(Copy, Clone, Debug)] + #[repr(transparent)] + pub struct MinidumpType: u32 { + /// Include just the information necessary to capture stack traces for all + /// existing threads in a process. + const Normal = 0; + /// Include the data sections from all loaded modules. + /// + /// This results in the inclusion of global variables, which can make + /// the minidump file significantly larger. + const WithDataSegs = 1 << 0; + /// Include all accessible memory in the process. + /// + /// The raw memory data is included at the end, so that the initial + /// structures can be mapped directly without the raw memory information. + /// This option can result in a very large file. + const WithFullMemory = 1 << 1; + /// Include high-level information about the operating system handles that + /// are active when the minidump is made. + const WithHandleData = 1 << 2; + /// Stack and backing store memory written to the minidump file should be + /// filtered to remove all but the pointer values necessary to reconstruct a + /// stack trace. + const FilterMemory = 1 << 3; + /// Stack and backing store memory should be scanned for pointer references + /// to modules in the module list. + /// + /// If a module is referenced by stack or backing store memory, the + /// [`MINIDUMP_CALLBACK_OUTPUT_0::ModuleWriteFlags`] field is set to + /// [`ModuleWriteFlags::ModuleReferencedByMemory`]. + const ScanMemory = 1 << 4; + /// Include information from the list of modules that were recently + /// unloaded, if this information is maintained by the operating system. + const WithUnloadedModules = 1 << 5; + /// Include pages with data referenced by locals or other stack memory. + /// This option can increase the size of the minidump file significantly. + const WithIndirectlyReferencedMemory = 1 << 6; + /// Filter module paths for information such as user names or important + /// directories. + /// + /// This option may prevent the system from locating the image file and + /// should be used only in special situations. + const FilterModulePaths = 1 << 7; + /// Include complete per-process and per-thread information from the + /// operating system. + const WithProcessThreadData = 1 << 8; + /// Scan the virtual address space for [`PAGE_READWRITE`](https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants) + /// memory to be included. + const WithPrivateReadWriteMemory = 1 << 9; + /// Reduce the data that is dumped by eliminating memory regions that + /// are not essential to meet criteria specified for the dump. + /// + /// This can avoid dumping memory that may contain data that is private + /// to the user. However, it is not a guarantee that no private information + /// will be present. + const WithoutOptionalData = 1 << 10; + /// Include memory region information. + /// + /// See [MINIDUMP_MEMORY_INFO_LIST](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_info_list) + const WithFullMemoryInfo = 1 << 11; + /// Include thread state information. + /// + /// See [MINIDUMP_THREAD_INFO_LIST](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_info_list) + const WithThreadInfo = 1 << 12; + /// Include all code and code-related sections from loaded modules to + /// capture executable content. + /// + /// For per-module control, use the [`ModuleWriteFlags::ModuleWriteCodeSegs`] + const WithCodeSegs = 1 << 13; + /// Turns off secondary auxiliary-supported memory gathering. + const WithoutAuxiliaryState = 1 << 14; + /// Requests that auxiliary data providers include their state in the + /// dump image; the state data that is included is provider dependent. + /// + /// This option can result in a large dump image. + const WithFullAuxiliaryState = 1 << 15; + /// Scans the virtual address space for [`PAGE_WRITECOPY`](https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants) memory to be included. + const WithPrivateWriteCopyMemory = 1 << 16; + /// If you specify [`MinidumpType::MiniDumpWithFullMemory`], the + /// `MiniDumpWriteDump` function will fail if the function cannot read + /// the memory regions; however, if you include + /// [`IgnoreInaccessibleMemory`], the `MiniDumpWriteDump` function will + /// ignore the memory read failures and continue to generate the dump. + /// + /// Note that the inaccessible memory regions are not included in the dump. + const IgnoreInaccessibleMemory = 1 << 17; + /// Adds security token related data. + /// + /// This will make the "!token" extension work when processing a user-mode dump. + const WithTokenInformation = 1 << 18; + /// Adds module header related data. + const WithModuleHeaders = 1 << 19; + /// Adds filter triage related data. + const FilterTriage = 1 << 20; + /// Adds AVX crash state context registers. + const WithAvxXStateContext = 1 << 21; + /// Adds Intel Processor Trace related data. + const WithIptTrace = 1 << 22; + /// Scans inaccessible partial memory pages. + const ScanInaccessiblePartialPages = 1 << 23; + /// Exclude all memory with the virtual protection attribute of [`PAGE_WRITECOMBINE`](https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants). + const FilterWriteCombinedMemory = 1 << 24; + } +} + +pub type VS_FIXEDFILEINFO_FILE_FLAGS = u32; + +#[repr(C, packed(4))] +pub struct MINIDUMP_USER_STREAM { + pub Type: u32, + pub BufferSize: u32, + pub Buffer: *mut std::ffi::c_void, +} +#[repr(C, packed(4))] +pub struct MINIDUMP_USER_STREAM_INFORMATION { + pub UserStreamCount: u32, + pub UserStreamArray: *mut MINIDUMP_USER_STREAM, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_EXCEPTION_INFORMATION { + pub ThreadId: u32, + pub ExceptionPointers: *mut EXCEPTION_POINTERS, + pub ClientPointers: BOOL, +} + +pub type VS_FIXEDFILEINFO_FILE_OS = i32; +pub type VS_FIXEDFILEINFO_FILE_TYPE = i32; +pub type VS_FIXEDFILEINFO_FILE_SUBTYPE = i32; + +#[repr(C)] +pub struct VS_FIXEDFILEINFO { + pub dwSignature: u32, + pub dwStrucVersion: u32, + pub dwFileVersionMS: u32, + pub dwFileVersionLS: u32, + pub dwProductVersionMS: u32, + pub dwProductVersionLS: u32, + pub dwFileFlagsMask: u32, + pub dwFileFlags: VS_FIXEDFILEINFO_FILE_FLAGS, + pub dwFileOS: VS_FIXEDFILEINFO_FILE_OS, + pub dwFileType: VS_FIXEDFILEINFO_FILE_TYPE, + pub dwFileSubtype: VS_FIXEDFILEINFO_FILE_SUBTYPE, + pub dwFileDateMS: u32, + pub dwFileDateLS: u32, +} +#[repr(C, packed(4))] +pub struct MINIDUMP_MODULE_CALLBACK { + pub FullPath: *mut u16, + pub BaseOfImage: u64, + pub SizeOfImage: u32, + pub CheckSum: u32, + pub TimeDateStamp: u32, + pub VersionInfo: VS_FIXEDFILEINFO, + pub CvRecord: *mut std::ffi::c_void, + pub SizeOfCvRecord: u32, + pub MiscRecord: *mut std::ffi::c_void, + pub SizeOfMiscRecord: u32, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_INCLUDE_THREAD_CALLBACK { + pub ThreadId: u32, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_INCLUDE_MODULE_CALLBACK { + pub BaseOfImage: u64, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_IO_CALLBACK { + pub Handle: HANDLE, + pub Offset: u64, + pub Buffer: *mut std::ffi::c_void, + pub BufferBytes: u32, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_READ_MEMORY_FAILURE_CALLBACK { + pub Offset: u64, + pub Bytes: u32, + pub FailureStatus: Hresult, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_VM_QUERY_CALLBACK { + pub Offset: u64, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_VM_PRE_READ_CALLBACK { + pub Offset: u64, + pub Buffer: *mut std::ffi::c_void, + pub Size: u32, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_VM_POST_READ_CALLBACK { + pub Offset: u64, + pub Buffer: *mut std::ffi::c_void, + pub Size: u32, + pub Completed: u32, + pub Status: Hresult, +} + +/// Oof, so we have a problem with these structs, they are all packed(4), but +/// `CONTEXT` is aligned by either 4 (x86) or 16 (x86_64/aarch64)...which Rust +/// doesn't currently allow https://github.com/rust-lang/rust/issues/59154, so +/// we need to basically cheat with a big byte array until that issue is fixed (possibly never) +#[repr(C)] +pub struct CALLBACK_CONTEXT([u8; std::mem::size_of::<CONTEXT>()]); + +cfg_if::cfg_if! { + if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { + #[repr(C, packed(4))] + pub struct MINIDUMP_THREAD_CALLBACK { + pub ThreadId: u32, + pub ThreadHandle: HANDLE, + pub Context: CALLBACK_CONTEXT, + pub SizeOfContext: u32, + pub StackBase: u64, + pub StackEnd: u64, + } + + #[repr(C, packed(4))] + pub struct MINIDUMP_THREAD_EX_CALLBACK { + pub ThreadId: u32, + pub ThreadHandle: HANDLE, + pub Context: CALLBACK_CONTEXT, + pub SizeOfContext: u32, + pub StackBase: u64, + pub StackEnd: u64, + pub BackingStoreBase: u64, + pub BackingStoreEnd: u64, + } + } else if #[cfg(target_arch = "aarch64")] { + #[repr(C, packed(4))] + pub struct MINIDUMP_THREAD_CALLBACK { + pub ThreadId: u32, + pub ThreadHandle: HANDLE, + pub Pad: u32, + pub Context: CALLBACK_CONTEXT, + pub SizeOfContext: u32, + pub StackBase: u64, + pub StackEnd: u64, + } + + #[repr(C, packed(4))] + pub struct MINIDUMP_THREAD_EX_CALLBACK { + pub ThreadId: u32, + pub ThreadHandle: HANDLE, + pub Pad: u32, + pub Context: CALLBACK_CONTEXT, + pub SizeOfContext: u32, + pub StackBase: u64, + pub StackEnd: u64, + pub BackingStoreBase: u64, + pub BackingStoreEnd: u64, + } + } +} + +#[repr(C)] +pub union MINIDUMP_CALLBACK_INPUT_0 { + pub Status: Hresult, + pub Thread: std::mem::ManuallyDrop<MINIDUMP_THREAD_CALLBACK>, + pub ThreadEx: std::mem::ManuallyDrop<MINIDUMP_THREAD_EX_CALLBACK>, + pub Module: std::mem::ManuallyDrop<MINIDUMP_MODULE_CALLBACK>, + pub IncludeThread: std::mem::ManuallyDrop<MINIDUMP_INCLUDE_THREAD_CALLBACK>, + pub IncludeModule: std::mem::ManuallyDrop<MINIDUMP_INCLUDE_MODULE_CALLBACK>, + pub Io: std::mem::ManuallyDrop<MINIDUMP_IO_CALLBACK>, + pub ReadMemoryFailure: std::mem::ManuallyDrop<MINIDUMP_READ_MEMORY_FAILURE_CALLBACK>, + pub SecondaryFlags: u32, + pub VmQuery: std::mem::ManuallyDrop<MINIDUMP_VM_QUERY_CALLBACK>, + pub VmPreRead: std::mem::ManuallyDrop<MINIDUMP_VM_PRE_READ_CALLBACK>, + pub VmPostRead: std::mem::ManuallyDrop<MINIDUMP_VM_POST_READ_CALLBACK>, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_CALLBACK_INPUT { + pub ProcessId: u32, + pub ProcessHandle: HANDLE, + pub CallbackType: u32, + pub Anonymous: MINIDUMP_CALLBACK_INPUT_0, +} + +pub type VIRTUAL_ALLOCATION_TYPE = u32; + +#[repr(C, packed(4))] +pub struct MINIDUMP_MEMORY_INFO { + pub BaseAddress: u64, + pub AllocationBase: u64, + pub AllocationProtect: u32, + __alignment1: u32, + pub RegionSize: u64, + pub State: VIRTUAL_ALLOCATION_TYPE, + pub Protect: u32, + pub Type: u32, + __alignment2: u32, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_CALLBACK_OUTPUT_0_0 { + pub MemoryBase: u64, + pub MemorySize: u32, +} + +#[repr(C)] +pub struct MINIDUMP_CALLBACK_OUTPUT_0_1 { + pub CheckCancel: BOOL, + pub Cancel: BOOL, +} + +#[repr(C)] +pub struct MINIDUMP_CALLBACK_OUTPUT_0_2 { + pub VmRegion: MINIDUMP_MEMORY_INFO, + pub Continue: BOOL, +} + +#[repr(C)] +pub struct MINIDUMP_CALLBACK_OUTPUT_0_3 { + pub VmQueryStatus: Hresult, + pub VmQueryResult: MINIDUMP_MEMORY_INFO, +} + +#[repr(C)] +pub struct MINIDUMP_CALLBACK_OUTPUT_0_4 { + pub VmReadStatus: Hresult, + pub VmReadBytesCompleted: u32, +} + +bitflags::bitflags! { + /// Identifies the type of module information that will be written to the + /// minidump file by the MiniDumpWriteDump function. + #[derive(Copy, Clone)] + #[repr(transparent)] + pub struct ModuleWriteFlags: u32 { + /// Only module information will be written to the minidump file. + const ModuleWriteModule = 0x0001; + const ModuleWriteDataSeg = 0x0002; + const ModuleWriteMiscRecord = 0x0004; + const ModuleWriteCvRecord = 0x0008; + const ModuleReferencedByMemory = 0x0010; + const ModuleWriteTlsData = 0x0020; + const ModuleWriteCodeSegs = 0x0040; + } +} + +#[repr(C)] +pub union MINIDUMP_CALLBACK_OUTPUT_0 { + pub ModuleWriteFlags: ModuleWriteFlags, + pub ThreadWriteFlags: u32, + pub SecondaryFlags: u32, + pub Anonymous1: std::mem::ManuallyDrop<MINIDUMP_CALLBACK_OUTPUT_0_0>, + pub Anonymous2: std::mem::ManuallyDrop<MINIDUMP_CALLBACK_OUTPUT_0_1>, + pub Handle: HANDLE, + pub Anonymous3: std::mem::ManuallyDrop<MINIDUMP_CALLBACK_OUTPUT_0_2>, + pub Anonymous4: std::mem::ManuallyDrop<MINIDUMP_CALLBACK_OUTPUT_0_3>, + pub Anonymous5: std::mem::ManuallyDrop<MINIDUMP_CALLBACK_OUTPUT_0_4>, + pub Status: Hresult, +} + +#[repr(C, packed(4))] +pub struct MINIDUMP_CALLBACK_OUTPUT { + pub Anonymous: MINIDUMP_CALLBACK_OUTPUT_0, +} + +pub type MINIDUMP_CALLBACK_ROUTINE = Option< + unsafe extern "system" fn( + CallbackParam: *mut std::ffi::c_void, + CallbackInput: *const MINIDUMP_CALLBACK_INPUT, + CallbackOutput: *mut MINIDUMP_CALLBACK_OUTPUT, + ) -> BOOL, +>; + +#[repr(C, packed(4))] +pub struct MINIDUMP_CALLBACK_INFORMATION { + pub CallbackRoutine: MINIDUMP_CALLBACK_ROUTINE, + pub CallbackParam: *mut std::ffi::c_void, +} + +#[link(name = "kernel32")] +extern "system" { + pub fn CloseHandle(handle: HANDLE) -> BOOL; + pub fn GetCurrentProcess() -> HANDLE; + pub fn GetCurrentThreadId() -> u32; + pub fn OpenProcess( + desired_access: PROCESS_ACCESS_RIGHTS, + inherit_handle: BOOL, + process_id: u32, + ) -> HANDLE; + pub fn OpenThread( + desired_access: THREAD_ACCESS_RIGHTS, + inherit_handle: BOOL, + thread_id: u32, + ) -> HANDLE; + pub fn ResumeThread(thread: HANDLE) -> u32; + pub fn SuspendThread(thread: HANDLE) -> u32; + pub fn GetThreadContext(thread: HANDLE, context: *mut CONTEXT) -> BOOL; +} + +#[link(name = "dbghelp")] +extern "system" { + pub fn MiniDumpWriteDump( + process: HANDLE, + process_id: u32, + file: HANDLE, + dump_type: MinidumpType, + exception_param: *const MINIDUMP_EXCEPTION_INFORMATION, + user_stream_param: *const MINIDUMP_USER_STREAM_INFORMATION, + callback_param: *const MINIDUMP_CALLBACK_INFORMATION, + ) -> BOOL; +} diff --git a/third_party/rust/minidump-writer/src/windows/minidump_writer.rs b/third_party/rust/minidump-writer/src/windows/minidump_writer.rs new file mode 100644 index 0000000000..70cc420e57 --- /dev/null +++ b/third_party/rust/minidump-writer/src/windows/minidump_writer.rs @@ -0,0 +1,309 @@ +#![allow(unsafe_code)] + +use crate::windows::errors::Error; +use crate::windows::ffi::{ + capture_context, CloseHandle, GetCurrentProcess, GetCurrentThreadId, GetThreadContext, + MiniDumpWriteDump, MinidumpType, OpenProcess, OpenThread, ResumeThread, SuspendThread, + EXCEPTION_POINTERS, EXCEPTION_RECORD, FALSE, HANDLE, MINIDUMP_EXCEPTION_INFORMATION, + MINIDUMP_USER_STREAM, MINIDUMP_USER_STREAM_INFORMATION, PROCESS_ALL_ACCESS, + STATUS_NONCONTINUABLE_EXCEPTION, THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION, + THREAD_SUSPEND_RESUME, +}; +use minidump_common::format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO, MINIDUMP_STREAM_TYPE}; +use scroll::Pwrite; +use std::os::windows::io::AsRawHandle; + +pub struct MinidumpWriter { + /// Optional exception information + exc_info: Option<MINIDUMP_EXCEPTION_INFORMATION>, + /// Handle to the crashing process, which could be ourselves + crashing_process: HANDLE, + /// The id of the process we are dumping + pid: u32, + /// The id of the 'crashing' thread + tid: u32, + /// The exception code for the dump + #[allow(dead_code)] + exception_code: i32, + /// Whether we are dumping the current process or not + is_external_process: bool, +} + +impl MinidumpWriter { + /// Creates a minidump of the current process, optionally including an + /// exception code and the CPU context of the specified thread. If no thread + /// is specified the current thread CPU context is used. + /// + /// Note that it is inherently unreliable to dump the currently running + /// process, at least in the event of an actual exception. It is recommended + /// to dump from an external process if possible via [`Self::dump_crash_context`] + /// + /// # Errors + /// + /// In addition to the errors described in [`Self::dump_crash_context`], this + /// function can also fail if `thread_id` is specified and we are unable to + /// acquire the thread's context + pub fn dump_local_context( + exception_code: Option<i32>, + thread_id: Option<u32>, + minidump_type: Option<MinidumpType>, + destination: &mut std::fs::File, + ) -> Result<(), Error> { + let exception_code = exception_code.unwrap_or(STATUS_NONCONTINUABLE_EXCEPTION); + + // SAFETY: syscalls, while this encompasses most of the function, the user + // has no invariants to uphold so the entire function is not marked unsafe + unsafe { + let mut exception_context = if let Some(tid) = thread_id { + let mut ec = std::mem::MaybeUninit::uninit(); + + // We need to suspend the thread to get its context, which would be bad + // if it's the current thread, so we check it early before regrets happen + if tid == GetCurrentThreadId() { + capture_context(ec.as_mut_ptr()); + } else { + // We _could_ just fallback to the current thread if we can't get the + // thread handle, but probably better for this to fail with a specific + // error so that the caller can do that themselves if they want to + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openthread + let thread_handle = OpenThread( + THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME, // desired access rights, we only need to get the context, which also requires suspension + FALSE, // inherit handles + tid, // thread id + ); + + if thread_handle == 0 { + return Err(Error::ThreadOpen(std::io::Error::last_os_error())); + } + + struct OwnedHandle(HANDLE); + + impl Drop for OwnedHandle { + fn drop(&mut self) { + // SAFETY: syscall + unsafe { CloseHandle(self.0) }; + } + } + + let thread_handle = OwnedHandle(thread_handle); + + // As noted in the GetThreadContext docs, we have to suspend the thread before we can get its context + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-suspendthread + if SuspendThread(thread_handle.0) == u32::MAX { + return Err(Error::ThreadSuspend(std::io::Error::last_os_error())); + } + + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadcontext + if GetThreadContext(thread_handle.0, ec.as_mut_ptr()) == 0 { + // Try to be a good citizen and resume the thread + ResumeThread(thread_handle.0); + + return Err(Error::ThreadContext(std::io::Error::last_os_error())); + } + + // _presumably_ this will not fail if SuspendThread succeeded, but if it does + // there's really not much we can do about it, thus we don't bother checking the + // return value + ResumeThread(thread_handle.0); + } + + ec.assume_init() + } else { + let mut ec = std::mem::MaybeUninit::uninit(); + capture_context(ec.as_mut_ptr()); + ec.assume_init() + }; + + let mut exception_record: EXCEPTION_RECORD = std::mem::zeroed(); + + let exception_ptrs = EXCEPTION_POINTERS { + ExceptionRecord: &mut exception_record, + ContextRecord: &mut exception_context, + }; + + exception_record.ExceptionCode = exception_code; + + let cc = crash_context::CrashContext { + exception_pointers: (&exception_ptrs as *const EXCEPTION_POINTERS).cast(), + process_id: std::process::id(), + thread_id: thread_id.unwrap_or_else(|| GetCurrentThreadId()), + exception_code, + }; + + Self::dump_crash_context(cc, minidump_type, destination) + } + } + + /// Writes a minidump for the context described by [`crash_context::CrashContext`]. + /// + /// # Errors + /// + /// Fails if the process specified in the context is not the local process + /// and we are unable to open it due to eg. security reasons, or we fail to + /// write the minidump, which can be due to a host of issues with both acquiring + /// the process information as well as writing the actual minidump contents to disk + /// + /// # Safety + /// + /// If [`crash_context::CrashContext::exception_pointers`] is specified, it + /// is the responsibility of the caller to ensure that the pointer is valid + /// for the duration of this function call. + pub fn dump_crash_context( + crash_context: crash_context::CrashContext, + minidump_type: Option<MinidumpType>, + destination: &mut std::fs::File, + ) -> Result<(), Error> { + let pid = crash_context.process_id; + + // SAFETY: syscalls + let (crashing_process, is_external_process) = unsafe { + if pid != std::process::id() { + let proc = OpenProcess( + PROCESS_ALL_ACCESS, // desired access + FALSE, // inherit handles + pid, // pid + ); + + if proc == 0 { + return Err(std::io::Error::last_os_error().into()); + } + + (proc, true) + } else { + (GetCurrentProcess(), false) + } + }; + + let pid = crash_context.process_id; + let tid = crash_context.thread_id; + let exception_code = crash_context.exception_code; + + let exc_info = (!crash_context.exception_pointers.is_null()).then_some( + // https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_exception_information + MINIDUMP_EXCEPTION_INFORMATION { + ThreadId: crash_context.thread_id, + // This is a mut pointer for some reason...I don't _think_ it is + // actually mut in practice...? + ExceptionPointers: crash_context.exception_pointers as *mut _, + /// The `EXCEPTION_POINTERS` contained in crash context is a pointer into the + /// memory of the process that crashed, as it contains an `EXCEPTION_RECORD` + /// record which is an internally linked list, so in the case that we are + /// dumping a process other than the current one, we need to tell + /// `MiniDumpWriteDump` that the pointers come from an external process so that + /// it can use eg `ReadProcessMemory` to get the contextual information from + /// the crash, rather than from the current process + ClientPointers: i32::from(is_external_process), + }, + ); + + let mdw = Self { + exc_info, + crashing_process, + pid, + tid, + exception_code, + is_external_process, + }; + + mdw.dump(minidump_type, destination) + } + + /// Writes a minidump to the specified file + fn dump( + mut self, + minidump_type: Option<MinidumpType>, + destination: &mut std::fs::File, + ) -> Result<(), Error> { + let exc_info = self.exc_info.take(); + + let mut user_streams = Vec::with_capacity(1); + + let mut breakpad_info = self.fill_breakpad_stream(); + + if let Some(bp_info) = &mut breakpad_info { + user_streams.push(MINIDUMP_USER_STREAM { + Type: MINIDUMP_STREAM_TYPE::BreakpadInfoStream as u32, + BufferSize: bp_info.len() as u32, + // Again with the mut pointer + Buffer: bp_info.as_mut_ptr().cast(), + }); + } + + let user_stream_infos = MINIDUMP_USER_STREAM_INFORMATION { + UserStreamCount: user_streams.len() as u32, + UserStreamArray: user_streams.as_mut_ptr(), + }; + + // Write the actual minidump + // https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump + // SAFETY: syscall + let ret = unsafe { + MiniDumpWriteDump( + self.crashing_process, // HANDLE to the process with the crash we want to capture + self.pid, // process id + destination.as_raw_handle() as HANDLE, // file to write the minidump to + minidump_type.unwrap_or(MinidumpType::Normal), + exc_info + .as_ref() + .map_or(std::ptr::null(), |ei| ei as *const _), // exceptionparam - the actual exception information + &user_stream_infos, // user streams + std::ptr::null(), // callback, unused + ) + }; + + if ret == 0 { + Err(std::io::Error::last_os_error().into()) + } else { + Ok(()) + } + } + + /// Create an MDRawBreakpadInfo stream to the minidump, to provide additional + /// information about the exception handler to the Breakpad processor. + /// The information will help the processor determine which threads are + /// relevant. The Breakpad processor does not require this information but + /// can function better with Breakpad-generated dumps when it is present. + /// The native debugger is not harmed by the presence of this information. + /// + /// This info is only relevant for in-process dumping + fn fill_breakpad_stream(&self) -> Option<[u8; 12]> { + if self.is_external_process { + return None; + } + + let mut breakpad_info = [0u8; 12]; + + let bp_info = MINIDUMP_BREAKPAD_INFO { + validity: BreakpadInfoValid::DumpThreadId.bits() + | BreakpadInfoValid::RequestingThreadId.bits(), + dump_thread_id: self.tid, + // SAFETY: syscall + requesting_thread_id: unsafe { GetCurrentThreadId() }, + }; + + // TODO: derive Pwrite for MINIDUMP_BREAKPAD_INFO + // https://github.com/rust-minidump/rust-minidump/pull/534 + let mut offset = 0; + breakpad_info.gwrite(bp_info.validity, &mut offset).ok()?; + breakpad_info + .gwrite(bp_info.dump_thread_id, &mut offset) + .ok()?; + breakpad_info + .gwrite(bp_info.requesting_thread_id, &mut offset) + .ok()?; + + Some(breakpad_info) + } +} + +impl Drop for MinidumpWriter { + fn drop(&mut self) { + // Note we close the handle regardless of whether it is the local handle + // or an external one, as noted in the docs + // + // > The pseudo handle need not be closed when it is no longer needed. + // > Calling the CloseHandle function with a pseudo handle has no effect. + // SAFETY: syscall + unsafe { CloseHandle(self.crashing_process) }; + } +} diff --git a/third_party/rust/minidump-writer/tests/common/mod.rs b/third_party/rust/minidump-writer/tests/common/mod.rs new file mode 100644 index 0000000000..bbb6abf35c --- /dev/null +++ b/third_party/rust/minidump-writer/tests/common/mod.rs @@ -0,0 +1,100 @@ +use std::error; +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; +use std::result; + +#[allow(unused)] +type Error = Box<dyn error::Error + std::marker::Send + std::marker::Sync>; +#[allow(unused)] +pub type Result<T> = result::Result<T, Error>; + +#[allow(unused)] +pub fn spawn_child(command: &str, args: &[&str]) { + let mut cmd_object = Command::new("cargo"); + let mut cmd_ref = cmd_object + .env("RUST_BACKTRACE", "1") + .arg("run") + .arg("-q") + .arg("--bin") + .arg("test") + .arg("--") + .arg(command); + for arg in args { + cmd_ref = cmd_ref.arg(arg); + } + let child = cmd_ref.output().expect("failed to execute child"); + + println!("Child output:"); + std::io::stdout().write_all(&child.stdout).unwrap(); + std::io::stdout().write_all(&child.stderr).unwrap(); + assert_eq!(child.status.code().expect("No return value"), 0); +} + +fn start_child_and_wait_for_threads_helper(cmd: &str, num: usize) -> Child { + let mut child = Command::new("cargo") + .env("RUST_BACKTRACE", "1") + .arg("run") + .arg("-q") + .arg("--bin") + .arg("test") + .arg("--") + .arg(cmd) + .arg(format!("{}", num)) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + + wait_for_threads(&mut child, num); + child +} + +#[allow(unused)] +pub fn start_child_and_wait_for_threads(num: usize) -> Child { + start_child_and_wait_for_threads_helper("spawn_and_wait", num) +} + +#[allow(unused)] +pub fn start_child_and_wait_for_named_threads(num: usize) -> Child { + start_child_and_wait_for_threads_helper("spawn_name_wait", num) +} + +#[allow(unused)] +pub fn start_child_and_wait_for_create_files(num: usize) -> Child { + start_child_and_wait_for_threads_helper("create_files_wait", num) +} + +#[allow(unused)] +pub fn wait_for_threads(child: &mut Child, num: usize) { + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut lines = 0; + while lines < num { + let mut buf = String::new(); + match f.read_line(&mut buf) { + Ok(_) => { + if buf == "1\n" { + lines += 1; + } + } + Err(e) => { + std::panic::panic_any(e); + } + } + } +} + +#[allow(unused)] +pub fn start_child_and_return(args: &[&str]) -> Child { + let mut child = Command::new("cargo") + .env("RUST_BACKTRACE", "1") + .arg("run") + .arg("-q") + .arg("--bin") + .arg("test") + .arg("--") + .args(args) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + + child +} diff --git a/third_party/rust/minidump-writer/tests/linux_minidump_writer.rs b/third_party/rust/minidump-writer/tests/linux_minidump_writer.rs new file mode 100644 index 0000000000..d9864bae13 --- /dev/null +++ b/third_party/rust/minidump-writer/tests/linux_minidump_writer.rs @@ -0,0 +1,773 @@ +#![cfg(any(target_os = "linux", target_os = "android"))] +#![allow(unused_imports, unused_variables)] + +use minidump::*; +use minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*}; +use minidump_writer::{ + app_memory::AppMemory, + crash_context::CrashContext, + errors::*, + maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, + minidump_writer::MinidumpWriter, + ptrace_dumper::PtraceDumper, + thread_info::Pid, +}; +use nix::{errno::Errno, sys::signal::Signal}; +use procfs_core::process::MMPermissions; +use std::collections::HashSet; + +use std::{ + io::{BufRead, BufReader}, + os::unix::process::ExitStatusExt, + process::{Command, Stdio}, +}; + +mod common; +use common::*; + +#[derive(Debug, PartialEq)] +enum Context { + With, + Without, +} + +impl Context { + pub fn minidump_writer(&self, pid: Pid) -> MinidumpWriter { + let mut mw = MinidumpWriter::new(pid, pid); + #[cfg(not(target_arch = "mips"))] + if self == &Context::With { + let crash_context = get_crash_context(pid); + mw.set_crash_context(crash_context); + } + mw + } +} + +#[cfg(not(target_arch = "mips"))] +fn get_ucontext() -> Result<crash_context::ucontext_t> { + let mut context = std::mem::MaybeUninit::uninit(); + unsafe { + let res = crash_context::crash_context_getcontext(context.as_mut_ptr()); + Errno::result(res)?; + + Ok(context.assume_init()) + } +} + +#[cfg(not(target_arch = "mips"))] +fn get_crash_context(tid: Pid) -> CrashContext { + let siginfo: libc::signalfd_siginfo = unsafe { std::mem::zeroed() }; + let context = get_ucontext().expect("Failed to get ucontext"); + #[cfg(not(target_arch = "arm"))] + let float_state = unsafe { std::mem::zeroed() }; + CrashContext { + inner: crash_context::CrashContext { + siginfo, + pid: std::process::id() as _, + tid, + context, + #[cfg(not(target_arch = "arm"))] + float_state, + }, + } +} + +macro_rules! contextual_tests { + () => {}; + ( fn $name:ident ($ctx:ident : Context) $body:block $($rest:tt)* ) => { + mod $name { + use super::*; + + fn test($ctx: Context) $body + + #[test] + fn run() { + test(Context::Without) + } + + #[cfg(not(target_arch = "mips"))] + #[test] + fn run_with_context() { + test(Context::With) + } + } + contextual_tests! { $($rest)* } + } +} + +contextual_tests! { + fn test_write_dump(context: Context) { + let num_of_threads = 3; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump") + .tempfile() + .unwrap(); + + let mut tmp = context.minidump_writer(pid); + let in_memory_buffer = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + + let mem_slice = std::fs::read(tmpfile.path()).expect("Failed to minidump"); + assert_eq!(mem_slice.len(), in_memory_buffer.len()); + assert_eq!(mem_slice, in_memory_buffer); + } + + fn test_write_and_read_dump_from_parent(context: Context) { + let mut child = start_child_and_return(&["spawn_mmap_wait"]); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_and_read_dump") + .tempfile() + .unwrap(); + + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + let mmap_addr = output + .next() + .unwrap() + .parse() + .expect("unable to parse mmap_addr"); + let memory_size = output + .next() + .unwrap() + .parse() + .expect("unable to parse memory_size"); + // Add information about the mapped memory. + let mapping = MappingInfo { + start_address: mmap_addr, + size: memory_size, + offset: 0, + permissions: MMPermissions::READ | MMPermissions::WRITE, + name: Some("a fake mapping".into()), + system_mapping_info: SystemMappingInfo { + start_address: mmap_addr, + end_address: mmap_addr + memory_size, + }, + }; + + let identifier = vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, + ]; + let entry = MappingEntry { + mapping, + identifier, + }; + + let mut tmp = context.minidump_writer(pid); + + tmp.set_user_mapping_list(vec![entry]) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + child.kill().expect("Failed to kill process"); + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let module_list: MinidumpModuleList = dump + .get_stream() + .expect("Couldn't find stream MinidumpModuleList"); + let module = module_list + .module_at_address(mmap_addr as u64) + .expect("Couldn't find user mapping module"); + assert_eq!(module.base_address(), mmap_addr as u64); + assert_eq!(module.size(), memory_size as u64); + assert_eq!(module.code_file(), "a fake mapping"); + assert_eq!( + module.debug_identifier(), + Some("33221100554477668899AABBCCDDEEFF0".parse().unwrap()) + ); + + let _: MinidumpException = dump.get_stream().expect("Couldn't find MinidumpException"); + let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = dump.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = dump.get_stream().expect("Couldn't find MinidumpSystemInfo"); + let _ = dump + .get_raw_stream(LinuxCpuInfo as u32) + .expect("Couldn't find LinuxCpuInfo"); + let _ = dump + .get_raw_stream(LinuxProcStatus as u32) + .expect("Couldn't find LinuxProcStatus"); + let _ = dump + .get_raw_stream(LinuxCmdLine as u32) + .expect("Couldn't find LinuxCmdLine"); + let _ = dump + .get_raw_stream(LinuxEnviron as u32) + .expect("Couldn't find LinuxEnviron"); + let _ = dump + .get_raw_stream(LinuxAuxv as u32) + .expect("Couldn't find LinuxAuxv"); + let _ = dump + .get_raw_stream(LinuxMaps as u32) + .expect("Couldn't find LinuxMaps"); + let _ = dump + .get_raw_stream(LinuxDsoDebug as u32) + .expect("Couldn't find LinuxDsoDebug"); + let _ = dump + .get_raw_stream(MozLinuxLimits as u32) + .expect("Couldn't find MozLinuxLimits"); + } + + fn test_write_with_additional_memory(context: Context) { + let mut child = start_child_and_return(&["spawn_alloc_wait"]); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("additional_memory") + .tempfile() + .unwrap(); + + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + let memory_addr = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) + .expect("unable to parse mmap_addr"); + let memory_size = output + .next() + .unwrap() + .parse() + .expect("unable to parse memory_size"); + + let app_memory = AppMemory { + ptr: memory_addr, + length: memory_size, + }; + + let mut tmp = context.minidump_writer(pid); + + tmp.set_app_memory(vec![app_memory]) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + child.kill().expect("Failed to kill process"); + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + + let section: MinidumpMemoryList = dump.get_stream().expect("Couldn't find MinidumpMemoryList"); + let region = section + .memory_at_address(memory_addr as u64) + .expect("Couldn't find memory region"); + + assert_eq!(region.base_address, memory_addr as u64); + assert_eq!(region.size, memory_size as u64); + + let mut values = Vec::<u8>::with_capacity(memory_size); + for idx in 0..memory_size { + values.push((idx % 255) as u8); + } + + // Verify memory contents. + assert_eq!(region.bytes, values); + } + + fn test_skip_if_requested(context: Context) { + let num_of_threads = 1; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("skip_if_requested") + .tempfile() + .unwrap(); + + let mut tmp = context.minidump_writer(pid); + + let pr_mapping_addr; + #[cfg(target_pointer_width = "64")] + { + pr_mapping_addr = 0x0102030405060708; + } + #[cfg(target_pointer_width = "32")] + { + pr_mapping_addr = 0x010203040; + }; + let res = tmp + .skip_stacks_if_mapping_unreferenced() + .set_principal_mapping_address(pr_mapping_addr) + .dump(&mut tmpfile); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + assert!(res.is_err()); + } + + fn test_sanitized_stacks(context: Context) { + if context == Context::With { + // FIXME the context's stack pointer very often doesn't lie in mapped memory, resulting + // in the stack memory having 0 size (so no slice will match `defaced` in the + // assertion). + return; + } + let num_of_threads = 1; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("sanitized_stacks") + .tempfile() + .unwrap(); + + let mut tmp = context.minidump_writer(pid); + tmp.sanitize_stack() + .dump(&mut tmpfile) + .expect("Faild to dump minidump"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let dump_array = std::fs::read(tmpfile.path()).expect("Failed to read minidump as vec"); + let thread_list: MinidumpThreadList = + dump.get_stream().expect("Couldn't find MinidumpThreadList"); + + let defaced; + #[cfg(target_pointer_width = "64")] + { + defaced = 0x0defaced0defacedusize.to_ne_bytes(); + } + #[cfg(target_pointer_width = "32")] + { + defaced = 0x0defacedusize.to_ne_bytes() + }; + + for thread in thread_list.threads { + let mem = thread.raw.stack.memory; + let start = mem.rva as usize; + let end = (mem.rva + mem.data_size) as usize; + let slice = &dump_array.as_slice()[start..end]; + assert!(slice.windows(defaced.len()).any(|window| window == defaced)); + } + } + + fn test_write_early_abort(context: Context) { + let mut child = start_child_and_return(&["spawn_alloc_wait"]); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("additional_memory") + .tempfile() + .unwrap(); + + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + // We do not read the actual memory_address, but use NULL, which + // should create an error during dumping and lead to a truncated minidump. + let _ = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) + .expect("unable to parse mmap_addr"); + let memory_addr = 0; + let memory_size = output + .next() + .unwrap() + .parse() + .expect("unable to parse memory_size"); + + let app_memory = AppMemory { + ptr: memory_addr, + length: memory_size, + }; + + let mut tmp = context.minidump_writer(pid); + + // This should fail, because during the dump an error is detected (try_from fails) + match tmp.set_app_memory(vec![app_memory]).dump(&mut tmpfile) { + Err(WriterError::SectionAppMemoryError(_)) => (), + _ => panic!("Wrong kind of error returned"), + } + + child.kill().expect("Failed to kill process"); + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents. There should be a truncated minidump available + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + // Should be there + let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpModuleList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + + // Should be missing: + assert!(dump.get_stream::<MinidumpMemoryList>().is_err()); + } + + fn test_named_threads(context: Context) { + let num_of_threads = 5; + let mut child = start_child_and_wait_for_named_threads(num_of_threads); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("named_threads") + .tempfile() + .unwrap(); + + let mut tmp = context.minidump_writer(pid); + let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents. There should be a truncated minidump available + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + + let threads: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + + let thread_names: MinidumpThreadNames = dump + .get_stream() + .expect("Couldn't find MinidumpThreadNames"); + + let thread_ids: Vec<_> = threads.threads.iter().map(|t| t.raw.thread_id).collect(); + let names: HashSet<_> = thread_ids + .iter() + .map(|id| thread_names.get_name(*id).unwrap_or_default()) + .map(|cow| cow.into_owned()) + .collect(); + let mut expected = HashSet::new(); + expected.insert("test".to_string()); + for id in 1..num_of_threads { + expected.insert(format!("thread_{}", id)); + } + assert_eq!(expected, names); + + } + + fn test_file_descriptors(context: Context) { + let num_of_files = 5; + let mut child = start_child_and_wait_for_create_files(num_of_files); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("testfiles") + .tempfile() + .unwrap(); + + let mut tmp = context.minidump_writer(pid); + let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents. There should be a truncated minidump available + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let fds: MinidumpHandleDataStream = dump.get_stream().expect("Couldn't find MinidumpHandleDataStream"); + // We check that we create num_of_files plus stdin, stdout and stderr + for i in 0..2 { + let descriptor = fds.handles.get(i).expect("Descriptor should be present"); + let fd = *descriptor.raw.handle().expect("Handle should be populated"); + assert_eq!(fd, i as u64); + } + + for i in 3..num_of_files { + let descriptor = fds.handles.get(i).expect("Descriptor should be present"); + let object_name = descriptor.object_name.as_ref().expect("The path should be populated"); + let file_name = object_name.split('/').last().expect("The filename should be present"); + assert!(file_name.starts_with("test_file")); + assert!(file_name.ends_with(&(i - 3).to_string())); + } + } +} + +#[test] +fn test_minidump_size_limit() { + let num_of_threads = 40; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + + let mut total_normal_stack_size = 0; + let normal_file_size; + // First, write a minidump with no size limit. + { + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump_unlimited") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + + normal_file_size = meta.len(); + + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let thread_list: MinidumpThreadList = + dump.get_stream().expect("Couldn't find MinidumpThreadList"); + for thread in thread_list.threads { + assert!(thread.raw.thread_id > 0); + assert!(thread.raw.stack.memory.data_size > 0); + total_normal_stack_size += thread.raw.stack.memory.data_size; + } + } + + // Second, write a minidump with a size limit big enough to not trigger + // anything. + { + // Set size limit arbitrarily 2MiB larger than the normal file size -- such + // that the limiting code will not kick in. + let minidump_size_limit = normal_file_size + 2 * 1024 * 1024; + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump_pseudolimited") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .set_minidump_size_limit(minidump_size_limit) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + + // Make sure limiting wasn't actually triggered. NOTE: If you fail this, + // first make sure that "minidump_size_limit" above is indeed set to a + // large enough value -- the limit-checking code in minidump_writer.rs + // does just a rough estimate. + // TODO: Fix this properly + //assert_eq!(meta.len(), normal_file_size); + let min = std::cmp::min(meta.len(), normal_file_size); + let max = std::cmp::max(meta.len(), normal_file_size); + + // Setting a stack limit limits the size of non-main stacks even before + // the limit is reached. This will cause slight variations in size + // between a limited and an unlimited minidump. + assert!(max - min < 1024, "max = {max:} min = {min:}"); + } + + // Third, write a minidump with a size limit small enough to be triggered. + { + // Set size limit to some arbitrary amount, such that the limiting code + // will kick in. The equation used to set this value was determined by + // simply reversing the size-limit logic a little bit in order to pick a + // size we know will trigger it. + + // Copyied from sections/thread_list_stream.rs + const LIMIT_AVERAGE_THREAD_STACK_LENGTH: u64 = 8 * 1024; + let mut minidump_size_limit = LIMIT_AVERAGE_THREAD_STACK_LENGTH * 40; + + // If, in reality, each of the threads' stack is *smaller* than + // kLimitAverageThreadStackLength, the normal file size could very well be + // smaller than the arbitrary limit that was just set. In that case, + // either of these numbers should trigger the size-limiting code, but we + // might as well pick the smallest. + if normal_file_size < minidump_size_limit { + minidump_size_limit = normal_file_size; + } + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump_limited") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .set_minidump_size_limit(minidump_size_limit) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + // Make sure the file size is at least smaller than the original. If this + // fails because it's the same size, then the size-limit logic didn't kick + // in like it was supposed to. + assert!(meta.len() < normal_file_size); + + let mut total_limit_stack_size = 0; + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let thread_list: MinidumpThreadList = + dump.get_stream().expect("Couldn't find MinidumpThreadList"); + for thread in thread_list.threads { + assert!(thread.raw.thread_id > 0); + assert!(thread.raw.stack.memory.data_size > 0); + total_limit_stack_size += thread.raw.stack.memory.data_size; + } + + // Make sure stack size shrunk by at least 1KB per extra thread. + // Note: The 1KB is arbitrary, and assumes that the thread stacks are big + // enough to shrink by that much. For example, if each thread stack was + // originally only 2KB, the current size-limit logic wouldn't actually + // shrink them because that's the size to which it tries to shrink. If + // you fail this part of the test due to something like that, the test + // logic should probably be improved to account for your situation. + + // Copyied from sections/thread_list_stream.rs + const LIMIT_BASE_THREAD_COUNT: usize = 20; + const MIN_PER_EXTRA_THREAD_STACK_REDUCTION: usize = 1024; + let min_expected_reduction = + (40 - LIMIT_BASE_THREAD_COUNT) * MIN_PER_EXTRA_THREAD_STACK_REDUCTION; + assert!(total_limit_stack_size < total_normal_stack_size - min_expected_reduction as u32); + } + + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); +} + +#[test] +fn test_with_deleted_binary() { + let num_of_threads = 1; + let binary_copy_dir = tempfile::Builder::new() + .prefix("deleted_binary") + .tempdir() + .unwrap(); + let binary_copy = binary_copy_dir.as_ref().join("binary_copy"); + + let path: &'static str = std::env!("CARGO_BIN_EXE_test"); + + std::fs::copy(path, &binary_copy).expect("Failed to copy binary"); + let mem_slice = std::fs::read(&binary_copy).expect("Failed to read binary"); + + let mut child = Command::new(&binary_copy) + .env("RUST_BACKTRACE", "1") + .arg("spawn_and_wait") + .arg(format!("{}", num_of_threads)) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + wait_for_threads(&mut child, num_of_threads); + + let pid = child.id() as i32; + + let build_id = PtraceDumper::elf_file_identifier_from_mapped_file(&mem_slice) + .expect("Failed to get build_id"); + + let guid = GUID { + data1: u32::from_ne_bytes(build_id[0..4].try_into().unwrap()), + data2: u16::from_ne_bytes(build_id[4..6].try_into().unwrap()), + data3: u16::from_ne_bytes(build_id[6..8].try_into().unwrap()), + data4: build_id[8..16].try_into().unwrap(), + }; + + // guid_to_string() is not public in minidump, so copied it here + // And append a zero, because module IDs include an "age" field + // which is always zero on Linux. + let filtered = format!( + "{:08X}{:04X}{:04X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}0", + guid.data1, + guid.data2, + guid.data3, + guid.data4[0], + guid.data4[1], + guid.data4[2], + guid.data4[3], + guid.data4[4], + guid.data4[5], + guid.data4[6], + guid.data4[7], + ); + // Strip out dashes + //let mut filtered: String = identifier.chars().filter(|x| *x != '-').collect(); + + std::fs::remove_file(&binary_copy).expect("Failed to remove binary"); + + let mut tmpfile = tempfile::Builder::new() + .prefix("deleted_binary") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Begin checks on dump + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let module_list: MinidumpModuleList = dump + .get_stream() + .expect("Couldn't find stream MinidumpModuleList"); + let main_module = module_list + .main_module() + .expect("Could not get main module"); + assert_eq!(main_module.code_file(), binary_copy.to_string_lossy()); + assert_eq!(main_module.debug_identifier(), filtered.parse().ok()); +} + +#[test] +fn test_memory_info_list_stream() { + let mut child = start_child_and_wait_for_threads(1); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("memory_info_list_stream") + .tempfile() + .unwrap(); + + // Write a minidump + MinidumpWriter::new(pid, pid) + .dump(&mut tmpfile) + .expect("cound not write minidump"); + child.kill().expect("Failed to kill process"); + + // Ensure the minidump has a MemoryInfoListStream present and has at least one entry. + let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + let list: MinidumpMemoryInfoList = dump.get_stream().expect("no memory info list"); + assert!(list.iter().count() > 1); +} diff --git a/third_party/rust/minidump-writer/tests/mac_minidump_writer.rs b/third_party/rust/minidump-writer/tests/mac_minidump_writer.rs new file mode 100644 index 0000000000..b2474b480d --- /dev/null +++ b/third_party/rust/minidump-writer/tests/mac_minidump_writer.rs @@ -0,0 +1,226 @@ +#![cfg(target_os = "macos")] + +mod common; +use common::start_child_and_return; + +use minidump::{ + CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpMiscInfo, + MinidumpModuleList, MinidumpSystemInfo, MinidumpThreadList, +}; +use minidump_writer::minidump_writer::MinidumpWriter; + +fn get_crash_reason<'a, T: std::ops::Deref<Target = [u8]> + 'a>( + md: &Minidump<'a, T>, +) -> CrashReason { + let exc: minidump::MinidumpException<'_> = + md.get_stream().expect("unable to find exception stream"); + + exc.get_crash_reason( + minidump::system_info::Os::MacOs, + if cfg!(target_arch = "x86_64") { + minidump::system_info::Cpu::X86_64 + } else if cfg!(target_arch = "aarch64") { + minidump::system_info::Cpu::Arm64 + } else { + unimplemented!() + }, + ) +} + +struct Captured<'md> { + #[allow(dead_code)] + task: u32, + thread: u32, + minidump: Minidump<'md, memmap2::Mmap>, +} + +fn capture_minidump(name: &str, exception_kind: u32) -> Captured<'_> { + // Create a mach port server to retrieve the crash details from the child + let mut server = crash_context::ipc::Server::create(&std::ffi::CString::new(name).unwrap()) + .expect("failed to create mach port service"); + + let mut child = start_child_and_return(&[name, &exception_kind.to_string()]); + + // Wait for the child to spinup and report a crash context to us + let mut rcc = server + .try_recv_crash_context(Some(std::time::Duration::from_secs(5))) + .expect("failed to receive context") + .expect("receive timed out"); + + let mut tmpfile = tempfile::Builder::new().prefix(name).tempfile().unwrap(); + + let task = rcc.crash_context.task; + let thread = rcc.crash_context.thread; + + let mut dumper = MinidumpWriter::with_crash_context(rcc.crash_context); + + dumper + .dump(tmpfile.as_file_mut()) + .expect("failed to write minidump"); + + // Signal the child that we've received and processed the crash context + rcc.acker + .send_ack(1, Some(std::time::Duration::from_secs(2))) + .expect("failed to send ack"); + + child.kill().expect("failed to kill child"); + + let minidump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + Captured { + task, + thread, + minidump, + } +} + +#[test] +fn dump_external_process() { + if std::env::var("CI").is_ok() { + println!("test disabled, consistently times out because of potato runners"); + return; + } + + let approximate_proc_start_time = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let md = capture_minidump( + "dump_external_process", + mach2::exception_types::EXC_BREAKPOINT, + ) + .minidump; + + let crash_reason = get_crash_reason(&md); + + assert!(matches!( + crash_reason, + CrashReason::MacGeneral(minidump_common::errors::ExceptionCodeMac::EXC_BREAKPOINT, _) + )); + + let _: MinidumpModuleList = md.get_stream().expect("Couldn't find MinidumpModuleList"); + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + let _: MinidumpBreakpadInfo = md.get_stream().expect("Couldn't find MinidumpBreakpadInfo"); + + let misc_info: MinidumpMiscInfo = md.get_stream().expect("Couldn't find MinidumpMiscInfo"); + + if let minidump::RawMiscInfo::MiscInfo2(mi) = &misc_info.raw { + // Unfortunately the minidump format only has 32-bit precision for the + // process start time + let process_create_time = mi.process_create_time as u64; + + assert!( + process_create_time >= approximate_proc_start_time + && process_create_time <= approximate_proc_start_time + 2 + ); + + // I've tried busy looping to spend CPU time to get this up, but + // MACH_TASK_BASIC_INFO which should give terminated thread times only ever + // reports 0, and TASK_THREAD_TIMES_INFO which should show active thread + // times I've only been able to get upt to a few thousand microseconds + // even when busy looping for well over a second, and those get truncated + // to whole seconds. And it seems that crashpad doesn't have tests around + // this, though that's hard to say given how tedious it is finding stuff + // in that bloated codebase + // assert!(mi.process_user_time > 0); + // assert!(mi.process_kernel_time > 0); + + // These aren't currently available on aarch64, or if they are, they + // are not via the same sysctlbyname mechanism. Would be nice if Apple + // documented...anything + if cfg!(target_arch = "x86_64") { + assert!(mi.processor_max_mhz > 0); + assert!(mi.processor_current_mhz > 0); + } + } else { + panic!("unexpected misc info type {:?}", misc_info); + } +} + +/// Validates we can actually walk the stack for each thread in the minidump, +/// this is using minidump-processor, which (currently) depends on breakpad +/// symbols, however https://github.com/mozilla/dump_syms is not available as +/// a library https://github.com/mozilla/dump_syms/issues/253, so we just require +/// that it already be installed, hence the ignore +#[test] +fn stackwalks() { + if std::env::var("CI").is_ok() { + println!("test disabled, consistently times out because of potato runners"); + return; + } + + println!("generating minidump..."); + let md = capture_minidump("stackwalks", mach2::exception_types::EXC_BREAKPOINT); + + // Generate the breakpad symbols + println!("generating symbols..."); + dump_syms::dumper::single_file( + &dump_syms::dumper::Config { + output: dump_syms::dumper::Output::Store(".test-symbols".into()), + symbol_server: None, + debug_id: None, + code_id: None, + arch: if cfg!(target_arch = "aarch64") { + "arm64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + panic!("invalid MacOS target architecture") + }, + num_jobs: 2, // default this + check_cfi: false, + emit_inlines: false, + mapping_var: None, + mapping_src: None, + mapping_dest: None, + mapping_file: None, + }, + "target/debug/test", + ) + .expect("failed to dump symbols"); + + let provider = minidump_unwind::Symbolizer::new(minidump_unwind::simple_symbol_supplier(vec![ + ".test-symbols".into(), + ])); + + let state = futures::executor::block_on(async { + minidump_processor::process_minidump(&md.minidump, &provider).await + }) + .unwrap(); + + //state.print(&mut std::io::stdout()).map_err(|_| ()).unwrap(); + + // We expect at least 2 threads, one of which is the fake crashing thread + let fake_crash_thread = state + .threads + .iter() + .find(|cs| cs.thread_id == md.thread) + .expect("failed to find crash thread"); + + assert_eq!( + fake_crash_thread.thread_name.as_deref(), + Some("test-thread") + ); + + assert!( + fake_crash_thread.frames.iter().any(|sf| { + sf.function_name + .as_ref() + .map_or(false, |fname| fname.ends_with("wait_until_killed")) + }), + "unable to locate expected function" + ); + + let mod_list: MinidumpModuleList = md + .minidump + .get_stream() + .expect("Couldn't find MinidumpModuleList"); + + // Ensure we found dyld + assert!(mod_list + .iter() + .any(|module| &module.name == "/usr/lib/dyld")); +} diff --git a/third_party/rust/minidump-writer/tests/ptrace_dumper.rs b/third_party/rust/minidump-writer/tests/ptrace_dumper.rs new file mode 100644 index 0000000000..1be27f0809 --- /dev/null +++ b/third_party/rust/minidump-writer/tests/ptrace_dumper.rs @@ -0,0 +1,317 @@ +//! All of these tests are specific to ptrace +#![cfg(any(target_os = "linux", target_os = "android"))] + +use minidump_writer::ptrace_dumper::PtraceDumper; +use nix::sys::mman::{mmap, MapFlags, ProtFlags}; +use nix::sys::signal::Signal; +use std::convert::TryInto; +use std::io::{BufRead, BufReader}; +use std::mem::size_of; +use std::os::unix::io::AsFd; +use std::os::unix::process::ExitStatusExt; + +mod common; +use common::*; + +#[test] +fn test_setup() { + spawn_child("setup", &[]); +} + +#[test] +fn test_thread_list_from_child() { + // Child spawns and looks in the parent (== this process) for its own thread-ID + spawn_child("thread_list", &[]); +} + +#[test] +fn test_thread_list_from_parent() { + let num_of_threads = 5; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + let mut dumper = PtraceDumper::new(pid).expect("Couldn't init dumper"); + assert_eq!(dumper.threads.len(), num_of_threads); + dumper.suspend_threads().expect("Could not suspend threads"); + + // let mut matching_threads = 0; + for (idx, curr_thread) in dumper.threads.iter().enumerate() { + println!("curr_thread: {:?}", curr_thread); + let info = dumper + .get_thread_info_by_index(idx) + .expect("Could not get thread info by index"); + let (_valid_stack_ptr, stack_len) = dumper + .get_stack_info(info.stack_pointer) + .expect("Could not get stack_pointer"); + + assert!(stack_len > 0); + + // TODO: I currently know of no way to write the thread_id into the registers using Rust, + // so this check is deactivated for now, because it always fails + /* + // In the helper program, we stored a pointer to the thread id in a + // specific register. Check that we can recover its value. + #[cfg(target_arch = "x86_64")] + let process_tid_location = info.regs.rcx; + #[cfg(target_arch = "x86")] + let process_tid_location = info.regs.ecx; + #[cfg(target_arch = "arm")] + let process_tid_location = info.regs.uregs[3]; + #[cfg(target_arch = "aarch64")] + let process_tid_location = info.regs.regs[3]; + #[cfg(target_arch = "mips")] + let process_tid_location = info.mcontext.gregs[1]; + + let thread_id_data = PtraceDumper::copy_from_process( + *curr_thread, + process_tid_location as *mut libc::c_void, + 4, + ) + .expect("Could not copy from process"); + let found_thread_id = i32::from_ne_bytes( + thread_id_data + .as_slice() + .try_into() + .expect("couldn't parse i32 from read data"), + ); + matching_threads += if *curr_thread == found_thread_id { + 1 + } else { + 0 + }; */ + } + dumper.resume_threads().expect("Failed to resume threads"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // We clean up the child process before checking the final result + // TODO: I currently know of no way to write the thread_id into the registers using Rust, + // so this check is deactivated for now, because it always fails + // assert_eq!(matching_threads, num_of_threads); +} + +// #[cfg(not(any(target_arch = "mips", target_arch = "arm-eabi"))] +#[cfg(not(target_arch = "mips"))] +#[test] +// Ensure that the linux-gate VDSO is included in the mapping list. +fn test_mappings_include_linux_gate() { + spawn_child("mappings_include_linux_gate", &[]); +} + +#[test] +fn test_linux_gate_mapping_id() { + if std::env::var("CI").is_ok() { + println!("disabled on CI, but works locally"); + return; + } + + spawn_child("linux_gate_mapping_id", &[]); +} + +#[test] +fn test_merged_mappings() { + let page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE).unwrap(); + let page_size = std::num::NonZeroUsize::new(page_size.unwrap() as usize).unwrap(); + let map_size = std::num::NonZeroUsize::new(3 * page_size.get()).unwrap(); + + let path: &'static str = std::env!("CARGO_BIN_EXE_test"); + let file = std::fs::File::open(path).unwrap(); + + // mmap two segments out of the helper binary, one + // enclosed in the other, but with different protections. + let mapped_mem = unsafe { + mmap( + None, + map_size, + ProtFlags::PROT_READ, + MapFlags::MAP_SHARED, + Some(file.as_fd()), + 0, + ) + .unwrap() + }; + + // Carve a page out of the first mapping with different permissions. + let _inside_mapping = unsafe { + mmap( + std::num::NonZeroUsize::new(mapped_mem as usize + 2 * page_size.get()), + page_size, + ProtFlags::PROT_NONE, + MapFlags::MAP_SHARED | MapFlags::MAP_FIXED, + Some(file.as_fd()), + // Map a different offset just to + // better test real-world conditions. + page_size.get().try_into().unwrap(), // try_into() in order to work for 32 and 64 bit + ) + }; + + spawn_child( + "merged_mappings", + &[ + path, + &format!("{}", mapped_mem as usize), + &format!("{map_size}"), + ], + ); +} + +#[test] +// Ensure that the linux-gate VDSO is included in the mapping list. +fn test_file_id() { + spawn_child("file_id", &[]); +} + +#[test] +fn test_find_mapping() { + spawn_child( + "find_mappings", + &[ + &format!("{}", libc::printf as *const () as usize), + &format!("{}", String::new as *const () as usize), + ], + ); +} + +#[test] +fn test_copy_from_process_self() { + if std::env::var("CI").is_ok() { + println!("disabled on CI, but works locally"); + return; + } + + let stack_var: libc::c_long = 0x11223344; + let heap_var: Box<libc::c_long> = Box::new(0x55667788); + spawn_child( + "copy_from_process", + &[ + &format!("{}", &stack_var as *const libc::c_long as usize), + &format!("{}", heap_var.as_ref() as *const libc::c_long as usize), + ], + ); +} + +#[test] +fn test_sanitize_stack_copy() { + let num_of_threads = 1; + let mut child = start_child_and_return(&["spawn_alloc_wait"]); + let pid = child.id() as i32; + + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + let heap_addr = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) + .expect("unable to parse mmap_addr"); + + let mut dumper = PtraceDumper::new(pid).expect("Couldn't init dumper"); + assert_eq!(dumper.threads.len(), num_of_threads); + dumper.suspend_threads().expect("Could not suspend threads"); + let thread_info = dumper + .get_thread_info_by_index(0) + .expect("Couldn't find thread_info"); + + let defaced; + #[cfg(target_pointer_width = "64")] + { + defaced = 0x0defaced0defacedusize.to_ne_bytes() + } + #[cfg(target_pointer_width = "32")] + { + defaced = 0x0defacedusize.to_ne_bytes() + }; + + let mut simulated_stack = vec![0xffu8; 2 * size_of::<usize>()]; + // Pointers into the stack shouldn't be sanitized. + simulated_stack[size_of::<usize>()..].copy_from_slice(&thread_info.stack_pointer.to_ne_bytes()); + + dumper + .sanitize_stack_copy( + &mut simulated_stack, + thread_info.stack_pointer, + size_of::<usize>(), + ) + .expect("Could not sanitize stack"); + + assert!(simulated_stack[size_of::<usize>()..] != defaced); + // Memory prior to the stack pointer should be cleared. + assert_eq!( + &simulated_stack[0..size_of::<usize>()], + vec![0u8; size_of::<usize>()].as_slice() + ); + + // Small integers should not be sanitized. + for ii in -4096..=4096isize { + simulated_stack = vec![0u8; 2 * size_of::<usize>()]; + simulated_stack[0..size_of::<usize>()].copy_from_slice(&(ii as usize).to_ne_bytes()); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with small integers"); + assert!(simulated_stack[size_of::<usize>()..] != defaced); + } + + // The instruction pointer definitely should point into an executable mapping. + let instr_ptr = thread_info.get_instruction_pointer(); + let mapping_info = dumper + .find_mapping_no_bias(instr_ptr) + .expect("Failed to find mapping info"); + assert!(mapping_info.is_executable()); + + // Pointers to code shouldn't be sanitized. + simulated_stack = vec![0u8; 2 * size_of::<usize>()]; + simulated_stack[size_of::<usize>()..].copy_from_slice(&instr_ptr.to_ne_bytes()); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with instr_ptr"); + assert!(simulated_stack[0..size_of::<usize>()] != defaced); + assert!(simulated_stack[size_of::<usize>()..] != defaced); + + // String fragments should be sanitized. + let junk = "abcdefghijklmnop".as_bytes(); + simulated_stack.copy_from_slice(&junk[0..2 * size_of::<usize>()]); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with junk"); + assert_eq!(simulated_stack[0..size_of::<usize>()], defaced); + assert_eq!(simulated_stack[size_of::<usize>()..], defaced); + + // Heap pointers should be sanititzed. + + // NOTE: The original test used the heap-address below, but here thread_info.regs.rcx + // is the instruction pointer, and thus in direct conflict with the "instruction pointer" + // testcase. + // Instead we just allocate something on the heap in the child and pass that address to this test. + // #[cfg(target_arch = "x86_64")] + // let heap_addr = thread_info.regs.rcx as usize; + // #[cfg(target_arch = "x86")] + // let heap_addr = thread_info.regs.ecx as usize; + // #[cfg(target_arch = "arm")] + // let heap_addr = thread_info.regs.uregs[3] as usize; + // #[cfg(target_arch = "aarch64")] + // let heap_addr = thread_info.regs.regs[3] as usize; + // #[cfg(target_arch = "mips")] + // let heap_addr = thread_info.mcontext.gregs[1] as usize; + + simulated_stack = vec![0u8; 2 * size_of::<usize>()]; + + simulated_stack[0..size_of::<usize>()].copy_from_slice(&heap_addr.to_ne_bytes()); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with heap addr"); + + assert_eq!(simulated_stack[0..size_of::<usize>()], defaced); + + dumper.resume_threads().expect("Failed to resume threads"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); +} diff --git a/third_party/rust/minidump-writer/tests/task_dumper.rs b/third_party/rust/minidump-writer/tests/task_dumper.rs new file mode 100644 index 0000000000..1411acc34a --- /dev/null +++ b/third_party/rust/minidump-writer/tests/task_dumper.rs @@ -0,0 +1,158 @@ +//! All of these tests are specific to the MacOS task dumper +#![cfg(target_os = "macos")] + +use minidump_writer::{mach::LoadCommand, task_dumper::TaskDumper}; +use std::fmt::Write; + +fn call_otool(args: &[&str]) -> String { + let mut cmd = std::process::Command::new("otool"); + cmd.args(args); + + let exe_path = std::env::current_exe().expect("unable to retrieve test executable path"); + cmd.arg(exe_path); + + let output = cmd.output().expect("failed to spawn otool"); + + assert!(output.status.success()); + + String::from_utf8(output.stdout).expect("stdout was invalid utf-8") +} + +/// Validates we can iterate the load commands for all of the images in the task +#[test] +fn iterates_load_commands() { + let lc_str = call_otool(&["-l"]); + + let mut expected = String::new(); + let mut lc_index = 0; + + expected.push('\n'); + + while let Some(nlc) = lc_str[lc_index..].find("Load command ") { + lc_index += nlc; + + let block = match lc_str[lc_index + 13..].find("Load command ") { + Some(ind) => &lc_str[lc_index + 13..lc_index + 13 + ind], + None => &lc_str[lc_index..], + }; + + // otool prints the load command index for each command, but we only + // handle the small subset of the available load commands we care about + // so just ignore that + let block = &block[block.find('\n').unwrap() + 1..]; + + // otool also prints all the sections for LC_SEGMENT_* commands, but + // we don't care about those, so ignore them + let block = match block.find("Section") { + Some(ind) => &block[..ind], + None => block, + }; + + lc_index += 13; + + let cmd = block + .find("cmd ") + .expect("load commnd didn't specify cmd kind"); + let cmd_end = block[cmd..] + .find('\n') + .expect("load cmd didn't end with newline"); + if matches!( + &block[cmd + 4..cmd + cmd_end], + "LC_SEGMENT_64" | "LC_UUID" | "LC_ID_DYLIB" | "LC_LOAD_DYLINKER" + ) { + expected.push_str(block); + } + } + + let task_dumper = TaskDumper::new( + // SAFETY: syscall + unsafe { mach2::traps::mach_task_self() }, + ); + + let mut actual = String::new(); + + // Unfortunately, Apple decided to move dynamic libs into a shared cache, + // removing them from the file system completely, and unless I'm missing it + // there is no way to get the load commands for the dylibs since otool + // only understands file paths? So we just get the load commands for the main + // executable instead, this means that we miss the `LC_ID_DYLIB` commands + // since they only apply to dylibs, but this test is more that we can + // correctly iterate through the load commands themselves, so this _should_ + // be fine... + let exe_img = task_dumper + .read_executable_image() + .expect("failed to read executable image"); + + { + let lcmds = task_dumper + .read_load_commands(&exe_img) + .expect("failed to read load commands"); + + for lc in lcmds.iter() { + match lc { + LoadCommand::Segment(seg) => { + let segname = std::str::from_utf8(&seg.segment_name).unwrap(); + let segname = &segname[..segname.find('\0').unwrap()]; + write!( + &mut actual, + " + cmd LC_SEGMENT_64 + cmdsize {} + segname {} + vmaddr 0x{:016x} + vmsize 0x{:016x} + fileoff {} + filesize {} + maxprot 0x{:08x} + initprot 0x{:08x} + nsects {} + flags 0x{:x}", + seg.cmd_size, + segname, + seg.vm_addr, + seg.vm_size, + seg.file_off, + seg.file_size, + seg.max_prot, + seg.init_prot, + seg.num_sections, + seg.flags, + ) + .unwrap(); + } + LoadCommand::Dylib(_dylib) => { + unreachable!(); + } + LoadCommand::Uuid(uuid) => { + let id = uuid::Uuid::from_bytes(uuid.uuid); + let mut uuid_buf = [0u8; uuid::fmt::Hyphenated::LENGTH]; + let uuid_str = id.hyphenated().encode_upper(&mut uuid_buf); + + write!( + &mut actual, + " + cmd LC_UUID + cmdsize {} + uuid {uuid_str} +", + uuid.cmd_size, + ) + .unwrap(); + } + LoadCommand::DylinkerCommand(dy_cmd) => { + write!( + &mut actual, + " + cmd LC_LOAD_DYLINKER + cmdsize {} + name {} (offset {})", + dy_cmd.cmd_size, dy_cmd.name, dy_cmd.name_offset, + ) + .unwrap(); + } + } + } + } + + similar_asserts::assert_eq!(expected, actual); +} diff --git a/third_party/rust/minidump-writer/tests/windows_minidump_writer.rs b/third_party/rust/minidump-writer/tests/windows_minidump_writer.rs new file mode 100644 index 0000000000..1b2c1d4bb9 --- /dev/null +++ b/third_party/rust/minidump-writer/tests/windows_minidump_writer.rs @@ -0,0 +1,184 @@ +#![cfg(all(target_os = "windows", target_arch = "x86_64"))] + +use minidump::{ + CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpSystemInfo, + MinidumpThreadList, +}; +use minidump_writer::minidump_writer::MinidumpWriter; +mod common; +use common::start_child_and_return; + +const EXCEPTION_ILLEGAL_INSTRUCTION: i32 = -1073741795; +const STATUS_INVALID_PARAMETER: i32 = -1073741811; +#[link(name = "kernel32")] +extern "system" { + fn GetCurrentThreadId() -> u32; +} + +fn get_crash_reason<'a, T: std::ops::Deref<Target = [u8]> + 'a>( + md: &Minidump<'a, T>, +) -> CrashReason { + let exc: minidump::MinidumpException<'_> = + md.get_stream().expect("unable to find exception stream"); + + exc.get_crash_reason( + minidump::system_info::Os::Windows, + minidump::system_info::Cpu::X86_64, + ) +} + +/// Ensures that we can write minidumps for the current process, even if this is +/// not necessarily the primary intended use case of out-of-process dumping +#[test] +fn dump_current_process() { + let mut tmpfile = tempfile::Builder::new() + .prefix("windows_current_process") + .tempfile() + .unwrap(); + + MinidumpWriter::dump_local_context( + Some(STATUS_INVALID_PARAMETER), + None, + None, + tmpfile.as_file_mut(), + ) + .expect("failed to write minidump"); + + let md = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + + let crash_reason = get_crash_reason(&md); + + assert_eq!( + crash_reason, + CrashReason::from_windows_error(STATUS_INVALID_PARAMETER as u32) + ); + + // SAFETY: syscall + let thread_id = unsafe { GetCurrentThreadId() }; + + let bp_info: MinidumpBreakpadInfo = + md.get_stream().expect("Couldn't find MinidumpBreakpadInfo"); + + assert_eq!(bp_info.dump_thread_id.unwrap(), thread_id); + assert_eq!(bp_info.requesting_thread_id.unwrap(), thread_id); +} + +#[test] +fn dump_specific_thread() { + let mut tmpfile = tempfile::Builder::new() + .prefix("windows_current_process") + .tempfile() + .unwrap(); + + let (tx, rx) = std::sync::mpsc::channel(); + + let jh = std::thread::spawn(move || { + // SAFETY: syscall + let thread_id = unsafe { GetCurrentThreadId() }; + while tx.send(thread_id).is_ok() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + }); + + let crashing_thread_id = rx.recv().unwrap(); + + MinidumpWriter::dump_local_context( + Some(STATUS_INVALID_PARAMETER), + Some(crashing_thread_id), + None, + tmpfile.as_file_mut(), + ) + .expect("failed to write minidump"); + + drop(rx); + jh.join().unwrap(); + + let md = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + + let crash_reason = get_crash_reason(&md); + + assert_eq!( + crash_reason, + CrashReason::from_windows_error(STATUS_INVALID_PARAMETER as u32) + ); + + // SAFETY: syscall + let requesting_thread_id = unsafe { GetCurrentThreadId() }; + + let bp_info: MinidumpBreakpadInfo = + md.get_stream().expect("Couldn't find MinidumpBreakpadInfo"); + + assert_eq!(bp_info.dump_thread_id.unwrap(), crashing_thread_id); + assert_eq!(bp_info.requesting_thread_id.unwrap(), requesting_thread_id); +} + +/// Ensures that we can write minidumps for an external process. Unfortunately +/// this requires us to know the actual pointer in the client process for the +/// exception, as the `MiniDumpWriteDump` syscall directly reads points from +/// the process memory, so we communicate that back from the client process +/// via stdout +#[test] +fn dump_external_process() { + use std::io::BufRead; + + let mut child = start_child_and_return(&[&format!("{:x}", EXCEPTION_ILLEGAL_INSTRUCTION)]); + + let (process_id, exception_pointers, thread_id, exception_code) = { + let mut f = std::io::BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + f.read_line(&mut buf).expect("failed to read stdout"); + assert!(!buf.is_empty()); + + let mut biter = buf.trim().split(' '); + + let process_id: u32 = biter.next().unwrap().parse().unwrap(); + let exception_pointers: usize = biter.next().unwrap().parse().unwrap(); + let thread_id: u32 = biter.next().unwrap().parse().unwrap(); + let exception_code = u32::from_str_radix(biter.next().unwrap(), 16).unwrap(); + + (process_id, exception_pointers, thread_id, exception_code) + }; + + let exception_code = exception_code as i32; + assert_eq!(exception_code, EXCEPTION_ILLEGAL_INSTRUCTION); + + let crash_context = crash_context::CrashContext { + exception_pointers: exception_pointers as _, + process_id, + thread_id, + exception_code, + }; + + let mut tmpfile = tempfile::Builder::new() + .prefix("windows_external_process") + .tempfile() + .unwrap(); + + // SAFETY: We keep the process we are dumping alive until the minidump is written + // and the test process keep the pointers it sent us alive until it is killed + MinidumpWriter::dump_crash_context(crash_context, None, tmpfile.as_file_mut()) + .expect("failed to write minidump"); + + child.kill().expect("failed to kill child"); + + let md = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + + let crash_reason = get_crash_reason(&md); + + assert_eq!( + crash_reason, + CrashReason::from_windows_code(EXCEPTION_ILLEGAL_INSTRUCTION as u32) + ); +} |