mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
The guard fired on real-exec integration suites (the §D matrix): under
an sbin-ful PATH, run_privileged passes a bare name through — correct
behavior, since the same call absolutizes under the unit's sbin-less
PATH. The guard's model ("dies under the unit") cannot see that
resolution from inside a dev PATH. It now stays silent when the exec
came through app/utils/system.py — the one door it exists to route
traffic through — and still fires on a raw call one frame away. The
§D integration module's opt-out is removed; the guard stays active
there, and the capdrop matrix leg passes with it on.
114 lines
4.9 KiB
Python
114 lines
4.9 KiB
Python
"""Runtime layer of the sbin guard (plan 75 §B2).
|
|
|
|
The static scan in ``test_sbin_command_guard.py`` only sees literal argv lists —
|
|
164 of the 288 raw ``subprocess.*`` sites in ``backend/app``. The other 43%
|
|
(variables, f-strings, concatenation) are opaque to AST, and they are where a
|
|
bare-name exec of an sbin-resident binary can still hide.
|
|
|
|
This module closes that gap at runtime, during tests. ``GuardedPopen`` wraps
|
|
``subprocess.Popen`` — the one primitive ``run``/``call``/``check_output``/
|
|
``check_call`` all funnel through — and inspects the *actual* argv at call
|
|
time. A bare-name head that resolves under the current ``$PATH`` but NOT under
|
|
a ``$PATH`` with the sbin dirs removed fails loudly:
|
|
|
|
works in dev (PATH has sbin), dies under the panel's systemd unit
|
|
(PATH has none) — plan 74's outage class, caught before it ships.
|
|
|
|
Its coverage is exactly "whatever the suite exercises" — honest, measurable,
|
|
and improving for free as tests are added. A call the suite never makes is a
|
|
call no test can vouch for either way; this guard makes no claim about it.
|
|
|
|
Deliberate non-failures:
|
|
|
|
* a head that resolves under NEITHER PATH is simply absent on this machine.
|
|
The real ``Popen`` will raise ``FileNotFoundError`` — the honest result, and
|
|
one probe-honesty suites exercise deliberately. The guard does not turn a
|
|
missing-on-this-box binary into a guard violation.
|
|
* string-form argv (``shell=True``) and heads containing a path separator are
|
|
not PATH searches and are left alone.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import traceback
|
|
|
|
|
|
class SbinPathError(BaseException):
|
|
"""A bare-name exec that only resolves through an sbin dir.
|
|
|
|
``BaseException`` on purpose: the bug class this catches ships BECAUSE a
|
|
blanket ``except Exception`` converts ``FileNotFoundError`` into a false
|
|
fact ("No Firewall Installed"). A plain ``AssertionError`` raised here
|
|
would be swallowed by exactly the handlers it exists to catch.
|
|
"""
|
|
|
|
|
|
def sanitized_path(env=None):
|
|
"""The current ``$PATH`` with every sbin dir removed — the shape of the
|
|
panel unit's ``Environment="PATH=..."`` that caused the plan 74 outage."""
|
|
env = os.environ if env is None else env
|
|
parts = env.get('PATH', '').split(os.pathsep)
|
|
kept = [p for p in parts
|
|
if p and os.path.basename(p.rstrip('/\\')) not in ('sbin',)]
|
|
return os.pathsep.join(kept)
|
|
|
|
|
|
def offending_head(argv, env=None):
|
|
"""argv[0] if this argv is a bare-name exec that only resolves through an
|
|
sbin dir, else ``None``. Pure and env-injectable so it can be tested
|
|
without spawning anything."""
|
|
if os.name != 'posix' or isinstance(argv, (str, bytes)) or not argv:
|
|
return None
|
|
head = argv[0]
|
|
if not isinstance(head, str) or os.path.sep in head:
|
|
return None # absolute, or explicitly relative — not a PATH search
|
|
if shutil.which(head, path=sanitized_path(env)):
|
|
return None # resolves without sbin: equally safe under the unit
|
|
if not shutil.which(head):
|
|
return None # absent on this machine — FileNotFoundError is honest
|
|
return head
|
|
|
|
|
|
def _callsite():
|
|
"""First stack frame outside this module and subprocess internals."""
|
|
for frame in reversed(traceback.extract_stack()):
|
|
base = os.path.basename(frame.filename)
|
|
if base != 'popen_guard.py' and base != 'subprocess.py':
|
|
return f'{frame.filename}:{frame.lineno}'
|
|
return '<unknown>'
|
|
|
|
|
|
def _routed_through_helper():
|
|
"""True when the exec came through the one-door helper module.
|
|
|
|
``run_privileged``/``run_unprivileged`` absolutize argv[0] exactly when
|
|
$PATH cannot — under the panel unit's sbin-less PATH the very call this
|
|
guard is looking at resolves to an absolute path. Firing on their
|
|
pass-through in an sbin-ful environment (every real-exec integration
|
|
test) would condemn the door itself; the guard hunts *bypasses* of it.
|
|
"""
|
|
for frame in traceback.extract_stack():
|
|
if frame.filename.replace('\\', '/').endswith('app/utils/system.py'):
|
|
return True
|
|
return False
|
|
|
|
|
|
class GuardedPopen(subprocess.Popen):
|
|
"""``subprocess.Popen`` that refuses bare-name sbin-only execs (test-time)."""
|
|
|
|
def __init__(self, args, *a, **kw):
|
|
head = offending_head(args)
|
|
if head is not None and not _routed_through_helper():
|
|
raise SbinPathError(
|
|
f'{head!r} is exec\'d by bare name but only resolves through '
|
|
f'an sbin dir. The panel\'s systemd unit ships a PATH with no '
|
|
f'sbin, so this call raises FileNotFoundError in production '
|
|
f'while passing every dev run — plan 74\'s outage class.\n'
|
|
f' called from: {_callsite()}\n'
|
|
f'Use run_privileged() (or run_unprivileged() when no root is '
|
|
f'needed); both resolve argv[0] to an absolute path when '
|
|
f'$PATH cannot.'
|
|
)
|
|
super().__init__(args, *a, **kw)
|