Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 385 additions & 0 deletions SPECS/jq/CVE-2026-47770.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,385 @@
From 7571f793ba3b02989f47e1f1c9dea5a4e2518ed7 Mon Sep 17 00:00:00 2001
From: AllSpark <allspark@microsoft.com>
Date: Sat, 27 Jun 2026 08:26:08 +0000
Subject: [PATCH] Guard deep structural equality and comparison recursion
(#3539)

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: AI Backport of None
---
src/builtin.c | 30 +++++++++++++++++--
src/jv.c | 37 +++++++++++++++++------
src/jv_aux.c | 81 ++++++++++++++++++++++++++++++++++++++++++++-------
tests/jq.test | 22 ++++++++++++++
4 files changed, 147 insertions(+), 23 deletions(-)

diff --git a/src/builtin.c b/src/builtin.c
index 08b94ac..5e8a9ec 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -336,7 +336,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;
}
@@ -431,11 +439,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 {
@@ -447,6 +461,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) ||
@@ -1065,6 +1081,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 dbf62dc..9bdabc2 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -888,16 +888,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;
}
@@ -1779,7 +1783,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++) {
@@ -1788,7 +1792,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;
@@ -2004,7 +2009,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;
@@ -2020,13 +2034,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;
@@ -2038,6 +2052,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 0855053..f44ccda 100644
--- a/src/jv_aux.c
+++ b/src/jv_aux.c
@@ -15,6 +15,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,7 +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)))
+ jv_equal(jv_copy(key), jv_array_get(jv_array_get(jv_copy(paths), j), start)) == 1)
j++;
// if i <= entry < j, then entry starts with key
if (delkey) {
@@ -602,7 +620,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);
@@ -622,9 +646,9 @@ int jv_cmp(jv a, jv b) {

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);
}
@@ -648,7 +672,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;
@@ -657,12 +683,12 @@ 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);
+ r = jvp_cmp(xa, xb, depth + 1);
if (r) break;
}
}
@@ -683,19 +709,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;
struct sort_entry* entries = jv_mem_calloc(n, sizeof(struct sort_entry));
for (int i=0; i<n; i++) {
entries[i].object = jv_array_get(jv_copy(objects), i);
@@ -704,7 +743,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;
}

@@ -713,7 +758,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);
@@ -728,13 +776,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);
diff --git a/tests/jq.test b/tests/jq.test
index 7bc6f06..bbb120f 100644
--- a/tests/jq.test
+++ b/tests/jq.test
@@ -2173,3 +2173,25 @@ null
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.45.4

Loading
Loading