Skip to content

Commit 82b7d1f

Browse files
committed
gh-156545, gh-158539: Fix deep flamegraph export on small C stacks and macOS runtime lookup (#158874)
(cherry picked from commit 114de19) Include the C-stack test helper from main, introduced by ce5ae29, which the regression test requires but 3.15 does not yet provide.
1 parent 9fef40a commit 82b7d1f

6 files changed

Lines changed: 105 additions & 77 deletions

File tree

‎Lib/profiling/sampling/stack_collector.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def export(self, filename):
6868
return True
6969

7070

71-
# Bounded by the unwinder's maximum captured stack depth (MAX_FRAMES).
72-
_FLAMEGRAPH_RECURSION_MARGIN = 2000
71+
# Allow for tree conversion and the dict/list frames in the Python JSON encoder.
72+
_FLAMEGRAPH_RECURSION_MARGIN = 6000
7373

7474

7575
class FlamegraphCollector(StackTraceCollector):
@@ -498,7 +498,12 @@ def _get_source_lines(self, func):
498498
return None
499499

500500
def _create_flamegraph_html(self, data):
501-
data_json = json.dumps(data)
501+
try:
502+
data_json = json.dumps(data)
503+
except RecursionError:
504+
# The C encoder can exhaust the C stack independently of the
505+
# Python recursion limit. iterencode() uses the Python encoder.
506+
data_json = "".join(json.JSONEncoder().iterencode(data))
502507

503508
template_dir = importlib.resources.files(__package__)
504509
vendor_dir = template_dir / "_vendor"

‎Lib/test/support/__init__.py‎

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
4747
"requires_limited_api", "requires_specialization", "thread_unsafe",
4848
"skip_if_unlimited_stack_size", "skip_if_huge_c_stack",
49+
"run_with_limited_c_stack",
4950
# sys
5051
"MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
5152
"is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
@@ -2830,30 +2831,94 @@ def exceeds_recursion_limit():
28302831
return 150_000
28312832

28322833

2834+
def _has_huge_c_stack(depth):
2835+
"""Check that *depth* recursive calls cannot exhaust the C stack."""
2836+
try:
2837+
from _testinternalcapi import get_c_recursion_remaining
2838+
except ImportError:
2839+
# Fall back to checking for an unlimited stack size.
2840+
if is_emscripten or is_wasi or os.name == "nt":
2841+
return False
2842+
import resource
2843+
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
2844+
return soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
2845+
else:
2846+
remaining = get_c_recursion_remaining()
2847+
# A negative value means integer overflow in the estimate
2848+
# (e.g. with an unlimited RLIMIT_STACK). The estimate is based on
2849+
# the size of the interpreter loop frame, so it is only a lower
2850+
# bound for recursion with smaller C frames.
2851+
return remaining >= depth or remaining < 0
2852+
2853+
28332854
def skip_if_huge_c_stack(depth=150_000):
28342855
"""Skip decorator for tests which cannot overflow the C stack.
28352856
28362857
Tests exhausting the C stack with *depth* recursive calls cannot
28372858
trigger the recursion protection if the C stack is too large (e.g.
28382859
with a large or unlimited RLIMIT_STACK), and either fail, or run
28392860
for a very long time, or crash, or consume all memory.
2861+
2862+
Prefer run_with_limited_c_stack() for tests recursing to a fixed depth.
28402863
"""
2841-
try:
2842-
from _testinternalcapi import get_c_recursion_remaining
2843-
except ImportError:
2844-
# Fall back to checking for an unlimited stack size.
2845-
huge = False
2846-
if not (is_emscripten or is_wasi) and os.name != "nt":
2847-
import resource
2848-
soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
2849-
huge = soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
2850-
else:
2851-
remaining = get_c_recursion_remaining()
2852-
# A negative value means integer overflow in the estimate
2853-
# (e.g. with an unlimited RLIMIT_STACK).
2854-
huge = remaining >= depth or remaining < 0
28552864
return unittest.skipIf(
2856-
huge, f"the C stack is large enough for {depth} recursive calls")
2865+
_has_huge_c_stack(depth),
2866+
f"the C stack is large enough for {depth} recursive calls")
2867+
2868+
2869+
# Small enough to be exhausted by tens of thousands of recursive calls,
2870+
# but not smaller than Py_C_STACK_SIZE (4 MiB) which the interpreter
2871+
# assumes if it cannot query the thread stack size.
2872+
C_STACK_SIZE = 8 * 1024 * 1024
2873+
2874+
2875+
def run_with_limited_c_stack(depth=150_000, size=C_STACK_SIZE):
2876+
"""Decorator for tests exhausting the C stack with *depth* recursive calls.
2877+
2878+
Run the test in a separate thread with the C stack of *size* bytes, so
2879+
that the outcome does not depend on the C stack size of the main thread
2880+
(which can be large or unlimited, see RLIMIT_STACK).
2881+
2882+
If a thread with the limited C stack cannot be created, run the test in
2883+
the current thread, but skip it if the C stack is too large.
2884+
"""
2885+
reason = f"the C stack is large enough for {depth} recursive calls"
2886+
def decorator(test):
2887+
@functools.wraps(test)
2888+
def wrapper(*args, **kwargs):
2889+
def run_test():
2890+
# The C stack can still be too large if limiting it failed.
2891+
if _has_huge_c_stack(depth):
2892+
raise unittest.SkipTest(reason)
2893+
test(*args, **kwargs)
2894+
2895+
try:
2896+
import threading
2897+
old_size = threading.stack_size(size)
2898+
except (ImportError, ValueError, RuntimeError):
2899+
# Setting the thread stack size is not supported.
2900+
return run_test()
2901+
2902+
exceptions = []
2903+
def run():
2904+
try:
2905+
run_test()
2906+
except BaseException as exc:
2907+
exceptions.append(exc)
2908+
2909+
thread = threading.Thread(target=run)
2910+
try:
2911+
thread.start()
2912+
except RuntimeError:
2913+
# Threads are not supported.
2914+
return run_test()
2915+
finally:
2916+
threading.stack_size(old_size)
2917+
thread.join()
2918+
if exceptions:
2919+
raise exceptions[0]
2920+
return wrapper
2921+
return decorator
28572922

28582923

28592924
# Windows doesn't have os.uname() but it doesn't support s390x.

‎Lib/test/test_profiling/test_sampling_profiler/test_collectors.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import tempfile
99
import unittest
1010

11-
from test.support import is_emscripten, set_recursion_limit
11+
from test.support import is_emscripten, run_with_limited_c_stack, set_recursion_limit
1212

1313
try:
1414
import _remote_debugging # noqa: F401
@@ -611,6 +611,7 @@ def test_flamegraph_collector_empty_export_fails(self):
611611
self.assertFalse(export_ok)
612612
self.assertEqual(os.path.getsize(flamegraph_out.name), 0)
613613

614+
@run_with_limited_c_stack(size=1024 * 1024)
614615
def test_flamegraph_deep_stack_export(self):
615616
flamegraph_out = tempfile.NamedTemporaryFile(
616617
suffix=".html", delete=False
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix deep flamegraph exports on platforms with small C stacks by falling back
2+
to the Python JSON encoder when the C encoder runs out of stack space.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix macOS runtime section lookup for remote debugging when virtual memory
2+
region sizes differ from Mach-O segment file sizes.

‎Python/remote_debug.h‎

Lines changed: 11 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,6 @@ _Py_RemoteDebug_CleanupProcHandle(proc_handle_t *handle) {
297297
static uintptr_t
298298
return_section_address64(
299299
const char* section,
300-
mach_port_t proc_ref,
301300
uintptr_t base,
302301
void* map
303302
) {
@@ -307,37 +306,15 @@ return_section_address64(
307306
int cmd_cnt = 0;
308307
struct segment_command_64* cmd = map + sizeof(struct mach_header_64);
309308

310-
mach_vm_size_t size = 0;
311-
mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
312-
mach_vm_address_t address = (mach_vm_address_t)base;
313-
vm_region_basic_info_data_64_t r_info;
314-
mach_port_t object_name;
315309
uintptr_t vmaddr = 0;
316310

317311
for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
318312
if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__TEXT") == 0) {
319313
vmaddr = cmd->vmaddr;
320314
}
321315
if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
322-
while (cmd->filesize != size) {
323-
address += size;
324-
kern_return_t ret = mach_vm_region(
325-
proc_ref,
326-
&address,
327-
&size,
328-
VM_REGION_BASIC_INFO_64,
329-
(vm_region_info_t)&r_info, // cppcheck-suppress [uninitvar]
330-
&count,
331-
&object_name
332-
);
333-
if (ret != KERN_SUCCESS) {
334-
PyErr_Format(PyExc_RuntimeError,
335-
"mach_vm_region failed while parsing 64-bit Mach-O binary "
336-
"at base address 0x%lx (kern_return_t: %d)",
337-
base, ret);
338-
return 0;
339-
}
340-
}
316+
// The section address only needs the image's ASLR slide.
317+
// VM regions need not match the segment's on-disk size.
341318

342319
int nsects = cmd->nsects;
343320
struct section_64* sec = (struct section_64*)(
@@ -360,7 +337,6 @@ return_section_address64(
360337
static uintptr_t
361338
return_section_address32(
362339
const char* section,
363-
mach_port_t proc_ref,
364340
uintptr_t base,
365341
void* map
366342
) {
@@ -370,37 +346,15 @@ return_section_address32(
370346
int cmd_cnt = 0;
371347
struct segment_command* cmd = map + sizeof(struct mach_header);
372348

373-
mach_vm_size_t size = 0;
374-
mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_t);
375-
mach_vm_address_t address = (mach_vm_address_t)base;
376-
vm_region_basic_info_data_t r_info;
377-
mach_port_t object_name;
378349
uintptr_t vmaddr = 0;
379350

380351
for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
381352
if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__TEXT") == 0) {
382353
vmaddr = cmd->vmaddr;
383354
}
384355
if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__DATA") == 0) {
385-
while (cmd->filesize != size) {
386-
address += size;
387-
kern_return_t ret = mach_vm_region(
388-
proc_ref,
389-
&address,
390-
&size,
391-
VM_REGION_BASIC_INFO,
392-
(vm_region_info_t)&r_info, // cppcheck-suppress [uninitvar]
393-
&count,
394-
&object_name
395-
);
396-
if (ret != KERN_SUCCESS) {
397-
PyErr_Format(PyExc_RuntimeError,
398-
"mach_vm_region failed while parsing 32-bit Mach-O binary "
399-
"at base address 0x%lx (kern_return_t: %d)",
400-
base, ret);
401-
return 0;
402-
}
403-
}
356+
// The section address only needs the image's ASLR slide.
357+
// VM regions need not match the segment's on-disk size.
404358

405359
int nsects = cmd->nsects;
406360
struct section* sec = (struct section*)(
@@ -423,7 +377,6 @@ return_section_address32(
423377
static uintptr_t
424378
return_section_address_fat(
425379
const char* section,
426-
mach_port_t proc_ref,
427380
uintptr_t base,
428381
void* map
429382
) {
@@ -473,11 +426,11 @@ return_section_address_fat(
473426
switch (hdr->magic) {
474427
case MH_MAGIC:
475428
case MH_CIGAM:
476-
return return_section_address32(section, proc_ref, base, (void*)hdr);
429+
return return_section_address32(section, base, (void*)hdr);
477430

478431
case MH_MAGIC_64:
479432
case MH_CIGAM_64:
480-
return return_section_address64(section, proc_ref, base, (void*)hdr);
433+
return return_section_address64(section, base, (void*)hdr);
481434

482435
default:
483436
PyErr_Format(PyExc_RuntimeError,
@@ -496,7 +449,7 @@ return_section_address_fat(
496449
}
497450

498451
static uintptr_t
499-
search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_size_t size, mach_port_t proc_ref)
452+
search_section_in_file(const char* secname, char* path, uintptr_t base)
500453
{
501454
int fd = open(path, O_RDONLY);
502455
if (fd == -1) {
@@ -533,15 +486,15 @@ search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_
533486
switch (magic) {
534487
case MH_MAGIC:
535488
case MH_CIGAM:
536-
result = return_section_address32(secname, proc_ref, base, map);
489+
result = return_section_address32(secname, base, map);
537490
break;
538491
case MH_MAGIC_64:
539492
case MH_CIGAM_64:
540-
result = return_section_address64(secname, proc_ref, base, map);
493+
result = return_section_address64(secname, base, map);
541494
break;
542495
case FAT_MAGIC:
543496
case FAT_CIGAM:
544-
result = return_section_address_fat(secname, proc_ref, base, map);
497+
result = return_section_address_fat(secname, base, map);
545498
break;
546499
default:
547500
PyErr_Format(PyExc_RuntimeError,
@@ -644,7 +597,7 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
644597
if (strncmp(filename, substr, strlen(substr)) == 0) {
645598
PyErr_Clear();
646599
uintptr_t result = search_section_in_file(
647-
secname, map_filename, address, size, proc_ref);
600+
secname, map_filename, address);
648601
if (result != 0) {
649602
if (validator == NULL || validator(handle, result)) {
650603
return result;

0 commit comments

Comments
 (0)