Skip to content

Commit e322a18

Browse files
miss-islingtonencukousethmlarsonch4n3-yoonStanFromIreland
authored
[3.11] gh-149079: Fix O(n^2) canonical ordering in unicodedata.normalize() (GH-149080) (GH-150843) (#155158)
[3.12] gh-149079: Fix O(n^2) canonical ordering in unicodedata.normalize() (GH-149080) (GH-150843) Replace the insertion sort used for canonical ordering of combining characters with a hybrid approach: insertion sort for short runs (< 20) and counting sort for longer runs, reducing worst-case complexity from O(n^2) to O(n). This prevents denial of service via crafted Unicode strings with many combining characters in alternating CCC order. (cherry picked from commit 991224b) (cherry picked from commit d3ab945) Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Seth Larson <seth@python.org> Co-authored-by: ch4n3-yoon <ch4n3.yoon@gmail.com> Co-authored-by: Seokchan Yoon <13852925+ch4n3-yoon@users.noreply.github.com> Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
1 parent 8720383 commit e322a18

3 files changed

Lines changed: 151 additions & 26 deletions

File tree

Lib/test/test_unicodedata.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,34 @@ def test_issue10254(self):
202202
b = 'C\u0338' * 20 + '\xC7'
203203
self.assertEqual(self.db.normalize('NFC', a), b)
204204

205+
def test_long_combining_mark_run(self):
206+
# gh-149079: avoid quadratic canonical ordering.
207+
payload = "a" + ("\u0300\u0327" * 32)
208+
nfd = "a" + ("\u0327" * 32) + ("\u0300" * 32)
209+
nfc = "\u00e0" + ("\u0327" * 32) + ("\u0300" * 31)
210+
211+
self.assertEqual(self.db.normalize("NFD", payload), nfd)
212+
self.assertEqual(self.db.normalize("NFKD", payload), nfd)
213+
self.assertEqual(self.db.normalize("NFC", payload), nfc)
214+
self.assertEqual(self.db.normalize("NFKC", payload), nfc)
215+
216+
def test_combining_mark_run_fast_paths(self):
217+
# gh-149079: cover short runs and already-sorted long runs.
218+
short_payload = "a" + ("\u0300\u0327" * 9) + "\u0300"
219+
short_nfd = "a" + ("\u0327" * 9) + ("\u0300" * 10)
220+
short_nfc = "\u00e0" + ("\u0327" * 9) + ("\u0300" * 9)
221+
long_sorted = "a" + ("\u0327" * 30) + ("\u0300" * 30)
222+
long_sorted_nfc = "\u00e0" + ("\u0327" * 30) + ("\u0300" * 29)
223+
224+
self.assertEqual(self.db.normalize("NFD", short_payload), short_nfd)
225+
self.assertEqual(self.db.normalize("NFKD", short_payload), short_nfd)
226+
self.assertEqual(self.db.normalize("NFC", short_payload), short_nfc)
227+
self.assertEqual(self.db.normalize("NFKC", short_payload), short_nfc)
228+
self.assertEqual(self.db.normalize("NFD", long_sorted), long_sorted)
229+
self.assertEqual(self.db.normalize("NFKD", long_sorted), long_sorted)
230+
self.assertEqual(self.db.normalize("NFC", long_sorted), long_sorted_nfc)
231+
self.assertEqual(self.db.normalize("NFKC", long_sorted), long_sorted_nfc)
232+
205233
def test_issue29456(self):
206234
# Fix #29456
207235
u1176_str_a = '\u1100\u1176\u11a8'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a potential denial of service in :func:`unicodedata.normalize`. The
2+
canonical ordering step of Unicode normalization used a quadratic-time insertion
3+
sort for reordering combining characters, which could be exploited with
4+
crafted input containing many combining characters in non-canonical order.
5+
Replaced with a linear-time counting sort for long runs.

Modules/unicodedata.c

Lines changed: 118 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -493,19 +493,80 @@ get_decomp_record(PyObject *self, Py_UCS4 code,
493493
#define NCount (VCount*TCount)
494494
#define SCount (LCount*NCount)
495495

496+
/* Small combining runs are usually cheaper with insertion sort. */
497+
#define CANONICAL_ORDERING_COUNTING_SORT_THRESHOLD 20
498+
499+
static void
500+
canonical_ordering_sort_insertion(int kind, void *data,
501+
Py_ssize_t start, Py_ssize_t end)
502+
{
503+
for (Py_ssize_t i = start + 1; i < end; i++) {
504+
Py_UCS4 code = PyUnicode_READ(kind, data, i);
505+
unsigned char combining = _getrecord_ex(code)->combining;
506+
Py_ssize_t j = i;
507+
508+
while (j > start) {
509+
Py_UCS4 previous = PyUnicode_READ(kind, data, j - 1);
510+
if (_getrecord_ex(previous)->combining <= combining) {
511+
break;
512+
}
513+
PyUnicode_WRITE(kind, data, j, previous);
514+
j--;
515+
}
516+
if (j != i) {
517+
PyUnicode_WRITE(kind, data, j, code);
518+
}
519+
}
520+
}
521+
522+
static void
523+
canonical_ordering_sort_counting(int kind, void *data,
524+
Py_ssize_t start, Py_ssize_t end,
525+
Py_UCS4 *sortbuf)
526+
{
527+
Py_ssize_t counts[256] = {0};
528+
Py_ssize_t run_length = end - start;
529+
Py_ssize_t total = 0;
530+
531+
for (Py_ssize_t i = start; i < end; i++) {
532+
Py_UCS4 code = PyUnicode_READ(kind, data, i);
533+
unsigned char combining = _getrecord_ex(code)->combining;
534+
counts[combining]++;
535+
}
536+
537+
for (size_t i = 0; i < Py_ARRAY_LENGTH(counts); i++) {
538+
Py_ssize_t count = counts[i];
539+
counts[i] = total;
540+
total += count;
541+
}
542+
543+
/* Reuse counts[] as the next output slot for each CCC. */
544+
for (Py_ssize_t i = start; i < end; i++) {
545+
Py_UCS4 code = PyUnicode_READ(kind, data, i);
546+
unsigned char combining = _getrecord_ex(code)->combining;
547+
sortbuf[counts[combining]++] = code;
548+
}
549+
for (Py_ssize_t i = 0; i < run_length; i++) {
550+
PyUnicode_WRITE(kind, data, start + i, sortbuf[i]);
551+
}
552+
}
553+
496554
static PyObject*
497555
nfd_nfkd(PyObject *self, PyObject *input, int k)
498556
{
499557
PyObject *result;
500558
Py_UCS4 *output;
501559
Py_ssize_t i, o, osize;
502-
int kind;
503-
const void *data;
560+
int input_kind, result_kind;
561+
const void *input_data;
562+
void *result_data;
504563
/* Longest decomposition in Unicode 3.2: U+FDFA */
505564
Py_UCS4 stack[20];
506565
Py_ssize_t space, isize;
507566
int index, prefix, count, stackptr;
508567
unsigned char prev, cur;
568+
Py_UCS4 *sortbuf = NULL;
569+
Py_ssize_t sortbuflen = 0;
509570

510571
stackptr = 0;
511572
isize = PyUnicode_GET_LENGTH(input);
@@ -525,11 +586,11 @@ nfd_nfkd(PyObject *self, PyObject *input, int k)
525586
return NULL;
526587
}
527588
i = o = 0;
528-
kind = PyUnicode_KIND(input);
529-
data = PyUnicode_DATA(input);
589+
input_kind = PyUnicode_KIND(input);
590+
input_data = PyUnicode_DATA(input);
530591

531592
while (i < isize) {
532-
stack[stackptr++] = PyUnicode_READ(kind, data, i++);
593+
stack[stackptr++] = PyUnicode_READ(input_kind, input_data, i++);
533594
while(stackptr) {
534595
Py_UCS4 code = stack[--stackptr];
535596
/* Hangul Decomposition adds three characters in
@@ -594,35 +655,66 @@ nfd_nfkd(PyObject *self, PyObject *input, int k)
594655
PyMem_Free(output);
595656
if (!result)
596657
return NULL;
658+
597659
/* result is guaranteed to be ready, as it is compact. */
598-
kind = PyUnicode_KIND(result);
599-
data = PyUnicode_DATA(result);
660+
result_kind = PyUnicode_KIND(result);
661+
result_data = PyUnicode_DATA(result);
600662

601-
/* Sort canonically. */
663+
/* Sort each consecutive combining-character run canonically. */
602664
i = 0;
603-
prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
604-
for (i++; i < PyUnicode_GET_LENGTH(result); i++) {
605-
cur = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
606-
if (prev == 0 || cur == 0 || prev <= cur) {
607-
prev = cur;
665+
while (i < o) {
666+
Py_ssize_t run_length, run_start;
667+
int needs_sort = 0;
668+
669+
Py_UCS4 ch = PyUnicode_READ(result_kind, result_data, i);
670+
prev = _getrecord_ex(ch)->combining;
671+
if (prev == 0) {
672+
i++;
608673
continue;
609674
}
610-
/* Non-canonical order. Need to switch *i with previous. */
611-
o = i - 1;
612-
while (1) {
613-
Py_UCS4 tmp = PyUnicode_READ(kind, data, o+1);
614-
PyUnicode_WRITE(kind, data, o+1,
615-
PyUnicode_READ(kind, data, o));
616-
PyUnicode_WRITE(kind, data, o, tmp);
617-
o--;
618-
if (o < 0)
619-
break;
620-
prev = _getrecord_ex(PyUnicode_READ(kind, data, o))->combining;
621-
if (prev == 0 || prev <= cur)
675+
676+
run_start = i++;
677+
while (i < o) {
678+
Py_UCS4 ch = PyUnicode_READ(result_kind, result_data, i);
679+
cur = _getrecord_ex(ch)->combining;
680+
if (cur == 0) {
622681
break;
682+
}
683+
if (prev > cur) {
684+
needs_sort = 1;
685+
}
686+
prev = cur;
687+
i++;
688+
}
689+
if (!needs_sort) {
690+
continue;
691+
}
692+
693+
run_length = i - run_start;
694+
if (run_length < CANONICAL_ORDERING_COUNTING_SORT_THRESHOLD) {
695+
canonical_ordering_sort_insertion(result_kind, result_data,
696+
run_start, i);
697+
continue;
623698
}
624-
prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
699+
700+
if (run_length > sortbuflen) {
701+
Py_UCS4 *new_sortbuf = PyMem_Resize(sortbuf,
702+
Py_UCS4,
703+
run_length);
704+
if (new_sortbuf == NULL) {
705+
PyErr_NoMemory();
706+
PyMem_Free(sortbuf);
707+
Py_DECREF(result);
708+
return NULL;
709+
}
710+
sortbuf = new_sortbuf;
711+
sortbuflen = run_length;
712+
}
713+
714+
canonical_ordering_sort_counting(result_kind, result_data,
715+
run_start, i, sortbuf);
625716
}
717+
PyMem_Free(sortbuf);
626718
return result;
627719
}
628720

0 commit comments

Comments
 (0)