fix(deepseek-math): stop symbolic_equal executing the answer it grades - #84
Merged
Conversation
1 task
#77) `symbolic_equal` had two independent execution paths, both reachable from a model's extracted answer through `math_equal` / `is_correct` / `eval_math`, and so from `gsm8k_0shot_gen` and `hendrycks_math_kshot_base_gen`: 1. bare `parse_expr(s)`, whose default namespace is built by `exec("from sympy import *", ...)` and carries `__builtins__`; 2. the raw-string fallback — when both parsers fail, upstream returns `s`, which then reaches `simplify(a - b)` and `N(a)`. Both sympify a string with sympy's own default namespace, not the caller's. (2) is the one that matters: it defeats a namespace fix and a quote screen on its own, because once `__import__` resolves a payload needs no quote at all. Guarding only the parse would have moved the hole, not closed it. Verified: `__import__('os').system(...)` supplied as an answer ran, and the sample still graded wrong, so nothing in a run looked unusual. The three guards already written for the UGMathBench grader move to `sieval/community/_sympy_guards.py` and are now shared. That extraction is licensed by coupling, not by call count: both graders hand model output to the same library under the same threat model, and a new escape route has to close in both or one is left open. The ugmathbench side is a pure move — its 55 tests pass unchanged. Score impact measured at zero, over both benchmarks' full stored runs and in two environments each. With `parse_latex` working and with it disabled — the adversarial case, since it sends every comparison down the guarded path (1622 of 5000 on MATH) — upstream and guarded agree on every sample: GSM8K 1319 (deepseek-llm-7b-chat) 63.3813 / 63.3055 MATH 5000 (Qwen2.5-72B) 61.2600 / 60.0200 Found while measuring: both stored runs were produced without `antlr4-python3-runtime`, so `parse_latex` raised into upstream's bare `except` and the symbolic path never ran — worth 1.24 pp on MATH, with no signal in any log. Already pinned in the `[math]` extra since #31; the figures above are the same runs measured with and without it, and the note is now recorded on both tasks so the next reader does not rediscover it. Refs #77 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t edge Review fixes for #84. No behaviour change -- docstrings, notes, index.json and one CLAUDE.md; the guards and the refusal are untouched. - The raw-string fallback reached `N`, not `simplify` and `N`. `simplify(a-b)` never sees the text: the subtraction runs first, and sympy's arithmetic dunders sympify strictly, so a raw `s` raises TypeError before simplify is entered (`str - str` and `str - Expr` both, verified). `N` alone is the whole vector and is enough to defeat a parse-only guard, so the argument for refusing the fallback stands unchanged -- but "both sympify" was wrong and would have sent the next person hardening this code at the wrong call. Corrected in the module docstring, the inline comment, both tasks' `reference_impl.notes`, and index.json. - Named the guard edge that can actually flip a verdict. The notes quantified the raw-string refusal (1622 fall-throughs, zero flips) but said nothing about the exponent pre-parse, which declines a right-nested `**` tower (`2**3**2`) and an integer exponent above `MAX_EXPONENT` -- both of which upstream evaluates. Unreachable while the antlr4 pin holds, since `parse_latex` resolves those spellings first, and the measured zero covers the disabled-`parse_latex` cells, so neither occurs in either stored run. Worth naming because it is reachable in exactly the environment the same note warns about. - `quotes_free`'s "nothing legitimate is lost" now carries evidence for both dialects. It generalized to both graders on the move but cited only UGMathBench's 42,064 quote-free gold slots; deepseek reads LaTeX, where the evidence is the replay recorded in that module's deviations note. - `sieval/community/CLAUDE.md` no longer contradicts its own contents. The charter said the directory holds "not original code" while `_sympy_guards.py` is exactly that, and the package-wide ruff/mypy/pre-commit exclusions -- which exist to keep vendored code byte-identical -- silently cover it, which is the wrong default for the module holding a security boundary. Documents the coupling that earns its place here, how to lint it by hand meanwhile, and the bar for adding more original code. Narrowing the exclusions to the vendored paths is left to its own change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ethan-scitix
force-pushed
the
worktree-issue-77-deepseek-math
branch
from
August 8, 2026 11:50
b20a5da to
24ac2c8
Compare
No content dropped, only length: the corrections from 24ac2c8 said in about half the words. Net prose added by the review fixes goes from ~58 lines to ~27. The one substantive addition is in `sieval/community/CLAUDE.md`, which now says why a shared first-party module *serves* upstream alignment instead of working against it -- the vendored graders keep a small annotated divergence each rather than an inline copy of the guards that would swamp a diff against upstream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type
Summary
symbolic_equalexecuted the answer it was grading, reachable from an extracted model answer viamath_equal/is_correct/eval_math— so from bothgsm8k_0shot_genandhendrycks_math_kshot_base_gen. Verified:__import__('os').system(...)supplied as an answer ran, and the sample still graded wrong, so nothing in a run looked unusual.parse_expr(s)— default namespace built byexec("from sympy import *", ...), which injects__builtins__;s, which then reachesN(a), andNsympifies a string with sympy's own default namespace, not the caller's. (Notsimplify(a - b)— see Review fixes below.)__import__resolves, a payload needs no quote — so the quote screen doesn't help either. Guarding only the parse would have moved the hole rather than closed it. Fixed by refusing: an unparseable answer becomesNoneand the comparison returnsFalse, instead of being handed tosympifyunder another name.sieval/community/_sympy_guards.py. Licensed by coupling, not call count: both graders hand model output to the same library under the same threat model, and a new escape route must close in both or one is left open. The ugmathbench side is a pure move — its 82 tests pass unchanged._fixedvariant, per the carve-out in fix(theoremqa): evaluate answers instead of executing them #83: the divergence is execution safety only, and it is quantified below.Related Issues
Refs #77 (the third known site it lists). Also corrects the issue on two points:
_fixed-shaped change #77 says deepseek_math is "vendored GPL upstream, so fidelity is the constraint". DeepSeek-Math's code is MIT (LICENSE-CODE) — GPL-3.0 is UGMathBench'sjudge_rule.py. Nothing here was license-constrained.run_cpu_boundwithGRADE_TIMEOUT(from feat(tasks): task-name variants, UGMathBench as _fixed, and grading off the shared event loop #68), and deepseek's owncall_with_timeoutpath is dead code (math_equalis only ever reached withtimeout=False). Executing in a worker is still an RCE, but it is not a session stall.Test Plan
Automated
sieval/community/is pre-commit-excluded; the 1747 pre-existing findings there are untouched)ty check)tests/unit, 3558 passed (post-rebase ontomain); newtest__sympy_guards.py, plus execution-safety cases intest_deepseek_math.pyscripts/check_preflight.py— all passTests pin the premises, not just the fix: that a cleared namespace alone still executes a nested-parse payload (so the quote screen isn't kept on faith), and that a quoteless
chr()-built payload is stopped only by the raw-string refusal.Manual — replay against two full stored runs, in two environments each
parse_latexonparse_latexoffparse_latexonparse_latexoff12,638 gradings, zero divergences. The
parse_latexoff rows are the adversarial case, not padding: with the LaTeX parser dead every comparison falls through to the guarded path — 1622 of 5000 on MATH reach the raw-string refusal specifically, and none of them changes a verdict.Incidental finding worth its own attention
Both stored runs were produced without
antlr4-python3-runtime, soparse_latexraised into upstream's bareexcept:and the symbolic path silently never ran. Disabling it reproduces each storedreport.jsonscore exactly (MATH 60.0200 = its recorded 60.02; GSM8K 63.3055 = its recorded 63.3055), which is how the cause was confirmed rather than guessed. It is worth 1.24 pp on MATH, with no signal in any log.Already pinned in the
[math]extra since #31, so main is correct and no change is needed — those runs simply predate the pin. Recorded in both tasks'reference_impl.notesso the next person reading an old number knows why it is low, and pinned by fourtest_symbolic_equality_still_workscases drawn from the real disagreements.Review fixes (
24ac2c85+5ddcdbae)Prose and metadata only — the guards and the refusal are untouched, and behaviour is unchanged.
N, notsimplifyandN.simplify(a - b)never sees the raw text: the subtraction runs first and sympy's arithmetic dunders sympify strictly, sostr - strandstr - Exprboth raiseTypeErrorbeforesimplifyis entered (verified both).Nalone is the whole vector, and alone it is enough to defeat a parse-only guard — so the case for refusing the fallback is unchanged, but "both sympify" would have pointed the next person hardening this code at the wrong call. Corrected in the module docstring, the inline comment, both tasks'reference_impl.notesandindex.json.**tower (2**3**2) and an integer exponent aboveMAX_EXPONENT— both of which upstream evaluates. Unreachable while the antlr4 pin holds, sinceparse_latexresolves those spellings first, and the measured zero covers the disabled-parse_latexcells, so neither occurs in either stored run. Worth naming because it is reachable in exactly the environment the same note warns about.quotes_freenow carries evidence for both dialects. It generalized to both graders on the move but cited only UGMathBench's 42,064 quote-free gold slots; deepseek reads LaTeX, where the evidence is the replay above.sieval/community/CLAUDE.mdno longer contradicts its own contents. The charter said the directory holds "not original code" while_sympy_guards.pyis exactly that, and the package-wide ruff/mypy/pre-commit exclusions — which exist to keep vendored code byte-identical — silently cover it, which is the wrong default for the module holding a security boundary. Documents the coupling that earns its place here, how to lint it by hand meanwhile, and the bar for adding more original code. Narrowing the exclusions to the vendored paths is left to its own change.Checklist
Required (all PRs)
AI-Generated Code - <model> (<provider>)in module docstringcore/ugmathbench.py's call sites were updated with them; no other referent existedIf: community/ Changes
deepseek_math.py, with the measurementIf: Breaking Change
Not breaking — no schema change, no metric moves, old result directories read the same.
🤖 Generated with Claude Code