-
Notifications
You must be signed in to change notification settings - Fork 54
COM trait methods should probably always be unsafe #97
Comments
Sorry, I'm going to hijack this a bit. I've been struggling with the same issue in Intercom and I'm curious to hear how my thoughts resonate. Given TL;DR: Bad COM libraries can do bad things, just like bad Rust crates can do bad things. We assume Rust crates follow Rust rules, thus we should also assume COM libraries follow COM rules. How far should
|
We actually struggled a lot with this question and one reason we open sourced the library so early in its development was to get people's feedback about this, so I'm very happy to see this being brought up. Bringing in @snf and @adrianwithah who have also given some thought to the issue. I personally am actually more sympathetic to the cause of requiring the user to mark all COM interface traits as Marking an API as unsafe means that the user of that API must verify the safety of the API against Rust's rules around memory and data race safety because the author of the API cannot or has not done so. If a crate has exposed an API as safe than the user of that API should assume that the author has verified that that that API can never been used in an unsafe way. In other words, if a user experiences memory unsafe issues in their program it can only be for one of two reasons, the user has used an unsafe API and failed to uphold the invariants required by the API OR there is a bug with respect to Rust's memory safety rules in that library. In the case of this |
I agree with @rylev here. I think it makes sense to assume that the COM parts of the system play by the COM rules, and the Rust parts of the system are safe according to the Rust rules. I see a number of problems, though, in that the former is nowhere nearly as precisely defined and documented. For a COM server that does something simple, I think it makes sense to allow its trait methods to be safe. But if it's possible to create unsafety by doing things that are allowable in the COM world, then I think it's better to err on the side of requiring "unsafe". I think someone basically needs to put on a "security researcher" hat and look for ways we can get to unsafety. I have a collection (the pointer return from IDWriteTextAnalysisSource::GetLocaleName method is fun, and then there's the whole question about whether there should be mutable access to ID3DBlob). |
Now I'm conflicted... :) I think it all boils down to "what should be verified?". Especially given COM's dynamic nature (you can register a different implementation in the system and that gets used instead), verifying everything is just not possible. In many cases verifying anything is difficult due to the binary-nature of COM libraries: What would I need to verify in order to provide a safe wrapper for Another issue I have with marking otherwise safe trait methods as
Maybe This of course doesn't cover method-based invariants, where whoever writes the trait should still add .. although why is com-rs/src/interfaces/iunknown.rs Lines 9 to 17 in 99112fc
|
+1 regarding @Rantanen's point on usability. I think making That being said, as @raphlinus has mentioned, COM rules are not as precisely defined, which has been my experience while working on this library. There are many gotchas (such as uninitialised out-pointers) that could present some nasty bugs. With that in mind, I am hesitant to just introduce the "trust" function without proper documentation about a vetting process. In my opinion, this would often encourage developers to blindly use the "trust" function. Using @Rantanen's example of |
What would Personally, I feel
More specifically what should not be included in the function-level blanket
fn get_ptr() -> *mut usize {
1234 as *mut _
} Using (dereferencing) any pointer requires So what would be included in parameter validity then? The big concern I've got is fn do_stuff_with_borrow(data: &Data) {
call_com_method(data as *const _ as *mut _);
} I think this is enough to turn my mind on the subject for any method that accepts pointer values of any kind. As long as all the parameters are The specific Supporting reference parameters wouldn't do anything to the callee being able to modify data behind |
I agree this is definitely a hair problem. I agree that we should not require the user to mark each usage of a method on a COM interfaces as unsafe (unless the user themselves cannot verify safe usage in which case they must mark the function as We need to then document what assumptions about the COM interface we make in this library. This is an extremely difficult thing to do correctly, but I believe we can at the very least start this process. Some things are obvious. For example, we assume that pointers to the interface remain valid as long as the reference count is above 0. We assume reference counts are decremented by 1 each time |
This misses couple of real world usage cases:
When it comes to
And just a reminder, the return values of
|
Sorry in advance if this is a dumb question as I haven't used COM before. Wouldn't dropping the runtime while keeping the object alive make all calls to that object crash the program, including |
That's a good point. It might be worth it to mark the runtime uninitialization as I feel this case is similar to normal Rust method calls. |
Thanks for bringing this up. I believe you are correct though I think we should do more research to understand exactly what happens when a COM interface gets called after uninitialization. My gut tells me that it leaves our binary in an undefined state so this really is unsafe, but if Windows is doing some sort of magic to abort the process immediately upon this happening then I don't believe that would qualify as unsafe. Of course, if the process is still allowed to run (e.g., to unwind its stack), then this is indeed UB and unsafe. |
Looking further into In addition to #105, the bigger issue is the not- Currently the following is "safe": fn return_instance() -> InterfaceRc<IFoo> {
ApartmentThreadedRuntime::new()
.unwrap()
.create_instance::<IFoo>(&FOO_CLSID)
.unwrap()
} The runtime gets dropped inside the function while the Given Another option would be to include the runtime lifetime in the |
Would using reference counting be too complex of a solution? Like each If that is too complex, would making the drop implementation panic unconditionally to force users of the library to call a special unsafe function to call I don't think marking the runtime's initialization as I'd also like to say that the rust SDL2 wrapper seems to wrestle with a similar problem where textures could potentially outlive their creators, and cause UB if they were dropped after their creator had been destroyed. The wrapper seems to have dealt with this problem by providing two implementations behind a feature gate: one reliant on lifetimes and another reliant on manual memory management. In the lifetime based solution, a created texture cannot outlive its creator by giving it a lifetime parameter, and it safely frees its memory through its drop implementation. In the solution based on manual memory management, there is no I'm not sure if that project handles the problem correctly; it just came to mind when I saw this problem and I feel like the the more possible solutions there are the better. |
Actually, would #101 be sufficient if it forces all traits to be |
I feel the options are:
I see
#101 affects mostly COM servers. The runtime lifetime is a problem for COM clients. Especially if there is ever support for IDL-based code generation, then all of those Persoanlly I'd be curious to see just how invasive |
This issue has turned into a massive collection of related issues. I'd like us to return back to the question of a COM trait's methods should be unsafe or not. I realize that the other issues are related to this question, but I think we can first decide here what we want, and then use this decision to drive the answers to the other questions. At the center of this is the fact that when a user declares a COM interface, they are not necessarily sure whether the concrete implementation backing this interface is safe or not. The reason for this is that it might not be known until runtime what actual implementation will be chosen. The user therefore might not be able to verify the safety of the implementation ahead of time. There are several solutions to this issue:
I'm personally leaning to the first option. Calls to COM interface methods are inherently unsafe. Hiding that fact merely because it's inconvenient betrays the purpose of Rust. However, I know several people are against this because it makes the crate much harder to use, so I'm definitely interested in finding a difference solution. If we believe that marking both the COM interface trait and the initialization of co_classes as unsafe could sufficiently require the user to verify safety, then perhaps that's the right way to go. In a large code base I'm not sure that is possible, however. |
TL;DR; I'm again leaning on marking everything This has been a bit of a roller coaster for me. Like I stated in my first comment in this issue, I've been giving this issue some thought previously from Intercom's perspective. Given Intercom aims to provide a user-level API, from that perspective my knee-jerk reaction was that I've since then clarified the difference between Intercom and com-rs in my head. And given com-rs is a lot lower level API that is likely to be wrapped by a safe abstraction, I don't see everything being However finally I started to wonder what that Especially currently when I think at this point I feel like
I feel com-rs can't assume everything is safe. So some methods definitely should be unsafe. What would those methods be? I feel a somewhat obvious concern are methods that deal with pointers.
Now given COM uses |
I believe this issue is mostly resolved through #120 . The topic of safety still deserves a deeper look, but I'd like to continue those in different threads. |
This way of declaring COM interfaces is really convenient, but IMO it provides a wrong feeling of safety. An erroneous COM server may cause memory errors, even in simple functions like
add_ref
. Further I would argue this functions are all special kinds of FFI calls (using the COM ABI), so like every normal FFI function they should always be unsafe. Currently the easiest way to use thecom_interface
macro (to not use unsafe at all) is probably the most dangerous one.This extends to the
com::InterfaceRc
type. I think constructing this struct should also be unsafe. The user should be forced to verify the called COM server or signal their trust by constructing this type within an unsafe block.This would lead to a less convenient usage, but I think this would still be a better approach, because most of the time the COM interfaces will probably not be used directly in idiomatic Rust code. Most of the time their will probably be behind another layer of abstraction and I think this should also be the layer providing the safe interface for the COM types.
However I see that especially in cases where COM is used as a system for plugin-like components it is in general impossible to verify the correctness, so their is probably a practical compromise needed.
The text was updated successfully, but these errors were encountered: