Repository: eyre-rs/eyre (Rust)

Issue:
Segfault in `Report::downcast()` introduced in 0.6.9
I recently had a test segfault after updating to the latest eyre (0.6.11). The same test passes on 0.6.8 but segfaults on 0.6.9 or later.

```
error: test failed, to rerun pass `--bin ...`

Caused by:
  process didn't exit successfully: ... (signal: 11, SIGSEGV: invalid memory reference)
```

Here is a minimal reproduction:
```rust
use eyre::Context;

fn main() {
    let err = trigger().unwrap_err();
    let _ = err.downcast::<MyError>();
}

#[derive(Debug, thiserror::Error)]
enum MyError {
    #[error("error building http client")]
    ClientConstruction,
    #[error(transparent)]
    SsmError(#[from] aws_sdk_ssm::Error),
}

fn trigger() -> eyre::Result<()> {
    do_thing().wrap_err("failed")?;
    Ok(())
}

fn do_thing() -> Result<(), MyError> {
    Err(MyError::ClientConstruction)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_me() {
        let err = trigger().unwrap_err();
        let _ = err.downcast::<MyError>();
    }
}
```

Cargo.toml deps
```toml
[dependencies]
eyre = "0.6.9" # or later
thiserror = "1"
aws-sdk-ssm = "1"
```

I included the test because `cargo test` gives slightly more info.

Something about the second error variant is related, even though that variant isn't constructed.
I used `aws_sdk_ssm::Error` in the error variant because that's how I hit the issue.

The issue seems to be in src/error.rs, in `context_drop_rest()`, which was changed in 0.6.9. Any reason why this code differs from `anyhow` ?


Discussion:
Additional details:
* Mac M1 (macOS 14.2.1)
* Rust 1.74.1 (also tried on 1.74.0 and 1.73.0, same result)
I get the same segfault also on latest Manjaro Linux (x86-64), using Rust 1.74.1.
I've managed to simplify the reproduction case to remove dependencies (`thiserror` and `aws-sdk-ssm` are off the hook):

```rust
#![allow(dead_code)]
use std::{error::Error, fmt::Display};

use eyre::Context;

fn main() {
    let err = trigger().unwrap_err();
    let _ = err.downcast::<MyError>();
    println!("it worked!");
}

#[derive(Debug)]
enum MyError {
    ClientConstruction,
    Other(String),
}

impl Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyError error")
    }
}
impl Error for MyError {}

fn trigger() -> eyre::Result<()> {
    Err(MyError::ClientConstruction).wrap_err("failed")
}
```

The failure I get is slightly different (on a Mac M1 Sonoma 14.2) :
```
eyre_test(48377,0x1e194d000) malloc: *** error for object 0x1025c23ff: pointer being freed was not allocated
eyre_test(48377,0x1e194d000) malloc: *** set a breakpoint in malloc_error_break to debug

[Process exited 255]
```

Some things I've noticed:
- replacing the `Other` variant with `Other(u32)` works fine. So does `Other` (empty variant). But `Other(Box<u32>)` fails
- removing `.wrap_err()` (i.e returning `Err(MyError::ClientConstruction.into())` in `trigger()`) makes it work
Thank you for opening the issue.

I will look into it and see what may be causing it
I believe it is that the inner error is incorrectly freed even though it was downcasted, we still need to free the wrapped error.

The content of the `Other` variant is likely because it gives the enum a Drop impl if any fields needs drop. Without it, the box deallocation will take shortcuts when freeing and not drop the contents, but just free
Any reason why this fix hasn't been released yet (and affected versions yanked)? It's a known soundness issue in a popular crate.



Patch:
diff --git a/eyre/src/error.rs b/eyre/src/error.rs
index 701a0fd..5a3d2d8 100644
--- a/eyre/src/error.rs
+++ b/eyre/src/error.rs
@@ -698,13 +698,13 @@ where
     // ptr::read to take ownership of that value.
     if TypeId::of::<D>() == target {
         unsafe {
-            e.cast::<ErrorImpl<ContextError<ManuallyDrop<E>, E>>>()
+            e.cast::<ErrorImpl<ContextError<ManuallyDrop<D>, E>>>()
                 .into_box()
         };
     } else {
         debug_assert_eq!(TypeId::of::<E>(), target);
         unsafe {
-            e.cast::<ErrorImpl<ContextError<E, ManuallyDrop<E>>>>()
+            e.cast::<ErrorImpl<ContextError<D, ManuallyDrop<E>>>>()
                 .into_box()
         };
     }


Test patch:
diff --git a/eyre/tests/drop/mod.rs b/eyre/tests/drop/mod.rs
index c9d7144..6f828cc 100644
--- a/eyre/tests/drop/mod.rs
+++ b/eyre/tests/drop/mod.rs
@@ -24,11 +24,13 @@ impl Flag {
 #[derive(Debug)]
 pub struct DetectDrop {
     has_dropped: Flag,
+    label: &'static str,
 }
 
 impl DetectDrop {
-    pub fn new(has_dropped: &Flag) -> Self {
+    pub fn new(label: &'static str, has_dropped: &Flag) -> Self {
         DetectDrop {
+            label,
             has_dropped: Flag {
                 atomic: Arc::clone(&has_dropped.atomic),
             },
@@ -46,6 +48,7 @@ impl Display for DetectDrop {
 
 impl Drop for DetectDrop {
     fn drop(&mut self) {
+        eprintln!("Dropping {}", self.label);
         let already_dropped = self.has_dropped.atomic.swap(true, SeqCst);
         assert!(!already_dropped);
     }
diff --git a/eyre/tests/test_context.rs b/eyre/tests/test_context.rs
index df536c5..93f229e 100644
--- a/eyre/tests/test_context.rs
+++ b/eyre/tests/test_context.rs
@@ -19,9 +19,10 @@ fn test_inference() -> Result<()> {
 macro_rules! context_type {
     ($name:ident) => {
         #[derive(Debug)]
+        #[repr(C)]
         struct $name {
-            message: &'static str,
             _drop: DetectDrop,
+            message: &'static str,
         }
 
         impl Display for $name {
@@ -37,6 +38,7 @@ context_type!(MidLevel);
 
 #[derive(Error, Debug)]
 #[error("{message}")]
+#[repr(C)]
 struct LowLevel {
     message: &'static str,
     drop: DetectDrop,
@@ -67,14 +69,14 @@ fn make_chain() -> (Report, Dropped) {
 
     let low = LowLevel {
         message: "no such file or directory",
-        drop: DetectDrop::new(&dropped.low),
+        drop: DetectDrop::new("LowLevel", &dropped.low),
     };
 
     // impl Report for Result<T, E>
     let mid = Err::<(), LowLevel>(low)
         .wrap_err(MidLevel {
             message: "failed to load config",
-            _drop: DetectDrop::new(&dropped.mid),
+            _drop: DetectDrop::new("MidLevel", &dropped.mid),
         })
         .unwrap_err();
 
@@ -82,7 +84,7 @@ fn make_chain() -> (Report, Dropped) {
     let high = Err::<(), Report>(mid)
         .wrap_err(HighLevel {
             message: "failed to start server",
-            _drop: DetectDrop::new(&dropped.high),
+            _drop: DetectDrop::new("HighLevel", &dropped.high),
         })
         .unwrap_err();
 
diff --git a/eyre/tests/test_convert.rs b/eyre/tests/test_convert.rs
index 2f58c6b..e0f07f9 100644
--- a/eyre/tests/test_convert.rs
+++ b/eyre/tests/test_convert.rs
@@ -11,7 +11,7 @@ fn test_convert() {
     maybe_install_handler().unwrap();
 
     let has_dropped = Flag::new();
-    let error: Report = Report::new(DetectDrop::new(&has_dropped));
+    let error: Report = Report::new(DetectDrop::new("TestConvert", &has_dropped));
     let box_dyn = Box::<dyn StdError + Send + Sync>::from(error);
     assert_eq!("oh no!", box_dyn.to_string());
     drop(box_dyn);
diff --git a/eyre/tests/test_downcast.rs b/eyre/tests/test_downcast.rs
index 8b0484d..6f66206 100644
--- a/eyre/tests/test_downcast.rs
+++ b/eyre/tests/test_downcast.rs
@@ -109,7 +109,7 @@ fn test_drop() {
     maybe_install_handler().unwrap();
 
     let has_dropped = Flag::new();
-    let error: Report = Report::new(DetectDrop::new(&has_dropped));
+    let error: Report = Report::new(DetectDrop::new("DetectDrop", &has_dropped));
     drop(error.downcast::<DetectDrop>().unwrap());
     assert!(has_dropped.get());
 }
diff --git a/eyre/tests/test_repr.rs b/eyre/tests/test_repr.rs
index 66683b9..71a5663 100644
--- a/eyre/tests/test_repr.rs
+++ b/eyre/tests/test_repr.rs
@@ -31,6 +31,6 @@ fn test_drop() {
     maybe_install_handler().unwrap();
 
     let has_dropped = Flag::new();
-    drop(Report::new(DetectDrop::new(&has_dropped)));
+    drop(Report::new(DetectDrop::new("TestDrop", &has_dropped)));
     assert!(has_dropped.get());
 }