Repository: luser/rust-minidump
Pull request #358 (2021-12-14 10:14:57)

Bump serde_json from 1.0.72 to 1.0.73
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.72 to 1.0.73.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/serde-rs/json/releases">serde_json's releases</a>.</em></p>
<blockquote>
<h2>v1.0.73</h2>
<ul>
<li>Update <code>itoa</code> dependency to 1.0</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/serde-rs/json/commit/296f718e18681c52909da99f24198133e58528b2"><code>296f718</code></a> Release 1.0.73</li>
<li><a href="https://github.com/serde-rs/json/commit/48455de6743221e1091bcff342d539b585b4431f"><code>48455de</code></a> Update to itoa 1.0</li>
<li><a href="https://github.com/serde-rs/json/commit/b66b0eb3224ce10a8e7cf15f3dd4b3035ecfc10f"><code>b66b0eb</code></a> Track raw pointers in miri CI run</li>
<li><a href="https://github.com/serde-rs/json/commit/52eec5e2ee24cd58a3be1b8d19e1e23d2ca795ca"><code>52eec5e</code></a> Add a miri test job in CI</li>
<li><a href="https://github.com/serde-rs/json/commit/aaaf125f0b2504b30abb0446adf18dae8e709a8b"><code>aaaf125</code></a> Disable TcpStream test on miri</li>
<li><a href="https://github.com/serde-rs/json/commit/f0774c482ae5ca619161312e573de0ffa69c9a60"><code>f0774c4</code></a> Raise required rustc from 1.31 to 1.36</li>
<li><a href="https://github.com/serde-rs/json/commit/c3eddcdf4006a53efd2d2d5aaa5eceea4f7f6f8c"><code>c3eddcd</code></a> Collect imports to top of test suite</li>
<li><a href="https://github.com/serde-rs/json/commit/a28529ddc94de34fb18024ac686d75e29d458032"><code>a28529d</code></a> Skip ui test when running in miri</li>
<li><a href="https://github.com/serde-rs/json/commit/7db1ea907de55338d4069924dd1575d4e9b6cf60"><code>7db1ea9</code></a> Ignore needless_late_init Clippy lint</li>
<li><a href="https://github.com/serde-rs/json/commit/fc0ca07b10af7cb42b3eb5ac36ca68862dec4be3"><code>fc0ca07</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/serde-rs/json/issues/831">#831</a> from serde-rs/from_u32</li>
<li>Additional commits viewable in <a href="https://github.com/serde-rs/json/compare/v1.0.72...v1.0.73">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_json&package-manager=cargo&previous-version=1.0.72&new-version=1.0.73)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b286b55..630411a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -87,7 +87,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 name = "breakpad-symbols"
 version = "0.9.6"
 dependencies = [
- "bytes",
+ "circular",
  "failure",
  "log",
  "minidump-common",
@@ -142,6 +142,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "circular"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea"
+
 [[package]]
 name = "clap"
 version = "2.33.3"
@@ -477,7 +483,7 @@ checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
 dependencies = [
  "bytes",
  "fnv",
- "itoa",
+ "itoa 0.4.8",
 ]
 
 [[package]]
@@ -518,7 +524,7 @@ dependencies = [
  "http-body",
  "httparse",
  "httpdate",
- "itoa",
+ "itoa 0.4.8",
  "pin-project-lite",
  "socket2",
  "tokio",
@@ -588,6 +594,12 @@ version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
 
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
 [[package]]
 name = "js-sys"
 version = "0.3.55"
@@ -1135,18 +1147,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.130"
+version = "1.0.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.130"
+version = "1.0.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1155,11 +1167,11 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.72"
+version = "1.0.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
+checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5"
 dependencies = [
- "itoa",
+ "itoa 1.0.1",
  "ryu",
  "serde",
 ]
@@ -1171,7 +1183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
 dependencies = [
  "form_urlencoded",
- "itoa",
+ "itoa 0.4.8",
  "ryu",
  "serde",
 ]
diff --git a/breakpad-symbols/Cargo.toml b/breakpad-symbols/Cargo.toml
index 1ffb514..4185f69 100644
--- a/breakpad-symbols/Cargo.toml
+++ b/breakpad-symbols/Cargo.toml
@@ -14,7 +14,7 @@ edition = "2018"
 travis-ci = { repository = "luser/rust-minidump" }
 
 [dependencies]
-bytes = "1.1.0"
+circular = "0.3.0"
 minidump-common = { version = "0.9.6", path = "../minidump-common" }
 range-map = "0.1.5"
 nom = "~1.2.2"
diff --git a/breakpad-symbols/src/lib.rs b/breakpad-symbols/src/lib.rs
index 5949da1..8acae05 100644
--- a/breakpad-symbols/src/lib.rs
+++ b/breakpad-symbols/src/lib.rs
@@ -29,11 +29,11 @@
 //!               "vswprintf");
 //! ```
 
-use bytes::Bytes;
 use failure::Error;
 use log::{debug, trace, warn};
 use reqwest::blocking::Client;
 use reqwest::Url;
+use tempfile::NamedTempFile;
 
 use std::borrow::Cow;
 use std::boxed::Box;
@@ -374,8 +374,7 @@ impl HttpSymbolSupplier {
     }
 }
 
-/// Save the data in `contents` to `path`.
-fn save_contents(contents: &[u8], url: &Url, final_path: &Path, tmp_path: &Path) -> io::Result<()> {
+fn create_cache_file(tmp_path: &Path, final_path: &Path) -> io::Result<NamedTempFile> {
     // Use tempfile to save things to our cache to ensure proper
     // atomicity of writes. We may want multiple instances of rust-minidump
     // to be sharing a cache, and we don't want one instance to see another
@@ -394,9 +393,10 @@ fn save_contents(contents: &[u8], url: &Url, final_path: &Path, tmp_path: &Path)
     })?;
     fs::create_dir_all(&base)?;
 
-    let mut temp = tempfile::NamedTempFile::new_in(tmp_path)?;
-    temp.write_all(contents)?;
+    NamedTempFile::new_in(tmp_path)
+}
 
+fn commit_cache_file(mut temp: NamedTempFile, final_path: &Path, url: &Url) -> io::Result<()> {
     // Append any extra metadata we also want to be cached as "INFO" lines,
     // because this is an established format that parsers will ignore the
     // contents of by default.
@@ -409,6 +409,7 @@ fn save_contents(contents: &[u8], url: &Url, final_path: &Path, tmp_path: &Path)
     // If another process already wrote this entry, prefer their value to
     // avoid needless file system churn.
     temp.persist_noclobber(final_path)?;
+
     Ok(())
 }
 
@@ -420,18 +421,55 @@ fn fetch_symbol_file(
     rel_path: &str,
     cache: &Path,
     tmp: &Path,
-) -> Result<(Url, Bytes), Error> {
-    let url = base_url.join(rel_path)?;
+) -> Result<SymbolFile, SymbolError> {
+    // This function is a bit of a complicated mess because we want to write
+    // the input to our symbol cache, but we're a streaming parser. So we
+    // use the bare SymbolFile::parse to get access to the contents of
+    // the input stream as it's downloaded+parsed to write it to disk.
+    //
+    // Note that caching is strictly "optional" because it's more important
+    // to parse the symbols. So if at any point the caching i/o fails, we just
+    // give up on caching but let the parse+download continue.
+
+    // First try to GET the file from a server
+    let url = base_url.join(rel_path).map_err(|_| SymbolError::NotFound)?;
     debug!("Trying {}", url);
-    let res = client.get(url.clone()).send()?.error_for_status()?;
-    let buf = res.bytes()?;
-    let local = cache.join(rel_path);
+    let res = client
+        .get(url.clone())
+        .send()
+        .and_then(|res| res.error_for_status())
+        .map_err(|_| SymbolError::NotFound)?;
+
+    // Now try to create the temp cache file (not yet in the cache)
+    let final_cache_path = cache.join(rel_path);
+    let mut temp = create_cache_file(tmp, &final_cache_path)
+        .map_err(|e| {
+            warn!("Failed to save symbol file in local disk cache: {}", e);
+        })
+        .ok();
+
+    // Now stream parse the file as it downloads.
+    let mut symbol_file = SymbolFile::parse(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) {
+                // Give up on caching this.
+                warn!("Failed to save symbol file in local disk cache: {}", e);
+                temp = None;
+            }
+        }
+    })?;
+    // Make note of what URL this symbol file was downloaded from.
+    symbol_file.url = Some(url.to_string());
 
-    match save_contents(&buf, &url, &local, tmp) {
-        Ok(_) => {}
-        Err(e) => warn!("Failed to save symbol file in local disk cache: {}", e),
+    // Try to finish the cache file and atomically swap it into the cache.
+    if let Some(temp) = temp {
+        let _ = commit_cache_file(temp, &final_cache_path, &url).map_err(|e| {
+            warn!("Failed to save symbol file in local disk cache: {}", e);
+        });
     }
-    Ok((url, buf))
+
+    Ok(symbol_file)
 }
 
 impl SymbolSupplier for HttpSymbolSupplier {
@@ -445,14 +483,10 @@ impl SymbolSupplier for HttpSymbolSupplier {
         // Now try urls
         if let Some(rel_path) = relative_symbol_path(module, "sym") {
             for url in &self.urls {
-                if let Ok((full_url, buf)) =
+                if let Ok(file) =
                     fetch_symbol_file(&self.client, url, &rel_path, &self.cache, &self.tmp)
                 {
-                    // Parse the symbol file and set the "url" statistic
-                    return SymbolFile::from_bytes(&buf).map(|mut file| {
-                        file.url = Some(full_url.into());
-                        file
-                    });
+                    return Ok(file);
                 }
             }
         }
diff --git a/breakpad-symbols/src/sym_file/mod.rs b/breakpad-symbols/src/sym_file/mod.rs
index 1602079..db890db 100644
--- a/breakpad-symbols/src/sym_file/mod.rs
+++ b/breakpad-symbols/src/sym_file/mod.rs
@@ -1,26 +1,109 @@
 // Copyright 2015 Ted Mielczarek. See the COPYRIGHT
 // file at the top-level directory of this distribution.
-
-use std::path::Path;
-
-use crate::sym_file::parser::{parse_symbol_bytes, parse_symbol_file};
 use crate::{FrameSymbolizer, FrameWalker, Module, SymbolError};
 
 pub use crate::sym_file::types::*;
+pub use parser::SymbolParser;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+
+use failure::format_err;
 
 mod parser;
 mod types;
 mod walker;
 
 impl SymbolFile {
-    /// Parse a `SymbolFile` from `path`.
-    pub fn from_file(path: &Path) -> Result<SymbolFile, SymbolError> {
-        parse_symbol_file(path)
+    /// Parse a SymbolFile from the given Reader.
+    ///
+    /// Every time a chunk of the input is parsed, that chunk will
+    /// be passed to `callback` to allow you to do something else
+    /// with the data as it's streamed in (e.g. you can save the
+    /// input to a cache).
+    ///
+    /// The reader is wrapped in a buffer reader so you shouldn't
+    /// buffer the input yourself.
+    pub fn parse<R: Read>(
+        mut input_reader: R,
+        mut callback: impl FnMut(&[u8]),
+    ) -> Result<SymbolFile, SymbolError> {
+        // This parse streams the input to avoid the need to materialize all of
+        // it into memory at once (symbol files can be a gigabyte!). As a result,
+        // we need to iteratively parse.
+        //
+        // We do this by repeatedly filling up a buffer with input and asking the
+        // parser to parse it. The parser will return how much of the input it
+        // consumed, which we can use to clear space in our buffer and to tell
+        // if it successfully consumed the whole input when the Reader runs dry.
+
+        // Having a fix-sized buffer has one fatal issue: if one atomic step
+        // of the parser needs more than this amount of data, then we won't
+        // be able to parse it.
+        //
+        // This can result in `buf` filling up and `buf.space()` becoming an
+        // empty slice. This in turn will make the reader yield 0 bytes, and
+        // we'll treat it like EOF and fail the parse. Bad error message UX,
+        // but good enough. This is also our safety-valve against bugs in the
+        // parser causing infinite loops.
+        //
+        // The "atom" of our parser is a line, and ~100kb is a pretty generous
+        // limit to have on the length of a line. However we actually only have
+        // *half* this value as our limit, as circular::Buffer will only
+        // `shift` the buffer's contents if over half of its capacity has been
+        // drained by `consume` -- and `space()` only grows when a `shift` happens.
+        //
+        // I have in fact seen 8kb function names (thanks generic combinators!), so we
+        // need a buffer size that's at least 16kb. I went with 100kb to be safe.
+        //
+        // FIXME: investigate using `Buffer::grow` to be more adaptive here?
+        let mut buf = circular::Buffer::with_capacity(100_000);
+        let mut parser = SymbolParser::new();
+        let mut fully_consumed = false;
+        loop {
+            // Read the data in, and tell the circular buffer about the new data
+            let size = input_reader.read(buf.space()).map_err(|e| {
+                SymbolError::LoadError(format_err!("couldn't read input stream {}", e))
+            })?;
+            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(format_err!(
+                        "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 an in-memory `SymbolFile` from `bytes`.
+    // Parse a SymbolFile from bytes.
     pub fn from_bytes(bytes: &[u8]) -> Result<SymbolFile, SymbolError> {
-        parse_symbol_bytes(bytes)
+        Self::parse(bytes, |_| ())
+    }
+
+    // Parse a SymbolFile from a file.
+    pub fn from_file(path: &Path) -> Result<SymbolFile, SymbolError> {
+        let file = File::open(path)
+            .map_err(|e| SymbolError::LoadError(format_err!("Couldn't open symbol file {}", e)))?;
+        Self::parse(file, |_| ())
     }
 
     /// Fill in as much source information for `frame` as possible.
diff --git a/breakpad-symbols/src/sym_file/parser.rs b/breakpad-symbols/src/sym_file/parser.rs
index 31c1127..27be3cd 100644
--- a/breakpad-symbols/src/sym_file/parser.rs
+++ b/breakpad-symbols/src/sym_file/parser.rs
@@ -5,13 +5,10 @@ use failure::format_err;
 use log::warn;
 use nom::IResult::*;
 use nom::*;
-use range_map::Range;
+use range_map::{Range, RangeMap};
 
-use std::borrow::Cow;
 use std::collections::HashMap;
-use std::fs::File;
-use std::io::Read;
-use std::path::Path;
+use std::fmt::Debug;
 use std::str;
 use std::str::FromStr;
 
@@ -20,11 +17,13 @@ use minidump_common::traits::IntoRangeMapSafe;
 use crate::sym_file::types::*;
 use crate::SymbolError;
 
-enum Line<'a> {
+#[derive(Debug)]
+enum Line {
+    Module,
     Info(Info),
-    File(u32, &'a str),
+    File(u32, String),
     Public(PublicSymbol),
-    Function(Function),
+    Function(Function, Vec<SourceLine>),
     StackWin(WinFrameType),
     StackCfi(StackInfoCfi),
 }
@@ -35,7 +34,7 @@ named!(
     complete!(preceded!(many0!(char!('\r')), char!('\n')))
 );
 
-// Match a hex string, parse it to a u64.
+// Match a hex string, parse it t)o a u64.
 named!(hex_str_u64<&[u8], u64>,
        map_res!(map_res!(hex_digit, str::from_utf8), |s| u64::from_str_radix(s, 16)));
 
@@ -62,7 +61,7 @@ named!(module_line<&[u8], ()>,
     || {}
 ));
 
-// Matches an INFO LINE record.
+// Matches an INFO URL record.
 named!(
     info_url<&[u8], Info>,
     chain!(
@@ -87,7 +86,7 @@ named!(
 );
 
 // Matches a FILE record.
-named!(file_line<&[u8], (u32, &str)>,
+named!(file_line<&[u8], (u32, String)>,
   chain!(
     tag!("FILE") ~
     space ~
@@ -95,7 +94,7 @@ named!(file_line<&[u8], (u32, &str)>,
     space ~
     filename: map_res!(not_line_ending, str::from_utf8) ~
     my_eol ,
-      ||{ (id, filename) }
+      ||{ (id, filename.to_string()) }
 ));
 
 // Matches a PUBLIC record.
@@ -140,8 +139,8 @@ named!(func_line_data<&[u8], SourceLine>,
       }
 ));
 
-// Matches a FUNC record and any following line records.
-named!(func_lines<&[u8], Function>,
+// Matches a FUNC record.
+named!(func_line<&[u8], Function>,
 chain!(
   tag!("FUNC") ~
   preceded!(space, tag!("m"))? ~
@@ -153,25 +152,14 @@ chain!(
   parameter_size: hex_u32 ~
   space ~
   name: map_res!(not_line_ending, str::from_utf8) ~
-  my_eol ~
-  lines: many0!(func_line_data) ,
+  my_eol ,
     || {
         Function {
             address,
             size,
             parameter_size,
             name: name.to_string(),
-            lines: lines.into_iter()
-                .map(|l| {
-                    // Line data from PDB files often has a zero-size line entry, so just
-                    // filter those out.
-                    if l.size > 0 {
-                        (Some(Range::new(l.address, l.address + l.size as u64 - 1)), l)
-                    } else {
-                        (None, l)
-                    }
-                })
-                .into_rangemap_safe(),
+            lines: RangeMap::new(),
         }
     }
     ));
@@ -265,7 +253,7 @@ chain!(
     ));
 
 // Matches a STACK CFI INIT record.
-named!(stack_cfi_init<&[u8], (CfiRules, u32)>,
+named!(stack_cfi_init<&[u8], StackInfoCfi>,
   chain!(
     tag!("STACK CFI INIT") ~
     space ~
@@ -276,26 +264,13 @@ named!(stack_cfi_init<&[u8], (CfiRules, u32)>,
     rules: map_res!(not_line_ending, str::from_utf8) ~
     my_eol ,
       || {
-          (CfiRules {
-              address,
-              rules: rules.to_string(),
-          },
-           size)
-      }
-));
-
-// Match a STACK CFI INIT record followed by zero or more STACK CFI records.
-named!(stack_cfi_lines<&[u8], StackInfoCfi>,
-  chain!(
-    init: stack_cfi_init ~
-    mut add_rules: many0!(stack_cfi) ,
-      move || {
-          let (init_rules, size) = init;
-          add_rules.sort();
           StackInfoCfi {
-              init: init_rules,
+              init: CfiRules {
+                  address,
+                  rules: rules.to_string(),
+              },
               size,
-              add_rules,
+              add_rules: Default::default(),
           }
       }
 ));
@@ -307,167 +282,319 @@ named!(line<&[u8], Line>,
     info_line => { |_| Line::Info(Info::Unknown) } |
     file_line => { |(i,f)| Line::File(i, f) } |
     public_line => { Line::Public } |
-    func_lines => { Line::Function } |
+    func_line => { |f| Line::Function(f, Vec::new()) } |
     stack_win_line => { Line::StackWin } |
-    stack_cfi_lines => { Line::StackCfi }
+    stack_cfi_init => { Line::StackCfi } |
+    module_line => { |_| Line::Module }
 ));
 
-// Return a `SymbolFile` given a vec of `Line` data.
-fn symbol_file_from_lines(lines: Vec<Line<'_>>) -> SymbolFile {
-    let mut files = HashMap::new();
-    let mut publics = vec![];
-    let mut funcs = vec![];
-    let mut stack_cfi = vec![];
-    let mut stack_win_framedata: Vec<StackInfoWin> = vec![];
-    let mut stack_win_fpo: Vec<StackInfoWin> = vec![];
-    let mut url = None;
-    for line in lines {
-        match line {
-            Line::Info(Info::Url(cached_url)) => {
-                url = Some(cached_url);
-            }
-            Line::Info(Info::Unknown) => {
-                // Don't care
-            }
-            Line::File(id, filename) => {
-                files.insert(id, filename.to_string());
-            }
-            Line::Public(p) => {
-                publics.push(p);
+/// A parser for SymbolFiles.
+///
+/// This is basically just a SymbolFile but with some extra state
+/// to handle streaming parsing.
+///
+/// Use this by repeatedly calling [`parse_more`][] until the
+/// whole input is consumed. Then call [`finish`][].
+#[derive(Debug, Default)]
+pub struct SymbolParser {
+    files: HashMap<u32, String>,
+    publics: Vec<PublicSymbol>,
+
+    // When building a RangeMap when need to sort an array of this
+    // format anyway, so we might as well construct it directly and
+    // save a giant allocation+copy.
+    functions: Vec<(Range<u64>, Function)>,
+    cfi_stack_info: Vec<(Range<u64>, StackInfoCfi)>,
+    win_stack_framedata_info: Vec<(Range<u64>, StackInfoWin)>,
+    win_stack_fpo_info: Vec<(Range<u64>, StackInfoWin)>,
+    url: Option<String>,
+    pub lines: u64,
+    cur_item: Option<Line>,
+}
+
+impl SymbolParser {
+    /// Creates a new SymbolParser.
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Parses as much of the input as it can, and then returns
+    /// how many bytes of the input was used. The *unused* portion of the
+    /// input must be resubmitted on subsequent calls to parse_more
+    /// (along with more data so we can make progress on the parse).
+    pub fn parse_more(&mut self, mut input: &[u8]) -> Result<usize, SymbolError> {
+        // We parse the input line-by-line, so trim away any part of the input
+        // that comes after the last newline (this is necessary for streaming
+        // parsing, as it can otherwise be impossible to tell if a line is
+        // truncated.)
+        input = if let Some(idx) = input.iter().rposition(|&x| x == b'\n') {
+            &input[..idx + 1]
+        } else {
+            // If there's no newline, then we can't process anything!
+            return Ok(0);
+        };
+        // Remember the (truncated) input so that we can tell how many bytes
+        // we've consumed.
+        let orig_input = input;
+
+        loop {
+            // If there's no more input, then we've consumed all of it
+            // (except for the partial line we trimmed away).
+            if input.is_empty() {
+                return Ok(orig_input.len());
             }
-            Line::Function(f) => {
-                funcs.push(f);
+
+            // First check if we're currently processing sublines of a
+            // multi-line item like `FUNC` and `STACK CFI INIT`.
+            // If we are, parse the next line as its subline format.
+            //
+            // If we encounter an error, this probably means we've
+            // reached a new item (which ends this one). To handle this,
+            // we can just finish off the current item and resubmit this
+            // line to the top-level parser (below). If the line is
+            // genuinely corrupt, then the top-level parser will also
+            // fail to read it.
+            //
+            // We `take` and then reconstitute the item for borrowing/move
+            // reasons.
+            match self.cur_item.take() {
+                Some(Line::Function(cur, mut lines)) => match func_line_data(input) {
+                    Done(new_input, line) => {
+                        lines.push(line);
+                        input = new_input;
+                        self.cur_item = Some(Line::Function(cur, lines));
+                        self.lines += 1;
+                        continue;
+                    }
+                    Error(_) | Incomplete(_) => {
+                        self.finish_item(Line::Function(cur, lines));
+                        continue;
+                    }
+                },
+                Some(Line::StackCfi(mut cur)) => match stack_cfi(input) {
+                    Done(new_input, line) => {
+                        cur.add_rules.push(line);
+                        input = new_input;
+                        self.cur_item = Some(Line::StackCfi(cur));
+                        self.lines += 1;
+                        continue;
+                    }
+                    Error(_) | Incomplete(_) => {
+                        self.finish_item(Line::StackCfi(cur));
+                        continue;
+                    }
+                },
+                _ => {
+                    // We're not parsing sublines, move on to top level parser!
+                }
             }
-            Line::StackWin(frame_type) => {
-                // PDB files contain lots of overlapping unwind info, so we have to filter
-                // some of it out.
-                fn insert_win_stack_info(stack_win: &mut Vec<StackInfoWin>, info: StackInfoWin) {
-                    if let Some(memory_range) = info.memory_range() {
-                        if let Some(last) = stack_win.last_mut() {
-                            if last.memory_range().unwrap().intersects(&memory_range) {
-                                if info.address > last.address {
-                                    // Sometimes we get STACK WIN directives where each line
-                                    // has an accurate starting point, but the length just
-                                    // covers the entire function, like so:
-                                    //
-                                    // addr: 0, len: 10
-                                    // addr: 1, len: 9
-                                    // addr: 4, len: 6
-                                    //
-                                    // In this case, the next instruction is the one that
-                                    // really defines the length of the previous one. So
-                                    // we need to fixup the lengths like so:
-                                    //
-                                    // addr: 0, len: 1
-                                    // addr: 1, len: 2
-                                    // addr: 4, len: 6
-                                    last.size = (info.address - last.address) as u32;
-                                } else if last.memory_range().unwrap() != memory_range {
-                                    // We silently drop identical ranges because sometimes
-                                    // duplicates happen, but we complain for non-trivial duplicates.
-                                    warn!(
-                                        "STACK WIN entry had bad intersections, dropping it {:?}",
-                                        info
-                                    );
-                                    return;
+
+            // Parse a top-level item, and first handle the Result
+            let line = match line(input) {
+                Done(new_input, line) => {
+                    // Success! Advance the input.
+                    input = new_input;
+                    line
+                }
+                Error(e) => {
+                    // The file has a completely corrupt line,
+                    // conservatively reject the entire parse.
+                    return Err(SymbolError::ParseError(format_err!(
+                        "Failed to parse file: {}",
+                        e
+                    )));
+                }
+                Incomplete(_) => {
+                    // One of our sub-parsers wants more input, which normally
+                    // would be fine for a streaming parser, bust the newline
+                    // preprocessing we do means this should never happen.
+                    // So Incomplete input is just another kind of parsing Error.
+                    return Err(SymbolError::ParseError(format_err!("Line was incomplete!")));
+                }
+            };
+
+            // Now store the item in our partial SymbolFile (or make it the cur_item
+            // if it has potential sublines we need to parse first).
+            match line {
+                Line::Module => {
+                    // We don't use this but it MUST be the first line
+                    if self.lines != 0 {
+                        return Err(SymbolError::ParseError(format_err!(
+                            "MODULE line found after the start of the file"
+                        )));
+                    }
+                }
+                Line::Info(Info::Url(cached_url)) => {
+                    self.url = Some(cached_url);
+                }
+                Line::Info(Info::Unknown) => {
+                    // Don't care
+                }
+                Line::File(id, filename) => {
+                    self.files.insert(id, filename.to_string());
+                }
+                Line::Public(p) => {
+                    self.publics.push(p);
+                }
+                Line::StackWin(frame_type) => {
+                    // PDB files contain lots of overlapping unwind info, so we have to filter
+                    // some of it out.
+                    fn insert_win_stack_info(
+                        stack_win: &mut Vec<(Range<u64>, StackInfoWin)>,
+                        info: StackInfoWin,
+                    ) {
+                        if let Some(memory_range) = info.memory_range() {
+                            if let Some((last_range, last_info)) = stack_win.last_mut() {
+                                if last_range.intersects(&memory_range) {
+                                    if info.address > last_info.address {
+                                        // Sometimes we get STACK WIN directives where each line
+                                        // has an accurate starting point, but the length just
+                                        // covers the entire function, like so:
+                                        //
+                                        // addr: 0, len: 10
+                                        // addr: 1, len: 9
+                                        // addr: 4, len: 6
+                                        //
+                                        // In this case, the next instruction is the one that
+                                        // really defines the length of the previous one. So
+                                        // we need to fixup the lengths like so:
+                                        //
+                                        // addr: 0, len: 1
+                                        // addr: 1, len: 2
+                                        // addr: 4, len: 6
+                                        last_info.size = (info.address - last_info.address) as u32;
+                                        *last_range = last_info.memory_range().unwrap();
+                                    } else if *last_range != memory_range {
+                                        // We silently drop identical ranges because sometimes
+                                        // duplicates happen, but we complain for non-trivial duplicates.
+                                        warn!(
+                                            "STACK WIN entry had bad intersections, dropping it {:?}",
+                                            info
+                                        );
+                                        return;
+                                    }
                                 }
                             }
+                            stack_win.push((memory_range, info));
+                        } else {
+                            warn!("STACK WIN entry had invalid range, dropping it {:?}", info);
                         }
-                        stack_win.push(info);
-                    } else {
-                        warn!("STACK WIN entry had invalid range, dropping it {:?}", info);
                     }
-                }
-                match frame_type {
-                    WinFrameType::FrameData(s) => {
-                        insert_win_stack_info(&mut stack_win_framedata, s);
-                    }
-                    WinFrameType::Fpo(s) => {
-                        insert_win_stack_info(&mut stack_win_fpo, s);
+                    match frame_type {
+                        WinFrameType::FrameData(s) => {
+                            insert_win_stack_info(&mut self.win_stack_framedata_info, s);
+                        }
+                        WinFrameType::Fpo(s) => {
+                            insert_win_stack_info(&mut self.win_stack_fpo_info, s);
+                        }
+                        // Just ignore other types.
+                        _ => {}
                     }
-                    // Just ignore other types.
-                    _ => {}
+                }
+                item @ Line::Function(_, _) => {
+                    // More sublines to parse
+                    self.cur_item = Some(item);
+                }
+                item @ Line::StackCfi(_) => {
+                    // More sublines to parse
+                    self.cur_item = Some(item);
+                }
+            }
+
+            // Make note that we've consumed a line of input.
+            self.lines += 1;
+        }
+    }
+
+    /// Finish processing an item (cur_item) which had sublines.
+    /// We now have all the sublines, so it's complete.
+    fn finish_item(&mut self, item: Line) {
+        match item {
+            Line::Function(mut cur, lines) => {
+                cur.lines = lines
+                    .into_iter()
+                    .map(|l| {
+                        // Line data from PDB files often has a zero-size line entry, so just
+                        // filter those out.
+                        if l.size > 0 {
+                            (
+                                Some(Range::new(l.address, l.address + l.size as u64 - 1)),
+                                l,
+                            )
+                        } else {
+                            (None, l)
+                        }
+                    })
+                    .into_rangemap_safe();
+
+                if let Some(range) = cur.memory_range() {
+                    self.functions.push((range, cur));
                 }
             }
-            Line::StackCfi(s) => {
-                stack_cfi.push(s);
+            Line::StackCfi(mut cur) => {
+                cur.add_rules.sort();
+                if let Some(range) = cur.memory_range() {
+                    self.cfi_stack_info.push((range, cur));
+                }
+            }
+            _ => {
+                unreachable!()
             }
         }
     }
-    publics.sort();
-    SymbolFile {
-        files,
-        publics,
-        functions: funcs
-            .into_iter()
-            .map(|f| (f.memory_range(), f))
-            .into_rangemap_safe(),
-        cfi_stack_info: stack_cfi
-            .into_iter()
-            .map(|s| (s.memory_range(), s))
-            .into_rangemap_safe(),
-        win_stack_framedata_info: stack_win_framedata
-            .into_iter()
-            .map(|s| (s.memory_range(), s))
-            .into_rangemap_safe(),
-        win_stack_fpo_info: stack_win_fpo
-            .into_iter()
-            .map(|s| (s.memory_range(), s))
-            .into_rangemap_safe(),
-        // Will get filled in by the caller
-        url,
-        // TODO
-        ambiguities_repaired: 0,
-        // TODO
-        ambiguities_discarded: 0,
-        // TODO
-        corruptions_discarded: 0,
-        // TODO
-        cfi_eval_corruptions: 0,
+
+    /// Finish the parse and create the final SymbolFile.
+    ///
+    /// Call this when the parser has consumed all the input.
+    pub fn finish(mut self) -> SymbolFile {
+        // If there's a pending multiline item, finish it now.
+        if let Some(item) = self.cur_item.take() {
+            self.finish_item(item);
+        }
+
+        // Now sort everything and bundle it up in its final format.
+        self.publics.sort();
+
+        SymbolFile {
+            files: self.files,
+            publics: self.publics,
+            functions: into_rangemap_safe(self.functions),
+            cfi_stack_info: into_rangemap_safe(self.cfi_stack_info),
+            win_stack_framedata_info: into_rangemap_safe(self.win_stack_framedata_info),
+            win_stack_fpo_info: into_rangemap_safe(self.win_stack_fpo_info),
+            // Will get filled in by the caller
+            url: self.url,
+            ambiguities_repaired: 0,
+            ambiguities_discarded: 0,
+            corruptions_discarded: 0,
+            cfi_eval_corruptions: 0,
+        }
     }
 }
 
-// Matches an entire symbol file.
-named!(symbol_file<&[u8], SymbolFile>,
-  chain!(
-    module_line? ~
-    lines: many0!(line) ,
-    || { symbol_file_from_lines(lines) })
-);
+// Copied from minidump-common, because we've preconstructed the array to sort.
+fn into_rangemap_safe<V: Clone + Eq + Debug>(mut input: Vec<(Range<u64>, V)>) -> RangeMap<u64, V> {
+    input.sort_by_key(|x| x.0);
+    let mut vec: Vec<(Range<u64>, V)> = Vec::with_capacity(input.len());
+    for (range, val) in input {
+        if let Some((last_range, last_val)) = vec.last_mut() {
+            if range.start <= last_range.end && val != *last_val {
+                continue;
+            }
 
-/// Parse a `SymbolFile` from `bytes`.
-pub fn parse_symbol_bytes(bytes: &[u8]) -> Result<SymbolFile, SymbolError> {
-    match symbol_file(bytes) {
-        Done(rest, symfile) => {
-            if rest == b"" {
-                Ok(symfile)
-            } else {
-                // Junk left over, or maybe didn't parse anything.
-                let next_line = rest
-                    .split(|b| *b == b'\r')
-                    .next()
-                    .map(String::from_utf8_lossy)
-                    .unwrap_or(Cow::Borrowed(""));
-                Err(format_err!(
-                    "Failed to parse file, next line was: `{}`",
-                    next_line
-                ))
+            if range.start <= last_range.end.saturating_add(1) && &val == last_val {
+                last_range.end = std::cmp::max(range.end, last_range.end);
+                continue;
             }
         }
-        Error(e) => Err(format_err!("Failed to parse file: {}", e)),
-        Incomplete(_) => Err(format_err!("Failed to parse file: incomplete data")),
+        vec.push((range, val));
     }
-    .map_err(SymbolError::ParseError)
+    RangeMap::from_sorted_vec(vec)
 }
 
-/// Parse a `SymbolFile` from `path`.
-pub fn parse_symbol_file(path: &Path) -> Result<SymbolFile, SymbolError> {
-    let mut f = File::open(path)
-        .map_err(|e| SymbolError::LoadError(format_err!("Failed to open file: {}", e)))?;
-    let mut bytes = vec![];
-    f.read_to_end(&mut bytes)
-        .map_err(|e| SymbolError::LoadError(format_err!("Failed to read file: {}", e)))?;
-    parse_symbol_bytes(&bytes)
+#[cfg(test)]
+fn parse_symbol_bytes(data: &[u8]) -> Result<SymbolFile, SymbolError> {
+    SymbolFile::parse(data, |_| ())
 }
 
 #[test]
@@ -521,14 +648,17 @@ fn test_info_url() {
 fn test_file_line() {
     let line = b"FILE 1 foo.c\n";
     let rest = &b""[..];
-    assert_eq!(file_line(line), Done(rest, (1, "foo.c")));
+    assert_eq!(file_line(line), Done(rest, (1, String::from("foo.c"))));
 }
 
 #[test]
 fn test_file_line_spaces() {
     let line = b"FILE  1234  foo bar.xyz\n";
     let rest = &b""[..];
-    assert_eq!(file_line(line), Done(rest, (1234, "foo bar.xyz")));
+    assert_eq!(
+        file_line(line),
+        Done(rest, (1234, String::from("foo bar.xyz")))
+    );
 }
 
 #[test]
@@ -571,7 +701,7 @@ fn test_func_lines_no_lines() {
     let line = b"FUNC c184 30 0 nsQueryInterfaceWithError::operator()(nsID const&, void**) const\n";
     let rest = &b""[..];
     assert_eq!(
-        func_lines(line),
+        func_line(line),
         Done(
             rest,
             Function {
@@ -593,56 +723,53 @@ fn test_func_lines_and_lines() {
 1010 10 52 8
 1020 10 62 15
 ";
-    if let Done(rest, f) = func_lines(data) {
-        assert_eq!(rest, &b""[..]);
-        assert_eq!(f.address, 0x1000);
-        assert_eq!(f.size, 0x30);
-        assert_eq!(f.parameter_size, 0x10);
-        assert_eq!(f.name, "some func".to_string());
-        assert_eq!(
-            f.lines.get(0x1000).unwrap(),
-            &SourceLine {
-                address: 0x1000,
-                size: 0x10,
-                file: 7,
-                line: 42,
-            }
-        );
-        assert_eq!(
-            f.lines.ranges_values().collect::<Vec<_>>(),
-            vec![
-                &(
-                    Range::<u64>::new(0x1000, 0x100F),
-                    SourceLine {
-                        address: 0x1000,
-                        size: 0x10,
-                        file: 7,
-                        line: 42,
-                    },
-                ),
-                &(
-                    Range::<u64>::new(0x1010, 0x101F),
-                    SourceLine {
-                        address: 0x1010,
-                        size: 0x10,
-                        file: 8,
-                        line: 52,
-                    },
-                ),
-                &(
-                    Range::<u64>::new(0x1020, 0x102F),
-                    SourceLine {
-                        address: 0x1020,
-                        size: 0x10,
-                        file: 15,
-                        line: 62,
-                    },
-                ),
-            ]
-        );
-    } else {
-        panic!("Failed to parse!");
-    }
+    let file = SymbolFile::from_bytes(data).expect("failed to parse!");
+    let (_, f) = file.functions.ranges_values().next().unwrap();
+    assert_eq!(f.address, 0x1000);
+    assert_eq!(f.size, 0x30);
+    assert_eq!(f.parameter_size, 0x10);
+    assert_eq!(f.name, "some func".to_string());
+    assert_eq!(
+        f.lines.get(0x1000).unwrap(),
+        &SourceLine {
+            address: 0x1000,
+            size: 0x10,
+            file: 7,
+            line: 42,
+        }
+    );
+    assert_eq!(
+        f.lines.ranges_values().collect::<Vec<_>>(),
+        vec![
+            &(
+                Range::<u64>::new(0x1000, 0x100F),
+                SourceLine {
+                    address: 0x1000,
+                    size: 0x10,
+                    file: 7,
+                    line: 42,
+                },
+            ),
+            &(
+                Range::<u64>::new(0x1010, 0x101F),
+                SourceLine {
+                    address: 0x1010,
+                    size: 0x10,
+                    file: 8,
+                    line: 52,
+                },
+            ),
+            &(
+                Range::<u64>::new(0x1020, 0x102F),
+                SourceLine {
+                    address: 0x1020,
+                    size: 0x10,
+                    file: 15,
+                    line: 62,
+                },
+            ),
+        ]
+    );
 }
 
 #[test]
@@ -652,11 +779,8 @@ fn test_func_with_m() {
 1010 10 52 8
 1020 10 62 15
 ";
-    if let Done(rest, _) = func_lines(data) {
-        assert_eq!(rest, &b""[..]);
-    } else {
-        panic!("Failed to parse!");
-    }
+    let file = SymbolFile::from_bytes(data).expect("failed to parse!");
+    let (_, _f) = file.functions.ranges_values().next().unwrap();
 }
 
 #[test]
@@ -736,13 +860,14 @@ fn test_stack_cfi_init() {
         stack_cfi_init(line),
         Done(
             rest,
-            (
-                CfiRules {
+            StackInfoCfi {
+                init: CfiRules {
                     address: 0xbadf00d,
                     rules: "init rules".to_string(),
                 },
-                0xabc
-            )
+                size: 0xabc,
+                add_rules: vec![],
+            }
         )
     );
 }
@@ -753,29 +878,27 @@ fn test_stack_cfi_lines() {
 STACK CFI deadf00d some rules
 STACK CFI deadbeef more rules
 ";
-    let rest = &b""[..];
+    let file = SymbolFile::from_bytes(data).expect("failed to parse!");
+    let (_, cfi) = file.cfi_stack_info.ranges_values().next().unwrap();
     assert_eq!(
-        stack_cfi_lines(data),
-        Done(
-            rest,
-            StackInfoCfi {
-                init: CfiRules {
-                    address: 0xbadf00d,
-                    rules: "init rules".to_string(),
+        cfi,
+        &StackInfoCfi {
+            init: CfiRules {
+                address: 0xbadf00d,
+                rules: "init rules".to_string(),
+            },
+            size: 0xabc,
+            add_rules: vec![
+                CfiRules {
+                    address: 0xdeadbeef,
+                    rules: "more rules".to_string(),
                 },
-                size: 0xabc,
-                add_rules: vec![
-                    CfiRules {
-                        address: 0xdeadbeef,
-                        rules: "more rules".to_string(),
-                    },
-                    CfiRules {
-                        address: 0xdeadf00d,
-                        rules: "some rules".to_string(),
-                    },
-                ],
-            }
-        )
+                CfiRules {
+                    address: 0xdeadf00d,
+                    rules: "some rules".to_string(),
+                },
+            ],
+        }
     );
 }
 
diff --git a/minidump-processor/src/processor.rs b/minidump-processor/src/processor.rs
index a202bbe..35f0b9b 100644
--- a/minidump-processor/src/processor.rs
+++ b/minidump-processor/src/processor.rs
@@ -197,7 +197,7 @@ where
     let (crash_reason, crash_address, crashing_thread_id) = if let Some(exception) = exception_ref {
         (
             Some(exception.get_crash_reason(system_info.os, system_info.cpu)),
-            Some(exception.get_crash_address(system_info.os)),
+            Some(exception.get_crash_address(system_info.os, system_info.cpu)),
             Some(exception.get_crashing_thread_id()),
         )
     } else {
diff --git a/minidump/README.md b/minidump/README.md
index e510484..03891fd 100644
--- a/minidump/README.md
+++ b/minidump/README.md
@@ -9,16 +9,16 @@ If you want richer analysis of the minidump (such as stackwalking and symbolicat
 
 # Usage
 
-The primary API for this library is the [`Minidump`][] struct, which can be
-instantiated by calling the [`Minidump::read`][] or [`Minidump::read_path`][] methods.
+The primary API for this library is the `Minidump` struct, which can be
+instantiated by calling the `Minidump::read` or `Minidump::read_path` methods.
 
 Succesfully parsing a Minidump struct means the minidump has a minimally valid
 header and stream directory. Individual streams are only parsed when they're
 requested.
 
 Although you may enumerate the streams in a minidump with methods like
-[`Minidump::all_streams`][], this is only really useful for debugging. Instead
-you should statically request streams with [`Minidump::get_stream`][].
+`Minidump::all_streams`, this is only really useful for debugging. Instead
+you should statically request streams with `Minidump::get_stream`.
 
 Depending on what analysis you're trying to perform, you may:
 
diff --git a/minidump/src/minidump.rs b/minidump/src/minidump.rs
index ad84f52..4dc328d 100644
--- a/minidump/src/minidump.rs
+++ b/minidump/src/minidump.rs
@@ -3732,6 +3732,16 @@ impl<'a> MinidumpStream<'a> for MinidumpException<'a> {
 }
 
 impl<'a> MinidumpException<'a> {
+    /// Get the cpu context of the crashing (or otherwise minidump-requesting) thread.
+    ///
+    /// CPU contexts are a platform-specific format, so SystemInfo is required
+    /// to reliably parse them. We used to use heuristics to avoid this requirement,
+    /// but this made us too brittle to otherwise-backwards-compatible additions
+    /// to the format.
+    ///
+    /// MiscInfo can contain additional details on the cpu context's format, but
+    /// is optional because those details can be safely ignored (at the cost of
+    /// being unable to parse some very obscure cpu state).
     pub fn context(
         &self,
         system_info: &MinidumpSystemInfo,
@@ -3742,9 +3752,18 @@ impl<'a> MinidumpException<'a> {
             .map(Cow::Owned)
     }
 
-    /// Get the crash address for an exception.
-    pub fn get_crash_address(&self, os: Os) -> u64 {
-        match (
+    /// Get the address that "caused" the crash.
+    ///
+    /// The meaning of this value depends on the kind of crash this was.
+    ///
+    /// By default, it's the instruction pointer at the time of the crash.
+    /// However, if the crash was caused by an illegal memory access, the
+    /// the address would be the memory address.
+    ///
+    /// So for instance, if you crashed from dereferencing a null pointer,
+    /// the crash_address will be 0 (or close to it, due to offsets).
+    pub fn get_crash_address(&self, os: Os, cpu: Cpu) -> u64 {
+        let addr = match (
             os,
             md::ExceptionCodeWindows::from_u32(self.raw.exception_record.exception_code),
         ) {
@@ -3755,14 +3774,33 @@ impl<'a> MinidumpException<'a> {
                 self.raw.exception_record.exception_information[1]
             }
             _ => self.raw.exception_record.exception_address,
+        };
+
+        // Sometimes on 32-bit these values can be incorrectly sign-extended,
+        // so mask and zero-extend them here.
+        match cpu.pointer_width() {
+            Some(4) => addr as u32 as u64,
+            _ => addr,
         }
     }
 
     /// Get the crash reason for an exception.
+    ///
+    /// The returned value reflects our best attempt to recover a
+    /// "native" error for the crashing system based on the OS and
+    /// things like raw error codes.
+    ///
+    /// This is an imperfect process, because OSes may have overlapping
+    /// error types (e.g. WinError and NTSTATUS overlap, so we have to
+    /// pick one arbirarily).
+    ///
+    /// The raw error codes can be extracted from [MinidumpException::raw][].
     pub fn get_crash_reason(&self, os: Os, cpu: Cpu) -> CrashReason {
         CrashReason::from_exception(&self.raw, os, cpu)
     }
 
+    /// The id of the thread that caused the crash (or otherwise requested
+    /// the minidump, even if there wasn't actually a crash).
     pub fn get_crashing_thread_id(&self) -> u32 {
         self.thread_id
     }
@@ -4539,13 +4577,14 @@ fn stream_vendor(stream_type: u32) -> &'static str {
 mod test {
     use super::*;
     use md::GUID;
+    use minidump_common::format::ProcessorArchitecture;
     use std::mem;
     use synth_minidump::{
-        self, AnnotationValue, CrashpadInfo, DumpString, Memory, MemoryInfo as SynthMemoryInfo,
-        MiscFieldsBuildString, MiscFieldsPowerInfo, MiscFieldsProcessTimes, MiscFieldsTimeZone,
-        MiscInfo5Fields, MiscStream, Module as SynthModule, ModuleCrashpadInfo, SimpleStream,
-        SynthMinidump, SystemInfo, Thread, ThreadName, UnloadedModule as SynthUnloadedModule,
-        STOCK_VERSION_INFO,
+        self, AnnotationValue, CrashpadInfo, DumpString, Exception, Memory,
+        MemoryInfo as SynthMemoryInfo, MiscFieldsBuildString, MiscFieldsPowerInfo,
+        MiscFieldsProcessTimes, MiscFieldsTimeZone, MiscInfo5Fields, MiscStream,
+        Module as SynthModule, ModuleCrashpadInfo, SimpleStream, SynthMinidump, SystemInfo, Thread,
+        ThreadName, UnloadedModule as SynthUnloadedModule, STOCK_VERSION_INFO,
     };
     use test_assembler::*;
 
@@ -5757,4 +5796,55 @@ c70206ca83eb2852-de0206ca83eb2852  -w-s  10bac9000 fd:05 1196511 /usr/lib64/libt
             MinidumpAnnotation::Invalid
         );
     }
+
+    #[test]
+    fn test_exception_x86() {
+        // Defaults to x86
+        let system_info = SystemInfo::new(Endian::Little);
+
+        let mut exception = Exception::new(Endian::Little);
+
+        // Check that we clear the erroneous high bits for 32-bit
+        exception.exception_record.exception_address = 0xf0e1_d2c3_b4a5_9687;
+        // FIXME: test other fields too
+
+        let dump = SynthMinidump::with_endian(Endian::Little)
+            .add_system_info(system_info)
+            .add_exception(exception);
+
+        let dump = read_synth_dump(dump).unwrap();
+
+        let system_stream = dump.get_stream::<MinidumpSystemInfo>().unwrap();
+        let exception_stream = dump.get_stream::<MinidumpException>().unwrap();
+        assert_eq!(
+            exception_stream.get_crash_address(system_stream.os, system_stream.cpu),
+            0xb4a5_9687
+        );
+    }
+
+    #[test]
+    fn test_exception_x64() {
+        // Defaults to x86
+        let system_info = SystemInfo::new(Endian::Little)
+            .set_processor_architecture(ProcessorArchitecture::PROCESSOR_ARCHITECTURE_AMD64 as u16);
+
+        let mut exception = Exception::new(Endian::Little);
+
+        // Check that we don't truncate this on 64-bit
+        exception.exception_record.exception_address = 0xf0e1_d2c3_b4a5_9687;
+        // FIXME: test other fields too
+
+        let dump = SynthMinidump::with_endian(Endian::Little)
+            .add_system_info(system_info)
+            .add_exception(exception);
+
+        let dump = read_synth_dump(dump).unwrap();
+
+        let system_stream = dump.get_stream::<MinidumpSystemInfo>().unwrap();
+        let exception_stream = dump.get_stream::<MinidumpException>().unwrap();
+        assert_eq!(
+            exception_stream.get_crash_address(system_stream.os, system_stream.cpu),
+            0xf0e1_d2c3_b4a5_9687
+        );
+    }
 }
diff --git a/synth-minidump/src/lib.rs b/synth-minidump/src/lib.rs
index 1f7f9ca..042a052 100644
--- a/synth-minidump/src/lib.rs
+++ b/synth-minidump/src/lib.rs
@@ -29,6 +29,8 @@ pub struct SynthMinidump {
     stream_directory: Section,
     /// System info (cpu arch, os, etc.)
     system_info: Option<SystemInfo>,
+    /// High level crash info (error code, crash address, assertion, ...)
+    exception: Option<Exception>,
     /// List of modules in this minidump.
     module_list: Option<ListStream<Module>>,
     /// List of unloaded modules in this minidump.
@@ -170,6 +172,7 @@ impl SynthMinidump {
             stream_directory_rva,
             stream_directory: Section::with_endian(endian),
             system_info: None,
+            exception: None,
             module_list: Some(ListStream::new(
                 md::MINIDUMP_STREAM_TYPE::ModuleListStream,
                 endian,
@@ -287,11 +290,18 @@ impl SynthMinidump {
         self
     }
 
+    /// Set the SystemInfo stream.
     pub fn add_system_info(mut self, system_info: SystemInfo) -> Self {
         self.system_info = Some(system_info);
         self
     }
 
+    /// Set the Exception stream.
+    pub fn add_exception(mut self, exception: Exception) -> Self {
+        self.exception = Some(exception);
+        self
+    }
+
     /// Set the contents of the `LinuxMaps` stream.
     pub fn set_linux_maps(mut self, maps: &[u8]) -> SynthMinidump {
         self.linux_maps = Some(SimpleStream {
@@ -397,6 +407,9 @@ impl SynthMinidump {
         if let Some(stream) = self.system_info.take() {
             self = self.add_stream(stream);
         }
+        if let Some(stream) = self.exception.take() {
+            self = self.add_stream(stream);
+        }
         if let Some(stream) = self.linux_maps.take() {
             self = self.add_stream(stream);
         }
@@ -1650,6 +1663,7 @@ pub struct SystemInfo {
 }
 
 pub enum CpuInfo {
+    // Note, even if you're not on x86 this is a fine default.
     X86CpuInfo {
         vendor_id: [u32; 3],
         version_information: u32,
@@ -1732,6 +1746,76 @@ impl Stream for SystemInfo {
     }
 }
 
+pub struct Exception {
+    section: Section,
+    pub thread_id: u32,
+    // __align: u32,
+    pub exception_record: ExceptionRecord,
+    // TODO: implement this LOCATION_DESCRIPTOR properly
+    pub thread_context: (u32, u32),
+}
+
+pub struct ExceptionRecord {
+    pub exception_code: u32,
+    pub exception_flags: u32,
+    pub exception_record: u64,
+    pub exception_address: u64,
+    pub number_parameters: u32,
+    // __align: u32,
+    pub exception_information: [u64; 15],
+}
+
+impl Exception {
+    pub fn new(endian: Endian) -> Self {
+        Self {
+            section: Section::with_endian(endian),
+            thread_id: 0,
+            exception_record: ExceptionRecord {
+                exception_code: 0,
+                exception_flags: 0,
+                exception_record: 0,
+                exception_address: 0,
+                number_parameters: 0,
+                exception_information: [0; 15],
+            },
+            thread_context: (0, 0),
+        }
+    }
+}
+
+impl_dumpsection!(Exception);
+
+impl From<Exception> for Section {
+    fn from(info: Exception) -> Self {
+        let mut section = info
+            .section
+            .D32(info.thread_id)
+            .D32(0) // __align
+            .D32(info.exception_record.exception_code)
+            .D32(info.exception_record.exception_flags)
+            .D64(info.exception_record.exception_record)
+            .D64(info.exception_record.exception_address)
+            .D32(info.exception_record.number_parameters)
+            .D32(0); // __align
+
+        for &chunk in &info.exception_record.exception_information {
+            section = section.D64(chunk);
+        }
+
+        section = section
+            .D32(info.thread_context.0)
+            .D32(info.thread_context.1);
+
+        section
+    }
+}
+
+impl Stream for Exception {
+    fn stream_type(&self) -> u32 {
+        md::MINIDUMP_STREAM_TYPE::ExceptionStream.into()
+    }
+}
+
 #[test]
 fn test_dump_header() {
     let dump = SynthMinidump::with_endian(Endian::Little).flags(0x9f738b33685cc84c);


Test patch:
diff --git a/minidump-stackwalk/tests/test-minidump-stackwalk.rs b/minidump-stackwalk/tests/test-minidump-stackwalk.rs
index a847eda..5337ac3 100644
--- a/minidump-stackwalk/tests/test-minidump-stackwalk.rs
+++ b/minidump-stackwalk/tests/test-minidump-stackwalk.rs
@@ -657,7 +657,10 @@ fn test_unloaded_json() {
     assert_eq!(stderr, "");
 }
 
+// Temporarily disabled because it races with the _json version
+// on creating the minidump via `unloaded_minidump`.
 #[test]
+#[ignore]
 fn test_unloaded_human() {
     let synth_path = unloaded_minidump();