{"repro_id":"REPRO-2026-00170","version":8,"title":"jq: integer overflow in jv_string_concat triggers heap buffer overflow on large strings","repro_type":"security","status":"published","severity":"high","description":"jq's `jv_string_concat` (called by the `+` operator on strings and by `add` over an array of strings) computes the size of the destination buffer using a 32-bit `int`/`uint32_t` arithmetic. When the sum of the two input string lengths exceeds `2^31` bytes, the size computation wraps around, allocating a heap buffer much smaller than what's about to be written. The subsequent `memcpy` writes past the buffer, corrupting adjacent heap data — a classic CWE-190 → CWE-122 chain.\n\nAny service that runs jq filters on attacker-controlled input (the common pattern in CI pipelines, log shippers, observability stacks, k8s admission webhooks, etc.) is exposed. The attacker only needs the ability to deliver ~2 GB of JSON string content to a jq invocation that concatenates it; on systems with sufficient memory the overflow is reachable and reproducible.","root_cause":"# RCA Report: CVE-2026-32316\n\n## Summary\n\nCVE-2026-32316 is an integer overflow vulnerability in jq's string concatenation path (`jvp_string_append` in `src/jv.c`). When two strings whose combined length exceeds `INT_MAX` (2,147,483,647 bytes) are concatenated, the allocation size `(currlen + len) * 2` overflows `uint32_t` and wraps to a tiny value. A subsequent `memcpy` then writes gigabytes of data into the undersized heap buffer, causing a heap-based buffer overflow (CWE-190 → CWE-122).\n\n## Impact\n\n- **Package/component affected**: jq (command-line JSON processor), specifically `jv_string_concat` / `jvp_string_append` in `src/jv.c`\n- **Affected versions**: `<= 1.8.1`\n- **Risk level**: High (CVSS 3.1: 7.5)\n- **Consequences**: Any service or pipeline that runs jq filters on attacker-controlled input can be crashed or have heap memory corrupted when the attacker supplies JSON strings that, when concatenated, exceed `INT_MAX` bytes. This is reachable in common patterns such as `add` over an array of large strings or direct `+` operations.\n\n## Root Cause\n\nIn `src/jv.c`, `jvp_string_append` computes the new allocation size using 32-bit unsigned arithmetic:\n\n```c\nuint32_t allocsz = (currlen + len) * 2;\n```\n\nWhen `currlen + len >= INT_MAX` (≈ 2.1 GB), the product can exceed `UINT32_MAX` and wrap around to a very small number (as small as 0, which is then clamped to 32 bytes). The function then allocates this tiny buffer via `jvp_string_alloc(allocsz)` and copies the full concatenated length with `memcpy`, writing far past the allocated region.\n\nThe same overflow pattern existed in `jvp_string_copy_replace_bad`, where `length * 3 + 1` could wrap `uint32_t` for very large inputs.\n\n**Fix commit**: `e47e56d226519635768e6aab2f38f0ab037c09e5` — adds explicit 64-bit overflow checks:\n\n```c\nif ((uint64_t)currlen + len >= INT_MAX) {\n    jv_free(string);\n    return jv_invalid_with_msg(jv_string(\"String too long\"));\n}\n```\n\n## Reproduction Steps\n\n1. Execute `repro/reproduction_steps.sh`\n2. The script clones the jq repository, builds two binaries with AddressSanitizer:\n   - **Vulnerable**: tag `jq-1.8.1`\n   - **Fixed**: commit `e47e56d226519635768e6aab2f38f0ab037c09e5`\n3. The script runs the trigger program on both binaries:\n   ```jq\n   \"A\" * 1073741824 as $a | $a + $a\n   ```\n   This creates a 1 GB string and concatenates it with another 1 GB string, producing a total length of 2 GB (> `INT_MAX`).\n4. **Expected evidence**:\n   - Vulnerable build: AddressSanitizer reports `heap-buffer-overflow` in `jvp_string_append` (called from `jv_string_concat` → `binop_plus`)\n   - Fixed build: jq exits gracefully with the error message `String too long`\n\n## Evidence\n\n- **Vulnerable ASAN output**: `logs/vulnerable_stderr.txt`\n  ```\n  ==7265==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x506000001311\n  WRITE of size 1073741824 at 0x506000001311 thread T0\n      #0 memcpy\n      #1 jvp_string_append src/jv.c:1191\n      #2 jv_string_concat src/jv.c:1501\n      #3 binop_plus src/builtin.c:96\n  ```\n  The buffer allocated was only 49 bytes, confirming the integer-overflow-induced undersized allocation.\n\n- **Fixed output**: `logs/fixed_stderr.txt`\n  ```\n  jq: error (at <unknown>): String too long\n  ```\n\n- **Runtime manifest**: `repro/runtime_manifest.json`\n\n## Recommendations / Next Steps\n\n1. **Upgrade jq to 1.8.2 or later** (or any build containing commit `e47e56d`). The fix is minimal and precisely targeted at the overflow conditions.\n2. **Input size limits**: For deployments that cannot immediately upgrade, enforce maximum JSON input sizes well below 2 GB to reduce the likelihood of triggering the overflow.\n3. **Regression testing**: Add automated tests that attempt string concatenation near `INT_MAX` boundaries to prevent re-introduction of this bug class.\n4. **Audit other integer size calculations**: Review other uses of `uint32_t` for buffer-size math in `src/jv.c` (e.g., `jvp_string_copy_replace_bad`) to ensure similar overflows are not present elsewhere.\n\n## Additional Notes\n\n- **Idempotency confirmed**: `repro/reproduction_steps.sh` was run twice consecutively; both runs produced identical ASAN crash output on the vulnerable build and identical graceful error output on the fixed build.\n- **Edge cases / limitations**: The reproduction requires a host with enough RAM to hold two ~1 GB strings plus ASAN overhead (≈ 3–4 GB peak). On memory-constrained systems, the trigger can be adjusted by reducing the multiplier, but the combined concatenated length must still exceed `INT_MAX` (2,147,483,647 bytes) to hit the vulnerable code path.\n","ghsa_id":"GHSA-q3h9-m34w-h76f","cve_id":"CVE-2026-32316","package":{"name":"jq","ecosystem":"github","affected_versions":"<= 1.8.1","fixed_version":"1.8.2"},"reproduced_at":"2026-05-28T10:56:03.502111+00:00","duration_secs":1953.52201628685,"tool_calls":182,"turns":163,"handoffs":2,"total_cost_usd":1.555516879999999,"agent_costs":{"repro":0.5434218900000001,"support":0.01614894,"vuln_variant":0.99594605},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":0.5434218900000001},"support":{"accounts/fireworks/models/kimi-k2p6":0.01614894},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":0.99594605}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-28T10:56:05.975989+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":4605,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":3766,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":6165,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":3024,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":2065,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":783,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":4280,"category":"ticket"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":762,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1686,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6013,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2838,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3125,"category":"other"},{"path":"bundle/logs/variant2_vuln.txt","filename":"variant2_vuln.txt","size":0,"category":"other"},{"path":"bundle/logs/fixed_stderr.txt","filename":"fixed_stderr.txt","size":42,"category":"other"},{"path":"bundle/logs/variant1_vuln.txt","filename":"variant1_vuln.txt","size":3728,"category":"other"},{"path":"bundle/logs/variant3_fixed.txt","filename":"variant3_fixed.txt","size":42,"category":"other"},{"path":"bundle/logs/variant5_fixed.txt","filename":"variant5_fixed.txt","size":42,"category":"other"},{"path":"bundle/logs/variant3_vuln.txt","filename":"variant3_vuln.txt","size":3321,"category":"other"},{"path":"bundle/logs/variant5_vuln.txt","filename":"variant5_vuln.txt","size":3728,"category":"other"},{"path":"bundle/logs/variant4_fixed.txt","filename":"variant4_fixed.txt","size":42,"category":"other"},{"path":"bundle/logs/variant2_fixed.txt","filename":"variant2_fixed.txt","size":42,"category":"other"},{"path":"bundle/logs/variant_reproduction_run.txt","filename":"variant_reproduction_run.txt","size":583,"category":"other"},{"path":"bundle/logs/vulnerable_stdout.txt","filename":"vulnerable_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/variant4_vuln.txt","filename":"variant4_vuln.txt","size":3728,"category":"other"},{"path":"bundle/logs/vulnerable_stderr.txt","filename":"vulnerable_stderr.txt","size":3728,"category":"other"},{"path":"bundle/logs/fixed_stdout.txt","filename":"fixed_stdout.txt","size":0,"category":"other"},{"path":"bundle/logs/variant1_fixed.txt","filename":"variant1_fixed.txt","size":42,"category":"other"}]}