Close three bypasses and one false positive Codex found on the round-43 branch:
- types.FunctionType() of an unvetted code object: the gadget was only flagged
when its first arg was a compile() result, so a code object from any other
producer (codeop.compile_command(), a loader's get_code(), or an opaque
name) ran source the recursive eval/exec analysis never saw. Replace the
compile-only denylist with an allowlist: FunctionType is allowed only when
its first arg is an ordinary in-source function's code object
(fn.__code__ / meth.__func__), whose body is analyzed normally, and fails
closed for everything else. Robust against new producers instead of chasing
each one.
- subscript-stored exec alias: storing a dynamic-exec builtin into a container
element (d['e'] = exec; d['e'](payload)) hid the sink from the name /
attribute call checks -- the alias tracker only followed plain-name targets
-- so the later subscript call ran an unreviewed payload. Flag the store
itself: there is no benign reason to stash exec / eval / compile / __import__
in a container slot.
- deserializer in an imported workdir module: the module import vetter (the
only scan of a helper .py the user wrote) checked shell / eval / import /
network sinks but not deserializers, so a helper calling pickle.loads on
bytes whose reducer runs posix.system spawned an unguarded child. Refuse a
workdir module that calls a reduce-executing deserializer (pickle / marshal /
dill / cloudpickle / jsonpickle load / loads / Unpickler / decode) or binds
one via from-import. json / importing pickle for dumps stay allowed.
- false positive: shell keywords as separators regardless of position. if /
while / until (and then / do / else / elif) were treated as command
separators everywhere, so `echo if touch` was rejected as if `touch` ran even
though it is just data passed to echo. Only reset command position for these
keywords when they appear AT command position (the compound-statement
header); real separators (; | && ...) still reset everywhere, so
`if touch x; then :; fi` stays blocked.
Regression coverage: TestRound44Bypasses in tests/test_sandbox_tools.py
(FunctionType of codeop / loader / producer code objects blocked while
fn.__code__ stays allowed; subscript-stored exec / eval / compile blocked while
a benign container store is allowed; `echo if touch` allowed while the compound
headers stay blocked) and two workdir-module vetter cases in
tests/test_sandbox_runtime_backstop.py (pickle.loads reduce payload denied,
json.loads still allowed).