GHSA-2x79-gwq3-vxxm: Uncontrolled resource consumption and loop with unreachable exit condition in facil.io and downstream iodine ruby gem
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.DetailsThe 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:Inside an array or object, the parser sees `i` or `I` and jumps to the `numeral:` label.It calls `fio_atol((char **)&tmp)`.For a bare `i` / `I`, `fio_atol` consumes zero characters and leaves `tmp == pos`.The current code only falls back to float parsing when `JSON_NUMERAL[*tmp]` is true.`JSON_NUMERAL['i'] == 0`, so the parser incorrectly accepts the value as an integer and sets `pos = tmp` without advancing.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:The parser accepts the empty string `""` as the first array element.It does not require a comma before the next token.The trailing `i` is then parsed as a new nested value.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`PoCEnvironment used for verification:`facil.io` commit: `162df84001d66789efa883eebb0567426d00148e``iodine` commit: `5bebba698d69023cf47829afe51052f8caa6c7f8`standalone compile against `fio_json_parser.h`Minimal standalone programUse 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`.`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`:Run:Then in another terminal send one of these payloads: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:Run:Then in a second terminal: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.ImpactThis 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 pointsConfidentiality impact: none observedIntegrity impact: none observedSuggested PatchTreat zero-consumption numeric parses as failures before accepting the token.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 gemVerified 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`
CVSS v4.0: 8.7