为什么安全编程很重要,以及为什么像 Rust 这样的语言很重要
作为程序员,你们当中有多少人真正理解编程安全或安全编程?它与应用安全或网络安全并不相同。我必须承认,在我职业生涯的早期,我对这些了解甚少,尤其是我并非计算机科学出身。但现在回想起来,我认为编程安全是每位程序员都应该了解的,并且应该在初级阶段就进行相关培训。
什么是安全编程?或者更准确地说,对于一种编程语言来说,安全意味着什么?或者更确切地说,不安全又意味着什么?我们先来明确一下背景。
如果您更喜欢通过观看视频来了解详情,请查看我在 FOSDEM'22 上就同一主题发表的演讲视频,该视频来自OktaDev YouTube 频道。
编程安全
编程安全 = 内存安全 + 类型安全 + 线程安全
在编程中,我们谈到“安全性”时,通常指的是三个不同方面的组合:内存安全、类型安全和线程安全。如果将空安全也算作内存安全之外的一项,那么就有四个方面,但今天我们先将这两者放在一起讨论。
内存安全
在内存安全的语言中,当你访问变量或数组中的元素时,你可以确保你访问的确实是你想要访问或被允许访问的内容。换句话说,无论你在程序中做什么,都不会错误地读取或写入其他变量或指针的内存。
那么,这为什么如此重要呢?难道所有主流编程语言不都保证了这一点吗?
是的,程度不一。但有些语言默认情况下是不安全的——例如 C 和 C++。在 C 或 C++ 中,你可能会误访问另一个变量的内存,或者两次释放同一个指针;这被称为双重释放错误。有时程序会在指针被释放后继续使用它,这被称为释放后使用 (UAF) 错误或悬空指针错误。这类行为被归类为未定义行为;它们不可预测,并且会导致安全漏洞,而不仅仅是程序崩溃。在这些情况下,程序崩溃反而是件好事,因为它不会导致安全漏洞。
我称之为我价值十亿美元的错误。那是我在1965年发明的空引用。
——托尼·霍尔
此外,还有空安全,它与内存安全有些关联。我之前主要用 Java/JavaScript 开发,所以我们对 null 的概念很熟悉。null 被誉为编程史上最糟糕的发明之一。垃圾回收机制的语言需要 null 的概念,以便在指针不再使用时将其释放。但这个概念也带来了诸多问题和麻烦,例如空指针异常。从技术上讲,这与内存安全有关,但大多数内存安全的语言仍然允许使用 null 作为值,从而导致空指针错误。
型式安全
在类型安全的语言中,访问变量时,会根据其存储方式访问正确的数据类型。这使我们能够放心地操作数据,而无需在运行时手动检查数据类型。内存安全是语言实现类型安全的必要条件。
螺纹安全
在线程安全的语言中,您可以同时从多个线程访问或修改同一块内存,而无需担心数据竞争。这通常是通过消息传递技术、互斥锁(mutex)和线程同步来实现的。线程安全是实现最佳内存安全和类型安全的必要条件,因此,内存安全和类型安全的语言通常也具有线程安全性。
为什么这很重要?
好的!为什么这很重要,我们为什么要关心?让我们先来看一些统计数据来了解一下。
内存安全问题
内存安全问题是大多数安全漏洞(CVE,即常见漏洞和披露)的根源。未定义行为可能被黑客利用来控制程序或泄露特权信息。如果您尝试在内存安全的语言中访问越界数组元素,程序将崩溃并抛出 panic 或错误,这是可以预见的。
这就是为什么 C/C++ 系统中与内存相关的漏洞通常会导致 CVE 漏洞和紧急补丁的发布。C/C++ 中还存在其他内存不安全行为,例如访问已弹出栈帧的指针、已释放的内存、迭代器失效等等。即使是内存安全的语言,即便并非最安全的语言,也能有效防止此类安全问题。
如果我们看一下统计数据,就会发现:
- 微软所有 CVE 中约有 70%是内存安全问题。
- Linux 内核漏洞中有三分之二源于内存安全问题。
- 苹果公司的一项研究发现,iOS 和 macOS 系统中 60-70% 的漏洞都是内存安全漏洞。
- 谷歌估计,90%的安卓漏洞都是内存安全问题。
- 70% 的 Chrome安全漏洞都是内存安全问题。
- 对在实际环境中被发现并利用的零日漏洞进行分析发现,超过 80% 的被利用漏洞都是内存安全问题。
- 历史上最受关注的安全问题之一就是内存安全问题:
这占了CVE总数的很大一部分,当然,其中大部分来自C/C++系统也就不足为奇了🤷
想象一下没有内存安全问题的世界。想象一下开发者能节省多少时间、多少金钱、多少资源。有时我会想,我们为什么还要用 C/C++?为什么我们明知内存安全问题严重,却仍然信任人类手动处理内存?这还没考虑到其他非 CVE 相关的内存问题,比如内存泄漏、内存效率等等。
螺纹安全问题
虽然线程安全不像内存安全那样臭名昭著,但它也是开发人员的一大难题,并可能导致安全问题。
线程安全问题可能导致两种类型的漏洞:
- 一个线程覆盖另一个线程的信息会导致信息丢失。
- 指针损坏可导致权限提升或远程执行
- 由于来自多个线程的信息交织在一起而导致的完整性损失
- 此类攻击中最著名的是TOCTOU(检查时间到使用时间)攻击,这是一种在检查条件(如安全凭证)和使用结果之间存在的竞争条件。
信息丢失和完整性丢失都可能被利用,并导致安全问题。虽然线程安全相关的漏洞比内存安全相关的漏洞更难利用且更少见,但仍然存在被利用的可能性。
型式安全问题
虽然类型安全不如内存安全和线程安全那么关键,但缺乏类型安全也会导致安全问题,而类型安全对于确保内存安全至关重要。
在非类型安全的编程语言中,存在底层漏洞利用的可能性,攻击者可以操纵数据结构并更改数据类型,从而获取特权信息。虽然这类漏洞利用非常罕见,但也并非闻所未闻。
为什么选择 Rust?
既然我们已经了解了编程安全的重要性,那么让我们来看看为什么 Rust 是最安全的语言之一,以及它是如何避免我们在 C/C++ 等语言中通常遇到的大多数安全问题的。
对于不太了解的人来说,Rust 是一种高级多范式编程语言。它非常适合函数式和命令式编程。它拥有非常现代化的工具链,在我看来,也是目前最好的编程语言工具链。虽然它最初是作为一种系统编程语言设计的,但其优势和灵活性使其成为一种通用编程语言,适用于各种应用场景。
“Rust 在其文档中大量使用了流行语,但这不仅仅是营销噱头;他们是真心实意地使用它们,而且这些流行语非常重要。”
Rust 的安全保证
安全性保证是 Rust 最重要的方面之一;Rust 从设计上就保证了内存安全、空值安全、类型安全和线程安全。
如果编译器检测到不安全代码,默认情况下会拒绝编译。你必须特意使用 ` unsafe--safe` 关键字才能破坏这些保证。因此,即使你必须编写不安全代码,你也明确地表明了这一点,从而可以轻松地将问题追溯到特定的代码块。
Rust 中的内存安全
Rust 利用其创新的所有权机制和编译器内置的借用检查器,在编译时确保内存安全。除非在 unsafe 代码块或函数中显式标记为 unsafe,否则编译器不允许内存不安全的代码运行。这种静态的编译时分析消除了许多类型的内存错误,再加上一些额外的运行时检查,Rust 保证了内存安全。Rust
在语言层面上没有 null 的概念。取而代之的是,Rust 提供了一个Option 枚举类型`null` ,可以用来标记值的存在与否。这使得生成的代码是 null 安全的,并且更容易处理,你永远不会在 Rust 中遇到空指针异常。
Rust 的所有权和借用机制使其成为内存效率最高的语言之一,同时避免了手动内存管理和垃圾回收的弊端。它的内存效率和速度可与 C/C++ 相媲美,内存安全性则优于 Java 和 Go 等采用垃圾回收机制的语言。
我在个人博客上写过关于不同语言内存管理的详细文章,如果您有兴趣了解更多关于 Java、Rust、JavaScript 和 Go 中的内存管理,请查看这些文章。
Rust 中的类型安全
Rust 是静态类型的语言,它通过严格的编译时类型检查和内存安全保证来确保类型安全。这并不特殊,因为大多数现代语言都是静态类型的。Rust 也允许在需要时使用 ` type`dyn关键字来实现一定程度的动态类型Any。但强大的类型推断和编译器即使在这些情况下也能确保类型安全。
锈蚀中的螺纹安全
Rust 使用与内存安全类似的概念来保证线程安全,并提供诸如通道、互斥锁和 ARC(原子引用计数)指针等标准库特性。在安全的 Rust 中,任何时候你都只能拥有一个对某个值的可变引用,或者无限多个对该值的只读引用。所有权机制使得共享状态不可能导致意外的数据竞争。这让我们能够专注于代码编写,而将线程间共享数据的管理交给编译器。
Rust 的其他特性
我在博客上发表了一篇详细的博文,分享了我对 Rust 的印象,并解释了 Rust 的一些出色特性,这些特性使其独树一帜。以下是这些特性的简要概述:
- 零成本抽象:Rust 提供真正的零成本抽象,这意味着你可以使用任意风格的代码,并添加任意数量的抽象,而无需付出任何性能代价。极少有语言能够做到这一点,这也是 Rust 速度如此之快的原因。无论你编写何种风格的代码,Rust 编译器始终会生成最佳字节码。这意味着你可以编写函数式风格的代码,并获得与命令式风格代码相同的性能。
- 默认情况下不可变:Rust 中的值默认是不可变的,或者说是只读的。必须显式声明其可变性。这一点,再加上按值传递或按引用传递的特性,使得编写无副作用的函数式代码变得非常容易。
- 模式匹配:Rust 对高级模式匹配提供了出色的支持。模式匹配在 Rust 中被广泛用于错误处理和控制流程。
- 高级泛型、trait 和类型:Rust 拥有高级泛型和trait ,支持类型别名和类型推断。虽然泛型与生命周期结合使用时容易变得复杂,但它仍然是 Rust 最强大的特性之一。
- 宏:Rust 也支持使用宏进行元编程。Rust 同时支持声明式宏和过程式宏。宏可以像注解、属性和函数一样使用。
- 强大的工具链和一流的编译器:Rust 拥有我见过和体验过的最优秀的编译器和工具链之一(与 JS、JVM 语言、Go、Python、Ruby、C#、PHP、C/C++ 相比)。它还拥有出色的文档,并且与工具链一起提供离线使用。这真是太棒了!
- 优秀的社区和生态系统:Rust 拥有最活跃、最友好的社区之一。虽然生态系统还很年轻,但却是发展最快的生态系统之一。
通常,编程语言会在安全性、速度和高级抽象之间做出选择。最好的情况下,你只能选择其中两项。例如,Java/C#/Go 以运行时开销为代价,提供了安全性和高级抽象;而 C++ 则以牺牲安全性为代价,提供了速度和抽象。但 Rust 却能同时提供这三项,并且还额外提供了良好的开发者体验。我认为其他主流语言很少能做到这一点。
“Rust,而不是Firefox,才是Mozilla对业界最大的贡献。”
– TechRepublic
这并不意味着 Rust 没有缺点,它也绝非万能灵药。Rust 的确存在一些问题,例如学习曲线陡峭和语言本身的复杂性。但就我个人而言,它已经是最接近万能灵药的语言了。但这并不意味着你应该把 Rust 用在所有事情上。如果你的应用场景需要速度、并发性、构建系统工具或构建命令行界面,那么 Rust 就是理想的选择。就我个人而言,除非你要为 Rust 不支持的旧平台构建工具,否则我建议在任何情况下都使用 Rust 而不是 C/C++。
了解更多关于 Rust 和安全方面的信息
如果您想了解更多关于 Rust 和安全性的知识,请查看以下资源。
- 无容器!如何使用 Rust 在 Kubernetes 上运行 WebAssembly 工作负载
- Rust 中的内存管理可视化
- 什么是内存安全?它为什么重要?
- Cookie 和令牌在安全身份验证中的比较
- 关于身份验证需要注意的事项
如果您喜欢这篇教程,您很可能也会喜欢我们发布的其他教程。请在 Twitter 上关注 @oktadev并订阅我们的 YouTube 频道,以便在发布新的开发者教程时收到通知。
文章来源:https://dev.to/oktadev/why-safe-programming-matters-and-why-a-language-like-rust-matters-3m45
