@ 261102.1

260511.1 · f58787c41835d9b17795730cb04925fdba25c71c

Detect circular module imports to prevent stack overflow

jq used to recurse without bound on mutual or self-referential `import` declarations, exhausting the stack. Track each library's load state with a `loading` flag set before its dependencies are processed; a recursive reference to an in-progress library now reports "circular import of X".

Fixes CVE-2026-44777.

reading declarations

Makefile.am

@@ -232,6 +232,8 @@ EXTRA_DIST = $(DOC_FILES) $(man_MANS) $(TESTS) $(TEST_LOG_COMPILER)     \         tests/modules/test_bind_order0.jq                               \         tests/modules/test_bind_order1.jq                               \         tests/modules/test_bind_order2.jq                               \+        tests/modules/cycle_a.jq tests/modules/cycle_b.jq               \+        tests/modules/cycle_self.jq                                     \         tests/onig.supp tests/local.supp                                \         tests/setup tests/torture/input0.json                           \         tests/optional.test tests/man.test tests/manonig.test           \

src/linker.c

@@ -20,9 +20,13 @@ #include "compile.h" #include "jv_alloc.h" +struct lib_entry {+  char *name;+  block def;+  int loading;+}; struct lib_loading_state {-  char **names;-  block *defs;+  struct lib_entry *entries;   uint64_t ct; }; static int load_library(jq_state *jq, jv lib_path,@@ -303,14 +307,24 @@ static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block     } else {       uint64_t state_idx = 0;       for (; state_idx < lib_state->ct; ++state_idx) {-        if (strcmp(lib_state->names[state_idx],jv_string_value(resolved)) == 0)+        if (strcmp(lib_state->entries[state_idx].name, jv_string_value(resolved)) == 0)           break;       }        if (state_idx < lib_state->ct) { // Found+        if (lib_state->entries[state_idx].loading) {+          jq_report_error(jq, jv_string_fmt("jq: error: circular import of %s\n",+                                            jv_string_value(resolved)));+          jv_free(resolved);+          jv_free(as);+          jv_free(deps);+          jv_free(jq_origin);+          jv_free(lib_origin);+          return 1;+        }         jv_free(resolved);         // Bind the library to the program-        bk = block_bind_library(lib_state->defs[state_idx], bk, OP_IS_CALL_PSEUDO, as_str);+        bk = block_bind_library(lib_state->entries[state_idx].def, bk, OP_IS_CALL_PSEUDO, as_str);       } else { // Not found.   Add it to the table before binding.         block dep_def_block = gen_noop();         nerrors += load_library(jq, resolved, is_data, raw, optional, as_str, &dep_def_block, lib_state);@@ -352,32 +366,38 @@ static int load_library(jq_state *jq, jv lib_path, int is_data, int raw, int opt       jq_report_error(jq, jv_string_fmt("jq: error loading data file %s: %s\n", jv_string_value(lib_path), jv_string_value(data)));       nerrors++;     }-    goto out;   } else if (is_data) {     // import "foo" as $bar;     program = gen_const_global(jv_copy(data), as);+    state_idx = lib_state->ct++;+    lib_state->entries = jv_mem_realloc(lib_state->entries, lib_state->ct * sizeof(struct lib_entry));+    lib_state->entries[state_idx].name = strdup(jv_string_value(lib_path));+    lib_state->entries[state_idx].def = program;+    lib_state->entries[state_idx].loading = 0;   } else {     // import "foo" as bar;     src = locfile_init(jq, jv_string_value(lib_path), jv_string_value(data), jv_string_length_bytes(jv_copy(data)));     nerrors += jq_parse_library(src, &program);     locfile_free(src);     if (nerrors == 0) {+      // Register the library before processing its dependencies so that+      // circular imports can be detected.+      state_idx = lib_state->ct++;+      lib_state->entries = jv_mem_realloc(lib_state->entries, lib_state->ct * sizeof(struct lib_entry));+      lib_state->entries[state_idx].name = strdup(jv_string_value(lib_path));+      lib_state->entries[state_idx].def = gen_noop();+      lib_state->entries[state_idx].loading = 1;+       char *lib_origin = strdup(jv_string_value(lib_path));       nerrors += process_dependencies(jq, jq_get_jq_origin(jq),                                       jv_string(dirname(lib_origin)),                                       &program, lib_state);       free(lib_origin);       program = block_bind_self(program, OP_IS_CALL_PSEUDO);+      lib_state->entries[state_idx].def = program;+      lib_state->entries[state_idx].loading = 0;     }   }-  if (nerrors == 0) {-    state_idx = lib_state->ct++;-    lib_state->names = jv_mem_realloc(lib_state->names, lib_state->ct * sizeof(const char *));-    lib_state->defs = jv_mem_realloc(lib_state->defs, lib_state->ct * sizeof(block));-    lib_state->names[state_idx] = strdup(jv_string_value(lib_path));-    lib_state->defs[state_idx] = program;-  }-out:   *out_block = program;   jv_free(lib_path);   jv_free(data);@@ -415,7 +435,7 @@ jv load_module_meta(jq_state *jq, jv mod_relpath) { int load_program(jq_state *jq, struct locfile* src, block *out_block) {   int nerrors = 0;   block program;-  struct lib_loading_state lib_state = {0,0,0};+  struct lib_loading_state lib_state = {0,0};   nerrors = jq_parse(src, &program);   if (nerrors)     return nerrors;@@ -441,14 +461,13 @@ int load_program(jq_state *jq, struct locfile* src, block *out_block) {   nerrors = process_dependencies(jq, jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state);   block libs = gen_noop();   for (uint64_t i = 0; i < lib_state.ct; ++i) {-    free(lib_state.names[i]);-    if (nerrors == 0 && !block_is_const(lib_state.defs[i]))-      libs = block_join(libs, lib_state.defs[i]);+    free(lib_state.entries[i].name);+    if (nerrors == 0 && !block_is_const(lib_state.entries[i].def))+      libs = block_join(libs, lib_state.entries[i].def);     else-      block_free(lib_state.defs[i]);+      block_free(lib_state.entries[i].def);   }-  free(lib_state.names);-  free(lib_state.defs);+  free(lib_state.entries);   if (nerrors)     block_free(program);   else

tests/modules/cycle_a.jq

new file mode 100644@@ -0,0 +1,2 @@+import "cycle_b" as b;+def f: null;

tests/modules/cycle_b.jq

new file mode 100644@@ -0,0 +1,2 @@+import "cycle_a" as a;+def f: null;

tests/modules/cycle_self.jq

new file mode 100644@@ -0,0 +1,2 @@+import "cycle_self" as s;+def f: null;

tests/shtest

@@ -382,17 +382,40 @@ if ! HOME="$mods/home2" $VALGRIND $Q $JQ -n 'include "g"; empty'; then     exit 1 fi +( cd "$JQBASEDIR" # so that relative library paths are guaranteed correct if ! $VALGRIND $Q $JQ -L ./tests/modules -ne 'import "test_bind_order" as check; check::check==true'; then     echo "Issue #817 regression?" 1>&2     exit 1 fi+) +( cd "$JQBASEDIR" if ! $VALGRIND $Q $JQ -L tests/modules -ne 'import "test_bind_order" as check; check::check==true'; then     echo "Issue #817 regression?" 1>&2     exit 1 fi+)++# CVE-2026-44777: Circular imports should be detected+if $VALGRIND $JQ -L "$mods" -ne 'import "cycle_a" as a; null' 2> $d/out; then+    echo "Mutual import should be rejected" 1>&2+    exit 1+fi+if ! grep -q "circular import" $d/out; then+    echo "Expected circular import error" 1>&2+    exit 1+fi++if $VALGRIND $JQ -L "$mods" -ne 'import "cycle_self" as s; null' 2> $d/out; then+    echo "Self import should be rejected" 1>&2+    exit 1+fi+if ! grep -q "circular import" $d/out; then+    echo "Expected circular import error" 1>&2+    exit 1+fi  ## Halt