You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support other types than dict for __builtins__ #58593
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=<Date2012-04-18.23:02:25.520>created_at=<Date2012-03-22.01:35:33.441>labels= ['interpreter-core', 'type-feature']
title='Support other types than dict for __builtins__'updated_at=<Date2019-03-06.01:35:04.886>user='https://github.com/vstinner'
CPython expects __builtins__ to be a dict, but it is interesting to be able to use another type. For example, my pysandbox project (sandbox to secure Python) requires a read-only mapping for __builtins__.
The PEP-416 was rejected, so there is no builtin frozendict type, but it looks like the dictproxy type will be exposed as a public type.
Attached patch uses PyDict_CheckExact() to check if __builtins__ is a dict and add a "slow-path" for other types. The overhead on runtime performance should be very low (near zero), PyDict_CheckExact() just dereference a pointer (to read the object type) and compare two pointers.
The patch depends on issue bpo-14383 patch (identifier.patch) for the __build_class__ identifier. I can write a new patch without this dependency if needed.
$ ./python test.py
1
Traceback (most recent call last):
File "x.py", line 4, in <module>exec(compile("__builtins__['superglobal']=1; print(superglobal)", "test", "exec"), ns)
File "test", line 1, in <module>TypeError: 'dictproxy' object does not support item assignment
Note: this protection is not enough to secure Python, but it is an important part of a Python sandbox.
Note: this protection is not enough to secure Python,
but it is an important part of a Python sandbox.
Oh, and by the way, I workaround the lack of read-only mapping in pysandbox by removing dict methods: dict.__init__(), dict.clear(), dict.update(), etc. This is a problem because these methods are useful in Python.
With my patch, Python doesn't check __builtins__ type whereas ceval.c replaces any lookup error by a NameError. Example:
$ ./python
Python 3.3.0a1+ (default:f8d01c8baf6a+, Mar 26 2012, 01:44:48)
>>> code=compile("print('Hello World!')", "", "exec")
>>> exec(code,{'__builtins__': {'print': print}})
Hello World!
>>> exec(code,{'__builtins__': {}})
NameError: name 'print' is not defined
>>> exec(code,{'__builtins__': 1})
NameError: name 'print' is not defined
It should only replace the current exception by NameError if the current exception is a LookupError.
And my patch on LOAD_GLOBAL is not correct, it does still call PyDict_GetItem. I'm waiting until bpo-14383 is done before writing a new patch.
if __build_class__ is missing, raise a NameError instead of surprising ImportError
add tests
if PyObject_GetItem on __builtins__ or globals fail, only raise NameError if the exception is a KeyError
Before my patch, a new dict was created for builtins if __builtins__ exists in global but is not a dict. With my patch, the __builtins__ is kept and the type is checked at runtime. If __builtins__ is not a mapping, an exception is raised on lookup in ceval.c.
We may check __builtins__ type in PyFrame_New() using:
Oops, patch version 2 was not correct: I forgot a { ... } in ceval.c.
New patch fixing this issue but leaves also the LOAD_GLOBAL code unchanged : keep the goto and don't try to factorize the 3 last instructions. LOAD_GLOBAL is really critical in performance.
With patch version 3, the overall overhead is +0.4% according to pybench.
The patch for this issue changed LOAD_GLOBAL to use PyObject_GetItem when globals() is a dict subclass, but LOAD_NAME, STORE_GLOBAL, and DELETE_GLOBAL weren't changed. (LOAD_NAME uses PyObject_GetItem for builtins now, but not for globals.)
This means that global lookup doesn't respect overridden __getitem__ inside a class statement (unless you explicitly declare the name global with a global statement, in which case LOAD_GLOBAL gets used instead of LOAD_NAME).
I don't have a strong opinion on whether STORE_GLOBAL or DELETE_GLOBAL should respect overridden __setitem__ or __delitem__, but the inconsistency between LOAD_GLOBAL and LOAD_NAME seems like a bug that should be fixed.
For reference, in the following code, the first 3 exec calls successfully print 5, and the last exec call fails, due to the LOAD_GLOBAL/LOAD_NAME inconsistency:
classFoo(dict):
def__getitem__(self, index):
return5ifindex=='y'elsesuper().__getitem__(index)
exec('print(y)', Foo())
exec('global y; print(y)', Foo())
exec('''class UsesLOAD_NAME: global y print(y)''', Foo())
exec('''class UsesLOAD_NAME: print(y)''', Foo())
Activity
vstinner commentedon Mar 22, 2012
CPython expects __builtins__ to be a dict, but it is interesting to be able to use another type. For example, my pysandbox project (sandbox to secure Python) requires a read-only mapping for __builtins__.
The PEP-416 was rejected, so there is no builtin frozendict type, but it looks like the dictproxy type will be exposed as a public type.
Attached patch uses PyDict_CheckExact() to check if __builtins__ is a dict and add a "slow-path" for other types. The overhead on runtime performance should be very low (near zero), PyDict_CheckExact() just dereference a pointer (to read the object type) and compare two pointers.
The patch depends on issue bpo-14383 patch (identifier.patch) for the __build_class__ identifier. I can write a new patch without this dependency if needed.
vstinner commentedon Mar 22, 2012
See the issue bpo-14386 which exposes dictproxy as a public type.
vstinner commentedon Mar 22, 2012
Example combining patches of bpo-14385 and bpo-14386 to run code with read-only __builtins__:
----------- test.py -------------
Output:
--------
Note: this protection is not enough to secure Python, but it is an important part of a Python sandbox.
vstinner commentedon Mar 22, 2012
Oh, and by the way, I workaround the lack of read-only mapping in pysandbox by removing dict methods: dict.__init__(), dict.clear(), dict.update(), etc. This is a problem because these methods are useful in Python.
vstinner commentedon Mar 25, 2012
With my patch, Python doesn't check __builtins__ type whereas ceval.c replaces any lookup error by a NameError. Example:
It should only replace the current exception by NameError if the current exception is a LookupError.
And my patch on LOAD_GLOBAL is not correct, it does still call PyDict_GetItem. I'm waiting until bpo-14383 is done before writing a new patch.
vstinner commentedon Apr 5, 2012
New version:
Before my patch, a new dict was created for builtins if __builtins__ exists in global but is not a dict. With my patch, the __builtins__ is kept and the type is checked at runtime. If __builtins__ is not a mapping, an exception is raised on lookup in ceval.c.
We may check __builtins__ type in PyFrame_New() using:
PyDict_Check(builtins) || (PyMapping_Check(mapping) && !PyList_Check(mapping) && !PyTuple_Check(mapping))
(PyDict_Check(builtins) is checked first for performance)
vstinner commentedon Apr 15, 2012
Oops, patch version 2 was not correct: I forgot a { ... } in ceval.c.
New patch fixing this issue but leaves also the LOAD_GLOBAL code unchanged : keep the goto and don't try to factorize the 3 last instructions. LOAD_GLOBAL is really critical in performance.
With patch version 3, the overall overhead is +0.4% according to pybench.
vstinner commentedon Apr 18, 2012
+ assert(!builtins);
Oops, the assert must be replaced with assert(builtins != NULL) -> fixed in patch version 4.
pitrou commentedon Apr 18, 2012
This looks fine.
python-dev commentedon Apr 18, 2012
New changeset e3ab8aa0216c by Victor Stinner in branch 'default':
Issue bpo-14385: Support other types than dict for __builtins__
http://hg.python.org/cpython/rev/e3ab8aa0216c
mjpieters commentedon Sep 1, 2012
I note that the documentation still states a dictionary is required for globals. Should that not be updated as well?
See http://docs.python.org/py3k/library/functions.html#exec
mjpieters commentedon Sep 1, 2012
Apologies, I meant to link to the dev docs:
http://docs.python.org/dev/library/functions.html#exec
KevinShweh commentedon Mar 5, 2019
The patch for this issue changed LOAD_GLOBAL to use PyObject_GetItem when globals() is a dict subclass, but LOAD_NAME, STORE_GLOBAL, and DELETE_GLOBAL weren't changed. (LOAD_NAME uses PyObject_GetItem for builtins now, but not for globals.)
This means that global lookup doesn't respect overridden __getitem__ inside a class statement (unless you explicitly declare the name global with a global statement, in which case LOAD_GLOBAL gets used instead of LOAD_NAME).
I don't have a strong opinion on whether STORE_GLOBAL or DELETE_GLOBAL should respect overridden __setitem__ or __delitem__, but the inconsistency between LOAD_GLOBAL and LOAD_NAME seems like a bug that should be fixed.
For reference, in the following code, the first 3 exec calls successfully print 5, and the last exec call fails, due to the LOAD_GLOBAL/LOAD_NAME inconsistency:
vstinner commentedon Mar 6, 2019
This issues is now closed. Please open a new issue.