package/jq: patch various CVEs

Fixes the following vulnerabilities by importing upstream patches:

- CVE-2026-39979: 2f09060afa

- CVE-2026-33948: 6374ae0bcd

- CVE-2026-33947: fb59f14910

- CVE-2026-32316: e47e56d226

- CVE-2026-40164: 0c7d133c3c

- CVE-2026-40612: d1a12569d9

- CVE-2026-41256: 5a015deae3

- CVE-2026-41257: 01b3cded76

- CVE-2026-43894: 9761ceb7d6

- CVE-2026-43895: 9d223f153c

- CVE-2026-43896: 532ccea608

- CVE-2026-44777: f58787c418

- CVE-2026-47770: 7122866869

- CVE-2026-49839: e987df0d46

- CVE-2026-54679: 46d1da3094

Tests were stripped out of the patches.

Signed-off-by: Thomas Perale <thomas.perale@mind.be>
Signed-off-by: Peter Korsgaard <peter@korsgaard.com>
This commit is contained in:
Thomas Perale
2026-06-30 22:15:06 +02:00
committed by Peter Korsgaard
parent a8e8fc2fa4
commit e8191c3e17
16 changed files with 2833 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
From 2f09060afab23fe9390cce7cb860b10416e1bf5f Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Mon, 13 Apr 2026 11:04:52 +0900
Subject: [PATCH] Fix out-of-bounds read in jv_parse_sized()
This fixes CVE-2026-39979.
Co-authored-by: Mattias Wadman <mattias.wadman@gmail.com>
Upstream: https://github.com/jqlang/jq/commit/2f09060afab23fe9390cce7cb860b10416e1bf5f
CVE: CVE-2026-39979
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/jv_parse.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/jv_parse.c b/src/jv_parse.c
index aa2054cc09..56847b5eaa 100644
--- a/src/jv_parse.c
+++ b/src/jv_parse.c
@@ -892,8 +892,9 @@ jv jv_parse_sized_custom_flags(const char* string, int length, int flags) {
if (!jv_is_valid(value) && jv_invalid_has_msg(jv_copy(value))) {
jv msg = jv_invalid_get_msg(value);
- value = jv_invalid_with_msg(jv_string_fmt("%s (while parsing '%s')",
+ value = jv_invalid_with_msg(jv_string_fmt("%s (while parsing '%.*s')",
jv_string_value(msg),
+ length,
string));
jv_free(msg);
}

View File

@@ -0,0 +1,35 @@
From 6374ae0bcdfe33a18eb0ae6db28493b1f34a0a5b Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Mon, 13 Apr 2026 08:46:11 +0900
Subject: [PATCH] Fix NUL truncation in the JSON parser
This fixes CVE-2026-33948.
Upstream: https://github.com/jqlang/jq/commit/6374ae0bcdfe33a18eb0ae6db28493b1f34a0a5b
CVE: CVE-2026-33948
[thomas: remove tests]
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/util.c | 8 +-------
tests/shtest | 6 ++++++
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/util.c b/src/util.c
index fdfdb96d88..80d65fc808 100644
--- a/src/util.c
+++ b/src/util.c
@@ -309,13 +309,7 @@ static int jq_util_input_read_more(jq_util_input_state *state) {
if (p != NULL)
state->current_line++;
- if (p == NULL && state->parser != NULL) {
- /*
- * There should be no NULs in JSON texts (but JSON text
- * sequences are another story).
- */
- state->buf_valid_len = strlen(state->buf);
- } else if (p == NULL && feof(state->current_input)) {
+ if (p == NULL && feof(state->current_input)) {
size_t i;
/*

View File

@@ -0,0 +1,72 @@
From fb59f1491058d58bdc3e8dd28f1773d1ac690a1f Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Mon, 13 Apr 2026 11:23:40 +0900
Subject: [PATCH] Limit path depth to prevent stack overflow
Deeply nested path arrays can cause unbounded recursion in
`jv_setpath`, `jv_getpath`, and `jv_delpaths`, leading to
stack overflow. Add a depth limit of 10000 to match the
existing `tojson` depth limit. This fixes CVE-2026-33947.
CVE: CVE-2026-33947
Upstream: https://github.com/jqlang/jq/commit/fb59f1491058d58bdc3e8dd28f1773d1ac690a1f
[thomas: remove tests]
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/jv_aux.c | 21 +++++++++++++++++++++
tests/jq.test | 25 +++++++++++++++++++++++++
2 files changed, 46 insertions(+)
diff --git a/src/jv_aux.c b/src/jv_aux.c
index 018f380b10..fd5ff96684 100644
--- a/src/jv_aux.c
+++ b/src/jv_aux.c
@@ -375,6 +375,10 @@ static jv jv_dels(jv t, jv keys) {
return t;
}
+#ifndef MAX_PATH_DEPTH
+#define MAX_PATH_DEPTH (10000)
+#endif
+
jv jv_setpath(jv root, jv path, jv value) {
if (jv_get_kind(path) != JV_KIND_ARRAY) {
jv_free(value);
@@ -382,6 +386,12 @@ jv jv_setpath(jv root, jv path, jv value) {
jv_free(path);
return jv_invalid_with_msg(jv_string("Path must be specified as an array"));
}
+ if (jv_array_length(jv_copy(path)) > MAX_PATH_DEPTH) {
+ jv_free(value);
+ jv_free(root);
+ jv_free(path);
+ return jv_invalid_with_msg(jv_string("Path too deep"));
+ }
if (!jv_is_valid(root)){
jv_free(value);
jv_free(path);
@@ -434,6 +444,11 @@ jv jv_getpath(jv root, jv path) {
jv_free(path);
return jv_invalid_with_msg(jv_string("Path must be specified as an array"));
}
+ if (jv_array_length(jv_copy(path)) > MAX_PATH_DEPTH) {
+ jv_free(root);
+ jv_free(path);
+ return jv_invalid_with_msg(jv_string("Path too deep"));
+ }
if (!jv_is_valid(root)) {
jv_free(path);
return root;
@@ -511,6 +526,12 @@ jv jv_delpaths(jv object, jv paths) {
jv_free(elem);
return err;
}
+ if (jv_array_length(jv_copy(elem)) > MAX_PATH_DEPTH) {
+ jv_free(object);
+ jv_free(paths);
+ jv_free(elem);
+ return jv_invalid_with_msg(jv_string("Path too deep"));
+ }
jv_free(elem);
}
if (jv_array_length(jv_copy(paths)) == 0) {

View File

@@ -0,0 +1,53 @@
From e47e56d226519635768e6aab2f38f0ab037c09e5 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Thu, 12 Mar 2026 20:28:43 +0900
Subject: [PATCH] Fix heap buffer overflow in `jvp_string_append` and
`jvp_string_copy_replace_bad`
In `jvp_string_append`, the allocation size `(currlen + len) * 2` could
overflow `uint32_t` when `currlen + len` exceeds `INT_MAX`, causing a small
allocation followed by a large `memcpy`.
In `jvp_string_copy_replace_bad`, the output buffer size calculation
`length * 3 + 1` could overflow `uint32_t`, again resulting in a small
allocation followed by a large write.
Add overflow checks to both functions to return an error for strings
that would exceed `INT_MAX` in length. Fixes CVE-2026-32316.
CVE: CVE-2026-32316
Upstream: https://github.com/jqlang/jq/commit/e47e56d226519635768e6aab2f38f0ab037c09e5
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/jv.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/jv.c b/src/jv.c
index 722a539391..2a62b48419 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -1114,7 +1114,12 @@ static jv jvp_string_copy_replace_bad(const char* data, uint32_t length) {
const char* end = data + length;
const char* i = data;
- uint32_t maxlength = length * 3 + 1; // worst case: all bad bytes, each becomes a 3-byte U+FFFD
+ // worst case: all bad bytes, each becomes a 3-byte U+FFFD
+ uint64_t maxlength = (uint64_t)length * 3 + 1;
+ if (maxlength >= INT_MAX) {
+ return jv_invalid_with_msg(jv_string("String too long"));
+ }
+
jvp_string* s = jvp_string_alloc(maxlength);
char* out = s->data;
int c = 0;
@@ -1174,6 +1179,10 @@ static uint32_t jvp_string_remaining_space(jvp_string* s) {
static jv jvp_string_append(jv string, const char* data, uint32_t len) {
jvp_string* s = jvp_string_ptr(string);
uint32_t currlen = jvp_string_length(s);
+ if ((uint64_t)currlen + len >= INT_MAX) {
+ jv_free(string);
+ return jv_invalid_with_msg(jv_string("String too long"));
+ }
if (jvp_refcnt_unshared(string.u.ptr) &&
jvp_string_remaining_space(s) >= len) {

View File

@@ -0,0 +1,90 @@
From 0c7d133c3c7e37c00b6d46b658a02244fdd3c784 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Mon, 13 Apr 2026 08:53:26 +0900
Subject: [PATCH] Randomize hash seed to mitigate hash collision DoS attacks
The hash function used a fixed seed, allowing attackers to craft colliding keys
and cause O(n^2) object parsing performance. Initialize the seed from a random
source at process startup to prevent the attack. This fixes CVE-2026-40164.
Co-authored-by: Asaf Meizner <asafmeizner@gmail.com>
CVE: CVE-2026-40164
Upstream: https://github.com/jqlang/jq/commit/0c7d133c3c7e37c00b6d46b658a02244fdd3c784
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
configure.ac | 2 ++
src/jv.c | 34 ++++++++++++++++++++++++++++++++--
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/configure.ac b/configure.ac
index 5dac42655a..f7067a4341 100644
--- a/configure.ac
+++ b/configure.ac
@@ -143,6 +143,8 @@ AC_CHECK_MEMBER([struct tm.tm_gmtoff], [AC_DEFINE([HAVE_TM_TM_GMT_OFF],1,[Define
AC_CHECK_MEMBER([struct tm.__tm_gmtoff], [AC_DEFINE([HAVE_TM___TM_GMT_OFF],1,[Define to 1 if the system has the __tm_gmt_off field in struct tm])],
[], [[#include <time.h>]])
AC_FIND_FUNC([setlocale], [c], [#include <locale.h>], [0,0])
+AC_FIND_FUNC([arc4random], [c], [#include <stdlib.h>], [])
+AC_FIND_FUNC([getentropy], [c], [#include <unistd.h>], [0, 0])
dnl Figure out if we have the pthread functions we actually need
AC_FIND_FUNC_NO_LIBS([pthread_key_create], [], [#include <pthread.h>], [NULL, NULL])
diff --git a/src/jv.c b/src/jv.c
index 2a62b48419..607ac174f7 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -40,6 +40,10 @@
#include <limits.h>
#include <math.h>
#include <float.h>
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <pthread.h>
#include "jv_alloc.h"
#include "jv.h"
@@ -1206,7 +1210,33 @@ static jv jvp_string_append(jv string, const char* data, uint32_t len) {
}
}
-static const uint32_t HASH_SEED = 0x432A9843;
+static uint32_t hash_seed;
+static pthread_once_t hash_seed_once = PTHREAD_ONCE_INIT;
+
+static void jvp_hash_seed_init(void) {
+ uint32_t seed;
+#if defined(HAVE_ARC4RANDOM)
+ seed = arc4random();
+#elif defined(HAVE_GETENTROPY)
+ if (getentropy(&seed, sizeof(seed)) != 0)
+ seed = (uint32_t)getpid() ^ (uint32_t)time(NULL);
+#else
+ int fd = open("/dev/urandom", O_RDONLY);
+ if (fd >= 0) {
+ if (read(fd, &seed, sizeof(seed)) != 4)
+ seed = (uint32_t)getpid() ^ (uint32_t)time(NULL);
+ close(fd);
+ } else {
+ seed = (uint32_t)getpid() ^ (uint32_t)time(NULL);
+ }
+#endif
+ hash_seed = seed;
+}
+
+static uint32_t jvp_hash_seed(void) {
+ pthread_once(&hash_seed_once, jvp_hash_seed_init);
+ return hash_seed;
+}
static uint32_t rotl32 (uint32_t x, int8_t r){
return (x << r) | (x >> (32 - r));
@@ -1225,7 +1255,7 @@ static uint32_t jvp_string_hash(jv jstr) {
int len = (int)jvp_string_length(str);
const int nblocks = len / 4;
- uint32_t h1 = HASH_SEED;
+ uint32_t h1 = jvp_hash_seed();
const uint32_t c1 = 0xcc9e2d51;
const uint32_t c2 = 0x1b873593;

View File

@@ -0,0 +1,120 @@
From d1a12569d91641135976a8536776a4a329c02cc2 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Fri, 24 Apr 2026 22:02:24 +0900
Subject: [PATCH] Limit the containment check depth
This fixes CVE-2026-40612.
CVE: CVE-2026-40612
Upstream: https://github.com/jqlang/jq/commit/d1a12569d91641135976a8536776a4a329c02cc2
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/builtin.c | 5 ++++-
src/jv.c | 40 +++++++++++++++++++++++++++-------------
tests/jq.test | 9 +++++++++
3 files changed, 40 insertions(+), 14 deletions(-)
diff --git a/src/builtin.c b/src/builtin.c
index d33e9fb162..2b2a2d40da 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -419,7 +419,10 @@ jv binop_greatereq(jv a, jv b) {
static jv f_contains(jq_state *jq, jv a, jv b) {
if (jv_get_kind(a) == jv_get_kind(b)) {
- return jv_bool(jv_contains(a, b));
+ int r = jv_contains(a, b);
+ if (r < 0)
+ return jv_invalid_with_msg(jv_string("Containment check too deep"));
+ return jv_bool(r);
} else {
return type_error2(a, b, "cannot have their containment checked");
}
diff --git a/src/jv.c b/src/jv.c
index 607ac174f7..4b18c00cf6 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -938,19 +938,19 @@ static void jvp_clamp_slice_params(int len, int *pstart, int *pend)
}
-static int jvp_array_contains(jv a, jv b) {
+static int jvp_contains(jv a, jv b, int depth);
+
+static int jvp_array_contains(jv a, jv b, int depth) {
int r = 1;
jv_array_foreach(b, bi, belem) {
int ri = 0;
jv_array_foreach(a, ai, aelem) {
- if (jv_contains(aelem, jv_copy(belem))) {
- ri = 1;
- break;
- }
+ ri = jvp_contains(aelem, jv_copy(belem), depth);
+ if (ri) break;
}
jv_free(belem);
- if (!ri) {
- r = 0;
+ if (ri <= 0) {
+ r = ri;
break;
}
}
@@ -1843,7 +1843,7 @@ static int jvp_object_equal(jv o1, jv o2) {
return len1 == len2;
}
-static int jvp_object_contains(jv a, jv b) {
+static int jvp_object_contains(jv a, jv b, int depth) {
assert(JVP_HAS_KIND(a, JV_KIND_OBJECT));
assert(JVP_HAS_KIND(b, JV_KIND_OBJECT));
int r = 1;
@@ -1851,9 +1851,9 @@ static int jvp_object_contains(jv a, jv b) {
jv_object_foreach(b, key, b_val) {
jv a_val = jv_object_get(jv_copy(a), key);
- r = jv_contains(a_val, b_val);
+ r = jvp_contains(a_val, b_val, depth);
- if (!r) break;
+ if (r <= 0) break;
}
return r;
}
@@ -2084,14 +2084,23 @@ int jv_identical(jv a, jv b) {
return r;
}
-int jv_contains(jv a, jv b) {
+#ifndef MAX_CONTAINS_DEPTH
+#define MAX_CONTAINS_DEPTH (10000)
+#endif
+
+static int jvp_contains(jv a, jv b, int depth) {
+ if (depth > MAX_CONTAINS_DEPTH) {
+ jv_free(a);
+ jv_free(b);
+ return -1;
+ }
int r = 1;
if (jv_get_kind(a) != jv_get_kind(b)) {
r = 0;
} else if (JVP_HAS_KIND(a, JV_KIND_OBJECT)) {
- r = jvp_object_contains(a, b);
+ r = jvp_object_contains(a, b, depth + 1);
} else if (JVP_HAS_KIND(a, JV_KIND_ARRAY)) {
- r = jvp_array_contains(a, b);
+ r = jvp_array_contains(a, b, depth + 1);
} else if (JVP_HAS_KIND(a, JV_KIND_STRING)) {
int b_len = jv_string_length_bytes(jv_copy(b));
if (b_len != 0) {
@@ -2107,3 +2116,8 @@ int jv_contains(jv a, jv b) {
jv_free(b);
return r;
}
+
+// Returns 1 (contained), 0 (not contained), or -1 (too deep)
+int jv_contains(jv a, jv b) {
+ return jvp_contains(a, b, 0);
+}

View File

@@ -0,0 +1,34 @@
From 5a015deae35d19e3ebbc65db6c157a80e76df738 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Fri, 24 Apr 2026 22:15:08 +0900
Subject: [PATCH] Fix NUL truncation in program files loaded with -f
This fixes CVE-2026-41256.
CVE: CVE-2026-41256
Upstream: https://github.com/jqlang/jq/commit/5a015deae35d19e3ebbc65db6c157a80e76df738
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/main.c | 8 ++++++++
tests/shtest | 7 +++++++
2 files changed, 15 insertions(+)
diff --git a/src/main.c b/src/main.c
index ce362607e2..fb5c7ab8e3 100644
--- a/src/main.c
+++ b/src/main.c
@@ -611,6 +611,14 @@ int main(int argc, char* argv[]) {
ret = JQ_ERROR_SYSTEM;
goto out;
}
+ int len = jv_string_length_bytes(jv_copy(data));
+ if ((size_t)len != strlen(jv_string_value(data))) {
+ fprintf(stderr, "jq: program file contains NUL bytes\n");
+ free(program_origin);
+ jv_free(data);
+ ret = JQ_ERROR_SYSTEM;
+ goto out;
+ }
jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string(dirname(program_origin))));
ARGS = JV_OBJECT(jv_string("positional"), ARGS,
jv_string("named"), jv_copy(program_arguments));

View File

@@ -0,0 +1,53 @@
From 01b3cded76daacbfddb7f8763700b0803bcb5c6f Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Fri, 24 Apr 2026 22:09:44 +0900
Subject: [PATCH] Fix signed-int overflow in `stack_reallocate`
This fixes CVE-2026-41257.
CVE: CVE-2026-41257
Upstream: https://github.com/jqlang/jq/commit/01b3cded76daacbfddb7f8763700b0803bcb5c6f
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/exec_stack.h | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/src/exec_stack.h b/src/exec_stack.h
index 2a063e8cf9..159c56e4fb 100644
--- a/src/exec_stack.h
+++ b/src/exec_stack.h
@@ -2,8 +2,10 @@
#define EXEC_STACK_H
#include <stddef.h>
#include <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <limits.h>
#include "jv_alloc.h"
/*
@@ -81,15 +83,19 @@ static stack_ptr* stack_block_next(struct stack* s, stack_ptr p) {
}
static void stack_reallocate(struct stack* s, size_t sz) {
- int old_mem_length = -(s->bound) + ALIGNMENT;
- char* old_mem_start = (s->mem_end != NULL) ? (s->mem_end - old_mem_length) : NULL;
+ size_t old_mem_length = (size_t)(-(s->bound)) + ALIGNMENT;
+ char* old_mem_start = s->mem_end != NULL ? s->mem_end - old_mem_length : NULL;
- int new_mem_length = align_round_up((old_mem_length + sz + 256) * 2);
+ size_t new_mem_length = align_round_up((old_mem_length + sz + 256) * 2);
+ if (new_mem_length > INT_MAX) {
+ fprintf(stderr, "jq: error: cannot allocate memory\n");
+ abort();
+ }
char* new_mem_start = jv_mem_realloc(old_mem_start, new_mem_length);
memmove(new_mem_start + (new_mem_length - old_mem_length),
new_mem_start, old_mem_length);
s->mem_end = new_mem_start + new_mem_length;
- s->bound = -(new_mem_length - ALIGNMENT);
+ s->bound = -(int)(new_mem_length - ALIGNMENT);
}
static stack_ptr stack_push_block(struct stack* s, stack_ptr p, size_t sz) {

View File

@@ -0,0 +1,53 @@
From 9761ceb7d6cc48c16b25f0ab1baaef0e701927e4 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Wed, 6 May 2026 19:45:24 +0900
Subject: [PATCH] Reject numeric literals longer than DEC_MAX_DIGITS
(999999999)
A signed-int overflow in decNumber's D2U macro lets huge literals
write attacker-controlled bytes past a stack buffer. Cap the length
before calling decNumberFromString, and pre-slice long strings in
jv_dump_string_trunc so the resulting error message doesn't itself
allocate a multi-GiB buffer.
Fixes CVE-2026-43894.
CVE: CVE-2026-43894
Upstream: https://github.com/jqlang/jq/commit/9761ceb7d6cc48c16b25f0ab1baaef0e701927e4
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/jv.c | 5 ++++-
src/jv_print.c | 4 ++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/jv.c b/src/jv.c
index 84fafef666..074ee310c5 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -578,7 +578,10 @@ static jvp_literal_number* jvp_literal_number_alloc(unsigned literal_length) {
}
static jv jvp_literal_number_new(const char * literal) {
- jvp_literal_number* n = jvp_literal_number_alloc(strlen(literal));
+ size_t len = strlen(literal);
+ if (len > DEC_MAX_DIGITS)
+ return JV_INVALID;
+ jvp_literal_number* n = jvp_literal_number_alloc(len);
decContext *ctx = DEC_CONTEXT();
decContextClearStatus(ctx, DEC_Conversion_syntax);
diff --git a/src/jv_print.c b/src/jv_print.c
index 5c86c5d97c..bc251070f7 100644
--- a/src/jv_print.c
+++ b/src/jv_print.c
@@ -408,6 +408,10 @@ jv jv_dump_string(jv x, int flags) {
}
char *jv_dump_string_trunc(jv x, char *outbuf, size_t bufsize) {
+ if (jv_get_kind(x) == JV_KIND_STRING &&
+ (size_t)jv_string_length_bytes(jv_copy(x)) > bufsize) {
+ x = jv_string_slice(x, 0, bufsize);
+ }
x = jv_dump_string(x, 0);
const char *str = jv_string_value(x);
const size_t len = strlen(str);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
From 532ccea6080ed6758f39fe9f6208a44b665023d2 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Tue, 5 May 2026 22:44:02 +0900
Subject: [PATCH] Limit recursive object merge depth to prevent stack overflow
This fixes CVE-2026-43896.
CVE: CVE-2026-43896
Upstream: https://github.com/jqlang/jq/commit/532ccea6080ed6758f39fe9f6208a44b665023d2
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/jv.c | 25 +++++++++++++++++++++++--
tests/jq.test | 9 +++++++++
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/src/jv.c b/src/jv.c
index feb68d1a1c..84fafef666 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -1933,16 +1933,33 @@ jv jv_object_merge(jv a, jv b) {
return a;
}
-jv jv_object_merge_recursive(jv a, jv b) {
+#ifndef MAX_OBJECT_MERGE_DEPTH
+#define MAX_OBJECT_MERGE_DEPTH (10000)
+#endif
+
+static jv jvp_object_merge_recursive(jv a, jv b, int depth) {
assert(JVP_HAS_KIND(a, JV_KIND_OBJECT));
assert(JVP_HAS_KIND(b, JV_KIND_OBJECT));
+ if (depth > MAX_OBJECT_MERGE_DEPTH) {
+ jv_free(a);
+ jv_free(b);
+ return jv_invalid_with_msg(jv_string("Object merge too deep"));
+ }
+
jv_object_foreach(b, k, v) {
jv elem = jv_object_get(jv_copy(a), jv_copy(k));
if (jv_is_valid(elem) &&
JVP_HAS_KIND(elem, JV_KIND_OBJECT) &&
JVP_HAS_KIND(v, JV_KIND_OBJECT)) {
- a = jv_object_set(a, k, jv_object_merge_recursive(elem, v));
+ jv merged = jvp_object_merge_recursive(elem, v, depth + 1);
+ if (!jv_is_valid(merged)) {
+ jv_free(k);
+ jv_free(a);
+ jv_free(b);
+ return merged;
+ }
+ a = jv_object_set(a, k, merged);
} else {
jv_free(elem);
a = jv_object_set(a, k, v);
@@ -1953,6 +1970,10 @@ jv jv_object_merge_recursive(jv a, jv b) {
return a;
}
+jv jv_object_merge_recursive(jv a, jv b) {
+ return jvp_object_merge_recursive(a, b, 0);
+}
+
/*
* Object iteration (internal helpers)
*/

View File

@@ -0,0 +1,142 @@
From f58787c41835d9b17795730cb04925fdba25c71c Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Mon, 11 May 2026 20:41:38 +0900
Subject: [PATCH] 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.
CVE: CVE-2026-44777
Upstream: https://github.com/jqlang/jq/commit/f58787c41835d9b17795730cb04925fdba25c71c
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/linker.c | 59 ++++++++++++++++++++++++-------------
6 files changed, 70 insertions(+), 20 deletions(-)
diff --git a/src/linker.c b/src/linker.c
index e9027004cc..03f46db05c 100644
--- a/src/linker.c
+++ b/src/linker.c
@@ -21,9 +21,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,30 +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;
}
}
- 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);
@@ -413,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;
@@ -439,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

View File

@@ -0,0 +1,423 @@
From 7122866869960b55cea3646bc91334ef55787831 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.
CVE: CVE-2026-47770
Upstream: https://github.com/jqlang/jq/commit/7122866869960b55cea3646bc91334ef55787831
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/builtin.c | 36 +++++++++++++++--
src/jv.c | 46 +++++++++++++++++-----
src/jv_aux.c | 104 ++++++++++++++++++++++++++++++++++++++++++--------
4 files changed, 180 insertions(+), 28 deletions(-)
diff --git a/src/builtin.c b/src/builtin.c
index 2b2a2d40da..8ee605ed2b 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) ||
@@ -848,6 +864,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;
@@ -1139,6 +1161,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 074ee310c5..bcd7c9b5b0 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -915,16 +915,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;
}
@@ -1074,7 +1078,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;
@@ -1831,7 +1842,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++) {
@@ -1840,7 +1851,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;
@@ -2056,7 +2068,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;
@@ -2072,13 +2093,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;
@@ -2090,6 +2111,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 fd5ff96684..4f5be0ab14 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"));
@@ -606,7 +624,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 +645,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 +675,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 +686,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 +706,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 +718,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 +757,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 +772,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 +790,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 +829,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 {

View File

@@ -0,0 +1,32 @@
From e987df0d463d85fd70825e042a082427e8275b86 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Mon, 8 Jun 2026 22:14:48 +0900
Subject: [PATCH] Fix heap-buffer-overflow in raw file loading
When `jv_string_append_buf` overflows the string length limit,
it returns an invalid `jv`; `jv_load_file` then re-entered it
on the invalid value and overran the heap. Break out of the loop
once the value is invalid.
Fixes CVE-2026-49839.
CVE: CVE-2026-49839
Upstream: https://github.com/jqlang/jq/commit/e987df0d463d85fd70825e042a082427e8275b86
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/jv_file.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/jv_file.c b/src/jv_file.c
index 7706b0e06e..fbc1e4d653 100644
--- a/src/jv_file.c
+++ b/src/jv_file.c
@@ -57,6 +57,8 @@ jv jv_load_file(const char* filename, int raw) {
if (raw) {
data = jv_string_append_buf(data, buf, n);
+ if (!jv_is_valid(data))
+ break;
} else {
jv_parser_set_buf(parser, buf, n, !feof(file));
jv value;

View File

@@ -0,0 +1,73 @@
From 46d1da30944ce93dd671ac72b6513fc0eb747837 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Tue, 16 Jun 2026 14:31:14 +0900
Subject: [PATCH] Tighten string length bounds and propagate invalid jv in
implode
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The bound added in CVE-2026-32316 (e47e56d22) still allowed
`sizeof(jvp_string) + (currlen + len) * 2 + 1` to wrap `size_t` on
32-bit platforms. Tighten the threshold so the final allocation
fits in 32-bit `size_t`.
Also break out of `jv_string_implode` and `f_string_implode` once
`jv_string_append_codepoint` returns an invalid `jv`; otherwise the
next iteration triggers the assertion in `jvp_string_ptr` (or
invokes undefined behavior under `-DNDEBUG`).
Fixes CVE-2026-54679.
Co-authored-by: Dirk Müller <dirk@dmllr.de>
CVE: CVE-2026-54679
Upstream: https://github.com/jqlang/jq/commit/46d1da30944ce93dd671ac72b6513fc0eb747837
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
src/builtin.c | 1 +
src/jv.c | 5 +++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/builtin.c b/src/builtin.c
index ed205d9935..a3b7a61ae8 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -1396,6 +1396,7 @@ static jv f_string_implode(jq_state *jq, jv a) {
if (nv < 0 || nv > 0x10FFFF || (nv >= 0xD800 && nv <= 0xDFFF))
nv = 0xFFFD; // U+FFFD REPLACEMENT CHARACTER
s = jv_string_append_codepoint(s, nv);
+ if (!jv_is_valid(s)) break;
}
jv_free(a);
diff --git a/src/jv.c b/src/jv.c
index bcd7c9b5b0..48a63e6e55 100644
--- a/src/jv.c
+++ b/src/jv.c
@@ -1197,7 +1197,7 @@ static uint32_t jvp_string_remaining_space(jvp_string* s) {
static jv jvp_string_append(jv string, const char* data, uint32_t len) {
jvp_string* s = jvp_string_ptr(string);
uint32_t currlen = jvp_string_length(s);
- if ((uint64_t)currlen + len >= INT_MAX) {
+ if ((uint64_t)currlen + len >= INT_MAX - sizeof(jvp_string) / 2) {
jv_free(string);
return jv_invalid_with_msg(jv_string("String too long"));
}
@@ -1398,7 +1398,7 @@ jv jv_string_repeat(jv j, int n) {
}
int len = jv_string_length_bytes(jv_copy(j));
int64_t res_len = (int64_t)len * n;
- if (res_len >= INT_MAX) {
+ if ((uint64_t)res_len >= INT_MAX - sizeof(jvp_string) / 2) {
jv_free(j);
return jv_invalid_with_msg(jv_string("Repeat string result too long"));
}
@@ -1483,6 +1483,7 @@ jv jv_string_implode(jv j) {
if (nv < 0 || nv > 0x10FFFF || (nv >= 0xD800 && nv <= 0xDFFF))
nv = 0xFFFD; // U+FFFD REPLACEMENT CHARACTER
s = jv_string_append_codepoint(s, nv);
+ if (!jv_is_valid(s)) break;
}
jv_free(j);

View File

@@ -12,6 +12,51 @@ JQ_LICENSE_FILES = COPYING
JQ_CPE_ID_VENDOR = jqlang
JQ_INSTALL_STAGING = YES
# 0001-fix-out-of-bounds-read-in-jv-parse-sized.patch
JQ_IGNORE_CVES += CVE-2026-39979
# 0002-fix-NUL-truncation-in-the-JSON-parser.patch
JQ_IGNORE_CVES += CVE-2026-33948
# 0003-limit-path-depth-to-prevent-stack-overflow.patch
JQ_IGNORE_CVES += CVE-2026-33947
# 0004-fix-heap-buffer-overflow-in-jvp-string-append-and-jvp-string-copy-replace-bad.patch
JQ_IGNORE_CVES += CVE-2026-32316
# 0005-randomize-hash-seed-to-mitigate-hash-collision-DoS-attacks.patch
JQ_IGNORE_CVES += CVE-2026-40164
# 0006-limit-the-containment-check-depth.patch
JQ_IGNORE_CVES += 8:CVE: CVE-2026-40612
# 0007-fix-NUL-truncation-in-program-files-loaded-with-f.patch
JQ_IGNORE_CVES += CVE-2026-41256
# 0008-fix-signed-int-overflow-in-stack-reallocate.patch
JQ_IGNORE_CVES += CVE-2026-41257
# 0009-reject-numeric-literals-longer-than-DEC-MAX-DIGITS.patch
JQ_IGNORE_CVES += CVE-2026-43894
# 0010-reject-embedded-NUL-bytes-in-module-import-paths.patch
JQ_IGNORE_CVES += CVE-2026-43895
# 0011-limit-recursive-object-merge-depth-to-prevent-stack-overflow.patch
JQ_IGNORE_CVES += CVE-2026-43896
# 0012-detect-circular-module-imports-to-prevent-stack-overflow.patch
JQ_IGNORE_CVES += CVE-2026-44777
# 0013-guard-deep-structural-equality-and-comparison-recursion.patch
JQ_IGNORE_CVES += CVE-2026-47770
# 0014-fix-heap-buffer-overflow-in-raw-file-loading.patch
JQ_IGNORE_CVES += CVE-2026-49839
# 0015-tighten-string-length-bounds-and-propagate-invalid-jv-in-implode.patch
JQ_IGNORE_CVES += CVE-2026-54679
# uses c99 specific features
JQ_CONF_ENV += CFLAGS="$(TARGET_CFLAGS) -std=c99"
HOST_JQ_CONF_ENV += CFLAGS="$(HOST_CFLAGS) -std=c99"