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

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to