From 232a5155ad3f9cb74d49f8e518ae846857d57568 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 5 Oct 2026 17:54:22 +0100 Subject: [PATCH 1/2] gh-156545: Fall back to Python JSON encoding for deep flamegraphs --- Lib/profiling/sampling/stack_collector.py | 11 ++++++++--- .../test_sampling_profiler/test_collectors.py | 3 ++- .../2026-10-05-18-00-00.gh-issue-156545.z1mQp7.rst | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-05-18-00-00.gh-issue-156545.z1mQp7.rst diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index e420bb6d2e9b87..9f73fa0db8dccf 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -68,8 +68,8 @@ def export(self, filename): return True -# Bounded by the unwinder's maximum captured stack depth (MAX_FRAMES). -_FLAMEGRAPH_RECURSION_MARGIN = 2000 +# Allow for tree conversion and the dict/list frames in the Python JSON encoder. +_FLAMEGRAPH_RECURSION_MARGIN = 6000 class FlamegraphCollector(StackTraceCollector): @@ -498,7 +498,12 @@ def _get_source_lines(self, func): return None def _create_flamegraph_html(self, data): - data_json = json.dumps(data) + try: + data_json = json.dumps(data) + except RecursionError: + # The C encoder can exhaust the C stack independently of the + # Python recursion limit. iterencode() uses the Python encoder. + data_json = "".join(json.JSONEncoder().iterencode(data)) template_dir = importlib.resources.files(__package__) vendor_dir = template_dir / "_vendor" diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index eb58c29dd361d3..1d398a1f5d7a2a 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -8,7 +8,7 @@ import tempfile import unittest -from test.support import is_emscripten, set_recursion_limit +from test.support import is_emscripten, run_with_limited_c_stack, set_recursion_limit try: import _remote_debugging # noqa: F401 @@ -611,6 +611,7 @@ def test_flamegraph_collector_empty_export_fails(self): self.assertFalse(export_ok) self.assertEqual(os.path.getsize(flamegraph_out.name), 0) + @run_with_limited_c_stack(size=1024 * 1024) def test_flamegraph_deep_stack_export(self): flamegraph_out = tempfile.NamedTemporaryFile( suffix=".html", delete=False diff --git a/Misc/NEWS.d/next/Library/2026-10-05-18-00-00.gh-issue-156545.z1mQp7.rst b/Misc/NEWS.d/next/Library/2026-10-05-18-00-00.gh-issue-156545.z1mQp7.rst new file mode 100644 index 00000000000000..dd67abce09da80 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-05-18-00-00.gh-issue-156545.z1mQp7.rst @@ -0,0 +1,2 @@ +Fix deep flamegraph exports on platforms with small C stacks by falling back +to the Python JSON encoder when the C encoder runs out of stack space. From abef1161c6ad6746fe71b98d8dfee23561d5eed9 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 5 Oct 2026 17:54:22 +0100 Subject: [PATCH 2/2] gh-158539: Remove the VM region size requirement from Mach-O section lookup --- ...-10-05-18-00-01.gh-issue-158539.k8Lv2a.rst | 2 + Python/remote_debug.h | 69 +++---------------- 2 files changed, 13 insertions(+), 58 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-05-18-00-01.gh-issue-158539.k8Lv2a.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-05-18-00-01.gh-issue-158539.k8Lv2a.rst b/Misc/NEWS.d/next/Library/2026-10-05-18-00-01.gh-issue-158539.k8Lv2a.rst new file mode 100644 index 00000000000000..aadac290075abf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-05-18-00-01.gh-issue-158539.k8Lv2a.rst @@ -0,0 +1,2 @@ +Fix macOS runtime section lookup for remote debugging when virtual memory +region sizes differ from Mach-O segment file sizes. diff --git a/Python/remote_debug.h b/Python/remote_debug.h index a279cd9dfd0305..a4a3a010926a7b 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -297,7 +297,6 @@ _Py_RemoteDebug_CleanupProcHandle(proc_handle_t *handle) { static uintptr_t return_section_address64( const char* section, - mach_port_t proc_ref, uintptr_t base, void* map ) { @@ -307,11 +306,6 @@ return_section_address64( int cmd_cnt = 0; struct segment_command_64* cmd = map + sizeof(struct mach_header_64); - mach_vm_size_t size = 0; - mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t); - mach_vm_address_t address = (mach_vm_address_t)base; - vm_region_basic_info_data_64_t r_info; - mach_port_t object_name; uintptr_t vmaddr = 0; for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) { @@ -319,25 +313,8 @@ return_section_address64( vmaddr = cmd->vmaddr; } if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) { - while (cmd->filesize != size) { - address += size; - kern_return_t ret = mach_vm_region( - proc_ref, - &address, - &size, - VM_REGION_BASIC_INFO_64, - (vm_region_info_t)&r_info, // cppcheck-suppress [uninitvar] - &count, - &object_name - ); - if (ret != KERN_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, - "mach_vm_region failed while parsing 64-bit Mach-O binary " - "at base address 0x%lx (kern_return_t: %d)", - base, ret); - return 0; - } - } + // The section address only needs the image's ASLR slide. + // VM regions need not match the segment's on-disk size. int nsects = cmd->nsects; struct section_64* sec = (struct section_64*)( @@ -360,7 +337,6 @@ return_section_address64( static uintptr_t return_section_address32( const char* section, - mach_port_t proc_ref, uintptr_t base, void* map ) { @@ -370,11 +346,6 @@ return_section_address32( int cmd_cnt = 0; struct segment_command* cmd = map + sizeof(struct mach_header); - mach_vm_size_t size = 0; - mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_t); - mach_vm_address_t address = (mach_vm_address_t)base; - vm_region_basic_info_data_t r_info; - mach_port_t object_name; uintptr_t vmaddr = 0; for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) { @@ -382,25 +353,8 @@ return_section_address32( vmaddr = cmd->vmaddr; } if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__DATA") == 0) { - while (cmd->filesize != size) { - address += size; - kern_return_t ret = mach_vm_region( - proc_ref, - &address, - &size, - VM_REGION_BASIC_INFO, - (vm_region_info_t)&r_info, // cppcheck-suppress [uninitvar] - &count, - &object_name - ); - if (ret != KERN_SUCCESS) { - PyErr_Format(PyExc_RuntimeError, - "mach_vm_region failed while parsing 32-bit Mach-O binary " - "at base address 0x%lx (kern_return_t: %d)", - base, ret); - return 0; - } - } + // The section address only needs the image's ASLR slide. + // VM regions need not match the segment's on-disk size. int nsects = cmd->nsects; struct section* sec = (struct section*)( @@ -423,7 +377,6 @@ return_section_address32( static uintptr_t return_section_address_fat( const char* section, - mach_port_t proc_ref, uintptr_t base, void* map ) { @@ -473,11 +426,11 @@ return_section_address_fat( switch (hdr->magic) { case MH_MAGIC: case MH_CIGAM: - return return_section_address32(section, proc_ref, base, (void*)hdr); + return return_section_address32(section, base, (void*)hdr); case MH_MAGIC_64: case MH_CIGAM_64: - return return_section_address64(section, proc_ref, base, (void*)hdr); + return return_section_address64(section, base, (void*)hdr); default: PyErr_Format(PyExc_RuntimeError, @@ -496,7 +449,7 @@ return_section_address_fat( } static uintptr_t -search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_size_t size, mach_port_t proc_ref) +search_section_in_file(const char* secname, char* path, uintptr_t base) { int fd = open(path, O_RDONLY); if (fd == -1) { @@ -533,15 +486,15 @@ search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_ switch (magic) { case MH_MAGIC: case MH_CIGAM: - result = return_section_address32(secname, proc_ref, base, map); + result = return_section_address32(secname, base, map); break; case MH_MAGIC_64: case MH_CIGAM_64: - result = return_section_address64(secname, proc_ref, base, map); + result = return_section_address64(secname, base, map); break; case FAT_MAGIC: case FAT_CIGAM: - result = return_section_address_fat(secname, proc_ref, base, map); + result = return_section_address_fat(secname, base, map); break; default: PyErr_Format(PyExc_RuntimeError, @@ -644,7 +597,7 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s if (strncmp(filename, substr, strlen(substr)) == 0) { PyErr_Clear(); uintptr_t result = search_section_in_file( - secname, map_filename, address, size, proc_ref); + secname, map_filename, address); if (result != 0) { if (validator == NULL || validator(handle, result)) { return result;