Limit recursion and output size (#48)

* Limit recursion and output size

This commit adds some rudimentary fuzzing for this crate and fixes the
fuzz bugs that it immediately encountered, namely very large recurison
and huge amounts of output. A recursion depth is added to the v0 printer
as well as a limited output printing. For now these values are not
configurable, but we could perhaps in the future add configuration if
necessary.

Closes #47

* Tweak CI config
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 61465ce..6ae8d0c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,17 +9,28 @@
       matrix:
         rust: [stable, beta, nightly]
     steps:
-    - uses: actions/checkout@master
+    - uses: actions/checkout@v2
     - name: Install Rust
       run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }}
     - run: cargo build --all
     - run: cargo test --all
 
+  fuzz_targets:
+    name: Fuzz Targets
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    # Note that building with fuzzers requires nightly since it uses unstable
+    # flags to rustc.
+    - run: rustup update nightly && rustup default nightly
+    - run: cargo install cargo-fuzz --vers "^0.10"
+    - run: cargo fuzz build --dev
+
   rustfmt:
     name: Rustfmt
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@master
+    - uses: actions/checkout@v2
     - name: Install Rust
       run: rustup update stable && rustup default stable && rustup component add rustfmt
     - run: cargo fmt -- --check
@@ -28,7 +39,7 @@
     name: Publish Documentation
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@master
+      - uses: actions/checkout@v2
       - name: Install Rust
         run: rustup update stable && rustup default stable
       - name: Build documentation
@@ -40,4 +51,4 @@
           git add .
           git -c user.name='ci' -c user.email='ci' commit -m init
           git push -f -q https://git:${{ secrets.github_token }}@github.com/${{ github.repository }} HEAD:gh-pages
-        if: github.event_name == 'push' && github.event.ref == 'refs/heads/master'
+        if: github.event_name == 'push' && github.event.ref == 'refs/heads/main'
diff --git a/Cargo.toml b/Cargo.toml
index 1ca52ee..5121bd6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,7 @@
 """
 
 [workspace]
-members = ["crates/capi"]
+members = ["crates/capi", "fuzz"]
 
 [dependencies]
 core = { version = '1.0.0', optional = true, package = 'rustc-std-workspace-core' }
diff --git a/fuzz/.gitignore b/fuzz/.gitignore
new file mode 100644
index 0000000..572e03b
--- /dev/null
+++ b/fuzz/.gitignore
@@ -0,0 +1,4 @@
+
+target
+corpus
+artifacts
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
new file mode 100644
index 0000000..4b7533f
--- /dev/null
+++ b/fuzz/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "rustc-demangle-fuzz"
+version = "0.0.0"
+authors = ["Automatically generated"]
+publish = false
+edition = "2018"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+rustc-demangle = { path = '..' }
+
+[[bin]]
+name = "demangle"
+path = "fuzz_targets/demangle.rs"
+test = false
+doc = false
diff --git a/fuzz/fuzz_targets/demangle.rs b/fuzz/fuzz_targets/demangle.rs
new file mode 100644
index 0000000..c1f7e87
--- /dev/null
+++ b/fuzz/fuzz_targets/demangle.rs
@@ -0,0 +1,14 @@
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+use std::fmt::Write;
+
+fuzz_target!(|data: &str| {
+    let mut s = String::new();
+    let sym = rustc_demangle::demangle(data);
+    drop(write!(s, "{}", sym));
+    s.truncate(0);
+
+    if let Ok(sym) = rustc_demangle::try_demangle(data) {
+        drop(write!(s, "{}", sym));
+    }
+});
diff --git a/src/lib.rs b/src/lib.rs
index 2b8684e..7adc0c7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -376,4 +376,23 @@
             "<core::result::Result<!, E> as std::process::Termination>::report::hfc41d0da4a40b3e8"
         );
     }
+
+    #[test]
+    fn limit_recursion() {
+        use std::fmt::Write;
+        let mut s = String::new();
+        assert!(write!(s, "{}", super::demangle("_RNvB_1a")).is_err());
+    }
+
+    #[test]
+    fn limit_output() {
+        use std::fmt::Write;
+        let mut s = String::new();
+        assert!(write!(
+            s,
+            "{}",
+            super::demangle("RYFG_FGyyEvRYFF_EvRYFFEvERLB_B_B_ERLRjB_B_B_")
+        )
+        .is_err());
+    }
 }
diff --git a/src/v0.rs b/src/v0.rs
index 4fd6cf0..f8696c8 100644
--- a/src/v0.rs
+++ b/src/v0.rs
@@ -1,6 +1,17 @@
 use core::char;
 use core::fmt;
-use core::fmt::Display;
+use core::fmt::{Display, Write};
+
+// Maximum recursion depth when printing symbols before we just bail out saying
+// "this symbol is invalid"
+const MAX_DEPTH: u32 = 1_000;
+
+// Approximately the maximum size of the symbol that we'll print. This is
+// approximate because it only limits calls writing to `LimitedFormatter`, but
+// not all writes exclusively go through `LimitedFormatter`. Some writes go
+// directly to the underlying formatter, but when that happens we always write
+// at least a little to the `LimitedFormatter`.
+const MAX_APPROX_SIZE: usize = 1_000_000;
 
 /// Representation of a demangled symbol name.
 pub struct Demangle<'a> {
@@ -58,13 +69,18 @@
 
 impl<'s> Display for Demangle<'s> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let mut remaining = MAX_APPROX_SIZE;
         let mut printer = Printer {
             parser: Ok(Parser {
                 sym: self.inner,
                 next: 0,
             }),
-            out: f,
+            out: LimitedFormatter {
+                remaining: &mut remaining,
+                inner: f,
+            },
             bound_lifetime_depth: 0,
+            depth: 0,
         };
         printer.print_path(true)
     }
@@ -563,8 +579,9 @@
 
 struct Printer<'a, 'b: 'a, 's> {
     parser: Result<Parser<'s>, Invalid>,
-    out: &'a mut fmt::Formatter<'b>,
+    out: LimitedFormatter<'a, 'b>,
     bound_lifetime_depth: u32,
+    depth: u32,
 }
 
 /// Mark the parser as errored, print `?` and return early.
@@ -603,12 +620,25 @@
         self.parser_mut().map(|p| p.eat(b)) == Ok(true)
     }
 
+    fn bump_depth(&mut self) -> fmt::Result {
+        self.depth += 1;
+        if self.depth > MAX_DEPTH {
+            Err(fmt::Error)
+        } else {
+            Ok(())
+        }
+    }
+
     /// Return a nested parser for a backref.
     fn backref_printer<'c>(&'c mut self) -> Printer<'c, 'b, 's> {
         Printer {
             parser: self.parser_mut().and_then(|p| p.backref()),
-            out: self.out,
+            out: LimitedFormatter {
+                remaining: self.out.remaining,
+                inner: self.out.inner,
+            },
             bound_lifetime_depth: self.bound_lifetime_depth,
+            depth: self.depth,
         }
     }
 
@@ -625,11 +655,11 @@
                 // Try to print lifetimes alphabetically first.
                 if depth < 26 {
                     let c = (b'a' + depth as u8) as char;
-                    c.fmt(self.out)
+                    c.fmt(self.out.inner)
                 } else {
                     // Use `'_123` after running out of letters.
                     self.out.write_str("_")?;
-                    depth.fmt(self.out)
+                    depth.fmt(self.out.inner)
                 }
             }
             None => invalid!(self),
@@ -684,16 +714,17 @@
     }
 
     fn print_path(&mut self, in_value: bool) -> fmt::Result {
+        self.bump_depth()?;
         let tag = parse!(self, next);
         match tag {
             b'C' => {
                 let dis = parse!(self, disambiguator);
                 let name = parse!(self, ident);
 
-                name.fmt(self.out)?;
-                if !self.out.alternate() {
+                name.fmt(self.out.inner)?;
+                if !self.out.inner.alternate() {
                     self.out.write_str("[")?;
-                    fmt::LowerHex::fmt(&dis, self.out)?;
+                    fmt::LowerHex::fmt(&dis, self.out.inner)?;
                     self.out.write_str("]")?;
                 }
             }
@@ -712,14 +743,14 @@
                         match ns {
                             'C' => self.out.write_str("closure")?,
                             'S' => self.out.write_str("shim")?,
-                            _ => ns.fmt(self.out)?,
+                            _ => ns.fmt(self.out.inner)?,
                         }
                         if !name.ascii.is_empty() || !name.punycode.is_empty() {
                             self.out.write_str(":")?;
-                            name.fmt(self.out)?;
+                            name.fmt(self.out.inner)?;
                         }
                         self.out.write_str("#")?;
-                        dis.fmt(self.out)?;
+                        dis.fmt(self.out.inner)?;
                         self.out.write_str("}")?;
                     }
 
@@ -727,7 +758,7 @@
                     None => {
                         if !name.ascii.is_empty() || !name.punycode.is_empty() {
                             self.out.write_str("::")?;
-                            name.fmt(self.out)?;
+                            name.fmt(self.out.inner)?;
                         }
                     }
                 }
@@ -761,6 +792,7 @@
             }
             _ => invalid!(self),
         }
+        self.depth -= 1;
         Ok(())
     }
 
@@ -782,6 +814,7 @@
             return self.out.write_str(ty);
         }
 
+        self.bump_depth()?;
         match tag {
             b'R' | b'Q' => {
                 self.out.write_str("&")?;
@@ -898,6 +931,7 @@
                 self.print_path(false)?;
             }
         }
+        self.depth -= 1;
         Ok(())
     }
 
@@ -932,7 +966,7 @@
             }
 
             let name = parse!(self, ident);
-            name.fmt(self.out)?;
+            name.fmt(self.out.inner)?;
             self.out.write_str(" = ")?;
             self.print_type()?;
         }
@@ -971,7 +1005,7 @@
             _ => invalid!(self),
         };
 
-        if !self.out.alternate() {
+        if !self.out.inner.alternate() {
             self.out.write_str(": ")?;
             let ty = basic_type(ty_tag).unwrap();
             self.out.write_str(ty)?;
@@ -993,7 +1027,7 @@
         for c in hex.chars() {
             v = (v << 4) | (c.to_digit(16).unwrap() as u64);
         }
-        v.fmt(self.out)
+        v.fmt(self.out.inner)
     }
 
     fn print_const_int(&mut self) -> fmt::Result {
@@ -1156,3 +1190,20 @@
         );
     }
 }
+
+struct LimitedFormatter<'a, 'b> {
+    remaining: &'a mut usize,
+    inner: &'a mut fmt::Formatter<'b>,
+}
+
+impl Write for LimitedFormatter<'_, '_> {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        match self.remaining.checked_sub(s.len()) {
+            Some(amt) => {
+                *self.remaining = amt;
+                self.inner.write_str(s)
+            }
+            None => Err(fmt::Error),
+        }
+    }
+}