Skip to content

Commit 00164c5

Browse files
danylo1999javanlacerda
authored andcommitted
Add support for /u{} and /x in unicode minimizer.
1 parent 6121550 commit 00164c5

2 files changed

Lines changed: 111 additions & 12 deletions

File tree

src/clusterfuzz/_internal/bot/minimizer/unicode_minimizer.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@
2020
from . import minimizer
2121
from . import utils
2222

23-
UNICODE_TOKEN_PATTERN = rb'(\\u[0-9a-fA-F]{4})'
23+
UNICODE_TOKEN_PATTERN = (
24+
rb'('
25+
rb'\\u[0-9a-fA-F]{4}' # \uXXXX format
26+
rb'|'
27+
rb'\\u\{[0-9a-fA-F]+\}' # \u{...} format
28+
rb'|'
29+
rb'\\x[0-9a-fA-F]{2}' # \xXX format
30+
rb')')
2431

2532

2633
def is_unicode_escape(s):
27-
"""Returns true if string matches \\uXXXX pattern."""
34+
"""Returns true if string matches \\uXXXX, \\u{...}, or \\xXX pattern."""
2835

2936
return re.fullmatch(UNICODE_TOKEN_PATTERN, s)
3037

@@ -39,10 +46,22 @@ def split_and_decode_unicode_literal(s):
3946
for token in intermediate_tokens:
4047
tokens.append(token)
4148
if re.fullmatch(UNICODE_TOKEN_PATTERN, token):
42-
hex_code = token[2:]
43-
decoded_char = int(hex_code, 16)
44-
# JS engines require UTF-8 encoding
45-
tokens.append(chr(decoded_char).encode('utf-8'))
49+
try:
50+
hex_code = b''
51+
if token.startswith(b'\\u{'):
52+
hex_code = token[3:-1]
53+
elif token.startswith(b'\\u') or token.startswith(b'\\x'):
54+
hex_code = token[2:]
55+
56+
decoded_char = int(hex_code, 16)
57+
# JS engines require UTF-8 encoding
58+
tokens.append(chr(decoded_char).encode('utf-8'))
59+
60+
except (ValueError, OverflowError):
61+
# If decoding fails, just don't append the replacement token.
62+
# The original escape is already in `tokens`.
63+
pass
64+
4665
return tokens
4766

4867

src/clusterfuzz/_internal/tests/core/bot/minimizer/unicode_minimizer_test.py

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,23 @@
2020

2121

2222
def replace_unicode(data):
23+
"""Helper function to replace all valid unicode/hex escapes."""
2324

2425
def replacer(match):
25-
hex_code = match.group(1)
26-
code_point = int(hex_code, 16)
27-
# JS engines require UTF-8 encoded input.
28-
return chr(code_point).encode('utf-8')
29-
30-
pattern = rb'\\u([0-9a-fA-F]{4})'
26+
hex_code = match.group(1) or match.group(2) or match.group(3)
27+
if not hex_code:
28+
return match.group(0)
29+
30+
try:
31+
code_point = int(hex_code, 16)
32+
# JS engines require UTF-8 encoded input.
33+
# This will fail for code_point > 0x10FFFF
34+
return chr(code_point).encode('utf-8')
35+
except (ValueError, OverflowError):
36+
# If it's an invalid code point, return the original escape.
37+
return match.group(0)
38+
39+
pattern = rb'\\u\{([0-9a-fA-F]+)\}|\\u([0-9a-fA-F]{4})|\\x([0-9a-fA-F]{2})'
3140
return re.sub(pattern, replacer, data)
3241

3342

@@ -108,3 +117,74 @@ def test_minimize_not_buggy_unicode_escape_multi_unicode(self):
108117

109118
self.assertEqual(
110119
result, b'text E text G some text H some other text I yet another text')
120+
121+
def test_minimize_buggy_hex_escape(self):
122+
"""Program crashes because of \\x41."""
123+
data = b'text \\x41 text' # \x41 = 'A'
124+
self.crash_programs.append(data)
125+
self.no_crash_programs.append(replace_unicode(data)) # b'text A text'
126+
127+
result = self._minimizer.minimize(data)
128+
self.assertEqual(result, b'text \\x41 text')
129+
130+
def test_minimize_not_buggy_hex_escape(self):
131+
"""Program crashes, but not because of \\x41."""
132+
data = b'text \\x41 text'
133+
self.crash_programs.append(data)
134+
self.crash_programs.append(replace_unicode(data)) # b'text A text'
135+
136+
result = self._minimizer.minimize(data)
137+
self.assertEqual(result, b'text A text')
138+
139+
def test_minimize_buggy_extended_unicode_escape(self):
140+
"""Program crashes because of \\u{42}."""
141+
data = b'text \\u{42} text' # \u{42} = 'B'
142+
self.crash_programs.append(data)
143+
self.no_crash_programs.append(replace_unicode(data)) # b'text B text'
144+
145+
result = self._minimizer.minimize(data)
146+
self.assertEqual(result, b'text \\u{42} text')
147+
148+
def test_minimize_not_buggy_extended_unicode_escape(self):
149+
"""Program crashes, but not because of \\u{42}."""
150+
data = b'text \\u{42} text'
151+
self.crash_programs.append(data)
152+
self.crash_programs.append(replace_unicode(data)) # b'text B text'
153+
154+
result = self._minimizer.minimize(data)
155+
self.assertEqual(result, b'text B text')
156+
157+
def test_minimize_buggy_all_escapes(self):
158+
"""Program crashes because of one of the mixed escapes."""
159+
data = b'A \\u0041 B \\x42 C \\u{43} D' # A, B, C
160+
self.crash_programs.append(data)
161+
self.no_crash_programs.append(replace_unicode(data)) # b'A A B B C C D'
162+
163+
result = self._minimizer.minimize(data)
164+
self.assertEqual(result, b'A \\u0041 B \\x42 C \\u{43} D')
165+
166+
def test_minimize_not_buggy_all_escapes(self):
167+
"""Program crashes, but not because of any of the mixed escapes."""
168+
data = b'A \\u0041 B \\x42 C \\u{43} D' # A, B, C
169+
self.crash_programs.append(data)
170+
self.crash_programs.append(replace_unicode(data)) # b'A A B B C C D'
171+
172+
result = self._minimizer.minimize(data)
173+
self.assertEqual(result, b'A A B B C C D')
174+
175+
def test_minimize_invalid_unicode_escape(self):
176+
"""Minimizer should not replace invalid escape sequences."""
177+
# \\u{110000} is outside the valid Unicode range (max 0x10FFFF)
178+
data = b'text \\u{110000} text'
179+
self.crash_programs.append(data)
180+
181+
# replace_unicode will fail to decode and return the original string
182+
replaced_data = replace_unicode(data)
183+
self.assertEqual(data, replaced_data)
184+
185+
# Add the (un)replaced data to crash_programs
186+
self.crash_programs.append(replaced_data)
187+
188+
result = self._minimizer.minimize(data)
189+
190+
self.assertEqual(result, data)

0 commit comments

Comments
 (0)