Job kind -> runner command line (docs/dev/ARCHITECTURE.md §6, the job engine).
Every "module" kind runs simnibs_python -m <module> <spec_path> — the existing
__main__.py contract every runner already implements or will implement (B4). tools is
different: it doesn't take a spec.json at all, it runs an arbitrary module with explicit args
from the config -- it is therefore allowlisted to tit.tools.*
modules only (ra_14 finding #2): any importable module (-m http.server, -m pip, an
attacker's own package on PYTHONPATH) would otherwise be remote code execution for any
token holder, and in the container that's host root via the mounted docker.sock.
The allowlist closes which module runs; :func:check_tool_args closes what it can be told
to touch (RUN-06). Every path-shaped config.args value -- a positional, an option's next
argument, --opt=value, or a value attached to a short option (-o/out.stl) -- must
resolve inside the manager's project root, checked when the argv is built, before the job is
spawned and therefore before any file is created. A tools job whose args mention a path but
whose manager has no project root bound is refused rather than trusted.
KindError
Bases: ValueError
Raised for an unknown kind, or a known kind whose runner module isn't wired up yet.
command_for
The argv to exec for one job. Raises :class:KindError for anything it can't build.
project_dir is the root a tools job's path arguments are jailed to
(:func:check_tool_args); :class:tit.jobs.manager.JobManager binds its own.
Source code in tit/jobs/kinds.py
| def command_for(
kind: str,
config: dict[str, Any],
spec_path: str,
*,
project_dir: str | None = None,
) -> list[str]:
"""The argv to exec for one job. Raises :class:`KindError` for anything it can't build.
*project_dir* is the root a ``tools`` job's path arguments are jailed to
(:func:`check_tool_args`); :class:`tit.jobs.manager.JobManager` binds its own.
"""
config = config or {}
if kind in MODULE_FOR_KIND:
module = MODULE_FOR_KIND[kind]
if not module_exists(module):
raise KindError(
f"kind '{kind}' maps to '{module}', which is not importable in this "
"environment; the TI-Toolbox install is incomplete or damaged."
)
return [PYTHON_INTERPRETER, "-m", module, spec_path]
if kind == "tools":
module = config.get("module")
if not module or not isinstance(module, str):
raise KindError(
"tools job needs a non-empty config.module (dotted module path)"
)
if _resolve_tool_module_path(module) is None:
raise KindError(
f"tools job: module {module!r} is not an allowed tit.tools module "
f"(must be '{TOOLS_MODULE_PREFIX}<name>' and resolve to a file inside "
f"{TOOLS_DIR})"
)
args = _string_list(config.get("args", []))
check_tool_args(args, project_dir)
return [PYTHON_INTERPRETER, "-m", module, *args]
raise KindError(f"unknown job kind: {kind!r}")
|
check_tool_args(args: list[str], project_dir: str | None) -> None
Raise :class:KindError unless every argument of a tools job stays in bounds.
RUN-06: config.args used to be forwarded verbatim, so an allowlisted tool could be
handed an absolute output path and made to write anywhere the container's user can write.
Every value that looks like a path must resolve inside project_dir: positionals (which
covers tit.tools.electrode_overlay's three paths), an option's next argument,
--opt=value, and every reading of a value attached to a short option.
Source code in tit/jobs/kinds.py
| def check_tool_args(args: list[str], project_dir: str | None) -> None:
"""Raise :class:`KindError` unless every argument of a ``tools`` job stays in bounds.
RUN-06: ``config.args`` used to be forwarded verbatim, so an allowlisted tool could be
handed an absolute output path and made to write anywhere the container's user can write.
Every value that looks like a path must resolve inside *project_dir*: positionals (which
covers ``tit.tools.electrode_overlay``'s three paths), an option's next argument,
``--opt=value``, and every reading of a value attached to a short option.
"""
from tit.paths import is_within
def check(where: str, value: str) -> None:
if not _looks_like_a_path(value):
return
if not project_dir:
raise KindError(
f"tools job: {where} {value!r} looks like a path, and this manager has no "
f"project directory to jail it to"
)
resolved = os.path.join(project_dir, os.path.expanduser(value))
if not is_within(project_dir, resolved):
raise KindError(
f"tools job: {where} {value!r} resolves outside the project directory"
)
pending: str | None = None
for arg in args:
if arg.startswith("-") and not os.path.isabs(arg):
option, sep, inline = arg.partition("=")
if sep:
check(option, inline)
if not arg.startswith("--"):
for value in _attached_values(arg):
check(arg, value)
pending = None if sep else option
continue
check(pending or "argument", arg)
pending = None
|
may_spawn_docker_siblings
may_spawn_docker_siblings(kind: str | None, config: Any = None) -> bool
Could a job of this kind/config have started a sibling Docker container?
Used to keep cancellation from touching Docker for the overwhelming majority of jobs that
never could have spawned one -- an unresponsive daemon then cannot delay their cancel.
Conservative on purpose: a pre job whose config does not mention any DWI stage flag at
all (an unrecognised or future shape) is treated as if it might have spawned one.
Source code in tit/jobs/kinds.py
| def may_spawn_docker_siblings(kind: str | None, config: Any = None) -> bool:
"""Could a job of this *kind*/*config* have started a sibling Docker container?
Used to keep cancellation from touching Docker for the overwhelming majority of jobs that
never could have spawned one -- an unresponsive daemon then cannot delay their cancel.
Conservative on purpose: a ``pre`` job whose config does not mention any DWI stage flag at
all (an unrecognised or future shape) is treated as if it might have spawned one.
"""
if kind != "pre":
return False
if not isinstance(config, dict):
return True
present = [f for f in DOCKER_SIBLING_STAGE_FLAGS if f in config]
if not present:
return True
return any(bool(config[f]) for f in present)
|