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:
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:
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.
Summary
On jq commit
5f2a14dd1b03a8b43015058ed006dd4ab24fb58f(jq-1.8.2rc1),the ordinary module loader recurses without cycle detection when two
otherwise valid modules
includeeach other. On a fresh ASan/UBSan build,jq -L witness -n 'include "a"; foo'terminates withAddressSanitizer: stack-overflow; the reproduced impact is denial ofservice by crashing the jq process through stack exhaustion.
Details
This issue is on jq's normal
-L/includesurface and does not requiremalformed 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()insrc/util.c:144,find_lib()insrc/linker.c:179,process_dependencies()insrc/linker.c:273andsrc/linker.c:312, andload_library()insrc/linker.c:362. That repeated path is consistentwith 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 thatappears 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, andObserved Resultsections 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
jq-1.8.2rc1at commit5f2a14dd1b03a8b43015058ed006dd4ab24fb58fx86_64clang 22.1.3-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer--with-oniguruma=builtin --disable-docsASAN_OPTIONS=detect_leaks=0andUBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1Affected Commit
5f2a14dd1b03a8b43015058ed006dd4ab24fb58fReproduction
One working build recipe:
Create a directory
witness/with these two files: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.jqwith:Then rerun the same command.
Observed Result
Fresh witness/control summary on commit
5f2a14dd1b03a8b43015058ed006dd4ab24fb58f: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:
The acyclic control run exits successfully and prints:
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
includetarget is already active in the currentresolution chain, jq should emit a loader diagnostic instead of descending
again.
Pseudo-logic:
An explicit regression test for the two-file mutual include above, plus the
acyclic control, should cover this case.