我的极简、内存安全的Go rsync如何规避漏洞
摘要
深入探讨极简、内存安全的Go实现的rsync如何避免原始C版本中存在的十几个漏洞,并与OpenBSD的openrsync及纵深防御技术进行对比。
<p><a href="https://lobste.rs/s/5y9u93/how_my_minimal_memory_safe_go_rsync_steers">评论</a></p>
查看缓存全文
缓存时间: 2026/05/24 14:57
# 我简洁、内存安全的 Go rsync 如何避开漏洞
来源:https://michael.stapelberg.ch/posts/2026-05-24-minimal-memory-safe-go-rsync-vulns/
目录
- [背景:我自己的 rsync](#context-my-own-rsync)
- [安全漏洞](#vulns)
- [2025 年 1 月批次](#jan2025)
- [2026 年 5 月批次](#may2026)
- [Go 的结论](#go-verdict)
- [gokrazy/rsync 的结论](#gokrazy-rsync-verdict)
- [术语不精确](#terminology)
- [与 OpenBSD 的 openrsync(C 语言)的比较](#openrsync)
- [纵深防御](#defenseindepth)
- [Linux 挂载命名空间](#mountnamespaces)
- [systemd 加固](#systemdhardening)
- [Linux Landlock](#landlock)
- [Go 的 os.Root](#goosroot)
- [结论](#conclusion)
2025 年 1 月,多位不同的安全研究人员共公布了 rsync(链接:https://www.openwall.com/lists/oss-security/2025/01/14/3)的 6 个安全漏洞,其中一些允许任意代码执行和文件泄漏。我自然想知道我的 gokrazy/rsync(链接:https://github.com/gokrazy/rsync)实现是否受到影响以及如何受影响。用现代内存安全语言 Go 实现我自己的(兼容但精简的)rsync,真的能排除整个类别的安全漏洞吗?这篇深入分析文章自 2025 年 1 月起就在酝酿,但因为我们在过程中又发现了更多未公布的漏洞而推迟了!“安全漏洞”一节现在涵盖了 2025 年 1 月批次和 2026 年 5 月批次的所有 12 个漏洞。**如果你在生产环境中运行(上游、Samba)rsync(链接:https://github.com/RsyncProject/rsync),请升级到 3.4.3 或更新版本。** 如果你在生产环境中运行 gokrazy/rsync(链接:https://github.com/gokrazy/rsync),请升级到 v0.3.3 或更新版本。你可以跳过具体安全问题的细节,直接跳转到:
- [关于使用 Go 是否有帮助的结论](#go-verdict)。
- [关于像 gokrazy/rsync 这样的精简重新实现是否有帮助的结论](#gokrazy-rsync-verdict)。
- [我与 OpenBSD 的 `openrsync`(用 C 编写)的比较](#openrsync)。
- [可在 Linux 上使用的纵深防御机制](#defenseindepth)。
- [结论](#conclusion)。
## 背景:我自己的 rsync
作为背景,我在 2022 年 6 月写过关于 rsync、我如何使用它以及它如何工作的博客文章(链接:https://michael.stapelberg.ch/posts/2022-06-18-rsync-overview/)。另见所有标记为“rsync”的帖子(链接:https://michael.stapelberg.ch/posts/tags/rsync/)。
编写自己的 rsync(当时只有服务器端,现在支持所有方向)的最初动机是为 distri(我用于快速包管理的 Linux 发行版研究项目,链接:https://distr1.org/)提供软件包。我想将这些包托管在 router7(链接:https://router7.org/)(我的小型家用 Linux+Go 互联网路由器)上,而 router7 又建立在 gokrazy(链接:https://gokrazy.org/)(我的 Go 应用平台)之上。我目前仍为这个原始目的运行着多个 gokrazy/rsync 服务器,还有很多其他用途!将 rsync 作为一个原语(你可以将其链接到你的 Go 程序中!)可用真是太棒了。
## 安全漏洞
本文涵盖以下安全漏洞:
- CVE-2024-12084 至 12088(原始报告,链接:https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj)
- CVE-2024-12747(由 Aleksei Gorban “loqpa” 独立发现)
- CVE-2026-29518(由 Damien Neil 和我发现!并由 Nullx3D(链接:https://nullx3d.com/)独立发现)
- CVE-2026-43617 至 43620
- CVE-2026-45232
第一批上述漏洞于 oss-security 邮件列表(链接:https://www.openwall.com/lists/oss-security/2025/01/14/3)上公布,但请注意原始报告比 oss-security 摘要更详细!后来的漏洞通过 rsync 项目的 GitHub Security Advisories(链接:https://github.com/RsyncProject/rsync/security/advisories)公布。
### 2025 年 1 月批次
#### CVE-2024-12084:堆缓冲区溢出(9.8)
**摘要:**
- rsync 执行了不充分的验证:它从网络读取(攻击者控制的)校验和长度,并将该长度与 `MAX_DIGEST_LEN` 进行比较。
- 然而,rsync 的数据结构总是声明一个 16 字节的缓冲区:`char sum2[SUM_LENGTH]`
- `SUM_LENGTH` 始终为 16(字节),足以容纳 MD4(链接:https://en.wikipedia.org/wiki/MD4)或 MD5(链接:https://en.wikipedia.org/wiki/MD5)校验和。
- `MAX_DIGEST_LEN` 过去是 16(字节),但当 rsync 编译时支持 SHA256 或 SHA512 校验和时,它可以更大。
- 因此,边界检查无效!攻击者可以越界写入。
- 该问题是在 2022 年 9 月的提交 `ae16850`(链接:https://github.com/RsyncProject/rsync/commit/ae16850dc58e884eb9f5cb7f772342b2db28f471)中引入的,该提交添加了 SHA256/SHA512 校验和的支持。
点击展开**校验和长度验证不当的完整描述**(引用 Google 安全报告,链接:https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj)
> 当守护进程读取校验和时,会读取两种不同的校验和:
> 1. 一个 32 位的 Adler-CRC32 校验和
> 2. 文件块的摘要。
>
> 摘要算法在协议协商开始时确定。相关代码如下所示:
>
> sender.c(链接:https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/sender.c#L96-L100):
> ````
> s->sums = new_array(struct sum_buf, s->count);
> for (i = 0; i < s->count; i++) {
> s->sums[i].sum1 = read_int(f);
> read_buf(f, s->sums[i].sum2, s->s2length);
> ````
> 最重要的是,注意 `sum2` 字段用 `s->s2length` 字节填充。`sum2` 的大小始终为 16:
>
> rsync.h(链接:https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/rsync.h#L955-L962)
> ````
> #define SUM_LENGTH 16
> // ...
> struct sum_buf {
> OFF_T offset; /**< 文件中的偏移量 */
> int32 len; /**< 文件块的长度 */
> uint32 sum1; /**< 简单校验和 */
> int32 chain; /**< 下一个哈希表冲突 */
> short flags; /**< 标志位 */
> char sum2[SUM_LENGTH]; /**< 校验和 */
> };
> ````
> `s2length` 是攻击者控制的值,可以最大为 `MAX_DIGEST_LEN` 字节,如下一个片段所示:
>
> io.c(链接:https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/io.c#L1979-L1984)
> ````
> sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f);
> if (sum->s2length < 0 || sum->s2length > MAX_DIGEST_LEN) {
> rprintf(FERROR, "Invalid checksum length %d [%s]\n", sum->s2length, who_am_i());
> exit_cleanup(RERR_PROTOCOL);
> }
> ````
> 问题在于 `MAX_DIGEST_LEN` 可能大于 16 字节,具体取决于编译时支持的摘要算法:
>
> md-defines.h(链接:https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/lib/md-defines.h#L11-L21)
> ````
> #define MD4_DIGEST_LEN 16
> #define MD5_DIGEST_LEN 16
> #if defined SHA512_DIGEST_LENGTH
> #define MAX_DIGEST_LEN SHA512_DIGEST_LENGTH
> #elif defined SHA256_DIGEST_LENGTH
> #define MAX_DIGEST_LEN SHA256_DIGEST_LENGTH
> #elif defined SHA_DIGEST_LENGTH
> #define MAX_DIGEST_LEN SHA_DIGEST_LENGTH
> #else
> #define MAX_DIGEST_LEN MD5_DIGEST_LEN /* 16 bytes */
> #endif
> ````
> `SHA256` 支持很常见,它将 `MAX_DIGEST_LENGTH` 值设置为 64。因此,攻击者可以在 `sum2` 缓冲区限制之外最多写入 48 字节。
**上游修复:** CVE-2024-12084 的上游修复(链接:https://github.com/RsyncProject/rsync/commit/0902b52f6687b1f7952422080d50b93108742e53)将 `sum2` 字段改为动态分配的 `sum2_array` 字段,该字段以 `xfer_sum_len` 长度分配,并修复了边界检查,使其检查 `xfer_sum_len`(此次传输算法的校验和长度)。
**Go 能帮助防止吗?** 是的:缺失或错误的边界检查在 Go 中不会导致堆缓冲区溢出!相反,尝试越界写入会导致 panic,因为 Go 运行时会执行边界检查。
**gokrazy/rsync 情况如何?** gokrazy/rsync 也存在验证不足的问题!不过我们的问题不同:不是大小混淆,我们根本就没有对校验和头部进行任何验证——哎呀!我们可以通过更改代码并运行测试来确认 Go 运行时的边界检查会在尝试越界写入时触发(链接:https://github.com/gokrazy/rsync/blob/3a9d1ec136e049b2bffc9db15230eb70308ac90d/rsyncd/sender.go#L136):
```` diff
diff --git i/types.go w/types.go
index 5601697..899fcb8 100644
--- i/types.go
+++ w/types.go
@@ -59,7 +59,7 @@ func (sh *SumHead) WriteTo(c *rsyncwire.Conn) error {
var buf rsyncwire.Buffer
buf.WriteInt32(sh.ChecksumCount)
buf.WriteInt32(sh.BlockLength)
- buf.WriteInt32(sh.ChecksumLength)
+ buf.WriteInt32(512 /*sh.ChecksumLength*/)
buf.WriteInt32(sh.RemainderLength)
return c.WriteString(buf.String())
}
````
如预期,Go 运行时会 panic 并显示以下信息:
````
panic: runtime error: slice bounds out of range [:512] with length 16
goroutine 277 [running]:
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).receiveSums(0xc0000d7b68)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:136 +0x339
github.com/gokrazy/rsync/rsyncd.(*sendTransfer).sendFiles(0xc0000d7b68, 0xc000120820)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/sender.go:46 +0x134
github.com/gokrazy/rsync/rsyncd.(*Server).handleConnSender(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:397 +0x26a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleConn(0xc000476090, {{0x95ed9b, 0x7}, {0xc000426810, 0x2a}, {0x0, 0x0, 0x0}}, {0xa2a120, 0xc0000b2ba0}, ...)
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:351 +0x37a
github.com/gokrazy/rsync/rsyncd.(*Server).HandleDaemonConn(0xc000476090, {0x94db80?, 0xc00018a040?}, {0x7fd15838b118, 0xc000428028}, {0xa2bd90, 0xc0002303c0})
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:307 +0xdbb
github.com/gokrazy/rsync/rsyncd.(*Server).Serve.func2()
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:450 +0xaf
created by github.com/gokrazy/rsync/rsyncd.(*Server).Serve in goroutine 260
/home/michael/go/src/github.com/gokrazy/rsync/rsyncd/rsyncd.go:448 +0xd2
````
当然,整个服务器崩溃并不是最好的失败模式,因此我添加了缺失的边界检查,将 panic 转化为错误(链接:https://github.com/gokrazy/rsync/commit/178216f10f2e05fd74bf865f2d0725fce4f907cd)。
#### CVE-2024-12085:栈信息泄漏破坏 ASLR(7.5)
**摘要:** 由于与上一个 CVE-2024-12084 漏洞相同的验证缺失,攻击者可以选择一个具有短校验和的校验和算法(例如,8 字节校验和的 `xxhash64`),然后声称发送了更长的校验和(例如 9 字节),从而在响应中泄漏一个字节的未初始化栈内容。泄漏一个字节的栈内容可能看似无害,但正如 Google 安全报告(链接:https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj)所说:
> 第一对漏洞是堆缓冲区溢出和信息泄漏。当结合使用时,它们允许客户端在运行 Rsync 服务器的机器上执行任意代码。客户端只需要对服务器具有匿名读取访问权限。
点击展开**信息泄漏的完整描述**(引用 Google 安全报告,链接:https://github.com/google/security-research/security/advisories/GHSA-p5pg-x43v-mvqj)
> 守护进程在 `hash_search()`(链接:https://github.com/RsyncProject/rsync/blob/9615a2492bbf96bc145e738ebff55bbb91e0bbee/match.c#L140-L145)中将客户端发送的块校验和与本地文件内容进行匹配。函数序言的一部分是在栈上分配一个 `MAX_DIGEST_LEN` 字节的缓冲区:
>
> ````c
> static void hash_search(int f, struct sum_struct *s, struct map_struct *buf, OFF_T len)
> {
> OFF_T offset, aligned_offset, end;
> int32 k, want_i, aligned_i, backup;
> char sum2[MAX_DIGEST_LEN];
> ````
>
> 然后守护进程遍历客户端发送的校验和,为每个块生成摘要,并与远程摘要进行比较:
>
> ````c
> if (!done_csum2) {
> map = (schar *)map_ptr(buf, offset, l);
> get_checksum2((char *)map, l, sum2);
> done_csum2 = 1;
> }
> if (memcmp(sum2, s->sums[i].sum2, s->s2length) != 0) {
> false_alarms++;
> continue;
> }
> ````
>
> 值得注意的是,比较的字节数又是 `s->s2length` 字节。在这种情况下,比较不会越界,因为 `s->s2length` 最多可以是 `MAX_DIGEST_LEN`。然而,本地的 `sum2` 缓冲区(不要与攻击者控制的 `s->sums[i].sum2` 混淆)是栈上的缓冲区,未被清零,因此包含未初始化的栈内容。
>
> 恶意客户端可以为文件的给定块发送一个(已知的)`xxhash64` 校验和,这导致守护进程向栈缓冲区 `sum2` 写入 8 个字节。然后攻击者可以将 `s->s2length` 设置为 9 字节。这种设置的结果是前 8 个字节匹配,而第 9 个字节(攻击者控制的)与一个未知的未初始化栈数据值进行比较。
>
> 攻击者可以将一个文件分成 255 个块,从而在每次文件下载中泄漏一个字节。攻击者可以逐步重复该过程,可以在同一连接中,也可以通过重置连接。结果,他们可以泄漏 `MAX_DIGEST_LEN - 8` 字节的未初始化栈数据,其中可能包含指向堆对象、栈 cookie、局部变量以及全局变量和返回指针的指针。利用这些指针,他们可以破解 ASLR。
**上游修复:** 有两个相关的上游修复:
- 提交“Some checksum buffer fixes”(链接:https://github.com/RsyncProject/rsync/commit/0902b52f6687b1f7952422080d50b93108742e53)阻止了此攻击,因为攻击者控制的 `s->s2length` 不再能大于传输的校验和长度。
- 提交“prevent information leak off the stack”(链接:https://github.com/RsyncProject/rsync/commit/589b0691e59f761ccb05ddb8e1124991440db2c7)将 `sum2` 内存初始化为零,从而使得通过 `sum2` 的任何栈泄漏都不可能。
**Go 能帮助防止吗?** 是的:Go 默认将所有变量初始化为零值。Go 程序员不需要记住显式初始化变量。
**gokrazy/rsync 情况如何?** gokrazy/rsync 不受此漏洞影响,因为它在读取校验和之前进行了适当的验证。此外,Go 会自动将变量初始化为零。
相似文章
Rust与C/C++在内存安全CVE上的差异
分析Rust与C/C++在内存安全CVE报告方式上的不同,论证即使存在错误,Rust的设计也能降低某些类型漏洞的发生。
Openrsync:由OpenBSD团队开发的rsync实现
Openrsync是OpenBSD团队采用BSD许可重新实现的rsync,兼容现代rsync协议并支持Unix系统。
Go中的零拷贝: sendfile、splice 以及 io.Copy 的成本
本文解释了Go的io.Copy如何自动使用sendfile和splice进行TCP上的零拷贝文件传输,并展示了将文件包装成普通的io.Reader(例如用于日志记录)会悄然禁用此优化,导致性能显著下降。基准测试和strace输出说明了这种影响。
Golang 代码审查笔记 II
来自 elttam 的后续博客文章,介绍了提高安全性的新 Go 语言特性、在代码审计中发现的有问题的编码模式(footguns),以及用于捕获这些模式的 Semgrep 规则。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。