第一章:Go unsafe包的核心设计哲学与安全边界
unsafe 包并非为日常开发而设,而是 Go 运行时、标准库底层(如 sync/atomic、reflect、runtime)及极少数需突破类型系统边界的场景提供“受控的不安全能力”。其设计哲学可凝练为三点:显式性(所有不安全操作必须显式导入并调用 unsafe 函数)、最小化(仅暴露指针转换、内存偏移、大小计算等基础原语,不提供任意内存读写)、无保证(文档明确声明:使用 unsafe 的代码不受 Go 兼容性承诺保护,可能随编译器优化或 GC 行为变更而失效)。
安全边界由语言规范与运行时共同划定。关键约束包括:
- 禁止通过
unsafe.Pointer绕过 GC 的指针追踪(例如:将栈上变量地址转为*T后长期持有,可能导致悬垂指针) - 禁止违反内存对齐规则访问字段(如对
struct{a int8; b int64}中b的地址直接做(*int64)(unsafe.Pointer(&s.a))会触发未定义行为) - 禁止在非
unsafe上下文中假设结构体字段布局(unsafe.Offsetof是唯一合法获取偏移的途径)
以下代码演示了安全的字段偏移访问模式:
package main
import (
"fmt"
"unsafe"
)
type Point struct {
X, Y int64
}
func main() {
p := Point{X: 10, Y: 20}
// ✅ 安全:通过 Offsetof 计算字段地址,符合内存布局约定
xAddr := unsafe.Pointer(&p.X) // 获取 X 字段原始地址
yAddr := unsafe.Pointer(uintptr(xAddr) + unsafe.Offsetof(p.Y)) // 基于偏移计算 Y 地址
// ✅ 安全:转换后立即使用,不跨函数边界传递指针
yPtr := (*int64)(yAddr)
fmt.Println("Y value via unsafe:", *yPtr) // 输出:20
}
该示例严格遵循“临时性”原则——指针仅在当前作用域内解引用,且所有偏移均通过 unsafe.Offsetof 动态计算,避免硬编码数值。任何脱离此范式的用法(如将 yAddr 作为返回值、存储到全局变量、或用于 reflect 之外的反射操作)均越界。
第二章:Pointer数学运算的禁令机制剖析
2.1 Pointer算术运算的底层语义与编译器拦截原理
指针算术的本质是地址偏移的类型安全缩放:p + n 实际计算为 ((char*)p) + n * sizeof(*p)。
编译器如何介入?
- 在 IR(如 LLVM IR)生成阶段插入类型检查
- 对越界访问(如
arr[5]当arr长度为 3)触发-Warray-bounds - 启用
-fsanitize=address时注入运行时边界桩代码
关键语义约束表
| 运算 | 合法前提 | 底层地址变化 |
|---|---|---|
p + 1 |
p 指向有效对象或紧邻末尾 |
p + sizeof(*p) |
p - q |
p, q 指向同一数组或均为 nullptr |
(char*)p - (char*)q / sizeof(*p) |
int arr[4] = {0};
int *p = arr;
p += 5; // ❌ 未定义行为:越过 arr[4](合法终点)后一个位置
逻辑分析:
p += 5计算为&arr[0] + 5 * sizeof(int)→&arr[0] + 20。但 C 标准仅保证&arr[4](即arr + 4)为合法哨兵地址;arr + 5超出“可指向范围”,触发 UB。Clang 在-O2 -fsanitize=undefined下会插入_ubsan_handle_add_overflow检查。
graph TD
A[源码: p += n] --> B[前端:类型推导 sizeof*T]
B --> C[中端:计算 offset = n * sizeof*T]
C --> D{是否超出对象边界?}
D -->|是| E[插入 sanitizer 桩 或 优化掉]
D -->|否| F[生成 add 指令]
2.2 unsafe.Add/unsafe.Offsetof在GC指针逃逸分析中的实际限制案例
Go 编译器对 unsafe.Add 和 unsafe.Offsetof 的指针操作存在隐式逃逸约束:当结果被赋值给可能逃逸的变量(如接口、切片底层数组、全局变量)时,原始对象将被迫堆分配。
GC 逃逸判定关键规则
unsafe.Offsetof本身不触发逃逸(仅计算偏移量常量);unsafe.Add(ptr, offset)若ptr是栈上局部变量地址,且结果被存储到非栈生命周期结构中 → 强制逃逸。
type Header struct {
data *[1024]byte
}
func badExample() interface{} {
var h Header
// ⚠️ h.data 是栈变量,但通过 unsafe.Add 得到的指针被转为 interface{}
p := unsafe.Add(unsafe.Pointer(&h), unsafe.Offsetof(h.data))
return p // h 整体逃逸至堆!
}
分析:
&h是栈地址;unsafe.Add返回的指针虽未解引用,但因interface{}需保存指针元信息,编译器无法证明其生命周期 ≤ 栈帧,故保守提升h到堆。参数p实际携带h的完整内存块逃逸。
逃逸行为对比表
| 操作 | 是否触发逃逸 | 原因 |
|---|---|---|
unsafe.Offsetof(h.data) |
否 | 编译期常量计算,无指针值生成 |
unsafe.Add(&h, offset) 赋值给局部 *byte |
否 | 生命周期明确绑定栈帧 |
unsafe.Add(&h, offset) 赋值给 interface{} 或 []byte |
是 | 接口/切片可能延长指针存活期,触发保守逃逸 |
graph TD
A[获取 &h 地址] --> B[unsafe.Offsetof 得到偏移]
B --> C[unsafe.Add 计算新指针]
C --> D{是否存入 interface/切片/全局?}
D -->|是| E[原始结构体 h 逃逸至堆]
D -->|否| F[保留在栈]
2.3 基于汇编验证的Pointer越界访问触发panic的全过程复现
当 Go 程序执行 *(*int)(unsafe.Pointer(uintptr(0) - 8)) 时,会绕过 GC 和边界检查,在运行时触发 runtime.sigpanic。
触发路径关键汇编片段(amd64)
MOVQ $0, AX // AX ← 0
SUBQ $8, AX // AX ← -8(非法地址)
MOVQ (AX), BX // 尝试读取地址-8 → #UD 或 SIGSEGV
该指令在用户态直接访存失败,内核投递 SIGSEGV,Go 运行时信号处理器捕获后调用 sigpanic(),最终调用 gopanic() 启动 panic 流程。
panic 栈传播关键状态
| 阶段 | 关键操作 |
|---|---|
| 信号捕获 | sigtramp → sighandler |
| 上下文保存 | save_g 保存当前 goroutine |
| panic 初始化 | gopanic(&goPanicData) |
核心验证步骤
- 编译时添加
-gcflags="-S"查看内联汇编; - 运行时通过
GODEBUG=asyncpreemptoff=1禁用抢占,确保 panic 在预期指令处触发。
2.4 在CGO交互场景中绕过Pointer算术限制的合规替代方案实践
CGO禁止直接对 *C.char 等 C 指针执行 +/- 运算,但可通过 Go 原生切片机制安全实现等效内存访问。
安全转换模式
// 将 C 字符串转为 Go 切片(不复制数据)
func cStringToSlice(cstr *C.char, len int) []byte {
if cstr == nil || len <= 0 {
return nil
}
// 使用 unsafe.Slice 替代指针算术(Go 1.17+)
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&struct{ data unsafe.Pointer; len, cap int }{
data: unsafe.Pointer(cstr),
len: len,
cap: len,
}))
return unsafe.Slice((*byte)(hdr.Data), hdr.Len)
}
unsafe.Slice 是 Go 官方推荐的替代方案,避免了 (*[n]byte)(unsafe.Pointer(p))[:n:n] 的类型逃逸与长度不安全问题;hdr.Data 保证底层地址对齐,hdr.Len 显式控制边界。
可选合规路径对比
| 方案 | 安全性 | Go 版本要求 | 是否需 unsafe |
|---|---|---|---|
unsafe.Slice |
✅ 高(边界检查由 runtime 保障) | 1.17+ | 是 |
C.GoBytes |
✅ 最高(完整拷贝) | 所有 | 否 |
reflect.SliceHeader 手动构造 |
⚠️ 低(易越界) | 所有 | 是 |
graph TD
A[C.char* ptr] --> B{长度已知?}
B -->|是| C[unsafe.Slice ptr, n]
B -->|否| D[C.strlen + unsafe.Slice]
C --> E[Go slice for safe access]
D --> E
2.5 对比Go 1.17–1.20各版本对uintptr+Pointer混合运算的诊断精度演进
Go 1.17 引入 unsafe.Pointer 与 uintptr 混合运算的静态检查初版,但仅捕获显式赋值(如 p = (*T)(unsafe.Pointer(u)));1.18 增强 SSA 分析,识别间接路径;1.19 引入跨函数逃逸跟踪;1.20 实现类型流敏感分析,可判定 uintptr 是否源自 unsafe.Pointer 的合法转换。
关键诊断能力对比
| 版本 | 检测覆盖范围 | 误报率 | 支持场景示例 |
|---|---|---|---|
| 1.17 | 直接赋值链 | 高 | u := uintptr(unsafe.Pointer(&x)); p := (*int)(unsafe.Pointer(u)) |
| 1.20 | 多跳指针传播 | 极低 | u := uintptr(unsafe.Pointer(&x)); u2 := u + 8; p := (*int)(unsafe.Pointer(u2)) |
func badPattern() {
x := 42
u := uintptr(unsafe.Pointer(&x)) // ✅ 合法起源
u2 := u + unsafe.Offsetof(struct{ a, b int }{}.b) // ⚠️ 1.17–1.18 无法验证 u2 是否仍“安全”
p := (*int)(unsafe.Pointer(u2)) // 🔍 1.20 可追溯 u2 的 uintptr 血缘并确认其有效性
}
该代码中,u2 是 u 的线性偏移,其安全性依赖 u 的原始合法性。1.20 的类型流分析能建模 uintptr 的“信任传递”,而早期版本仅检查 unsafe.Pointer(u2) 的直接来源,导致漏报。
graph TD
A[unsafe.Pointer→uintptr] -->|1.17| B[仅检查直接转换]
A -->|1.20| C[追踪 uintptr 衍生链]
C --> D[验证所有算术操作是否保持血缘可信]
第三章:SliceFromPtr实现的历史变迁与语义收敛
3.1 Go 1.17之前基于reflect.SliceHeader的非安全构造及其崩溃现场还原
在 Go 1.17 之前,开发者常通过 unsafe 拼接 reflect.SliceHeader 绕过类型系统构造 slice,但该方式极易触发运行时崩溃。
崩溃复现代码
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
data := []byte{1, 2, 3}
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&data))
// ❌ 错误:直接篡改 Data 字段指向已释放内存
hdr.Data = uintptr(unsafe.Pointer(&data[0])) + 1000 // 越界地址
s := *(*[]byte)(unsafe.Pointer(hdr))
fmt.Println(s[0]) // panic: runtime error: invalid memory address
}
逻辑分析:reflect.SliceHeader 是纯数据结构(Data, Len, Cap),无内存生命周期管理。此处将 Data 指向非法地址后,s 的底层指针失效;运行时 GC 不知情,后续访问触发 SIGSEGV。
关键风险点
SliceHeader不参与 Go 的逃逸分析与内存追踪- 手动构造的 slice 可能引用栈内存或已回收堆块
- Go 1.17 引入
unsafe.Slice替代方案,强制类型安全校验
| 方案 | 安全性 | 生命周期感知 | Go 版本支持 |
|---|---|---|---|
(*SliceHeader) + unsafe.Pointer |
❌ 非安全 | 否 | ≤1.16 |
unsafe.Slice(ptr, len) |
✅ 安全 | 是(需 ptr 有效) | ≥1.17 |
3.2 Go 1.18引入unsafe.Slice后的内存布局兼容性挑战与迁移实测
Go 1.18 引入 unsafe.Slice(unsafe.Pointer, int) 替代旧式 (*[n]T)(unsafe.Pointer(ptr))[:n:n] 惯用法,显著提升安全性与可读性,但引发底层内存布局假设失效风险。
兼容性断点示例
// Go 1.17 及之前(隐式数组头依赖)
ptr := unsafe.Pointer(&data[0])
old := (*[1<<20]int)(ptr)[:len(data):cap(data)] // 依赖编译器对[0]地址的“数组首元素”解释
// Go 1.18+ 推荐写法
new := unsafe.Slice((*int)(ptr), len(data)) // 直接构造切片,不涉数组头
该变更绕过编译器对底层数组头的隐式构造逻辑,导致部分依赖 reflect.SliceHeader 手动拼接或 unsafe 魔术偏移的代码panic。
迁移验证关键项
- ✅
unsafe.Slice返回切片的Data字段与原始指针一致 - ⚠️
Cap不再自动推导为uintptr(unsafe.Sizeof(T))*cap,需显式传入长度 - ❌ 旧式
(*[n]T)(p)[:]在 n 超出实际内存时可能静默越界,而unsafe.Slice无此行为(但也不做边界检查)
| 场景 | Go 1.17 行为 | Go 1.18 + unsafe.Slice |
|---|---|---|
ptr 指向单个 int,调用 Slice(ptr, 5) |
可能读写越界(未定义) | 同样未定义,但语义更清晰:仅按需构造长度为5的切片头 |
graph TD
A[原始指针 ptr] --> B{unsafe.Slice(ptr, n)}
B --> C[生成新切片头]
C --> D[Data = uintptr(ptr)]
C --> E[Len = n]
C --> F[Cap = n]
迁移时须同步校验所有 unsafe 切片构造点,并通过 -gcflags="-d=checkptr" 启用运行时指针有效性检测。
3.3 unsafe.Slice在栈分配slice与堆逃逸slice中的行为差异深度验证
栈上分配的 slice 示例
func stackSlice() []int {
var arr [4]int
return unsafe.Slice(&arr[0], 4) // ✅ 安全:arr 生命周期覆盖返回 slice
}
&arr[0] 指向栈帧内固定地址,函数返回后该栈空间被回收,但 Go 编译器能静态判定 arr 未逃逸(通过 -gcflags="-m" 验证),故 unsafe.Slice 返回值仍有效。
堆逃逸 slice 的陷阱
func heapEscape() []int {
arr := make([]int, 4) // → 逃逸至堆
return unsafe.Slice(&arr[0], 4) // ⚠️ 危险:arr 底层数组可能被 GC 回收
}
make 分配的底层数组位于堆,&arr[0] 取得的指针不绑定 arr 变量生命周期;函数返回后 arr 本地变量消失,但底层数组仅依赖 GC 引用计数——unsafe.Slice 返回的 slice 无引用保持,触发未定义行为。
关键差异对比
| 维度 | 站分配(数组字面量) | 堆逃逸(make) |
|---|---|---|
| 内存位置 | 栈 | 堆 |
| 生命周期控制 | 编译器静态保证 | GC 动态管理 |
| unsafe.Slice 安全性 | ✅ 可靠 | ❌ 不可靠 |
graph TD
A[调用 unsafe.Slice] --> B{底层数组来源}
B -->|栈上数组变量| C[编译器插入栈帧保护]
B -->|堆分配切片| D[仅依赖 GC 引用计数]
D --> E[无显式引用 → 提前回收风险]
第四章:Go 1.21 checkptr机制的运行时防御体系
4.1 checkptr插入点的编译器插桩逻辑与ssa阶段检测规则解析
checkptr 插入点在 SSA 构建后期触发,仅作用于指针解引用(*p)、切片/数组索引(s[i])及类型断言(x.(T))等潜在空指针风险操作。
插桩触发条件
- 操作数为非
nil常量且未被显式空检查覆盖 - 所属 Basic Block 已完成 dominator tree 计算
- 对应指针类型未标注
//go:noptr或unsafe.Pointer
SSA 检测规则核心
// 示例:编译器在 ssa.Builder 中生成 checkptr 调用
if ptr.Type().HasPointers() && !hasPrecedingNilCheck(ptr, block) {
call := b.NewCall("runtime.checkptr")
call.AddArg(ptr) // 参数1:待验证指针值
call.AddArg(b.ConstInt(1, types.Types[TUINT8])) // 参数2:校验模式(1=strict)
b.InsertAfter(call, instr) // 插入到原指令之后
}
该插桩逻辑确保所有动态指针访问前强制校验有效性。
call.AddArg(ptr)传递原始指针 SSA 值;第二参数控制校验强度:1表示严格模式(含 nil + 无效地址),为宽松模式(仅 nil)。
| 模式 | 校验项 | 性能开销 |
|---|---|---|
| strict (1) | ptr == nil ∨ !mem.valid(ptr) |
中等 |
| lax (0) | 仅 ptr == nil |
极低 |
graph TD
A[SSA Builder] --> B{是否指针操作?}
B -->|是| C[查找支配块内最近 nil 检查]
C --> D{已覆盖?}
D -->|否| E[插入 runtime.checkptr 调用]
D -->|是| F[跳过插桩]
4.2 利用GODEBUG=checkptr=0/1/2进行细粒度运行时指针合法性分级调试
Go 运行时通过 checkptr 调试标志实现对 unsafe.Pointer 转换行为的三级合规性检查:
GODEBUG=checkptr=0:完全禁用检查(仅用于性能压测或已验证安全场景)GODEBUG=checkptr=1:默认启用,拦截非法跨类型指针转换(如*int→*float64)GODEBUG=checkptr=2:最严格模式,额外检测指针算术越界与未对齐访问
# 启用严格检查并捕获非法转换
GODEBUG=checkptr=2 go run main.go
⚠️ 此环境变量仅在构建时未启用
-gcflags="-d=checkptr"时生效;若二者共存,以编译期标志为准。
检查级别对比表
| 级别 | 检测能力 | 典型触发场景 |
|---|---|---|
| 0 | 无检查 | unsafe.Pointer(&x) → *T(任意 T) |
| 1 | 类型兼容性校验 | *int → *[4]byte(大小不匹配) |
| 2 | 类型 + 对齐 + 边界双重校验 | &data[0] + 3 超出 slice 底层数组范围 |
运行时检查流程(简化)
graph TD
A[执行 unsafe.Pointer 转换] --> B{checkptr=0?}
B -- 是 --> C[跳过所有检查]
B -- 否 --> D{checkptr=1?}
D -- 是 --> E[验证类型可表示性]
D -- 否 --> F[执行完整三重校验]
E --> G[允许/拒绝]
F --> G
4.3 在自定义内存池(如sync.Pool改造)中规避checkptr误报的工程化实践
Go 1.22+ 的 checkptr 检查器会对非法指针转换(如 unsafe.Pointer 跨类型别名访问)触发编译期错误,而 sync.Pool 改造中常需绕过类型安全边界复用内存块,易被误判。
核心规避原则
- 避免
(*T)(unsafe.Pointer(p))直接强转; - 使用
reflect.SliceHeader/reflect.StringHeader中间桥接; - 所有
unsafe操作必须与原始分配类型严格对齐。
推荐实现模式
// 安全复用 []byte 底层内存:通过 reflect.SliceHeader 绕过 checkptr
func unsafeSlice(b []byte, cap int) []byte {
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
return reflect.MakeSlice(reflect.TypeOf(b).Elem(), len(b), cap).([]byte)
}
逻辑分析:
reflect.MakeSlice创建新切片头,而非重解释原始指针;cap参数控制容量上限,防止越界写入。b必须由make([]byte, ...)分配,确保底层内存可安全重用。
| 方案 | checkptr 兼容性 | 类型安全性 | 性能开销 |
|---|---|---|---|
直接 (*T)(unsafe.Pointer(...)) |
❌ 触发误报 | 低 | 极低 |
reflect.MakeSlice / MakeString |
✅ 安全 | 高 | 中(反射初始化) |
unsafe.Slice (Go 1.20+) |
✅(需 Go ≥1.20) | 中 | 低 |
graph TD
A[原始[]byte分配] --> B[放入Pool]
B --> C{Get时复用}
C --> D[通过reflect.SliceHeader重建头]
D --> E[返回类型安全切片]
4.4 静态分析工具(如govet、staticcheck)对checkptr敏感模式的协同检测覆盖
Go 1.22 引入 checkptr 检查机制,禁止不安全的指针算术跨类型边界。但单一工具覆盖有限,需多工具协同。
协同检测优势
govet -unsafeptr:捕获显式unsafe.Pointer转换中的类型不匹配staticcheck(SA1027):识别隐式指针逃逸与跨类型解引用go vet --checkptr(Go 1.22+):运行时插桩前的静态路径推导
典型误用模式检测对比
| 工具 | 检测模式 | 示例代码片段 |
|---|---|---|
govet |
(*int)(unsafe.Pointer(&x)) |
✅ 显式转换 |
staticcheck |
*(*int)(unsafe.Add(unsafe.Pointer(&b[0]), 4)) |
✅ 隐式越界解引用 |
go vet --checkptr |
(*int)(unsafe.Pointer(&s.field))(field 为非导出 struct 成员) |
✅ 跨包/跨类型边界 |
var s struct{ a, b int }
p := (*int)(unsafe.Pointer(&s.a)) // govet: OK; staticcheck: OK; checkptr: OK
q := (*int)(unsafe.Pointer(&s)) // govet: ❌ 无告警;staticcheck: ❌;checkptr: ✅ 跨字段边界
该转换将整个 struct 地址强制转为
*int,违反checkptr的“指向对象起始地址”约束。--checkptr启用后会报possible misuse of unsafe.Pointer,而其他工具因缺乏内存布局建模无法捕获。
graph TD
A[源码含unsafe.Pointer操作] --> B{govet -unsafeptr}
A --> C{staticcheck SA1027}
A --> D{go vet --checkptr}
B --> E[显式类型转换违规]
C --> F[隐式算术+解引用违规]
D --> G[运行时不可验证的指针路径]
第五章:unsafe安全编程范式的终局思考
在 Rust 生态中,unsafe 块从来不是“绕过安全”的快捷键,而是对底层契约的显式声明——开发者在此处主动承担内存安全、数据竞争与 ABI 兼容性的全部责任。真实项目中,这一范式正经历从“必要之恶”到“可验证契约”的范式迁移。
零拷贝图像处理中的边界校验实践
在 image-rs 的 WebAssembly 后端优化中,团队将 Vec<u8> 转换为 &[u8] 时不再依赖 std::slice::from_raw_parts 的裸调用,而是封装为 CheckedSlice::from_ptr(ptr, len),内部集成 ptr.is_null() == false && len <= isize::MAX as usize && ptr.add(len).is_aligned_to(1) 的运行时断言,并在 debug 模式下启用 addr_of! 辅助指针有效性验证。该封装被 cargo-fuzz 连续运行 72 小时未触发 panic。
FFI 跨语言调用的生命周期契约建模
当 Rust 与 C++ 共享 struct AudioBuffer { data: *mut f32, len: usize } 时,原始设计仅靠文档约定“C++ 保证 data 在回调返回前有效”。重构后引入 RAII 句柄 AudioBufferHandle,其 Drop 实现包含 extern "C" fn rust_buffer_dropped(handle_id: u64) 回调,强制 C++ 端注册释放钩子。以下为关键契约验证逻辑:
impl Drop for AudioBufferHandle {
fn drop(&mut self) {
if let Some(cb) = unsafe { CXX_DROP_CALLBACK.load(Ordering::Acquire) } {
cb(self.id);
}
}
}
安全边界收缩的量化指标
某嵌入式 SDK 的 unsafe 代码占比变化趋势(经 cargo-geiger 统计):
| 版本 | unsafe 行数 | 占比 | 主要收缩来源 |
|---|---|---|---|
| v1.2.0 | 1,842 | 3.7% | 手动 Box::new_uninit().assume_init() 替换为 MaybeUninit::array_assume_init() |
| v2.5.0 | 419 | 0.9% | std::ptr::copy_nonoverlapping 全部替换为 core::ptr::copy_nonoverlapping + const_evaluatable_checked 属性 |
内存模型验证工具链落地
在 Linux 内核 Rust 模块开发中,团队将 miri 集成进 CI 流水线,但发现其无法覆盖 asm! 场景。于是构建了自定义 llvm-mca 插件,在编译期对所有 unsafe 函数生成的 LLVM IR 进行指令级访存分析,标记出所有潜在的 load/store 地址未对齐路径,并关联源码行号。该插件已拦截 17 处 #[repr(packed)] 结构体在 ARM64 上的未对齐访问隐患。
跨 crate 不安全契约的文档化协议
tokio-uring 与 bytes 协作时,Bytes::as_ptr() 返回的指针有效期必须严格绑定于 Bytes 实例生命周期。双方通过 #[doc(alias = "unsafe-contract: bytes-ptr-validity")] 标准化注释,并由 cargo-semver-checks 插件扫描所有 unsafe 函数签名,确保每个 *const T 参数均附带 /// # Safety: ... must outlive the returned pointer 显式说明。
Rust 编译器正在将 unsafe 块转化为可形式化验证的中间表示,而开发者正用 #[cfg(verify_undefined_behavior)] 条件编译逐步收编未定义行为的灰色地带。
