Skip to content

Commit 5e48006

Browse files
fix(internal): closure bytecode wrapping for Python 3.11 [backport 2.0] (#7052)
Backport 9bd1fb9 from #7044 to 2.0. We fix the bytecode wrapping for closures to ensure that the frame objects don't end up referencing missing data, ultimately resulting in a segmentation fault in profiling tools. Fixes #6701. ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [x] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [x] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [x] This PR doesn't touch any of that. Co-authored-by: Gabriele N. Tornetta <[email protected]>
1 parent d345d35 commit 5e48006

File tree

3 files changed

+49
-2
lines changed

3 files changed

+49
-2
lines changed

ddtrace/internal/wrapping/__init__.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,30 @@ def wrap_bytecode(wrapper, wrapped):
130130
Instr("LOAD_CONST", wrapped, lineno=lineno),
131131
]
132132
if PY >= (3, 11):
133-
# THis is required to start a new frame
133+
# From insert_prefix_instructions
134134
instrs[0:0] = [
135135
Instr("RESUME", 0, lineno=lineno - 1),
136136
Instr("PUSH_NULL", lineno=lineno),
137137
]
138138

139+
if code.co_cellvars:
140+
from bytecode import CellVar
141+
142+
instrs[0:0] = [Instr("MAKE_CELL", CellVar(_), lineno=lineno) for _ in code.co_cellvars]
143+
144+
if code.co_freevars:
145+
instrs.insert(0, Instr("COPY_FREE_VARS", len(code.co_freevars), lineno=lineno))
146+
139147
# Build the tuple of all the positional arguments
140148
if nargs:
141-
instrs.extend([Instr("LOAD_FAST", argname, lineno=lineno) for argname in argnames])
149+
instrs.extend(
150+
[
151+
Instr("LOAD_DEREF", CellVar(argname), lineno=lineno)
152+
if PY >= (3, 11) and argname in code.co_cellvars
153+
else Instr("LOAD_FAST", argname, lineno=lineno)
154+
for argname in argnames
155+
]
156+
)
142157
instrs.append(Instr("BUILD_TUPLE", nargs, lineno=lineno))
143158
if varargs:
144159
instrs.extend(
@@ -213,6 +228,8 @@ def wrap(f, wrapper):
213228

214229
code = wrap_bytecode(wrapper, wrapped)
215230
code.freevars = f.__code__.co_freevars
231+
if PY >= (3, 11):
232+
code.cellvars = f.__code__.co_cellvars
216233
code.name = f.__code__.co_name
217234
code.filename = f.__code__.co_filename
218235
code.flags = f.__code__.co_flags
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Fix an issue that could have caused some tracing integrations to create
5+
invalid references to objects in Python frames, ultimately causing profiling
6+
tools to potentially induce a segmentation fault.

tests/internal/test_wrapping.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,27 @@ async def g():
467467
await gen.aclose()
468468

469469
assert channel == [True] + [0, ValueError, 1] * 10 + ["GeneratorExit"]
470+
471+
472+
def test_wrap_closure():
473+
channel = []
474+
475+
def wrapper(f, args, kwargs):
476+
channel.append((args, kwargs))
477+
retval = f(*args, **kwargs)
478+
channel.append(retval)
479+
return retval
480+
481+
def outer(answer=42):
482+
def f(a, b, c=None):
483+
return (a, b, c, answer)
484+
485+
return f
486+
487+
wrap(outer, wrapper)
488+
489+
closure = outer()
490+
wrap(closure, wrapper)
491+
492+
assert closure(1, 2, 3) == (1, 2, 3, 42)
493+
assert channel == [((42,), {}), closure, ((1, 2, 3), {}), (1, 2, 3, 42)]

0 commit comments

Comments
 (0)