Apr 15, 2026·6 min read·3 visits
A flaw in the JSON parser of facil.io and iodine causes an infinite loop when parsing malformed numerals starting with 'i' or 'I'. This allows unauthenticated remote attackers to exhaust CPU resources and cause a severe denial of service.
An uncontrolled resource consumption vulnerability in the facil.io C framework and the iodine Ruby gem allows remote attackers to cause a Denial of Service (DoS). The vulnerability is triggered by parsing crafted JSON payloads containing malformed numeral values, resulting in an infinite loop that exhausts CPU resources.
The vulnerability is located within the fio_json_parse function defined in the fio_json_parser.h header file. This parser functions as the core JSON processing unit for the facil.io C framework, which is designed for high-performance networking applications. The iodine Ruby gem heavily relies on this vendored parser for handling JSON operations in HTTP and WebSocket contexts.
The flaw represents a classic CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop'). When the JSON parser encounters specific malformed inputs, the internal state machine fails to advance the read cursor. The parser continuously attempts to evaluate the same byte sequence without making forward progress or transitioning to an error state.
Attackers exploit this behavior by submitting crafted JSON payloads containing nested structures combined with bare characters like i or I. Because the framework operates as an asynchronous event loop, triggering this infinite loop inside the single-threaded parsing process locks the executing core, rendering the process incapable of handling further requests.
The root cause originates in the numeral parsing logic implemented between lines 434 and 468 of lib/facil/fiobj/fio_json_parser.h. The parser utilizes an internal state machine to process JSON tokens sequentially. When the parser operates inside an array or object context and identifies a character starting a new value, it checks whether the sequence represents a numerical input.
Upon encountering the characters i or I—often used in systems to denote inf or Infinity—the parser state machine jumps to the numeral: label. At this stage, the parser invokes the fio_atol function, passing a double pointer (char **)&tmp to attempt standard integer conversion. The fio_atol function relies on standard numerical characters to consume the buffer.
When fio_atol processes a bare i character, it successfully consumes zero characters and returns immediately. Crucially, the pointer tmp remains equal to the original position pointer pos. Following this operation, the parser evaluates the condition if (!tmp || JSON_NUMERAL[*tmp]). The lookup table JSON_NUMERAL['i'] evaluates to false (0), bypassing the failure detection branch.
Because the failure branch is bypassed, the parser erroneously concludes that a valid numeric sequence was processed. It executes pos = tmp, updating the main parser position to the exact same memory location it started at. Since the parsing depth remains greater than zero and the end of the buffer has not been reached, the outer while loop restarts, immediately encountering the same i character and repeating the cycle infinitely.
Analyzing the source code reveals the structural oversight in pointer validation. The vulnerable logic implicitly trusts that the underlying conversion functions (fio_atol and fio_atof) will either advance the memory pointer upon success or that the subsequent character lookup (JSON_NUMERAL) will fail decisively upon error.
The patch addresses this gap by explicitly verifying whether the pointer has moved. The developer introduced a direct comparison between the temporary pointer tmp and the position pointer pos.
--- 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;By injecting the tmp == pos check, the condition enforces forward progress. If fio_atol or fio_atof consumes zero characters, tmp will equal pos. The logical OR condition will immediately trigger, causing the routine to recognize the invalid input and correctly jump to the error label, terminating the loop and gracefully failing the parse operation.
Exploitation requires no authentication, specific configurations, or complex interaction chains. An attacker merely needs network access to an endpoint that parses JSON payloads using facil.io or iodine. This typically involves sending an HTTP POST request with the Content-Type: application/json header and a specially crafted body.
The minimal payload required to trigger the vulnerability is simply [i. This string initiates an array and immediately introduces the malformed numeral character. Alternative payloads such as {"a":i or [""i achieve the same outcome by tricking the parser into evaluating the i character as a new value context.
The following Ruby code demonstrates a minimal proof-of-concept for an iodine server. When the Iodine::JSON.parse method is invoked on the request body containing [i, the worker thread immediately locks.
require "iodine"
APP = proc do |env|
body = env["rack.input"].read.to_s
warn "Parsing JSON: #{body.inspect}"
Iodine::JSON.parse(body) # Triggers infinite loop on '[i'
[200, { "Content-Type" => "text/plain" }, ["ok"]]
end
Iodine.listen service: :http, port: "3000", handler: APP
Iodine.threads = 1
Iodine.startTo execute the exploit against the vulnerable server, an attacker utilizes standard HTTP clients. The command printf '[i' | curl -X POST --data-binary @- http://127.0.0.1:3000/ dispatches the payload without URL encoding or trailing line breaks, guaranteeing the parser processes the exact bytes required to stall the state machine.
The operational impact of this vulnerability is a complete localized Denial of Service. When the infinite loop initiates, the underlying thread consumes 100% of the assigned CPU core's cycles. It will never timeout natively because the thread is actively processing code rather than blocking on I/O.
In architectures like facil.io and iodine which utilize asynchronous event loops, a blocked worker thread ceases processing all other queued connections assigned to that thread. If the application runs a single-threaded event loop, a single malformed request brings down the entire application.
In multi-threaded deployments, an attacker can achieve a total service outage by sending concurrent requests equal to the number of configured worker threads. Once all threads are trapped in the infinite loop parsing [i, the application becomes entirely unresponsive to legitimate traffic. The system administrator must forcefully terminate and restart the process to restore functionality.
The primary remediation strategy requires updating the vulnerable packages to their patched versions. Applications relying on the iodine Ruby gem must upgrade to version greater than 0.7.58. Applications utilizing the facil.io C framework directly must integrate the latest commits from the master branch, specifically ensuring the fio_json_parser.h changes are applied.
For environments where immediate patching is impossible, network-level mitigations can provide temporary defense. Web Application Firewalls (WAFs) can be configured to inspect incoming JSON bodies for the specific byte sequences used in the exploit. Regular expressions targeting \[\s*i or :\s*i can intercept the most common variants of the payload before they reach the application layer.
Developers should verify their deployment architectures include adequate resource monitoring and process supervision. Configuring orchestration tools to health-check the application and restart uncooperative containers can reduce the total downtime during an active attack, though it does not eliminate the underlying vulnerability.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
facil.io boazsegev | 0.7.5, 0.7.6 | - |
iodine boazsegev | <= 0.7.58 | > 0.7.58 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | GHSA-2x79-gwq3-vxxm |
| CWE | CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop') |
| Attack Vector | Network |
| CVSS v4.0 Score | 8.7 (High) |
| Exploit Status | Proof of Concept Available |
| Impact | Denial of Service (CPU Exhaustion) |
The program contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.