内存安全最棘手的问题
摘要
本文讨论了一个基本的内存安全挑战,涉及带标签的联合体,其中指向一个变体的指针在联合体被覆盖为另一个变体后被使用,导致类型混淆。作者还认为,缓冲区溢出是最容易被利用的内存错误,如果采用更好的数组语法本可以缓解。
<header>
<h1>内存安全最棘手的问题</h1>
<time class="meta" datetime="2026-07-20">2026年7月20日</time>
</header>
<p>提升一个
<a href="https://lobste.rs/s/vzkmtj/forget_borrow_checkers_c3_solved_memory#c_uuhbpy">lobsters 评论</a>
以便于参考。</p>
<p>核心的内存安全反例,即最难解决的问题,与析构函数或堆无关:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">const</span> std = <span class="hl-built_in">@import</span>(<span class="hl-string">"std"</span>);</span>
<span class="line"></span>
<span class="line"><span class="hl-keyword">const</span> E = <span class="hl-keyword">union</span>(<span class="hl-keyword">enum</span>) {</span>
<span class="line"> a: <span class="hl-type">u128</span>,</span>
<span class="line"> b: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line">};</span>
<span class="line"></span>
<span class="line"><span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> main</span>() <span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-keyword">const</span> bad_addr: <span class="hl-type">u128</span> = <span class="hl-built_in">@intFromPtr</span>(<span class="hl-operator">&</span>main);</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">var</span> e: E = .{ .b = <span class="hl-string">"hello"</span> };</span>
<span class="line"> <span class="hl-keyword">const</span> oh_no_pointer: <span class="hl-operator">*</span><span class="hl-keyword">const</span> []<span class="hl-keyword">const</span> <span class="hl-type">u8</span> = <span class="hl-keyword">switch</span> (e) {</span>
<span class="line"> .a => <span class="hl-keyword">unreachable</span>,</span>
<span class="line"> .b => <span class="hl-operator">|</span><span class="hl-operator">*</span>p<span class="hl-operator">|</span> p,</span>
<span class="line"> };</span>
<span class="line"> e = .{ .a = (<span class="hl-numbers">16</span> <span class="hl-operator"><<</span> <span class="hl-numbers">64</span>) <span class="hl-operator">+</span> bad_addr };</span>
<span class="line"> <span class="hl-keyword">const</span> oh_no: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span> = oh_no_pointer.<span class="hl-operator">*</span>;</span>
<span class="line"> std.debug.print(<span class="hl-string">"{s}<span class="hl-string">\n</span>"</span>, .{oh_no});</span>
<span class="line">}</span></code></pre>
</figure>
<figure class="code-block">
<pre><code><span class="line">$ zig run main.zig</span>
<span class="line">��C�� �</span></code></pre>
</figure>
<p>这种例子同样也会破坏 Ada 的安全性:</p>
<p><a href="https://www.enyo.de/fw/notes/ada-type-safety.html" class="url">https://www.enyo.de/fw/notes/ada-type-safety.html</a></p>
<p>我们有一个带标签的联合体,可以容纳 <code>A</code> 或 <code>B</code>。我们将联合体初始化为 <code>A</code>,获取指向其内部数据的指针,将原始内容覆盖为 <code>B</code>,然后使用该指针。指针的类型仍然是 <code>A</code>,但它指向的字节现在属于 <code>B</code>:这就产生了类型混淆。</p>
<hr>
<p>话虽如此,我们关注内存不安全主要是因为它们会导致可被利用的软件,而上面这个例子在实际中的影响程度尚不清楚。巧合的是,在实践中迄今为止最容易被利用的内存错误——臭名昭著的缓冲区溢出——通过编译器插入的边界检查来修复是微不足道的。工业界在内存安全方面最大的失误是没有听取 Walter Bright 的意见:</p>
<p><a href="https://digitalmars.com/articles/C-biggest-mistake.html" class="url">https://digitalmars.com/articles/C-biggest-mistake.html</a></p>
<p>我敢打赌,如果我们在 C11 左右有了 <code>char a[..]</code> 语法,很多问题就不会发生了!</p>
<p>另见 <a href="https://matklad.github.io/2025/12/30/memory-safety-is.html"><em>什么是内存安全?</em></a></p>
查看缓存全文
缓存时间: 2026/07/20 21:23
# 内存安全中最棘手的问题
来源:https://matklad.github.io/2026/07/20/memory-safety-hardest-problem.html
2026年7月20日
为了方便参考,将 Lobsters 上一条令人振奋的评论(https://lobste.rs/s/vzkmtj/forget_borrow_checkers_c3_solved_memory#c_uuhbpy)提炼出来。
内存安全的核心反例,那个最难解决的问题,与析构函数或堆根本无关:
``
const std = @import("std");
const E = union(enum) {
a: u128,
b: []const u8,
};
pub fn main() void {
const bad_addr: u128 = @intFromPtr(&main);
var e: E = .{ .b = "hello" };
const oh_no_pointer: *const []const u8 = switch (e) {
.a => unreachable,
.b => |*p| p,
};
e = .{ .a = (16 << 64) + bad_addr };
const oh_no: []const u8 = oh_no_pointer.*;
std.debug.print("{s}\n", .{oh_no});
}
``
``
$ zig run main.zig
��C�� �
``
这类示例同样也破坏了 Ada 的类型安全:
https://www.enyo.de/fw/notes/ada-type-safety.html
我们有一个标签联合,它既可以存放 `A` 也可以存放 `B`。我们将联合初始化为 `A`,取一个指向其内部数据的指针,然后将原始值覆盖为 `B`,最后再使用这个指针。该指针的类型仍然是 `A`,但它指向的字节现在已经属于 `B`:这就造成了类型混淆。
---
话虽如此,我们之所以关心内存不安全,主要是因为它会导致可利用的软件漏洞,而上述示例在实践中的影响力尚不确定。幸运的是,实际中最容易利用的内存错误——臭名昭著的缓冲区溢出——通过编译器插入边界检查就能轻松修复。整个行业在内存安全方面最大的失误,就是没有听从 Walter Bright 的建议:
https://digitalmars.com/articles/C-biggest-mistake.html
我敢打赌,如果我们在 C11 时代就有了 `char a[...]` 这种语法,不少问题压根就不会发生!
另请参阅:*什么是内存安全?*(https://matklad.github.io/2025/12/30/memory-safety-is.html)
相似文章
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
内存安全是生死攸关的问题
作者认为,内存不安全的开源软件极易受到即将到来的人工智能漏洞查找代理的攻击,这使内存安全成为道德义务,并且Rust必须作为领先且零开销的内存安全语言取得成功。
Rust与C/C++在内存安全CVE上的差异
分析Rust与C/C++在内存安全CVE报告方式上的不同,论证即使存在错误,Rust的设计也能降低某些类型漏洞的发生。
安全 Rust 的边界
TokioConf 2026 的一篇演讲/博客文章探讨了如何通过为复杂指针结构实现追踪式垃圾回收,将安全 Rust 推向极限,并分享处理循环引用与原始指针 GC 设计的技巧。
关于C数组类型语义的讨论
本文解释了C数组类型的令人困惑的行为,包括它们退化为指针、sizeof和函数参数等例外情况,并将其与函数类型进行比较,提出了一种数组和指针严格分离的心理模型。