Skip to content

Commit 078f181

Browse files
authored
bpo-32176: Set CO_NOFREE in the code object constructor (GH-4675)
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.
1 parent 7324b5c commit 078f181

File tree

4 files changed

+59
-6
lines changed

4 files changed

+59
-6
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
@@ -124,12 +124,20 @@ PyCode_New(int argcount, int kwonlyargcount,
124124
if (PyUnicode_READY(filename) < 0)
125125
return NULL;
126126

127-
n_cellvars = PyTuple_GET_SIZE(cellvars);
128127
intern_strings(names);
129128
intern_strings(varnames);
130129
intern_strings(freevars);
131130
intern_strings(cellvars);
132131
intern_string_constants(consts);
132+
133+
/* Check for any inner or outer closure references */
134+
n_cellvars = PyTuple_GET_SIZE(cellvars);
135+
if (!n_cellvars && !PyTuple_GET_SIZE(freevars)) {
136+
flags |= CO_NOFREE;
137+
} else {
138+
flags &= ~CO_NOFREE;
139+
}
140+
133141
/* Create mapping between cells and arguments if needed. */
134142
if (n_cellvars) {
135143
Py_ssize_t total_args = argcount + kwonlyargcount +

Python/compile.c

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5273,11 +5273,6 @@ compute_code_flags(struct compiler *c)
52735273
/* (Only) inherit compilerflags in PyCF_MASK */
52745274
flags |= (c->c_flags->cf_flags & PyCF_MASK);
52755275

5276-
if (!PyDict_GET_SIZE(c->u->u_freevars) &&
5277-
!PyDict_GET_SIZE(c->u->u_cellvars)) {
5278-
flags |= CO_NOFREE;
5279-
}
5280-
52815276
return flags;
52825277
}
52835278

0 commit comments

Comments
 (0)