From: "Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)" <hthakar@cisco.com>
To: openembedded-devel@lists.openembedded.org
Cc: xe-linux-external@cisco.com, Hetvi Thakar <hthakar@cisco.com>
Subject: [meta-python][scarthgap][PATCH 4/4] python3-ujson: Fix CVE-2026-54911
Date: Wed, 19 Aug 2026 22:16:30 -0700 [thread overview]
Message-ID: <20260820051630.63383-4-hthakar@cisco.com> (raw)
In-Reply-To: <20260820051630.63383-1-hthakar@cisco.com>
From: Hetvi Thakar <hthakar@cisco.com>
This patch applies the upstream fix referenced in [2], using the
commit shown in [1].
[1] https://github.com/ultrajson/ultrajson/commit/169eaf36b1116fece5034ee79a7a0ef3f6deedcf
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-54911
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
.../python/python3-ujson/CVE-2026-54911.patch | 267 ++++++++++++++++++
.../python/python3-ujson_5.9.0.bb | 1 +
2 files changed, 268 insertions(+)
create mode 100644 meta-python/recipes-devtools/python/python3-ujson/CVE-2026-54911.patch
diff --git a/meta-python/recipes-devtools/python/python3-ujson/CVE-2026-54911.patch b/meta-python/recipes-devtools/python/python3-ujson/CVE-2026-54911.patch
new file mode 100644
index 0000000000..c7c14066d4
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-ujson/CVE-2026-54911.patch
@@ -0,0 +1,267 @@
+From 92a7b67d7b6155c2e3bc225fa0238ea35249bb36 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Br=C3=A9nainn=20Woodsend?= <bwoodsend@gmail.com>
+Date: Fri, 24 Apr 2026 21:59:45 +0100
+Subject: [PATCH] More UTF-8 validation for ujson.dumps(b"...",
+ reject_bytes=False)
+
+* Fix off by one errors in detecting end of string mid sequence
+* Add missing check for codepoints > max unicode
+* Add missing check for bad continuation bytes
+
+CVE: CVE-2026-54911
+Upstream-Status: Backport [https://github.com/ultrajson/ultrajson/commit/169eaf36b1116fece5034ee79a7a0ef3f6deedcf]
+
+Backport Changes:
+- Adjusted source paths for ujson 5.9.0's pre-src-layout tree.
+- Relocated regression tests to the matching section of the 5.9.0 test suite.
+- Added the `random` import required by the backported fuzz regression test;
+ newer upstream already imports it, while ujson 5.9.0 does not.
+
+(cherry picked from commit 169eaf36b1116fece5034ee79a7a0ef3f6deedcf)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ lib/ultrajsondec.c | 6 ++--
+ lib/ultrajsonenc.c | 46 ++++++++++++++++++++++-----
+ tests/test_ujson.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 118 insertions(+), 10 deletions(-)
+
+diff --git a/lib/ultrajsondec.c b/lib/ultrajsondec.c
+index bccb0aa..5583376 100644
+--- a/lib/ultrajsondec.c
++++ b/lib/ultrajsondec.c
+@@ -531,7 +531,7 @@ static FASTCALL_ATTR JSOBJ FASTCALL_MSVC decode_string ( struct DecoderState *ds
+ return SetError(ds, -1, "Invalid octet in UTF-8 sequence when decoding 'string'");
+ }
+ ucs |= (*inputOffset++) & 0x3f;
+- if (ucs < 0x80) return SetError (ds, -1, "Overlong 2 byte UTF-8 sequence detected when decoding 'string'");
++ if (ucs < 0x80) return SetError (ds, -1, "Overlong 2-byte UTF-8 sequence detected when decoding 'string'");
+ *(escOffset++) = (JSUINT32) ucs;
+ break;
+ }
+@@ -554,7 +554,7 @@ static FASTCALL_ATTR JSOBJ FASTCALL_MSVC decode_string ( struct DecoderState *ds
+ ucs |= oct & 0x3f;
+ }
+
+- if (ucs < 0x800) return SetError (ds, -1, "Overlong 3 byte UTF-8 sequence detected when encoding string");
++ if (ucs < 0x800) return SetError (ds, -1, "Overlong 3-byte UTF-8 sequence detected when encoding string");
+ *(escOffset++) = (JSUINT32) ucs;
+ break;
+ }
+@@ -577,7 +577,7 @@ static FASTCALL_ATTR JSOBJ FASTCALL_MSVC decode_string ( struct DecoderState *ds
+ ucs |= oct & 0x3f;
+ }
+
+- if (ucs < 0x10000) return SetError (ds, -1, "Overlong 4 byte UTF-8 sequence detected when decoding 'string'");
++ if (ucs < 0x10000) return SetError (ds, -1, "Overlong 4-byte UTF-8 sequence detected when decoding 'string'");
+
+ *(escOffset++) = (JSUINT32) ucs;
+ break;
+diff --git a/lib/ultrajsonenc.c b/lib/ultrajsonenc.c
+index 0f9fde3..5bafd52 100644
+--- a/lib/ultrajsonenc.c
++++ b/lib/ultrajsonenc.c
+@@ -347,17 +347,24 @@ static int Buffer_EscapeStringValidated (JSOBJ obj, JSONObjectEncoder *enc, cons
+ continue;
+ }
+
++ // https://en.wikipedia.org/wiki/UTF-8#Description
+ case 2:
+ {
+ JSUTF32 in;
+ JSUTF16 in16;
+
+- if (end - io < 1)
++ if (end - io < 2)
+ {
+ enc->offset += (of - enc->offset);
+ SetError (obj, enc, "Unterminated UTF-8 sequence when encoding string");
+ return FALSE;
+ }
++ if ((io[1] & 0xc0) != 0x80)
++ {
++ enc->offset += (of - enc->offset);
++ SetError (obj, enc, "Invalid continuation byte in 2-byte UTF-8 sequence detected when encoding string");
++ return FALSE;
++ }
+
+ memcpy(&in16, io, sizeof(JSUTF16));
+ in = (JSUTF32) in16;
+@@ -371,7 +378,7 @@ static int Buffer_EscapeStringValidated (JSOBJ obj, JSONObjectEncoder *enc, cons
+ if (ucs < 0x80)
+ {
+ enc->offset += (of - enc->offset);
+- SetError (obj, enc, "Overlong 2 byte UTF-8 sequence detected when encoding string");
++ SetError (obj, enc, "Overlong 2-byte UTF-8 sequence detected when encoding string");
+ return FALSE;
+ }
+
+@@ -385,13 +392,26 @@ static int Buffer_EscapeStringValidated (JSOBJ obj, JSONObjectEncoder *enc, cons
+ JSUTF16 in16;
+ JSUINT8 in8;
+
+- if (end - io < 2)
++ if (end - io < 3)
+ {
+ enc->offset += (of - enc->offset);
+ SetError (obj, enc, "Unterminated UTF-8 sequence when encoding string");
+ return FALSE;
+ }
+-
++ if ((io[1] & 0xc0) != 0x80 || (io[2] & 0xc0) != 0x80)
++ {
++ enc->offset += (of - enc->offset);
++ SetError (obj, enc, "Invalid continuation byte in 3-byte UTF-8 sequence detected when encoding string");
++ return FALSE;
++ }
++ // Under normal UTF-8 decoding rules, UTF-16 surrogates should also be disallowed
++ // but in JSON, they're special cased and rewritten later as \udc7f.
++ // if ((JSUINT8) io[0] == 0xed && (JSUINT8) io[1] >= 0xa0)
++ // {
++ // enc->offset += (of - enc->offset);
++ // SetError (obj, enc, "Illegal UTF-16 surrogate in 3-byte UTF-8 sequence detected when encoding string");
++ // return FALSE;
++ // }
+ memcpy(&in16, io, sizeof(JSUTF16));
+ memcpy(&in8, io + 2, sizeof(JSUINT8));
+ #ifdef __LITTLE_ENDIAN__
+@@ -407,7 +427,7 @@ static int Buffer_EscapeStringValidated (JSOBJ obj, JSONObjectEncoder *enc, cons
+ if (ucs < 0x800)
+ {
+ enc->offset += (of - enc->offset);
+- SetError (obj, enc, "Overlong 3 byte UTF-8 sequence detected when encoding string");
++ SetError (obj, enc, "Overlong 3-byte UTF-8 sequence detected when encoding string");
+ return FALSE;
+ }
+
+@@ -418,12 +438,24 @@ static int Buffer_EscapeStringValidated (JSOBJ obj, JSONObjectEncoder *enc, cons
+ {
+ JSUTF32 in;
+
+- if (end - io < 3)
++ if (end - io < 4)
+ {
+ enc->offset += (of - enc->offset);
+ SetError (obj, enc, "Unterminated UTF-8 sequence when encoding string");
+ return FALSE;
+ }
++ if ((io[1] & 0xc0) != 0x80 || (io[2] & 0xc0) != 0x80 || (io[3] & 0xc0) != 0x80)
++ {
++ enc->offset += (of - enc->offset);
++ SetError (obj, enc, "Invalid continuation byte in 4-byte UTF-8 sequence detected when encoding string");
++ return FALSE;
++ }
++ if (((JSUINT8) io[0] >= 0xf4 && (JSUINT8) io[1] >= 0x90) || (JSUINT8) io[0] >= 0xf5)
++ {
++ enc->offset += (of - enc->offset);
++ SetError (obj, enc, ">U+10FFFF in 4-byte UTF-8 sequence detected when encoding string");
++ return FALSE;
++ }
+
+ memcpy(&in, io, sizeof(JSUTF32));
+ #ifdef __LITTLE_ENDIAN__
+@@ -434,7 +466,7 @@ static int Buffer_EscapeStringValidated (JSOBJ obj, JSONObjectEncoder *enc, cons
+ if (ucs < 0x10000)
+ {
+ enc->offset += (of - enc->offset);
+- SetError (obj, enc, "Overlong 4 byte UTF-8 sequence detected when encoding string");
++ SetError (obj, enc, "Overlong 4-byte UTF-8 sequence detected when encoding string");
+ return FALSE;
+ }
+
+diff --git a/tests/test_ujson.py b/tests/test_ujson.py
+index ccff37f..9024dac 100644
+--- a/tests/test_ujson.py
++++ b/tests/test_ujson.py
+@@ -5,6 +5,7 @@ import io
+ import json
+ import math
+ import os.path
++import random
+ import re
+ import subprocess
+ import sys
+@@ -1053,6 +1053,81 @@ def test_reject_bytes_false():
+ assert ujson.dumps(data, reject_bytes=False) == '{"a":"b"}'
+
+
++@pytest.mark.parametrize(
++ "codepoint",
++ [0x0, 0x7F, 0x80, 0x7FF, 0x800, 0xFFFF, 0x10000, 0x10FFFF],
++)
++def test_reject_bytes_false_codepoint_boundaries(codepoint):
++ char = chr(codepoint)
++ assert ujson.loads(ujson.dumps(char.encode(), reject_bytes=False)) == char
++
++
++@pytest.mark.parametrize(
++ "value, error",
++ [
++ # Bad start bytes
++ (b"\xfd", "Unsupported UTF-8 sequence length when encoding string"),
++ (b"\xfc:", "Unsupported UTF-8 sequence length when encoding string"),
++ (b"U>\xfb", "Unsupported UTF-8 sequence length when encoding string"),
++ (b"\\\xf8\x98\t", "Unsupported UTF-8 sequence length when encoding string"),
++ (b"\x9b", "'utf-8' codec can't decode byte 0x9b in position 1:"),
++ (b"B\x8a", "'utf-8' codec can't decode byte 0x8a in position 2:"),
++ # Bad continuation bytes (any non-start byte not matching 0b10xx_xxxx)
++ (b"\xcf\x13", "Invalid continuation byte in 2-byte UTF-8 sequence"),
++ (b"\xcfa", "Invalid continuation byte in 2-byte UTF-8 sequence"),
++ (b"\xd8\xcf\xd3", "Invalid continuation byte in 2-byte UTF-8 sequence"),
++ (b"\xd2\t\x8b\x84", "Invalid continuation byte in 2-byte UTF-8 sequence"),
++ (b"\xe2\x17\xce", "Invalid continuation byte in 3-byte UTF-8 sequence"),
++ (b"\xe2a\x17\xce", "Invalid continuation byte in 3-byte UTF-8 sequence"),
++ (b"\xe2\x17a", "Invalid continuation byte in 3-byte UTF-8 sequence"),
++ (b"\xe0\x9c\xc6\xde", "Invalid continuation byte in 3-byte UTF-8 sequence"),
++ (b"\xf0H\xce\x9b", "Invalid continuation byte in 4-byte UTF-8 sequence"),
++ (b"\xf0\xce4\x9b", "Invalid continuation byte in 4-byte UTF-8 sequence"),
++ # Truncated UTF-8 sequences
++ (b"\xc3", "Unterminated UTF-8 sequence when encoding string"),
++ (b"\x8c$\xe3", "Unterminated UTF-8 sequence when encoding string"),
++ (b"\x8c\xe3$", "Unterminated UTF-8 sequence when encoding string"),
++ (b"=\x8c\xe36", "Unterminated UTF-8 sequence when encoding string"),
++ (b"\x08\x11\xe3", "Unterminated UTF-8 sequence when encoding string"),
++ (b"\xf0\x90\x94", "Unterminated UTF-8 sequence when encoding string"),
++ # Small codepoints using longer byte sequences than they need
++ (b"\xc0\xa2", "Overlong 2-byte UTF-8 sequence"),
++ (b"A\xc1\x9c", "Overlong 2-byte UTF-8 sequence"),
++ (b"\xc1\xbf", "Overlong 2-byte UTF-8 sequence"),
++ (b"N\xc0\xb4\xb4", "Overlong 2-byte UTF-8 sequence"),
++ (b"\xe0\x9d\xb3", "Overlong 3-byte UTF-8 sequence"),
++ (b"E\xe0\x9e\x8b", "Overlong 3-byte UTF-8 sequence"),
++ (b"\xe0\x9f\xbf", "Overlong 3-byte UTF-8 sequence"),
++ (b"\xf0\x80\x80\x80", "Overlong 4-byte UTF-8 sequence"),
++ (b"\xf0\x8f\xbf\xbf", "Overlong 4-byte UTF-8 sequence"),
++ (b"\xf0\x85\xa7\xbd", "Overlong 4-byte UTF-8 sequence"),
++ # Codepoints above unicode max
++ (b"\xf4\x90\x80\x80", r">U\+10FFFF in 4-byte UTF-8 sequence"),
++ (b"\xf7\x8f\x99\x90", r">U\+10FFFF in 4-byte UTF-8 sequence"),
++ (b"\xf7\xbf\xbf\xbf", r">U\+10FFFF in 4-byte UTF-8 sequence"),
++ ],
++)
++def test_dump_bytes_invalid_utf8(value, error):
++ with pytest.raises((OverflowError, UnicodeDecodeError), match=error):
++ ujson.dumps(bytes(value), reject_bytes=False)
++
++
++def test_dump_bytes_fuzz():
++ # ujson.dumps(..., reject_bytes=False) should accept or reject the same byte
++ # sequences as b"...".decode() when unpaired surrogates are allowed
++ for seed in range(10000):
++ r = random.Random(seed)
++ a = r.randbytes(r.randrange(8))
++ try:
++ expected = a.decode(errors="surrogatepass")
++ except UnicodeDecodeError:
++ with pytest.raises((UnicodeDecodeError, OverflowError)):
++ ujson.dumps(a, reject_bytes=False)
++ else:
++ actual = ujson.loads(ujson.dumps(a, reject_bytes=False))
++ assert actual == expected, (a, [bin(i) for i in a], actual, expected)
++
++
+ def test_encode_special_keys():
+ data = {None: 0, True: 1, False: 2}
+ assert ujson.dumps(data) == '{"null":0,"true":1,"false":2}'
+--
+2.35.6
+
diff --git a/meta-python/recipes-devtools/python/python3-ujson_5.9.0.bb b/meta-python/recipes-devtools/python/python3-ujson_5.9.0.bb
index ed08ede1d9..bd1f06acf9 100644
--- a/meta-python/recipes-devtools/python/python3-ujson_5.9.0.bb
+++ b/meta-python/recipes-devtools/python/python3-ujson_5.9.0.bb
@@ -14,6 +14,7 @@ SRC_URI += " \
file://CVE-2026-32875.patch \
file://CVE-2026-32874.patch \
file://CVE-2026-44660.patch \
+ file://CVE-2026-54911.patch \
"
DEPENDS += "python3-setuptools-scm-native"
--
2.35.6
prev parent reply other threads:[~2026-08-20 5:19 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-20 5:16 [meta-python][scarthgap][PATCH 1/4] python3-ujson: Fix CVE-2026-32875 Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-08-20 5:16 ` [meta-python][scarthgap][PATCH 2/4] python3-ujson: Fix CVE-2026-32874 Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-08-20 5:16 ` [meta-python][scarthgap][PATCH 3/4] python3-ujson: Fix CVE-2026-44660 Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-08-20 5:16 ` Hetvi Thakar -X (hthakar - E INFOCHIPS PRIVATE LIMITED at Cisco) [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260820051630.63383-4-hthakar@cisco.com \
--to=hthakar@cisco.com \
--cc=openembedded-devel@lists.openembedded.org \
--cc=xe-linux-external@cisco.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox