Hi Sylvain, Thanks for taking the time to review this. I had a feeling it would need another round, and your notes helped a lot. I've reworked it.
Fair point about mixing the regression fix with new security fixes. Would you rather I keep them in, or drop them and ship just the regression fixes? And if they stay, should this go out as a new DLA instead of DLA-4662-2? I'm leaning towards keeping them in (and that's what the debdiff shows), but I'm happy either way. The changelog now has one bullet per issue. The two regressions start with the CVE, the args2obj bug starts with the GHSA, and the string-reference bug starts with 9163e096, since it has no CVE. Comparisons are version numbers, not DLA-4662-1. The regression patches are named CVE-2026-47770-2.patch and CVE-2026-32316-2.patch. CVE-2026-32316-2 explains why it is original; upstream's fix adds jv_string_repeat(), and that symbol would change the libjq1 ABI. CVE-2026-47770-2 explains why there is no upstream series to pick. jv_delpaths() on master still does not check what jv_sort() returns. The header also notes that Trixie shipped the same guards in 1.7.1-6+deb13u3. The delpaths abort is a Bookworm regression from the CVE-2026-47770 fix. On deb12u1, delpaths on two paths nested 10000 deep returns null. On deb12u2, that same call aborts in jv_array_length(). This update reports "Comparison too deep". Trixie included that guard in its first security update. Bookworm's first update shipped the depth cap without it. DSA-6416-2 is a different problem, the CVE-2026-44777 module crash. Bookworm has that fix, and the module cases I tested do pass. On CVE-2026-47770-2.patch, I agree that picking upstream pre-reqs is the better approach, so I checked whether that was possible here. I went through the history from jq 1.6 to current master. Nothing adds a check on the jv_sort() result in jv_delpaths(), and nothing teaches jv_string_concat() or jvp_string_append() to pass an invalid value through. The commits that do touch that area are dc849e9 (repeat performance) and 46d1da3 (a tighter length bound, plus invalid handling in implode). Upstream master still has the same unchecked loop, so there is no upstream series to pick. The patch is needed only because our CVE-2026-47770 backport makes jv_sort() able to return an error in 1.6, and it does the minimum to pass that error up instead of asserting. The updated debdiff against deb12u2 is attached. I'll push releasing this to Friday (October 16th, 2026), since I consider your (helpful) feedback showstopping. Best regards, Simon Quigley [email protected]
diff -Nru jq-1.6/debian/changelog jq-1.6/debian/changelog
--- jq-1.6/debian/changelog 2026-07-01 03:47:59.000000000 -0500
+++ jq-1.6/debian/changelog 2026-10-13 11:08:18.000000000 -0500
@@ -1,3 +1,28 @@
+jq (1.6-2.1+deb12u3) bookworm-security; urgency=medium
+
+ * Non-maintainer upload by the LTS Team.
+ * CVE-2026-47770: The depth cap in 1.6-2.1+deb12u2 lets jv_sort()
+ return an error. delpaths on two paths nested 10000 deep then
+ aborted in jv_array_length(). 1.6-2.1+deb12u1 returned null. It
+ now reports "Comparison too deep". jv_string_concat(),
+ jv_dump_string_trunc() and the error prints in main.c pass the
+ error through instead of asserting. Upstream still asserts on
+ this input, so the patch is original.
+ * CVE-2026-32316: Repeating a string past the length bound still
+ aborted after 1.6-2.1+deb12u2. It now fails with "Repeat string
+ result too long". A count below 1 still returns the old result:
+ null for 0 or less, and the input string for a fraction below 1.
+ Upstream's jv_string_repeat() would add a symbol to libjq1, so
+ the check is done in f_multiply() instead.
+ * GHSA-gf4g-95wj-4q4r: args2obj() double-frees when
+ jq_compile_args() is given an array. The command line always
+ passes an object, so only libjq is affected. No CVE is assigned.
+ * 9163e096: f_multiply() aliased the string it was repeating, so
+ ("a"*200)*30 copied from freed memory. This was not introduced by
+ 1.6-2.1+deb12u2. No CVE is assigned.
+
+ -- Simon Quigley <[email protected]> Tue, 13 Oct 2026 14:11:07 -0500
+
jq (1.6-2.1+deb12u2) bookworm-security; urgency=high
* Non-maintainer upload by the LTS Team.
diff -Nru jq-1.6/debian/patches/9163e096.patch
jq-1.6/debian/patches/9163e096.patch
--- jq-1.6/debian/patches/9163e096.patch 1969-12-31 18:00:00.000000000
-0600
+++ jq-1.6/debian/patches/9163e096.patch 2026-10-13 11:08:18.000000000
-0500
@@ -0,0 +1,43 @@
+Origin:
https://github.com/jqlang/jq/commit/9163e09605383a88f6e953d6cb5cc2aebe18c84f
+Reviewed-by: Simon Quigley <[email protected]>
+Last-Update: 2026-10-13
+
+Not part of the CVE fixes above. 1.6's f_multiply() aliased the string
+it was repeating, so ("a"*200)*30 copied from memory the first growing
+append had freed. The reference change matches upstream. It is applied
+on top of the length check in CVE-2026-32316-2.patch, which is why the
+context differs from the upstream diffstat.
+
+From 9163e09605383a88f6e953d6cb5cc2aebe18c84f Mon Sep 17 00:00:00 2001
+From: itchyny <[email protected]>
+Date: Fri, 8 May 2020 14:22:25 +0900
+Subject: [PATCH] Fix multiple string multiplication
+
+---
+ src/builtin.c | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+Index: jq-1.6/src/builtin.c
+===================================================================
+--- jq-1.6.orig/src/builtin.c
++++ jq-1.6/src/builtin.c
+@@ -327,7 +327,7 @@ static jv f_multiply(jq_state *jq, jv in
+ jv_free(num);
+ return jv_invalid_with_msg(jv_string("Repeat string result too long"));
+ }
+- jv res = str;
++ jv res = jv_copy(str);
+
+ for (n = jv_number_value(num) - 1; n > 0; n--) {
+ res = jv_string_append_buf(res, jv_string_value(str), alen);
+@@ -337,8 +337,9 @@ static jv f_multiply(jq_state *jq, jv in
+ }
+
+ jv_free(num);
++ jv_free(str);
+ if (n < 0) {
+- jv_free(str);
++ jv_free(res);
+ return jv_null();
+ }
+ return res;
diff -Nru jq-1.6/debian/patches/CVE-2026-32316-2.patch
jq-1.6/debian/patches/CVE-2026-32316-2.patch
--- jq-1.6/debian/patches/CVE-2026-32316-2.patch 1969-12-31
18:00:00.000000000 -0600
+++ jq-1.6/debian/patches/CVE-2026-32316-2.patch 2026-10-13
11:08:18.000000000 -0500
@@ -0,0 +1,60 @@
+Origin: vendor
+Reviewed-by: Simon Quigley <[email protected]>
+Last-Update: 2026-10-13
+
+1.6-2.1+deb12u2 made jvp_string_append() return invalid past the
+length bound. f_multiply() still appends in a loop and never checks,
+so the next iteration aborts. A count below 1 still returns what 1.6
+returned before: null for 0 or less, and the input string for a
+fraction below 1.
+
+Upstream fixed this in 1.8 with jv_string_repeat(). That function is
+exported, so shipping it would add a symbol to libjq1. The length
+check is done in f_multiply() instead, and the error text is
+upstream's "Repeat string result too long".
+
+Index: jq-1.6/src/builtin.c
+===================================================================
+--- jq-1.6.orig/src/builtin.c
++++ jq-1.6/src/builtin.c
+@@ -318,10 +318,23 @@ static jv f_multiply(jq_state *jq, jv in
+ }
+ int n;
+ size_t alen = jv_string_length_bytes(jv_copy(str));
++ double d = jv_number_value(num);
++ // The result has to stay within the length bound that
++ // jvp_string_append() enforces, and the repeat count has to survive
++ // the conversion to int below.
++ if (d > INT_MAX || (d >= 1 && (uint64_t)alen * (uint64_t)d >= INT_MAX)) {
++ jv_free(str);
++ jv_free(num);
++ return jv_invalid_with_msg(jv_string("Repeat string result too long"));
++ }
+ jv res = str;
+
+- for (n = jv_number_value(num) - 1; n > 0; n--)
++ for (n = jv_number_value(num) - 1; n > 0; n--) {
+ res = jv_string_append_buf(res, jv_string_value(str), alen);
++ // jvp_string_append() returns invalid once the result would exceed
++ // its length bound; appending to that would assert in jvp_string_ptr().
++ if (!jv_is_valid(res)) break;
++ }
+
+ jv_free(num);
+ if (n < 0) {
+Index: jq-1.6/tests/jq.test
+===================================================================
+--- jq-1.6.orig/tests/jq.test
++++ jq-1.6/tests/jq.test
+@@ -1586,6 +1586,11 @@ null
+ null
+ "Comparison too deep"
+
++# repeating a string past the length bound errors instead of aborting
++[.[] | try (.s * .n) catch .]
++[{"s":"abc","n":1000000000},{"s":"a","n":3},{"s":"abc","n":0}]
++["Repeat string result too long","aaa",null]
++
+ # nan is parsed as a valid NaN value from JSON
+
+ fromjson | isnan
diff -Nru jq-1.6/debian/patches/CVE-2026-47770-2.patch
jq-1.6/debian/patches/CVE-2026-47770-2.patch
--- jq-1.6/debian/patches/CVE-2026-47770-2.patch 1969-12-31
18:00:00.000000000 -0600
+++ jq-1.6/debian/patches/CVE-2026-47770-2.patch 2026-10-13
11:08:18.000000000 -0500
@@ -0,0 +1,122 @@
+Origin: vendor
+Reviewed-by: Simon Quigley <[email protected]>
+Last-Update: 2026-10-13
+
+1.6-2.1+deb12u2 caps comparison depth, so jv_sort() can return an
+error. jv_delpaths() passed that error to jv_array_length() and
+aborted. The same input returned null in 1.6-2.1+deb12u1. Callers of
+jvp_string_append() abort the same way, because they still assume a
+real string comes back.
+
+Upstream master still asserts on this input. There is no commit to
+cherry-pick, and taking the later jv.c rework would change far more
+of 1.6 than these guards. 1.6 has no stderr_cb, so that call is not
+patched.
+
+Trixie shipped the same guards in 1.7.1-6+deb13u3.
+
+Index: jq-1.6/src/jv.c
+===================================================================
+--- jq-1.6.orig/src/jv.c
++++ jq-1.6/src/jv.c
+@@ -540,6 +540,9 @@ static uint32_t jvp_string_remaining_spa
+ }
+
+ static jv jvp_string_append(jv string, const char* data, uint32_t len) {
++ // Once the length bound below has been hit the string is invalid, and
++ // callers that append in a loop would otherwise pass it back in here.
++ if (!jv_is_valid(string)) return string;
+ jvp_string* s = jvp_string_ptr(string);
+ uint32_t currlen = jvp_string_length(s);
+ if ((uint64_t)currlen + len >= INT_MAX - sizeof(jvp_string) / 2) {
+@@ -854,6 +857,15 @@ jv jv_string_slice(jv j, int start, int
+ }
+
+ jv jv_string_concat(jv a, jv b) {
++ // Either side may already be invalid; jvp_string_ptr() asserts on those.
++ if (!jv_is_valid(a)) {
++ jv_free(b);
++ return a;
++ }
++ if (!jv_is_valid(b)) {
++ jv_free(a);
++ return b;
++ }
+ a = jvp_string_append(a, jv_string_value(b),
+ jvp_string_length(jvp_string_ptr(b)));
+ jv_free(b);
+Index: jq-1.6/src/jv_aux.c
+===================================================================
+--- jq-1.6.orig/src/jv_aux.c
++++ jq-1.6/src/jv_aux.c
+@@ -447,6 +447,11 @@ jv jv_delpaths(jv object, jv paths) {
+ return jv_invalid_with_msg(jv_string("Paths must be specified as an
array"));
+ }
+ paths = jv_sort(paths, jv_copy(paths));
++ // jv_sort() reports a too-deep comparison by returning an invalid jv.
++ if (!jv_is_valid(paths)) {
++ jv_free(object);
++ return paths;
++ }
+ jv_array_foreach(paths, i, elem) {
+ if (jv_get_kind(elem) != JV_KIND_ARRAY) {
+ jv_free(object);
+Index: jq-1.6/src/jv_print.c
+===================================================================
+--- jq-1.6.orig/src/jv_print.c
++++ jq-1.6/src/jv_print.c
+@@ -394,6 +394,14 @@ char *jv_dump_string_trunc(jv x, char *o
+ x = jv_string_slice(x, 0, bufsize);
+ }
+ x = jv_dump_string(x,0);
++ // Only strings are pre-truncated above, so dumping a large array or object
++ // can exceed the string length bound and come back invalid.
++ if (!jv_is_valid(x)) {
++ jv_free(x);
++ strncpy(outbuf, "<value too large>", bufsize);
++ outbuf[bufsize - 1] = 0;
++ return outbuf;
++ }
+ const char* p = jv_string_value(x);
+ const size_t len = strlen(p);
+ strncpy(outbuf, p, bufsize);
+Index: jq-1.6/src/main.c
+===================================================================
+--- jq-1.6.orig/src/main.c
++++ jq-1.6/src/main.c
+@@ -206,7 +206,8 @@ static int process(jq_state *jq, jv valu
+ // Halt with no output
+ } else if (jv_is_valid(error_message)) {
+ error_message = jv_dump_string(jv_copy(error_message), 0);
+- fprintf(stderr, "%s\n", jv_string_value(error_message));
++ fprintf(stderr, "%s\n", jv_is_valid(error_message) ?
++ jv_string_value(error_message) : "<value too large>");
+ } // else no message on stderr; use --debug-trace to see a message
+ fflush(stderr);
+ jv_free(error_message);
+@@ -220,7 +221,8 @@ static int process(jq_state *jq, jv valu
+ } else {
+ msg = jv_dump_string(msg, 0);
+ fprintf(stderr, "jq: error (at %s) (not a string): %s\n",
+- jv_string_value(input_pos), jv_string_value(msg));
++ jv_string_value(input_pos), jv_is_valid(msg) ?
++ jv_string_value(msg) : "<value too large>");
+ }
+ ret = 5;
+ jv_free(input_pos);
+Index: jq-1.6/tests/jq.test
+===================================================================
+--- jq-1.6.orig/tests/jq.test
++++ jq-1.6/tests/jq.test
+@@ -1581,6 +1581,11 @@ try ((reduce range(10001) as $_ ({}; {a:
+ null
+ "Comparison too deep"
+
++# delpaths reports a too-deep path comparison instead of aborting
++(reduce range(10000) as $i ([];[.])) as $x | try (null |
delpaths([[$x],[$x]])) catch .
++null
++"Comparison too deep"
++
+ # nan is parsed as a valid NaN value from JSON
+
+ fromjson | isnan
diff -Nru jq-1.6/debian/patches/GHSA-gf4g-95wj-4q4r.patch
jq-1.6/debian/patches/GHSA-gf4g-95wj-4q4r.patch
--- jq-1.6/debian/patches/GHSA-gf4g-95wj-4q4r.patch 1969-12-31
18:00:00.000000000 -0600
+++ jq-1.6/debian/patches/GHSA-gf4g-95wj-4q4r.patch 2026-10-13
11:08:18.000000000 -0500
@@ -0,0 +1,39 @@
+Origin:
https://github.com/jqlang/jq/commit/3985b80ce50bd75c6eb5a97cb3348c3f835ca8e0
+Reviewed-by: Simon Quigley <[email protected]>
+Last-Update: 2026-10-13
+
+The execute.c hunk is unchanged. The jq_test.c tests do not fit the
+1.6 harness, which has no skip/take arguments, and are left out. The
+command line always passes an object, so only libjq is affected.
+
+From 3985b80ce50bd75c6eb5a97cb3348c3f835ca8e0 Mon Sep 17 00:00:00 2001
+From: Scott Seal <[email protected]>
+Date: Mon, 9 Mar 2026 03:30:08 -0500
+Subject: [PATCH] Fix use-after-free in args2obj() array argument path (#3498)
+
+Copy `kk` and `vk` before passing to jv_object_get(), which consumes
+its arguments. Without copies, subsequent loop iterations read freed
+memory (CWE-416) and post-loop jv_free() double-frees (CWE-415).
+
+This bug was introduced in `b279713e` when array processing was hoisted
+out of
+the loop into locals without accounting for jv_object_get() semantics.
+Fixes GHSA-gf4g-95wj-4q4r.
+---
+ src/execute.c | 2 +-
+ src/jq_test.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 188 insertions(+), 1 deletion(-)
+
+Index: jq-1.6/src/execute.c
+===================================================================
+--- jq-1.6.orig/src/execute.c
++++ jq-1.6/src/execute.c
+@@ -1144,7 +1144,7 @@ args2obj(jv args)
+ jv kk = jv_string("name");
+ jv vk = jv_string("value");
+ jv_array_foreach(args, i, v)
+- r = jv_object_set(r, jv_object_get(jv_copy(v), kk), jv_object_get(v, vk));
++ r = jv_object_set(r, jv_object_get(jv_copy(v), jv_copy(kk)),
jv_object_get(v, jv_copy(vk)));
+ jv_free(args);
+ jv_free(kk);
+ jv_free(vk);
diff -Nru jq-1.6/debian/patches/series jq-1.6/debian/patches/series
--- jq-1.6/debian/patches/series 2026-07-01 03:45:02.000000000 -0500
+++ jq-1.6/debian/patches/series 2026-10-13 11:08:18.000000000 -0500
@@ -27,3 +27,7 @@
CVE-2026-54679-1.patch
CVE-2026-54679-2.patch
CVE-2026-54679-3.patch
+CVE-2026-47770-2.patch
+CVE-2026-32316-2.patch
+GHSA-gf4g-95wj-4q4r.patch
+9163e096.patch
OpenPGP_signature.asc
Description: OpenPGP digital signature
