Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions agents/anyplot/code/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,30 @@
# (`dataclasses._FuncBuilder`), and numpy's file opener (`numpy.lib._datasource.open`).
ESCAPE_ATTRIBUTES: frozenset[str] = frozenset(
{
# Frame, generator, coroutine, traceback and code-object introspection (security review of
# 2026-10-14): a generator's frame reaches the interpreter's builtins without a dunder name.
"gi_frame",
"gi_code",
"gi_yieldfrom",
"cr_frame",
"cr_code",
"cr_await",
"cr_origin",
"ag_frame",
"ag_code",
"ag_await",
"f_back",
Comment thread
MarkusNeusinger marked this conversation as resolved.
Resolved
"f_builtins",
"f_globals",
"f_locals",
"f_code",
"f_trace",
"f_generator", # Python 3.14: the generator or coroutine that owns a frame
"tb_frame",
"tb_next",
"co_code",
"co_consts",
"co_names",
# Native code and raw memory through a value, not an import (second opinion of 2026-10-14):
# `np.zeros(1).ctypes._ctypes.CDLL(None)` reaches libc, `from_address` reads and writes any
# address, `as_strided` views memory outside an array, `.dump` pickles an array to a file;
Expand Down
10 changes: 10 additions & 0 deletions changelog.d/validator-frame-introspection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Fixed

- **The plot-code validator refuses frame and generator introspection.** A security review
of 2026-10-14 found that a generator's frame (`gi_frame`, then `f_builtins`) reached the
interpreter's builtins without a dunder name, which the SECURITY profile allowed. Frame,
generator, coroutine, traceback and code-object attributes (`gi_frame`, `gi_code`,
`cr_frame`, `f_back`, `f_builtins`, `f_globals`, `f_locals`, `tb_frame`, `co_consts` and
their siblings) are now refused on any object. The renderer's gVisor sandbox (read-only,
no egress, no secrets in its environment) bounded the exposure; catalogue eligibility is
unchanged (285 matplotlib and 287 seaborn files). (#12169)
23 changes: 23 additions & 0 deletions tests/unit/agents/code/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ def test_a_reader_call_says_where_the_data_comes_from(self) -> None:
"import cmath\nz = cmath.exp(1j)\n",
"v = df._values\n", # a private attribute of a value, not of an imported module
"top = df.main.max()\n", # a column called main: calendar.main is banned on the calendar module only
"up = df['close'] >= df['open']\n", # OHLC columns: a key named like a builtin is data, not a reach for it
]
ESCAPES = [
("import warnings\nline = warnings.formatwarning('', UserWarning, 'f', 1)\n", "banned-call"),
Expand Down Expand Up @@ -464,6 +465,16 @@ def test_a_reader_call_says_where_the_data_comes_from(self) -> None:
("import matplotlib\nrun = matplotlib.subprocess\n", "banned-attribute"),
("import matplotlib\nm = matplotlib.os\n", "banned-attribute"),
("import warnings\nw = warnings.sys\n", "banned-attribute"),
# Frame and generator introspection (security review of 2026-10-14): each name alone is refused.
("g = (i for i in range(1))\nframe = g.gi_frame\n", "banned-attribute"),
("g = (i for i in range(1))\ncode = g.gi_code\n", "banned-attribute"),
("names = frame.f_builtins\n", "banned-attribute"),
("names = frame.f_globals\n", "banned-attribute"),
("caller = frame.f_back\n", "banned-attribute"),
("names = frame.f_locals\n", "banned-attribute"),
("frame = error.tb_frame\n", "banned-attribute"),
("frame = coroutine.cr_frame\n", "banned-attribute"),
("values = code.co_consts\n", "banned-attribute"),
Comment thread
MarkusNeusinger marked this conversation as resolved.
Resolved
# Native code and raw memory through a value (second opinion of 2026-10-14): these passed before.
('lib = np.zeros(1).ctypes._ctypes.CDLL(None)\nlib.system(b"id")\n', "banned-attribute"),
(
Expand Down Expand Up @@ -501,6 +512,18 @@ def test_allowed(self, body: str) -> None:
def test_escape_is_caught(self, body: str, rule: str) -> None:
assert rule in security_rules(body)

@pytest.mark.parametrize(
"name",
[
*("gi_frame", "gi_code", "gi_yieldfrom", "cr_frame", "cr_code", "cr_await", "cr_origin"),
*("ag_frame", "ag_code", "ag_await", "f_back", "f_builtins", "f_globals", "f_locals"),
*("f_code", "f_trace", "f_generator", "tb_frame", "tb_next", "co_code", "co_consts", "co_names"),
],
)
def test_every_introspection_attribute_is_refused(self, name: str) -> None:
"""Each spelling of the 2026-10-14 frame and generator denylist, so a typo cannot reopen one."""
assert "banned-attribute" in security_rules(f"value = thing.{name}\n")

def test_a_private_module_in_an_import_path_is_named_and_logged_by_its_known_prefix(self) -> None:
findings = validate_security(plot("import scipy.fft._backend as backend\n"), library="matplotlib")

Expand Down
Loading