Job kind -> runner command line (TODO.md §2.3, and the B1 assignment notes).
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; :data:TOOL_ARG_POLICY and :func:check_tool_args
close what it can be told to touch (RUN-06). Every path-shaped config.args value must
resolve inside the manager's project root, and the options a tool treats as identifiers
(--pipeline, --node) must be safe names -- 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 yet (its runner module has not been created — see the "
"backend change budget in TODO.md §3). This kind cannot run until that "
"lands."
)
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(module, args, project_dir)
return [PYTHON_INTERPRETER, "-m", module, *args]
raise KindError(f"unknown job kind: {kind!r}")
|
check_tool_args(module: str, 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.
Source code in tit/jobs/kinds.py
| def check_tool_args(module: str, 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.
"""
from tit.paths import is_valid_subject_id, is_within
policy = TOOL_ARG_POLICY.get(module, {})
def check(option: str | None, value: str) -> None:
rule = policy.get(option or "", "")
where = option or "argument"
if rule == "name":
if not _SAFE_ARG_NAME.match(value):
raise KindError(
f"tools job: {where} {value!r} must be a plain name "
f"(letters, digits, '_', '-', '.', at most 64 characters)"
)
return
if rule == "subjects":
for sid in value.split(","):
if sid and not is_valid_subject_id(sid):
raise KindError(
f"tools job: {where} has an invalid subject id {sid!r}"
)
return
if rule == "root":
if not project_dir or os.path.realpath(value) != os.path.realpath(
project_dir
):
raise KindError(
f"tools job: {where} must be this server's project directory"
)
return
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):
pending = None
option, sep, inline = arg.partition("=")
if sep:
check(option, inline)
elif option in policy or option.startswith("--"):
pending = option
continue
check(pending, 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)
|