Version in base suite: 3.16.3-1 Base version: ruby-oj_3.16.3-1 Target version: ruby-oj_3.16.3-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/ruby-oj/ruby-oj_3.16.3-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/ruby-oj/ruby-oj_3.16.3-1+deb13u1.dsc changelog | 17 + gbp.conf | 2 patches/CVE-2026-oj-security-bbde91a.patch | 39 +++ patches/CVE-2026-oj-security-ec368db.patch | 317 +++++++++++++++++++++++++++++ patches/series | 2 salsa-ci.yml | 4 6 files changed, 381 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp6ds6m1_z/ruby-oj_3.16.3-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp6ds6m1_z/ruby-oj_3.16.3-1+deb13u1.dsc: no acceptable signature found diff -Nru ruby-oj-3.16.3/debian/changelog ruby-oj-3.16.3/debian/changelog --- ruby-oj-3.16.3/debian/changelog 2023-12-18 20:43:34.000000000 +0000 +++ ruby-oj-3.16.3/debian/changelog 2026-09-25 01:27:32.000000000 +0000 @@ -1,3 +1,20 @@ +ruby-oj (3.16.3-1+deb13u1) trixie-security; urgency=high + + * Fix multiple memory-safety issues in the Oj C extension: + - CVE-2026-54502 / CVE-2026-54896: clamp extreme :indent (stack/heap + overflow) + - CVE-2026-54897: Oj::Doc iterator use-after-free on reentrant close + - CVE-2026-54898: parser use-after-free when callbacks mutate input + - CVE-2026-54899: symbol_keys cache use-after-free + - CVE-2026-54900: create_id / form_attr negative-size memcpy + - CVE-2026-54901: unmarked hash_class / array_class GC use-after-free + - CVE-2026-54902: SAJ long-key use-after-free + - CVE-2026-54903: integer overflow / heap corruption on >2GB strings + - CVE-2026-54500: uninitialized / OOB read on long keys in intern.c + - CVE-2026-54592: stack overflow in Oj::Doc#each_child on deep nesting + + -- Simon Quigley Thu, 24 Sep 2026 20:27:32 -0500 + ruby-oj (3.16.3-1) unstable; urgency=medium * New upstream version 3.16.3 diff -Nru ruby-oj-3.16.3/debian/gbp.conf ruby-oj-3.16.3/debian/gbp.conf --- ruby-oj-3.16.3/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 +++ ruby-oj-3.16.3/debian/gbp.conf 2026-09-25 01:27:32.000000000 +0000 @@ -0,0 +1,2 @@ +[DEFAULT] +pristine-tar = True diff -Nru ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-bbde91a.patch ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-bbde91a.patch --- ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-bbde91a.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-bbde91a.patch 2026-09-25 01:27:32.000000000 +0000 @@ -0,0 +1,39 @@ +Description: backport of upstream intern.c / fast.c security fixes (bbde91a / #1015) + Backport C security fixes from upstream commit bbde91a (v3.17.3) covering + CVE-2026-54500 and CVE-2026-54592: correct rb_intern3 buffer pointer in + form_attr, and add DepthError / where-- in doc_each_child. +Origin: https://github.com/ohler55/oj/commit/bbde91a679728f94c4492ebc3683f4fa3309049f +Forwarded: not-needed +Last-Update: 2026-09-22 +--- +--- a/ext/oj/fast.c ++++ b/ext/oj/fast.c +@@ -1500,6 +1500,9 @@ static VALUE doc_each_child(int argc, VA + Leaf first = (*doc->where)->elements->next; + Leaf e = first; + ++ if (MAX_STACK <= (doc->where + 1) - doc->where_path) { ++ rb_raise(rb_const_get_at(Oj, rb_intern("DepthError")), "Path too deep. Limit is %d levels.", MAX_STACK); ++ } + doc->where++; + do { + *doc->where = e; +@@ -1509,6 +1512,7 @@ static VALUE doc_each_child(int argc, VA + } + e = e->next; + } while (e != first); ++ doc->where--; + } + if (0 < wlen) { + memcpy(doc->where_path, save_path, sizeof(Leaf) * (wlen + 1)); +--- a/ext/oj/intern.c ++++ b/ext/oj/intern.c +@@ -69,7 +69,7 @@ static VALUE form_attr(const char *str, + memcpy(b + 1, str, len); + b[len + 1] = '\0'; + } +- id = rb_intern3(buf, len + 1, oj_utf8_encoding); ++ id = rb_intern3(b, len + 1, oj_utf8_encoding); + OJ_R_FREE(b); + return id; + } diff -Nru ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-ec368db.patch ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-ec368db.patch --- ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-ec368db.patch 1970-01-01 00:00:00.000000000 +0000 +++ ruby-oj-3.16.3/debian/patches/CVE-2026-oj-security-ec368db.patch 2026-09-25 01:27:32.000000000 +0000 @@ -0,0 +1,317 @@ +Description: backport of upstream stack/extreme-size security fixes (ec368db / #1014) + Backport C security fixes from upstream commit ec368db (v3.17.2) covering + CVE-2026-54502 and CVE-2026-54896 through CVE-2026-54903. + . + Includes CVE-2026-54902 saj2.c mark() fix: always rb_gc_mark SAJ stack keys + (drop if (!cache_keys) guard). Upstream ec368db claimed the SAJ long-key UAF + fixed but omitted this hunk; folded here rather than a separate patch. + . + Keep oj_rxclass_match(int len) unchanged for ABI; gate call sites with + INT_MAX before casting. parser_parse freezes an rb_str_dup copy, never the + caller string. Dropped non-security hunks: CHANGELOG, version.rb, notes, safe.c. +Origin: https://github.com/ohler55/oj/commit/ec368dbe936ef0104b782e4b0f67b17d6c7276f7 +Forwarded: not-needed +Last-Update: 2026-09-22 +--- +--- a/ext/oj/compat.c ++++ b/ext/oj/compat.c +@@ -2,6 +2,7 @@ + // Licensed under the MIT License. See LICENSE file in the project root for license details. + + #include ++#include + + #include "encode.h" + #include "err.h" +@@ -27,6 +28,9 @@ static void hash_set_cstr(ParseInfo pi, + volatile VALUE rkey = oj_calc_hash_key(pi, kval); + + if (Yes == pi->options.create_ok && NULL != pi->options.str_rx.head) { ++ if (INT_MAX < len) { ++ rb_raise(rb_eArgError, "string too long"); ++ } + VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, (int)len); + + if (Qnil != clas) { +@@ -84,6 +88,9 @@ static void add_cstr(ParseInfo pi, const + volatile VALUE rstr = oj_cstr_to_value(str, len, (size_t)pi->options.cache_str); + + if (Yes == pi->options.create_ok && NULL != pi->options.str_rx.head) { ++ if (INT_MAX < len) { ++ rb_raise(rb_eArgError, "string too long"); ++ } + VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, (int)len); + + if (Qnil != clas) { +@@ -155,6 +162,9 @@ static void array_append_cstr(ParseInfo + volatile VALUE rstr = oj_cstr_to_value(str, len, (size_t)pi->options.cache_str); + + if (Yes == pi->options.create_ok && NULL != pi->options.str_rx.head) { ++ if (INT_MAX < len) { ++ rb_raise(rb_eArgError, "string too long"); ++ } + VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, (int)len); + + if (Qnil != clas) { +--- a/ext/oj/custom.c ++++ b/ext/oj/custom.c +@@ -2,6 +2,7 @@ + // Licensed under the MIT License. See LICENSE file in the project root for license details. + + #include ++#include + #include + + #include "code.h" +@@ -909,6 +910,9 @@ static void hash_set_cstr(ParseInfo pi, + volatile VALUE rkey = oj_calc_hash_key(pi, kval); + + if (Yes == pi->options.create_ok && NULL != pi->options.str_rx.head) { ++ if (INT_MAX < len) { ++ rb_raise(rb_eArgError, "string too long"); ++ } + VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, (int)len); + + if (Qnil != clas) { +@@ -1014,6 +1018,9 @@ static void array_append_cstr(ParseInfo + volatile VALUE rstr = rb_utf8_str_new(str, len); + + if (Yes == pi->options.create_ok && NULL != pi->options.str_rx.head) { ++ if (INT_MAX < len) { ++ rb_raise(rb_eArgError, "string too long"); ++ } + VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, (int)len); + + if (Qnil != clas) { +--- a/ext/oj/fast.c ++++ b/ext/oj/fast.c +@@ -80,7 +80,7 @@ static void each_leaf(Doc doc, VALUE se + static int move_step(Doc doc, const char *path, int loc); + static Leaf get_doc_leaf(Doc doc, const char *path); + static Leaf get_leaf(Leaf *stack, Leaf *lp, const char *path); +-static void each_value(Doc doc, Leaf leaf); ++static void each_value(Doc doc, Leaf leaf, VALUE self); + + VALUE oj_doc_class = Qundef; + +@@ -952,6 +952,9 @@ static void each_leaf(Doc doc, VALUE sel + } + } else { + rb_yield(self); ++ if (NULL == DATA_PTR(self)) { ++ rb_raise(rb_eIOError, "Document closed."); ++ } + } + } + +@@ -1045,19 +1048,22 @@ static int move_step(Doc doc, const char + return loc; + } + +-static void each_value(Doc doc, Leaf leaf) { ++static void each_value(Doc doc, Leaf leaf, VALUE self) { + if (COL_VAL == leaf->value_type) { + if (0 != leaf->elements) { + Leaf first = leaf->elements->next; + Leaf e = first; + + do { +- each_value(doc, e); ++ each_value(doc, e, self); + e = e->next; + } while (e != first); + } + } else { + rb_yield(leaf_value(doc, leaf)); ++ if (NULL == DATA_PTR(self)) { ++ rb_raise(rb_eIOError, "Document closed."); ++ } + } + } + +@@ -1498,6 +1504,9 @@ static VALUE doc_each_child(int argc, VA + do { + *doc->where = e; + rb_yield(self); ++ if (NULL == DATA_PTR(self)) { ++ rb_raise(rb_eIOError, "Document closed."); ++ } + e = e->next; + } while (e != first); + } +@@ -1543,7 +1552,7 @@ static VALUE doc_each_value(int argc, VA + path = StringValuePtr(*argv); + } + if (0 != (leaf = get_doc_leaf(doc, path))) { +- each_value(doc, leaf); ++ each_value(doc, leaf, self); + } + } + return Qnil; +--- a/ext/oj/oj.c ++++ b/ext/oj/oj.c +@@ -19,6 +19,8 @@ + #include "parse.h" + #include "rails.h" + ++#define MAX_INDENT 16 ++ + typedef struct _yesNoOpt { + VALUE sym; + char *attr; +@@ -638,7 +640,10 @@ static int parse_options_cb(VALUE k, VAL + case T_FIXNUM: + copts->dump_opts.indent_size = 0; + *copts->dump_opts.indent_str = '\0'; +- copts->indent = FIX2INT(v); ++ if (MAX_INDENT < FIX2INT(v)) { ++ rb_raise(rb_eArgError, "indent limited to %d characters.", MAX_INDENT); ++ } ++ copts->indent = FIX2INT(v); + break; + case T_STRING: + if (sizeof(copts->dump_opts.indent_str) <= (len = RSTRING_LEN(v))) { +--- a/ext/oj/parse.c ++++ b/ext/oj/parse.c +@@ -227,7 +227,7 @@ void oj_scanner_init(void) { + static void read_escaped_str(ParseInfo pi, const char *start) { + struct _buf buf; + const char *s; +- int cnt = (int)(pi->cur - start); ++ size_t cnt = pi->cur - start; + uint32_t code; + Val parent = stack_peek(&pi->stack); + +@@ -502,7 +502,7 @@ static void read_num(ParseInfo pi) { + // A trailing . is not a valid decimal but if encountered allow it + // except when mimicking the JSON gem or in strict mode. + if (StrictMode == pi->options.mode || CompatMode == pi->options.mode) { +- int pos = (int)(pi->cur - ni.str); ++ size_t pos = pi->cur - ni.str; + + if (1 == pos || (2 == pos && ni.neg)) { + oj_set_error_at(pi, oj_parse_error_class, __FILE__, __LINE__, "not a number"); +--- a/ext/oj/parser.c ++++ b/ext/oj/parser.c +@@ -668,6 +668,10 @@ static void parse(ojParser p, const byte + p->cur = b - json; + p->funcs[p->stack[p->depth]].open_object(p); + p->depth++; ++ if ((int)sizeof(p->stack) <= p->depth) { ++ parse_error(p, "too deeply nested"); ++ break; ++ } + p->stack[p->depth] = OBJECT_FUN; + p->map = key1_map; + break; +@@ -690,6 +694,10 @@ static void parse(ojParser p, const byte + p->cur = b - json; + p->funcs[p->stack[p->depth]].open_array(p); + p->depth++; ++ if ((int)sizeof(p->stack) <= p->depth) { ++ parse_error(p, "too deeply nested"); ++ break; ++ } + p->stack[p->depth] = ARRAY_FUN; + p->map = value_map; + break; +@@ -1365,8 +1373,14 @@ static VALUE parser_missing(int argc, VA + * Returns the result according to the delegate of the parser. + */ + static VALUE parser_parse(VALUE self, VALUE json) { +- ojParser p; +- const byte *ptr = (const byte *)StringValuePtr(json); ++ ojParser p; ++ volatile VALUE json_copy; ++ const byte *ptr; ++ ++ Check_Type(json, T_STRING); ++ json_copy = rb_str_dup(json); ++ rb_str_freeze(json_copy); ++ ptr = (const byte *)StringValuePtr(json_copy); + + TypedData_Get_Struct(self, struct _ojParser, &oj_parser_type, p); + +--- a/ext/oj/usual.c ++++ b/ext/oj/usual.c +@@ -63,7 +63,7 @@ static VALUE form_attr(const char *str, + memcpy(b + 1, str, len); + b[len + 1] = '\0'; + +- id = rb_intern3(buf, len + 1, oj_utf8_encoding); ++ id = rb_intern3(b, len + 1, oj_utf8_encoding); + OJ_R_FREE(b); + return id; + } +@@ -200,7 +200,10 @@ static void push_key(ojParser p) { + d->ktail = d->khead + pos; + d->kend = d->khead + cap; + } +- d->ktail->len = klen; ++ if (32000 < klen) { ++ rb_raise(oj_json_parser_error_class, "Key too long. Keys are limited to 32,000 bytes."); ++ } ++ d->ktail->len = (int16_t)klen; + if (klen < sizeof(d->ktail->buf)) { + memcpy(d->ktail->buf, key, klen); + d->ktail->buf[klen] = '\0'; +@@ -619,12 +622,16 @@ static void dfree(ojParser p) { + Usual d = (Usual)p->ctx; + + cache_free(d->str_cache); ++ d->str_cache = NULL; + cache_free(d->attr_cache); ++ d->attr_cache = NULL; + if (NULL != d->sym_cache) { + cache_free(d->sym_cache); ++ d->sym_cache = NULL; + } + if (NULL != d->class_cache) { + cache_free(d->class_cache); ++ d->class_cache = NULL; + } + OJ_R_FREE(d->vhead); + OJ_R_FREE(d->chead); +@@ -651,6 +658,12 @@ static void mark(ojParser p) { + if (NULL != d->class_cache) { + cache_mark(d->class_cache); + } ++ if (Qnil != d->hash_class) { ++ rb_gc_mark(d->hash_class); ++ } ++ if (Qnil != d->array_class) { ++ rb_gc_mark(d->array_class); ++ } + for (vp = d->vhead; vp < d->vtail; vp++) { + if (Qundef != *vp) { + rb_gc_mark(*vp); +@@ -1061,10 +1074,10 @@ static VALUE opt_symbol_keys_set(ojParse + if (NULL != d->sym_cache) { + cache_free(d->sym_cache); + d->sym_cache = NULL; ++ d->key_cache = NULL; + } +- if (!d->cache_keys) { +- d->get_key = str_key; +- } ++ d->cache_keys = false; ++ d->get_key = str_key; + } + return (NULL != d->sym_cache) ? Qtrue : Qfalse; + } +--- a/ext/oj/saj2.c ++++ b/ext/oj/saj2.c +@@ -547,10 +547,9 @@ static void mark(ojParser p) { + if (Qnil != d->handler) { + rb_gc_mark(d->handler); + } +- if (!d->cache_keys) { +- for (kp = d->keys; kp < d->tail; kp++) { +- rb_gc_mark(*kp); +- } ++ /* CVE-2026-54902: always mark stack keys; cached keys are also live VALUEs */ ++ for (kp = d->keys; kp < d->tail; kp++) { ++ rb_gc_mark(*kp); + } + } + diff -Nru ruby-oj-3.16.3/debian/patches/series ruby-oj-3.16.3/debian/patches/series --- ruby-oj-3.16.3/debian/patches/series 2023-12-18 20:43:34.000000000 +0000 +++ ruby-oj-3.16.3/debian/patches/series 2026-09-25 01:27:32.000000000 +0000 @@ -1,2 +1,4 @@ 01_dont_mess_with_loadpath.patch 03_find_test_helper.patch +CVE-2026-oj-security-ec368db.patch +CVE-2026-oj-security-bbde91a.patch diff -Nru ruby-oj-3.16.3/debian/salsa-ci.yml ruby-oj-3.16.3/debian/salsa-ci.yml --- ruby-oj-3.16.3/debian/salsa-ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ ruby-oj-3.16.3/debian/salsa-ci.yml 2026-09-25 01:27:32.000000000 +0000 @@ -0,0 +1,4 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml