Skip to content

Commit c8f32aa

Browse files
authored
[3.6] bpo-32176: Set CO_NOFREE in the code object constructor (GH-4684)
Previously, CO_NOFREE was set in the compiler, which meant it could end up being set incorrectly when code objects were created directly. Setting it in the constructor based on freevars and cellvars ensures it is always accurate, regardless of how the code object is defined. (cherry picked from commit 078f181)
1 parent 2ad350a commit c8f32aa

File tree

4 files changed

+59
-13
lines changed

4 files changed

+59
-13
lines changed

Lib/test/test_code.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
103103
"""
104104

105+
import inspect
105106
import sys
106107
import threading
107108
import unittest
@@ -130,6 +131,10 @@ def dump(co):
130131
print("%s: %s" % (attr, getattr(co, "co_" + attr)))
131132
print("consts:", tuple(consts(co.co_consts)))
132133

134+
# Needed for test_closure_injection below
135+
# Defined at global scope to avoid implicitly closing over __class__
136+
def external_getitem(self, i):
137+
return f"Foreign getitem: {super().__getitem__(i)}"
133138

134139
class CodeTest(unittest.TestCase):
135140

@@ -141,6 +146,46 @@ def test_newempty(self):
141146
self.assertEqual(co.co_name, "funcname")
142147
self.assertEqual(co.co_firstlineno, 15)
143148

149+
@cpython_only
150+
def test_closure_injection(self):
151+
# From https://bugs.python.org/issue32176
152+
from types import FunctionType, CodeType
153+
154+
def create_closure(__class__):
155+
return (lambda: __class__).__closure__
156+
157+
def new_code(c):
158+
'''A new code object with a __class__ cell added to freevars'''
159+
return CodeType(
160+
c.co_argcount, c.co_kwonlyargcount, c.co_nlocals,
161+
c.co_stacksize, c.co_flags, c.co_code, c.co_consts, c.co_names,
162+
c.co_varnames, c.co_filename, c.co_name, c.co_firstlineno,
163+
c.co_lnotab, c.co_freevars + ('__class__',), c.co_cellvars)
164+
165+
def add_foreign_method(cls, name, f):
166+
code = new_code(f.__code__)
167+
assert not f.__closure__
168+
closure = create_closure(cls)
169+
defaults = f.__defaults__
170+
setattr(cls, name, FunctionType(code, globals(), name, defaults, closure))
171+
172+
class List(list):
173+
pass
174+
175+
add_foreign_method(List, "__getitem__", external_getitem)
176+
177+
# Ensure the closure injection actually worked
178+
function = List.__getitem__
179+
class_ref = function.__closure__[0].cell_contents
180+
self.assertIs(class_ref, List)
181+
182+
# Ensure the code correctly indicates it accesses a free variable
183+
self.assertFalse(function.__code__.co_flags & inspect.CO_NOFREE,
184+
hex(function.__code__.co_flags))
185+
186+
# Ensure the zero-arg super() call in the injected method works
187+
obj = List([1, 2, 3])
188+
self.assertEqual(obj[0], "Foreign getitem: 1")
144189

145190
def isinterned(s):
146191
return s is sys.intern(('_' + s + '_')[1:-1])
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
co_flags.CO_NOFREE is now always set correctly by the code object
2+
constructor based on freevars and cellvars, rather than needing to be set
3+
correctly by the caller. This ensures it will be cleared automatically when
4+
additional cell references are injected into a modified code object and
5+
function.

Objects/codeobject.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,20 @@ PyCode_New(int argcount, int kwonlyargcount,
136136
if (PyUnicode_READY(filename) < 0)
137137
return NULL;
138138

139-
n_cellvars = PyTuple_GET_SIZE(cellvars);
140139
intern_strings(names);
141140
intern_strings(varnames);
142141
intern_strings(freevars);
143142
intern_strings(cellvars);
144143
intern_string_constants(consts);
144+
145+
/* Check for any inner or outer closure references */
146+
n_cellvars = PyTuple_GET_SIZE(cellvars);
147+
if (!n_cellvars && !PyTuple_GET_SIZE(freevars)) {
148+
flags |= CO_NOFREE;
149+
} else {
150+
flags &= ~CO_NOFREE;
151+
}
152+
145153
/* Create mapping between cells and arguments if needed. */
146154
if (n_cellvars) {
147155
Py_ssize_t total_args = argcount + kwonlyargcount +

Python/compile.c

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5190,18 +5190,6 @@ compute_code_flags(struct compiler *c)
51905190
/* (Only) inherit compilerflags in PyCF_MASK */
51915191
flags |= (c->c_flags->cf_flags & PyCF_MASK);
51925192

5193-
n = PyDict_Size(c->u->u_freevars);
5194-
if (n < 0)
5195-
return -1;
5196-
if (n == 0) {
5197-
n = PyDict_Size(c->u->u_cellvars);
5198-
if (n < 0)
5199-
return -1;
5200-
if (n == 0) {
5201-
flags |= CO_NOFREE;
5202-
}
5203-
}
5204-
52055193
return flags;
52065194
}
52075195

0 commit comments

Comments
 (0)