Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,34 @@ def test_unicode_funcnames(self):
self.assertEqual(count, 1)
self.assert_samples_equal(samples, collector)

def test_surrogates_in_frame_names(self):
cases = [
("\udce9", r"\udce9"), # A surrogate-escaped filename byte.
("\uddd0", r"\uddd0"), # The surrogate from gh-160601.
("\ud800", r"\ud800"),
("\udfff", r"\udfff"),
("\ud800\udfff", r"\ud800\udfff"),
("caf\xe9\udce9\U0001f40d", "caf\xe9\\udce9\U0001f40d"),
]
for name, escaped_name in cases:
with self.subTest(name=name):
frames = [
make_frame(f"/path/{name}.py", 42, f"func_{name}"),
make_frame("test.py", 1, "main"),
]
samples = [[make_interpreter(0, [make_thread(1, frames)])]] * 3
collector, count = self.roundtrip(samples)
self.assertEqual(count, 3)

expected_frames = [
make_frame(f"/path/{escaped_name}.py", 42,
f"func_{escaped_name}"),
frames[1],
]
expected = [[make_interpreter(
0, [make_thread(1, expected_frames)])]] * 3
self.assert_samples_equal(expected, collector)

def test_special_char_filenames(self):
"""Filenames with special characters roundtrip exactly."""
filenames = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a :exc:`UnicodeEncodeError` in :mod:`profiling.sampling` binary mode
when a sampled filename or function name contains surrogate characters.
These characters are now written as backslash escapes in the binary file.
13 changes: 9 additions & 4 deletions Modules/_remote_debugging/binary_io_writer.c
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,15 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)
}
}

Py_ssize_t str_len;
const char *str_data = PyUnicode_AsUTF8AndSize(string, &str_len);
if (!str_data) {
/* Frame names can contain surrogates (e.g. from undecodable filenames).
* Escape them to keep the string table valid UTF-8. */
PyObject *encoded = PyUnicode_AsEncodedString(string, "utf-8", "backslashreplace");
if (encoded == NULL) {
return -1;
}
Py_ssize_t str_len = PyBytes_GET_SIZE(encoded);
if ((uintmax_t)str_len > UINT32_MAX) {
Py_DECREF(encoded);
writer->state = BINARY_WRITER_LIMIT_REACHED;
PyErr_Format(PyExc_OverflowError,
"string length %zd exceeds binary format maximum %u",
Expand All @@ -408,10 +411,12 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)

char *str_copy = PyMem_Malloc(str_len + 1);
if (!str_copy) {
Py_DECREF(encoded);
PyErr_NoMemory();
return -1;
}
memcpy(str_copy, str_data, str_len + 1);
memcpy(str_copy, PyBytes_AS_STRING(encoded), str_len + 1);
Py_DECREF(encoded);

*index = (uint32_t)writer->string_count;

Expand Down
Loading