登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  业界新闻

Rust Miri 缓存输出泄漏问题的工程防护思路

来源:17golang原创

时间:2026-11-01 03:52:49 386浏览 收藏

Rust Miri 本身是用来发现未定义行为的工具,但它运行在 CI 中时也会接触进程环境。旧版 Miri 曾把环境变量保存到 target/,如果工作流再把这个目录放进 GitHub Actions 缓存,并允许低信任 Pull Request 恢复缓存,原本只在 CI 步骤中出现的 secrets 就可能跟着构建输出留下来。工程上的重点不是停用所有 Miri,而是让 Miri 任务不携带业务凭据,也不把它产生的 target/ 当作可跨信任边界复用的缓存。

官方资料:https://blog.rust-lang.org/2026/09/21/github-actions-leaking-secrets-when-miri-output-is-cached/

Miri 项目地址:https://github.com/rust-lang/miri

GitHub 缓存文档:https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching

先按四个条件判断是否进入风险面:CI 是否运行 cargo miri,该步骤是否能读取 secrets,是否缓存了 target/,以及低信任 PR 是否能恢复这份缓存。命中越多,越应该先隔离 secrets、停止缓存 Miri 的 target/,再清理旧缓存并评估凭据轮换。

先把 Miri 缓存输出的风险链拆开

这类问题通常不是单个配置造成的,而是四个普通动作叠加:Miri 需要在多次调用之间保留部分构建相关环境;工作流把构建目录缓存起来;主分支或受信任分支能够写入缓存;低信任 PR 能够在后续运行中读取缓存。只要敏感环境变量在 Miri 步骤可见,缓存就不再只是“加速依赖安装”的目录。

Miri CI、target缓存和低信任PR之间的环境变量风险链结构图
图1:Miri CI、target/ 缓存与低信任 PR 之间的风险链说明图;这是静态结构图,不是运行截图或泄漏证据。

官方公告把影响条件写得很具体:运行 cargo miri 的 job 有 secrets、工作流缓存 target/,并且缓存可以被 Pull Request 使用。不要只盯着 actions/cache,第三方 Rust 缓存 action 也可能把目标目录纳入缓存范围。

盘点工作流到底暴露了什么

先在仓库中查出 Miri、缓存 action 和目标目录。这个命令只读取工作流文本,不会打印 secrets 的值;输出应作为整改清单,而不是把整份 CI 日志上传到公共位置。

# 在仓库根目录执行,定位 Miri、缓存 action 与 target/ 的声明。
rg -n --glob '.github/workflows/**' 'cargo miri|miri|actions/cache|rust-cache|target/' .

# 单独找出工作流级和 job 级的 secrets 继承,便于缩小权限范围。
rg -n --glob '.github/workflows/**' 'secrets:|secrets\.|env:' .

把结果按 job 记录四列:是否运行 Miri、是否能看到 secrets、缓存路径是什么、缓存的读取者是谁。env: 写在 workflow 或 job 级别时,往往比一个 step 的 env: 影响范围更大;前置步骤通过环境文件或生成文件传递值,也应视为同一条数据流。

还要区分“缓存 Cargo 下载依赖”和“缓存编译输出”。前者可以服务普通构建,但仍不能放入令牌;后者可能包含工具生成的元数据,尤其是 target/,不应在低信任 PR 与受信任分支之间共享。

让 Miri 步骤不携带业务 secrets

最小修复是把 Miri 从发布凭据所在 job 中拆出来。Miri 只负责检查 Rust 代码的未定义行为,不需要云密钥、部署令牌或生产 API 密钥;需要访问私有依赖时,也应采用最小范围、短生命周期的凭据,并重新判断缓存是否能跨信任边界读取。

# 示例:Miri job 不继承 workflow 级 secrets,避免测试工具接触发布凭据。
name: rust-ci

on: [push, pull_request]

jobs:
  miri:
    runs-on: ubuntu-latest
    # 这里故意不设置 secrets 环境变量,也不复用发布 job 的 env。
    steps:
      - uses: actions/checkout@v4
      - name: 安装 Miri
        run: rustup component add miri --toolchain nightly
      - name: 运行 Miri 测试
        run: cargo +nightly miri test

  release:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # 只有真正需要发布的步骤才读取部署凭据。
      - name: 发布
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./ci/release.sh

上面的版本只示意权限边界。不要把 secrets 放回 workflow 顶层,再期待 Miri 步骤“不会主动使用”它;环境变量一旦进入进程,工具、构建脚本或依赖都可能看到它。对于来自外部贡献者的 Pull Request,优先使用不含 secrets 的 pull_request 流程,发布则放到受保护分支的独立 job。

不要把 Miri 的 target/ 当成通用缓存

GitHub Actions 的缓存适合复用明确、可重建且不含敏感信息的输入。缓存 action 在命中时会恢复路径,在成功完成后也可能用当前路径创建新缓存;因此“这次没有打印 secrets”不等于缓存内容没有风险。

# 示例:Miri job 不缓存 target/;让每次检查使用隔离的构建输出。
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 安装 Miri
        run: rustup component add miri --toolchain nightly
      # 不使用 actions/cache 缓存 target/,避免跨运行复用 Miri 产物。
      - name: 运行 Miri
        run: cargo +nightly miri test

  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 缓存 Cargo 下载目录
        uses: actions/cache@v4
        with:
          # 这里只缓存下载内容,不把 target/ 放进共享缓存。
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      - name: 运行普通测试
        run: cargo test --all-targets

依赖缓存也不是天然安全区。GitHub 文档明确提醒不要把访问令牌或登录凭据放在缓存路径中;缓存路径应尽量只包含可由锁文件和工具链重新生成的依赖数据。若团队必须缓存 target/ 以缩短普通测试时间,至少不要让 Miri job 写入同一缓存键,也不要让低信任 PR 读取受信任分支生成的目标目录。

补上权限、日志和旧缓存处置

修复配置后,还要处理已经生成的缓存。旧缓存不会因为工作流文件改了就自动消失;应删除受影响的缓存,并按照组织的凭据响应流程判断是否轮换可能暴露的令牌。文章讨论的是工程防护,不假设某个项目一定已经发生实际窃取。

# 用 GitHub CLI 查看仓库缓存条目;先确认仓库和权限,再按组织流程删除受影响条目。
gh cache list --limit 100

# 列出工作流中的高风险组合,供代码审查复查;不读取任何 secret 值。
rg -n --glob '.github/workflows/**' 'cargo +?nightly miri|cargo miri|target/|secrets\.' .

# 检查本地实际使用的 Rust 工具链,避免把“已修复”误当成任意 nightly 都已更新。
rustup show active-toolchain

日志中不要输出环境变量、完整配置对象或带令牌的命令行。只记录“哪个 job 运行了 Miri、是否启用 target/ 缓存、清理动作是否完成、工具链版本是什么”这类可审计事实。若组织决定轮换令牌,应把轮换时间、受影响范围和回滚联系人记录在安全变更单中,而不是写入构建日志。

Miri CI 防护基线的 secrets隔离、缓存隔离、旧缓存清理和发布检查结构图
图2:围绕 CI 防护基线的四层控制说明图;这是静态防护结构图,不代表某个仓库已经完成整改。

发布前用四类检查确认没有回退

Rust 官方公告说明,后续 nightly 将不再让 Miri 全量保存环境变量,但不要把工具链升级当成唯一防线。缓存路径、job 权限和第三方构建脚本仍然需要独立检查。发布前可以按下面的清单复查:

检查项合格状态发现问题时的动作
Miri 工具链记录实际 nightly,并确认使用了包含修复的版本更新并固定工具链,再重新审阅缓存和权限
敏感输入Miri job 没有业务 secrets 或不必要的令牌将 secrets 下沉到独立发布步骤
缓存范围Miri 不写入可跨信任边界复用的 target/删除该缓存路径或完全关闭该 job 缓存
历史缓存受影响缓存已清理,凭据风险已评估清理缓存并按响应流程轮换凭据

如果一个仓库只在受保护分支运行 Miri,且缓存从不被外部 PR 恢复,风险面会小很多,但仍不建议让不必要的 secrets 进入 Miri 进程。把“谁能写入缓存”和“谁能读取缓存”分别画出来,通常比只看缓存 key 更容易发现跨信任边界的问题。

几个容易混淆的边界

只升级 nightly 就够了吗?

不够。升级可以消除公告所描述的 Miri 全量环境保存行为,但工作流中的 build script、第三方 action 和自定义测试工具仍可能把环境写入构建产物。secrets 最小化和缓存隔离应继续保留。

只缓存 Cargo registry 会不会自动安全?

不会自动安全。它比缓存 target/ 更容易限定范围,但仍要确认路径中没有令牌、私有配置或构建脚本写入的敏感文件。GitHub 的缓存文档把“缓存路径可被读取者访问”作为基本前提。

Pull Request 不能写缓存,还需要担心吗?

需要区分写和读。常见配置是受信任分支写入,Pull Request 只恢复;如果缓存中存在敏感输出,不能写入并不代表不能读取。应同时审阅缓存读取范围、分支策略和事件触发方式。

Miri 任务完全禁用是不是最稳妥?

它能快速降低当前风险,但会失去对未定义行为、数据竞争和部分别名规则的动态检查。更合适的长期方案通常是保留无 secrets、无共享 target/ 缓存的 Miri job,把发布凭据和构建加速策略放到不同信任边界。

归根结底,Rust Miri 缓存输出泄漏问题提醒我们:构建产物也要按数据敏感度治理。先拆出 Miri、secrets、缓存路径和 PR 读取者四个条件,再落实步骤级凭据、缓存隔离、旧缓存清理和工具链复查,才能让修复从一次性公告响应变成可持续的 CI 安全基线。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>