为Rust设计COM库
我曾在微软MSRC英国团队实习,致力于安全系统编程语言(SSPL)项目,该项目探索使用安全编程语言作为防范内存安全相关漏洞的主动措施。本博客介绍我在SSPL团队指导下完成的项目:构建一个开源的Rust库,使开发者能够以符合Rust习惯的方式消费和生产进程内组件对象模型(COM)组件。
项目概述
现有Rust crate(如winapi-rs、Intercom等)已提供COM支持,但我们的目标是创建一个更符合Rust习惯的库。COM是一种语言无关的、面向对象的二进制创建标准,其明确定义的应用程序二进制接口(ABI)使得不同语言编写的二进制文件能够互操作。
COM组件通过一组接口暴露数据。每个接口定义一个虚拟方法表(vtable),类似于C++对象的虚拟函数布局。vtable包含指向实际函数实现的函数指针,COM组件存储指向这些vtable的指针(vpointer)。通过强制vtable中函数指针的布局,COM允许任何语言将内部数据类型映射到COM对象的vtable,从而调用COM对象上的暴露函数。
为什么为Rust启用COM支持?
微软正在调查采用更安全的系统编程语言以消除一类漏洞,Rust是其中之一。Rust采用的挑战之一是与C++和现有微软工具的互操作性。COM是微软广泛使用的标准,支持COM是实现与现有组件互操作的必要步骤。此外,该库还允许COM开发者利用Rust编译器的内存安全保证创建更安全的COM组件。
实现与项目设计
消费COM组件
要消费COM组件,首先需要在Rust中描述接口(例如IAnimal)。然后通过Runtime结构实例化COM对象(该结构使用CoInitializeEx/CoUninitialize控制COM库的生命周期),返回接口指针(指向vpointer的指针)。最后通过接口指针调用方法。
|
|
消费端的宏使用相对直接,因为COM为消费者抽象了COM组件的实现细节。
创建自己的COM对象
在生产端,我们花费了大量时间使设计既可扩展又符合习惯。最初,我们尝试通过将用户定义的对象包装在ComBox结构中来完全抽象生成的vtable的存在,该结构持有vpointer并传递给消费者进行交互。
然而,这种解决方案基于用户永远不需要访问其vpointer的假设,但该假设被证明是错误的。COM允许在创建COM组件时使用许多功能,例如聚合(Aggregation),它允许你暴露另一个COM对象的接口,就像是你自己的一样。为了启用聚合,用户需要显式地向聚合对象提供自己的vpointer,这影响了我们的设计决策。
现在,我们知道需要隐藏COM细节,但同时为有经验的COM开发者提供访问这些细节的途径。假设你想创建一个名为CatDog的COM组件,它继承自COM接口ICat和IDog,你需要这样写:
|
|
开发者将在结构中定义他们的用户字段。#[co_class]宏然后扩展结构以包含COM字段(vpointer、ref_counts等)。用户必须实现一个构造函数“new”,在其中通过宏生成的“allocate”函数初始化COM字段。主要区别在于方法是在包含vpointer的包装结构上定义的,这授予用户访问这些字段的权限,因为它们现在在同一范围内!
它到底有多安全?
尽管Rust提供内存安全保证,但在使用unsafe超集时,所有保证都可能失效。COM促进语言之间的互操作性,这些交互需要频繁使用Rust的unsafe超集。通常,Rust开发者会围绕这些交互创建安全包装器,验证原始指针并将其转换为安全类型。例如,可空指针将被检查并转换为Option,因此开发者必须显式处理null情况等。
不幸的是,我们无法为我们的库复制此解决方案。这些包装器并非万无一失。我们可以强制用户显式处理空指针,但悬空引用呢?无法验证传递给我们的指针是否指向垃圾或无效内存。这些包装器如何按照Rust标准标记为安全?
上述场景与我们的库之间的一个关键区别是,它们包装的是特定的库/API,而我们试图包装的是标准/协议。在他们的案例中,他们可以参考文档、检查代码库等,然后生成针对该库的安全包装器。这将是一个有效的案例,可以将包装器标记为安全,因为他们已经检查了所包装的不安全代码,确保返回有效的指针。由于我们是为标准制作包装器,而不是特定的库或API,我们无法保证每个COM组件都会正确实现。由于我们对所包装的代码一无所知,我们不能自动将这些生成的包装器标记为安全。我们能做的是为用户提供一个选项,一旦他们完成了尽职调查,就可以将交互标记为安全。
如果我们不能假设包装器是安全的,那么在影响方面我们处于什么位置?与C/C++系统语言相比,使用Rust将使开发者更容易编写安全的COM组件。首先,不安全代码主要围绕外部函数接口(FFI)交互编写。开发者仍然可以为独立于这些FFI交互的逻辑流编写安全的Rust代码。这在编写新的COM组件时尤其重要。例如,多线程数据结构通常用于高性能代码。这些数据结构背后的逻辑流容易受到数据竞争的影响。由于编译器优化,这些数据竞争可能反过来导致难以跟踪的内存安全漏洞。Rust的所有权模型跨线程持久存在,消除了数据竞争的可能性。
其次,Rust强制开发者采取 conscientious 和主动的方法来处理内存安全。例如,开发者必须使用Rust中的unsafe关键字标记上述FFI交互为不安全。这向库用户发出信号,要求他们调查这些FFI交互并对他们正在交互的COM对象进行尽职调查。将这种信号机制直接嵌入编译器比在代码注释或文档中详细说明更难被忽略。作为库的维护者,我们也被迫对内存安全保持 diligent。在暴露代码时,我们需要能够准确评估代码是否安全,以符合Rust的安全标准。
最后,如果我在MSRC UK工作却没有包括我与安全工程师合作的视角,那将是一种遗憾!拥有这些显式的unsafe块大大减少了安全工程师必须梳理以识别内存安全漏洞的表面区域,因为内存安全漏洞只能源自不安全代码。
如你所见,Rust开发者有许多工具可供使用,以编写安全代码,并确保其代码的安全使用。将这些工具直接放在编译器中,而不是必须拾取外部复杂工具,有助于提高生产力和安全性。
下一步是什么?
该项目现已开源并在GitHub上可用!无论你是对贡献感兴趣还是只是对项目好奇,我们都欢迎你来查看。我们希望从社区获得尽可能多的反馈,因为这是一个以用户体验为设计目标的库。还有许多COM功能我们尚未覆盖,包括进程外交互等。一旦我们基于社区的反馈建立了稳定的基础,我们就可以研究这些功能。
最后的话
这个项目从第一天到最后一天都挑战着我。不得不同时学习Rust和COM,将我推离舒适区很远。然而,通过看到项目的完成和学习到这么多,挣扎得到了更大的满足感。
通过在微软的这次实习,我有机会与一些我见过的最聪明的人一起工作并从他们的视角看问题。感谢MSRC英国团队的热情好客并赋予我团队成员的权力。感谢我的团队成员Ryan和Sebastian,在这个项目上与他们合作非常愉快,并在许多障碍中拯救了我。最后,感谢我的导师Sebastian在将该项目委托给我时采取了信念的飞跃,并在整个过程中耐心指导。
Hadrian Wei Heng Lim,软件工程师实习生,MSRC