Skip to content

Commit 3f423bf

Browse files
akushonkamencodelion
authored andcommitted
fix(z3): close string-based evasions of the AST attribute denylist
QA probes found two solver-code escapes that pass the gate and execute: z3.__dict__['__builtins__']['__import__']('os').system(...) and z3.__getattribute__('__spec__').loader... — __dict__/__getattribute__ turn every denied attribute into a constant-string lookup, and a module's __dict__ exposes its real __builtins__ (dir(z3) contains __builtins__, so the namespace dump in execute_code_in_process() already replaces the restricted builtins with the full ones). getattr/setattr reach the same attributes via constant strings and resolve at runtime. - Deny getattr/setattr/delattr calls - Deny __dict__/__getattribute__/__getattr__/__setattr__/__delattr__ attributes - Add 6 regression tests (suite: 35 passed); allow-cases unchanged
1 parent 81f02f3 commit 3f423bf

2 files changed

Lines changed: 22 additions & 7 deletions

File tree

‎optillm/z3_solver.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,26 @@ class TimeoutException(Exception):
2020
# exactly the modules execute_code_in_process() makes available.
2121
_ALLOWED_SOLVER_IMPORTS = {'z3', 'sympy', 'math', 'itertools', 'fractions'}
2222

23-
# Calls that provide arbitrary execution or file/IO access; the declared
24-
# solver subset has no use for any of them.
23+
# Calls that provide arbitrary execution or file/IO access, or that reach the
24+
# same denied attributes through string arguments (getattr/setattr/delattr);
25+
# the declared solver subset has no use for any of them.
2526
_DENIED_SOLVER_CALLS = {
2627
'eval', 'exec', 'compile', '__import__', 'open', 'input', 'breakpoint',
27-
'globals', 'locals', 'vars',
28+
'globals', 'locals', 'vars', 'getattr', 'setattr', 'delattr',
2829
}
2930

3031
# Dunder attributes used in classic module-object traversal escapes (e.g.
31-
# z3.__loader__, ().__class__.__bases__[0].__subclasses__()). A fixed
32-
# denylist instead of banning every dunder so legitimate solver code that
33-
# touches z3/sympy internals by name is not rejected.
32+
# z3.__loader__, ().__class__.__bases__[0].__subclasses__()). __dict__ and
33+
# __getattribute__ are denied because they turn every other denied attribute
34+
# into a string lookup (z3.__dict__['__loader__'],
35+
# z3.__getattribute__('__spec__')), and a module's __dict__ exposes its real
36+
# __builtins__. A fixed denylist instead of banning every dunder so legitimate
37+
# solver code that touches z3/sympy internals by name is not rejected.
3438
_DENIED_SOLVER_ATTRS = {
3539
'__class__', '__bases__', '__subclasses__', '__mro__', '__globals__',
3640
'__builtins__', '__init__', '__code__', '__closure__', '__loader__',
37-
'__spec__', '__import__', '__reduce__',
41+
'__spec__', '__import__', '__reduce__', '__dict__', '__getattribute__',
42+
'__getattr__', '__setattr__', '__delattr__',
3843
}
3944

4045
def _validate_solver_ast(tree: ast.AST) -> None:

‎tests/test_z3_solver_guard.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ def _response(content, completion_tokens=10):
6060
("sympy.__spec__", "__spec__"),
6161
("f = z3.Bool.__init__", "__init__"),
6262
("import z3\nz3.__builtins__", "__builtins__"),
63+
# String-based evasions of the attribute denylist (QA finding): getattr
64+
# reaches the same objects via a constant argument, and __dict__ /
65+
# __getattribute__ turn every dunder into a string lookup. A module's
66+
# __dict__ also exposes its real __builtins__.
67+
("getattr(z3, '__loader__')", "getattr()"),
68+
("setattr(z3, '__loader__', None)", "setattr()"),
69+
("delattr(z3, 'Int')", "delattr()"),
70+
("z3.__dict__['__builtins__']['__import__']('os')", "__dict__"),
71+
("z3.__getattribute__('__spec__')", "__getattribute__"),
72+
("sympy.__getattr__('_load')", "__getattr__"),
6373
])
6474
def test_guard_rejects(code, violation):
6575
with pytest.raises(ValueError) as excinfo:

0 commit comments

Comments
 (0)