Skip to content

stack overflow in module loading on mutual `include`

Moderate
itchyny published GHSA-rmpv-jgvr-wpr9 May 11, 2026

Package

jqlang/jq

Affected versions

<= 1.8.1

Patched versions

1.8.2

Description

Summary

On jq commit 5f2a14dd1b03a8b43015058ed006dd4ab24fb58f (jq-1.8.2rc1),
the ordinary module loader recurses without cycle detection when two
otherwise valid modules include each other. On a fresh ASan/UBSan build,
jq -L witness -n 'include "a"; foo' terminates with
AddressSanitizer: stack-overflow; the reproduced impact is denial of
service by crashing the jq process through stack exhaustion.

Details

This issue is on jq's normal -L / include surface and does not require
malformed syntax or invalid module contents. The minimal witness is only a
two-file include cycle between valid modules.

The failing stack repeatedly re-enters jq's module-resolution path through
jq_realpath() in src/util.c:144, find_lib() in src/linker.c:179,
process_dependencies() in src/linker.c:273 and src/linker.c:312, and
load_library() in src/linker.c:362. That repeated path is consistent
with missing cycle detection during dependency resolution.

Expected behavior is a stable loader error that reports the include cycle
instead of recurring until stack exhaustion.

The closest related change I found is commit
3cd7e0da00f2a65e544d8f8bb8a42edf822bb5a2
(Fix crash when importing a module with errors twice (#3497)), but that
appears to address repeated imports of parse-invalid modules rather than two
valid modules that mutually include each other.

PoC

The complete reproduced environment, build steps, witness input, control
input, and fresh output are included below under the dedicated
Environment, Affected Commit, Reproduction, and Observed Result
sections so the issue can be replayed directly.

Impact

This is a denial-of-service vulnerability in jq's module-loading path. Any
workflow that runs jq against attacker-influenced programs or attacker-
influenced module search paths can be forced to crash by a valid pair of
mutually including modules.

Based on the current reproduction, I am not claiming memory corruption,
sandbox escape, or code execution; the demonstrated impact is process
termination from unbounded recursion and stack exhaustion.

Environment

  • Source revision / version: jq-1.8.2rc1 at commit
    5f2a14dd1b03a8b43015058ed006dd4ab24fb58f
  • Host OS: Arch Linux
  • Architecture: x86_64
  • Toolchain: clang 22.1.3
  • Build mode: AddressSanitizer + UndefinedBehaviorSanitizer with
    -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer
  • Configure flags: --with-oniguruma=builtin --disable-docs
  • Runtime knobs: ASAN_OPTIONS=detect_leaks=0 and
    UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1

Affected Commit

  • latest reproduced head: 5f2a14dd1b03a8b43015058ed006dd4ab24fb58f

Reproduction

One working build recipe:

git clone https://github.com/jqlang/jq.git
cd jq
git checkout 5f2a14dd1b03a8b43015058ed006dd4ab24fb58f
autoreconf -fi
CC=clang \
CFLAGS='-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer' \
LDFLAGS='-fsanitize=address,undefined' \
./configure --with-oniguruma=builtin --disable-docs
make -j"$(nproc)"

Create a directory witness/ with these two files:

# witness/a.jq
include "b";
def foo: 1;
# witness/b.jq
include "a";
def bar: 2;

Run the witness:

ASAN_OPTIONS=detect_leaks=0 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
./jq -L witness -n 'include "a"; foo'

Acyclic control: replace witness/b.jq with:

def bar: 2;

Then rerun the same command.

Observed Result

Fresh witness/control summary on commit
5f2a14dd1b03a8b43015058ed006dd4ab24fb58f:

witness_exit=1
witness_asan_stack_overflow=1
control_exit=0
control_stdout=1
control_stderr_bytes=0

Observed behavior: the mutual-include witness exits nonzero and overflows the
stack during module resolution.

Expected behavior: jq should detect the include cycle and return a stable
loader error instead of recurring until stack exhaustion.

Sanitized ASan excerpt from the witness run:

AddressSanitizer:DEADLYSIGNAL
ERROR: AddressSanitizer: stack-overflow
    #0 ... in realpath_stk
    #1 ... in realpath
    #2 ... in jq_realpath src/util.c:144:15
    #3 ... in find_lib src/linker.c:179:19
    #4 ... in process_dependencies src/linker.c:273:19
    #5 ... in load_library src/linker.c:362:18
    #6 ... in process_dependencies src/linker.c:312:20
    #7 ... in load_library src/linker.c:362:18

The acyclic control run exits successfully and prints:

1

Proposed Patch Or Fix Sketch

The most direct fix seems to be tracking which modules are currently being
resolved, keyed by the canonicalized module path already used by the loader.
If a newly requested include target is already active in the current
resolution chain, jq should emit a loader diagnostic instead of descending
again.

Pseudo-logic:

load_library(path):
  if path is already in active_resolution_stack:
    error("module include cycle: ...")
  push path
  resolve dependencies
  pop path

An explicit regression test for the two-file mutual include above, plus the
acyclic control, should cover this case.

Severity

Moderate

CVE ID

CVE-2026-44777

Weaknesses

Uncontrolled Recursion

The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack. Learn more on MITRE.

Credits