Repository: luser/rust-minidump
Pull request #329 (2021-12-01 21:51:21)

WIP make symbolication async
I was curious how bad this was and then suddenly I was "done" lol

This compiles and correctly handles amd64.

TODO: 

* [x] copy-paste changes to other stackwalking backends (and re-enable them)
* [x] make tests work with async (with `#[tokio::test]`?)
* [x] make sure the symbolic-syms shim still compiles
* [x] performance analysis (socc-pair --bench)
* ~~[ ] test `#[tokio::main(flavor = "current_thread")]`~~ (perf analysis came back good enough that I don't care)


Gankra: implements #327 
Gankra: Me: what if I add Send + Sync everywhere. Surely that won't work. There's like, borrows and stuff right?

rustc: seems fine

Me: well, this is the entire purpose for your existence so I'm going to assume this is correct..?

---------

I feel like I'm probably not full leveraging the power of async in fetch_symbol_file and save_contents, but that's probably better left to the sentry folks who actually use async all the time.
luser: > Me: what if I add Send + Sync everywhere. Surely that won't work. There's like, borrows and stuff right?

The minidump data is only ever borrowed immutably. :) At one point I pondered what a multithreaded `process_minidump` implementation would look like but I discarded that idea because the actual unwinding takes no time at all. If anything, per the Discord discussion we had today, doing all the unwinding in one pass, fetching all the symbol files needed to symbolicate in parallel, and then symbolicating the stacks in one fell swoop would be the most clever approach. (The only downside being that the current heuristics for stack scanning rely on symbols to determine if a return address is inside a known function.)
luser: Given the CI errors here, you're probably going to need to pull in `#[tokio::test]`.
Gankra: As far as I know, this is a complete implementation. Still not gonna land it anytime this ~month ideally but it's here if anyone wants to build on it.
Gankra: cc @nornagon in case this is at all still relevant to you
nornagon: Oh cool! Is the conversion to the new symbolizer done too?
Gankra: Unfortunately the folks at sentry decided to focus on something else, and I had higher priorities to take care of first. Also in local testing we've still found this implementation is still significantly faster than breakpad, which is nice. 

Now that rust-minidump is ready for production, I checked in with sentry and they cited the lack of async symbolication support as an issue. So this is in some sense "preparing" for the eventual future where they're the backend. But for the short-to-midterm breakpad-symbols is gonna be The Backend.

Note that mozilla is in the middle of transitioning our infrastructure to rust-minidump, so I'm holding off on landing this patch until probably january when everyone's back from the holidays, to minimize risk. In the meantime, this branch is here for you to build on top of if you want (I imagine you still need to add a bit more async to make minidump loading work for you, and to add a bunch of wasm cfgs). If you'd like to touch base on what electron needs from this, I'm happy to discuss further details on whatever your preferred platform is.
Gankra: Rebased, but this is effectively reverting the functionality of #359 right now. I need to rework the code to use `Response::chunks` which is the "async" version of Read for reqwest. This is slightly annoying because File i/o and what not does not use `chunks`. Might just copy-paste the code, idk.
Gankra: Async streaming parsing now works? Kinda sloppy but seems legit?

```
miniump-stackwalk runtime: 00m:06s:885ms (6885ms)
miniump-stackwalk max memory (rss): 255MB (268423168 bytes)
```
Gankra: I think that's a wrap for me in 2021, see y'all in January.
Gankra: Ok one last note before I forget:

In this design, there's kind of a split async personality:

* When reading from a file, once you start reading you're in a blocking loop that will run to completion.
* When downloading a file, the process is fully async, and will `await` any time we need more data from the network.

This is intuitively the right balance since async file I/O is a Lie, but I don't know enough about what people "want" from async to know for sure. That said it could all be moot in a world of Symbolic, because all this code gets banished to hell.

Although I could imagine this "version" being friendlier for compile-to-wasm? So maybe we do end up keeping both paths around. *shrug*
Gankra: I added a --mock-server mode to `socc-pair`, so I can now bench the server path without actually hitting the network:

```
socc-pair
--api-token=<snip>
--run-local=C:\Users\ninte\dev\rust-minidump\minidump-stackwalk 
--crash-id=9520523c-45ff-4d6c-a186-4fec80211216 
--output-file=output.diff 
--mock-server 
--bench=30
```

# Summary of Results

## This Change


* average runtime: 1602ms -> 1643ms (1.02x runtime, probably within margin of error)
* average max-memory: 253MB -> 254MB (no significant change)

## Remeasurement of streaming parsing change


* average runtime: 1864ms -> 1602ms (0.86x runtime, nice little win but easily dwarfed by real download times)
* average max-memory: 383MB -> 253MB (0.66x memory usage!)


# Individual Results:

## async (this PR) - 30 iterations, cold cache (local static file server):

```
benchmark results (ms):
  1630, 1642, 1604, 1645, 1645, 1655, 1649, 1626, 1684, 1641, 1633, 1652, 1628, 1624, 1622, 1660, 1640, 1663, 1599, 1717, 1648, 1674, 1671, 1627, 1626, 1666, 1624, 1624, 1643, 1642, 
average runtime: 00m:01s:643ms (1643ms)
median runtime: 00m:01s:642ms (1642ms)
min runtime: 00m:01s:599ms (1599ms)
max runtime: 00m:01s:717ms (1717ms)

max memory (rss) results (bytes):
  266653696, 266702848, 266485760, 265920512, 267214848, 266309632, 267427840, 266227712, 266633216, 265838592, 266575872, 266588160, 265940992, 266084352, 266993664, 266498048, 265887744, 266072064, 266149888, 266407936, 266211328, 266932224, 266964992, 267255808, 266276864, 266936320, 266256384, 267132928, 265351168, 267161600, 
average max-memory: 254MB (266503099 bytes)
median max-memory: 254MB (266498048 bytes)
min max-memory: 253MB (265351168 bytes)
max max-memory: 255MB (267427840 bytes)
```

```

## master - 30 iterations, cold cache (local static file server):

  1610, 1612, 1574, 1612, 1600, 1568, 1583, 1594, 1590, 1607, 1625, 1581, 1610, 1799, 1599, 1592, 1606, 1594, 1597, 1601, 1571, 1586, 1590, 1596, 1592, 1589, 1603, 1595, 1577, 1631, 
average runtime: 00m:01s:602ms (1602ms)
median runtime: 00m:01s:596ms (1596ms)
min runtime: 00m:01s:568ms (1568ms)
max runtime: 00m:01s:799ms (1799ms)

max memory (rss) results (bytes):
  265908224, 266510336, 265248768, 265646080, 265555968, 265035776, 266092544, 265515008, 265510912, 265961472, 266174464, 264835072, 265707520, 265990144, 266002432, 266915840, 266076160, 265310208, 265334784, 266268672, 265588736, 265003008, 265302016, 265940992, 265338880, 265875456, 265576448, 265646080, 265846784, 265408512, 
average max-memory: 253MB (265704243 bytes)
median max-memory: 253MB (265646080 bytes)
min max-memory: 252MB (264835072 bytes)
max max-memory: 254MB (266915840 bytes)
```




## before streaming parser (2dc7ecc384f8ed123387cace30157c5288) - 30 iterations, cold cache (local static file server):

```
benchmark results (ms):
  1771, 1897, 1815, 1888, 2049, 2035, 1733, 1932, 1736, 1751, 1890, 1742, 1912, 1790, 1881, 2069, 1911, 1724, 1762, 1855, 1832, 2029, 1915, 1932, 1863, 1872, 1830, 1762, 1854, 1903, 
average runtime: 00m:01s:864ms (1864ms)
median runtime: 00m:01s:872ms (1872ms)
min runtime: 00m:01s:724ms (1724ms)
max runtime: 00m:02s:069ms (2069ms)

max memory (rss) results (bytes):
  402206720, 401584128, 402771968, 400928768, 401539072, 402026496, 401924096, 402530304, 402178048, 402096128, 402280448, 401825792, 401575936, 402894848, 402010112, 401981440, 401850368, 402341888, 401457152, 402329600, 401281024, 401469440, 401362944, 402178048, 402026496, 402587648, 402583552, 402243584, 401514496, 402354176, 
average max-memory: 383MB (401997824 bytes)
median max-memory: 383MB (402026496 bytes)
min max-memory: 382MB (400928768 bytes)
max max-memory: 384MB (402894848 bytes)
```
Gankra: @jan-auer I'm inclined to land this so it stops being a looming specter that I keep needing to rebase. Any final objections / arguments against it?
Gankra: No objections, landing as soon as CI passes...

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 32ee87d..0114f98 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -39,6 +39,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "async-trait"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -72,6 +83,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 name = "breakpad-symbols"
 version = "0.9.6"
 dependencies = [
+ "async-trait",
  "circular",
  "log",
  "minidump-common",
@@ -80,6 +92,7 @@ dependencies = [
  "reqwest",
  "tempfile",
  "thiserror",
+ "tokio",
 ]
 
 [[package]]
@@ -359,12 +372,6 @@ version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
 
-[[package]]
-name = "futures-io"
-version = "0.3.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
-
 [[package]]
 name = "futures-sink"
 version = "0.3.19"
@@ -384,12 +391,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
 dependencies = [
  "futures-core",
- "futures-io",
  "futures-task",
- "memchr",
  "pin-project-lite",
  "pin-utils",
- "slab",
 ]
 
 [[package]]
@@ -586,6 +590,15 @@ version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
 
+[[package]]
+name = "lock_api"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
+dependencies = [
+ "scopeguard",
+]
+
 [[package]]
 name = "log"
 version = "0.4.14"
@@ -659,6 +672,7 @@ dependencies = [
 name = "minidump-processor"
 version = "0.9.6"
 dependencies = [
+ "async-trait",
  "breakpad-symbols",
  "clap",
  "doc-comment",
@@ -671,6 +685,7 @@ dependencies = [
  "synth-minidump",
  "test-assembler",
  "thiserror",
+ "tokio",
 ]
 
 [[package]]
@@ -686,6 +701,7 @@ dependencies = [
  "simplelog",
  "synth-minidump",
  "test-assembler",
+ "tokio",
 ]
 
 [[package]]
@@ -849,6 +865,31 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
@@ -972,6 +1013,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
 [[package]]
 name = "scroll"
 version = "0.10.2"
@@ -994,9 +1041,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce"
+checksum = "3fed7948b6c68acbb6e20c334f55ad635dc0f75506963de4464289fbd3b051ac"
 dependencies = [
  "bitflags",
  "core-foundation",
@@ -1007,9 +1054,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7"
+checksum = "a57321bf8bc2362081b2599912d2961fe899c0efadf1b4b2f8d48b3e253bb96c"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -1070,6 +1117,15 @@ dependencies = [
  "yaml-rust",
 ]
 
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "similar"
 version = "2.1.0"
@@ -1093,6 +1149,12 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
 
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
 [[package]]
 name = "smart-default"
 version = "0.6.0"
@@ -1271,10 +1333,25 @@ dependencies = [
  "memchr",
  "mio",
  "num_cpus",
+ "once_cell",
+ "parking_lot",
  "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
  "winapi",
 ]
 
+[[package]]
+name = "tokio-macros"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "tokio-native-tls"
 version = "0.3.0"
diff --git a/README.md b/README.md
index 6455c51..c4ffbe4 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,8 @@ use minidump::Minidump;
 use minidump_processor::{http_symbol_supplier, ProcessorOptions, Symbolizer};
 use serde_json::Value;
 
-fn main() -> Result<(), ()> {
+#[tokio::main]
+async fn main() -> Result<(), ()> {
     // Read the minidump
     let dump = Minidump::read_path("../testdata/test.dmp").map_err(|_| ())?;
  
@@ -77,6 +78,7 @@ fn main() -> Result<(), ()> {
     ));
  
     let state = minidump_processor::process_minidump_with_options(&dump, &provider, options)
+        .await
         .map_err(|_| ())?;
 
     // Write the JSON output to an arbitrary writer (here, a Vec).
diff --git a/breakpad-symbols/Cargo.toml b/breakpad-symbols/Cargo.toml
index fe33063..66ec8de 100644
--- a/breakpad-symbols/Cargo.toml
+++ b/breakpad-symbols/Cargo.toml
@@ -14,15 +14,19 @@ edition = "2018"
 travis-ci = { repository = "luser/rust-minidump" }
 
 [dependencies]
+async-trait = "0.1.51"
 circular = "0.3.0"
 minidump-common = { version = "0.9.6", path = "../minidump-common" }
 range-map = "0.1.5"
 nom = "~1.2.2"
 log = "0.4.1"
-reqwest = { version = "0.11.6", features = ["blocking", "gzip"] }
+reqwest = { version = "0.11.6", features = ["gzip"] }
 tempfile = "3.3.0"
 thiserror = "1.0.30"
 
 # Private API, only here to support the fuzzer
 [features]
 fuzz = []
+
+[dev-dependencies]
+tokio =  { version = "1.12.0", features = ["full"] }
diff --git a/breakpad-symbols/src/lib.rs b/breakpad-symbols/src/lib.rs
index 13684d6..3bba521 100644
--- a/breakpad-symbols/src/lib.rs
+++ b/breakpad-symbols/src/lib.rs
@@ -17,32 +17,37 @@
 //!
 //! ```
 //! # std::env::set_current_dir(env!("CARGO_MANIFEST_DIR"));
-//! use breakpad_symbols::{SimpleSymbolSupplier,Symbolizer,SimpleFrame,SimpleModule};
+//! use breakpad_symbols::{SimpleSymbolSupplier, Symbolizer, SimpleFrame, SimpleModule};
 //! use std::path::PathBuf;
-//! let paths = vec!(PathBuf::from("../testdata/symbols/"));
-//! let supplier = SimpleSymbolSupplier::new(paths);
-//! let symbolizer = Symbolizer::new(supplier);
 //!
-//! // Simple function name lookup with debug file, debug id, address.
-//! assert_eq!(symbolizer.get_symbol_at_address("test_app.pdb",
-//!                                             "5A9832E5287241C1838ED98914E9B7FF1",
-//!                                             0x1010)
-//!               .unwrap(),
-//!               "vswprintf");
+//! #[tokio::main]
+//! async fn main() {
+//!     let paths = vec!(PathBuf::from("../testdata/symbols/"));
+//!     let supplier = SimpleSymbolSupplier::new(paths);
+//!     let symbolizer = Symbolizer::new(supplier);
+//!
+//!     // Simple function name lookup with debug file, debug id, address.
+//!     assert_eq!(symbolizer.get_symbol_at_address("test_app.pdb",
+//!         "5A9832E5287241C1838ED98914E9B7FF1",
+//!         0x1010)
+//!         .await
+//!         .unwrap(),
+//!         "vswprintf");
+//! }
 //! ```
 
+use async_trait::async_trait;
 use log::{debug, trace, warn};
-use reqwest::blocking::Client;
-use reqwest::Url;
+use reqwest::{Client, Url};
 use tempfile::NamedTempFile;
 
 use std::borrow::Cow;
 use std::boxed::Box;
-use std::cell::RefCell;
 use std::collections::HashMap;
 use std::fs;
 use std::io::{self, Write};
 use std::path::{Path, PathBuf};
+use std::sync::Mutex;
 use std::time::Duration;
 
 pub use minidump_common::traits::Module;
@@ -168,7 +173,7 @@ fn replace_or_add_extension(filename: &str, match_extension: &str, new_extension
 ///
 /// [module_line]: https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md#MODULE-records
 /// [packagesymbols]: https://gist.github.com/luser/2ad32d290f224782fcfc#file-packagesymbols-py
-pub fn relative_symbol_path(module: &dyn Module, extension: &str) -> Option<String> {
+pub fn relative_symbol_path(module: &(dyn Module + Sync), extension: &str) -> Option<String> {
     module.debug_file().and_then(|debug_file| {
         module.debug_identifier().map(|debug_id| {
             // Can't use PathBuf::file_name here, it doesn't handle
@@ -250,12 +255,14 @@ impl PartialEq for SymbolError {
 }
 
 /// A trait for things that can locate symbols for a given module.
+#[async_trait]
 pub trait SymbolSupplier {
     /// Locate and load a symbol file for `module`.
     ///
     /// Implementations may use any strategy for locating and loading
     /// symbols.
-    fn locate_symbols(&self, module: &dyn Module) -> Result<SymbolFile, SymbolError>;
+    async fn locate_symbols(&self, module: &(dyn Module + Sync))
+        -> Result<SymbolFile, SymbolError>;
 }
 
 /// An implementation of `SymbolSupplier` that loads Breakpad text-format symbols from local disk
@@ -276,8 +283,12 @@ impl SimpleSymbolSupplier {
     }
 }
 
+#[async_trait]
 impl SymbolSupplier for SimpleSymbolSupplier {
-    fn locate_symbols(&self, module: &dyn Module) -> Result<SymbolFile, SymbolError> {
+    async fn locate_symbols(
+        &self,
+        module: &(dyn Module + Sync),
+    ) -> Result<SymbolFile, SymbolError> {
         if let Some(rel_path) = relative_symbol_path(module, "sym") {
             for path in self.paths.iter() {
                 let test_path = path.join(&rel_path);
@@ -305,8 +316,12 @@ impl StringSymbolSupplier {
     }
 }
 
+#[async_trait]
 impl SymbolSupplier for StringSymbolSupplier {
-    fn locate_symbols(&self, module: &dyn Module) -> Result<SymbolFile, SymbolError> {
+    async fn locate_symbols(
+        &self,
+        module: &(dyn Module + Sync),
+    ) -> Result<SymbolFile, SymbolError> {
         if let Some(symbols) = self.modules.get(&*module.code_file()) {
             return SymbolFile::from_bytes(symbols.as_bytes());
         }
@@ -417,7 +432,7 @@ fn commit_cache_file(mut temp: NamedTempFile, final_path: &Path, url: &Url) -> i
 
 /// Fetch a symbol file from the URL made by combining `base_url` and `rel_path` using `client`,
 /// save the file contents under `cache` + `rel_path` and also return them.
-fn fetch_symbol_file(
+async fn fetch_symbol_file(
     client: &Client,
     base_url: &Url,
     rel_path: &str,
@@ -439,6 +454,7 @@ fn fetch_symbol_file(
     let res = client
         .get(url.clone())
         .send()
+        .await
         .and_then(|res| res.error_for_status())
         .map_err(|_| SymbolError::NotFound)?;
 
@@ -451,7 +467,7 @@ fn fetch_symbol_file(
         .ok();
 
     // Now stream parse the file as it downloads.
-    let mut symbol_file = SymbolFile::parse(res, |data| {
+    let mut symbol_file = SymbolFile::parse_async(res, |data| {
         // While we're downloading+parsing, save this data to the the disk cache too
         if let Some(file) = temp.as_mut() {
             if let Err(e) = file.write_all(data) {
@@ -460,7 +476,8 @@ fn fetch_symbol_file(
                 temp = None;
             }
         }
-    })?;
+    })
+    .await?;
     // Make note of what URL this symbol file was downloaded from.
     symbol_file.url = Some(url.to_string());
 
@@ -474,10 +491,14 @@ fn fetch_symbol_file(
     Ok(symbol_file)
 }
 
+#[async_trait]
 impl SymbolSupplier for HttpSymbolSupplier {
-    fn locate_symbols(&self, module: &dyn Module) -> Result<SymbolFile, SymbolError> {
+    async fn locate_symbols(
+        &self,
+        module: &(dyn Module + Sync),
+    ) -> Result<SymbolFile, SymbolError> {
         // Check local paths first.
-        let local_result = self.local.locate_symbols(module);
+        let local_result = self.local.locate_symbols(module).await;
         if !matches!(local_result, Err(SymbolError::NotFound)) {
             // Everything but NotFound prevents cascading
             return local_result;
@@ -486,7 +507,7 @@ impl SymbolSupplier for HttpSymbolSupplier {
         if let Some(rel_path) = relative_symbol_path(module, "sym") {
             for url in &self.urls {
                 if let Ok(file) =
-                    fetch_symbol_file(&self.client, url, &rel_path, &self.cache, &self.tmp)
+                    fetch_symbol_file(&self.client, url, &rel_path, &self.cache, &self.tmp).await
                 {
                     return Ok(file);
                 }
@@ -580,7 +601,7 @@ impl FrameSymbolizer for SimpleFrame {
 type ModuleKey = (String, String, Option<String>, Option<String>);
 
 /// Helper for deriving a hash key from a `Module` for `Symbolizer`.
-fn key(module: &dyn Module) -> ModuleKey {
+fn key(module: &(dyn Module + Sync)) -> ModuleKey {
     (
         module.code_file().to_string(),
         module.code_identifier().to_string(),
@@ -610,21 +631,21 @@ fn key(module: &dyn Module) -> ModuleKey {
 /// [fill_symbol]: struct.Symbolizer.html#method.fill_symbol
 pub struct Symbolizer {
     /// Symbol supplier for locating symbols.
-    supplier: Box<dyn SymbolSupplier + 'static>,
+    supplier: Box<dyn SymbolSupplier + Send + Sync + 'static>,
     /// Cache of symbol locating results.
     // TODO?: use lru-cache: https://crates.io/crates/lru-cache/
     // note that using an lru-cache would mess up the fact that we currently
     // use this for statistics collection. Splitting out statistics would be
     // way messier but not impossible.
-    symbols: RefCell<HashMap<ModuleKey, Result<SymbolFile, SymbolError>>>,
+    symbols: Mutex<HashMap<ModuleKey, Result<SymbolFile, SymbolError>>>,
 }
 
 impl Symbolizer {
     /// Create a `Symbolizer` that uses `supplier` to locate symbols.
-    pub fn new<T: SymbolSupplier + 'static>(supplier: T) -> Symbolizer {
+    pub fn new<T: SymbolSupplier + Send + Sync + 'static>(supplier: T) -> Symbolizer {
         Symbolizer {
             supplier: Box::new(supplier),
-            symbols: RefCell::new(HashMap::new()),
+            symbols: Mutex::new(HashMap::new()),
         }
     }
 
@@ -637,7 +658,7 @@ impl Symbolizer {
     /// See [the module-level documentation][module] for an example.
     ///
     /// [module]: index.html
-    pub fn get_symbol_at_address(
+    pub async fn get_symbol_at_address(
         &self,
         debug_file: &str,
         debug_id: &str,
@@ -645,7 +666,7 @@ impl Symbolizer {
     ) -> Option<String> {
         let k = (debug_file, debug_id);
         let mut frame = SimpleFrame::with_instruction(address);
-        self.fill_symbol(&k, &mut frame).ok()?;
+        self.fill_symbol(&k, &mut frame).await.ok()?;
         frame.function
     }
 
@@ -662,30 +683,35 @@ impl Symbolizer {
     /// ```
     /// # std::env::set_current_dir(env!("CARGO_MANIFEST_DIR"));
     /// use breakpad_symbols::{SimpleSymbolSupplier,Symbolizer,SimpleFrame,SimpleModule};
-    /// use std::path::PathBuf;
-    /// let paths = vec!(PathBuf::from("../testdata/symbols/"));
-    /// let supplier = SimpleSymbolSupplier::new(paths);
-    /// let symbolizer = Symbolizer::new(supplier);
-    /// let m = SimpleModule::new("test_app.pdb", "5A9832E5287241C1838ED98914E9B7FF1");
-    /// let mut f = SimpleFrame::with_instruction(0x1010);
-    /// let _ = symbolizer.fill_symbol(&m, &mut f);
-    /// assert_eq!(f.function.unwrap(), "vswprintf");
-    /// assert_eq!(f.source_file.unwrap(), r"c:\program files\microsoft visual studio 8\vc\include\swprintf.inl");
-    /// assert_eq!(f.source_line.unwrap(), 51);
+    ///
+    /// #[tokio::main]
+    /// async fn main() {
+    ///     use std::path::PathBuf;
+    ///     let paths = vec!(PathBuf::from("../testdata/symbols/"));
+    ///     let supplier = SimpleSymbolSupplier::new(paths);
+    ///     let symbolizer = Symbolizer::new(supplier);
+    ///     let m = SimpleModule::new("test_app.pdb", "5A9832E5287241C1838ED98914E9B7FF1");
+    ///     let mut f = SimpleFrame::with_instruction(0x1010);
+    ///     let _ = symbolizer.fill_symbol(&m, &mut f).await;
+    ///     assert_eq!(f.function.unwrap(), "vswprintf");
+    ///     assert_eq!(f.source_file.unwrap(),
+    ///         r"c:\program files\microsoft visual studio 8\vc\include\swprintf.inl");
+    ///     assert_eq!(f.source_line.unwrap(), 51);
+    /// }
     /// ```
     ///
     /// [simplemodule]: struct.SimpleModule.html
     /// [simpleframe]: struct.SimpleFrame.html
-    pub fn fill_symbol(
+    pub async fn fill_symbol(
         &self,
-        module: &dyn Module,
-        frame: &mut dyn FrameSymbolizer,
+        module: &(dyn Module + Sync),
+        frame: &mut (dyn FrameSymbolizer + Send),
     ) -> Result<(), FillSymbolError> {
         let k = key(module);
-        self.ensure_module(module, &k);
+        self.ensure_module(module, &k).await;
 
         // Symbols will always contain an entry after ensure_module (though it may be an Err).
-        self.symbols.borrow()[&k]
+        self.symbols.lock().unwrap()[&k]
             .as_ref()
             .map(|sym| {
                 sym.fill_symbol(module, frame);
@@ -698,7 +724,8 @@ impl Symbolizer {
     /// Keys are the file name of the module (code_file's file name).
     pub fn stats(&self) -> HashMap<String, SymbolStats> {
         self.symbols
-            .borrow()
+            .lock()
+            .unwrap()
             .iter()
             .map(|(k, res)| {
                 let mut stats = SymbolStats::default();
@@ -727,10 +754,14 @@ impl Symbolizer {
     /// Tries to use CFI to walk the stack frame of the FrameWalker
     /// using the symbols of the given Module. Output will be written
     /// using the FrameWalker's `set_caller_*` APIs.
-    pub fn walk_frame(&self, module: &dyn Module, walker: &mut dyn FrameWalker) -> Option<()> {
+    pub async fn walk_frame(
+        &self,
+        module: &(dyn Module + Sync),
+        walker: &mut (dyn FrameWalker + Send),
+    ) -> Option<()> {
         let k = key(module);
-        self.ensure_module(module, &k);
-        if let Some(Ok(ref sym)) = self.symbols.borrow().get(&k) {
+        self.ensure_module(module, &k).await;
+        if let Some(Ok(ref sym)) = self.symbols.lock().unwrap().get(&k) {
             trace!("unwind: found symbols for address, searching for cfi entries");
             sym.walk_frame(module, walker)
         } else {
@@ -742,10 +773,10 @@ impl Symbolizer {
     /// Ensures there is an entry in the `symbols` map for the given key
     /// (although it may be an Error). Will not change the entry if it already
     /// exists (so if they first time we look is an Error, it always will be).
-    fn ensure_module(&self, module: &dyn Module, k: &ModuleKey) {
-        if !self.symbols.borrow().contains_key(k) {
-            let res = self.supplier.locate_symbols(module);
-            self.symbols.borrow_mut().insert(k.clone(), res);
+    async fn ensure_module(&self, module: &(dyn Module + Sync), k: &ModuleKey) {
+        if !self.symbols.lock().unwrap().contains_key(k) {
+            let res = self.supplier.locate_symbols(module).await;
+            self.symbols.lock().unwrap().insert(k.clone(), res);
         }
     }
 }
@@ -787,8 +818,8 @@ mod test {
     use std::io::Write;
     use std::path::{Path, PathBuf};
 
-    #[test]
-    fn test_relative_symbol_path() {
+    #[tokio::test]
+    async fn test_relative_symbol_path() {
         let m = SimpleModule::new("foo.pdb", "abcd1234");
         assert_eq!(
             &relative_symbol_path(&m, "sym").unwrap(),
@@ -829,8 +860,8 @@ mod test {
         assert!(relative_symbol_path(&bad3, "sym").is_none());
     }
 
-    #[test]
-    fn test_relative_symbol_path_abs_paths() {
+    #[tokio::test]
+    async fn test_relative_symbol_path_abs_paths() {
         {
             let m = SimpleModule::new("/path/to/foo.bin", "abcd1234");
             assert_eq!(
@@ -883,14 +914,17 @@ mod test {
         write_symbol_file(path, b"this is not a symbol file\n");
     }
 
-    #[test]
-    fn test_simple_symbol_supplier() {
+    #[tokio::test]
+    async fn test_simple_symbol_supplier() {
         let t = tempfile::tempdir().unwrap();
         let paths = mksubdirs(t.path(), &["one", "two"]);
 
         let supplier = SimpleSymbolSupplier::new(paths.clone());
         let bad = SimpleModule::default();
-        assert_eq!(supplier.locate_symbols(&bad), Err(SymbolError::NotFound));
+        assert_eq!(
+            supplier.locate_symbols(&bad).await,
+            Err(SymbolError::NotFound)
+        );
 
         // Try loading symbols for each of two modules in each of the two
         // search paths.
@@ -902,11 +936,14 @@ mod test {
         {
             let m = SimpleModule::new(file, id);
             // No symbols present yet.
-            assert_eq!(supplier.locate_symbols(&m), Err(SymbolError::NotFound));
+            assert_eq!(
+                supplier.locate_symbols(&m).await,
+                Err(SymbolError::NotFound)
+            );
             write_good_symbol_file(&path.join(sym));
             // Should load OK now that it exists.
             assert!(
-                matches!(supplier.locate_symbols(&m), Ok(_)),
+                matches!(supplier.locate_symbols(&m).await, Ok(_)),
                 "{}",
                 format!("Located symbols for {}", sym)
             );
@@ -915,9 +952,12 @@ mod test {
         // Write a malformed symbol file, verify that it's found but fails to load.
         let mal = SimpleModule::new("baz.pdb", "ffff0000");
         let sym = "baz.pdb/ffff0000/baz.sym";
-        assert_eq!(supplier.locate_symbols(&mal), Err(SymbolError::NotFound));
+        assert_eq!(
+            supplier.locate_symbols(&mal).await,
+            Err(SymbolError::NotFound)
+        );
         write_bad_symbol_file(&paths[0].join(sym));
-        let res = supplier.locate_symbols(&mal);
+        let res = supplier.locate_symbols(&mal).await;
         assert!(
             matches!(res, Err(SymbolError::ParseError(..))),
             "{}",
@@ -925,8 +965,8 @@ mod test {
         );
     }
 
-    #[test]
-    fn test_symbolizer() {
+    #[tokio::test]
+    async fn test_symbolizer() {
         let t = tempfile::tempdir().unwrap();
         let path = t.path();
 
@@ -943,7 +983,7 @@ FUNC 1000 30 10 some func
 ",
         );
         let mut f1 = SimpleFrame::with_instruction(0x1010);
-        symbolizer.fill_symbol(&m1, &mut f1).unwrap();
+        symbolizer.fill_symbol(&m1, &mut f1).await.unwrap();
         assert_eq!(f1.function.unwrap(), "some func");
         assert_eq!(f1.function_base.unwrap(), 0x1000);
         assert_eq!(f1.source_file.unwrap(), "foo.c");
@@ -953,6 +993,7 @@ FUNC 1000 30 10 some func
         assert_eq!(
             symbolizer
                 .get_symbol_at_address("foo.pdb", "abcd1234", 0x1010)
+                .await
                 .unwrap(),
             "some func"
         );
@@ -960,7 +1001,7 @@ FUNC 1000 30 10 some func
         let m2 = SimpleModule::new("bar.pdb", "ffff0000");
         let mut f2 = SimpleFrame::with_instruction(0x1010);
         // No symbols present, should not find anything.
-        assert!(symbolizer.fill_symbol(&m2, &mut f2).is_err());
+        assert!(symbolizer.fill_symbol(&m2, &mut f2).await.is_err());
         assert!(f2.function.is_none());
         assert!(f2.function_base.is_none());
         assert!(f2.source_file.is_none());
@@ -974,7 +1015,7 @@ FUNC 1000 30 10 another func
 1000 30 7 53
 ",
         );
-        assert!(symbolizer.fill_symbol(&m2, &mut f2).is_err());
+        assert!(symbolizer.fill_symbol(&m2, &mut f2).await.is_err());
         assert!(f2.function.is_none());
         assert!(f2.function_base.is_none());
         assert!(f2.source_file.is_none());
@@ -982,6 +1023,7 @@ FUNC 1000 30 10 another func
         // This should also use cached results.
         assert!(symbolizer
             .get_symbol_at_address("bar.pdb", "ffff0000", 0x1010)
+            .await
             .is_none());
     }
 }
diff --git a/breakpad-symbols/src/sym_file/mod.rs b/breakpad-symbols/src/sym_file/mod.rs
index 78406a9..aebc97c 100644
--- a/breakpad-symbols/src/sym_file/mod.rs
+++ b/breakpad-symbols/src/sym_file/mod.rs
@@ -90,6 +90,64 @@ impl SymbolFile {
         }
     }
 
+    /// `parse` but async
+    pub async fn parse_async(
+        mut response: reqwest::Response,
+        mut callback: impl FnMut(&[u8]),
+    ) -> Result<SymbolFile, SymbolError> {
+        let mut chunk;
+        let mut slice = &[][..];
+        let mut input_reader = &mut slice;
+        let mut buf = circular::Buffer::with_capacity(100_000);
+        let mut parser = SymbolParser::new();
+        let mut fully_consumed = false;
+        loop {
+            // Little rube-goldberg machine to stream the contents:
+            // * get a chunk (Bytes) from the Response
+            // * get its underlying slice
+            // * then get a mutable reference to that slice
+            // * then Read that mutable reference in our circular buffer
+            // * when the slice runs out, get the next chunk and repeat
+            if input_reader.is_empty() {
+                chunk = response
+                    .chunk()
+                    .await
+                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
+                    .unwrap_or_default();
+                slice = &chunk[..];
+                input_reader = &mut slice;
+            }
+            // Read the data in, and tell the circular buffer about the new data
+            let size = input_reader.read(buf.space())?;
+            buf.fill(size);
+
+            // If the reader returned nothing, then we're done. On the previous
+            // iteration we submitted the last bytes of the input. If the parser
+            // consumed all of those bytes, then the file perfectly parsed!
+            if size == 0 {
+                if fully_consumed {
+                    return Ok(parser.finish());
+                } else {
+                    return Err(SymbolError::ParseError(
+                        "unexpected EOF during parsing of SymbolFile (or a line was too long?) at line",
+                        parser.lines
+                    ));
+                }
+            }
+
+            // Ask the parser to parse more of the input
+            let input = buf.data();
+            let consumed = parser.parse_more(input)?;
+
+            // Give the other consumer of this Reader a chance to use this data.
+            callback(&input[..consumed]);
+
+            // Remember for the next iteration if all the input was consumed.
+            fully_consumed = input.len() == consumed;
+            buf.consume(consumed);
+        }
+    }
+
     // Parse a SymbolFile from bytes.
     pub fn from_bytes(bytes: &[u8]) -> Result<SymbolFile, SymbolError> {
         Self::parse(bytes, |_| ())
diff --git a/minidump-processor/Cargo.toml b/minidump-processor/Cargo.toml
index 0b41a73..ca056b8 100644
--- a/minidump-processor/Cargo.toml
+++ b/minidump-processor/Cargo.toml
@@ -20,6 +20,7 @@ breakpad-syms = ["breakpad-symbols"]
 symbolic-syms = []
 
 [dependencies]
+async-trait = "0.1.51"
 breakpad-symbols = { version = "0.9.6", path = "../breakpad-symbols", optional = true }
 clap = "2.34"
 log = "0.4"
@@ -33,4 +34,5 @@ thiserror = "1.0.30"
 [dev-dependencies]
 test-assembler = "0.1.6"
 synth-minidump = { path = "../synth-minidump" }
-doc-comment = "0.3.3"
\ No newline at end of file
+doc-comment = "0.3.3"
+tokio =  { version = "1.12.0", features = ["full"] }
diff --git a/minidump-processor/README.md b/minidump-processor/README.md
index bd34c36..26e85a6 100644
--- a/minidump-processor/README.md
+++ b/minidump-processor/README.md
@@ -20,7 +20,8 @@ use minidump::Minidump;
 use minidump_processor::{http_symbol_supplier, ProcessorOptions, Symbolizer};
 use serde_json::Value;
 
-fn main() -> Result<(), ()> {
+#[tokio::main]
+async fn main() -> Result<(), ()> {
     // Read the minidump
     let dump = Minidump::read_path("../testdata/test.dmp").map_err(|_| ())?;
  
@@ -45,6 +46,7 @@ fn main() -> Result<(), ()> {
     ));
  
     let state = minidump_processor::process_minidump_with_options(&dump, &provider, options)
+        .await
         .map_err(|_| ())?;
 
     // Write the JSON output to an arbitrary writer (here, a Vec).
diff --git a/minidump-processor/src/lib.rs b/minidump-processor/src/lib.rs
index ce36200..61ade2c 100644
--- a/minidump-processor/src/lib.rs
+++ b/minidump-processor/src/lib.rs
@@ -21,7 +21,8 @@
 //! use minidump_processor::{http_symbol_supplier, ProcessorOptions, Symbolizer};
 //! use serde_json::Value;
 //!  
-//! fn main() -> Result<(), ()> {
+//! #[tokio::main]
+//! async fn main() -> Result<(), ()> {
 //!     // Read the minidump
 //!     let dump = Minidump::read_path("../testdata/test.dmp").map_err(|_| ())?;
 //!  
@@ -46,6 +47,7 @@
 //!     ));
 //!  
 //!     let state = minidump_processor::process_minidump_with_options(&dump, &provider, options)
+//!         .await
 //!         .map_err(|_| ())?;
 //!  
 //!     // Write the JSON output to an arbitrary writer (here, a Vec).
diff --git a/minidump-processor/src/processor.rs b/minidump-processor/src/processor.rs
index 8fc5b8d..5723003 100644
--- a/minidump-processor/src/processor.rs
+++ b/minidump-processor/src/processor.rs
@@ -43,40 +43,41 @@ pub enum ProcessError {
 /// use minidump::Minidump;
 /// use std::path::PathBuf;
 /// use breakpad_symbols::{Symbolizer, SimpleSymbolSupplier};
+/// use minidump_processor::ProcessError;
 ///
-/// # std::env::set_current_dir(env!("CARGO_MANIFEST_DIR"));
-/// # fn foo() -> Result<(), minidump_processor::ProcessError> {
-/// let mut dump = Minidump::read_path("../testdata/test.dmp")?;
-/// let supplier = SimpleSymbolSupplier::new(vec!(PathBuf::from("../testdata/symbols")));
-/// let symbolizer = Symbolizer::new(supplier);
-/// let state = minidump_processor::process_minidump(&mut dump, &symbolizer)?;
-/// assert_eq!(state.threads.len(), 2);
-/// println!("Processed {} threads", state.threads.len());
-/// # Ok(())
-/// # }
-/// # fn main() { foo().unwrap() }
+/// #[tokio::main]
+/// async fn main() -> Result<(), ProcessError> {
+///     # std::env::set_current_dir(env!("CARGO_MANIFEST_DIR"));
+///     let mut dump = Minidump::read_path("../testdata/test.dmp")?;
+///     let supplier = SimpleSymbolSupplier::new(vec!(PathBuf::from("../testdata/symbols")));
+///     let symbolizer = Symbolizer::new(supplier);
+///     let state = minidump_processor::process_minidump(&mut dump, &symbolizer).await?;
+///     assert_eq!(state.threads.len(), 2);
+///     println!("Processed {} threads", state.threads.len());
+///     Ok(())
+/// }
 /// ```
-pub fn process_minidump<'a, T, P>(
+pub async fn process_minidump<'a, T, P>(
     dump: &Minidump<'a, T>,
     symbol_provider: &P,
 ) -> Result<ProcessState, ProcessError>
 where
     T: Deref<Target = [u8]> + 'a,
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     // No Evil JSON Here!
-    process_minidump_with_options(dump, symbol_provider, ProcessorOptions::default())
+    process_minidump_with_options(dump, symbol_provider, ProcessorOptions::default()).await
 }
 
 /// The same as [`process_minidump`] but with extra options.
-pub fn process_minidump_with_options<'a, T, P>(
+pub async fn process_minidump_with_options<'a, T, P>(
     dump: &Minidump<'a, T>,
     symbol_provider: &P,
-    options: ProcessorOptions,
+    options: ProcessorOptions<'_>,
 ) -> Result<ProcessState, ProcessError>
 where
     T: Deref<Target = [u8]> + 'a,
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     // Thread list is required for processing.
     let thread_list = dump
@@ -253,9 +254,8 @@ where
         let stack = thread.stack_memory(&memory_list);
 
         let mut stack =
-            stackwalker::walk_stack(&context, stack.as_deref(), &modules, symbol_provider);
+            stackwalker::walk_stack(&context, stack.as_deref(), &modules, symbol_provider).await;
         stack.thread_id = id;
-
         for frame in &mut stack.frames {
             // If the frame doesn't have a loaded module, try to find an unloaded module
             // that overlaps with its address range. The may be multiple, so record all
diff --git a/minidump-processor/src/stackwalker/amd64.rs b/minidump-processor/src/stackwalker/amd64.rs
index 30caeef..fc9eb66 100644
--- a/minidump-processor/src/stackwalker/amd64.rs
+++ b/minidump-processor/src/stackwalker/amd64.rs
@@ -26,16 +26,16 @@ const FRAME_POINTER_REGISTER: &str = "rbp";
 // FIXME: rdi and rsi are also preserved on windows (but not in sysv) -- we should handle that?
 const CALLEE_SAVED_REGS: &[&str] = &["rbx", "rbp", "r12", "r13", "r14", "r15"];
 
-fn get_caller_by_cfi<P>(
+async fn get_caller_by_cfi<P>(
     ctx: &CONTEXT_AMD64,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying cfi");
 
@@ -66,7 +66,9 @@ where
         stack_memory,
     };
 
-    symbol_provider.walk_frame(module, &mut stack_walker)?;
+    symbol_provider
+        .walk_frame(module, &mut stack_walker)
+        .await?;
     let caller_ip = stack_walker.caller_ctx.rip;
     let caller_sp = stack_walker.caller_ctx.rsp;
 
@@ -104,12 +106,12 @@ fn callee_forwarded_regs(valid: &MinidumpContextValidity) -> HashSet<&'static st
 fn get_caller_by_frame_pointer<P>(
     ctx: &CONTEXT_AMD64,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     _modules: &MinidumpModuleList,
     _symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying frame pointer");
     if let MinidumpContextValidity::Some(ref which) = callee.context.valid {
@@ -202,15 +204,15 @@ where
     Some(StackFrame::from_context(context, FrameTrust::FramePointer))
 }
 
-fn get_caller_by_scan<P>(
+async fn get_caller_by_scan<P>(
     ctx: &CONTEXT_AMD64,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying scan");
     // Stack scanning is just walking from the end of the frame until we encounter
@@ -250,7 +252,7 @@ where
     for i in 0..scan_range {
         let address_of_ip = last_sp.checked_add(i * POINTER_WIDTH)?;
         let caller_ip = stack_memory.get_memory_at_address(address_of_ip as u64)?;
-        if instruction_seems_valid(caller_ip, modules, symbol_provider) {
+        if instruction_seems_valid(caller_ip, modules, symbol_provider).await {
             // ip is pushed by CALL, so sp is just address_of_ip + ptr
             let caller_sp = address_of_ip.checked_add(POINTER_WIDTH)?;
 
@@ -352,25 +354,25 @@ where
 /// If we applied this more rigorous validation to cfi/fp methods, we
 /// would just discard the correct register values from the known frame
 /// and immediately start doing unreliable scans.
-fn instruction_seems_valid<P>(
+async fn instruction_seems_valid<P>(
     instruction: Pointer,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> bool
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     if is_non_canonical(instruction) || instruction == 0 {
         return false;
     }
 
-    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider)
+    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider).await
 }
 
 fn stack_seems_valid(
     caller_sp: Pointer,
     callee_sp: Pointer,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
 ) -> bool {
     // The stack shouldn't *grow* when we unwind
     if caller_sp <= callee_sp {
@@ -399,55 +401,62 @@ fn is_non_canonical(ptr: Pointer) -> bool {
     ptr > 0x7FFFFFFFFFFF && ptr < 0xFFFF800000000000
 }
 
+#[async_trait::async_trait]
 impl Unwind for CONTEXT_AMD64 {
-    fn get_caller_frame<P>(
+    async fn get_caller_frame<P>(
         &self,
         callee: &StackFrame,
         grand_callee: Option<&StackFrame>,
-        stack_memory: Option<&MinidumpMemory>,
+        stack_memory: Option<&MinidumpMemory<'_>>,
         modules: &MinidumpModuleList,
         syms: &P,
     ) -> Option<StackFrame>
     where
-        P: SymbolProvider,
+        P: SymbolProvider + Sync,
     {
-        stack_memory
-            .as_ref()
-            .and_then(|stack| {
-                get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms)
-                    .or_else(|| get_caller_by_frame_pointer(self, callee, stack, modules, syms))
-                    .or_else(|| get_caller_by_scan(self, callee, stack, modules, syms))
-            })
-            .and_then(|mut frame| {
-                // We now check the frame to see if it looks like unwinding is complete,
-                // based on the frame we computed having a nonsense value. Returning
-                // None signals to the unwinder to stop unwinding.
-
-                // if the instruction is within the first ~page of memory, it's basically
-                // null, and we can assume unwinding is complete.
-                if frame.context.get_instruction_pointer() < 4096 {
-                    trace!("unwind: instruction pointer was nullish, assuming unwind complete");
-                    return None;
-                }
-                // If the new stack pointer is at a lower address than the old,
-                // then that's clearly incorrect. Treat this as end-of-stack to
-                // enforce progress and avoid infinite loops.
-                if frame.context.get_stack_pointer() <= self.rsp {
-                    trace!("unwind: stack pointer went backwards, assuming unwind complete");
-                    return None;
-                }
+        let stack = stack_memory.as_ref()?;
+
+        // .await doesn't like closures, so don't use Option chaining
+        let mut frame = None;
+        if frame.is_none() {
+            frame = get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms).await;
+        }
+        if frame.is_none() {
+            frame = get_caller_by_frame_pointer(self, callee, stack, modules, syms);
+        }
+        if frame.is_none() {
+            frame = get_caller_by_scan(self, callee, stack, modules, syms).await;
+        }
+        let mut frame = frame?;
+
+        // We now check the frame to see if it looks like unwinding is complete,
+        // based on the frame we computed having a nonsense value. Returning
+        // None signals to the unwinder to stop unwinding.
+
+        // if the instruction is within the first ~page of memory, it's basically
+        // null, and we can assume unwinding is complete.
+        if frame.context.get_instruction_pointer() < 4096 {
+            trace!("unwind: instruction pointer was nullish, assuming unwind complete");
+            return None;
+        }
+        // If the new stack pointer is at a lower address than the old,
+        // then that's clearly incorrect. Treat this as end-of-stack to
+        // enforce progress and avoid infinite loops.
+        if frame.context.get_stack_pointer() <= self.rsp {
+            trace!("unwind: stack pointer went backwards, assuming unwind complete");
+            return None;
+        }
 
-                // Ok, the frame now seems well and truly valid, do final cleanup.
+        // Ok, the frame now seems well and truly valid, do final cleanup.
 
-                // A caller's ip is the return address, which is the instruction
-                // *after* the CALL that caused us to arrive at the callee. Set
-                // the value to one less than that, so it points within the
-                // CALL instruction. This is important because we use this value
-                // to lookup the CFI we need to unwind the next frame.
-                let ip = frame.context.get_instruction_pointer() as u64;
-                frame.instruction = ip - 1;
+        // A caller's ip is the return address, which is the instruction
+        // *after* the CALL that caused us to arrive at the callee. Set
+        // the value to one less than that, so it points within the
+        // CALL instruction. This is important because we use this value
+        // to lookup the CFI we need to unwind the next frame.
+        let ip = frame.context.get_instruction_pointer() as u64;
+        frame.instruction = ip - 1;
 
-                Some(frame)
-            })
+        Some(frame)
     }
 }
diff --git a/minidump-processor/src/stackwalker/arm.rs b/minidump-processor/src/stackwalker/arm.rs
index a34aa71..707517e 100644
--- a/minidump-processor/src/stackwalker/arm.rs
+++ b/minidump-processor/src/stackwalker/arm.rs
@@ -26,16 +26,16 @@ const PROGRAM_COUNTER: &str = Registers::ProgramCounter.name();
 const LINK_REGISTER: &str = Registers::LinkRegister.name();
 const CALLEE_SAVED_REGS: &[&str] = &["r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11"];
 
-fn get_caller_by_cfi<P>(
+async fn get_caller_by_cfi<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying cfi");
     let valid = &callee.context.valid;
@@ -59,7 +59,9 @@ where
         stack_memory,
     };
 
-    symbol_provider.walk_frame(module, &mut stack_walker)?;
+    symbol_provider
+        .walk_frame(module, &mut stack_walker)
+        .await?;
     let caller_pc = stack_walker.caller_ctx.get_register_always(PROGRAM_COUNTER);
     let caller_sp = stack_walker.caller_ctx.get_register_always(STACK_POINTER);
 
@@ -95,12 +97,12 @@ fn callee_forwarded_regs(valid: &MinidumpContextValidity) -> HashSet<&'static st
 fn get_caller_by_frame_pointer<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     _modules: &MinidumpModuleList,
     _symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying frame pointer");
     // Assume that the standard %fp-using ARM calling convention is in use.
@@ -171,15 +173,15 @@ where
     Some(StackFrame::from_context(context, FrameTrust::FramePointer))
 }
 
-fn get_caller_by_scan<P>(
+async fn get_caller_by_scan<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying scan");
     // Stack scanning is just walking from the end of the frame until we encounter
@@ -206,7 +208,7 @@ where
     for i in 0..scan_range {
         let address_of_pc = last_sp.checked_add(i * POINTER_WIDTH)?;
         let caller_pc = stack_memory.get_memory_at_address(address_of_pc as u64)?;
-        if instruction_seems_valid(caller_pc, modules, symbol_provider) {
+        if instruction_seems_valid(caller_pc, modules, symbol_provider).await {
             // pc is pushed by CALL, so sp is just address_of_pc + ptr
             let caller_sp = address_of_pc.checked_add(POINTER_WIDTH)?;
 
@@ -262,15 +264,15 @@ where
 /// would just discard the correct register values from the known frame
 /// and immediately start doing unreliable scans.
 #[allow(clippy::match_like_matches_macro)]
-fn instruction_seems_valid<P>(
+async fn instruction_seems_valid<P>(
     instruction: Pointer,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> bool
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
-    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider)
+    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider).await
 }
 
 /*
@@ -279,7 +281,7 @@ where
 fn stack_seems_valid(
     caller_sp: Pointer,
     callee_sp: Pointer,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
 ) -> bool {
     // The stack shouldn't *grow* when we unwind
     if caller_sp < callee_sp {
@@ -293,65 +295,72 @@ fn stack_seems_valid(
 }
 */
 
+#[async_trait::async_trait]
 impl Unwind for ArmContext {
-    fn get_caller_frame<P>(
+    async fn get_caller_frame<P>(
         &self,
         callee: &StackFrame,
         grand_callee: Option<&StackFrame>,
-        stack_memory: Option<&MinidumpMemory>,
+        stack_memory: Option<&MinidumpMemory<'_>>,
         modules: &MinidumpModuleList,
         syms: &P,
     ) -> Option<StackFrame>
     where
-        P: SymbolProvider,
+        P: SymbolProvider + Sync,
     {
-        stack_memory
-            .as_ref()
-            .and_then(|stack| {
-                get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms)
-                    .or_else(|| get_caller_by_frame_pointer(self, callee, stack, modules, syms))
-                    .or_else(|| get_caller_by_scan(self, callee, stack, modules, syms))
-            })
-            .and_then(|mut frame| {
-                // We now check the frame to see if it looks like unwinding is complete,
-                // based on the frame we computed having a nonsense value. Returning
-                // None signals to the unwinder to stop unwinding.
-
-                // if the instruction is within the first ~page of memory, it's basically
-                // null, and we can assume unwinding is complete.
-                if frame.context.get_instruction_pointer() < 4096 {
-                    trace!("unwind: instruction pointer was nullish, assuming unwind complete");
-                    return None;
-                }
-                // If the new stack pointer is at a lower address than the old,
-                // then that's clearly incorrect. Treat this as end-of-stack to
-                // enforce progress and avoid infinite loops.
-                let sp = frame.context.get_stack_pointer();
-                let last_sp = self.get_register_always("sp") as u64;
-                if sp <= last_sp {
-                    // Arm leaf functions may not actually touch the stack (thanks
-                    // to the link register allowing you to "push" the return address
-                    // to a register), so we need to permit the stack pointer to not
-                    // change for the first frame of the unwind. After that we need
-                    // more strict validation to avoid infinite loops.
-                    let is_leaf = callee.trust == FrameTrust::Context && sp == last_sp;
-                    if !is_leaf {
-                        trace!("unwind: stack pointer went backwards, assuming unwind complete");
-                        return None;
-                    }
-                }
-
-                // Ok, the frame now seems well and truly valid, do final cleanup.
-
-                // A caller's ip is the return address, which is the instruction
-                // *after* the CALL that caused us to arrive at the callee. Set
-                // the value to 2 less than that, so it points to the CALL instruction
-                // (arm instructions are all 2 bytes wide). This is important because
-                // we use this value to lookup the CFI we need to unwind the next frame.
-                let ip = frame.context.get_instruction_pointer() as u64;
-                frame.instruction = ip - 2;
-
-                Some(frame)
-            })
+        let stack = stack_memory.as_ref()?;
+
+        // .await doesn't like closures, so don't use Option chaining
+        let mut frame = None;
+        if frame.is_none() {
+            frame = get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms).await;
+        }
+        if frame.is_none() {
+            frame = get_caller_by_frame_pointer(self, callee, stack, modules, syms);
+        }
+        if frame.is_none() {
+            frame = get_caller_by_scan(self, callee, stack, modules, syms).await;
+        }
+        let mut frame = frame?;
+
+        // We now check the frame to see if it looks like unwinding is complete,
+        // based on the frame we computed having a nonsense value. Returning
+        // None signals to the unwinder to stop unwinding.
+
+        // if the instruction is within the first ~page of memory, it's basically
+        // null, and we can assume unwinding is complete.
+        if frame.context.get_instruction_pointer() < 4096 {
+            trace!("unwind: instruction pointer was nullish, assuming unwind complete");
+            return None;
+        }
+        // If the new stack pointer is at a lower address than the old,
+        // then that's clearly incorrect. Treat this as end-of-stack to
+        // enforce progress and avoid infinite loops.
+        let sp = frame.context.get_stack_pointer();
+        let last_sp = self.get_register_always("sp") as u64;
+        if sp <= last_sp {
+            // Arm leaf functions may not actually touch the stack (thanks
+            // to the link register allowing you to "push" the return address
+            // to a register), so we need to permit the stack pointer to not
+            // change for the first frame of the unwind. After that we need
+            // more strict validation to avoid infinite loops.
+            let is_leaf = callee.trust == FrameTrust::Context && sp == last_sp;
+            if !is_leaf {
+                trace!("unwind: stack pointer went backwards, assuming unwind complete");
+                return None;
+            }
+        }
+
+        // Ok, the frame now seems well and truly valid, do final cleanup.
+
+        // A caller's ip is the return address, which is the instruction
+        // *after* the CALL that caused us to arrive at the callee. Set
+        // the value to 2 less than that, so it points to the CALL instruction
+        // (arm instructions are all 2 bytes wide). This is important because
+        // we use this value to lookup the CFI we need to unwind the next frame.
+        let ip = frame.context.get_instruction_pointer() as u64;
+        frame.instruction = ip - 2;
+
+        Some(frame)
     }
 }
diff --git a/minidump-processor/src/stackwalker/arm64.rs b/minidump-processor/src/stackwalker/arm64.rs
index c2faeb9..a536c70 100644
--- a/minidump-processor/src/stackwalker/arm64.rs
+++ b/minidump-processor/src/stackwalker/arm64.rs
@@ -28,16 +28,16 @@ const CALLEE_SAVED_REGS: &[&str] = &[
     "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "x29",
 ];
 
-fn get_caller_by_cfi<P>(
+async fn get_caller_by_cfi<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying cfi");
 
@@ -62,7 +62,9 @@ where
         stack_memory,
     };
 
-    symbol_provider.walk_frame(module, &mut stack_walker)?;
+    symbol_provider
+        .walk_frame(module, &mut stack_walker)
+        .await?;
 
     let caller_pc = stack_walker.caller_ctx.get_register_always(PROGRAM_COUNTER);
     let caller_sp = stack_walker.caller_ctx.get_register_always(STACK_POINTER);
@@ -104,12 +106,12 @@ fn get_caller_by_frame_pointer<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     _symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying frame pointer");
     // Assume that the standard %fp-using ARM64 calling convention is in use.
@@ -202,7 +204,7 @@ where
 fn get_link_register_by_frame_pointer(
     ctx: &ArmContext,
     valid: &MinidumpContextValidity,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     grand_callee: Option<&StackFrame>,
     modules: &MinidumpModuleList,
 ) -> Option<Pointer> {
@@ -282,15 +284,15 @@ fn ptr_auth_strip(modules: &MinidumpModuleList, ptr: Pointer) -> Pointer {
     ptr
 }
 
-fn get_caller_by_scan<P>(
+async fn get_caller_by_scan<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying scan");
     // Stack scanning is just walking from the end of the frame until we encounter
@@ -317,7 +319,7 @@ where
     for i in 0..scan_range {
         let address_of_pc = last_sp.checked_add(i * POINTER_WIDTH)?;
         let caller_pc = stack_memory.get_memory_at_address(address_of_pc as u64)?;
-        if instruction_seems_valid(caller_pc, modules, symbol_provider) {
+        if instruction_seems_valid(caller_pc, modules, symbol_provider).await {
             // pc is pushed by CALL, so sp is just address_of_pc + ptr
             let caller_sp = address_of_pc.checked_add(POINTER_WIDTH)?;
 
@@ -372,19 +374,19 @@ where
 /// If we applied this more rigorous validation to cfi/fp methods, we
 /// would just discard the correct register values from the known frame
 /// and immediately start doing unreliable scans.
-fn instruction_seems_valid<P>(
+async fn instruction_seems_valid<P>(
     instruction: Pointer,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> bool
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     if is_non_canonical(instruction) || instruction == 0 {
         return false;
     }
 
-    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider)
+    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider).await
 }
 
 fn is_non_canonical(instruction: Pointer) -> bool {
@@ -398,7 +400,7 @@ fn is_non_canonical(instruction: Pointer) -> bool {
 fn stack_seems_valid(
     caller_sp: Pointer,
     callee_sp: Pointer,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
 ) -> bool {
     // The stack shouldn't *grow* when we unwind
     if caller_sp < callee_sp {
@@ -412,76 +414,74 @@ fn stack_seems_valid(
 }
 */
 
+#[async_trait::async_trait]
 impl Unwind for ArmContext {
-    fn get_caller_frame<P>(
+    async fn get_caller_frame<P>(
         &self,
         callee: &StackFrame,
         grand_callee: Option<&StackFrame>,
-        stack_memory: Option<&MinidumpMemory>,
+        stack_memory: Option<&MinidumpMemory<'_>>,
         modules: &MinidumpModuleList,
         syms: &P,
     ) -> Option<StackFrame>
     where
-        P: SymbolProvider,
+        P: SymbolProvider + Sync,
     {
-        stack_memory
-            .as_ref()
-            .and_then(|stack| {
-                get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms)
-                    .or_else(|| {
-                        get_caller_by_frame_pointer(
-                            self,
-                            callee,
-                            grand_callee,
-                            stack,
-                            modules,
-                            syms,
-                        )
-                    })
-                    .or_else(|| get_caller_by_scan(self, callee, stack, modules, syms))
-            })
-            .and_then(|mut frame| {
-                // We now check the frame to see if it looks like unwinding is complete,
-                // based on the frame we computed having a nonsense value. Returning
-                // None signals to the unwinder to stop unwinding.
-
-                // if the instruction is within the first ~page of memory, it's basically
-                // null, and we can assume unwinding is complete.
-                if frame.context.get_instruction_pointer() < 4096 {
-                    trace!("unwind: instruction pointer was nullish, assuming unwind complete");
-                    return None;
-                }
-
-                // If the new stack pointer is at a lower address than the old,
-                // then that's clearly incorrect. Treat this as end-of-stack to
-                // enforce progress and avoid infinite loops.
-
-                let sp = frame.context.get_stack_pointer();
-                let last_sp = self.get_register_always("sp") as u64;
-                if sp <= last_sp {
-                    // Arm leaf functions may not actually touch the stack (thanks
-                    // to the link register allowing you to "push" the return address
-                    // to a register), so we need to permit the stack pointer to not
-                    // change for the first frame of the unwind. After that we need
-                    // more strict validation to avoid infinite loops.
-                    let is_leaf = callee.trust == FrameTrust::Context && sp == last_sp;
-                    if !is_leaf {
-                        trace!("unwind: stack pointer went backwards, assuming unwind complete");
-                        return None;
-                    }
-                }
-
-                // Ok, the frame now seems well and truly valid, do final cleanup.
-
-                // A caller's ip is the return address, which is the instruction
-                // *after* the CALL that caused us to arrive at the callee. Set
-                // the value to 4 less than that, so it points to the CALL instruction
-                // (arm64 instructions are all 4 bytes wide). This is important because
-                // we use this value to lookup the CFI we need to unwind the next frame.
-                let ip = frame.context.get_instruction_pointer() as u64;
-                frame.instruction = ip - 4;
-
-                Some(frame)
-            })
+        let stack = stack_memory.as_ref()?;
+
+        // .await doesn't like closures, so don't use Option chaining
+        let mut frame = None;
+        if frame.is_none() {
+            frame = get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms).await;
+        }
+        if frame.is_none() {
+            frame = get_caller_by_frame_pointer(self, callee, grand_callee, stack, modules, syms);
+        }
+        if frame.is_none() {
+            frame = get_caller_by_scan(self, callee, stack, modules, syms).await;
+        }
+        let mut frame = frame?;
+
+        // We now check the frame to see if it looks like unwinding is complete,
+        // based on the frame we computed having a nonsense value. Returning
+        // None signals to the unwinder to stop unwinding.
+
+        // if the instruction is within the first ~page of memory, it's basically
+        // null, and we can assume unwinding is complete.
+        if frame.context.get_instruction_pointer() < 4096 {
+            trace!("unwind: instruction pointer was nullish, assuming unwind complete");
+            return None;
+        }
+
+        // If the new stack pointer is at a lower address than the old,
+        // then that's clearly incorrect. Treat this as end-of-stack to
+        // enforce progress and avoid infinite loops.
+
+        let sp = frame.context.get_stack_pointer();
+        let last_sp = self.get_register_always("sp") as u64;
+        if sp <= last_sp {
+            // Arm leaf functions may not actually touch the stack (thanks
+            // to the link register allowing you to "push" the return address
+            // to a register), so we need to permit the stack pointer to not
+            // change for the first frame of the unwind. After that we need
+            // more strict validation to avoid infinite loops.
+            let is_leaf = callee.trust == FrameTrust::Context && sp == last_sp;
+            if !is_leaf {
+                trace!("unwind: stack pointer went backwards, assuming unwind complete");
+                return None;
+            }
+        }
+
+        // Ok, the frame now seems well and truly valid, do final cleanup.
+
+        // A caller's ip is the return address, which is the instruction
+        // *after* the CALL that caused us to arrive at the callee. Set
+        // the value to 4 less than that, so it points to the CALL instruction
+        // (arm64 instructions are all 4 bytes wide). This is important because
+        // we use this value to lookup the CFI we need to unwind the next frame.
+        let ip = frame.context.get_instruction_pointer() as u64;
+        frame.instruction = ip - 4;
+
+        Some(frame)
     }
 }
diff --git a/minidump-processor/src/stackwalker/arm64_old.rs b/minidump-processor/src/stackwalker/arm64_old.rs
index b587760..7fdecc2 100644
--- a/minidump-processor/src/stackwalker/arm64_old.rs
+++ b/minidump-processor/src/stackwalker/arm64_old.rs
@@ -28,16 +28,16 @@ const CALLEE_SAVED_REGS: &[&str] = &[
     "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "x29",
 ];
 
-fn get_caller_by_cfi<P>(
+async fn get_caller_by_cfi<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying cfi");
 
@@ -62,7 +62,9 @@ where
         stack_memory,
     };
 
-    symbol_provider.walk_frame(module, &mut stack_walker)?;
+    symbol_provider
+        .walk_frame(module, &mut stack_walker)
+        .await?;
 
     let caller_pc = stack_walker.caller_ctx.get_register_always(PROGRAM_COUNTER);
     let caller_sp = stack_walker.caller_ctx.get_register_always(STACK_POINTER);
@@ -104,12 +106,12 @@ fn get_caller_by_frame_pointer<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     _symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying frame pointer");
     // Assume that the standard %fp-using ARM64 calling convention is in use.
@@ -202,7 +204,7 @@ where
 fn get_link_register_by_frame_pointer(
     ctx: &ArmContext,
     valid: &MinidumpContextValidity,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     grand_callee: Option<&StackFrame>,
     modules: &MinidumpModuleList,
 ) -> Option<Pointer> {
@@ -282,15 +284,15 @@ fn ptr_auth_strip(modules: &MinidumpModuleList, ptr: Pointer) -> Pointer {
     ptr
 }
 
-fn get_caller_by_scan<P>(
+async fn get_caller_by_scan<P>(
     ctx: &ArmContext,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying scan");
     // Stack scanning is just walking from the end of the frame until we encounter
@@ -317,7 +319,7 @@ where
     for i in 0..scan_range {
         let address_of_pc = last_sp.checked_add(i * POINTER_WIDTH)?;
         let caller_pc = stack_memory.get_memory_at_address(address_of_pc as u64)?;
-        if instruction_seems_valid(caller_pc, modules, symbol_provider) {
+        if instruction_seems_valid(caller_pc, modules, symbol_provider).await {
             // pc is pushed by CALL, so sp is just address_of_pc + ptr
             let caller_sp = address_of_pc.checked_add(POINTER_WIDTH)?;
 
@@ -372,19 +374,19 @@ where
 /// If we applied this more rigorous validation to cfi/fp methods, we
 /// would just discard the correct register values from the known frame
 /// and immediately start doing unreliable scans.
-fn instruction_seems_valid<P>(
+async fn instruction_seems_valid<P>(
     instruction: Pointer,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> bool
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     if is_non_canonical(instruction) || instruction == 0 {
         return false;
     }
 
-    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider)
+    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider).await
 }
 
 fn is_non_canonical(instruction: Pointer) -> bool {
@@ -398,7 +400,7 @@ fn is_non_canonical(instruction: Pointer) -> bool {
 fn stack_seems_valid(
     caller_sp: Pointer,
     callee_sp: Pointer,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
 ) -> bool {
     // The stack shouldn't *grow* when we unwind
     if caller_sp < callee_sp {
@@ -412,76 +414,74 @@ fn stack_seems_valid(
 }
 */
 
+#[async_trait::async_trait]
 impl Unwind for ArmContext {
-    fn get_caller_frame<P>(
+    async fn get_caller_frame<P>(
         &self,
         callee: &StackFrame,
         grand_callee: Option<&StackFrame>,
-        stack_memory: Option<&MinidumpMemory>,
+        stack_memory: Option<&MinidumpMemory<'_>>,
         modules: &MinidumpModuleList,
         syms: &P,
     ) -> Option<StackFrame>
     where
-        P: SymbolProvider,
+        P: SymbolProvider + Sync,
     {
-        stack_memory
-            .as_ref()
-            .and_then(|stack| {
-                get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms)
-                    .or_else(|| {
-                        get_caller_by_frame_pointer(
-                            self,
-                            callee,
-                            grand_callee,
-                            stack,
-                            modules,
-                            syms,
-                        )
-                    })
-                    .or_else(|| get_caller_by_scan(self, callee, stack, modules, syms))
-            })
-            .and_then(|mut frame| {
-                // We now check the frame to see if it looks like unwinding is complete,
-                // based on the frame we computed having a nonsense value. Returning
-                // None signals to the unwinder to stop unwinding.
-
-                // if the instruction is within the first ~page of memory, it's basically
-                // null, and we can assume unwinding is complete.
-                if frame.context.get_instruction_pointer() < 4096 {
-                    trace!("unwind: instruction pointer was nullish, assuming unwind complete");
-                    return None;
-                }
-
-                // If the new stack pointer is at a lower address than the old,
-                // then that's clearly incorrect. Treat this as end-of-stack to
-                // enforce progress and avoid infinite loops.
-
-                let sp = frame.context.get_stack_pointer();
-                let last_sp = self.get_register_always("sp") as u64;
-                if sp <= last_sp {
-                    // Arm leaf functions may not actually touch the stack (thanks
-                    // to the link register allowing you to "push" the return address
-                    // to a register), so we need to permit the stack pointer to not
-                    // change for the first frame of the unwind. After that we need
-                    // more strict validation to avoid infinite loops.
-                    let is_leaf = callee.trust == FrameTrust::Context && sp == last_sp;
-                    if !is_leaf {
-                        trace!("unwind: stack pointer went backwards, assuming unwind complete");
-                        return None;
-                    }
-                }
-
-                // Ok, the frame now seems well and truly valid, do final cleanup.
-
-                // A caller's ip is the return address, which is the instruction
-                // *after* the CALL that caused us to arrive at the callee. Set
-                // the value to 4 less than that, so it points to the CALL instruction
-                // (arm64 instructions are all 4 bytes wide). This is important because
-                // we use this value to lookup the CFI we need to unwind the next frame.
-                let ip = frame.context.get_instruction_pointer() as u64;
-                frame.instruction = ip - 4;
-
-                Some(frame)
-            })
+        let stack = stack_memory.as_ref()?;
+
+        // .await doesn't like closures, so don't use Option chaining
+        let mut frame = None;
+        if frame.is_none() {
+            frame = get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms).await;
+        }
+        if frame.is_none() {
+            frame = get_caller_by_frame_pointer(self, callee, grand_callee, stack, modules, syms);
+        }
+        if frame.is_none() {
+            frame = get_caller_by_scan(self, callee, stack, modules, syms).await;
+        }
+        let mut frame = frame?;
+
+        // We now check the frame to see if it looks like unwinding is complete,
+        // based on the frame we computed having a nonsense value. Returning
+        // None signals to the unwinder to stop unwinding.
+
+        // if the instruction is within the first ~page of memory, it's basically
+        // null, and we can assume unwinding is complete.
+        if frame.context.get_instruction_pointer() < 4096 {
+            trace!("unwind: instruction pointer was nullish, assuming unwind complete");
+            return None;
+        }
+
+        // If the new stack pointer is at a lower address than the old,
+        // then that's clearly incorrect. Treat this as end-of-stack to
+        // enforce progress and avoid infinite loops.
+
+        let sp = frame.context.get_stack_pointer();
+        let last_sp = self.get_register_always("sp") as u64;
+        if sp <= last_sp {
+            // Arm leaf functions may not actually touch the stack (thanks
+            // to the link register allowing you to "push" the return address
+            // to a register), so we need to permit the stack pointer to not
+            // change for the first frame of the unwind. After that we need
+            // more strict validation to avoid infinite loops.
+            let is_leaf = callee.trust == FrameTrust::Context && sp == last_sp;
+            if !is_leaf {
+                trace!("unwind: stack pointer went backwards, assuming unwind complete");
+                return None;
+            }
+        }
+
+        // Ok, the frame now seems well and truly valid, do final cleanup.
+
+        // A caller's ip is the return address, which is the instruction
+        // *after* the CALL that caused us to arrive at the callee. Set
+        // the value to 4 less than that, so it points to the CALL instruction
+        // (arm64 instructions are all 4 bytes wide). This is important because
+        // we use this value to lookup the CFI we need to unwind the next frame.
+        let ip = frame.context.get_instruction_pointer() as u64;
+        frame.instruction = ip - 4;
+
+        Some(frame)
     }
 }
diff --git a/minidump-processor/src/stackwalker/mod.rs b/minidump-processor/src/stackwalker/mod.rs
index 517da63..92503b5 100644
--- a/minidump-processor/src/stackwalker/mod.rs
+++ b/minidump-processor/src/stackwalker/mod.rs
@@ -80,15 +80,15 @@ where
     }
 }
 
-fn get_caller_frame<P>(
+async fn get_caller_frame<P>(
     callee_frame: &StackFrame,
     grand_callee_frame: Option<&StackFrame>,
-    stack_memory: Option<&MinidumpMemory>,
+    stack_memory: Option<&MinidumpMemory<'_>>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     match callee_frame.context.raw {
         /*
@@ -97,51 +97,66 @@ where
         MinidumpRawContext::SPARC(ctx) => ctx.get_caller_frame(stack_memory),
         MinidumpRawContext::MIPS(ctx) => ctx.get_caller_frame(stack_memory),
          */
-        MinidumpRawContext::Arm(ref ctx) => ctx.get_caller_frame(
-            callee_frame,
-            grand_callee_frame,
-            stack_memory,
-            modules,
-            symbol_provider,
-        ),
-        MinidumpRawContext::Arm64(ref ctx) => ctx.get_caller_frame(
-            callee_frame,
-            grand_callee_frame,
-            stack_memory,
-            modules,
-            symbol_provider,
-        ),
-        MinidumpRawContext::OldArm64(ref ctx) => ctx.get_caller_frame(
-            callee_frame,
-            grand_callee_frame,
-            stack_memory,
-            modules,
-            symbol_provider,
-        ),
-        MinidumpRawContext::Amd64(ref ctx) => ctx.get_caller_frame(
-            callee_frame,
-            grand_callee_frame,
-            stack_memory,
-            modules,
-            symbol_provider,
-        ),
-        MinidumpRawContext::X86(ref ctx) => ctx.get_caller_frame(
-            callee_frame,
-            grand_callee_frame,
-            stack_memory,
-            modules,
-            symbol_provider,
-        ),
+        MinidumpRawContext::Arm(ref ctx) => {
+            ctx.get_caller_frame(
+                callee_frame,
+                grand_callee_frame,
+                stack_memory,
+                modules,
+                symbol_provider,
+            )
+            .await
+        }
+        MinidumpRawContext::Arm64(ref ctx) => {
+            ctx.get_caller_frame(
+                callee_frame,
+                grand_callee_frame,
+                stack_memory,
+                modules,
+                symbol_provider,
+            )
+            .await
+        }
+        MinidumpRawContext::OldArm64(ref ctx) => {
+            ctx.get_caller_frame(
+                callee_frame,
+                grand_callee_frame,
+                stack_memory,
+                modules,
+                symbol_provider,
+            )
+            .await
+        }
+        MinidumpRawContext::Amd64(ref ctx) => {
+            ctx.get_caller_frame(
+                callee_frame,
+                grand_callee_frame,
+                stack_memory,
+                modules,
+                symbol_provider,
+            )
+            .await
+        }
+        MinidumpRawContext::X86(ref ctx) => {
+            ctx.get_caller_frame(
+                callee_frame,
+                grand_callee_frame,
+                stack_memory,
+                modules,
+                symbol_provider,
+            )
+            .await
+        }
         _ => None,
     }
 }
 
-fn fill_source_line_info<P>(
+async fn fill_source_line_info<P>(
     frame: &mut StackFrame,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     // Find the module whose address range covers this frame's instruction.
     if let Some(module) = modules.module_at_address(frame.instruction) {
@@ -150,18 +165,18 @@ fn fill_source_line_info<P>(
         frame.module = Some(module.clone());
 
         // This is best effort, so ignore any errors.
-        let _ = symbol_provider.fill_symbol(module, frame);
+        let _ = symbol_provider.fill_symbol(module, frame).await;
     }
 }
 
-pub fn walk_stack<P>(
+pub async fn walk_stack<P>(
     maybe_context: &Option<&MinidumpContext>,
-    stack_memory: Option<&MinidumpMemory>,
+    stack_memory: Option<&MinidumpMemory<'_>>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> CallStack
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     // Begin with the context frame, and keep getting callers until there are
     // no more.
@@ -172,7 +187,7 @@ where
         let ctx = context.clone();
         let mut maybe_frame = Some(StackFrame::from_context(ctx, FrameTrust::Context));
         while let Some(mut frame) = maybe_frame {
-            fill_source_line_info(&mut frame, modules, symbol_provider);
+            fill_source_line_info(&mut frame, modules, symbol_provider).await;
             trace!(
                 "unwind: unwinding {}",
                 frame
@@ -189,7 +204,8 @@ where
                 stack_memory,
                 modules,
                 symbol_provider,
-            );
+            )
+            .await;
         }
         trace!("unwind: finished stack unwind\n");
     } else {
@@ -206,13 +222,13 @@ where
 
 /// Checks if we can dismiss the validity of an instruction based on our symbols,
 /// to refine the quality of each unwinder's instruction_seems_valid implementation.
-fn instruction_seems_valid_by_symbols<P>(
+async fn instruction_seems_valid_by_symbols<P>(
     instruction: u64,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> bool
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     if let Some(module) = modules.module_at_address(instruction as u64) {
         // Create a dummy frame symbolizing implementation to feed into
@@ -242,7 +258,11 @@ where
             has_name: false,
         };
 
-        if symbol_provider.fill_symbol(module, &mut frame).is_ok() {
+        if symbol_provider
+            .fill_symbol(module, &mut frame)
+            .await
+            .is_ok()
+        {
             frame.has_name
         } else {
             // If the symbol provider returns an Error, this means that we
diff --git a/minidump-processor/src/stackwalker/unwind.rs b/minidump-processor/src/stackwalker/unwind.rs
index cb7d3bd..480e25f 100644
--- a/minidump-processor/src/stackwalker/unwind.rs
+++ b/minidump-processor/src/stackwalker/unwind.rs
@@ -6,16 +6,17 @@ use crate::SymbolProvider;
 use minidump::{MinidumpMemory, MinidumpModuleList};
 
 /// A trait for things that can unwind to a caller.
+#[async_trait::async_trait]
 pub trait Unwind {
     /// Get the caller frame of this frame.
-    fn get_caller_frame<P>(
+    async fn get_caller_frame<P>(
         &self,
         callee: &StackFrame,
         grand_callee: Option<&StackFrame>,
-        stack_memory: Option<&MinidumpMemory>,
+        stack_memory: Option<&MinidumpMemory<'_>>,
         modules: &MinidumpModuleList,
         symbol_provider: &P,
     ) -> Option<StackFrame>
     where
-        P: SymbolProvider;
+        P: SymbolProvider + Sync;
 }
diff --git a/minidump-processor/src/stackwalker/x86.rs b/minidump-processor/src/stackwalker/x86.rs
index 11db053..ebecb4e 100644
--- a/minidump-processor/src/stackwalker/x86.rs
+++ b/minidump-processor/src/stackwalker/x86.rs
@@ -25,16 +25,16 @@ const STACK_POINTER_REGISTER: &str = "esp";
 const FRAME_POINTER_REGISTER: &str = "ebp";
 const CALLEE_SAVED_REGS: &[&str] = &["ebp", "ebx", "edi", "esi"];
 
-fn get_caller_by_cfi<P>(
+async fn get_caller_by_cfi<P>(
     ctx: &CONTEXT_X86,
     callee: &StackFrame,
     grand_callee: Option<&StackFrame>,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying cfi");
 
@@ -65,7 +65,9 @@ where
         stack_memory,
     };
 
-    symbol_provider.walk_frame(module, &mut stack_walker)?;
+    symbol_provider
+        .walk_frame(module, &mut stack_walker)
+        .await?;
     let caller_ip = stack_walker.caller_ctx.eip;
     let caller_sp = stack_walker.caller_ctx.esp;
 
@@ -116,12 +118,12 @@ fn callee_forwarded_regs(valid: &MinidumpContextValidity) -> HashSet<&'static st
 fn get_caller_by_frame_pointer<P>(
     ctx: &CONTEXT_X86,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     _modules: &MinidumpModuleList,
     _symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying frame pointer");
     if let MinidumpContextValidity::Some(ref which) = callee.context.valid {
@@ -193,15 +195,15 @@ where
     Some(StackFrame::from_context(context, FrameTrust::FramePointer))
 }
 
-fn get_caller_by_scan<P>(
+async fn get_caller_by_scan<P>(
     ctx: &CONTEXT_X86,
     callee: &StackFrame,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> Option<StackFrame>
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     trace!("unwind: trying scan");
     // Stack scanning is just walking from the end of the frame until we encounter
@@ -241,7 +243,7 @@ where
     for i in 0..scan_range {
         let address_of_ip = last_sp.checked_add(i * POINTER_WIDTH)?;
         let caller_ip = stack_memory.get_memory_at_address(address_of_ip as u64)?;
-        if instruction_seems_valid(caller_ip, modules, symbol_provider) {
+        if instruction_seems_valid(caller_ip, modules, symbol_provider).await {
             // ip is pushed by CALL, so sp is just address_of_ip + ptr
             let caller_sp = address_of_ip.checked_add(POINTER_WIDTH)?;
 
@@ -340,19 +342,19 @@ where
 /// If we applied this more rigorous validation to cfi/fp methods, we
 /// would just discard the correct register values from the known frame
 /// and immediately start doing unreliable scans.
-fn instruction_seems_valid<P>(
+async fn instruction_seems_valid<P>(
     instruction: Pointer,
     modules: &MinidumpModuleList,
     symbol_provider: &P,
 ) -> bool
 where
-    P: SymbolProvider,
+    P: SymbolProvider + Sync,
 {
     if instruction == 0 {
         return false;
     }
 
-    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider)
+    super::instruction_seems_valid_by_symbols(instruction as u64, modules, symbol_provider).await
 }
 
 /*
@@ -361,7 +363,7 @@ where
 fn stack_seems_valid(
     caller_sp: Pointer,
     callee_sp: Pointer,
-    stack_memory: &MinidumpMemory,
+    stack_memory: &MinidumpMemory<'_>,
 ) -> bool {
     // The stack shouldn't *grow* when we unwind
     if caller_sp <= callee_sp {
@@ -375,55 +377,62 @@ fn stack_seems_valid(
 }
 */
 
+#[async_trait::async_trait]
 impl Unwind for CONTEXT_X86 {
-    fn get_caller_frame<P>(
+    async fn get_caller_frame<P>(
         &self,
         callee: &StackFrame,
         grand_callee: Option<&StackFrame>,
-        stack_memory: Option<&MinidumpMemory>,
+        stack_memory: Option<&MinidumpMemory<'_>>,
         modules: &MinidumpModuleList,
         syms: &P,
     ) -> Option<StackFrame>
     where
-        P: SymbolProvider,
+        P: SymbolProvider + Sync,
     {
-        stack_memory
-            .as_ref()
-            .and_then(|stack| {
-                get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms)
-                    .or_else(|| get_caller_by_frame_pointer(self, callee, stack, modules, syms))
-                    .or_else(|| get_caller_by_scan(self, callee, stack, modules, syms))
-            })
-            .and_then(|mut frame| {
-                // We now check the frame to see if it looks like unwinding is complete,
-                // based on the frame we computed having a nonsense value. Returning
-                // None signals to the unwinder to stop unwinding.
-
-                // if the instruction is within the first ~page of memory, it's basically
-                // null, and we can assume unwinding is complete.
-                if frame.context.get_instruction_pointer() < 4096 {
-                    trace!("unwind: instruction pointer was nullish, assuming unwind complete");
-                    return None;
-                }
-                // If the new stack pointer is at a lower address than the old,
-                // then that's clearly incorrect. Treat this as end-of-stack to
-                // enforce progress and avoid infinite loops.
-                if frame.context.get_stack_pointer() <= self.esp as u64 {
-                    trace!("unwind: stack pointer went backwards, assuming unwind complete");
-                    return None;
-                }
+        let stack = stack_memory.as_ref()?;
+
+        // .await doesn't like closures, so don't use Option chaining
+        let mut frame = None;
+        if frame.is_none() {
+            frame = get_caller_by_cfi(self, callee, grand_callee, stack, modules, syms).await;
+        }
+        if frame.is_none() {
+            frame = get_caller_by_frame_pointer(self, callee, stack, modules, syms);
+        }
+        if frame.is_none() {
+            frame = get_caller_by_scan(self, callee, stack, modules, syms).await;
+        }
+        let mut frame = frame?;
+
+        // We now check the frame to see if it looks like unwinding is complete,
+        // based on the frame we computed having a nonsense value. Returning
+        // None signals to the unwinder to stop unwinding.
+
+        // if the instruction is within the first ~page of memory, it's basically
+        // null, and we can assume unwinding is complete.
+        if frame.context.get_instruction_pointer() < 4096 {
+            trace!("unwind: instruction pointer was nullish, assuming unwind complete");
+            return None;
+        }
+        // If the new stack pointer is at a lower address than the old,
+        // then that's clearly incorrect. Treat this as end-of-stack to
+        // enforce progress and avoid infinite loops.
+        if frame.context.get_stack_pointer() <= self.esp as u64 {
+            trace!("unwind: stack pointer went backwards, assuming unwind complete");
+            return None;
+        }
 
-                // Ok, the frame now seems well and truly valid, do final cleanup.
+        // Ok, the frame now seems well and truly valid, do final cleanup.
 
-                // A caller's ip is the return address, which is the instruction
-                // *after* the CALL that caused us to arrive at the callee. Set
-                // the value to one less than that, so it points within the
-                // CALL instruction. This is important because we use this value
-                // to lookup the CFI we need to unwind the next frame.
-                let ip = frame.context.get_instruction_pointer() as u64;
-                frame.instruction = ip - 1;
+        // A caller's ip is the return address, which is the instruction
+        // *after* the CALL that caused us to arrive at the callee. Set
+        // the value to one less than that, so it points within the
+        // CALL instruction. This is important because we use this value
+        // to lookup the CFI we need to unwind the next frame.
+        let ip = frame.context.get_instruction_pointer() as u64;
+        frame.instruction = ip - 1;
 
-                Some(frame)
-            })
+        Some(frame)
     }
 }
diff --git a/minidump-processor/src/symbols.rs b/minidump-processor/src/symbols.rs
index 56398cf..e35e559 100644
--- a/minidump-processor/src/symbols.rs
+++ b/minidump-processor/src/symbols.rs
@@ -65,7 +65,8 @@
 //! use minidump::Minidump;
 //! use minidump_processor::{http_symbol_supplier, ProcessorOptions, Symbolizer};
 //!
-//! fn main() -> Result<(), ()> {
+//! #[tokio::main]
+//! async fn main() -> Result<(), ()> {
 //!     // Read the minidump
 //!     let dump = Minidump::read_path("../testdata/test.dmp").map_err(|_| ())?;
 //!
@@ -87,29 +88,37 @@
 //!     ));
 //!
 //!     let state = minidump_processor::process_minidump_with_options(&dump, &provider, options)
+//!         .await
 //!         .map_err(|_| ())?;
 //!     state.print(&mut std::io::stdout()).map_err(|_| ())?;
 //!     Ok(())
 //! }
 //! ```
 //!
+
+use async_trait::async_trait;
 use minidump::Module;
 use std::collections::HashMap;
 pub use symbols_shim::*;
 
+#[async_trait]
 pub trait SymbolProvider {
-    fn fill_symbol(
+    async fn fill_symbol(
         &self,
-        module: &dyn Module,
-        frame: &mut dyn FrameSymbolizer,
+        module: &(dyn Module + Sync),
+        frame: &mut (dyn FrameSymbolizer + Send),
     ) -> Result<(), FillSymbolError>;
-    fn walk_frame(&self, module: &dyn Module, walker: &mut dyn FrameWalker) -> Option<()>;
+    async fn walk_frame(
+        &self,
+        module: &(dyn Module + Sync),
+        walker: &mut (dyn FrameWalker + Send),
+    ) -> Option<()>;
     fn stats(&self) -> HashMap<String, SymbolStats>;
 }
 
 #[derive(Default)]
 pub struct MultiSymbolProvider {
-    providers: Vec<Box<dyn SymbolProvider>>,
+    providers: Vec<Box<dyn SymbolProvider + Send + Sync>>,
 }
 
 impl MultiSymbolProvider {
@@ -117,31 +126,36 @@ impl MultiSymbolProvider {
         Default::default()
     }
 
-    pub fn add(&mut self, provider: Box<dyn SymbolProvider>) {
+    pub fn add(&mut self, provider: Box<dyn SymbolProvider + Send + Sync>) {
         self.providers.push(provider);
     }
 }
 
+#[async_trait]
 impl SymbolProvider for MultiSymbolProvider {
-    fn fill_symbol(
+    async fn fill_symbol(
         &self,
-        module: &dyn Module,
-        frame: &mut dyn FrameSymbolizer,
+        module: &(dyn Module + Sync),
+        frame: &mut (dyn FrameSymbolizer + Send),
     ) -> Result<(), FillSymbolError> {
         // Return Ok if *any* symbol provider came back with Ok, so that the user can
         // distinguish between having no symbols at all and just not being able to
         // symbolize this particular frame.
         let mut best_result = Err(FillSymbolError {});
         for p in self.providers.iter() {
-            let new_result = p.fill_symbol(module, frame);
+            let new_result = p.fill_symbol(module, frame).await;
             best_result = best_result.or(new_result);
         }
         best_result
     }
 
-    fn walk_frame(&self, module: &dyn Module, walker: &mut dyn FrameWalker) -> Option<()> {
+    async fn walk_frame(
+        &self,
+        module: &(dyn Module + Sync),
+        walker: &mut (dyn FrameWalker + Send),
+    ) -> Option<()> {
         for p in self.providers.iter() {
-            let result = p.walk_frame(module, walker);
+            let result = p.walk_frame(module, walker).await;
             if result.is_some() {
                 return result;
             }
@@ -163,6 +177,7 @@ impl SymbolProvider for MultiSymbolProvider {
 #[cfg(feature = "breakpad-syms")]
 mod symbols_shim {
     use super::SymbolProvider;
+    use async_trait::async_trait;
     pub use breakpad_symbols::{
         FillSymbolError, FrameSymbolizer, FrameWalker, SymbolError, SymbolFile, SymbolStats,
         SymbolSupplier, Symbolizer,
@@ -171,16 +186,22 @@ mod symbols_shim {
     use std::collections::HashMap;
     use std::path::PathBuf;
     use std::time::Duration;
+
+    #[async_trait]
     impl SymbolProvider for Symbolizer {
-        fn fill_symbol(
+        async fn fill_symbol(
             &self,
-            module: &dyn Module,
-            frame: &mut dyn FrameSymbolizer,
+            module: &(dyn Module + Sync),
+            frame: &mut (dyn FrameSymbolizer + Send),
         ) -> Result<(), FillSymbolError> {
-            self.fill_symbol(module, frame)
+            self.fill_symbol(module, frame).await
         }
-        fn walk_frame(&self, module: &dyn Module, walker: &mut dyn FrameWalker) -> Option<()> {
-            self.walk_frame(module, walker)
+        async fn walk_frame(
+            &self,
+            module: &(dyn Module + Sync),
+            walker: &mut (dyn FrameWalker + Send),
+        ) -> Option<()> {
+            self.walk_frame(module, walker).await
         }
         fn stats(&self) -> HashMap<String, SymbolStats> {
             self.stats()
@@ -246,6 +267,7 @@ mod symbols_shim {
     #![allow(dead_code)]
 
     use super::SymbolProvider;
+    use async_trait::async_trait;
     use minidump::Module;
     use std::collections::HashMap;
     use std::path::PathBuf;
@@ -254,12 +276,16 @@ mod symbols_shim {
     // Import symbolic here
 
     /// A trait for things that can locate symbols for a given module.
+    #[async_trait]
     pub trait SymbolSupplier {
         /// Locate and load a symbol file for `module`.
         ///
         /// Implementations may use any strategy for locating and loading
         /// symbols.
-        fn locate_symbols(&mut self, module: &dyn Module) -> Result<SymbolFile, SymbolError>;
+        async fn locate_symbols(
+            &mut self,
+            module: &(dyn Module + Sync),
+        ) -> Result<SymbolFile, SymbolError>;
     }
 
     /// A trait for setting symbol information on something like a stack frame.
@@ -315,27 +341,32 @@ mod symbols_shim {
     /// [fill_symbol]: struct.Symbolizer.html#method.fill_symbol
     pub struct Symbolizer {
         /// Symbol supplier for locating symbols.
-        supplier: Box<dyn SymbolSupplier + 'static>,
+        supplier: Box<dyn SymbolSupplier + 'static + Send + Sync>,
     }
 
     impl Symbolizer {
         /// Create a `Symbolizer` that uses `supplier` to locate symbols.
-        pub fn new<T: SymbolSupplier + 'static>(supplier: T) -> Symbolizer {
+        pub fn new<T: SymbolSupplier + 'static + Send + Sync>(supplier: T) -> Symbolizer {
             Symbolizer {
                 supplier: Box::new(supplier),
             }
         }
     }
 
+    #[async_trait]
     impl SymbolProvider for Symbolizer {
-        fn fill_symbol(
+        async fn fill_symbol(
             &self,
-            _module: &dyn Module,
-            _frame: &mut dyn FrameSymbolizer,
+            _module: &(dyn Module + Sync),
+            _frame: &mut (dyn FrameSymbolizer + Send),
         ) -> Result<(), FillSymbolError> {
             unimplemented!()
         }
-        fn walk_frame(&self, _module: &dyn Module, _walker: &mut dyn FrameWalker) -> Option<()> {
+        async fn walk_frame(
+            &self,
+            _module: &(dyn Module + Sync),
+            _walker: &mut (dyn FrameWalker + Send),
+        ) -> Option<()> {
             unimplemented!()
         }
         fn stats(&self) -> HashMap<String, SymbolStats> {
@@ -454,20 +485,32 @@ mod symbols_shim {
 
     struct StringSymbolSupplier {}
 
+    #[async_trait]
     impl SymbolSupplier for HttpSymbolSupplier {
-        fn locate_symbols(&mut self, _module: &dyn Module) -> Result<SymbolFile, SymbolError> {
+        async fn locate_symbols(
+            &mut self,
+            _module: &(dyn Module + Sync),
+        ) -> Result<SymbolFile, SymbolError> {
             unimplemented!()
         }
     }
 
+    #[async_trait]
     impl SymbolSupplier for SimpleSymbolSupplier {
-        fn locate_symbols(&mut self, _module: &dyn Module) -> Result<SymbolFile, SymbolError> {
+        async fn locate_symbols(
+            &mut self,
+            _module: &(dyn Module + Sync),
+        ) -> Result<SymbolFile, SymbolError> {
             unimplemented!()
         }
     }
 
+    #[async_trait]
     impl SymbolSupplier for StringSymbolSupplier {
-        fn locate_symbols(&mut self, _module: &dyn Module) -> Result<SymbolFile, SymbolError> {
+        async fn locate_symbols(
+            &mut self,
+            _module: &(dyn Module + Sync),
+        ) -> Result<SymbolFile, SymbolError> {
             unimplemented!()
         }
     }
diff --git a/minidump-stackwalk/Cargo.toml b/minidump-stackwalk/Cargo.toml
index ee340f9..9b2a4cd 100644
--- a/minidump-stackwalk/Cargo.toml
+++ b/minidump-stackwalk/Cargo.toml
@@ -20,6 +20,7 @@ log = "0.4"
 minidump = { version = "0.9.6", path = "../minidump" }
 minidump-processor = { version = "0.9.6", path = "../minidump-processor" }
 simplelog = "0.11.2"
+tokio =  { version = "1.12.0", features = ["full"] }
 
 [features]
 vendored-openssl = ["openssl/vendored"]
diff --git a/minidump-stackwalk/src/main.rs b/minidump-stackwalk/src/main.rs
index 1bba66d..82918fe 100644
--- a/minidump-stackwalk/src/main.rs
+++ b/minidump-stackwalk/src/main.rs
@@ -221,7 +221,8 @@ native debuginfo formats. We recommend using a version of dump_syms to generate
 }
 
 #[cfg_attr(test, allow(dead_code))]
-fn main() {
+#[tokio::main]
+async fn main() {
     let matches = make_app().get_matches();
 
     // This is a little hack to generate a markdown version of the --help message,
@@ -388,7 +389,8 @@ fn main() {
                 ))));
             }
 
-            match minidump_processor::process_minidump_with_options(&dump, &provider, options) {
+            match minidump_processor::process_minidump_with_options(&dump, &provider, options).await
+            {
                 Ok(state) => {
                     let mut stdout;
                     let mut output_f;


Test patch:
diff --git a/minidump-processor/src/stackwalker/amd64_unittest.rs b/minidump-processor/src/stackwalker/amd64_unittest.rs
index 1b672b5..6964eb9 100644
--- a/minidump-processor/src/stackwalker/amd64_unittest.rs
+++ b/minidump-processor/src/stackwalker/amd64_unittest.rs
@@ -29,7 +29,7 @@ impl TestFixture {
         }
     }
 
-    pub fn walk_stack(&self, stack: Section) -> CallStack {
+    pub async fn walk_stack(&self, stack: Section) -> CallStack {
         let context = MinidumpContext {
             raw: MinidumpRawContext::Amd64(self.raw.clone()),
             valid: MinidumpContextValidity::All,
@@ -50,6 +50,7 @@ impl TestFixture {
             &self.modules,
             &symbolizer,
         )
+        .await
     }
 
     pub fn add_symbols(&mut self, name: String, symbols: String) {
@@ -57,8 +58,8 @@ impl TestFixture {
     }
 }
 
-#[test]
-fn test_simple() {
+#[tokio::test]
+async fn test_simple() {
     let mut f = TestFixture::new();
     let stack = Section::new();
     stack.start().set_const(0x80000000);
@@ -69,15 +70,15 @@ fn test_simple() {
     f.raw.rip = 0x00007400c0000200;
     f.raw.rbp = 0x8000000080000000;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
     let f = &s.frames[0];
     let m = f.module.as_ref().unwrap();
     assert_eq!(m.code_file(), "module1");
 }
 
-#[test]
-fn test_caller_pushed_rbp() {
+#[tokio::test]
+async fn test_caller_pushed_rbp() {
     // Functions typically push their %rbp upon entry and set %rbp pointing
     // there.  If stackwalking finds a plausible address for the next frame's
     // %rbp directly below the return address, assume that it is indeed the
@@ -112,7 +113,7 @@ fn test_caller_pushed_rbp() {
     f.raw.rbp = frame0_rbp.value().unwrap();
     f.raw.rsp = stack.start().value().unwrap();
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -148,8 +149,8 @@ fn test_caller_pushed_rbp() {
     }
 }
 
-#[test]
-fn test_scan_without_symbols() {
+#[tokio::test]
+async fn test_scan_without_symbols() {
     // When the stack walker resorts to scanning the stack,
     // only addresses located within loaded modules are
     // considered valid return addresses.
@@ -190,7 +191,7 @@ fn test_scan_without_symbols() {
     f.raw.rbp = frame1_rbp.value().unwrap();
     f.raw.rsp = stack.start().value().unwrap();
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -241,8 +242,8 @@ fn test_scan_without_symbols() {
     }
 }
 
-#[test]
-fn test_scan_with_symbols() {
+#[tokio::test]
+async fn test_scan_with_symbols() {
     // Test that we can refine our scanning using symbols. Specifically we
     // should be able to reject pointers that are in modules but don't map to
     // any FUNC/PUBLIC record.
@@ -283,7 +284,7 @@ fn test_scan_with_symbols() {
         String::from("FUNC 100 400 10 marsupial\n"),
     );
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -369,13 +370,13 @@ fn init_cfi_state() -> (TestFixture, Section, CONTEXT_AMD64, MinidumpContextVali
     (f, stack, expected, expected_valid)
 }
 
-fn check_cfi(
+async fn check_cfi(
     f: TestFixture,
     stack: Section,
     expected: CONTEXT_AMD64,
     expected_valid: MinidumpContextValidity,
 ) {
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -413,8 +414,8 @@ fn check_cfi(
     unreachable!();
 }
 
-#[test]
-fn test_cfi_at_4000() {
+#[tokio::test]
+async fn test_cfi_at_4000() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -426,11 +427,11 @@ fn test_cfi_at_4000() {
     expected.set_register("rsp", frame1_rsp.value().unwrap());
     f.raw.set_register("rip", 0x00007400c0004000);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4001() {
+#[tokio::test]
+async fn test_cfi_at_4001() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -444,11 +445,11 @@ fn test_cfi_at_4001() {
     f.raw.set_register("rip", 0x00007400c0004001);
     f.raw.set_register("rbx", 0xbe0487d2f9eafe29);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4002() {
+#[tokio::test]
+async fn test_cfi_at_4002() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -463,11 +464,11 @@ fn test_cfi_at_4002() {
     f.raw.set_register("rbx", 0xed1b02e8cc0fc79c); // saved %r12
     f.raw.set_register("r12", 0xb0118de918a4bcea); // callee's (distinct) %r12 value
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4003() {
+#[tokio::test]
+async fn test_cfi_at_4003() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -486,11 +487,11 @@ fn test_cfi_at_4003() {
     f.raw.set_register("r12", 0x89d04fa804c87a43); // callee's (distinct) %r12
     f.raw.set_register("r13", 0x5118e02cbdb24b03); // callee's (distinct) %r13
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4004() {
+#[tokio::test]
+async fn test_cfi_at_4004() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -509,11 +510,11 @@ fn test_cfi_at_4004() {
     f.raw.set_register("r12", 0x46b1b8868891b34a); // callee's (distinct) %r12
     f.raw.set_register("r13", 0x5118e02cbdb24b03); // callee's (distinct) %r13
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4005() {
+#[tokio::test]
+async fn test_cfi_at_4005() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -532,11 +533,11 @@ fn test_cfi_at_4005() {
     f.raw.set_register("r12", 0x46b1b8868891b34a); // callee's %r12
     f.raw.set_register("r13", 0x00007400c0005510); // return address
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4006() {
+#[tokio::test]
+async fn test_cfi_at_4006() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame0_rbp = Label::new();
@@ -558,11 +559,11 @@ fn test_cfi_at_4006() {
     f.raw.set_register("r12", 0x26e007b341acfebd); // callee's %r12
     f.raw.set_register("r13", 0x00007400c0005510); // return address
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_frame_pointer_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_overflow() {
     // Make sure we don't explode when trying frame pointer analysis on a value
     // that will overflow.
 
@@ -584,14 +585,14 @@ fn test_frame_pointer_overflow() {
     f.raw.rbp = bad_frame_ptr;
     f.raw.rsp = stack.start().value().unwrap() as Pointer;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 
     // As long as we don't panic, we're good!
 }
 
-#[test]
-fn test_frame_pointer_barely_no_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_barely_no_overflow() {
     // This is test_caller_pushed_rbp but with the all the values pushed
     // as close to the upper memory boundary as possible, to confirm that
     // our code doesn't randomly overflow *AND* isn't overzealous in
@@ -627,7 +628,7 @@ fn test_frame_pointer_barely_no_overflow() {
     f.raw.rbp = frame0_fp.value().unwrap() as Pointer;
     f.raw.rsp = stack.start().value().unwrap();
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
diff --git a/minidump-processor/src/stackwalker/arm64_unittest.rs b/minidump-processor/src/stackwalker/arm64_unittest.rs
index 152bacd..203beb9 100644
--- a/minidump-processor/src/stackwalker/arm64_unittest.rs
+++ b/minidump-processor/src/stackwalker/arm64_unittest.rs
@@ -33,7 +33,7 @@ impl TestFixture {
         }
     }
 
-    pub fn walk_stack(&self, stack: Section) -> CallStack {
+    pub async fn walk_stack(&self, stack: Section) -> CallStack {
         let context = MinidumpContext {
             raw: MinidumpRawContext::Arm64(self.raw.clone()),
             valid: MinidumpContextValidity::All,
@@ -54,6 +54,7 @@ impl TestFixture {
             &self.modules,
             &symbolizer,
         )
+        .await
     }
 
     pub fn add_symbols(&mut self, name: String, symbols: String) {
@@ -61,8 +62,8 @@ impl TestFixture {
     }
 }
 
-#[test]
-fn test_simple() {
+#[tokio::test]
+async fn test_simple() {
     let mut f = TestFixture::new();
     let stack = Section::new();
     stack.start().set_const(0x80000000);
@@ -73,15 +74,15 @@ fn test_simple() {
     f.raw.set_register("pc", 0x4000c020);
     f.raw.set_register("fp", 0x80000000);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
     let f = &s.frames[0];
     let m = f.module.as_ref().unwrap();
     assert_eq!(m.code_file(), "module1");
 }
 
-#[test]
-fn test_scan_without_symbols() {
+#[tokio::test]
+async fn test_scan_without_symbols() {
     // Scanning should work without any symbols
     let mut f = TestFixture::new();
     let mut stack = Section::new();
@@ -111,7 +112,7 @@ fn test_scan_without_symbols() {
     f.raw.set_register("pc", 0x40005510);
     f.raw.set_register("sp", stack.start().value().unwrap());
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -166,8 +167,8 @@ fn test_scan_without_symbols() {
     }
 }
 
-#[test]
-fn test_scan_with_symbols() {
+#[tokio::test]
+async fn test_scan_with_symbols() {
     // Test that we can refine our scanning using symbols. Specifically we
     // should be able to reject pointers that are in modules but don't map to
     // any FUNC/PUBLIC record.
@@ -205,7 +206,7 @@ fn test_scan_with_symbols() {
         String::from("FUNC 100 400 10 marsupial\n"),
     );
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -238,8 +239,8 @@ fn test_scan_with_symbols() {
     }
 }
 
-#[test]
-fn test_scan_first_frame() {
+#[tokio::test]
+async fn test_scan_first_frame() {
     // The first (context) frame gets extra long scans, this test checks that.
     let mut f = TestFixture::new();
     let mut stack = Section::new();
@@ -271,7 +272,7 @@ fn test_scan_first_frame() {
     f.raw.set_register("pc", 0x40005510);
     f.raw.set_register("sp", stack.start().value().unwrap());
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -304,8 +305,8 @@ fn test_scan_first_frame() {
     }
 }
 
-#[test]
-fn test_frame_pointer() {
+#[tokio::test]
+async fn test_frame_pointer() {
     // Frame-pointer-based unwinding
     let mut f = TestFixture::new();
     let mut stack = Section::new();
@@ -345,7 +346,7 @@ fn test_frame_pointer() {
     f.raw.set_register("fp", frame1_fp.value().unwrap());
     f.raw.set_register("sp", stack.start().value().unwrap());
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -407,8 +408,8 @@ fn test_frame_pointer() {
     }
 }
 
-#[test]
-fn test_ptr_auth_strip() {
+#[tokio::test]
+async fn test_ptr_auth_strip() {
     // Same as the basic frame pointer test but extra high bits have been set which
     // must be masked out. This is vaguely emulating Arm Pointer Authentication,
     // although very synthetically. This might break if we implement more accurate
@@ -455,7 +456,7 @@ fn test_ptr_auth_strip() {
     f.raw.set_register("fp", frame1_fp.value().unwrap());
     f.raw.set_register("sp", stack.start().value().unwrap());
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -590,13 +591,13 @@ fn init_cfi_state() -> (TestFixture, Section, Context, MinidumpContextValidity)
     (f, stack, expected, expected_valid)
 }
 
-fn check_cfi(
+async fn check_cfi(
     f: TestFixture,
     stack: Section,
     expected: Context,
     expected_valid: MinidumpContextValidity,
 ) {
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -634,8 +635,8 @@ fn check_cfi(
     unreachable!();
 }
 
-#[test]
-fn test_cfi_at_4000() {
+#[tokio::test]
+async fn test_cfi_at_4000() {
     let (mut f, mut stack, expected, expected_valid) = init_cfi_state();
 
     stack = stack.append_repeated(0, 120);
@@ -643,11 +644,11 @@ fn test_cfi_at_4000() {
     f.raw.set_register("pc", 0x0000000040004000);
     f.raw.set_register("lr", 0x0000000040005510);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4001() {
+#[tokio::test]
+async fn test_cfi_at_4001() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -665,11 +666,11 @@ fn test_cfi_at_4001() {
     f.raw.set_register("x20", 0x623135ac35ac6231);
     f.raw.set_register("fp", 0x5fc4be14be145fc4);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4002() {
+#[tokio::test]
+async fn test_cfi_at_4002() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -693,11 +694,11 @@ fn test_cfi_at_4002() {
     f.raw.iregs[22] = 0x2561562f562f2561; // distinct callee x22
     f.raw.set_register("fp", 0x5fc4be14be145fc4);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4003() {
+#[tokio::test]
+async fn test_cfi_at_4003() {
     let (mut f, mut stack, mut expected, mut expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -722,11 +723,11 @@ fn test_cfi_at_4003() {
     f.raw.iregs[1] = 0xfb756319fb756319;
     f.raw.set_register("fp", 0x5fc4be14be145fc4);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4004() {
+#[tokio::test]
+async fn test_cfi_at_4004() {
     // Should just be the same as 4003
 
     let (mut f, mut stack, mut expected, mut expected_valid) = init_cfi_state();
@@ -753,11 +754,11 @@ fn test_cfi_at_4004() {
     f.raw.iregs[1] = 0xfb756319fb756319;
     f.raw.set_register("fp", 0x5fc4be14be145fc4);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4005() {
+#[tokio::test]
+async fn test_cfi_at_4005() {
     // Here we move the .cfa, but provide an explicit rule to recover the SP,
     // so again there should be no change in the registers recovered.
 
@@ -784,11 +785,11 @@ fn test_cfi_at_4005() {
     f.raw.set_register("pc", 0x0000000040004005);
     f.raw.iregs[1] = 0xfb756319fb756319;
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4006() {
+#[tokio::test]
+async fn test_cfi_at_4006() {
     // Here we provide an explicit rule for the PC, and have the saved .ra be
     // bogus.
 
@@ -816,11 +817,11 @@ fn test_cfi_at_4006() {
     f.raw.set_register("pc", 0x0000000040004006);
     f.raw.iregs[1] = 0xfb756319fb756319;
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_reject_backwards() {
+#[tokio::test]
+async fn test_cfi_reject_backwards() {
     // Check that we reject rules that would cause the stack pointer to
     // move in the wrong direction.
 
@@ -832,12 +833,12 @@ fn test_cfi_reject_backwards() {
     f.raw.set_register("sp", 0x0000000080000000);
     f.raw.set_register("lr", 0x0000000040005510);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 }
 
-#[test]
-fn test_cfi_reject_bad_exprs() {
+#[tokio::test]
+async fn test_cfi_reject_bad_exprs() {
     // Check that we reject rules whose expressions' evaluation fails.
 
     let (mut f, mut stack, _expected, _expected_valid) = init_cfi_state();
@@ -847,12 +848,12 @@ fn test_cfi_reject_bad_exprs() {
     f.raw.set_register("pc", 0x0000000040007000);
     f.raw.set_register("sp", 0x0000000080000000);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 }
 
-#[test]
-fn test_frame_pointer_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_overflow() {
     // Make sure we don't explode when trying frame pointer analysis on a value
     // that will overflow.
 
@@ -876,14 +877,14 @@ fn test_frame_pointer_overflow() {
         .set_register("sp", stack.start().value().unwrap() as Pointer);
     f.raw.set_register("lr", 0x00007500b0000110);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 
     // As long as we don't panic, we're good!
 }
 
-#[test]
-fn test_frame_pointer_barely_no_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_barely_no_overflow() {
     // This is a simple frame pointer test but with the all the values pushed
     // as close to the upper memory boundary as possible, to confirm that
     // our code doesn't randomly overflow *AND* isn't overzealous in
@@ -922,7 +923,7 @@ fn test_frame_pointer_barely_no_overflow() {
         .set_register("sp", stack.start().value().unwrap() as Pointer);
     f.raw.set_register("lr", return_address);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -970,8 +971,8 @@ fn test_frame_pointer_barely_no_overflow() {
     }
 }
 
-#[test]
-fn test_frame_pointer_infinite_equality() {
+#[tokio::test]
+async fn test_frame_pointer_infinite_equality() {
     // Leaf functions on Arm are allowed to not update the stack pointer, so
     // it's valid for the frame pointer analysis to conclude that the stack
     // pointer doesn't change. However we must only provide this allowance
@@ -1023,7 +1024,7 @@ fn test_frame_pointer_infinite_equality() {
     f.raw.set_register("fp", frame1_fp.value().unwrap());
     f.raw.set_register("sp", stack.start().value().unwrap());
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
diff --git a/minidump-processor/src/stackwalker/arm_unittest.rs b/minidump-processor/src/stackwalker/arm_unittest.rs
index 8ab9f79..c6f12b9 100644
--- a/minidump-processor/src/stackwalker/arm_unittest.rs
+++ b/minidump-processor/src/stackwalker/arm_unittest.rs
@@ -29,7 +29,7 @@ impl TestFixture {
         }
     }
 
-    pub fn walk_stack(&self, stack: Section) -> CallStack {
+    pub async fn walk_stack(&self, stack: Section) -> CallStack {
         let context = MinidumpContext {
             raw: MinidumpRawContext::Arm(self.raw.clone()),
             valid: MinidumpContextValidity::All,
@@ -50,6 +50,7 @@ impl TestFixture {
             &self.modules,
             &symbolizer,
         )
+        .await
     }
 
     pub fn add_symbols(&mut self, name: String, symbols: String) {
@@ -57,8 +58,8 @@ impl TestFixture {
     }
 }
 
-#[test]
-fn test_simple() {
+#[tokio::test]
+async fn test_simple() {
     let mut f = TestFixture::new();
     let stack = Section::new();
     stack.start().set_const(0x80000000);
@@ -69,15 +70,15 @@ fn test_simple() {
     f.raw.set_register("pc", 0x4000c020);
     f.raw.set_register("fp", 0x80000000);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
     let f = &s.frames[0];
     let m = f.module.as_ref().unwrap();
     assert_eq!(m.code_file(), "module1");
 }
 
-#[test]
-fn test_scan_without_symbols() {
+#[tokio::test]
+async fn test_scan_without_symbols() {
     // Scanning should work without any symbols
     let mut f = TestFixture::new();
     let mut stack = Section::new();
@@ -108,7 +109,7 @@ fn test_scan_without_symbols() {
     f.raw
         .set_register("sp", stack.start().value().unwrap() as u32);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -163,8 +164,8 @@ fn test_scan_without_symbols() {
     }
 }
 
-#[test]
-fn test_scan_first_frame() {
+#[tokio::test]
+async fn test_scan_first_frame() {
     // The first (context) frame gets extra long scans, this test checks that.
     let mut f = TestFixture::new();
     let mut stack = Section::new();
@@ -197,7 +198,7 @@ fn test_scan_first_frame() {
     f.raw
         .set_register("sp", stack.start().value().unwrap() as u32);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -230,8 +231,8 @@ fn test_scan_first_frame() {
     }
 }
 
-#[test]
-fn test_frame_pointer() {
+#[tokio::test]
+async fn test_frame_pointer() {
     // Frame-pointer-based unwinding
     let mut f = TestFixture::new();
     let mut stack = Section::new();
@@ -272,7 +273,7 @@ fn test_frame_pointer() {
     f.raw
         .set_register("sp", stack.start().value().unwrap() as u32);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -334,8 +335,8 @@ fn test_frame_pointer() {
     }
 }
 
-#[test]
-fn test_frame_pointer_infinite_equality() {
+#[tokio::test]
+async fn test_frame_pointer_infinite_equality() {
     // Leaf functions on Arm are allowed to not update the stack pointer, so
     // it's valid for the frame pointer analysis to conclude that the stack
     // pointer doesn't change. However we must only provide this allowance
@@ -386,7 +387,7 @@ fn test_frame_pointer_infinite_equality() {
     f.raw
         .set_register("sp", stack.start().value().unwrap() as u32);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -492,13 +493,13 @@ fn init_cfi_state() -> (TestFixture, Section, CONTEXT_ARM, MinidumpContextValidi
     (f, stack, expected, expected_valid)
 }
 
-fn check_cfi(
+async fn check_cfi(
     f: TestFixture,
     stack: Section,
     expected: CONTEXT_ARM,
     expected_valid: MinidumpContextValidity,
 ) {
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -536,8 +537,8 @@ fn check_cfi(
     unreachable!();
 }
 
-#[test]
-fn test_cfi_at_4000() {
+#[tokio::test]
+async fn test_cfi_at_4000() {
     let (mut f, mut stack, expected, expected_valid) = init_cfi_state();
 
     stack = stack.append_repeated(0, 120);
@@ -545,11 +546,11 @@ fn test_cfi_at_4000() {
     f.raw.set_register("pc", 0x40004000);
     f.raw.set_register("lr", 0x40005510);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4001() {
+#[tokio::test]
+async fn test_cfi_at_4001() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -565,11 +566,11 @@ fn test_cfi_at_4001() {
     f.raw.iregs[4] = 0x635adc9f;
     f.raw.set_register("fp", 0xbe145fc4);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4002() {
+#[tokio::test]
+async fn test_cfi_at_4002() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -592,11 +593,11 @@ fn test_cfi_at_4002() {
     f.raw.iregs[7] = 0xa449829f; // distinct callee r7
     f.raw.set_register("fp", 0xbe145fc4);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4003() {
+#[tokio::test]
+async fn test_cfi_at_4003() {
     let (mut f, mut stack, mut expected, mut expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -619,11 +620,11 @@ fn test_cfi_at_4003() {
     f.raw.set_register("pc", 0x40004003);
     f.raw.iregs[1] = 0xfb756319;
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4004() {
+#[tokio::test]
+async fn test_cfi_at_4004() {
     // Should be the same as 4003
     let (mut f, mut stack, mut expected, mut expected_valid) = init_cfi_state();
 
@@ -647,11 +648,11 @@ fn test_cfi_at_4004() {
     f.raw.set_register("pc", 0x40004004);
     f.raw.iregs[1] = 0xfb756319;
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4005() {
+#[tokio::test]
+async fn test_cfi_at_4005() {
     let (mut f, mut stack, mut expected, mut expected_valid) = init_cfi_state();
 
     let frame1_sp = Label::new();
@@ -674,11 +675,11 @@ fn test_cfi_at_4005() {
     f.raw.set_register("pc", 0x40004005);
     f.raw.iregs[1] = 0xfb756319;
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4006() {
+#[tokio::test]
+async fn test_cfi_at_4006() {
     // Here we provide an explicit rule for the PC, and have the saved .ra be
     // bogus.
 
@@ -705,11 +706,11 @@ fn test_cfi_at_4006() {
     f.raw.set_register("pc", 0x40004006);
     f.raw.iregs[1] = 0xfb756319;
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_reject_backwards() {
+#[tokio::test]
+async fn test_cfi_reject_backwards() {
     // Check that we reject rules that would cause the stack pointer to
     // move in the wrong direction.
 
@@ -721,12 +722,12 @@ fn test_cfi_reject_backwards() {
     f.raw.set_register("sp", 0x80000000);
     f.raw.set_register("lr", 0x40005510);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 }
 
-#[test]
-fn test_cfi_reject_bad_exprs() {
+#[tokio::test]
+async fn test_cfi_reject_bad_exprs() {
     // Check that we reject rules whose expressions' evaluation fails.
 
     let (mut f, mut stack, _expected, _expected_valid) = init_cfi_state();
@@ -736,12 +737,12 @@ fn test_cfi_reject_bad_exprs() {
     f.raw.set_register("pc", 0x40007000);
     f.raw.set_register("sp", 0x80000000);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 }
 
-#[test]
-fn test_frame_pointer_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_overflow() {
     // Make sure we don't explode when trying frame pointer analysis on a value
     // that will overflow.
 
@@ -765,14 +766,14 @@ fn test_frame_pointer_overflow() {
         .set_register("sp", stack.start().value().unwrap() as Pointer);
     f.raw.set_register("lr", 0x7b302000);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 
     // As long as we don't panic, we're good!
 }
 
-#[test]
-fn test_frame_pointer_overflow_nonsense_32bit_stack() {
+#[tokio::test]
+async fn test_frame_pointer_overflow_nonsense_32bit_stack() {
     // same as test_frame_pointer_overflow, but we're going to abuse the fact
     // that rust-minidump prefers representing things in 64-bit to create
     // impossible stack addresses that overflow 32-bit integers but appear
@@ -801,14 +802,14 @@ fn test_frame_pointer_overflow_nonsense_32bit_stack() {
         .set_register("sp", stack.start().value().unwrap() as Pointer);
     f.raw.set_register("lr", 0x7b302000);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 
     // As long as we don't panic, we're good!
 }
 
-#[test]
-fn test_frame_pointer_barely_no_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_barely_no_overflow() {
     // This is a simple frame pointer test but with the all the values pushed
     // as close to the upper memory boundary as possible, to confirm that
     // our code doesn't randomly overflow *AND* isn't overzealous in
@@ -847,7 +848,7 @@ fn test_frame_pointer_barely_no_overflow() {
         .set_register("sp", stack.start().value().unwrap() as Pointer);
     f.raw.set_register("lr", return_address);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
diff --git a/minidump-processor/src/stackwalker/x86_unittest.rs b/minidump-processor/src/stackwalker/x86_unittest.rs
index e9051d8..e392491 100644
--- a/minidump-processor/src/stackwalker/x86_unittest.rs
+++ b/minidump-processor/src/stackwalker/x86_unittest.rs
@@ -29,7 +29,7 @@ impl TestFixture {
         }
     }
 
-    pub fn walk_stack(&self, stack: Section) -> CallStack {
+    pub async fn walk_stack(&self, stack: Section) -> CallStack {
         let context = MinidumpContext {
             raw: MinidumpRawContext::X86(self.raw.clone()),
             valid: MinidumpContextValidity::All,
@@ -50,6 +50,7 @@ impl TestFixture {
             &self.modules,
             &symbolizer,
         )
+        .await
     }
 
     pub fn add_symbols(&mut self, name: String, symbols: String) {
@@ -57,15 +58,15 @@ impl TestFixture {
     }
 }
 
-#[test]
-fn test_simple() {
+#[tokio::test]
+async fn test_simple() {
     let mut f = TestFixture::new();
     let mut stack = Section::new();
     stack.start().set_const(0x80000000);
     stack = stack.D32(0).D32(0); // end-of-stack marker
     f.raw.eip = 0x40000200;
     f.raw.ebp = 0x80000000;
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
     let f = &s.frames[0];
     let m = f.module.as_ref().unwrap();
@@ -75,8 +76,8 @@ fn test_simple() {
 // Walk a traditional frame. A traditional frame saves the caller's
 // %ebp just below the return address, and has its own %ebp pointing
 // at the saved %ebp.
-#[test]
-fn test_traditional() {
+#[tokio::test]
+async fn test_traditional() {
     let mut f = TestFixture::new();
     let frame0_ebp = Label::new();
     let frame1_ebp = Label::new();
@@ -94,7 +95,7 @@ fn test_traditional() {
     f.raw.eip = 0x4000c7a5;
     f.raw.esp = stack.start().value().unwrap() as u32;
     f.raw.ebp = frame0_ebp.value().unwrap() as u32;
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
     {
         let f0 = &s.frames[0];
@@ -116,8 +117,8 @@ fn test_traditional() {
 
 // Walk a traditional frame, but use a bogus %ebp value, forcing a scan
 // of the stack for something that looks like a return address.
-#[test]
-fn test_traditional_scan() {
+#[tokio::test]
+async fn test_traditional_scan() {
     let mut f = TestFixture::new();
     let frame1_esp = Label::new();
     let frame1_ebp = Label::new();
@@ -144,7 +145,7 @@ fn test_traditional_scan() {
     // for something that looks like a return address.
     f.raw.ebp = 0xd43eed6e;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -187,8 +188,8 @@ fn test_traditional_scan() {
 }
 
 // Force scanning for a return address a long way down the stack
-#[test]
-fn test_traditional_scan_long_way() {
+#[tokio::test]
+async fn test_traditional_scan_long_way() {
     let mut f = TestFixture::new();
     let frame1_esp = Label::new();
     let frame1_ebp = Label::new();
@@ -217,7 +218,7 @@ fn test_traditional_scan_long_way() {
     // for something that looks like a return address.
     f.raw.ebp = 0xd43eed6e;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -306,13 +307,13 @@ fn init_cfi_state() -> (TestFixture, Section, CONTEXT_X86, MinidumpContextValidi
     (f, stack, expected, expected_valid)
 }
 
-fn check_cfi(
+async fn check_cfi(
     f: TestFixture,
     stack: Section,
     expected: CONTEXT_X86,
     expected_valid: MinidumpContextValidity,
 ) {
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -350,8 +351,8 @@ fn check_cfi(
     unreachable!();
 }
 
-#[test]
-fn test_cfi_at_4000() {
+#[tokio::test]
+async fn test_cfi_at_4000() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -363,11 +364,11 @@ fn test_cfi_at_4000() {
     expected.set_register("esp", frame1_rsp.value().unwrap() as u32);
     f.raw.set_register("eip", 0x40004000);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4001() {
+#[tokio::test]
+async fn test_cfi_at_4001() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -381,11 +382,11 @@ fn test_cfi_at_4001() {
     f.raw.set_register("eip", 0x40004001);
     f.raw.set_register("ebx", 0x91aa9a8b);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4002() {
+#[tokio::test]
+async fn test_cfi_at_4002() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -400,11 +401,11 @@ fn test_cfi_at_4002() {
     f.raw.set_register("ebx", 0x53d1379d);
     f.raw.set_register("esi", 0xa5c790ed);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4003() {
+#[tokio::test]
+async fn test_cfi_at_4003() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -423,11 +424,11 @@ fn test_cfi_at_4003() {
     f.raw.set_register("esi", 0xa97f229d);
     f.raw.set_register("edi", 0xb05cc997);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4004() {
+#[tokio::test]
+async fn test_cfi_at_4004() {
     // Should be the same as 4003
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
@@ -447,11 +448,11 @@ fn test_cfi_at_4004() {
     f.raw.set_register("esi", 0xa97f229d);
     f.raw.set_register("edi", 0xb05cc997);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4005() {
+#[tokio::test]
+async fn test_cfi_at_4005() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame1_rsp = Label::new();
@@ -470,11 +471,11 @@ fn test_cfi_at_4005() {
     f.raw.set_register("esi", 0x0fb7dc4e);
     f.raw.set_register("edi", 0x40005510);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
-#[test]
-fn test_cfi_at_4006() {
+#[tokio::test]
+async fn test_cfi_at_4006() {
     let (mut f, mut stack, mut expected, expected_valid) = init_cfi_state();
 
     let frame0_ebp = Label::new();
@@ -497,12 +498,12 @@ fn test_cfi_at_4006() {
     f.raw.set_register("esi", 0x743833c9);
     f.raw.set_register("edi", 0x40005510);
 
-    check_cfi(f, stack, expected, expected_valid);
+    check_cfi(f, stack, expected, expected_valid).await;
 }
 
 // Totally basic STACK WIN frame data, no weird stuff.
-#[test]
-fn test_stack_win_frame_data_basic() {
+#[tokio::test]
+async fn test_stack_win_frame_data_basic() {
     let mut f = TestFixture::new();
     let symbols = [
         "STACK WIN 4 aa85 176 0 0 4 10 4 0 1",
@@ -544,7 +545,7 @@ fn test_stack_win_frame_data_basic() {
         .set_register("esp", stack.start().value().unwrap() as u32);
     f.raw.set_register("ebp", 0xf052c1de);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -591,8 +592,8 @@ fn test_stack_win_frame_data_basic() {
 }
 
 // Totally basic STACK WIN frame data, no weird stuff.
-#[test]
-fn test_stack_win_frame_data_overlapping() {
+#[tokio::test]
+async fn test_stack_win_frame_data_overlapping() {
     // Same as frame_data_basic but there are extra entries which technically overlap
     // with this one, but in a way that is easily disambiguated by preferring the
     // one with the higher base address. This happens frequently in real symbol files.
@@ -647,7 +648,7 @@ fn test_stack_win_frame_data_overlapping() {
         .set_register("esp", stack.start().value().unwrap() as u32);
     f.raw.set_register("ebp", 0xf052c1de);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
@@ -694,8 +695,8 @@ fn test_stack_win_frame_data_overlapping() {
 }
 
 // Testing that grand_callee_parameter_size is properly computed.
-#[test]
-fn test_stack_win_frame_data_parameter_size() {
+#[tokio::test]
+async fn test_stack_win_frame_data_parameter_size() {
     let mut f = TestFixture::new();
 
     let module1_symbols = ["FUNC 1000 100 c module1::wheedle\n"];
@@ -756,7 +757,7 @@ fn test_stack_win_frame_data_parameter_size() {
     f.raw
         .set_register("ebp", frame0_ebp.value().unwrap() as u32);
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 3);
 
     {
@@ -819,8 +820,8 @@ fn test_stack_win_frame_data_parameter_size() {
     }
 }
 
-#[test]
-fn test_frame_pointer_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_overflow() {
     // Make sure we don't explode when trying frame pointer analysis on a value
     // that will overflow.
 
@@ -842,14 +843,14 @@ fn test_frame_pointer_overflow() {
     f.raw.ebp = bad_frame_ptr;
     f.raw.esp = stack.start().value().unwrap() as Pointer;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 
     // As long as we don't panic, we're good!
 }
 
-#[test]
-fn test_frame_pointer_overflow_nonsense_32bit_stack() {
+#[tokio::test]
+async fn test_frame_pointer_overflow_nonsense_32bit_stack() {
     // same as test_frame_pointer_overflow, but we're going to abuse the fact
     // that rust-minidump prefers representing things in 64-bit to create
     // impossible stack addresses that overflow 32-bit integers but appear
@@ -876,14 +877,14 @@ fn test_frame_pointer_overflow_nonsense_32bit_stack() {
     f.raw.ebp = bad_frame_ptr as u32;
     f.raw.esp = stack.start().value().unwrap() as Pointer;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 1);
 
     // As long as we don't panic, we're good!
 }
 
-#[test]
-fn test_frame_pointer_barely_no_overflow() {
+#[tokio::test]
+async fn test_frame_pointer_barely_no_overflow() {
     // This is test_tradition but with the all the values pushed
     // as close to the upper memory boundary as possible, to confirm that
     // our code doesn't randomly overflow *AND* isn't overzealous in
@@ -919,7 +920,7 @@ fn test_frame_pointer_barely_no_overflow() {
     f.raw.ebp = frame0_fp.value().unwrap() as Pointer;
     f.raw.esp = stack.start().value().unwrap() as Pointer;
 
-    let s = f.walk_stack(stack);
+    let s = f.walk_stack(stack).await;
     assert_eq!(s.frames.len(), 2);
 
     {
diff --git a/minidump-processor/tests/test_processor.rs b/minidump-processor/tests/test_processor.rs
index f4de449..10dd9dd 100644
--- a/minidump-processor/tests/test_processor.rs
+++ b/minidump-processor/tests/test_processor.rs
@@ -44,13 +44,14 @@ fn testdata_symbol_path() -> PathBuf {
     path
 }
 
-#[test]
-fn test_processor() {
+#[tokio::test]
+async fn test_processor() {
     let dump = read_test_minidump().unwrap();
     let state = minidump_processor::process_minidump(
         &dump,
         &Symbolizer::new(simple_symbol_supplier(vec![])),
     )
+    .await
     .unwrap();
     assert_eq!(state.system_info.os, Os::Windows);
     assert_eq!(state.system_info.os_version.unwrap(), "5.1.2600");
@@ -114,8 +115,8 @@ fn test_processor() {
     assert_eq!(state.threads[1].frames.len(), 0);
 }
 
-#[test]
-fn test_processor_symbols() {
+#[tokio::test]
+async fn test_processor_symbols() {
     let dump = read_test_minidump().unwrap();
     let path = testdata_symbol_path();
     println!("symbol path: {:?}", path);
@@ -123,6 +124,7 @@ fn test_processor_symbols() {
         &dump,
         &Symbolizer::new(simple_symbol_supplier(vec![path])),
     )
+    .await
     .unwrap();
     let f0 = &state.threads[0].frames[0];
     assert_eq!(
@@ -146,14 +148,15 @@ fn minimal_minidump() -> SynthMinidump {
         .add_memory(stack)
 }
 
-fn read_synth_dump(dump: SynthMinidump) -> ProcessState {
+async fn read_synth_dump(dump: SynthMinidump) -> ProcessState {
     let dump = Minidump::read(dump.finish().unwrap()).unwrap();
     minidump_processor::process_minidump(&dump, &Symbolizer::new(simple_symbol_supplier(vec![])))
+        .await
         .unwrap()
 }
 
-#[test]
-fn test_linux_cpu_info() {
+#[tokio::test]
+async fn test_linux_cpu_info() {
     // Whitespace intentionally wonky to test robustness
 
     let input = b"
@@ -161,13 +164,13 @@ microcode : 0x1e34a6789
 ";
 
     let dump = minimal_minidump().set_linux_cpu_info(input);
-    let state = read_synth_dump(dump);
+    let state = read_synth_dump(dump).await;
 
     assert_eq!(state.system_info.cpu_microcode_version, Some(0x1e34a6789));
 }
 
-#[test]
-fn test_linux_lsb_release() {
+#[tokio::test]
+async fn test_linux_lsb_release() {
     // Whitespace intentionally wonky to test robustness
     {
         let input = br#"
@@ -177,7 +180,7 @@ DISTRIB_ID="hello"
 DISTRIB_DESCRIPTION= wow long string!!!
 "#;
         let dump = minimal_minidump().set_linux_lsb_release(input);
-        let state = read_synth_dump(dump);
+        let state = read_synth_dump(dump).await;
 
         let LinuxStandardBase {
             id,
@@ -200,7 +203,7 @@ ID="hello"
 PRETTY_NAME= wow long string!!!
 "#;
         let dump = minimal_minidump().set_linux_lsb_release(input);
-        let state = read_synth_dump(dump);
+        let state = read_synth_dump(dump).await;
 
         let LinuxStandardBase {
             id,
@@ -216,24 +219,24 @@ PRETTY_NAME= wow long string!!!
     }
 }
 
-#[test]
-fn test_linux_environ() {
+#[tokio::test]
+async fn test_linux_environ() {
     // Whitespace intentionally wonky to test robustness
 
     // TODO: add tests for values we care about
     let input = b"";
 
     let dump = minimal_minidump().set_linux_environ(input);
-    let _state = read_synth_dump(dump);
+    let _state = read_synth_dump(dump).await;
 }
 
-#[test]
-fn test_linux_proc_status() {
+#[tokio::test]
+async fn test_linux_proc_status() {
     // Whitespace intentionally wonky to test robustness
 
     // TODO: add tests for values we care about
     let input = b"";
 
     let dump = minimal_minidump().set_linux_proc_status(input);
-    let _state = read_synth_dump(dump);
+    let _state = read_synth_dump(dump).await;
 }