new file mode 100644
@@ -0,0 +1,484 @@
+From 63ae676bc6e97635d8246c78f5947ec47eb85a26 Mon Sep 17 00:00:00 2001
+From: Yu-Fu Fu <yufu@yfu.tw>
+Date: Fri, 22 May 2026 04:07:16 -0700
+Subject: [PATCH] Guard deep structural equality and comparison recursion
+ (#3539)
+
+jv_equal and jv_cmp overflows the C stack on deeply nested
+input. Cap recursion at 10000 with -1 / INT_MIN sentinels;
+operators that compose user expressions surface this as
+"Equality check too deep" / "Comparison too deep".
+
+Fixes CVE-2026-47770.
+
+Signed-off-by: Anton Skorup <antonsk@axis.com>
+Upstream-Status: Backport [https://github.com/jqlang/jq/commit/7122866869960b55cea3646bc91334ef55787831]
+---
+ src/builtin.c | 36 +++++++++++++++--
+ src/jv.c | 46 +++++++++++++++++-----
+ src/jv_aux.c | 105 ++++++++++++++++++++++++++++++++++++++++++--------
+ tests/jq.test | 40 +++++++++++++++++++
+ 4 files changed, 198 insertions(+), 29 deletions(-)
+
+diff --git a/src/builtin.c b/src/builtin.c
+index ac56f9f..ac25f90 100644
+--- a/src/builtin.c
++++ b/src/builtin.c
+@@ -295,7 +295,15 @@ jv binop_minus(jv a, jv b) {
+ jv_array_foreach(a, i, x) {
+ int include = 1;
+ jv_array_foreach(b, j, y) {
+- if (jv_equal(jv_copy(x), y)) {
++ int equal = jv_equal(jv_copy(x), y);
++ if (equal < 0) {
++ jv_free(out);
++ jv_free(x);
++ jv_free(a);
++ jv_free(b);
++ return jv_invalid_with_msg(jv_string("Equality check too deep"));
++ }
++ if (equal) {
+ include = 0;
+ break;
+ }
+@@ -379,11 +387,17 @@ jv binop_mod(jv a, jv b) {
+ #undef dtoi
+
+ jv binop_equal(jv a, jv b) {
+- return jv_bool(jv_equal(a, b));
++ int r = jv_equal(a, b);
++ if (r < 0)
++ return jv_invalid_with_msg(jv_string("Equality check too deep"));
++ return jv_bool(r);
+ }
+
+ jv binop_notequal(jv a, jv b) {
+- return jv_bool(!jv_equal(a, b));
++ int r = jv_equal(a, b);
++ if (r < 0)
++ return jv_invalid_with_msg(jv_string("Equality check too deep"));
++ return jv_bool(!r);
+ }
+
+ enum cmp_op {
+@@ -395,6 +409,8 @@ enum cmp_op {
+
+ static jv order_cmp(jv a, jv b, enum cmp_op op) {
+ int r = jv_cmp(a, b);
++ if (r == INT_MIN)
++ return jv_invalid_with_msg(jv_string("Comparison too deep"));
+ return jv_bool((op == CMP_OP_LESS && r < 0) ||
+ (op == CMP_OP_LESSEQ && r <= 0) ||
+ (op == CMP_OP_GREATEREQ && r >= 0) ||
+@@ -845,6 +861,12 @@ static jv f_bsearch(jq_state *jq, jv input, jv target) {
+ while (start < end) {
+ int mid = start + (end - start) / 2;
+ int result = jv_cmp(jv_copy(target), jv_array_get(jv_copy(input), mid));
++ if (result == INT_MIN) {
++ jv_free(answer);
++ jv_free(input);
++ jv_free(target);
++ return jv_invalid_with_msg(jv_string("Comparison too deep"));
++ }
+ if (result == 0) {
+ answer = jv_number(mid);
+ break;
+@@ -1136,6 +1158,14 @@ static jv minmax_by(jv values, jv keys, int is_min) {
+ for (int i=1; i<jv_array_length(jv_copy(values)); i++) {
+ jv item = jv_array_get(jv_copy(keys), i);
+ int cmp = jv_cmp(jv_copy(item), jv_copy(retkey));
++ if (cmp == INT_MIN) {
++ jv_free(item);
++ jv_free(values);
++ jv_free(keys);
++ jv_free(retkey);
++ jv_free(ret);
++ return jv_invalid_with_msg(jv_string("Comparison too deep"));
++ }
+ if ((cmp < 0) == (is_min == 1)) {
+ jv_free(retkey);
+ retkey = item;
+diff --git a/src/jv.c b/src/jv.c
+index f701b46..e079d08 100644
+--- a/src/jv.c
++++ b/src/jv.c
+@@ -912,16 +912,20 @@ static jv* jvp_array_write(jv* a, int i) {
+ }
+ }
+
+-static int jvp_array_equal(jv a, jv b) {
++static int jvp_equal(jv a, jv b, int depth);
++
++static int jvp_array_equal(jv a, jv b, int depth) {
+ if (jvp_array_length(a) != jvp_array_length(b))
+ return 0;
+ if (jvp_array_ptr(a) == jvp_array_ptr(b) &&
+ jvp_array_offset(a) == jvp_array_offset(b))
+ return 1;
+ for (int i=0; i<jvp_array_length(a); i++) {
+- if (!jv_equal(jv_copy(*jvp_array_read(a, i)),
+- jv_copy(*jvp_array_read(b, i))))
+- return 0;
++ int r = jvp_equal(jv_copy(*jvp_array_read(a, i)),
++ jv_copy(*jvp_array_read(b, i)),
++ depth);
++ if (r <= 0)
++ return r;
+ }
+ return 1;
+ }
+@@ -1071,7 +1075,14 @@ jv jv_array_indexes(jv a, jv b) {
+ int alen = jv_array_length(jv_copy(a));
+ for (int ai = 0; ai < alen; ++ai) {
+ jv_array_foreach(b, bi, belem) {
+- if (!jv_equal(jv_array_get(jv_copy(a), ai + bi), belem))
++ int equal = jv_equal(jv_array_get(jv_copy(a), ai + bi), belem);
++ if (equal < 0) {
++ jv_free(res);
++ jv_free(a);
++ jv_free(b);
++ return jv_invalid_with_msg(jv_string("Equality check too deep"));
++ }
++ if (!equal)
+ idx = -1;
+ else if (bi == 0 && idx == -1)
+ idx = ai;
+@@ -1828,7 +1839,7 @@ static int jvp_object_length(jv object) {
+ return n;
+ }
+
+-static int jvp_object_equal(jv o1, jv o2) {
++static int jvp_object_equal(jv o1, jv o2, int depth) {
+ int len2 = jvp_object_length(o2);
+ int len1 = 0;
+ for (int i=0; i<jvp_object_size(o1); i++) {
+@@ -1837,7 +1848,8 @@ static int jvp_object_equal(jv o1, jv o2) {
+ jv* slot2 = jvp_object_read(o2, slot->string);
+ if (!slot2) return 0;
+ // FIXME: do less refcounting here
+- if (!jv_equal(jv_copy(slot->value), jv_copy(*slot2))) return 0;
++ int r = jvp_equal(jv_copy(slot->value), jv_copy(*slot2), depth);
++ if (r <= 0) return r;
+ len1++;
+ }
+ return len1 == len2;
+@@ -2032,7 +2044,16 @@ int jv_get_refcnt(jv j) {
+ * Higher-level operations
+ */
+
+-int jv_equal(jv a, jv b) {
++#ifndef MAX_EQUAL_DEPTH
++#define MAX_EQUAL_DEPTH (10000)
++#endif
++
++static int jvp_equal(jv a, jv b, int depth) {
++ if (depth > MAX_EQUAL_DEPTH) {
++ jv_free(a);
++ jv_free(b);
++ return -1;
++ }
+ int r;
+ if (jv_get_kind(a) != jv_get_kind(b)) {
+ r = 0;
+@@ -2048,13 +2069,13 @@ int jv_equal(jv a, jv b) {
+ r = jvp_number_equal(a, b);
+ break;
+ case JV_KIND_ARRAY:
+- r = jvp_array_equal(a, b);
++ r = jvp_array_equal(a, b, depth + 1);
+ break;
+ case JV_KIND_STRING:
+ r = jvp_string_equal(a, b);
+ break;
+ case JV_KIND_OBJECT:
+- r = jvp_object_equal(a, b);
++ r = jvp_object_equal(a, b, depth + 1);
+ break;
+ default:
+ r = 1;
+@@ -2066,6 +2087,11 @@ int jv_equal(jv a, jv b) {
+ return r;
+ }
+
++// Returns 1 if equal, 0 if not equal, or -1 if the comparison is too deep
++int jv_equal(jv a, jv b) {
++ return jvp_equal(a, b, 0);
++}
++
+ int jv_identical(jv a, jv b) {
+ int r;
+ if (a.kind_flags != b.kind_flags
+diff --git a/src/jv_aux.c b/src/jv_aux.c
+index 594a21f..a39f1f1 100644
+--- a/src/jv_aux.c
++++ b/src/jv_aux.c
+@@ -16,6 +16,24 @@ static double jv_number_get_value_and_consume(jv number) {
+ return value;
+ }
+
++#ifndef MAX_CMP_DEPTH
++#define MAX_CMP_DEPTH (10000)
++#endif
++
++struct sort_cmp_state {
++ int too_deep;
++};
++
++#ifdef _MSC_VER
++static __declspec(thread) struct sort_cmp_state sort_cmp_state;
++#else
++#ifdef HAVE___THREAD
++static __thread struct sort_cmp_state sort_cmp_state;
++#else
++static struct sort_cmp_state sort_cmp_state;
++#endif
++#endif
++
+ static jv parse_slice(jv j, jv slice, int* pstart, int* pend) {
+ // Array slices
+ jv start_jv = jv_object_get(jv_copy(slice), jv_string("start"));
+@@ -471,8 +489,7 @@ static jv delpaths_sorted(jv object, jv paths, int start) {
+ int delkey = jv_array_length(jv_array_get(jv_copy(paths), i)) == start + 1;
+ jv key = jv_array_get(jv_array_get(jv_copy(paths), i), start);
+ while (j < jv_array_length(jv_copy(paths)) &&
+- jv_equal(jv_copy(key), jv_array_get(jv_array_get(jv_copy(paths), j), start)))
+- j++;
++ jv_equal(jv_copy(key), jv_array_get(jv_array_get(jv_copy(paths), j), start)) == 1);
+ // if i <= entry < j, then entry starts with key
+ if (delkey) {
+ // deleting this entire key, we don't care about any more specific deletions
+@@ -606,7 +623,13 @@ jv jv_keys(jv x) {
+ }
+ }
+
+-int jv_cmp(jv a, jv b) {
++static int jvp_cmp(jv a, jv b, int depth) {
++ if (depth > MAX_CMP_DEPTH) {
++ jv_free(a);
++ jv_free(b);
++ return INT_MIN;
++ }
++
+ if (jv_get_kind(a) != jv_get_kind(b)) {
+ int r = (int)jv_get_kind(a) - (int)jv_get_kind(b);
+ jv_free(a);
+@@ -621,14 +644,13 @@ int jv_cmp(jv a, jv b) {
+ case JV_KIND_FALSE:
+ case JV_KIND_TRUE:
+ // there's only one of each of these values
+- r = 0;
+ break;
+
+ case JV_KIND_NUMBER: {
+ if (jvp_number_is_nan(a)) {
+- r = jv_cmp(jv_null(), jv_copy(b));
++ r = jvp_cmp(jv_null(), jv_copy(b), depth);
+ } else if (jvp_number_is_nan(b)) {
+- r = jv_cmp(jv_copy(a), jv_null());
++ r = jvp_cmp(jv_copy(a), jv_null(), depth);
+ } else {
+ r = jvp_number_cmp(a, b);
+ }
+@@ -652,7 +674,9 @@ int jv_cmp(jv a, jv b) {
+ }
+ jv xa = jv_array_get(jv_copy(a), i);
+ jv xb = jv_array_get(jv_copy(b), i);
+- r = jv_cmp(xa, xb);
++ r = jvp_cmp(xa, xb, depth + 1);
++ if (r == INT_MIN)
++ break;
+ i++;
+ }
+ break;
+@@ -661,13 +685,14 @@ int jv_cmp(jv a, jv b) {
+ case JV_KIND_OBJECT: {
+ jv keys_a = jv_keys(jv_copy(a));
+ jv keys_b = jv_keys(jv_copy(b));
+- r = jv_cmp(jv_copy(keys_a), keys_b);
++ r = jvp_cmp(jv_copy(keys_a), keys_b, depth + 1);
+ if (r == 0) {
+ jv_array_foreach(keys_a, i, key) {
+ jv xa = jv_object_get(jv_copy(a), jv_copy(key));
+ jv xb = jv_object_get(jv_copy(b), key);
+- r = jv_cmp(xa, xb);
+- if (r) break;
++ r = jvp_cmp(xa, xb, depth + 1);
++ if (r != 0)
++ break;
+ }
+ }
+ jv_free(keys_a);
+@@ -680,6 +705,11 @@ int jv_cmp(jv a, jv b) {
+ return r;
+ }
+
++// Returns <0, 0, >0 if a is less than, equal to, or greater than b, or
++// INT_MIN if the comparison is too deep
++int jv_cmp(jv a, jv b) {
++ return jvp_cmp(a, b, 0);
++}
+
+ struct sort_entry {
+ jv object;
+@@ -687,19 +717,32 @@ struct sort_entry {
+ int index;
+ };
+
++static void sort_entry_array_free(struct sort_entry* entries, int start, int n) {
++ for (int i = start; i < n; i++) {
++ jv_free(entries[i].key);
++ jv_free(entries[i].object);
++ }
++ jv_mem_free(entries);
++}
++
+ static int sort_cmp(const void* pa, const void* pb) {
+ const struct sort_entry* a = pa;
+ const struct sort_entry* b = pb;
+ int r = jv_cmp(jv_copy(a->key), jv_copy(b->key));
++ if (r == INT_MIN) {
++ sort_cmp_state.too_deep = 1;
++ return 0;
++ }
+ // comparing by index if r == 0 makes the sort stable
+ return r ? r : (a->index - b->index);
+ }
+
+-static struct sort_entry* sort_items(jv objects, jv keys) {
++static struct sort_entry* sort_items(jv objects, jv keys, int *too_deep) {
+ assert(jv_get_kind(objects) == JV_KIND_ARRAY);
+ assert(jv_get_kind(keys) == JV_KIND_ARRAY);
+ assert(jv_array_length(jv_copy(objects)) == jv_array_length(jv_copy(keys)));
+ int n = jv_array_length(jv_copy(objects));
++ *too_deep = 0;
+ if (n == 0) {
+ jv_free(objects);
+ jv_free(keys);
+@@ -713,7 +756,13 @@ static struct sort_entry* sort_items(jv objects, jv keys) {
+ }
+ jv_free(objects);
+ jv_free(keys);
++ sort_cmp_state.too_deep = 0;
+ qsort(entries, n, sizeof(struct sort_entry), sort_cmp);
++ if (sort_cmp_state.too_deep) {
++ sort_entry_array_free(entries, 0, n);
++ *too_deep = 1;
++ return NULL;
++ }
+ return entries;
+ }
+
+@@ -722,7 +771,10 @@ jv jv_sort(jv objects, jv keys) {
+ assert(jv_get_kind(keys) == JV_KIND_ARRAY);
+ assert(jv_array_length(jv_copy(objects)) == jv_array_length(jv_copy(keys)));
+ int n = jv_array_length(jv_copy(objects));
+- struct sort_entry* entries = sort_items(objects, keys);
++ int too_deep = 0;
++ struct sort_entry* entries = sort_items(objects, keys, &too_deep);
++ if (too_deep)
++ return jv_invalid_with_msg(jv_string("Comparison too deep"));
+ jv ret = jv_array();
+ for (int i=0; i<n; i++) {
+ jv_free(entries[i].key);
+@@ -737,13 +789,24 @@ jv jv_group(jv objects, jv keys) {
+ assert(jv_get_kind(keys) == JV_KIND_ARRAY);
+ assert(jv_array_length(jv_copy(objects)) == jv_array_length(jv_copy(keys)));
+ int n = jv_array_length(jv_copy(objects));
+- struct sort_entry* entries = sort_items(objects, keys);
++ int too_deep = 0;
++ struct sort_entry* entries = sort_items(objects, keys, &too_deep);
++ if (too_deep)
++ return jv_invalid_with_msg(jv_string("Comparison too deep"));
+ jv ret = jv_array();
+ if (n > 0) {
+ jv curr_key = entries[0].key;
+ jv group = jv_array_append(jv_array(), entries[0].object);
+ for (int i = 1; i < n; i++) {
+- if (jv_equal(jv_copy(curr_key), jv_copy(entries[i].key))) {
++ int equal = jv_equal(jv_copy(curr_key), jv_copy(entries[i].key));
++ if (equal < 0) {
++ jv_free(curr_key);
++ jv_free(group);
++ sort_entry_array_free(entries, i, n);
++ jv_free(ret);
++ return jv_invalid_with_msg(jv_string("Equality check too deep"));
++ }
++ if (equal) {
+ jv_free(entries[i].key);
+ } else {
+ jv_free(curr_key);
+@@ -765,11 +828,21 @@ jv jv_unique(jv objects, jv keys) {
+ assert(jv_get_kind(keys) == JV_KIND_ARRAY);
+ assert(jv_array_length(jv_copy(objects)) == jv_array_length(jv_copy(keys)));
+ int n = jv_array_length(jv_copy(objects));
+- struct sort_entry* entries = sort_items(objects, keys);
++ int too_deep = 0;
++ struct sort_entry* entries = sort_items(objects, keys, &too_deep);
++ if (too_deep)
++ return jv_invalid_with_msg(jv_string("Comparison too deep"));
+ jv ret = jv_array();
+ jv curr_key = jv_invalid();
+ for (int i = 0; i < n; i++) {
+- if (jv_equal(jv_copy(curr_key), jv_copy(entries[i].key))) {
++ int equal = jv_equal(jv_copy(curr_key), jv_copy(entries[i].key));
++ if (equal < 0) {
++ jv_free(curr_key);
++ sort_entry_array_free(entries, i, n);
++ jv_free(ret);
++ return jv_invalid_with_msg(jv_string("Equality check too deep"));
++ }
++ if (equal) {
+ jv_free(entries[i].key);
+ jv_free(entries[i].object);
+ } else {
+diff --git a/tests/jq.test b/tests/jq.test
+index 5013bce..f35ef94 100644
+--- a/tests/jq.test
++++ b/tests/jq.test
+@@ -2560,3 +2560,43 @@ null
+ try delpaths([[range(10001) | 0]]) catch .
+ null
+ "Path too deep"
++
++# regression test for CVE-2026-40612
++reduce range(10000) as $_ ([]; [.]) | contains([[]])
++null
++true
++
++try (reduce range(10001) as $_ ([]; [.]) as $x | $x | contains($x)) catch .
++null
++"Containment check too deep"
++
++# regression test for CVE-2026-43896
++reduce range(10000) as $_ ({}; {a: .}) as $x | $x * $x | length
++null
++1
++
++try (reduce range(10001) as $_ ({}; {a: .}) as $x | $x * $x) catch .
++null
++"Object merge too deep"
++
++# regression test for deep structural equality recursion
++try ((reduce range(10001) as $_ ([]; [.])) as $x | (reduce range(10001) as $_ ([]; [.])) as $y | $x == $y) catch .
++null
++"Equality check too deep"
++
++# regression tests for deep ordering comparisons
++try ((reduce range(10001) as $_ ([]; [.])) as $x | [$x, $x] | sort) catch .
++null
++"Comparison too deep"
++
++try ((reduce range(10001) as $_ ([]; [.])) as $x | [$x, $x] | unique) catch .
++null
++"Comparison too deep"
++
++try ((reduce range(10001) as $_ ({}; {a: .})) as $x | [$x, $x] | sort) catch .
++null
++"Comparison too deep"
++
++try ((reduce range(10001) as $_ ({}; {a: .})) as $x | [$x, $x] | unique) catch .
++null
++"Comparison too deep"
+--
+2.43.0
+
@@ -17,6 +17,7 @@ SRC_URI = "git://github.com/jqlang/jq.git;protocol=https;branch=master;tag=jq-${
file://CVE-2026-33947.patch \
file://CVE-2026-33948.patch \
file://CVE-2026-39979.patch \
+ file://CVE-2026-47770.patch \
"
inherit autotools ptest