Skip to content

v3.9.37

Choose a tag to compare

@github-actions github-actions released this 17 Sep 04:34
· 44 commits to master since this release

v3.9.37

这一版来自对 v3.9.36 的一次独立对抗审查(1 个 P1 + 4 个 P2,全部复现后修复并复核)。
核心是凭据库的锁被重写成"实例身份":并发写入不再可能丢记录,写者中途死亡留下的锁也不再
需要人工干预。默认路径逐字节不变(270 组完整请求 Buffer 对拍),无 API 破坏,ACU ^22 仍默认关。


用户可感知

并发写凭据不再可能丢记录(锁重写)

旧实现把锁的身份放在固定路径上(<凭据文件>.lock,里面一个 owner.json),回收死锁靠"重读那个
路径再删"。两个进程都读到同一个死 owner 时,先完成回收的那一个会重新拿锁并读取快照,而慢的那一个
继续按固定路径删除——删掉的是新持有者刚建立的锁,于是两个写者同时进入,后写的覆盖先写的。
确定性交错探针可复现。

现在:每次写入建立不可变的 claim 实例(目录名自带 host/pid/随机 token,身份就是名字本身),
回收只删除被证明确实已死的那一个实例(同机 + ESRCH),发布前再次确认自己仍持有该实例。
另外在固定路径上放了一个永久普通文件哨兵,旧版本既创建不了也删不掉它——因此升级后新旧版本
不会互相覆盖

写者中途死亡不再需要人工解锁

旧实现在"创建目录成功、写 owner.json 之前"死亡(或该写入撞上 ENOSPC)会留下无法回收的锁:
新的身份写在名字里,不再有"第二次写决定身份"的窗口,这类残留会被作为可证明死亡的实例自动回收。

运维注意(唯一需要动手的地方):如果升级前旧版本崩溃留下了空的 <凭据文件>.lock 目录,
新版本不会自动接管它(空目录可能是另一个旧写者的初始化窗口,年龄不能作为死亡证据)。
此时写入会明确返回 BUSY,删掉那个空目录即可恢复。这是有意的保守设计。

其他

  • 登录响应里的 credentialStored 只在实际落盘成功后才为 true(此前凭据存储未启用也会报"已保存");
  • 会话复用索引不再泄漏:resolve 路径写入的索引现在也登记归属,淘汰时清理得干净(复现中曾残留 29 条)。

工程与门禁

  • 修掉 CI 门禁的自比较:wire-byte-identity 原用 git describe --tags --abbrev=0,在一个已打
    tag 的提交
    上它会返回该提交自己的 tag,于是"与上一个 release 比较"退化成与自己比较、门禁空转。
    现在由 scripts/wire-base.mjs 选择严格早于待测提交的 annotated release tag,并把基线 SHA 写进
    日志;无可选基线时失败关闭。
  • 新增确定性交错与故障注入测试族:锁回收、悬停迁移、ENOSPC/EIO、发布前失去 claim 等 24 条;
    两条最初没有任何测试能判红的安全层(guard 后二次读取旧身份、缺失身份永不算死亡)已补齐用例,
    各自在对应突变下变红。

验证

  • npm test 4683 tests / 4606 pass / 0 fail / 77 skipped(skip 全部带原因);
  • 270/270 完整请求字节对拍(vs 上一个 release),黄金帧 12/12;
  • 突变规格 68 份 / 678 条,anchor 全唯一;两条被改动的基线实测复算吻合(317、50);
  • 独立复核(由写这批代码的会话执行,只信复跑结果):发布围栏与索引归属两层在源码被改坏时测试确实变红

破坏性变更

无公开接口、路由、字段或默认开关的变化。