Repository: dtolnay/cxx (Rust)

Issue:
unsoundness when a shared type is also declared as a C++ type
cxx silently accepts bridge blocks like this:
```rust
#[cxx::bridge(namespace = "org::blobstore")]
mod ffi {
    struct BlobMetadata {
        f: Box<BlobMetadata>,
    }

    // C++ types and signatures exposed to Rust.
    unsafe extern "C++" {
        // ...

        type BlobMetadata;

        // ...
    }
}
```
Note that `BlobMetadata` is declared both as a "shared" type and an opaque C++ type.

If one now also defines a `BlobMetadata` type on the C++ side, the entire thing compiles without any warnings, but `BlobMetadata` will be silently transmuted between the Rust and C++ version at the boundary. Here's a diff for the cxx demo that shows this in action:
```diff
diff --git a/demo/include/blobstore.h b/demo/include/blobstore.h
index d89583aa..428dcfae 100644
--- a/demo/include/blobstore.h
+++ b/demo/include/blobstore.h
@@ -6,7 +6,10 @@ namespace org {
 namespace blobstore {
 
 struct MultiBuf;
-struct BlobMetadata;
+
+struct BlobMetadata {
+  uintptr_t f;
+};
 
 class BlobstoreClient {
 public:
diff --git a/demo/src/blobstore.cc b/demo/src/blobstore.cc
index 7cf40dfe..1f4983bf 100644
--- a/demo/src/blobstore.cc
+++ b/demo/src/blobstore.cc
@@ -54,12 +54,7 @@ void BlobstoreClient::tag(uint64_t blobid, rust::Str tag) const {
 // Retrieve metadata about a blob.
 BlobMetadata BlobstoreClient::metadata(uint64_t blobid) const {
   BlobMetadata metadata{};
-  auto blob = impl->blobs.find(blobid);
-  if (blob != impl->blobs.end()) {
-    metadata.size = blob->second.data.size();
-    std::for_each(blob->second.tags.cbegin(), blob->second.tags.cend(),
-                  [&](auto &t) { metadata.tags.emplace_back(t); });
-  }
+  metadata.f = blobid;
   return metadata;
 }
 
diff --git a/demo/src/main.rs b/demo/src/main.rs
index e43f1562..6c15561c 100644
--- a/demo/src/main.rs
+++ b/demo/src/main.rs
@@ -1,9 +1,9 @@
 #[cxx::bridge(namespace = "org::blobstore")]
 mod ffi {
     // Shared structs with fields visible to both languages.
+    #[derive(Debug)]
     struct BlobMetadata {
-        size: usize,
-        tags: Vec<String>,
+        f: Box<BlobMetadata>,
     }
 
     // Rust types and signatures exposed to C++.
@@ -18,11 +18,10 @@ mod ffi {
         include!("demo/include/blobstore.h");
 
         type BlobstoreClient;
+        type BlobMetadata;
 
         fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
-        fn put(&self, parts: &mut MultiBuf) -> u64;
-        fn tag(&self, blobid: u64, tag: &str);
-        fn metadata(&self, blobid: u64) -> BlobMetadata;
+        fn metadata(self: &BlobstoreClient, blobid: u64) -> BlobMetadata;
     }
 }
 
@@ -43,17 +42,7 @@ pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
 
 fn main() {
     let client = ffi::new_blobstore_client();
-
-    // Upload a blob.
-    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
-    let mut buf = MultiBuf { chunks, pos: 0 };
-    let blobid = client.put(&mut buf);
-    println!("blobid = {blobid}");
-
-    // Add a tag.
-    client.tag(blobid, "rust");
-
-    // Read back the tags.
-    let metadata = client.metadata(blobid);
-    println!("tags = {:?}", metadata.tags);
+    let metadata = client.metadata(1);
+    println!("f at addr {:p}", &raw const *metadata.f);
+    println!("f = {:?}", metadata.f);
 }
```
This prints
```
f at addr 0x1
```
and then it segfaults. In other words, the `1` stored in field `f` on the C++ side has been transmuted to a `Box` on the Rust side.

I am not entirely sure whether this is the kind of issue cxx is intended to catch, but it very much seems like it -- the Rust and C++ side both make sense, the error is entirely in how they are linked together.

(Thanks to @Patrick-6 for initially running into this; I then just did the minimal example.)


Discussion: This is an "extern shared struct" and cxx will enforce that they are declared as `unsafe extern "C++"`, unlike safe shared structs and safe (opaque) extern C++ types.

```console
error[cxxbridge]: extern shared struct must be declared in an `unsafe extern` block
   ┌─ src/main.rs:17:9
   │
17 │         type BlobMetadata;
   │         ^^^^^^^^^^^^^^^^^ extern shared struct must be declared in an `unsafe extern` block
```

These days with more recent enhancements to `ExternType` it would be more advised to write the same thing like this, so it is possible that the older approach can just be deprecated.

```rust
use cxx::ExternType;

#[repr(C)]
struct BlobMetadata {
    f: Box<BlobMetadata>,
}

unsafe impl ExternType for BlobMetadata {
    type Id = cxx::type_id!("org::blobstore::BlobMetadata");
    type Kind = cxx::kind::Trivial;
}

#[cxx::bridge(namespace = "org::blobstore")]
mod ffi {
    extern "C++" {
        type BlobMetadata = crate::BlobMetadata;

        // ...
    }
}
```


Thanks for taking a look! Searching the docs on cxx.rs I didn't find anything specifically talking about "extern shared structs".

I also didn't realize `extern "C++"` without `unsafe` is even a thing, given that even the demo uses `unsafe` there. And it seems almost all such blocks will be `unsafe` because...
```
  error[cxxbridge]: block must be declared `unsafe extern "C++"` if it contains any safe-to-call C++ functions
     ┌─ src/main.rs:17:5
     │
  17 │     extern "C++" {
     │     ^^^^^^^^^^^^ block must be declared `unsafe extern "C++"` if it contains any safe-to-call C++ functions
```
So this seems like something that can easily happen accidentally (as it did in Miri, which led to this report).



Fix (patch):
diff --git a/macro/src/expand.rs b/macro/src/expand.rs
index 9c23fab8d..014d34dc2 100644
--- a/macro/src/expand.rs
+++ b/macro/src/expand.rs
@@ -3,6 +3,7 @@ use crate::syntax::attrs::{self, OtherAttrs};
 use crate::syntax::cfg::CfgExpr;
 use crate::syntax::file::Module;
 use crate::syntax::instantiate::{ImplKey, NamedImplKey};
+use crate::syntax::namespace::Namespace;
 use crate::syntax::qualified::QualifiedName;
 use crate::syntax::report::Errors;
 use crate::syntax::symbol::Symbol;
@@ -16,7 +17,7 @@ use proc_macro2::{Ident, Span, TokenStream};
 use quote::{format_ident, quote, quote_spanned, ToTokens};
 use std::fmt::{self, Display};
 use std::mem;
-use syn::{parse_quote, punctuated, Generics, Lifetime, Result, Token};
+use syn::{parse_quote, punctuated, Generics, Lifetime, Result, Token, Visibility};
 
 pub(crate) fn bridge(mut ffi: Module) -> Result<TokenStream> {
     let ref mut errors = Errors::new();
@@ -70,7 +71,9 @@ fn expand(ffi: Module, doc: Doc, attrs: OtherAttrs, apis: &[Api], types: &Types)
             Api::Enum(enm) => expanded.extend(expand_enum(enm)),
             Api::CxxType(ety) => {
                 let ident = &ety.name.rust;
-                if !types.structs.contains_key(ident) && !types.enums.contains_key(ident) {
+                if types.structs.contains_key(ident) {
+                    hidden.extend(expand_extern_shared_struct(ety, &ffi));
+                } else if !types.enums.contains_key(ident) {
                     expanded.extend(expand_cxx_type(ety));
                     hidden.extend(expand_cxx_type_assert_pinned(ety, types));
                 }
@@ -457,6 +460,74 @@ fn expand_cxx_type_assert_pinned(ety: &ExternType, types: &Types) -> TokenStream
     }
 }
 
+fn expand_extern_shared_struct(ety: &ExternType, ffi: &Module) -> TokenStream {
+    let module = &ffi.ident;
+    let name = &ety.name.rust;
+    let namespaced_name = display_namespaced(&ety.name);
+
+    let visibility = match &ffi.vis {
+        Visibility::Public(_) => "pub ".to_owned(),
+        Visibility::Restricted(vis) => {
+            format!(
+                "pub(in {}) ",
+                vis.path
+                    .segments
+                    .iter()
+                    .map(|segment| segment.ident.to_string())
+                    .collect::<Vec<_>>()
+                    .join("::"),
+            )
+        }
+        Visibility::Inherited => String::new(),
+    };
+
+    let namespace_attr = if ety.name.namespace == Namespace::ROOT {
+        String::new()
+    } else {
+        format!(
+            "#[namespace = \"{}\"]\n        ",
+            ety.name
+                .namespace
+                .iter()
+                .map(Ident::to_string)
+                .collect::<Vec<_>>()
+                .join("::"),
+        )
+    };
+
+    let message = format!(
+        "\
+        \nShared struct redeclared as an unsafe extern C++ type is deprecated.\
+        \nIf this is intended to be a shared struct, remove this `type {name}`.\
+        \nIf this is intended to be an extern type, change it to:\
+        \n\
+        \n    use cxx::ExternType;\
+        \n    \
+        \n    #[repr(C)]\
+        \n    {visibility}struct {name} {{\
+        \n        ...\
+        \n    }}\
+        \n    \
+        \n    unsafe impl ExternType for {name} {{\
+        \n        type Id = cxx::type_id!(\"{namespaced_name}\");\
+        \n        type Kind = cxx::kind::Trivial;\
+        \n    }}\
+        \n    \
+        \n    {visibility}mod {module} {{\
+        \n        {namespace_attr}extern \"C++\" {{\
+        \n            type {name} = crate::{name};\
+        \n        }}\
+        \n        ...\
+        \n    }}",
+    );
+
+    quote! {
+        #[deprecated = #message]
+        struct #name {}
+        let _ = #name {};
+    }
+}
+
 fn expand_cxx_function_decl(efn: &ExternFn, types: &Types) -> TokenStream {
     let generics = &efn.generics;
     let receiver = efn.receiver().into_iter().map(|receiver| {