2025 年 2 月 5 日,curl 8.12.0 发布,配置文件里多了一种新语法:可以按字节切分变量的值。九个月后,2025 年 11 月 5 日发布的 8.17.0 悄悄加了一道五层递归上限。夹在这两个版本之间的 curl,配置文件具备图灵完备性——套上标准的理想化假设之后,它能模拟任何一台图灵机。2026 年 10 月 29 日,安全研究者 Nicolas Seriot 发文给出了完整的构造和复现步骤,curl 作者 Daniel Stenberg 随后在 Mastodon 上转发了这份分析。
两个特性,一次组合
第一块积木在 curl 8.3(2023 年 9 月)就位:变量展开。配置文件里可以定义变量,用 {{变量名}} 取值,配合 --expand-variables 还能从命令行注入。
第二块积木是 8.12 加的字节切片。[0-0] 取变量的第一个字节,[1-] 取第一个字节之后的全部,空切片返回空字符串。语法本身平淡无奇,作用是让脚本对变量值做点字符串加工。
把两块积木拼在一起,性质就变了:变量可以逐字节增长(r={{r}}x 给尾部添一个 x),也可以逐字节收缩(r[1-]={{r}} 砍掉头部一个字节);而 expand-config = "state_{{t}}.conf" 允许根据变量的值决定下一个读入哪个配置文件。变量是数据,文件名是跳转目标,组合起来恰好是「寄存器加条件跳转」,这是构成一台计算机的全部要件。
一个计数器就是一串 x
可计算性理论里早有对应物。Minsky 在 1961 年的工作证明:一台只有两个无界计数器、只支持「加一」「减一」「为零则跳转」三种操作的机器,能模拟任何图灵机——把任意多个寄存器打包进两个数的质因数分解,三条指令就够了。这类模型后来被称为 Minsky 机或计数器机,是可计算性课程里的标准内容。
Seriot 的构造把 Minsky 机原样翻译进 curl 配置:计数器 n 表示为变量里的 n 个 x,指令状态表示为配置文件,文件名充当程序标签。翻译表如下。
| Minsky 指令 | curl 配置写法 |
|---|---|
| 寄存器 R | 变量 r(值为 R 个 x) |
| INC(R) | expand-variable = "r={{r}}x" |
| DEC(R) | expand-variable = "r[1-]={{r}}" |
| JZ(R, 标签) | expand-variable = "t[0-0]={{r}}" 加 expand-config = "state_{{t}}.conf" |
| JMP(S) | expand-config = "S.conf" |
| 停机 | 不再发起 expand-config |
JZ 的翻译这样工作:t[0-0]={{r}} 把寄存器的第一个字节存进 t。寄存器非零时 t 是 x,跳进 state_x.conf;寄存器为零时 t 是空串,跳进 state_.conf。条件分支由文件名的后缀完成,curl 自己完全不知道在做「判断」。
两个计数器凭什么够用?答案是编码。把 k 个寄存器的数值打包成 2^r1 × 3^r2 × 5^r3 这样的质因数乘积,两个足够大的数就能装下任意多组计数值:读第 i 个寄存器就是数质因数 p_i 的幂次,加一是乘以 p_i,减一是除以 p_i,除尽与否恰好对应「为零则跳转」。这套技巧是可计算性教科书里的标准内容,Seriot 做的事情是把 curl 配置语法逐条映射到这套指令集上,并验证映射可以跑通。
用四个配置文件算 2+3
Seriot 附了一个最小演示:四个配置文件,程序是「把 B 的内容逐个搬到 A」,B 初始为 3 个 x,A 初始为 2 个 x,跑完 A 就是 5,也就是加法。
入口 main.conf:
url = "file:///dev/null"
variable = "a=xx"
variable = "b=xxx"
expand-config = "dec.conf"
expand-write-out = "A={{a}} B={{b}}\n"循环头 dec.conf 读 B 的首字节决定走向:
expand-variable = "t[0-0]={{b}}"
expand-config = "dec_{{t}}.conf"B 非空时进入 dec_x.conf,搬一个 x 并回到循环头;B 为空时进入空的 dec_.conf,程序自然停机。

为验证这条路径,我们从源码编译了 curl 8.12.0,在演示目录里执行:
$ curl-8.12.0/b/src/curl --config main.conf
A=xxxxx B=A 变量里躺着五个 x,加法算完,退出码 0。一份只做 HTTP 下载的工具,靠着配置语法算出了 2+3。
真实二进制的三道墙
图灵完备的证明依赖「无界字符串、无界资源」这个理想化假设,真实二进制里立着三道墙。
第一道是内容上限:src/var.c 里定义了 MAX_EXPAND_CONTENT 等于 10,000,000 字节,单次展开的内容不能超过 10MB,计数器再大也存不下。
第二道是栈深度。这套构造里没有显式循环,每一步都是一层嵌套的配置加载,每层占一个 C 栈帧,并在整个计算结束前保持文件打开。Seriot 测了 8.12 的最小倒数循环:8MB 栈在 922 步后段错误。想算点有用的东西,这个步数预算相当紧张。
第三道是内存形态:变量每次重新赋值都会保留旧版本直到清理,部分查找操作的代价随步数增长。
所以准确的说法是:这九个月里的 curl 在理论模型下图灵完备,实际运行则是一台步数和计数器都被严格封顶的计数器机。可计算性理论的标准假设本来就包含无界资源,Seriot 的构造在模型层面成立,三道墙只影响它能算多大,不影响它能算什么。
修复只提了递归,没提计算
8.17.0 带进的修复是 PR 19168,标题「tool_parsecfg: detect and error on recursive --config use」。PR 描述写的是防止配置文件无限包含自身导致问题,回归测试 test 774 验证的也确实是自包含场景。图灵完备性没有出现在公开说明里,「上限为什么是 5」同样没有解释。
我们在 8.22.0 上复测了同一份 PoC,curl 直接拒绝执行:
$ curl --config main.conf
curl: Max config file recursion level reached (5)
curl: dec_x.conf:3 config file option 'expand-config' is badly used here
curl: dec.conf:2 config file option 'expand-config' is badly used here
curl: main.conf:4 config file option 'expand-config' is badly used here递归链从 main.conf 第 4 行进 dec.conf,再进 dec_x.conf,第五层触发上限。九个月的窗口就此关闭。
安全影响方面,Seriot 在原文里明确标注为 None:配置文件通常由用户自己编写,能替你改写 curl 配置文件的攻击者,本来就能在你的 shell 里执行任意命令,不需要绕道「把 curl 变成计算机」这条复杂路径。这次分析的价值更多在工程视角:8.3 的变量展开和 8.12 的字节切片各自合入时都通过了正常评审,谁也没有指出两者组合之后配置文件拥有了无界计算能力,从 8.12 发布到完整分析发表,中间隔了 20 个月。
意外图灵完备的名单又长了一行
这类「设计目标之外获得全部计算能力」的案例有一个不短的名单:C++ 模板在 2003 年被证明图灵完备;Dolan 在 2013 年证明 x86 的 mov 指令单独就能构成图灵机;SQL 的递归 CTE 让声明式查询语言拥有了过程式计算的全部能力;Churchill、Biderman 和 Herrick 在 2019 年的论文中把任意图灵机嵌入一局万智牌,证明判定一局牌的胜负与停机问题同样困难。curl 的配置语法现在也在这份名单上。
对写配置文件的人来说,这件事留下的记录很具体:curl 8.17.0 起配置包含最多五层,依赖深层嵌套配置的脚本需要扁平化;{{变量}}、[0-0]、[1-] 三个语法都还在,单文件内的字符串加工不受影响;那份 2+3 的 PoC 会在 8.17 及以上版本报 Max config file recursion level reached (5) 后退出,在 8.12 到 8.16 上输出 A=xxxxx B=。
来源:
- How curl was accidentally Turing-complete for nine months(Nicolas Seriot,2026-10-29)
- curl PR #19168: tool_parsecfg: detect and error on recursive --config use
- curl 8.3.0 / 8.12.0 / 8.17.0 发布日期来自 GitHub Releases