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
theoremqa_kshot_base_gen evaluates model output with bare eval() at three sites — :266, :313, :366 — guarded by _EVAL_GLOBALS, which sets "__builtins__": {}.
That guard is not a sandbox. Clearing __builtins__ does not prevent attribute traversal, and the standard catch_warnings route reaches a live open:
fromsieval.tasks.theoremqa_kshot_base_genimport_EVAL_GLOBALSpayload= (
"[c for c in ().__class__.__base__.__subclasses__() ""if c.__name__=='catch_warnings'][0]()._module.__builtins__['open']""('/tmp/pwned','w')"
)
eval(payload, _EVAL_GLOBALS)
Observed: /tmp/pwned is created. The three call sites reach this from an extracted model answer, so any model being evaluated can write files as the eval process.
Two smaller notes on the same guard:
factorial is exposed, so eval("factorial(2000000)", _EVAL_GLOBALS) runs for 16 s on this machine (measured) with no upper bound. Grading is synchronous on the shared event loop — MultiTaskRunner.arun starts every TaskRunner in one anyio.run — so that stalls every other task in the session, not just this one.
This is not just a hardening change — it changes grading semantics
This is the part worth deciding before writing any code. The module already says so at :78:
# Upstream uses bare eval(num). SiEval keeps eval sandboxed for task runtime # safety, so builtins such as abs/round/pow are intentionally unavailable.
So a model answering abs(-5) or round(1.6) scores differently here than under upstream's evaluator. That divergence is real, deliberate, and unquantified — and reference_impl.notes currently claims the opposite:
"answer_clean and numeric matching mirror official utils.py/number_utils.py."
Meanwhile the task ships under the unqualified name theoremqa_kshot_base_gen.
Under the variant convention introduced in #68 (.claude/rules/tasks.md, sieval/tasks/CLAUDE.md), the unqualified name means "what upstream measures, bugs included", and a local correction takes a _fixed variant owing (a) every divergence enumerated in reference_impl.notes and (b) a quantified score impact. By that rule:
the existing sandboxing is already a convention violation — an unquantified, score-affecting divergence under an unqualified name, with notes asserting fidelity;
and closing the RCE properly makes the divergence larger, so it lands in the same place: a _fixed variant, not a patch to the unqualified task.
Put plainly: hardening the evaluator is itself a _fixed. That is why this is an issue and not a quick PR.
Proposed sequencing
Now, cheap and semantics-neutral: correct reference_impl.notes so it stops claiming to mirror upstream, and record the known divergence (missing builtins) as text. Regenerate sieval/meta/index.json.
Then:theoremqa_kshot_base_gen_fixed with a real evaluator (a tokenising parser rather than eval, the way sieval/community/ugmathbench.py::_parse_sympy_source does it — it was tested against these same payloads and does not execute them, because parse_expr rewrites name tokens into symbols), plus the offload, plus the quantified delta against the current task on a stored run.
Open decision: the convention needs an explicit safety carve-out
This outlives the theoremqa fix — do not close it with the RCE.
Step 3 above leaves the unqualified theoremqa_kshot_base_gen in an awkward place. The convention says the unqualified name tracks upstream including its bugs, and upstream's bug here is an unsandboxed eval on model output. Read literally, the rule asks us to ship a remote-code-execution path under the plain name.
That cannot be the intent, but right now nothing says so. The rule is silent on the case, which means the next person to hit it has to re-derive the answer — and the two readings ("be faithful" vs "don't ship an RCE") point opposite ways.
Proposed: state the exception in .claude/rules/tasks.md and sieval/tasks/CLAUDE.md, next to the existing "the unqualified name tracks upstream" paragraph. Rough wording:
Fidelity stops at execution safety. Tracking upstream never extends to reproducing a path that executes model output, writes outside the run directory, or cannot be bounded. Where upstream's own harness does that, the unqualified task carries the hardened behaviour, and the divergence is recorded in reference_impl.notes with its score impact quantified like any other. This is the one divergence that does not require a _fixed variant — a variant exists so two readings can be compared, and "the unsafe reading" is not one we intend to run.
Note this cuts against the rest of the convention on purpose, so it should be argued rather than slipped in — hence flagging it here instead of folding it into #68. Two things to settle:
if a task takes the hardened behaviour under its unqualified name, does the score impact still have to be quantified before it may ship as stable? (I would say yes — it is the same obligation _fixed carries, and it is the only thing keeping "hardened" from becoming an excuse.)
Known sites this would already apply to: theoremqa_kshot_base_gen (this issue), sieval/community/deepseek_math.py:317, and — already hardened under #68 — sieval/community/ugmathbench.py.
sieval/community/deepseek_math.py:317 — passes model output to parse_expr as well; vendored GPL upstream, so fidelity is the constraint there and it needs its own decision
Before submitting a new issue...
Make sure you already searched for relevant issues and documentation.
Your current environment
main@8aa09ae3)sieval/tasks/theoremqa_kshot_base_gen.py, not a dependencyparse_exprevaluating model output)🐛 Describe the bug
theoremqa_kshot_base_genevaluates model output with bareeval()at three sites —:266,:313,:366— guarded by_EVAL_GLOBALS, which sets"__builtins__": {}.That guard is not a sandbox. Clearing
__builtins__does not prevent attribute traversal, and the standardcatch_warningsroute reaches a liveopen:Observed:
/tmp/pwnedis created. The three call sites reach this from an extracted model answer, so any model being evaluated can write files as the eval process.Two smaller notes on the same guard:
factorialis exposed, soeval("factorial(2000000)", _EVAL_GLOBALS)runs for 16 s on this machine (measured) with no upper bound. Grading is synchronous on the shared event loop —MultiTaskRunner.arunstarts everyTaskRunnerin oneanyio.run— so that stalls every other task in the session, not just this one.anyio.to_thread.run_sync;theoremqawas left alone because its fix is not mechanical (see below).This is not just a hardening change — it changes grading semantics
This is the part worth deciding before writing any code. The module already says so at
:78:So a model answering
abs(-5)orround(1.6)scores differently here than under upstream's evaluator. That divergence is real, deliberate, and unquantified — andreference_impl.notescurrently claims the opposite:Meanwhile the task ships under the unqualified name
theoremqa_kshot_base_gen.Under the variant convention introduced in #68 (
.claude/rules/tasks.md,sieval/tasks/CLAUDE.md), the unqualified name means "what upstream measures, bugs included", and a local correction takes a_fixedvariant owing (a) every divergence enumerated inreference_impl.notesand (b) a quantified score impact. By that rule:_fixedvariant, not a patch to the unqualified task.Put plainly: hardening the evaluator is itself a
_fixed. That is why this is an issue and not a quick PR.Proposed sequencing
reference_impl.notesso it stops claiming to mirror upstream, and record the known divergence (missing builtins) as text. Regeneratesieval/meta/index.json._fixedpattern being exercised —ugmathbench_0shot_gen_fixedis its first user and hasn't merged yet. Migrating a second benchmark onto a convention that has not yet been proven end-to-end is the wrong order.theoremqa_kshot_base_gen_fixedwith a real evaluator (a tokenising parser rather thaneval, the waysieval/community/ugmathbench.py::_parse_sympy_sourcedoes it — it was tested against these same payloads and does not execute them, becauseparse_exprrewrites name tokens into symbols), plus the offload, plus the quantified delta against the current task on a stored run.Open decision: the convention needs an explicit safety carve-out
This outlives the theoremqa fix — do not close it with the RCE.
Step 3 above leaves the unqualified
theoremqa_kshot_base_genin an awkward place. The convention says the unqualified name tracks upstream including its bugs, and upstream's bug here is an unsandboxedevalon model output. Read literally, the rule asks us to ship a remote-code-execution path under the plain name.That cannot be the intent, but right now nothing says so. The rule is silent on the case, which means the next person to hit it has to re-derive the answer — and the two readings ("be faithful" vs "don't ship an RCE") point opposite ways.
Proposed: state the exception in
.claude/rules/tasks.mdandsieval/tasks/CLAUDE.md, next to the existing "the unqualified name tracks upstream" paragraph. Rough wording:Note this cuts against the rest of the convention on purpose, so it should be argued rather than slipped in — hence flagging it here instead of folding it into #68. Two things to settle:
eval/exec/subprocess), or also unbounded computation (thefactorial(2000000)case above, and the power towers guarded in feat(tasks): task-name variants, UGMathBench as _fixed, and grading off the shared event loop #68)?stable? (I would say yes — it is the same obligation_fixedcarries, and it is the only thing keeping "hardened" from becoming an excuse.)Known sites this would already apply to:
theoremqa_kshot_base_gen(this issue),sieval/community/deepseek_math.py:317, and — already hardened under #68 —sieval/community/ugmathbench.py.Related
parse_expron model output) insieval/community/ugmathbench.pysieval/community/deepseek_math.py:317— passes model output toparse_expras well; vendored GPL upstream, so fidelity is the constraint there and it needs its own decisionBefore submitting a new issue...