The Exploit
Attacker needs only the ability to supply a jq expression to a vulnerable process that evaluates untrusted jq queries.
jq -n '("a"*1073741824) + ("b"*1073741825) | length'
This command builds two strings whose combined length exceeds 2^31 bytes, then asks jq for the resulting length. On vulnerable jq versions through 1.8.1 this will crash the process with heap corruption instead of returning 2147483649.
The attacker observes the jq process aborting or segfaulting under a malformed query. In a service, that means denial of service or a corrupted heap state that could be weaponized into more serious exploitation.
What the Patch Did
Before:
uint32_t maxlength = length * 3 + 1; // worst case: all bad bytes, each becomes a 3-byte U+FFFD
jvp_string* s = jvp_string_alloc(maxlength);
After:
uint64_t maxlength = (uint64_t)length * 3 + 1;
if (maxlength >= INT_MAX) {
return jv_invalid_with_msg(jv_string("String too long"));
}
jvp_string* s = jvp_string_alloc(maxlength);
Before:
if (jvp_refcnt_unshared(string.u.ptr) &&
jvp_string_remaining_space(s) >= len) {
After:
if ((uint64_t)currlen + len >= INT_MAX) {
jv_free(string);
return jv_invalid_with_msg(jv_string("String too long"));
}
if (jvp_refcnt_unshared(string.u.ptr) &&
jvp_string_remaining_space(s) >= len) {
The patch adds explicit integer bounds checks before string buffer allocation and before string append growth. It enforces a maximum string size for jq string operations and converts arithmetic into 64-bit before checking, preventing 32-bit overflow on large lengths.
Root Cause
This is an integer overflow leading to a heap buffer overflow: CWE-190 triggering CWE-122. Attack-controlled jq expressions can produce very large strings through concatenation or replacement operations. Those string lengths flow into jvp_string_copy_replace_bad() and jvp_string_append() without limits. The code computes allocation sizes using 32-bit arithmetic, so when the total string length exceeds INT_MAX, the allocation size wraps around and allocates an undersized heap buffer. The attacker-controlled string then gets copied into that buffer, crossing the trust boundary from untrusted jq input to internal string memory management unchecked.
Why It Works
The single load-bearing line is:
if ((uint64_t)currlen + len >= INT_MAX) {
That is the gatekeeper for the append path. Without it, currlen + len can overflow in 32-bit arithmetic and the string append code will try to fit a giant combined string into a too-small buffer. The other added check in jvp_string_copy_replace_bad() is also important: it stops overflows in the sanitization path where length * 3 + 1 can exceed 32 bits for badly encoded content. The cast to uint64_t makes the arithmetic safe, and the jv_invalid_with_msg(...) path turns an oversized request into a controlled error instead of a buffer overflow.
Hardening Checklist
- Add explicit upper bounds before every dynamic
malloc/reallocbased on user-controlled sizes; compare againstINT_MAXorSIZE_MAX. - Use wide integer arithmetic (
uint64_t) before overflow-prone additions or multiplications. - Reject unbounded string growth in string operations the same way arrays and objects are already bounded.
- Prefer a safe failure path like
jv_invalid_with_msg(jv_string("String too long"))instead of letting unchecked allocation proceed. - Fuzz string operators with huge repeat/concatenate inputs to catch integer overflow edge cases.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-32316