Harden sandbox: bash comment / newline tokenization, sqlite .backup/.open operands, iconv -o, timeit strings, PATH / git command substitutions

Close eight shell-scanner and dynamic-execution gaps Codex found on the round-57 branch (all P1).

- bash # comments and unquoted newlines: the command scanner rewrote unquoted
  newlines to ` ; ` before shlex, and shlex's own # handling fired mid-word, so
  `echo ok #\nsed -i ...` (a comment swallowing the synthesized separator) and
  `echo ok#; sed -i ...` (a mid-word # treated as a comment) hid the second
  command. Strip bash comments at their real physical-line boundaries first, then
  rewrite newlines, and clear shlex's commenters so its non-bash-accurate # parsing
  cannot re-introduce the miss.

- shell newlines in the sensitive-read scan: the read scanner never rewrote
  newlines, so `echo ok\ncat /etc/passwd` read `cat /etc/passwd` as arguments of
  the non-reader `echo`. Apply the same comment-strip + newline-rewrite so each
  physical line starts a fresh command context.

- sqlite dot-command file operands: the dot-file check only inspected the first
  operand and omitted .open, so `.backup main /tmp/x` (the file is the LAST
  operand, after an optional schema name) and `.open /tmp/x` created databases
  outside the workdir. Scan .backup / .save / .open by their last operand and add
  .open to the file-operand set.

- iconv output files: iconv writes its converted output to -o / --output in an
  unguarded child, so `printf x | iconv -o /tmp/p` escaped. Block an escaping
  -o FILE / --output FILE / --output=FILE / -oFILE operand.

- timeit string execution: timeit.timeit / .repeat / Timer(...) compile and
  execute their stmt / setup STRING arguments, so
  `timeit.timeit("import os; os.system('...')")` ran outside the eval/exec gate.
  Analyze the stmt / setup strings like exec payloads (a benign body passes, a
  callable stmt carries no source and is left alone).

- command substitutions in PATH assignments: the PATH-entry check only recognized
  $VAR expansions, so `PATH=$(pwd) evil` was treated as a trusted expansion and a
  planted workdir executable could be resolved through it. Treat a $() / backtick
  command substitution in a PATH value as a dynamic, unsafe entry.

- dynamic git path operands: path-valued git operands only resolved same-command
  $VAR assignments, so `git init $(printf /tmp/x)` and the backtick form were
  accepted and native git created the path outside the workdir. Treat a $() /
  backtick command substitution in a git path operand as escaping (tokenization
  splits the substitution into separators, so the fragments are re-detected).

Regression coverage: TestRound58Bypasses in tests/test_sandbox_tools.py (comment /
newline command positions for the write and read scans, sqlite .backup / .save /
.open operands, iconv -o forms, PATH and git command substitutions, timeit
stmt / setup string execution) plus a round58 benign-allowed set (a real trailing
comment, a benign second line, workdir-relative git, iconv with no output file,
trusted PATH expansions, workdir-local sqlite .backup / .open, a benign timeit
body, and timeit.default_timer with no code string).
This commit is contained in:
danielhanchen 2026-07-11 03:02:05 +00:00
commit bb1d6c9c7e
2 changed files with 329 additions and 3 deletions

View file

@ -292,15 +292,23 @@ _ARGV_TAIL_SCAN_COMMANDS = frozenset(
_OPENSSL_WRITE_FLAGS = frozenset(
{"-out", "-writerand", "-keyout", "-CAout", "-CAkeyout", "-CAserial"}
)
# iconv writes its converted output to the -o / --output file in an unguarded child, so an
# escaping value writes a host path the realpath guard never sees (printf x | iconv -o /tmp/p).
_ICONV_WRITE_FLAGS = frozenset({"-o", "--output"})
# sqlite3 CLI dot-commands that WRITE (or read) an arbitrary file argument in the unguarded
# child: `.output FILE` / `.once FILE` redirect query output to FILE, `.excel` / `.import` /
# `.backup FILE` / `.save FILE` / `.dump FILE` / `.clone FILE` create files, `.log FILE` writes
# a log, and `.read FILE` sources SQL from FILE. A FILE that escapes the workdir writes / reads a
# host path the realpath guard never sees. The group captures the FILE operand for a path check.
_SQLITE_DOTFILE_RE = re.compile(
r"(?m)^\s*\.(?:output|once|excel|import|backup|save|dump|clone|log|read)\b\s+(?:-{1,2}\S+\s+)*"
r"(?m)^\s*\.(?:output|once|excel|import|dump|clone|log|read)\b\s+(?:-{1,2}\S+\s+)*"
r"(?P<f>(?:'[^']*'|\"[^\"]*\"|\S+))"
)
# .backup ?DB? FILE / .save ?DB? FILE put the written FILE LAST (an optional schema name precedes
# it), and .open ?OPTIONS? FILE puts the opened/created database file after its options. The first
# operand of these (a schema name, or an option token) is not the file, so capture the whole tail
# and check the LAST bare operand instead of the first.
_SQLITE_LASTFILE_RE = re.compile(r"(?m)^\s*\.(?:backup|save|open)\b[^\n]*")
# sqlite3 dot-commands that RUN a system shell command in the unguarded child: `.shell CMD` /
# `.system CMD` ("Run CMD ARGS... in a system shell"), and `.excel` (opens the result in a
# system program). These execute regardless of any path check, so match the command itself.
@ -429,6 +437,12 @@ def _path_value_is_unsafe(value: str, assignments = None) -> bool:
# ~ / ~user expand to HOME, which is the session workdir in the sandbox.
if e.startswith("~"):
return True
# A command substitution $(...) / `...` in a PATH entry is a DYNAMIC value the analyzer
# cannot resolve (PATH=$(pwd) points the search list at the cwd, where an earlier sandboxed
# step may have planted an executable), so treat it as unsafe rather than a trusted $VAR
# expansion -- otherwise the $ branch below swallows $( as a non-matching variable.
if "$(" in e or "`" in e:
return True
if e.startswith("${"):
inner = e[2:]
if inner.endswith("}"):
@ -540,6 +554,12 @@ def _git_operand_escapes(tok: str, assigns = None) -> bool:
earlier in the SAME command, as the WHOLE token (``OUT=/tmp/repo; git init $OUT``) OR as a
PREFIX (``P=/tmp; git init $P/repo``, ``openssl rand -out $P/key``). An unknown external
expansion is left to the literal check (so ``git clone $REPO_URL`` is not a false positive)."""
# A command substitution $(...) / `...` operand (git init $(printf /tmp/x)) is a DYNAMIC path
# the analyzer cannot resolve: the real shell expands it and native git creates the result
# outside the workdir, so fail closed. Tokenization splits `$(` into a bare `$` and `(`
# (and backticks into their own tokens), so the fragment left as the operand is `$` / `` ` ``.
if tok in ("$", "`") or "$(" in tok or "`" in tok:
return True
m = re.match(r"\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?(.*)$", tok)
if m and assigns and m.group(1) in assigns:
return _arg_escapes_workdir(assigns[m.group(1)] + m.group(2))
@ -1067,6 +1087,62 @@ def _rewrite_unquoted_newlines(command: str) -> str:
return "".join(out)
def _strip_bash_comments(command: str) -> str:
"""Remove bash ``#`` comments, respecting quotes / escapes. A ``#`` begins a comment only at a
WORD BOUNDARY (start of string, or after unquoted whitespace / a metacharacter) and runs to the
end of the PHYSICAL line; a ``#`` inside a word (``echo ok#``) or inside quotes is literal. Run
BEFORE newline rewriting so each comment terminates at its real line break rather than a
synthesized ``;`` separator, and pair it with ``lexer.commenters = ""`` so shlex (whose default
``#`` handling is not bash-accurate and fires mid-word) does not re-introduce the miss."""
out = []
q = None
i = 0
n = len(command)
boundary = True # the start of the string is a word boundary
while i < n:
ch = command[i]
if q == "'":
out.append(ch)
if ch == "'":
q = None
boundary = False
i += 1
continue
if q == '"':
out.append(ch)
if ch == "\\" and i + 1 < n:
out.append(command[i + 1])
i += 2
continue
if ch == '"':
q = None
boundary = False
i += 1
continue
if ch == "\\" and i + 1 < n:
out.append(ch)
out.append(command[i + 1])
boundary = False
i += 2
continue
if ch in ("'", '"'):
out.append(ch)
q = ch
boundary = False
i += 1
continue
if ch == "#" and boundary:
# A comment runs to the end of the physical line; drop it but KEEP the newline so it
# still separates the following command.
while i < n and command[i] not in ("\n", "\r"):
i += 1
continue
out.append(ch)
boundary = ch in (" ", "\t", "\n", "\r", ";", "&", "|", "(", ")", "<", ">")
i += 1
return "".join(out)
def _mask_quoted_separators(command: str) -> str:
"""Neutralize command-boundary characters that are DATA inside quotes (blank them to a
space) so the regex command-position scan does not treat a quoted separator -- echo
@ -1334,6 +1410,10 @@ def _find_blocked_commands(command: str) -> set[str]:
"""
blocked: set[str] = set()
# Strip bash # comments FIRST (at their real physical-line boundaries), so a comment does not
# swallow the ` ; ` synthesized from a following newline (echo ok #\nsed -i ...) and a mid-word
# # (echo ok#; rm ...) is not mistaken by shlex for a comment. commenters is cleared below too.
command = _strip_bash_comments(command)
# Normalize bash ANSI-C ($'...') / locale ($"...") quoting first: shlex leaves
# `$'touch'` as `$touch`, so a writer/interpreter hidden behind ANSI-C quoting would
# never match the blocklist even though bash decodes and runs it. Then expand ${IFS} to
@ -1362,6 +1442,9 @@ def _find_blocked_commands(command: str) -> set[str]:
else:
lexer = shlex.shlex(command, posix = True, punctuation_chars = ";&|()`<")
lexer.whitespace_split = True
lexer.commenters = (
"" # bash comments are pre-stripped; shlex's # handling is not bash-accurate
)
tokens = list(lexer)
except ValueError:
tokens = command.split()
@ -1851,7 +1934,13 @@ def _find_blocked_commands(command: str) -> set[str]:
# "$PATH" + value (a trailing / doubled separator or . entry is then the unsafe one).
elif _an == "PATH":
_pval = ("$PATH" + _av) if _append else _av
if _path_value_is_unsafe(_pval, _local_assigns):
# PATH=$(pwd) / PATH=/x:$(cmd): a command substitution in the value is a DYNAMIC search
# path (it can point at the cwd where an earlier step planted an exe). Tokenization
# splits `$(` into a trailing `$` on this token and a following `(`, so detect that
# shape here; a backtick form leaves an empty value token which _path_value_is_unsafe
# already flags.
_pathsub = _av.endswith("$") and _ei + 1 < len(tokens) and tokens[_ei + 1] == "("
if _pathsub or _path_value_is_unsafe(_pval, _local_assigns):
blocked.add("unsafe-path-assign")
# GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE / GIT_OBJECT_DIRECTORY / ... set git's repo /
# tree / index / object-store path directly, so an escaping value writes outside the workdir
@ -1932,6 +2021,12 @@ def _find_blocked_commands(command: str) -> set[str]:
_seg = []
for k in range(i + 1, len(tokens)):
if tokens[k] in _SHELL_SEPARATORS or tokens[k] in _SHELL_KEYWORDS_AS_SEP:
# A command substitution ( `...` / $(...) ) used as a git operand (git init
# `printf /tmp/x` / git worktree add $(pwd)/out) is split by tokenization into
# separator tokens; re-inject a backtick marker so the operand scan flags it as a
# dynamic escaping path. A backtick starts one directly; `$(` leaves a trailing `$`.
if tokens[k] == "`" or (tokens[k] == "(" and _seg and _seg[-1].endswith("$")):
_seg.append("`")
break
_seg.append(tokens[k])
_joined = " ".join(_seg)
@ -2122,6 +2217,30 @@ def _find_blocked_commands(command: str) -> set[str]:
):
blocked.add("openssl-write-outside")
# iconv -o FILE / --output FILE / --output=FILE / -oFILE writes FILE in an unguarded iconv
# child. Block when the output path escapes the workdir; a workdir-local -o and the no-output
# forms (iconv -f utf8 -t utf16 file, printing to stdout) stay allowed.
for i in _cmd_word_idx:
if _token_basename(tokens[i]) != "iconv":
continue
_ic_cwd_escapes = _cwd_wrapper_escapes(tokens, i)
for k in range(i + 1, len(tokens)):
t = tokens[k]
if t in _SHELL_SEPARATORS or t in _SHELL_KEYWORDS_AS_SEP:
break
_op = None
if t in _ICONV_WRITE_FLAGS and k + 1 < len(tokens):
_op = tokens[k + 1] # separated form: -o FILE / --output FILE
elif t.startswith("--output="):
_op = t[len("--output=") :]
elif t.startswith("-o") and len(t) > 2:
_op = t[2:] # glued short form: -oFILE
if _op is not None and (
_git_operand_escapes(_op, _local_assigns)
or (_ic_cwd_escapes and _operand_relative_local(_op))
):
blocked.add("iconv-write-outside")
# sqlite3 <DBFILE> creates / opens a database in an unguarded child (no realpath guard), and
# its dot-commands (.output / .backup / .dump / .read ...) read + write arbitrary files. Flag
# a DBFILE operand that escapes the workdir, and any dot-file target that escapes. A local DB
@ -2188,6 +2307,23 @@ def _find_blocked_commands(command: str) -> set[str]:
or (_sqlite_cwd_escapes and _operand_relative_local(_dot_f))
):
blocked.add("sqlite3-write-outside")
# .backup / .save / .open put the target FILE as the LAST operand (an optional schema
# name or option tokens precede it), so check the last bare operand for an escape.
for _m in _SQLITE_LASTFILE_RE.finditer(_unq):
try:
_ops = shlex.split(_m.group(0).strip())
except ValueError:
_ops = _m.group(0).split()
_tail = [
_o for _o in _ops[1:] if not _o.startswith("-")
] # drop the dot-command word and option flags
if _tail:
_bk_f = _tail[-1]
if _bk_f not in ("stdout", "stderr", "off") and (
_git_operand_escapes(_bk_f, _local_assigns)
or (_sqlite_cwd_escapes and _operand_relative_local(_bk_f))
):
blocked.add("sqlite3-write-outside")
if t.startswith("-"):
_sk += 1
continue
@ -6110,8 +6246,15 @@ def _scan_command_string_for_reads(
recursively scanned, so a read hidden behind a normal command-prefix form is still caught."""
if not command or _depth > 6:
return None
# Strip bash # comments and rewrite unquoted newlines to `;` so each physical shell line starts
# a fresh command context: without this, `echo ok\ncat /etc/passwd` reads `cat /etc/passwd` as
# arguments of the non-reader `echo` and the real second-line read is missed.
# Model bash brace expansion so a brace-hidden reader / path (`{cat,/etc/passwd}`) is seen.
cmd = _expand_braces(_expand_ifs(_normalize_ansi_c_quotes(command)))
cmd = _expand_braces(
_expand_ifs(
_normalize_ansi_c_quotes(_rewrite_unquoted_newlines(_strip_bash_comments(command)))
)
)
def _traversal_hits_sensitive(norm):
# A relative path that climbs out of the workdir with '..' can name a host secret
@ -6162,6 +6305,9 @@ def _scan_command_string_for_reads(
try:
_lx = shlex.shlex(cmd, posix = True, punctuation_chars = ";&|()`<>")
_lx.whitespace_split = True
_lx.commenters = (
"" # bash comments are pre-stripped; shlex's # handling is not bash-accurate
)
ptoks = list(_lx)
except ValueError:
ptoks = cmd.split()
@ -6995,6 +7141,52 @@ def _check_signal_escape_patterns(
}
)
def _analyze_timeit_code_arg(call_node, arg_node, label):
"""Analyze a timeit stmt / setup argument, which timeit COMPILES and EXECUTES. A string
(literal or foldable) is recursed like an exec payload -- a benign body (sum(range(10)))
passes, a shell / escape body blocks. A non-string arg (a callable stmt, timeit's other
supported form) carries no source and is left alone, so ordinary timeit use is not blocked."""
if arg_node is None:
return
try:
_src = _const_fold(arg_node, _const_env)
if not isinstance(_src, str):
# A bare string literal that _const_fold declined (kept for clarity) is still source.
if isinstance(arg_node, ast.Constant) and isinstance(arg_node.value, str):
_src = arg_node.value
else:
return # a callable stmt / opaque non-string arg carries no analyzable source
parsed_kind, _ = _safe_parse_inner(_src, "exec", _depth, _budget)
if parsed_kind == "PARSED":
_inner_safe, _inner_info = _check_signal_escape_patterns(_src, _depth + 1, _budget)
if not _inner_safe and not _inner_info.get("error"):
dynamic_exec.append(
{
"type": "dynamic_exec",
"line": getattr(call_node, "lineno", -1),
"description": (
f"timeit {label} string reaches unsafe operation: "
f"{_first_unsafe_reason(_inner_info)}"
),
}
)
elif parsed_kind == "BOUND_HIT":
dynamic_exec.append(
{
"type": "dynamic_exec",
"line": getattr(call_node, "lineno", -1),
"description": f"timeit {label} string exceeds static-analysis bounds",
}
)
except Exception:
dynamic_exec.append(
{
"type": "dynamic_exec",
"line": getattr(call_node, "lineno", -1),
"description": f"timeit {label} string could not be statically verified",
}
)
def _ast_name_matches(node, names):
if isinstance(node, ast.Name):
return node.id in names
@ -7345,6 +7537,12 @@ def _check_signal_escape_patterns(
self.runpy_aliases = {"runpy"}
# from runpy import run_path as X / run_module as Y -> {"X", "Y"}.
self.runpy_func_aliases: set[str] = set()
# import timeit as t -> {"timeit", "t"}. timeit.timeit / .repeat / Timer(...) COMPILE
# and EXECUTE their stmt / setup STRING args, so a string payload there is analyzed
# like an exec payload (a callable stmt carries no source and stays allowed).
self.timeit_aliases = {"timeit"}
# from timeit import timeit as X / repeat as Y / Timer as Z -> {"X", "Y", "Z"}.
self.timeit_func_aliases: set[str] = set()
# import inspect as i -> {"inspect", "i"}. inspect.getclosurevars(fn) hands back
# the cells a guard wrapper closes over (the original unguarded callable), so
# treat it as a closure-recovery gadget like __closure__ / cell_contents.
@ -7414,6 +7612,8 @@ def _check_signal_escape_patterns(
self.types_aliases.add(alias.asname or "types")
elif alias.name == "runpy":
self.runpy_aliases.add(alias.asname or "runpy")
elif alias.name == "timeit":
self.timeit_aliases.add(alias.asname or "timeit")
elif alias.name == "inspect":
self.inspect_aliases.add(alias.asname or "inspect")
elif alias.name == "operator":
@ -7524,6 +7724,12 @@ def _check_signal_escape_patterns(
for alias in node.names:
if alias.name in ("spawn", "fork"):
self.pty_func_aliases.add(alias.asname or alias.name)
elif node.module == "timeit":
# from timeit import timeit / repeat / Timer: bare-name aliases of the
# string-executing entry points.
for alias in node.names:
if alias.name in ("timeit", "repeat", "Timer"):
self.timeit_func_aliases.add(alias.asname or alias.name)
elif node.module == "inspect":
for alias in node.names:
if alias.name == "getclosurevars":
@ -8840,6 +9046,28 @@ def _check_signal_escape_patterns(
# eval / exec / compile (bare builtin, single-assignment alias, builtins
# attribute / subscript, inline container, or an indirect callee expression --
# a ternary / boolean fallback -- that evaluates to one of them).
# timeit.timeit / .repeat / Timer(...) (attribute, from-import, or single-assignment
# alias) COMPILE and EXECUTE their stmt (arg0 / kw 'stmt') and setup (arg1 / kw 'setup')
# STRING args, so analyze those like exec payloads (a callable stmt carries no source).
if _analyzer_on and (
(
isinstance(func, ast.Attribute)
and func.attr in ("timeit", "repeat", "Timer")
and _ast_name_matches(func.value, self.timeit_aliases)
)
or (isinstance(func, ast.Name) and func.id in self.timeit_func_aliases)
or self._rhs_module_attr(func, ("timeit", "repeat", "Timer"), self.timeit_aliases)
):
_t_stmt = node.args[0] if node.args else None
_t_setup = node.args[1] if len(node.args) > 1 else None
for _kw in node.keywords:
if _kw.arg == "stmt":
_t_stmt = _kw.value
elif _kw.arg == "setup":
_t_setup = _kw.value
_analyze_timeit_code_arg(node, _t_stmt, "stmt")
_analyze_timeit_code_arg(node, _t_setup, "setup")
exec_func_id = self._resolve_exec_callee(func)
if exec_func_id is not None:

View file

@ -5655,3 +5655,101 @@ class TestRound57Bypasses:
)
def test_round57_benign_allowed(self, code):
_ok(code)
class TestRound58Bypasses:
# bash # comments and unquoted newlines: a comment must terminate at its physical line (not the
# synthesized ; from a following newline), a mid-word # (echo ok#) is not a comment, and each
# physical line starts a fresh command position for both the write scan and the read scan.
@pytest.mark.parametrize(
"cmd",
[
"echo ok #\nsed -i s/a/b/ /tmp/p", # comment ends at newline, second line writes
"echo ok#; sed -i s/a/b/ /tmp/p", # mid-word # is literal; ; then a writer
"true\ntouch /tmp/p", # newline separates a fresh writer command
],
)
def test_comment_and_newline_command_positions(self, cmd):
_blocked(_sh(cmd), expect_phrase = "blocked command")
def test_read_after_newline_scanned(self):
_blocked(_sh("echo ok\ncat /etc/passwd"), expect_phrase = "/etc/passwd")
@pytest.mark.parametrize(
"cmd",
[
# sqlite dot-command file operands: .backup ?DB? FILE (file is LAST) and .open FILE.
"sqlite3 :memory: '.backup main /tmp/x'",
"sqlite3 :memory: '.save main /tmp/x'",
"sqlite3 :memory: '.open /tmp/x' 'create table t(x)'",
"sqlite3 :memory: '.open --new /tmp/x'",
],
)
def test_sqlite_dotfile_operands(self, cmd):
_blocked(_sh(cmd), expect_phrase = "blocked command")
@pytest.mark.parametrize(
"cmd",
[
"printf hi | iconv -o /tmp/p", # -o FILE
"iconv --output=/tmp/p f", # --output=FILE
"iconv --output /tmp/p f", # --output FILE
"iconv -o/tmp/p f", # glued -oFILE
],
)
def test_iconv_output_escape(self, cmd):
_blocked(_sh(cmd), expect_phrase = "blocked command")
@pytest.mark.parametrize(
"cmd",
[
"PATH=$(pwd) evil", # command substitution in PATH value
"PATH=/x:$(pwd) evil",
"PATH=`pwd` evil", # backtick form
],
)
def test_path_command_substitution(self, cmd):
_blocked(_sh(cmd), expect_phrase = "blocked command")
@pytest.mark.parametrize(
"cmd",
[
"git init $(printf /tmp/x)", # $() operand
"git init `printf /tmp/y`", # backtick operand
"git worktree add $(pwd)/out",
],
)
def test_git_dynamic_path_operand(self, cmd):
_blocked(_sh(cmd), expect_phrase = "blocked command")
@pytest.mark.parametrize(
"code",
[
# timeit compiles + executes its stmt / setup STRING args, so an escape body blocks.
"import timeit\ntimeit.timeit(\"import os; os.system('touch /tmp/p')\", number=1)",
"import timeit\ntimeit.Timer(\"__import__('os').system('touch /tmp/p')\").timeit()",
'import timeit\ntimeit.timeit("x=1", setup="import os; os.system(\'touch /tmp/p\')")',
"import timeit as _t\n_t.repeat(\"import os; os.system('touch /tmp/p')\")",
],
)
def test_timeit_string_execution(self, code):
_blocked(code, expect_phrase = "timeit")
@pytest.mark.parametrize(
"code",
[
# Benign forms across the round-58 checks stay allowed.
_sh("echo hi # a trailing comment"), # a real comment
_sh("echo ok\necho bye"), # benign second line
_sh("git init repo"), # workdir-relative git
_sh("iconv -f utf8 -t utf16 file.txt"), # iconv with no output file
_sh("PATH=$PATH echo hi"), # trusted PATH expansion
_sh("PATH=/usr/bin echo hi"), # absolute PATH entry
_sh("sqlite3 :memory: '.backup main side.db'"), # workdir-local backup
_sh("sqlite3 :memory: '.open local.db'"), # workdir-local open
"import timeit\ntimeit.timeit('sum(range(10))', number=1)", # benign timeit body
"import timeit\nt = timeit.default_timer()\nprint(t)", # no code-string arg
],
)
def test_round58_benign_allowed(self, code):
_ok(code)