Skip to content

fix(agents): refuse native code and raw memory reached through a value - #12167

Merged
MarkusNeusinger merged 4 commits into
mainfrom
fix/validator-escape-names
Oct 14, 2026
Merged

MarkusNeusinger merged 4 commits into
mainfrom
fix/validator-escape-names

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • The SECURITY profile had a gap that a second opinion found on 2026-10-14. A NumPy value exposes .ctypes, so code can reach libc (np.zeros(1).ctypes._ctypes.CDLL(None).system(...)) or read and write any address (type(...).from_address(0)). The private-name rule fires only on imported names, so the call result passed.
  • The fix bans these names as attributes on any object: ctypes, _ctypes, cffi, data_as, from_address, from_buffer(_copy), as_strided, dump(s), extbuild, get_rdataset and webuse.
  • .to_string() and .info() are refused only with a target (buf). Without one they return text and stay allowed. sliding_window_view stays allowed too.
  • Why now: the planned run_data_code tool would hand user-written Python to this validator. Today the code still comes from the adapter model, and the gVisor sandbox (no egress) contains the damage, but the validator must not have the gap.

Plan

N/A

Test plan

  • 13 new escape cases in ESCAPES. pytest tests/unit tests/integration: 10,024 passed. ruff passes.
  • Catalogue eligibility sweep (agents.evals.eligibility) is identical before and after: 285 matplotlib and 287 seaborn files eligible, same SECURITY rule counts.

Second opinion of 2026-10-14: np.zeros(1).ctypes._ctypes.CDLL(None) and
type(...).from_address(0) passed the SECURITY profile, since the
private-name rule applies only to imported names. Ban ctypes, _ctypes,
cffi, data_as, from_address, from_buffer(_copy), as_strided, dump(s),
extbuild, get_rdataset and webuse on any object, and to_string()/info()
with a target. Catalogue eligibility unchanged (285 / 287).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Copilot AI balanced review requested due to automatic review settings October 14, 2026 09:05
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Aliased buffer-writer methods bypass validation, while safe targetless info calls can be rejected.

2 open findings
What changed in this PR

Hardens the plot-code validator against native-code, raw-memory, network, and buffer-write escapes.

Changes:

  • Expands banned escape attributes.
  • Adds targeted buffer-writer validation.
  • Adds regression tests and a changelog fragment.
File Description
agents/​anyplot/​code/​validate.py Adds escape names and buffer-writer checks.
tests/​unit/​agents/​code/​test_validate.py Adds security regression cases.
changelog.d/​validator-native-escape-names.md Documents the validator hardening.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agents/anyplot/code/validate.py Resolved
Comment thread agents/anyplot/code/validate.py Resolved
…econd argument

Copilot review of #12167: a method bound to a name hid its target from
the call check, and info() takes verbose first. The buffer writers may now
only be called where they are named, and a call is refused when its buf
(position 0 for to_string, 1 for info, or the keyword) is not None, or a
starred argument could carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pandas string dispatch can still invoke to_string or info with a writable buffer, bypassing the new validation.

1 open finding
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity Include from_buffer_copy and dumps in the exhaustive validator list

changelog.d/​validator-native-escape-names.md:8

This exhaustive list omits two names added by this PR: from_buffer_copy and dumps. Include them so the release note accurately describes the new validator behavior.

🧠 Review effort: Balanced

Comment thread agents/anyplot/code/validate.py Resolved
@codecov

codecov Bot commented Oct 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot review of #12167: df.agg("to_string", buf=...) reached the
writer by name; _check_dispatch_name now includes the buffer writers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@MarkusNeusinger
MarkusNeusinger merged commit e0edecd into main Oct 14, 2026
15 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the fix/validator-escape-names branch October 14, 2026 09:43
MarkusNeusinger added a commit that referenced this pull request Oct 14, 2026
…12172)

## Summary

- **Data from code, offline (owner decision of 2026-10-14).** A user can
paste or type a snippet that generates the data (Python, JavaScript, R,
a formula from a maths or physics textbook), or name a computation in
words ("draw the Lorenz attractor"). The root translates it into Python,
and the new root tool `run_data_code(code, source_language)` runs it in
the renderer sandbox. The resulting table becomes the next dataset
version, which the user confirms.
- **Which tool computes the data depends on what the computation
needs.** A closed formula over one or two ranges stays on the safe
formula language (`generate_data`). Numerical integration (ODEs),
iteration, random numbers, cumulative sums or several dependent steps go
to `run_data_code`.
- **Layers, each designed and reviewed by a Fable second opinion before
and after the build:**
  - **Validator data profile** (`validate_data_code`):
    - Every SECURITY rule except savefig, and no plotting stack.
- Allowed: `random`, scipy computing modules. Not allowed: sklearn,
statsmodels.
    - Every private attribute on any receiver is banned.
    - A top-level `df` is required.
- Literal-data caps: 20 per container and 40 per file, for numbers and
for text, counting list repetition. The model cannot write rows from
memory.
    - The `__main__` guard is stripped first.
- It inherits #12167 and #12169: native code, raw memory, and frame
introspection are refused.
  - **Harness data mode** (`harness.py --data`):
- Imports everything first, then arms an audit hook that refuses every
`open` (read too), every new import, directory probes, ctypes,
subprocess, sockets, exec/fork, marshal and pickle, and every route to a
frame.
    - Catches everything, `SystemExit` included.
- Takes `df`, resets a non-default index, refuses MultiIndex columns,
and writes `out.csv` under the shared caps.
- **Renderer route:** an additive `POST /run-data` with its own wire
models (`extra="forbid"`), the same caller check, slot, watchdog and
kill path, and source ≤ 16 KiB, timeout ≤ 30 s. An old renderer answers
404, which the tool maps to `unavailable`. The deploy smoke runs a 2-row
snippet.
- **No raw snippet output reaches the model.** Errors are a fixed
vocabulary (exception class and line only); stdout is discarded.
- **One ingestion guard for pastes and code tables.**
`dataset_guard.check_dataset` (pre-filter + dataset judge) runs on both,
so cells generated by code are judged too.
- **Rate and budget.** One rated start per turn through
`ledger.rate_start` / `RunQueue.rate`; the plot pipeline will use the
same helper when the queue becomes pipeline-only. Also 30 data runs per
user per day (`AGENT_DAILY_DATA_RUNS`), at most 2 calls per turn, and a
6,000-character code cap.
- **Dataset route.** A classifier runs before parsing and answers `422
code` or `422 formula`. The page offers `.send()` to the chat for code
within 2,000 characters.
  - **Scope rubric:**
    - Data code is in scope.
    - A general-purpose script is out of scope.
    - Code that touches files, the network or the system is an attack.
- off-053 (a sine wave "for my homework") is now `in_scope` under the
formula path.
- **UI.** The data panel shows the Python that made a version in a
collapsed `# code that made this data` block.

## Deploy order

The renderer should deploy first (`/run-data`), then the agents service.
Either order is safe: against an old renderer, the tool answers
`unavailable` and runs nothing.

## Measurements

- **Scope eval on the real judge** (Claude Haiku 5.5, `eu`, the combined
state after merging main): passed. 219 cases, adversarial recall 100 %,
false refusals 0 %, exact 99.1 %, and 4 cases unavailable (the judge's
answer failed its schema twice).
- **Prep eval for tool choice on the real model:** the three cases
described in words (Lorenz ODE, logistic map, random walk) chose
`run_data_code`, and the four formula cases chose `generate_data`. The
fake renderer's fixture table made the root re-run, so table sizes are
checked only with a real renderer, after deploy.

## Plan

`docs/concepts/agent-network.md` (data tools, renderer API, threat
table) and `agents/README.md`.

## Test plan

- [x] `pytest tests/unit tests/integration`: 10,464 passed. ruff, ruff
format, mypy and the changelog check pass.
- [x] Frontend: lint, Prettier, type-check, 1,020 Vitest tests and build
pass.
- [x] Harness data mode was run in subprocesses against the escapes from
both Fable reviews; each one fails the run. pandas, numpy and scipy work
under the guard.
- [x] Scope eval and the routing prep cases, as above.
- [ ] After deploy: the renderer's `/run-data` smoke, then the live test
round (Swiss BFS data, a JS snippet, Lorenz in words, an attempt at a
general-purpose script).
- **Not verified:** the audit hook in plot mode (it is armed in data
mode only, because there is no local Docker for a catalogue render
sweep), and the data panel in a browser on both viewports and themes.
The live test round covers the panel.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants