我的极简、内存安全的Go rsync如何规避漏洞

Lobsters Hottest 新闻

摘要

深入探讨极简、内存安全的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 会自动将变量初始化为零。

相似文章

Go中的零拷贝: sendfile、splice 以及 io.Copy 的成本

Hacker News Top

本文解释了Go的io.Copy如何自动使用sendfile和splice进行TCP上的零拷贝文件传输,并展示了将文件包装成普通的io.Reader(例如用于日志记录)会悄然禁用此优化,导致性能显著下降。基准测试和strace输出说明了这种影响。

Golang 代码审查笔记 II

Lobsters Hottest

来自 elttam 的后续博客文章,介绍了提高安全性的新 Go 语言特性、在代码审计中发现的有问题的编码模式(footguns),以及用于捕获这些模式的 Semgrep 规则。