How curl was accidentally Turing-complete for nine months
Nicolas Seriot#
Computation > How curl was accidentally Turing-complete for nine months#
29th October 2026
From curl 8.12 through 8.16, config files could slice variable values and include other config files without an explicit depth cap. Together, these features let curl simulate any two-counter Minsky program. Under the usual idealization of unbounded strings and resources, this construction was Turing-complete.
Curl 8.17.0 added a five-level include cap that breaks this reduction.
Update 2026-10-30: Jonathan Tremesaygues wrote me: I couldn't help but implement “99 Bottles of Beer on the Wall” 99_bottles.tar.gz
Minsky 2-Counter Reduction#
A counter value n is represented by n copies of x.
Each instruction state uses one or more config files, with file names acting as labels, and expand-config selecting the next file.
Syntax:
{{b}}expands the current value[0-0]keeps only the first byte (xor empty)[1-]keeps everything except the first byte- empty slices yield empty strings (doc)
| Minsky | Curl config |
|---|---|
Register R |
Variable r (R is the number of x bytes) |
INC(R) |
expand-variable = "r={{r}}x" |
DEC(R) |
expand-variable = "r[1-]={{r}}" |
JZ(R, _, _x) |
expand-variable = "t[0-0]={{r}}" expand-config = "state_{{t}}.conf" |
JMP(S) |
expand-config = "S.conf" |
| Halt | No further expand-config |
With that, we can compile any two-counter program into curl config files.
PoC: Adding 2 + 3#
Four files in curl_addition.zip compute 2 + 3.

Build a local curl 8.12.0 binary if needed:
Then, from the addition/ directory, run:
/tmp/curl-tc-build/curl-8.12.0/src/curl -q -s -K main.conf
Expected output:
A=xxxxx B=
Runtime Bounds#
The reduction assumes unbounded counters and unbounded steps. The real binary has neither:
- Expanded content (curl's own limit). Each expansion is capped at
MAX_EXPAND_CONTENT= 10,000,000 bytes (src/var.c). - Step count. There is no loop: each step is a nested
--configthat returns only at halt. Every level uses a C stack frame and keeps its config file open until it finishes. In a minimal countdown loop on 8.12, an 8 MB stack allowed 922 steps before a segfault. - Retained variable versions. Reassignment adds a new entry while retaining the old one until cleanup. Memory use and some variable lookups grow with the number of steps.
History and Version Support#
- curl 8.3 (2023-09): Variables expansion
- curl 8.12 (2025-02): Byte-range assignment
- curl 8.17 (2025-11): five-level cap defeats the reduction
The pull request says the cap prevents a config file from including itself indefinitely. Its regression test does exactly that and expects an error. The public explanation does not discuss Turing completeness or why the cap is five.
A Bash while loop can complement bounded curl by invoking a config once per step and carrying the counters and instruction label between invocations. The repetition then comes from Bash, so this is a composition rather than curl-alone Turing completeness.
Security impact#
None.