从一个拼写错误到两个 PR:给 xLua 挑错的一次记录

学 xLua 的时候看到一个拼错的文件名,顺着挖下去,最后挖出了签名工具里一个三年多没人报告的回归。这篇记录怎么核实一个问题是真的、为什么最后的修法和一开始想的正好相反,以及哪些问题我决定不提。

学 xLua 的协程示例时,我注意到一行代码:

1
luaenv.DoString("require 'coruntine_test'");

coruntine,少了个 o、多了个 n。示例照样能跑,因为 Lua 文件本身也叫 coruntine_test.lua.txt,两边拼错得很一致。

这种错误本身没什么影响,但它让我好奇:一个这么多人用的项目,还有多少类似的地方?于是我顺着找了下去。这篇记录的就是这一路:从几个拼写错误开始,最后找到签名工具里一个从 2023 年起就存在的回归,向 Tencent/xLua 提了两个 PR。

一份清单

我先在 ChatGPT 的帮助下,把找到的问题整理成了一份清单,一共 8 条:

编号问题
XL-01签名工具:生成器写出的私钥叫 key_ras,签名器却去找 key_rsa
XL-02密钥生成器检测到已有密钥后,仍然覆盖
XL-03协程示例的 coruntine_test
XL-04异步热修复示例里的局部变量 rightPposition、inteval
XL-05示例目录 04_LuaObjectOrented
XL-06热修复示例的类名 StatefullTest
XL-07示例和单测里的结构体 Pedding,疑似 Padding
XL-08英文 README 把 02 示例描述成 “use Mono to write MonoBehaviour”

清单看着挺像回事,但它是对话里整理出来的,我并不确定每一条都对。在拿去提 PR 之前,得先把每一条都核实一遍。

先核实:每一条都是真的吗

核实是交给 Claude Code 做的,我在旁边看它怎么查。方法其实很朴素:

  1. 用最新的源码。 从官方仓库克隆 master,而不是用自己学习时改过的那份工程。
  2. 每个名字都 git grep 一遍。 不只看清单里列的那一处,还要看所有引用:代码、配置、文档,连已经生成好的 docs/public/*.html 都算。XL-06 的 StatefullTest 就是这样查出来的:除了 C# 代码,它还出现在四份热修复文档的示例代码里。
  3. 用 git log 找来源。 一个问题是一直都在,还是某次修改引入的,决定了它该怎么描述。
  4. 查重。 用 gh search issues --include-prs 按每个拼错的词、文件名、关键词搜一遍上游的 Issue 和 PR。

结果是 8 条都对得上源码,上游也没有专门报告这些问题的 Issue 或 PR。但「是真的」不等于「值得提」,而清单里最重要的第一条,影响和修法都和清单写的不一样。

最有意思的一条:签名工具的文件名

xLua 支持给 Lua 脚本签名,防止被篡改。流程分三步:

  1. KeyPairsGen 在当前目录生成一对 RSA 密钥:私钥 key_ras,公钥 key_ras.pub;
  2. FilesSignature 用私钥给每个 .lua 文件签名,把 128 字节的签名写在文件开头;
  3. 游戏运行时,SignatureLoader 用打包进去的公钥校验签名。

这三步之间唯一的约定,就是文件名。而源码里,生成器写的是 key_ras,签名器找的是 key_rsa:

1
2
3
4
5
6
7
8
9
// KeyPairsGen.cs
File.WriteAllText("key_ras", rsa.ToXmlString(true));

// FilesSignature.cs
if (!File.Exists("key_rsa"))
{
    Console.WriteLine("No key_rsa file found!");
    return;
}

git log 显示,签名器的这个名字是 2023 年 8 月的 #1087 改的。那个 PR 的说明里写着「修复了代码中的一些变量命名和拼写错误」:贡献者大概觉得 ras 是 rsa 拼反了,就顺手改了。但生成器、文档、示例资源都没跟着改,于是两边对不上了。

官方工具其实没坏

清单把这条标成了最高优先级的功能回归。但 Claude Code 在核对时多看了一步:仓库里除了源码,还有一份预编译好的 Tools/KeyPairsGen.exe 和 Tools/FilesSignature.exe,文档让用户直接用这两个 exe。

它从两个 exe 里把字符串提了出来,发现两个都还在用 key_ras。再看 git log,这两个 exe 最后一次更新是 2023 年 6 月,比 #1087 早两个多月。

所以真实情况是:按文档用官方 exe 的人,一切正常;只有从源码自己编译签名器的人会撞上这个问题。比如 macOS 用户,没法直接运行这两个 exe。上游有一个从 2021 年开到现在的 Issue #942,问的正是有没有 mac 版或者源码。

这改变了两件事。

第一是严重程度。这不是「所有人的签名流程都坏了」,而是「源码和发布的工具不一致了」。PR 说明里要按这个写,不能夸大。

第二是修法。清单建议把 key_rsa 定为标准名,生成器、文档、示例资源都跟着改。但现在看,用 key_ras 的有生成器、两个 exe、文档、示例资源,以及所有用户手里已经存在的密钥文件;用 key_rsa 的只有被 #1087 改过的那一个源文件。要是全部统一成 key_rsa,就得重新编译、提交 exe(外部贡献者提交的二进制,维护者一般不会收),用户已有的密钥文件也会失效。

所以最后的修法正好反过来:把签名器里的三处 key_rsa 改回 key_ras。一个文件,三行。

这也是我在这件事里最大的收获:清单给出的方向是错的,而纠正它的证据不在源码里,在二进制里。 只读 .cs 文件的话,这一点是看不出来的。

顺带的一个 bug:说「已存在」,然后照样覆盖

同一个生成器里还有另一个问题:

1
2
3
4
5
6
if (File.Exists("key_ras") || File.Exists("key_ras.pub"))
{
    Console.WriteLine("key pairs existed!");
}
var rsa = new RSACryptoServiceProvider();
File.WriteAllText("key_ras", rsa.ToXmlString(true));

它检查了文件是否存在,提示了一句,然后……接着往下执行,把两个文件都覆盖了。if 分支里少了一个 return。

这个后果比文件名那个更严重:误运行一次,私钥就被换掉了。已经发布的游戏里打包的是旧公钥,之后用新私钥签的脚本,在玩家那边全都过不了校验。

修法就是补上那个 return。

怎么证明修好了

改动只有四行,但我不想只靠「看起来对」。验证分成了几层:

先复现。 用 Mono 的 mcs 编译修改前的源码,确认两个 bug 都真实存在:签名器输出 No key_rsa file found! 后直接退出;第二次运行生成器后,两个密钥文件的 SHA1 都变了。

再跑完整流程。 修改后重新编译,在一个临时目录里依次运行:生成密钥、对包含子目录的 .lua 文件签名、用公钥校验。再运行一次生成器,确认文件没被改动;只留公钥或只留私钥时,也都不会写入任何文件。

校验程序本身也要验证。 「签名校验通过」需要一个校验程序,而这个程序是另外写的,模仿 SignatureLoader.load_and_verify:读公钥、取前 128 字节当签名、用 SHA1 校验。一个永远返回「通过」的校验程序毫无意义,所以还测了三件事:内容被篡改的文件要报失败;换一把错误的公钥要报失败;仓库里官方提交的已签名示例文件,用官方公钥要报通过。最后一条说明这个校验程序和 xLua 真实的签名格式是一致的。

最后是独立审查。 开一个全新上下文的子代理,只给它 diff 和完成标准,让它找正确性问题。它用 -langversion:3(对应项目目标的 .NET 3.5 时代)又编译了一遍,按同样的场景跑过,结论是没有问题。

没验证到的也写进了 PR 说明:没有在 Windows 上用项目自带的 .csproj 编译,也没测 SignatureLoader 的 UWP 分支。

哪些没提,为什么

清单里有三条,我决定不提:

  • XL-05 目录名 04_LuaObjectOrented:改目录名会让所有指向它的外部链接失效,换来的只是一个拼写正确的目录名。
  • XL-06 类名 StatefullTest:热修复文档的示例代码里写着 CS.StatefullTest,用户会照着抄。改了类名,就得同时改四份文档,而这只是一个示例类。
  • XL-07 Pedding:它是一个只有一个 byte 字段、嵌在另一个结构体里的类型,看起来像是在测内存对齐,大概率确实是 Padding。但它同时出现在示例和单元测试里,作者原意也没法确认,改名的收益接近零。

挑错和修错是两回事。一个问题是真的,不代表修它的收益大于成本。

拆成几个 PR

一开始的计划是拆成四五个小 PR,每个只修一件事。但看了上游的情况之后改了主意:

  • 从 2025 年 2 月起,就有 PR 一直开着没人处理;2025 年 11 月之后,一个 PR 都没合过。
  • 现在开着的 12 个 PR 里,9 个的 CLA 检查还是 PENDING。但也有签了 CLA 的 PR 从 2024 年开到现在,所以 CLA 不是主要原因,维护者没时间才是。

在一个不活跃的仓库里,PR 越多,每个被看到的机会越小。最后合成了两个:

  1. #1221:签名工具的两个 bug(XL-01、XL-02),两个 commit;
  2. #1222:示例和英文 README 的三处拼写和描述问题(XL-03、XL-04、XL-08),三个 commit。

每个 commit 只修一件事,审阅的人可以一个一个看。

AI 在这件事里做了什么

这件事从头到尾都有 AI 参与,分工大概是这样:

  • ChatGPT:帮我整理最初的问题清单。清单里的大方向是对的,但 XL-01 的严重程度和修法都错了。
  • Claude Code:核对源码、查重、发现 exe 里的字符串、写修复代码和验证程序、跑测试、开 PR。
  • 我:发现最初那个拼写错误,决定查下去;在每个节点拍板:改回 key_ras 而不是统一成 key_rsa、哪几条不提、拆成几个 PR、什么时候提交和推送。

有个细节挺有意思:建 fork 和 git push 这两步,Claude Code 被它自己的安全检查拦下了,因为这两步都会对外产生影响、不容易撤回。最后是我自己在终端里执行的。我觉得这样的边界是对的:在本地怎么改都行,往外发的那一步由人来按。

回头看,AI 让「核实」这件事变得便宜了:搜索、grep、编译、跑十几个测试场景,都是几分钟的事。但它也让「看起来对」变得更便宜了:那份清单写得很像回事,要是直接照着提,第一个 PR 就会把修法弄反。

现在

两个 PR 都还开着。签名工具那个 PR 修的是一个真实的回归,有复现,有验证;拼写那个 PR 很小,零风险。会不会合、什么时候合,取决于维护者。

不管结果如何,这一趟下来我对 xLua 的签名机制、热修复的类型引用、Unity 资源的 .meta GUID 都比之前熟悉了很多。这大概是挑错最实在的收获:为了证明一个地方是错的,你得先把它真正读懂。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计