{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/lobste-rs/2026-10-30-how-curl-was-accidentally-turing-complete-for-nine-months/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "### Nicolas Seriot\n\n#### [Computation](https://seriot.ch/computation/index.html) > How curl was accidentally Turing-complete for nine months\n\n*29th October 2026*\n\nFrom 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.\n\nCurl 8.17.0 added a five-level include cap that breaks this reduction.\n\n**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](https://seriot.ch/resources/99_bottles.tar.gz)\n\n### Minsky 2-Counter Reduction\n\nA counter value `n` is represented by `n` copies of `x`.\n\nEach instruction state uses one or more config files, with file names acting as labels, and `expand-config` selecting the next file.\n\nSyntax:\n\n- `{{b}}` expands the current value\n- `[0-0]` keeps only the first byte (`x` or empty)\n- `[1-]` keeps everything except the first byte\n- empty slices yield empty strings ([doc](https://github.com/curl/curl/blob/curl-8_12_0/docs/cmdline-opts/variable.md))\n\n| Minsky         | Curl config                                                             |\n| -------------- | ----------------------------------------------------------------------- |\n| Register `R`   | Variable `r` (`R` is the number of `x` bytes)                           |\n| `INC(R)`       | `expand-variable = \"r={{r}}x\"`                                          |\n| `DEC(R)`       | `expand-variable = \"r[1-]={{r}}\"`                                       |\n| `JZ(R, _, _x)` | `expand-variable = \"t[0-0]={{r}}\"` `expand-config = \"state_{{t}}.conf\"` |\n| `JMP(S)`       | `expand-config = \"S.conf\"`                                              |\n| Halt           | No further `expand-config`                                              |\n\nWith that, we can compile any two-counter program into curl config files.\n\n### PoC: Adding 2 + 3\n\nFour files in [curl\\_addition.zip](https://seriot.ch/resources/curl_addition.zip) compute 2 + 3.\n\n![](https://seriot.ch/resources/curl_config.png)\n\nBuild a local curl 8.12.0 binary if needed:\n\n```\nmkdir -p /tmp/curl-tc-build\ncd /tmp/curl-tc-build\ncurl -fsSLO https://curl.se/download/curl-8.12.0.tar.gz\ntar -xzf curl-8.12.0.tar.gz\ncd curl-8.12.0\n./configure --without-ssl --disable-shared --disable-manual --disable-docs\nmake -j4\n```\n\nThen, from the `addition/` directory, run:\n\n```\n/tmp/curl-tc-build/curl-8.12.0/src/curl -q -s -K main.conf\n```\n\nExpected output:\n\n```\nA=xxxxx B=\n```\n\n### Runtime Bounds\n\nThe reduction assumes unbounded counters and unbounded steps. The real binary has neither:\n\n- **Expanded content (curl's own limit).** Each expansion is capped at `MAX_EXPAND_CONTENT` = 10,000,000 bytes ([`src/var.c`](https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L42)).\n- **Step count.** There is no loop: each step is a nested `--config` that 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.\n- **Retained variable versions.** Reassignment [adds a new entry](https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L347-L370) while retaining the old one until [cleanup](https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L58-L78). Memory use and some variable lookups grow with the number of steps.\n\n### History and Version Support\n\n- curl 8.3 (2023-09): Variables expansion\n- curl 8.12 (2025-02): Byte-range assignment\n- curl 8.17 (2025-11): [five-level cap](https://github.com/curl/curl/blob/curl-8_17_0/src/tool_parsecfg.h#L28-L30) defeats the reduction\n\nThe [pull request](https://github.com/curl/curl/pull/19168) says the cap prevents a config file from including itself indefinitely. Its [regression test](https://github.com/curl/curl/blob/curl-8_17_0/tests/data/test774) does exactly that and expects an error. The public explanation does not discuss Turing completeness or why the cap is five.\n\nA 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.\n\n### Security impact\n\nNone.\n",
  "authors": [
    "seriot.ch via kytta"
  ],
  "captured_at": "2026-10-30T22:55:36.634700404+00:00",
  "category": "news",
  "content_html": "<h3>Nicolas Seriot</h3>\n<h4><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://seriot.ch/computation/index.html\">Computation</a> &gt; How curl was accidentally Turing-complete for nine months</h4>\n<p><em>29th October 2026</em></p>\n<p>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.</p>\n<p>Curl 8.17.0 added a five-level include cap that breaks this reduction.</p>\n<p><strong>Update 2026-10-30</strong>: Jonathan Tremesaygues wrote me: <em>I couldn't help but implement “99 Bottles of Beer on the Wall”</em> <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://seriot.ch/resources/99_bottles.tar.gz\">99_bottles.tar.gz</a></p>\n<h3>Minsky 2-Counter Reduction</h3>\n<p>A counter value <code>n</code> is represented by <code>n</code> copies of <code>x</code>.</p>\n<p>Each instruction state uses one or more config files, with file names acting as labels, and <code>expand-config</code> selecting the next file.</p>\n<p>Syntax:</p>\n<ul>\n<li><code>{{b}}</code> expands the current value</li>\n<li><code>[0-0]</code> keeps only the first byte (<code>x</code> or empty)</li>\n<li><code>[1-]</code> keeps everything except the first byte</li>\n<li>empty slices yield empty strings (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/blob/curl-8_12_0/docs/cmdline-opts/variable.md\">doc</a>)</li>\n</ul>\n<table>\n<thead>\n<tr>\n<th>Minsky</th>\n<th>Curl config</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Register <code>R</code></td>\n<td>Variable <code>r</code> (<code>R</code> is the number of <code>x</code> bytes)</td>\n</tr>\n<tr>\n<td><code>INC(R)</code></td>\n<td><code>expand-variable = &quot;r={{r}}x&quot;</code></td>\n</tr>\n<tr>\n<td><code>DEC(R)</code></td>\n<td><code>expand-variable = &quot;r[1-]={{r}}&quot;</code></td>\n</tr>\n<tr>\n<td><code>JZ(R, _, _x)</code></td>\n<td><code>expand-variable = &quot;t[0-0]={{r}}&quot;</code> <code>expand-config = &quot;state_{{t}}.conf&quot;</code></td>\n</tr>\n<tr>\n<td><code>JMP(S)</code></td>\n<td><code>expand-config = &quot;S.conf&quot;</code></td>\n</tr>\n<tr>\n<td>Halt</td>\n<td>No further <code>expand-config</code></td>\n</tr>\n</tbody>\n</table>\n<p>With that, we can compile any two-counter program into curl config files.</p>\n<h3>PoC: Adding 2 + 3</h3>\n<p>Four files in <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://seriot.ch/resources/curl_addition.zip\">curl_addition.zip</a> compute 2 + 3.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://seriot.ch/resources/curl_config.png\" alt=\"\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Build a local curl 8.12.0 binary if needed:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">mkdir</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span> /tmp/curl-tc-build</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-cd syntax-shell\">cd</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> /tmp/curl-tc-build</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">curl</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>fsSLO</span> https://curl.se/download/curl-8.12.0.tar.gz</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">tar</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>xzf</span> curl-8.12.0.tar.gz</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-cd syntax-shell\">cd</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> curl-8.12.0</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">./configure</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>without-ssl</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>disable-shared</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>disable-manual</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>disable-docs</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">make</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>j4</span></span>\n</span></span></code></pre>\n<p>Then, from the <code>addition/</code> directory, run:</p>\n<pre><code><span class=\"code-snippet\">/tmp/curl-tc-build/curl-8.12.0/src/curl -q -s -K main.conf\n</span></code></pre>\n<p>Expected output:</p>\n<pre><code><span class=\"code-snippet\">A=xxxxx B=\n</span></code></pre>\n<h3>Runtime Bounds</h3>\n<p>The reduction assumes unbounded counters and unbounded steps. The real binary has neither:</p>\n<ul>\n<li><strong>Expanded content (curl's own limit).</strong> Each expansion is capped at <code>MAX_EXPAND_CONTENT</code> = 10,000,000 bytes (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L42\"><code>src/var.c</code></a>).</li>\n<li><strong>Step count.</strong> There is no loop: each step is a nested <code>--config</code> that 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.</li>\n<li><strong>Retained variable versions.</strong> Reassignment <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L347-L370\">adds a new entry</a> while retaining the old one until <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L58-L78\">cleanup</a>. Memory use and some variable lookups grow with the number of steps.</li>\n</ul>\n<h3>History and Version Support</h3>\n<ul>\n<li>curl 8.3 (2023-09): Variables expansion</li>\n<li>curl 8.12 (2025-02): Byte-range assignment</li>\n<li>curl 8.17 (2025-11): <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/blob/curl-8_17_0/src/tool_parsecfg.h#L28-L30\">five-level cap</a> defeats the reduction</li>\n</ul>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/pull/19168\">pull request</a> says the cap prevents a config file from including itself indefinitely. Its <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/curl/curl/blob/curl-8_17_0/tests/data/test774\">regression test</a> does exactly that and expects an error. The public explanation does not discuss Turing completeness or why the cap is five.</p>\n<p>A Bash <code>while</code> 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.</p>\n<h3>Security impact</h3>\n<p>None.</p>\n",
  "content_markdown": "### Nicolas Seriot\n\n#### [Computation](https://seriot.ch/computation/index.html) > How curl was accidentally Turing-complete for nine months\n\n*29th October 2026*\n\nFrom 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.\n\nCurl 8.17.0 added a five-level include cap that breaks this reduction.\n\n**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](https://seriot.ch/resources/99_bottles.tar.gz)\n\n### Minsky 2-Counter Reduction\n\nA counter value `n` is represented by `n` copies of `x`.\n\nEach instruction state uses one or more config files, with file names acting as labels, and `expand-config` selecting the next file.\n\nSyntax:\n\n- `{{b}}` expands the current value\n- `[0-0]` keeps only the first byte (`x` or empty)\n- `[1-]` keeps everything except the first byte\n- empty slices yield empty strings ([doc](https://github.com/curl/curl/blob/curl-8_12_0/docs/cmdline-opts/variable.md))\n\n| Minsky         | Curl config                                                             |\n| -------------- | ----------------------------------------------------------------------- |\n| Register `R`   | Variable `r` (`R` is the number of `x` bytes)                           |\n| `INC(R)`       | `expand-variable = \"r={{r}}x\"`                                          |\n| `DEC(R)`       | `expand-variable = \"r[1-]={{r}}\"`                                       |\n| `JZ(R, _, _x)` | `expand-variable = \"t[0-0]={{r}}\"` `expand-config = \"state_{{t}}.conf\"` |\n| `JMP(S)`       | `expand-config = \"S.conf\"`                                              |\n| Halt           | No further `expand-config`                                              |\n\nWith that, we can compile any two-counter program into curl config files.\n\n### PoC: Adding 2 + 3\n\nFour files in [curl\\_addition.zip](https://seriot.ch/resources/curl_addition.zip) compute 2 + 3.\n\n![](https://seriot.ch/resources/curl_config.png)\n\nBuild a local curl 8.12.0 binary if needed:\n\n```\nmkdir -p /tmp/curl-tc-build\ncd /tmp/curl-tc-build\ncurl -fsSLO https://curl.se/download/curl-8.12.0.tar.gz\ntar -xzf curl-8.12.0.tar.gz\ncd curl-8.12.0\n./configure --without-ssl --disable-shared --disable-manual --disable-docs\nmake -j4\n```\n\nThen, from the `addition/` directory, run:\n\n```\n/tmp/curl-tc-build/curl-8.12.0/src/curl -q -s -K main.conf\n```\n\nExpected output:\n\n```\nA=xxxxx B=\n```\n\n### Runtime Bounds\n\nThe reduction assumes unbounded counters and unbounded steps. The real binary has neither:\n\n- **Expanded content (curl's own limit).** Each expansion is capped at `MAX_EXPAND_CONTENT` = 10,000,000 bytes ([`src/var.c`](https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L42)).\n- **Step count.** There is no loop: each step is a nested `--config` that 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.\n- **Retained variable versions.** Reassignment [adds a new entry](https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L347-L370) while retaining the old one until [cleanup](https://github.com/curl/curl/blob/curl-8_12_0/src/var.c#L58-L78). Memory use and some variable lookups grow with the number of steps.\n\n### History and Version Support\n\n- curl 8.3 (2023-09): Variables expansion\n- curl 8.12 (2025-02): Byte-range assignment\n- curl 8.17 (2025-11): [five-level cap](https://github.com/curl/curl/blob/curl-8_17_0/src/tool_parsecfg.h#L28-L30) defeats the reduction\n\nThe [pull request](https://github.com/curl/curl/pull/19168) says the cap prevents a config file from including itself indefinitely. Its [regression test](https://github.com/curl/curl/blob/curl-8_17_0/tests/data/test774) does exactly that and expects an error. The public explanation does not discuss Turing completeness or why the cap is five.\n\nA 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.\n\n### Security impact\n\nNone.\n",
  "dateCreated": "2026-10-30T22:55:36.634700404+00:00",
  "date_created": "2026-10-30T07:24:01+00:00",
  "date_modified": "2026-10-30T07:24:01+00:00",
  "date_published": "2026-10-30T07:24:01+00:00",
  "description": "Comments",
  "external_url": "https://seriot.ch/computation/curl.html",
  "git": {
    "commit": "20068632b65d2830a2d54f25ca1d90d697bea2c0",
    "path": "items/lobste-rs/2026/10/2026-10-30-how-curl-was-accidentally-turing-complete-for-nine-months.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/20068632b65d2830a2d54f25ca1d90d697bea2c0/items/lobste-rs/2026/10/2026-10-30-how-curl-was-accidentally-turing-complete-for-nine-months.md"
  },
  "id": "urn:aggr:item:9731107b0848ba5ad88591a5167bdc86a8f11a55",
  "isBasedOn": {
    "@id": "https://seriot.ch/computation/curl.html"
  },
  "mainEntity": {
    "@id": "https://seriot.ch/computation/curl.html",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/lobste-rs/2026-10-30-how-curl-was-accidentally-turing-complete-for-nine-months/#webpage"
    },
    "author": [
      {
        "@type": "Person",
        "name": "seriot.ch via kytta"
      }
    ],
    "dateModified": "2026-10-30T07:24:01+00:00",
    "datePublished": "2026-10-30T07:24:01+00:00",
    "headline": "How curl was accidentally Turing-complete for nine months",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://seriot.ch/resources/curl_config.png",
      "width": 160
    },
    "timeRequired": "PT3M",
    "url": "https://seriot.ch/computation/curl.html",
    "wordCount": 521
  },
  "name": "Archived snapshot: How curl was accidentally Turing-complete for nine months",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://seriot.ch/resources/curl_config.png",
    "width": 160
  },
  "reading_minutes": 3,
  "replicated_at": null,
  "source": "lobste-rs",
  "source_name": "Lobsters: Top Stories of the Past Week",
  "summary": "Comments",
  "tags": [
    "compsci"
  ],
  "temporalCoverage": "2026-10-30T22:55:36.634700404+00:00",
  "title": "How curl was accidentally Turing-complete for nine months",
  "url": "https://aggr.aymericbeaumet.com/items/lobste-rs/2026-10-30-how-curl-was-accidentally-turing-complete-for-nine-months/",
  "word_count": 521
}